حاولت هنا أن أوضح في الممارسة العملية ما هي بعض المفاهيم المهمة من مجال إنشاء برنامج التحويل البرمجي. هناك احتمال أن مثل هذه القصص المكتملة لمدة 15 دقيقة يمكن أن تكون وسيلة جيدة للغطس في مواضيع معقدة. لن يكون من الجيد عدم قراءة ما يرد أدناه بشكل سلبي ، ولكن أيضًا التحقق من الشفرة في العمل.
إذا نجحت التجربة الأولى ، فيمكنك في المستقبل توقع "رسومات" مدتها 15 دقيقة حول موضوع المجمعين.
ما سوف تناقش
دعونا نجعل مترجم التعبير الحسابي. واحد يقوم بترجمة النص المصدر بترميز عكسي (يُسمى أيضًا RPN أو POLIZ) إلى رمز وسيط يعمل مع الحزمة. لكن يمكننا الاستغناء عن المترجمين الفوريين هنا. بعد ذلك ، نترجم النتيجة على الفور إلى تمثيل C. وهذا هو ، نحصل على مترجم من RPN إلى C.
بالمناسبة ، سنكتب المترجم في بيثون. لكن لا تدع هذا يوقف أولئك الذين يفضلون لغة برمجة أخرى. إليك تمرين مفيد لك: قم بترجمة الكود أدناه إلى لغتك المفضلة. أو استخدم ترجمة مكتملة بالفعل:
تطبيق F # (بواسطة gsomix ):
الإصدار الأول
إصدار SSA
لنبدأ بالتحليل
def scan(source): tokens = source.split() return [("Push", int(x)) if x[0].isdigit() else ("Op", x) for x in tokens]
ماذا فعلنا هنا؟ تتلقى وظيفة المسح سلسلة من المستخدم بترميز بولندي عكسي ("2 2 +").
وفي الناتج نحصل على تمثيل وسيط. هنا مثال:
[ ('Push', 2), ('Push', 2), ('Op', '+') ]
لذلك ، لدينا بالفعل المترجم. لكنه تافه جدا. أذكر أنه في البداية كان حوالي C رمز.
لنبث في C
def trans(ir): code = [] for tag, val in ir: if tag == "Push": code.append("st[sp] = %d;" % val) code.append("sp += 1;") elif tag == "Op": code.append("st[sp - 2] = st[sp - 2] %s st[sp - 1];" % val) code.append("sp -= 1;") return "\n".join(code)
ما الذي يحدث هنا؟ دعنا ننظر إلى إخراج هذه الوظيفة (باستخدام نفس المثال مع "2 2 +").
st[sp] = 2; sp += 1; st[sp] = 2; sp += 1; st[sp - 2] = st[sp - 2] + st[sp - 1]; sp -= 1;
نعم ، يبدو بالفعل مثل رمز C. تلعب Array st دور المكدس ، و sp هو مؤشرها. عادةً ما تعمل آلات الرصة الافتراضية مع هذه الأشياء.
هذا فقط الجهاز نفسه - المترجم الفوري ، ليس لدينا. هناك مترجم. ماذا تركنا؟ من الضروري إضافة الإطارات اللازمة لبرنامج C.
لدينا أول مترجم النهائي
ST_SIZE = 100 C_CODE = r"""#include <stdio.h> int main(int argc, char** argv) { int st[%d], sp = 0; %s printf("%%d\n", st[sp - 1]); return 0; }""" def scan(source): tokens = source.split() return [("Push", int(x)) if x[0].isdigit() else ("Op", x) for x in tokens] def trans(ir): code = [] for tag, val in ir: if tag == "Push": code.append("st[sp] = %d;" % val) code.append("sp += 1;") elif tag == "Op": code.append("st[sp - 2] = st[sp - 2] %s st[sp - 1];" % val) code.append("sp -= 1;") return "\n".join(code) def rpn_to_c(source): return C_CODE % (ST_SIZE, trans(scan(source))) print(rpn_to_c("2 2 +"))
يبقى تجميع إخراج هذا البرنامج باستخدام برنامج التحويل البرمجي C.
هل ما زلت مستعدًا للمتابعة؟ ثم دعونا نناقش ما فعلناه. هناك لحظة مشكوك فيها - يقوم المترجم الخاص بنا بترجمة تعبيرات ثابتة ، ويمكنك حسابها ببساطة في مرحلة الترجمة. لا معنى لترجمتها إلى كود. ولكن دعنا نأخذ ذلك الآن لأن بعض الحجج يمكن أن تتراكم من الخارج. دعونا نتحدث عن حقيقة أنه يمكن إعطاء المعنى العملي لتنميتنا في وقت لاحق. من المهم الآن الحصول على فكرة عامة عن بناء أبسط المترجمين ، أليس كذلك؟
هل تحب العنوان؟ SSA - هذا يبدو صلبًا جدًا لأي مترجم. والآن سوف نستخدم نفس SSA. ما هذا؟ دعنا ننتقل في النظام.
نقوم حاليًا بإنشاء رمز C ، دون أي أجهزة افتراضية. ولكن لماذا نحتاج إلى عمل أولي في شكل عمليات مكدس؟ دعنا نستبدل هذه العمليات بالعمل مع المتغيرات العادية من C. علاوة على ذلك ، لن نحفظ المتغيرات - سيكون لدينا اسم جديد لكل تعبير. دع المترجم C يتعامل مع كل هذا. اتضح أنه معنا يتم تعيين قيمة لكل متغير مرة واحدة فقط. وهذا ، بالمناسبة ، هو شكل SSA .
هنا لدينا مترجم جديد.
C_CODE = r"""#include <stdio.h> int main(int argc, char** argv) { %s printf("%%d\n", %s); return 0; }""" def scan(source): tokens = source.split() return [("Push", int(x)) if x[0].isdigit() else ("Op", x) for x in tokens] def trans(ir): stack, code = [], [] name_cnt = 0 for tag, val in ir: if tag == "Push": code.append("int t%d = %d;" % (name_cnt, val)) stack.append("t%d" % name_cnt) name_cnt += 1 elif tag == "Op": a, b = stack.pop(), stack.pop() code.append("int t%d = %s %s %s;" % (name_cnt, b, val, a)) stack.append("t%d" % name_cnt) name_cnt += 1 return "\n".join(code), stack.pop() def rpn_to_c(source): return C_CODE % trans(scan(source)) print(rpn_to_c("2 2 +"))
يرجى ملاحظة أنه لم يعد هناك مكدس في كود C ، وأن العمل به يتم محاكاته أثناء الترجمة. لا يحتوي المكدس المستخدم في عملية التحويل البرمجي على قيم ، ولكن يحتوي على أسماء متغيرة.
ها هي النتيجة النهائية:
#include <stdio.h> int main(int argc, char** argv) { int t0 = 2; int t1 = 2; int t2 = t0 + t1; printf("%d\n", t2); return 0; }
ملخص
يبدو أن وقت درسنا المشترك قد انتهى. لقد شاركنا في ترجمة البرنامج من لغة إلى أخرى. وهذا ما يسمى ترجمة من مصدر إلى مصدر. أو - مجرد ترجمة ، والتي يمكن اعتبارها مرادفًا للترجمة ، ولكن عادةً ما يترجم المترجم البرنامج من تمثيل رفيع المستوى إلى مستوى منخفض. هناك أيضًا الكلمة الطنانة "transpiler" للمترجم من مصدر إلى مصدر. لكن ذكر كلمة "transpiler" يمكن أن يكون مزعجًا بالنسبة لخبراء المترجم ، كن حذرًا!
تجربة مع الكود. في انتظار ردود الفعل!