الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

simple lexer

مغلق
بدأه hasan_aljudy في 12 نوفمبر 2005 · 7 رد · 2,923 مشاهدة · في هندسة البرمجيات
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

بسم الله الرحمن الرحيم

السلام عليكم و رحمة الله و بركاته.

احم .. ضمن محاولتي لعمل parser للغة D, كنت بدأت بمشروع لهذا الأمر, من مدة ليست بالبعدية تقريبا .. لكنني توقفت عن تطويره لسبب ما .. ربما لأنني احسست ان الوضع اصبح معقدا بعض الشيء, حيث قمت بعمل الـ lexer و مشي الحال,و لكن عندما جئت للـ parser دخت قليلا ثم انصرفت عن المشروع.

المهم الان, لا اريد ان يذهب جهدي هباءا, لذلك اريد عرض المشروع و شرحه بعض الشيء, لعله يفيد بعض الأشخاص.

بداية ما هو الـ lexer؟

في الحقيقة لا اعرف بالضبط! لكن التعريف الموجود في الـ documentation الخاصة باللغة اللتي أنا بصددها (لغة D) تقول:

lexical analysis
The source file is divided up into a sequence of tokens. Special tokens are replaced with other tokens. Special token sequences are processed and removed

يعني ناخذ ملف نصي, و اللذي هو بطبيعة الحال مكون من سلسلة حروف, و نقوم بتحويله الى سلسلة tokens.

هنا سؤال يطرح نفسه, ما هو الـ Token؟

في الحقيقة من الصعب علي الإجابة عن هذا السؤال بشكل محدد .. هناك تعريف في ويكيبيديا

http://en.wikipedia.org/wiki/Token_(parser)

يمكن القول انها وحدة البناء الأساسية في النص, مثلا

if ( someThing ) { doSomeThing(); }

مكون من عدة tokens, و هي if يليها القوس ( يليها كلمة someThing .. الخ.

يمكننا اعتبار ان كل "توكن" مكونة من نص و لها نوع (او اسم او وصف ..)

فإذا وضعناها في قائمة ووضعنا وصفا للـ token, فإن الـ tokens الموجودة في الكود السابق هي:

If                             IF
(                              OPEN_PARENTHESIS
someThing                 IDENTIFIER
)                              CLOSE_PARENTHESIS
{                              OPEN_CURLY_BRACE
doSomeThing             IDENTIFIER
(                               OPEN_PARENTHESIS
)                               CLOSE_PARENTHESIS
;                                SEMICOLON
}                                CLOSE_CURLY_BRACE

طبعا نحن لسنا ملزمين بهذه التسميات, و لكن هذا مجرد مثال.

تعريف انواع الـ tokens يعتمد على تعريف اللغة نفسها.

الهدف من الـ lexer هو اخذ النص و تحليله الى سلسلة tokens, و هذا هو كل ما اقوم بفعله في البرنامج اللذي أتحدث عنه.

سأقوم إن شاء الله بإرفاق البرنامج, و هذا مثال على تحويل نص الى سلسلة Tokens, حيث يقوم بعرض كل token في سطر لوحدها (ليثبت انه يفهم النص و يستطيع تحويله الى tokens)

المدخلات:

import lex.module_file;
import lex.lexer;

import std.stdio;

void main()
{
    Module m = new Module("test_bed.d");
    //m.printContent();
    TokenizedModule tm = lexical( m );
    tm.dumpTokens();
}

المخرجات:

import
lex
.
module_file
;
import
lex
.
lexer
;
import
std
.
stdio
;
void
main
(
)
{
Module
m
=
new
Module
(
"test_bed.d"
)
;
TokenizedModule
tm
=
lexical
(
m
)
;
tm
.
dumpTokens
(
)
;
}

سأقوم إن شاء الله في المشاركات القادمة بشرح مبسط للكود مع توضيح لبعض الأفكار.

ملاحظة: إذا اردت ترجمة الكود, تحتاج الى dmd compiler إضافة الى build tool

http://www.digitalmars.com/d/dcompiler.html

http://www.dsource.org/projects/build/

بالنسبة للـ build, فاسم الملف يحتوي على رقم الاصدارة, انا شخصيا احذف رقم الاصدارة و اجعل اسم الملف build.exe فقط,و أضعه في الملف ….\dmd\bin و طبعا يجب اضافته الى الـ PATH!! لن اقوم بشرح هذه الأمور هنا, فإذا لم تكن تعرف هذه الأساسيات فليس من المفروض ان تخوض في الموضوع اللذي اتكلم فيه!

d-analyzer.zip

تم تعديل هذه المشاركة بواسطة hasan_aljudy في 12 نوفمبر 2005 في 12:06

#2

جميل جدا

اذكر أيام مادة "لغات البرمجة"

كان عندنا مشروع تصميم Lecxer and parser

لنكون في النهاية مترجم صغير كتكوت

أخذ منا حوالي 3000 سطر أقل أو أكثر

بس بعد انتهائه كنا جدا فخورين

لذا انصحك انك تمكل في مشروعك

وحاتكسب خبرة جيدة منها...

خاصة عندما نتكلم عن المتغيرات وحجز اماكن لها في الذاكرة

و أنواع الـAccess والنطاق Scope...

ونوع إرسال الباراميتر...

أيام حلوة...

لماذا تكون الليغو اللعبة الأكثر عبقرية في العالم؟

لأنها غير قابلة للتجزئة ، وتختلف فيما بينها بالألوان و الأشكال ، وتمتلك القدرة على تكوين علاقات مع بعض. نستطيع أن نقول أن أجزاء الليغو أبدية. وهي تشبه الذرات في تراكيبها للكون

-----

وائل بن أحمد كابلي

مستشار تطوير برمجيات | مايكروسوفت للخدمات الاستشارية

MCSE | MCTS SharePoint Infrastructure | MCTS SharePoint - Development | MCP | MSF Essentials

http://blogs.msdn.com/wael

@waelkabli

#3

أولا, كيف نمثل الـ Token داخل البرنامج؟

في الحقيقة كيفية تمثيل البيانات هو أمر من المفترض انه يعود اولا و اخيرا الى المبرمج, بعد ان ينظر الى متطلبات البرنامج عليه ان يختار طريقة مناسبة لتمثيل (او تشفير) البيانات.

بالنسبة لي, قمت فقط بتمثيل ثلاثة أشياء: نوع الـ token, النص اللذي يمثل الـ token, و موقع الـ token في الملف الأصلي.

بالنسبة لنوع التوكن, فالأنواع وضعتها في enum اسمه TOK (و الاسم اشتققته من الـ lexer الحقيقي الموجود في الكومبايلر dmd).

اما موقع التوكن في النص الأصلي, فقمت بتخزين الـ string اللذي يمثل النص و هو في الحقيقة من نوع char[] و قمت ايضا بتخزين موقع اول و آخر حرف للتوكن.

التمثل موجود على شكل كلاس اسمه Token في الملف lex\token.d

class Token
{
private:
        TOK type;
        TokenInfo info;        
public:
        this( int start, int end, char[] txt, TOK type )
        {                
                this.info = new TokenInfo( start, end, txt );
                this.type = type;
        }
        
        TOK getType()
        {
                return type;
        }
        
        TokenInfo getTokenInfo()
        {
                return info;
        }
}

class TokenInfo
{
public:
        int start;
        int end;
        char[] txt;
public:
        this( int start, int end, char[] txt )
        {
                this.start = start;
                this.end = end;
                this.txt = txt;
        }
}

طريقة العمل:

من أجل تقسيم النص الى مجموعة "توكنات", نقوم بعملية مسح للنص, و تسجيل التوكنات كلما شاهدناها.

تحدثت عن هذا الأمر في موضوع سابق:

/index.php?showtopic=78617

الطريقة اللتي اعرفها (و اللتي اعتقد انها مستخدمة في الـ compilers) هي قرائة النص حرفا حرفا, و تحديد ماذا يمثل كل حرف منها, طبعا نفترض ان النص صحيح و يتبع قواعد اللغة, و على هذه الفرضية, فإن النص بكامله عبارة عن tokens متتابعة وراء بعضها, فإذا بدأنا في قراءة الملف, فيجب ان يكون اول حرف هو بداية token, و علينا ان نقوم بقراءة الحروف التالية حتى ننتهي من هذا التوكن. بعد ذلك سنكون قد وصلنا الى بداية التوكن القادمة, فنقوم بقرائتها, الخ. و هكذا حتى ننتهي من قرائة الملف بكامله. و إذا صادفنا حالة غير معروفة, يعني حاولنا قراءة التوكن و لكننا لم نفهمها, فسنقول ان هناك خطأ في كتابة الكود.

فمثلا, يمكن القول انك إذا وجدت حرفا ابجديا, فهذا يعني انك في بداية كلمة, و إذا وجدت رقما, فهذا يعني انك في بداية رقم .. و إذا وجدت مسافة, فهذا يعني انك في بداية مساحة بيضاء او whitespace.

فإذا وجدت نفسك في بداية كلمة, فإنك تدخل نفسك في حلقة متواصلة لقراءة كل حروف الكلمة, و عندما تجد ان الكلمة قد انتهت, تقوم بالخروج من هذه الحلفة و العودة الى قرائة حروف النص المراد تحليله.

و كمثال على هذه الحلقة (و هذا المثال اقرب الى psedu-code منه الى كود حقيقي)

while( !doneProcessingText() )
{
   char c = peekNextChar();    

   if( isWordStart( c ) )
   {
       readNextWord();
   }
   else if( isNumberStart( c ) )
   {
       readNextNumber();
   }
   else if( isWhiteSpace( c ) )
   {
       readWhiteSpace();
   }
   else if( isSymbol( c ) )
   {
       readSymbol();
   }
   else
   {
       error("unknown token!");
   }
}

هنا يجب ان يكون لدينا المام بقواعد اللغة, او في الحقيقة يجب ان يكون لدينا مرجع reference لقواعد اللغة, و يجب ان يكون مرجع موثوق و معترف به, و الأفضل طبعا ان يكون official documentation او شي من هذا القبيل. بالنسبة لي, المرجع هو http://www.digitalmars.com/d/lex.html

يجب ان نعرف جميع انواع الـ Tokens الموجودة, و ما هو اللذي يحدد بداية التوكن, و ما هي الأجزاء التابعة للتوكن.

لو كان لدينا النص:

var + 10

فسنحلله الى عدة توكنات كالتالي:

اولا نقرأ حرف v, نعرف ان هذا حرف, و الحروف دائما هي بداية الكلمات, او الـ identifiers, فنقوم بالدخول في دورة لقراءة الحروف التالية, و طالما انها جزء صحيح من التوكن, سنعتبرها تابعة لنفس التوكن, الى أن نصل الى حرف لا يعد جزءا مقبولا من التوكن, فنقوم بالتوقف عن قراءة هذه التوكن, و نعتبر انفسنا قد انتهينا منها, و نتجه الى الـ Token القادمة.

في هذا المثال, سنقوم بقراءة الحرف a ثم الحرف r و كلاهما جزء من الـ identifier, ثم نقوم بعدها بقراءة مسافة, و ندرك انها ليست جزءا من الـ identifier, فنتوقف عن القراءة و نقول ان التوكن الحالية هي var و هي من نوع identifier.

نحن الان واقفون عند مسافة, و هي بداية توكن من نوع white space, فندخل الان في دورة لقراءة الـ whitespaces و وضعها في توكن جديد.

و لكن لاحظ ان المسافات البيضاء لا معنى لها في اللغة! لذلك يمكننا بعد الانتهاء من قراءة توكن المسافة, يمكننا ان نهمله و نعتبره و كأنه غير موجود, و نستمر في قراءة التوكنات القادمة.

الخ.

طبعا سنرى بعد ذلك الحرف + و هنا علينا التأكد من ان هذا هو حد هذا التوكن, و هو في هذه الحالة operator, او هكذا نستطيع ان نسميه. طبعا تحليله قد يكون معقدا بعض الشيء, لان علينا التأكد من أنه لوحده, و انه ليس بداية توكن آخر مثل ++ او += و هذه العملية قد تبدو معقدة, و لكن لغة D لها من الخصائص ما يمكننا من جعل الأمر سهلا للغاية!! سأتحدث عن هذا حين يأتي وقته إن شاء الله.

عموما, في الكود المثال اعلاه, شكل الـ funcion المستخدم لقراءة الكلمات, readNextWord,و هو تخيلي بالكامل في هذه الحالة, شكله قد يكون هكذا:

void readNextWord()
{
   char c = peekNextChar();
   while( isAlphabeticalLetter( c ) || isDigit( c ) )
   {
       readNextChar();
       
       c = peekNextChar();
   }    
}

حيث نقوم بقراءة الحروف التالية, و ما دامت هذه الحروف جزءا من التوكن, نقوم بقرائتها.

طبعا مجرد المرور الى حروف التوكن لا يكفي, لا بد من تخزين هذا التوكن بطريقة ما, فبعد الجهد اللذي بذلناه في التعرف عليها, لا نريد ان تضيع جهودنا هباء!

و كفكرة عامة, يمكن تعديل الـ function بحيث يقوم بتسجيل بداية و نهاية النص,و يقوم بإنشاء كائن Token جديد, و يضيفه الى قائمة الـ Tokens

void readNextWord()
{
   int startIndex = getCurrentIndex();
   
   char c = peekNextChar();
   while( isAlphabeticalLetter( c ) || isDigit( c ) )
   {
       readNextChar();
       
       c = peekNextChar();
   }    
   
   int endIndex = getCurrentIndex();
   
   Token word = new Token( WORD_TOKEN, startIndex, endIndex, getOriginalText() );
   addToTokenList( word );
}

باختصار, لمعرفة اي توكن, نقوم بعملية منطقية بسيطة:

if( isTokenStart( c ) )
       readToken();

و لقراءة اي توكن, ندخل في لووب شكلها هكذا:

while( isTokenPart( c ) )
{
        c = readNextChar();
}

و يمكن دمجهما بهذا الشكل:

if( isTokenStart( c ) )
{
        while( isTokenPart( c ) )
        {
                c = readNextChar();
        }
}

و الـ pseudo code العامة يمكن التعبير عنها بهذا الشكل:

if we detect the start of a certain token, start reading it like this:

    record current cursor postion as the starting position

    while current character is part of the token
        read next character

    record current cursor position as the ending position

    create a token between the start and end positions.

    add this token to our token list

هذا كلام كله نظري تقريبا, و لكن هناك تطبيق عملي في البرنامج المرفق. سأقوم إن شاء الله بشرح ما يقوم به البرنامج في "الحلقة" القادمة.

#4

بالمناسبة يمكن تحميل نسخة جاهزة من اخر اصدارة من الكومبايلر dmd و هي 0.139 مع اداة build من هنا:

http://box.net/public/aljudy/dfiles/stuff/...-with-build.rar

و لفتح ملفات الـ d افضل استعمال برنامج dcode و اللذي يمكن الحصول عليه من هنا:

http://www.dprogramming.com/dcode.php

نأتي الى شرح المشروع:

المشروع مقسم مبدئيا الى عدة اقسام, lex و parse و util و هذه هي اسماء المجلدات الظاهرة في المشروع.

الملف الرئيسي هو main.d, اما test_bed.d فهو ليس جزء من المشروع, بل هو ملف نجري عليه الاختبارات, يعني نقوم بتحليله باستخدام مشروعنا لنرى النتائج و نقيس مدى نجاح المشروع :)

المجلد parse يحتوي على بعض المحاولات الأولية في كتابة الـ parser و هو جزء لم انتهي منه بعد, بل يا دوبني بدأت للتو, فهذا لن نتكلم عنه الان.

المجلد util هو كما يقول اسمه, يحتوي على utility classes and functions, حاليا وضعت فيه كلاس Array و هو في الحقيقة template, و وضعت فيها ايضا functions لكتابة رسائل في الـ debugging mode.

المجلد lex هو ما يهمنا حاليا, فهو اللذي يحتوي على الـ lexer.

عملية الـ lexical analysis او الـ lexing (اختصارا) تبدأ بقراءة الملف النصي ثم المرور عليه لتحديد الـ Tokens الموجودة فيه.

الملفات في لغة الـ D تعتبر modules, لذلك قمت بالتعبير عن الملف بكلاس Module, و بعد ان تتم عملية الـ lexing اقوم بتخزين النتيجة في TokenizedModule للتعبير عن module قمنا بعملية lexing عليه.

الـ Module يحتوي فقط على النص الموجود في الملف, بينما الـ TokenizedModule يحتوي على reference للـ Module الأصلي, إضافة الى array من الـ tokens تمثل التوكنات الموجودة في النص مرتبة بشكل متسلسل.

كلاس الـ Token تحدثت عنه في المرة السابقة, و لكن ما لم أتحدث عنه هو الـ enum اللذي يمثل نوع التوكن, و هو TOK الموجود في lex\token_enum.d حيث يمثل الأنواع المفترضة للتوكنز. معظم العناصر في القائمة هنا مأخوذ من كود الـ front end للـ dmd و هو مفتوح المصدر (الـ front end فقط), مع بعض التغييرات اللتي قمت بها, لتسهيل المهمة علي قليلا!

عملية تحويل النص من سلسلة حروف الى سلسلة توكنز تتطلب الية لمسح النص و المشي عليه حرفا حرفا مع طريقة للاحتفاظ بالـ index للحرف اللذي نقوم حاليا بمسحه.

من الممكن استخدام مؤشر char * للقيام بهذا, و لكن هذا يعقد العملية كثيرا, لأننا غالبا ما نريد القيام بأمور معقدة مثل قراءة عدة حروف قادمة ثم التراجع الى الخلف اذا اكتشفنا ان تلك الحروف لم تكن كما نظن .. الخ. و بما أننا سنستخدم هذه الخاصية كثيرا, (بل هي الحجر الأساس في العملية) فقد قمت بعمل كلاس اسمه TextScanner وظيفته ان يقوم بعملية المسح, حيث اهم وظيفة له هي قراءة الحرف القادم إما عن طريق read او عن طريق peek و الفرق بينها هو أن peek لا تقوم بتحريك المؤشر, بينما read يقوم بتحريك المؤشر على النص.

يمكن ايضا اضافة خواص أخرى مثل unread و ستكون وظيفتها الرجوع الى الوراء, و لكنني لم اطبقها هنا لأني لم أحتج اليها.

في الحقيقة فكرة هذا الكلاس اخذتها من eclipse و هو الـ IDE الشهير (بين مبرمجي الجافا على الأقل) المفتوح المصدر. المشكلة الوحيدة ان الـ interface لهذا الكلاس في eclipse ضعيفة و بدائية جدا, فهي لا تحتوي سوى على read و unread حتى انه لا يمكن للشخص متابعة المكان الحالي للمؤشر cursor و لذلك فإن الأكواد التي تستخدمه معقدة جدا .. لدرجة ان استخدام مؤشرات char * قد يكون افضل منها!!!

قمت في حينها بعملية تغليف لهذا الكلاس بإضافة امور مثل peek و read( int length ) لقرائة عدة حروف مرة واحدة .. الخ. و قمت بعد ذلك بنقل الفكرة و تطبيقها في هذا المشروع.

عملية الـ lexing تتم في الـ function

public TokenizedModule lexical( Module m )

حيث يأخذ كـ input ملفا على شكل Module و يقوم بإخراج TokenizedModule, اي نفس الملف بعد ان قمنا بعملية lexing و تخزين قائمة بالـ tokens.

الكود موجود في lex\lexer.d و الشكل العام هو كما وضحنا في الحلقة السابقة, مع استثناء بسيط, و هو اننا الان في كود حقيقي (جزء من مشروع) لذلك هناك بعض التفاصيل الإضافية.

نقوم أولا بإنشاء TextScanner على الملف, و نستخدمه من اجل المرور على جميع الحروف الموجودة في النص. ثم نقوم بإنشاء Array من الـ Tokens لنقوم فيه بتخزين ما سنجده من الـ tokens في ذلك الملف.

لاحظوا الـ syntax قد يبدو غريبا للبعض:

Array!(Token)

وضع علامة تعجيب هنا تعني اننا نريد انشاء نسخة من هذه الـ template على النوع Token, و هي مماثلة للتعبير

Array<Token>

في السي بلص بلص و الجافا (و على ما اعتقد السي شارب ايضا).

بعدها نقوم بالدخول في حلقة, شرط انتهائها هو الوصول الى نهاية النص, و طبعا الـ scanner هو ما نستخدمه للمرور على النص, لذلك فإننا نصل الى نهاية النص عندما يكون الـ TextScanner sc قد وصل الى النهاية.

داخل الحلقة, نقوم بالنظر الى الـ token القادمة (اللتي سنقرأها الآن .. يمكن البعض يفضل اعتبارها الحالية و ليست القادمة), المهم, إذا كانت هذه التوكن عبارة عن تعليق او مساحة بيضاء, نقوم بقرائتها و إهمالها (لا نظيفها الى قائمة التوكنز), و إذا لم تكن كذلك فإننا نقوم بقرائتها و إضافتها الى قائمة التوكنز.

        /*
            scan the next token and add it to our token list
        */
        int start = sc.getCursorPosition();
        TOK t = nextToken( sc );
        int end = sc.getCursorPosition();
        assert( end > start );
        char[] txt = sc.getSlice(start, end);
        tokens.add( new Token( start, end, txt, t ) )

حيث الـ sc هو الـ scanner و اسميته sc اختصارا.

اعتقد انه لا داعي لشرح هذه الأسطر, فهي تطبيق مباشر لما تحدثت عنه في المرة السابقة.

بعد الانتهاء من قرائة جميع الـ tokens, أقوم بأخذ الملف الأصلي Module m و اخذ قائمة التوكنز Array!(Token) tokens و اقوم باستخدامهما لإنشاء كائن جديد هو عبارة عن دمج لهذين الشيئين, و هذ الكائن هو TokenizedModule و أقوم بإرجاعه من الـ function

    return new TokenizedModule( m, tokens );

هذه هي هيكلة قلب البرنامج, و طبعا هي تعتمد اعتمادا كليا على nextToken لأنها تحدد كم سيتحرك مؤشر القراءة, و ما هو نوع الـ token اللذي قرأناه!

تعريف الـ function هو كالتالي:

private TOK nextToken( TextScanner sc )

حيث يأخذ TextScanner و يستخدمه لقراءة اول حرف, ثم تحديد نوع الـ Token و قراءة باقي الحروف المكونة لها (اي للـ token), ثم بعد ذلك يقوم بإرجاع نوع الـ token اللذي تم قرائته.

بشكل عام, يمكن تقسيم جميع انواع التوكنات الى اربعة انواع:

إما strings او numbers او identifiers او operators

الـ strings هي ما كان من قبيل "hello there" او x"ab d0 91 e3" او 'x' ... الخ

اما الأرقام فهي كل ما كان رقما, سواءا كان صحيحا او عشريا او تخليليا ..

اما الـ identifiers فهي كل "الكلمات" بما فيها الـ keywords حيث ان الـ keyword هو حالة خاصة من الـ identifier, و تحديدا هو identifier محجوز مسبقا.

اما الـ operators فهي جميع الرموز الرياضية و غيرها, مثل + او = او >> او % و ما الى ذلك.

لتحديد نوع التوكن الحالية, نحتاج لمعرفة ما هو أول حرف نقوم بقارئته حاليا, و لكن هذا ليس كافيا, ففي بعض الأحيان نحتاج لمعرفة اول حرفين, لماذا؟ لانه في لغة الـ D جميع الـ identifiers يبدأو بحروف, و لكن هناك انواع اخرى من التوكنز تبدأ بحروف, فبعض انواع الـ string ايضا يبدأ بحرف, و تحديدا كمثال على ذلك:

r" this is a string token"

كما تشاهدون فإنها تبدأ بـ r" لذلك فمجرد معرفة ان اول حرف هو r لا يعني شيئا, بل يجب معرفة ما هو ثاني حرف, هل هو " ام لا؟ بعدها فقط نستطيع تحديد نوع الـ token.

لذلك نقوم بتعريف متغيرين, الأول يمثل الحرف الأول من النص, و الثاني يمثل اول حرفين من النص:

    char c = sc.peek();
    char[] cc = sc.peek(2);

و لاحظوا اننا نستخدم peek.

لغة الـ D فيها أمر جميل جدا, و هو السماح بـ strings في الـ switch statement, لذلك يمكننا القيام بشيء من قبيل:

    switch( cc )    //it's EXTREMELY important that this test comes before the test for identifiers
    {
        case `r"`: return scanWysiwygString( sc );
        case `x"`: return scanHexString( sc );
        default: break;                
    }

و هكذا نعلم انه اذا لم تكن cc تساوي r" فإن c (و اللتي هي حرف r) ستكون بداية identifier token.

لاحظوا اننا عندما نكتشف نوع التوكن, فإننا نقوم فورا باستدعاء الـ function المختص بقراءة هذا النوع من الـ tokens و الخروج من الـ functiopn, مثل:

    if( isNumberStart( c ) )
    {
        return scanNumber( sc );
    }

لاحظوا الـ return, حيث نريد الخروج من هذا الـ function اول ما نقرأ التوكن!

طبعا بإمكانكم الاطلاع على كود الـ function, و هو ليس معقدا كثيرا جدا, و يا ريت تقومون بفعل هذا الآن!

.

.

.

ها, قرأتوه؟

طبعا كما قد تلاحظون, فإننا بعد محاولة اكتشاف جميع انواع الـ token المعروفة لدينا, نصل الى آخر سطر في الـ function حيث نقوم بحذف خطأ! (أقصد throw an exception!! آسف على الترجمة التعبانة!) على اعتبار اننا اذا لم نعرف نوع التوكن, فمعنى هذا ان هناك خلل في النص! (طبعا احتمال ان يكون الخطأ منا نحن! يعني من البرنامج! و لكننا نتعامل كأن برنامجنا يعمل بشكل سليم و صحيح).

كما ذكرت سابقا, المرجع اللذي نستخدمه لتحديد انواع التوكنز هو

http://www.digitalmars.com/d/lex.html

تم تعديل هذه المشاركة بواسطة hasan_aljudy في 17 نوفمبر 2005 في 23:00

#5

اخي hasan_aljudy شكرا لك فعلاُ هذه المواضيع تهمنى جداُ

وأنا معاك متابع الموضوع اتمنا ان تستمر لعلي اكمل تحليل مشروعي في القريب وأنا حاليا في هذه النقاط بالذات في بناء شجرة Parser .

#6

أفترض ان قراء هذا المقال هم مبرمجين من مستوى دون المتوسط فما فوق, و افترض انهم يستطيعون قراءة الكود و فهمه بشكل لا بأس به, لذلك لا اريد صرف هذا المقال الى شرح كل سطر في الكود.

كما اني ارجو ان يقوم القارئ اولا بالاطلاع على الكود, لأنني اعتقد انه من الصعب ان يستطيع المرء فهم هذا المقال إذا لم يكن قد اطلع على الكود.

ما اريد التحدث عنه هو بعض التقنيات المستخدمة لقراءة الـ tokens, بعض الـ tokens مثل الـ identifiers قرائتها بسيطة, مجرد التهام كافة الأحرف و الـ underscores و الأرقام!

بعض الـ tokens تتطلب الية معقدة لقرائتها لان هناك العديد من الاحتمالات, مثل الأرقام!

بشكل عام توكن الرقم يبدا بـ digit, و يمكن ان يحتوي على اي عدد من الأرقام digits, يمكن ايضا أن يحتوي على underscores, يعني 8_234_323 لتسهيل قراءة الأرقام الكبيرة!! كما يمكن ان يحتوي طبعا على نقطة . لتحديد الأرقام العشرية, و يمكن ان يحتوي على suffix عبارة عن حرف او اثنين لتحديد نوع الرقم. مثل 12_345.34F او 123Lu و هنا يأتي التعقيد.

طبعا انا كسول ولا اريد التعامل مع هذا التعقيد, و قلت لنفسي ان الـ lexer اللذي اريده لا يهتم كثيرا بأنواع المتغيرات اللتي تمثل الـ tokens, بل يهمه فقط تحديد الـ token و نوعها (ليس نوعها كمتغير, و لكن نوعها كتوكن). لذلك قمت بكتابة الـ numberScanner بحيث يقبل ان يحتوي الرقم على حروف و ارقام و _ و . بأي عدد, يعني يمكن ان نقبل رقم 12.45.3 و هو طبعا غلط! و لكني اتساهل في هذه الناحية! الاستثناء الوحيد هو لو وجدنا .. نقطتين متتابعتين, فعندها اوقف المسح لأن النقطتين المتتابعتين هي توكن لوحدها!

هناك سؤال وجيه, و هو لماذا اتعامل مع الأرقام بهذا التساهل و أقبل tokens غلط؟

الاجابة متعلقة بهدف الـ lexer, فأنا لا ازال بعيدا عن كتابة compiler كامل, لذلك الدقة التامة غير مطلوبة, و إنما أنا افكر في استخدام الـ lexer لبناء أداة تساعد في كتابة محرر للغة مع intellisense و أشياء من هذا القبيل!

السبب الثاني هو انني احاول تبسيط العملية, فالتأكد من جميع القواعد قد يكون معقدا بعض الشيء, مع العلم انني لم اجرب, و ربما يكون اسهل مما اتصور.

طبعا, يمكن بسهولة جدا ان نقوم بعمل pass أخرى على الـ token list و نحدد انواع الأرقام, يعني كلما نشوف number token نقوم بتحليلها لتحديد نوعها و اكتشاف الأخطاء إن وجدت!

ذكرت قبل قليل ان قراءة الـ identifiers هي عملية بسيطة و مباشرة, مجرد قراءة حروف الكلمة! و لكن ما لم أذكره هو اننا يجب ان ننتبه الى أن هذا الـ identifier قد يكون عبارة عن keyword, و في هذه الحالة يجب ان تكون القيمة اللتي نرجعها هي نوع الـ keyword و ليس مجرد TOKidentifier.

و لمعرفة ما إذا كان الـ identifier اللذي قرأناه عبارة عن keyword أم لا, نقوم بتخزين الـ identifier في string أثناء قرائتنا له, ثم نقوم بعد ذلك بالتأكد منه:

public TOK scanIdentifier( TextScanner sc )
{
    char[] ident = "";    
    while( isIdentifierPart( sc.peek() ) )
    {
        ident ~= sc.read();
    }    
    
    if( isKeyword( ident ) )
    {
        return getTokenOfKeyword( ident );
    }    
    else
    {
        return TOK.TOKidentifier;
    }        
}

بالنسبة لـgetTokenOfKeyword , فهي تقوم بتحديد نوع الـ Token المفروض ارجاعه من هذه الكلمة عن طريق hashtable او associative array كما يسمى في لغة الـ D.

معظم انواع الـ Token يمكن تعريفها على شكل RegularExpression, لمن يعلم معنى المصطلح.

اما من لم لا يعلم معناه فلا داعي ان يتعب نفسه, لأني قمت بكتابة الـ lexer قبل ان ادرس الـ RegularExpression, و بعد ان درسته لم اعلم ما الفائدة من دراسته ..

أظن انني لو كنت قد درسته قبل كتابة الـ lexer لكنت دخت في متاهات الـ DFA و الـ transitions دون إنجاز شيء يذكر :lol:

المهم, هناك استثناء واحد, و هو الـ nested comment, حيث انها ليست معرفة على شكل regular expression.

/+

    This is a comment .. 
    I can open a comment inside this comment, and close it without closing this parent comment.
    /+
       this is a comment inside a comment
   +/
   This line is still part of the comment!
+/

لقراءة هذا التعليق كـ token يجب علينا عد الـ /+ و الـ +/ و هي مسألة ليست معقدة إطلاقا.

أنا شخصيا استخدمت الـ recursion في القراءة, فكلما رأيت بداية تعليق داخلي اقوم بالدخول في نفس الـ function لقرائته, و هكذا لا أقوم بعد الـ /+ و الـ +/, و إنما استخدم الـ stack من أجل مطابقة كل /+ مع نظيرتها +/

الكود موجود في lex\whitespace_scanner.d

void skipNestingComment( TextScanner sc )
{
    assert( sc.peek(2) == "/+" );
    sc.read(2);
    
    while( sc.peek(2) != "+/" )
    {
        if( sc.peek(2) == "/+" )
        {
            skipNestingComment( sc );
        }
        else
        {
            sc.read();
        }
    }
    
    assert( sc.peek(2) == "+/" );
    sc.read(2);
}

لاحظوا ان اسم الـ function هو skip و ليس scan و السبب في ذلك انني في هذا الـ lexer أقوم بإهمال الـ comments و الـ whitespace, يعني بعد قرائة الـ token لا أقوم بإضافتها الى اية tokenlist! حتى ان هذا الـ function لا يقوم بإعادة نوع التوكن.

آخر ما أريد التحدث عنه هو طريقة قرائة الرموز و الاشارات operators.

مشكلة الـ operators انك لو وجدت > مثلا, لا تستطيع تحويلها الى توكن "أكبر من", الا بعد ان تتأكد انها ليست بداية توكن أخرى, مثل >= "أكبر من او يساوي".

The source text is split into tokens using the maximal munch technique, i.e., the lexical analyzer tries to make the longest token it can. For example >> is a right shift token, not two greater than tokens.

لحل هذه المشكلة, قمت بعمل جدول من associative array يحتوي على جميع الـ operators مع نوع الـ token اللذي يمثله كل منها, بالضبط مثل ما فعلت مع الـ keywords.

نلاحظ ان اكبر طول ممكن للـ operator token هو 4 حروف, لأن هذا هو طول اطول operator.

عندما يجد الـ lexer نفسه يتعامل مع operator token, فإنه يقوم بقراءة اربعة حروف من مكان المؤشر الحالي(بدون تحريك المؤشر, عن طريق peek), و يحاول البحث عنها في الـ operator table او جدول الإشارات, فإن لم يجدها فسيقوم بقراءة ثلاثة حروف, و هكذا يكرر نفس العملية, و متى ما وجد الطول المناسب, فسيقوم بتحريك المؤشر بقدر هذا الطول, و يقوم الـ function بإرجاع نوع التوكن عن طريق البحث عنها في الـ hash table.

الكود موجود في lex\operator_scanner.d

    for( int i = 4; i >= 1; i-- )
    {
        char[] op = sc.peek(i);
        if( isKnownSymbol(op) )
        {
            sc.read(i);
            return getTokenOfSymbol( op );
        }
    }

لاحظوا ان الـ lexer يعتمد بشكل كبير على الكلاس TextScanner, و من دونه كنت سأستخدم مؤشرات char * طوال الوقت, و لكانت قراءة الكود اصعب مما هي عليه الان بعشرات المرات!! في الحقيقة كنت قد كتبت قبل ايام كود صغير بلغة السي يقوم بقراءة ارقام (مفصولة بمسافات و فواصل) من ملف, و استخدمت المؤشرات مباشرة char * و اكتشفت فيما بعد وجود bug تخربط الشغلة بالكامل و لكني لم اكن منتبه لها! و في الحقيقة لا زلت اعتقد ان ذلك الكود يحتوي على bug, مع انه لا يتجاوز 20 سطر في الـ C!!!!

هناك إشكال صغير في طريقتي المتبعة في الـ D, و هي عند ارسال و استلام كائن TextScanner بين الـ functions, حيث هناك دائما شك, هل قام الـ function السابق بعمل read ام peek؟ و هل المؤشر الان هو على الحرف المطلوب ام بعده؟

مثلا, هناك function يقوم بعمل scan و يكتشف وجود الرمز //, فيقوم باستدعاء الـ function المناسب لقراءة توكن التعليق, comment token. لنفرض ان الـ function اللذي يقوم بقراءة التعليق اسمه commentScanner, و انه يستلم بارامتر sc من نوع TextScanner, السؤال الآن, هل المؤشر واقف على // ام ان الـ function السابق قام بقرائته نيابة عنا؟

يمكننا وضع بروتوكول معين (او طريقة معينة) لهذه الأشياء, و لكن احتمال الخطأ يظل واردا .. ماذا نفعل؟

في هذه الحالة نستخدم بعض خواص اللغة D, و هنا نستخدم خاصية دعم اللغة المباشر لمبدأ design by contracts.

حيث يوجد assert مبني في اللغة نفسها, و نستخدمه للتأكد من هذه الأشياء, لذلك ترون دائما ان الـ functions اللتي تقوم بقراءة التوكنز تبدأ بأشياء من قبيل:

	assert( sc.peek(2) == "/*" );
	sc.read(2); //read the "/*"

لتفادي هذه المشاكل.

هذا كل ما لدي لشرحه حول المشروع, و هو بسيط للغاية و لا يستحق كل هذا التضخيم! (حجم الشرح في الـ Word وصل الى 13 صحفة! :lol: لا ادري على ماذا!)

الخطوة الأهم هي عملية الـ parsing, و بصراحة لم أصل الى الشيء الكثير فيها, و لكني لدي فكرة مشروع بسيط قد يساعدني في العملية. باختصار افكر في عمل parser للغة بسيطة جدا .. من اختراعي يعني .. او في الحقيقة هي ليست من اختراعي تماما.

عموما لندعه لوقته!

#7

في الحقيقة انا مستغرب ان هناك اكثر من 100 قرائة للموضوع و مع ذلك لم يحمل الملف سوى 4 اشخاص!! لحظة لحظة .. لا تظنوا انني ابحث عن كلمة مشكوور! فقط اتعجب من نسبة قرائة الموضوع الى نسبة تحميل الملف! الظاهر انه لم يقرأ الموضوع سوى 10 اشخاص, و انه ربما قام بعض الأشخاص بزيارة الموضوع عشرات المرات, بشكل جعل عداد الزوار يظهر هذا الرقم!

على كل حال, قمت بعمل فلاش صغير, يقوم بعرض مثال على كيفية قرائة نص عادي و تحويله الى tokens.

الفلاش قصير جدا, و قد أخذ من وقتي أكثر من ست ساعات!!!

لا ادري لماذا يعجبني اضاعة وقتي بهذا الشكل, على كل حال, ارجو ان يساهم هذا الفلاش في توضيح بعض الأشياء :lol:

lexer_demo.zip

#8

اخي hasan_aljudy انا الخامس الي نزل الملف :P

أنا معاك

لكني لا اركز كثير على الكودات في C لذلك اقراء الكود المكتوب هنا وأفهمة لأتعامل مع لغة اخرى بنفس المنطق .

أرجو ان تستمر لكن كسل الإعضاء من التعليق الذي قد يسبب احباط عند كاتب الموضوع لكن اخي hasan_aljudy سيكون مثل هذه المواضيع بصمة لك في هذا المنتدى . وهي مرجع في المستقبل لمن بحث في هذا المنتدى .

في انتضار البقية . :rolleyes:

سوف اقراء درسك اليوم .

تم تعديل هذه المشاركة بواسطة فواز الشمري في 20 نوفمبر 2005 في 10:50

هذا الموضوع مغلق.

مواضيع مشابهة