كيفية الاقتراب من كتابة المترجم الفوري من الصفر
نشرت: 2022-03-11يقول البعض أن "كل شيء يتلخص في الآحاد والأصفار" - لكن هل نفهم حقًا كيف تُترجم برامجنا إلى تلك البتات؟
يأخذ كل من المترجمين والمترجمين الفوريين سلسلة أولية تمثل البرنامج ، ويحللونها ، ويفهمونها. على الرغم من أن المترجمين الفوريين هم الأبسط من الاثنين ، إلا أن كتابة مترجم بسيط للغاية (لا يفعل سوى الجمع والضرب) سيكون مفيدًا. سنركز على القاسم المشترك بين المترجمين والمترجمين الفوريين: ترجمة وتحليل المدخلات.
ما يجب فعله وما لا يجب فعله في كتابة المترجم الفوري الخاص بك
قد يتساءل القراء ما هو الخطأ في regex؟ التعبيرات العادية قوية ، لكن القواعد النحوية للشفرة المصدر ليست بسيطة بما يكفي لتحليلها بواسطتها. لا توجد لغات خاصة بالمجال (DSL) ، وقد يحتاج العميل إلى DSL مخصص لتعبيرات التفويض ، على سبيل المثال. ولكن حتى بدون تطبيق هذه المهارة بشكل مباشر ، فإن كتابة مترجم فوري يجعل تقدير الجهد المبذول وراء العديد من لغات البرمجة وتنسيقات الملفات و DSLs أسهل بكثير.
يمكن أن تكون الكتابة اليدوية بشكل صحيح أمرًا صعبًا مع كل الحالات المعقدة. لهذا السبب توجد أدوات شائعة ، مثل ANTLR ، يمكنها إنشاء محللات للعديد من لغات البرمجة الشائعة. هناك أيضًا مكتبات تسمى مجموعات المحلل اللغوي ، والتي تمكن المطورين من كتابة المحلل اللغوي مباشرة بلغات البرمجة المفضلة لديهم. تتضمن الأمثلة FastParse لـ Scala و Parsec لـ Python.
نوصي ، في سياق احترافي ، أن يستخدم القراء مثل هذه الأدوات والمكتبات لتجنب إعادة اختراع العجلة. ومع ذلك ، فإن فهم تحديات وإمكانيات كتابة مترجم فوري من البداية سيساعد المطورين على الاستفادة من هذه الحلول بشكل أكثر فعالية.
نظرة عامة على مكونات المترجم الفوري
المترجم هو برنامج معقد ، لذلك هناك عدة مراحل له:
- lexer هو جزء من المترجم الفوري يحول سلسلة من الأحرف (نص عادي) إلى سلسلة من الرموز المميزة.
- المحلل اللغوي ، بدوره ، يأخذ سلسلة من الرموز وينتج شجرة بناء جملة مجردة (AST) للغة. عادة ما يتم تحديد القواعد التي يعمل بها المحلل اللغوي بواسطة قواعد النحو الرسمية.
- المترجم هو برنامج يفسر AST لمصدر برنامج ما على الفور (بدون تجميعه أولاً).
لن نبني هنا مترجمًا شفويًا محددًا ومتكاملًا. بدلاً من ذلك ، سوف نستكشف كل جزء من هذه الأجزاء ومشكلاتها الشائعة بأمثلة منفصلة. في النهاية ، سيبدو رمز المستخدم كما يلي:
val input = "2 * 7 + 5" val tokens = Lexer(input).lex() val ast = Parser(tokens).parse() val res = Interpreter(ast).interpret() println(s"Result is: $res") بعد المراحل الثلاث ، نتوقع أن يقوم هذا الرمز بحساب القيمة النهائية ونتائج الطباعة Result is: 19 . يحدث هذا البرنامج التعليمي لاستخدام Scala لأنه:
- موجز للغاية ، يناسب الكثير من التعليمات البرمجية في شاشة واحدة.
- موجه نحو التعبير ، دون الحاجة إلى متغيرات غير مهيأة / فارغة.
- اكتب آمنًا ، مع مكتبة مجموعات قوية ، وتعدادات ، وفئات حالة.
على وجه التحديد ، تتم كتابة الكود هنا في بنية Scala3 الاختيارية الأقواس (بناء جملة يشبه Python ، قائم على المسافة البادئة). لكن لا يوجد أي من الأساليب خاص بـ Scala ، و Scala مشابه للعديد من اللغات الأخرى: سيجد القراء أنه من السهل تحويل عينات التعليمات البرمجية هذه إلى لغات أخرى. باستثناء ذلك ، يمكن تشغيل الأمثلة عبر الإنترنت باستخدام Scastie.
أخيرًا ، تحتوي أقسام Lexer و Parser و Interpreter على أمثلة مختلفة لقواعد النحو. كما يوضح GitHub repo المقابل ، فإن التبعيات في الأمثلة اللاحقة تتغير قليلاً لتنفيذ هذه القواعد النحوية ، لكن المفاهيم العامة تظل كما هي.
المترجم المكون 1: كتابة المعجم
لنفترض أننا نرغب في lex هذه السلسلة: "123 + 45 true * false1" . يحتوي على أنواع مختلفة من الرموز المميزة:
- عدد صحيح حرفية
- عامل التشغيل A
+ - عامل
* - حرفية
true - المعرف
false1
سيتم تخطي المسافة البيضاء بين الرموز المميزة في هذا المثال.
في هذه المرحلة ، لا يجب أن تكون التعبيرات منطقية ؛ يقوم lexer ببساطة بتحويل سلسلة الإدخال إلى قائمة الرموز المميزة. (تُرك مهمة "فهم الرموز المميزة" للمحلل.)
سنستخدم هذا الرمز لتمثيل رمز مميز:
case class Token( tpe: Token.Type, text: String, startPos: Int ) object Token: enum Type: case Num case Plus case Times case Identifier case True case False case EOFكل رمز له نوع وتمثيل نصي وموضع في الإدخال الأصلي. يمكن أن يساعد الموقف المستخدمين النهائيين لـ lexer في تصحيح الأخطاء.
رمز EOF هو رمز مميز يشير إلى نهاية الإدخال. لا يوجد في النص المصدر ؛ نحن نستخدمه فقط لتبسيط مرحلة التحليل اللغوي.
سيكون هذا ناتج lexer الخاص بنا:
Lexing input: 123 + 45 true * false1 Tokens: List( Token(tpe = Num, text = "123", tokenStartPos = 0), Token(tpe = Plus, text = "+", tokenStartPos = 4), Token(tpe = Num, text = "45", tokenStartPos = 6), Token(tpe = True, text = "true", tokenStartPos = 9), Token(tpe = Times, text = "*", tokenStartPos = 14), Token(tpe = Identifier, text = "false1", tokenStartPos = 16), Token(tpe = EOF, text = "<EOF>", tokenStartPos = 22) )دعنا نفحص التنفيذ:
class Lexer(input: String): def lex(): List[Token] = val tokens = mutable.ArrayBuffer.empty[Token] var currentPos = 0 while currentPos < input.length do val tokenStartPos = currentPos val lookahead = input(currentPos) if lookahead.isWhitespace then currentPos += 1 // ignore whitespace else if lookahead == '+' then currentPos += 1 tokens += Token(Type.Plus, lookahead.toString, tokenStartPos) else if lookahead == '*' then currentPos += 1 tokens += Token(Type.Times, lookahead.toString, tokenStartPos) else if lookahead.isDigit then var text = "" while currentPos < input.length && input(currentPos).isDigit do text += input(currentPos) currentPos += 1 tokens += Token(Type.Num, text, tokenStartPos) else if lookahead.isLetter then // first must be letter var text = "" while currentPos < input.length && input(currentPos).isLetterOrDigit do text += input(currentPos) currentPos += 1 val tpe = text match case "true" => Type.True // special casing literals case "false" => Type.False case _ => Type.Identifier tokens += Token(tpe, text, tokenStartPos) else error(s"Unknown character '$lookahead' at position $currentPos") tokens += Token(Type.EOF, "<EOF>", currentPos) // special end marker tokens.toListنبدأ بقائمة فارغة من الرموز المميزة ، ثم ننتقل عبر السلسلة ونضيف الرموز المميزة فور ظهورها.
نستخدم حرف lookahead لتحديد نوع الرمز المميز التالي . لاحظ أن شخصية lookahead ليست دائمًا الشخصية الأبعد التي يتم فحصها. بناءً على lookahead ، نعرف كيف يبدو الرمز المميز ونستخدم currentPos لمسح جميع الأحرف المتوقعة في الرمز المميز الحالي ، ثم أضف الرمز المميز إلى القائمة:
إذا كان رأس النظر عبارة عن مسافة بيضاء ، فإننا نتخطاه. الرموز المميزة ذات الحرف الواحد تافهة ؛ نضيفها ونزيد الفهرس. بالنسبة للأعداد الصحيحة ، نحتاج فقط إلى الاهتمام بالفهرس.
نصل الآن إلى شيء معقد بعض الشيء: المعرفات مقابل العناصر الحرفية. القاعدة هي أننا نأخذ أطول مباراة ممكنة ونتحقق مما إذا كانت حرفية ؛ إذا لم يكن كذلك ، فهو معرّف.
توخى الحذر عند التعامل مع عوامل تشغيل مثل < و <= . هناك عليك أن تنظر إلى الأمام مرة أخرى لترى ما إذا كانت = قبل أن تستنتج أنها عامل تشغيل <= . خلاف ذلك ، فهو مجرد < .
مع ذلك ، أنتج lexer لدينا قائمة من الرموز المميزة.
المترجم المكون 2: كتابة المحلل اللغوي
علينا أن نعطي بعض البنية لرموزنا المميزة - لا يمكننا فعل الكثير بالقائمة وحدها. على سبيل المثال ، نحتاج إلى معرفة:
ما هي التعبيرات المتداخلة؟ ما هي العوامل التي يتم تطبيقها بأي ترتيب؟ ما هي قواعد تحديد النطاق التي تنطبق ، إن وجدت؟
يدعم هيكل الشجرة التعشيش والترتيب. لكن أولاً ، علينا تحديد بعض القواعد لبناء الأشجار. نود أن يكون المحلل اللغوي لدينا واضحًا - لإرجاع نفس البنية دائمًا لمدخل معين.
لاحظ أن المحلل اللغوي التالي لا يستخدم مثال lexer السابق . هذا الرقم مخصص لجمع الأرقام ، لذا فإن قواعده النحوية بها رمزان مميزان فقط ، '+' و NUM :
expr -> expr '+' expr expr -> NUM المكافئ ، باستخدام حرف الأنبوب ( | ) كرمز "أو" كما هو الحال في التعبيرات العادية ، هو:
expr -> expr '+' expr | NUM في كلتا الحالتين ، لدينا قاعدتان: واحدة تنص على أنه يمكننا جمع اثنين من expr s والأخرى تقول أن expr يمكن أن يكون رمزًا مميزًا NUM ، وهو ما يعني هنا عددًا صحيحًا غير سالب.
عادة ما يتم تحديد القواعد بقواعد نحوية رسمية . تتكون القواعد الرسمية من: القواعد نفسها ، كما هو موضح أعلاه قاعدة البداية (القاعدة الأولى محددة ، لكل اصطلاح) نوعان من الرموز لتحديد القواعد باستخدام: المحطات الطرفية: "الأحرف" (وغيرها من الشخصيات) من لغتنا - الرموز غير القابلة للاختزال التي تشكل الرموز المميزة Nonterminals: التركيبات الوسيطة المستخدمة للتحليل (أي الرموز التي يمكن استبدالها)
يمكن أن يكون الجانب غير النهائي فقط على الجانب الأيسر من القاعدة ؛ يمكن أن يحتوي الجانب الأيمن على كل من المحطات الطرفية وغير الطرفية. في المثال أعلاه ، المحطات الطرفية هي '+' و NUM ، والطرف غير النهائي الوحيد هو expr . للحصول على مثال أوسع ، في لغة Java ، لدينا محطات طرفية مثل 'true' و '+' و Identifier و '[' و nonterminals مثل BlockStatements و ClassBody و MethodOrFieldDecl .
هناك العديد من الطرق التي يمكننا بها تنفيذ هذا المحلل اللغوي. هنا ، سنستخدم أسلوب التحليل "العودي". إنه النوع الأكثر شيوعًا لأنه أسهل أنواع الفهم والتنفيذ.
يستخدم محلل النسب العودية وظيفة واحدة لكل لغة غير نهائية في القواعد. يبدأ من قاعدة البداية وينزل من هناك (ومن ثم "النسب") ، لمعرفة القاعدة التي يجب تطبيقها في كل وظيفة. يعتبر الجزء "العودي" حيويًا لأنه يمكننا إجراء تداخل تكراري غير متكرر! لا تستطيع Regexes فعل ذلك: لا يمكنها حتى التعامل مع الأقواس المتوازنة. لذلك نحن بحاجة إلى أداة أكثر قوة.
قد يبدو المحلل اللغوي للقاعدة الأولى مثل هذا (الكود الكامل):
def expr() = expr() eat('+') expr() تتحقق وظيفة eat() مما إذا كان lookahead يطابق الرمز المميز المتوقع ثم تحرك مؤشر lookahead. لسوء الحظ ، لن يعمل هذا بعد لأننا بحاجة إلى إصلاح بعض المشكلات في قواعدنا.
الغموض النحوي
المسألة الأولى هي غموض قواعدنا الذي قد لا يكون واضحًا للوهلة الأولى:
expr -> expr '+' expr | NUM بالنظر إلى الإدخال 1 + 2 + 3 ، يمكن للمحلل أن يختار حساب إما expr expr أولاً في AST الناتج:
لهذا السبب نحتاج إلى إدخال بعض عدم التماثل :
expr -> expr '+' NUM | NUMمجموعة التعبيرات التي يمكننا تمثيلها بهذه القواعد لم تتغير منذ إصدارها الأول. الآن فقط لا لبس فيه : المحلل اللغوي يذهب يسارًا دائمًا. فقط ما نحتاجه!

هذا يجعل عمليتنا + ارتباطية يسارية ، لكن هذا سيصبح واضحًا عندما نصل إلى قسم المترجم الفوري.
قواعد اليسار العودية
لسوء الحظ ، الإصلاح أعلاه لا يحل مشكلتنا الأخرى ، العودية اليسرى:
def expr() = expr() eat('+') eat(NUM)لدينا العودية اللانهائية هنا. إذا أردنا الدخول في هذه الوظيفة ، فسنحصل في النهاية على خطأ تجاوز سعة مكدس. لكن تحليل النظرية يمكن أن يساعد!
لنفترض أن لدينا قواعد نحوية مثل هذه ، حيث يمكن أن تكون alpha أي سلسلة من المحطات الطرفية وغير النهائية:
A -> A alpha | Bيمكننا إعادة كتابة هذه القواعد على النحو التالي:
A -> BA' A' -> alpha A' | epsilon هناك ، epsilon عبارة عن سلسلة فارغة - لا شيء ولا رمز.
لنأخذ المراجعة الحالية لقواعدنا:
expr -> expr '+' NUM | NUM باتباع طريقة إعادة كتابة قواعد التحليل المفصلة أعلاه ، مع كون alpha '+' NUM ، تصبح قواعدنا:
expr -> NUM exprOpt exprOpt -> '+' NUM exprOpt | epsilonالآن القواعد على ما يرام ، ويمكننا تحليلها بمحلل النسب العودي. دعونا نرى كيف سيبحث هذا المحلل اللغوي عن هذا التكرار الأخير لقواعدنا اللغوية:
class Parser(allTokens: List[Token]): import Token.Type private var tokens = allTokens private var lookahead = tokens.head def parse(): Unit = expr() if lookahead.tpe != Type.EOF then error(s"Unknown token '${lookahead.text}' at position ${lookahead.tokenStartPos}") private def expr(): Unit = eat(Type.Num) exprOpt() private def exprOpt(): Unit = if lookahead.tpe == Type.Plus then eat(Type.Plus) eat(Type.Num) exprOpt() // else: end recursion, epsilon private def eat(tpe: Type): Unit = if lookahead.tpe != tpe then error(s"Expected: $tpe, got: ${lookahead.tpe} at position ${lookahead.startPos}") tokens = tokens.tail lookahead = tokens.head هنا نستخدم رمز EOF لتبسيط المحلل اللغوي الخاص بنا. نحن على يقين دائمًا من وجود رمز مميز واحد على الأقل في قائمتنا ، لذلك لا نحتاج إلى التعامل مع حالة خاصة لقائمة فارغة.
أيضًا ، إذا قمنا بالتبديل إلى lexer المتدفق ، فلن يكون لدينا قائمة في الذاكرة بل مكرر ، لذلك نحتاج إلى علامة لمعرفة متى نصل إلى نهاية الإدخال. عندما نصل إلى النهاية ، يجب أن يكون رمز EOF هو الرمز المميز الأخير المتبقي.
بالمشي خلال الشفرة ، يمكننا أن نرى أن التعبير يمكن أن يكون مجرد رقم. إذا لم يتبق شيء ، فلن يكون الرمز المميز التالي علامة Plus ، لذلك سنتوقف عن التحليل. سيكون الرمز المميز الأخير هو EOF ، وسننتهي.
إذا كانت سلسلة الإدخال تحتوي على المزيد من الرموز المميزة ، فيجب أن تبدو مثل + 123 . هذا هو المكان الذي يبدأ فيه العودية على exprOpt() !
توليد AST
الآن بعد أن قمنا بتحليل تعبيرنا بنجاح ، من الصعب فعل أي شيء به كما هو. يمكننا وضع بعض الاسترجاعات في المحلل اللغوي الخاص بنا ، لكن ذلك سيكون مرهقًا جدًا وغير قابل للقراءة. بدلاً من ذلك ، سنعيد AST ، شجرة تمثل تعبير الإدخال:
case class Expr(num: Int, exprOpt: ExprOpt) enum ExprOpt: case Opt(num: Int, exprOpt: ExprOpt) case Epsilonهذا يشبه قواعدنا ، باستخدام فئات البيانات البسيطة.
يعرض المحلل اللغوي الآن بنية بيانات مفيدة:
class Parser(allTokens: List[Token]): import Token.Type private var tokens = allTokens private var lookahead = tokens.head def parse(): Expr = val res = expr() if lookahead.tpe != Type.EOF then error(s"Unknown token '${lookahead.text}' at position ${lookahead.tokenStartPos}") else res private def expr(): Expr = val num = eat(Type.Num) Expr(num.text.toInt, exprOpt()) private def exprOpt(): ExprOpt = if lookahead.tpe == Type.Plus then eat(Type.Plus) val num = eat(Type.Num) ExprOpt.Opt(num.text.toInt, exprOpt()) else ExprOpt.Epsilon بالنسبة إلى eat() error() وتفاصيل التنفيذ الأخرى ، يرجى الاطلاع على GitHub repo المصاحب.
تبسيط القواعد
لا يزال من الممكن تحسين ExprOpt nonterminal:
'+' NUM exprOpt | epsilonمن الصعب التعرف على النمط الذي يمثله في قواعدنا بمجرد النظر إليه. اتضح أنه يمكننا استبدال هذه العودية ببنية أبسط:
('+' NUM)* تعني هذه التركيبة ببساطة أن '+' NUM لا يحدث مرة أو أكثر.
الآن تبدو قواعدنا الكاملة كما يلي:
expr -> NUM exprOpt* exprOpt -> '+' NUMوتبدو AST لدينا أجمل:
case class Expr(num: Int, exprOpts: Seq[ExprOpt]) case class ExprOpt(num: Int) يكون المحلل اللغوي الناتج بنفس الطول ولكنه أبسط في الفهم والاستخدام. لقد قضينا على Epsilon ، وهو ما يعنيه الآن البدء بهيكل فارغ.
لم نكن بحاجة إلى فئة ExprOpt هنا. كان بإمكاننا فقط وضع case class Expr(num: Int, exprOpts: Seq[Int]) ، أو بتنسيق نحوي ، NUM ('+' NUM)* . فلماذا لم نفعل؟
ضع في اعتبارك أنه إذا كان لدينا عدة عوامل تشغيل محتملة ، مثل - أو * ، فسنحصل على قواعد نحوية مثل هذه:
expr -> NUM exprOpt* exprOpt -> [+-*] NUM في هذه الحالة ، يحتاج AST بعد ذلك إلى ExprOpt لاستيعاب نوع المشغل:
case class Expr(num: Int, exprOpts: Seq[ExprOpt]) case class ExprOpt(op: String, num: Int) لاحظ أن بناء الجملة [+-*] في القواعد يعني نفس الشيء كما في التعبيرات العادية: "أحد هذه الأحرف الثلاثة." سنرى هذا في العمل قريبًا.
المكون 3: كتابة مترجم فوري
سيستفيد مترجمنا من lexer والمحلل اللغوي للحصول على AST لتعبير الإدخال الخاص بنا ثم تقييم AST بالطريقة التي نريدها. في هذه الحالة ، نحن نتعامل مع الأعداد ونريد حساب مجموعها.
في تطبيق مثال المترجم الشفهي الخاص بنا ، سوف نستخدم هذه القواعد البسيطة:
expr -> NUM exprOpt* exprOpt -> [+-] NUMوهذا AST:
case class Expr(num: Int, exprOpts: Seq[ExprOpt]) case class ExprOpt(op: Token.Type, num: Int)(لقد غطينا كيفية تنفيذ lexer ومحلل لقواعد نحوية مماثلة ، ولكن أي قارئ يتعثر يمكنه الاطلاع على تطبيقات lexer والمحلل لهذه القواعد الدقيقة في الريبو.)
سنرى الآن كيفية كتابة مترجم للقواعد أعلاه:
class Interpreter(ast: Expr): def interpret(): Int = eval(ast) private def eval(expr: Expr): Int = var tmp = expr.num expr.exprOpts.foreach { exprOpt => if exprOpt.op == Token.Type.Plus then tmp += exprOpt.num else tmp -= exprOpt.num } tmp إذا قمنا بتحليل المدخلات الخاصة بنا في AST دون مواجهة أي خطأ ، فنحن على يقين من أنه سيكون لدينا دائمًا NUM واحد على الأقل. ثم نأخذ الأرقام الاختيارية ونجمعها (أو نطرحها من) النتيجة.
أصبحت الملاحظة من البداية حول الارتباط الأيسر لـ + واضحة الآن: نبدأ من الرقم الموجود في أقصى اليسار ونضيف عددًا آخر ، من اليسار إلى اليمين. قد يبدو هذا غير مهم للإضافة ، ولكن ضع في اعتبارك الطرح: يتم تقييم التعبير 5-2-1 على أنه (5 - 2) - 1 = 3 - 1 = 2 5 - 2 - 1 5 - (2 - 1) = 5 - 1 = 4 !
ولكن إذا أردنا تجاوز تفسير عوامل الجمع والناقص ، فهناك قاعدة أخرى يجب تحديدها.
الأولوية
نحن نعرف كيفية تحليل تعبير بسيط مثل 1 + 2 + 3 ، ولكن عندما يتعلق الأمر بـ 2 + 3 * 4 + 5 ، لدينا مشكلة صغيرة.
يتفق معظم الناس على الاتفاقية القائلة بأن الضرب له أسبقية أعلى من الجمع. لكن المحلل اللغوي لا يعرف ذلك. لا يمكننا تقييمه على أنه ((2 + 3) * 4) + 5 . بدلًا من ذلك نريد (2 + (3 * 4)) + 5 .
هذا يعني أننا بحاجة إلى حساب الضرب أولًا . يحتاج الضرب إلى أن يكون أبعد من جذر AST لإجباره على التقييم قبل الإضافة. لهذا ، نحتاج إلى تقديم طبقة أخرى من المراوغة.
إصلاح القواعد النحوية الساذجة من البداية إلى النهاية
هذه هي القواعد النحوية الأصلية ذات الاتجاه الأيسر ، والتي ليس لها قواعد أسبقية:
expr -> expr '+' expr | expr '*' expr | NUMأولاً: نعطيها قواعد الأسبقية ونزيل غموضها :
expr -> expr '+' term | term term -> term '*' NUM | NUMثم تحصل على قواعد غير متكررة لليسار :
expr -> term exprOpt* exprOpt -> '+' term term -> NUM termOpt* termOpt -> '*' NUMوالنتيجة AST معبرة بشكل جميل:
case class Expr(term: Term, exprOpts: Seq[ExprOpt]) case class ExprOpt(term: Term) case class Term(num: Int, termOpts: Seq[TermOpt]) case class TermOpt(num: Int)هذا يتركنا مع تطبيق مترجم فوري موجز:
class Interpreter(ast: Expr): def interpret(): Int = eval(ast) private def eval(expr: Expr): Int = var tmp = eval(expr.term) expr.exprOpts.foreach { exprOpt => tmp += eval(exprOpt.term) } tmp private def eval(term: Term): Int = var tmp = term.num term.termOpts.foreach { termOpt => tmp *= termOpt.num } tmpكما كان من قبل ، تمت تغطية الأفكار الموجودة في المعجم والقواعد المطلوبة في وقت سابق ، ولكن يمكن للقراء العثور عليها في الريبو إذا لزم الأمر.
الخطوات التالية في كتابة المترجمين الفوريين
لم نقم بتغطية هذا ، لكن معالجة الأخطاء والإبلاغ عنها هي سمات مهمة لأي محلل. كمطورين ، نحن نعلم مدى الإحباط الذي يمكن أن يحدث عندما ينتج عن المترجم أخطاء محيرة أو مضللة. إنها منطقة بها العديد من المشكلات المثيرة للاهتمام لحلها ، مثل إعطاء رسائل خطأ صحيحة ودقيقة ، وعدم ردع المستخدم برسائل أكثر من اللازم ، والتعافي بأمان من الأخطاء. الأمر متروك للمطورين الذين يكتبون مترجمًا أو مترجمًا لضمان حصول المستخدمين المستقبليين على تجربة أفضل.
أثناء تجولنا في أمثلة المعجمين والمحللين والمترجمين الفوريين ، قمنا فقط بخدش سطح النظريات الكامنة وراء المجمعين والمترجمين الفوريين ، والتي تغطي موضوعات مثل:
- النطاقات وجداول الرموز
- أنواع ثابتة
- أمثلية وقت الترجمة
- أجهزة تحليل البرامج الثابتة والنباتات
- تنسيق الكود والطباعة الجميلة
- اللغات الخاصة بالمجال
لمزيد من القراءة ، أوصي بهذه الموارد:
- أنماط تنفيذ اللغة بواسطة Terence Parr
- كتاب مجاني على الإنترنت ، صياغة المترجمين الفوريين ، من تأليف بوب نيستروم
- مقدمة في القواعد والتحليل بواسطة Paul Klint
- كتابة رسائل خطأ جيدة للمجمع بواسطة كاليب ميريديث
- الملاحظات من الدورة التدريبية لجامعة شرق كارولينا "ترجمة البرامج وتجميعها"
