المُترجِم¶
تتضمن عملية الترجمة في MicroPython الخطوات التالية:
يحوّل المحلل المعجمي (lexer) دفق النص الذي يكوّن برنامج MicroPython إلى وحدات معجمية (tokens).
ثم يحوّل المحلل النحوي (parser) الوحدات المعجمية إلى صيغة نحوية مجردة (شجرة تحليل).
بعد ذلك يُولَّد الرمز البايتي (bytecode) أو الرمز الأصلي (native code) استناداً إلى شجرة التحليل.
لأغراض هذا النقاش سنضيف ميزة لغوية بسيطة add1 يمكن استخدامها في Python على النحو التالي:
>>> add1 3
4
>>>
تأخذ عبارة add1 عدداً صحيحاً كوسيط وتضيف إليه 1.
إضافة قاعدة نحوية¶
تستند قواعد MicroPython النحوية إلى قواعد CPython النحوية وهي معرّفة في py/grammar.h. وهذه القواعد هي ما يُستخدَم لتحليل ملفات مصدر MicroPython.
هناك ماكروهان يجب أن تعرفهما لتعريف قاعدة نحوية: DEF_RULE و DEF_RULE_NC. يتيح لك DEF_RULE تعريف قاعدة مع دالة ترجمة مرتبطة بها، بينما لا يحتوي DEF_RULE_NC على دالة ترجمة (NC) لها.
يبدو التعريف النحوي البسيط مع دالة ترجمة لعبارة add1 الجديدة كما يلي:
DEF_RULE(add1_stmt, c(add1_stmt), and(2), tok(KW_ADD1), rule(testlist))
الوسيط الثاني c(add1_stmt) هو دالة الترجمة المقابلة التي ينبغي تنفيذها في py/compile.c لتحويل هذه القاعدة إلى رمز قابل للتنفيذ.
يمكن أن يكون الوسيط الثالث المطلوب or أو and. يحدد هذا عدد العُقَد المرتبطة بالعبارة. على سبيل المثال، في هذه الحالة، تشبه عبارة add1 لدينا التعليمة ADD1 في لغة التجميع. فهي تأخذ وسيطاً عددياً واحداً. لذلك، يرتبط بـ add1_stmt عقدتان. إحداهما للعبارة نفسها، أي القيمة الحرفية add1 المقابلة لـ KW_ADD1، والأخرى لوسيطها، وهي قاعدة testlist التي تمثل قاعدة التعبير على المستوى الأعلى.
ملاحظة
قاعدة add1 هنا ما هي إلا مثال وليست جزءاً من قواعد MicroPython النحوية القياسية.
الوسيط الرابع في هذا المثال هو الوحدة المعجمية المرتبطة بالقاعدة، KW_ADD1. وينبغي تعريف هذه الوحدة المعجمية في المحلل المعجمي عن طريق تحرير py/lexer.h.
يتحقق تعريف القاعدة نفسها دون دالة ترجمة باستخدام الماكرو DEF_RULE_NC وحذف وسيط دالة الترجمة:
DEF_RULE_NC(add1_stmt, and(2), tok(KW_ADD1), rule(testlist))
تحمل بقية الوسائط المعنى نفسه. يجب أن تُعالَج القاعدة التي لا تحتوي على دالة ترجمة معالجةً صريحة من قبل جميع القواعد التي قد تتضمن هذه القاعدة كعقدة. وتُستخدَم قواعد NC هذه عادةً للتعبير عن أجزاء فرعية من بنية نحوية معقدة لا يمكن التعبير عنها في قاعدة واحدة.
ملاحظة
يأخذ الماكروهان DEF_RULE و DEF_RULE_NC وسائط أخرى. لفهم متعمق للمعاملات المدعومة، راجع py/grammar.h.
إضافة وحدة معجمية¶
ينبغي أن يكون لكل قاعدة معرّفة في القواعد النحوية وحدة معجمية مرتبطة بها معرّفة في py/lexer.h. أضف هذه الوحدة المعجمية عن طريق تحرير التعداد _mp_token_kind_t:
typedef enum _mp_token_kind_t {
...
MP_TOKEN_KW_OR,
MP_TOKEN_KW_PASS,
MP_TOKEN_KW_RAISE,
MP_TOKEN_KW_RETURN,
MP_TOKEN_KW_TRY,
MP_TOKEN_KW_WHILE,
MP_TOKEN_KW_WITH,
MP_TOKEN_KW_YIELD,
MP_TOKEN_KW_ADD1,
...
} mp_token_kind_t;
ثم حرّر أيضاً py/lexer.c لإضافة النص الحرفي للكلمة المفتاحية الجديدة:
static const char *const tok_kw[] = {
...
"or",
"pass",
"raise",
"return",
"try",
"while",
"with",
"yield",
"add1",
...
};
لاحظ أن الكلمة المفتاحية تُسمّى بحسب ما تريده أن تكون. وللاتساق، حافظ على معيار التسمية وفقاً لذلك.
ملاحظة
يجب أن يطابق ترتيب هذه الكلمات المفتاحية في py/lexer.c ترتيب الوحدات المعجمية في التعداد المعرّف في py/lexer.h.
التحليل النحوي¶
في مرحلة التحليل النحوي، يأخذ المحلل النحوي الوحدات المعجمية التي ينتجها المحلل المعجمي ويحوّلها إلى شجرة نحوية مجردة (AST) أو شجرة تحليل. تنفيذ المحلل النحوي معرّف في py/parse.c.
يحتفظ المحلل النحوي أيضاً بجدول من الثوابت لاستخدامه في جوانب مختلفة من التحليل النحوي، على نحو مشابه لما يفعله جدول الرموز.
تُجرى خلال هذه المرحلة عدة تحسينات مثل طي الثوابت على الأعداد الصحيحة لمعظم العمليات، مثل المنطقية والثنائية والأحادية وغيرها، وتحسينات على الأقواس المحيطة بالتعبيرات، إلى جانب بعض التحسينات على السلاسل النصية.
تجدر الإشارة إلى أن سلاسل التوثيق (docstrings) يتم تجاهلها ولا يمكن للمُترجِم الوصول إليها. وحتى التحسينات مثل تجميع السلاسل النصية لا تُطبَّق على سلاسل التوثيق.
تمريرات المُترجِم¶
كما هو الحال في كثير من المُترجِمات، يترجم MicroPython كل الرمز إلى رمز MicroPython البايتي أو رمز أصلي. والوظيفة التي تحقق ذلك مُنفَّذة في py/compile.c. والطريقة الأكثر صلة التي ينبغي أن تعرفها هي هذه:
mp_obj_t mp_compile(mp_parse_tree_t *parse_tree, qstr source_file, bool is_repl) {
// Create a context for this module, and set its globals dict.
mp_module_context_t *context = m_new_obj(mp_module_context_t);
context->module.globals = mp_globals_get();
// Compile the input parse_tree to a raw-code structure.
mp_compiled_module_t cm;
cm.context = context;
mp_compile_to_raw_code(parse_tree, source_file, is_repl, &cm);
// Create and return a function object that executes the outer module.
return mp_make_function_from_proto_fun(cm.rc, cm.context, NULL);
}
يترجم المُترجِم الرمز عبر أربع تمريرات: النطاق، وحجم المكدس، وحجم الرمز، والتوليد. تُشغّل كل تمريرة رمز C نفسه على بنية بيانات AST نفسها، مع حساب أشياء مختلفة في كل مرة استناداً إلى نتائج التمريرة السابقة.
التمريرة الأولى¶
في التمريرة الأولى، يتعرّف المُترجِم على المعرّفات المعروفة (المتغيرات) ونطاقها، سواءً كانت عامة أو محلية أو مُغلَقاً عليها (closed over) أو غير ذلك. وفي التمريرة نفسها، يحسب المولّد (للرمز البايتي أو الرمز الأصلي) أيضاً عدد التسميات اللازمة للرمز المُولَّد.
// Compile pass 1.
comp->emit = emit_bc;
comp->emit_method_table = &emit_bc_method_table;
uint max_num_labels = 0;
for (scope_t *s = comp->scope_head; s != NULL && comp->compile_error == MP_OBJ_NULL; s = s->next) {
if (s->emit_options == MP_EMIT_OPT_ASM) {
compile_scope_inline_asm(comp, s, MP_PASS_SCOPE);
} else {
compile_scope(comp, s, MP_PASS_SCOPE);
// Check if any implicitly declared variables should be closed over.
for (size_t i = 0; i < s->id_info_len; ++i) {
id_info_t *id = &s->id_info[i];
if (id->kind == ID_INFO_KIND_GLOBAL_IMPLICIT) {
scope_check_to_close_over(s, id);
}
}
}
...
}
التمريرتان الثانية والثالثة¶
تتضمن التمريرتان الثانية والثالثة حساب حجم مكدس Python وحجم الرمز للرمز البايتي أو الرمز الأصلي. وبعد التمريرة الثالثة لا يمكن أن يتغير حجم الرمز، وإلا فستكون تسميات القفز غير صحيحة.
for (scope_t *s = comp->scope_head; s != NULL && comp->compile_error == MP_OBJ_NULL; s = s->next) {
...
// Pass 2: Compute the Python stack size.
compile_scope(comp, s, MP_PASS_STACK_SIZE);
// Pass 3: Compute the code size.
if (comp->compile_error == MP_OBJ_NULL) {
compile_scope(comp, s, MP_PASS_CODE_SIZE);
}
...
}
قُبيل التمريرة الثانية مباشرةً يجري اختيار نوع الرمز المراد توليده، والذي يمكن أن يكون إما أصلياً أو رمزاً بايتياً.
// Choose the emitter type.
switch (s->emit_options) {
case MP_EMIT_OPT_NATIVE_PYTHON:
case MP_EMIT_OPT_VIPER:
if (emit_native == NULL) {
emit_native = NATIVE_EMITTER(new)(&comp->compile_error, &comp->next_label, max_num_labels);
}
comp->emit_method_table = NATIVE_EMITTER_TABLE;
comp->emit = emit_native;
break;
default:
comp->emit = emit_bc;
comp->emit_method_table = &emit_bc_method_table;
break;
}
خيار الرمز البايتي هو الافتراضي، لكن ما يميز خيار الرمز الأصلي هو وجود خيار آخر عبر VIPER. راجع قسم توليد الرمز الأصلي لمزيد من التفاصيل حول تعليقات viper التوضيحية.
يوجد أيضاً دعم لـ رمز التجميع المضمّن (inline assembly code)، حيث تُكتب تعليمات التجميع كاستدعاءات لدوال Python لكنها تُولَّد مباشرةً كرمز الآلة المقابل. ولهذا المُجمِّع ثلاث تمريرات فقط (النطاق، وحجم الرمز، والتوليد) ويستخدم تنفيذاً مختلفاً، وليس الدالة compile_scope. راجع مرجع المُجمِّع المضمّن لمزيد من التفاصيل.
التمريرة الرابعة¶
تولّد التمريرة الرابعة الرمز النهائي القابل للتنفيذ، إما رمزاً بايتياً في الآلة الافتراضية، أو رمزاً أصلياً مباشرةً بواسطة وحدة المعالجة المركزية (CPU).
for (scope_t *s = comp->scope_head; s != NULL && comp->compile_error == MP_OBJ_NULL; s = s->next) {
...
// Pass 4: Emit the compiled bytecode or native code.
if (comp->compile_error == MP_OBJ_NULL) {
compile_scope(comp, s, MP_PASS_EMIT);
}
}
توليد الرمز البايتي¶
تقابِل العبارات في رمز Python عادةً رمزاً بايتياً مُولَّداً، فعلى سبيل المثال يولّد a + b "push a" ثم "push b" ثم "binary op add". وبعض العبارات لا تولّد شيئاً بل تؤثر بدلاً من ذلك في أشياء أخرى مثل نطاق المتغيرات، فعلى سبيل المثال global a.
يبدو تنفيذ دالة تولّد رمزاً بايتياً مشابهاً لهذا:
void mp_emit_bc_unary_op(emit_t *emit, mp_unary_op_t op) {
emit_write_bytecode_byte(emit, 0, MP_BC_UNARY_OP_MULTI + op);
}
نستخدم تعبيرات العامل الأحادي مثالاً هنا لكن تفاصيل التنفيذ مشابهة لسائر العبارات/التعبيرات. الطريقة emit_write_bytecode_byte() هي غلاف حول الدالة الرئيسية emit_get_cur_to_write_bytecode() التي يجب أن تستدعيها جميع الدوال لتوليد الرمز البايتي.
توليد الرمز الأصلي¶
على نحو مشابه لكيفية توليد الرمز البايتي، ينبغي أن تكون هناك دالة مقابلة في py/emitnative.c لكل عبارة رمزية:
static void emit_native_unary_op(emit_t *emit, mp_unary_op_t op) {
vtype_kind_t vtype;
emit_pre_pop_reg(emit, &vtype, REG_ARG_2);
if (vtype == VTYPE_PYOBJ) {
emit_call_with_imm_arg(emit, MP_F_UNARY_OP, op, REG_ARG_1);
emit_post_push_reg(emit, VTYPE_PYOBJ, REG_RET);
} else {
adjust_stack(emit, 1);
EMIT_NATIVE_VIPER_TYPE_ERROR(emit,
MP_ERROR_TEXT("unary op %q not implemented"), mp_unary_op_method_name[op]);
}
}
الفرق هنا هو أنه يتعين علينا التعامل مع تنميط viper (viper typing). تتيح لنا تعليقات viper التوضيحية التعامل مع أكثر من نوع واحد من المتغيرات. افتراضياً تكون جميع المتغيرات كائنات Python، لكن مع viper يمكن أيضاً الإعلان عن متغير كمتغير ذي نوع آلي مثل عدد صحيح أصلي أو مؤشر. ويمكن اعتبار viper مجموعةً فائقة من Python، حيث تُعالَج كائنات Python العادية كالمعتاد، بينما تُعالَج المتغيرات الآلية الأصلية بطريقة محسّنة باستخدام تعليمات الآلة المباشرة للعمليات. وقد يكسر تنميط viper التكافؤ مع Python لأن الأعداد الصحيحة، على سبيل المثال، تصبح أعداداً صحيحة أصلية ويمكن أن تطفح (overflow)، بخلاف أعداد Python الصحيحة التي تتسع تلقائياً إلى دقة عشوائية.