مرحبا بالجميع! كمبرمج ، أبحث دائمًا عن طرق لتحسين مهاراتي. ليلة الجمعة ، جاءت الفكرة إلى رأسي - "ألن أكتب مترجم؟"
من يهتم بمعرفة ما جاء بها ، مرحبا بك في القط.
استنادًا إلى "النظرية الكلاسيكية للمترجم" V. E. Karpov ، يمكننا تمييز 5 مراحل رئيسية للتجميع:
- التحليل المعجمي
- التحليل
- توليد الكود الأوسط
- التحسين
- جيل نهائي ، كائن ، رمز
عن كل شيء ، خمسة أجزاء ، يمكنك كتابة الكثير من الجمل والمقالات. ولكن ، اليوم ، سنتحدث عن الأولين وكيف اخترت هيكلها في مكتبة منفصلة.
عندما قررت أن أكتب ، حتى ولو كان جزءًا صغيرًا ، من المترجم ، لم أفكر في اللغة التي كنت أكتب لها ، ولهذا السبب ، كانت النتيجة مكتبة للتحليل المعجم والنحوي لأي لغة.
الرمز
قبل أن تبني بناء جملة وشجرة معجمية ، قم بإنشاء الشفرة الناتجة والقيام بأشياء لذيذة أخرى ، تحتاج إلى تقسيم شفرة المصدر إلى خطوط وشخصيات وأرقام.
حيث سيكون لكل عنصر نوع محدد بدقة. سيتم اعتبار الرموز المميزة للنوع غير المحدد كأخطاء في بناء الجملة أثناء التحليل.
في سياق التجميع ، تعتبر شفرة المصدر بمثابة خريطة مصدر ، ومن الممارسة الجيدة تخزينها في عملية المعجم والتحليل للتغذية المرتدة من المبرمج والإشارة إلى أخطاء بناء الجملة في شفرة المصدر.
يمكنك تقسيم شفرة المصدر إلى مصفوفة من الرموز المميزة باستخدام تعبير عادي بسيط:
/\S+/gm
يمكن أن يتغير اعتمادًا على شروط التحليل الإضافية ، مثل: تحليل فاصل الأسطر ، تحليل علامات التبويب ، تحليل المسافات.
ستكون نتيجة الفصل عبارة عن مجموعة من كلمات شفرة المصدر ، ويتم تحليل الكلمات من الفضاء إلى الفضاء ، أي هذا التصميم:
let hello = 'world';
سيتم تحويلها إلى مجموعة الرموز المميزة التالية:
["let", "hello", "=", "'world';"]
من أجل الحصول على المجموعة النهائية من الرموز المميزة ، تحتاج إلى مراجعة كل منها بتعبير عادي إضافي:
/(\W)|(\w+)/gm
ستكون النتيجة مجموعة من الرموز المميزة التي نحتاجها:
["let", "hello", "=", "'", "world", "'", ";"]
جميع الرموز التي تلقيناها ، نكتب إلى المصفوفة ، بالإضافة إلى مؤشراتها في خريطة المصدر
التحليل المعجمي
الآن بعد أن أصبح لدينا مجموعة من الرموز المميزة ، نحتاج إلى تحديد نوعها لتمريرها إلى التحليل.
تسمى الخوارزمية التي تحدد الرموز وأنواعها - Lexer
الرمز المميز ونوعه ، الذي يحدده Lexer ، يسمى Token
يمكن أن يكون لكل رمز مميز نوع محدد بشكل فريد ، على سبيل المثال:
['let', 'const', 'var']
ومع ذلك ، قمت بتنفيذ مخطط لتحديد أنواع الرموز باستخدام ما يسمى Solver'ov.
يعمل على النحو التالي:
1. يمكنك تحديد ثوابت لأنواع الرمز المميز:
const DefaultTokenTypes = { KEYWORD: "Keyword", IDENTIFIER: "Identifier", OPERATOR: "Operator", DELIMITER: "Delimiter", LINEBREAK: "Linebreak", STRING: "String", NUMERIC: "Numeric", UNKNOWN: 'Unknown' };
2. بعد ذلك ، من الضروري تحديد ما يسمى Solver'y:
const solvers = {}; solvers[MyTokenTypes.KEYWORD] = { include: [ 'const', 'let' ] }; solvers[MyTokenTypes.NUMERIC] = { regexp: /^[0-9.]*$/gm }; solvers[DefaultTokenTypes.STRING] = { type: StringSolver, delimiters: ['"', "'", '`'] }; solvers[MyTokenTypes.IDENTIFIER] = { regexp: /^[a-zA-Z_][a-zA-Z_0-9]*$/gm }; solvers[MyTokenTypes.DELIMITER] = { default: true };
كما ترى ، يمكن أن تحتوي الرموز المميزة على إعدادات مختلفة:
include - صفيف من الكلمات ، بالصدفة التي يمكن من خلالها تحديد نوع الرمز المميز.
regexp - تعبير عادي ، عن طريق الصدفة ، يمكن تحديد نوع الرمز المميز.
افتراضي - النوع القياسي للرموز المميزة غير المحددة.
يمكنك أيضًا ملاحظة معلمة
النوع ، التي تشير إلى أن هذا الحل يجب أن يكون موروثًا من المحدد في
النوعفي هذه الحالة ، يقوم حلالا بتعريف سلاسل محاطة بأحد الأحرف المحددة في
المحددات3. نستخدم المذيبات لصفيف الرمز المميز ونحصل على مصفوفة من الرموز المميزة المكتوبة. لرمز مصدر معين:
let a = 50;
نحصل على الشجرة التالية:
[ { "type": "Keyword", "value": "let", "range": [0, 3] }, { "type": "Identifier", "value": "a", "range": [4, 5] }, { "type": "Delimiter", "value": "=", "range": [6, 7] }, { "type": "Numeric", "value": "50", "range": [8, 10] }, { "type": "Delimiter", "value": ";", "range": [10, 11] } ]
حيث
Range هو بداية ونهاية الجزء في شفرة المصدر.
التحليل
بعد تلقي مصفوفة من الرموز المميزة ، يجب عليك تحليلها وتحديد البنية النحوية (الشجرة) لشفرة المصدر.
هناك العديد من الخيارات للتحليل ، لكني اخترت خوارزمية مباشرة من أعلى إلى أسفل.
يتم تحليل الرموز واحدًا تلو الآخر باستخدام مجموعة من القوالب. إذا كان القالب يتطابق مع التسلسل الحالي للرموز المميزة - في شجرة البنية ، يتم إنشاء فرع جديد.
مثال على قالب واحد من مصفوفة:
let declaration = new SequenceNode({ tokenType: MyTokenTypes.KEYWORD, type: MyNodeTypes.DECLARATION, sequence: [ {type: MyTokenTypes.KEYWORD}, {type: MyTokenTypes.IDENTIFIER}, {type: MyTokenTypes.DELIMITER}, {type: [MyTokenTypes.NUMERIC, MyTokenTypes.STRING]}, ';' ], onError: (e) => {
tokenType - يصف الرمز المميز الذي يبدأ منه التحقق من المطابقة.
النوع - يصف نوع العقدة ، ويجب أيضًا تحديد جميع الأنواع ، على غرار الأنواع المميزة.
تسلسل - صفيف من تسلسل ، يحتوي على أنواع من الرموز المميزة ، أو قيم محددة ، أو عقد أخرى من شجرة بناء الجملة.
onError - رد الاتصال ، والذي سيعمل عند
حدوث خطأ في بناء الجملة ، أثناء تحليل هذه العقدة ، فإنه يعيد نوع الخطأ + مكانه في التعليمات البرمجية المصدر.
لنحلل
تسلسل هذه العقدة:
[ {type: MyTokenTypes.KEYWORD},
قمت بتطبيق العديد من الأشكال المختلفة للعقد ، لأغراض مختلفة: تحديد تسلسل الرموز المميزة ، وتحديد مجموعة من العناصر (الوسيطات ، الكتل). يسمح بتحليل وظائف الأسهم دون أي مشاكل.
يمكنك أن تقرأ عن جميع أشكال المحلل والعقد التي قمت بتطبيقها في توثيق هذه المكتبة.
المواد
→ رابط المصدر:
Tyk→
نظرية المترجم الكلاسيكي