המהדר¶
תהליך ההידור ב-MicroPython כולל את השלבים הבאים:
המנתח הלקסיקלי (lexer) ממיר את זרם הטקסט המרכיב תוכנית MicroPython לאסימונים (tokens).
המנתח התחבירי (parser) ממיר לאחר מכן את האסימונים לתחביר מופשט (עץ ניתוח).
לאחר מכן נפלט bytecode או קוד נייטיב על בסיס עץ הניתוח.
לצורך הדיון הזה נוסיף תכונת שפה פשוטה 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. הוסף אסימון זה על ידי עריכת ה-enum _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 חייב להתאים לסדר האסימונים ב-enum המוגדר ב-py/lexer.h.
ניתוח תחבירי¶
בשלב הניתוח התחבירי המנתח מקבל את האסימונים שמייצר המנתח הלקסיקלי וממיר אותם לעץ תחביר מופשט (AST) או עץ ניתוח. המימוש של המנתח התחבירי מוגדר בקובץ py/parse.c.
המנתח התחבירי גם מתחזק טבלה של קבועים לשימוש בהיבטים שונים של הניתוח, בדומה למה שעושה טבלת סמלים.
מספר אופטימיזציות כמו קיפול קבועים על מספרים שלמים עבור רוב הפעולות, למשל לוגיות, בינאריות, אונריות וכו«, ושיפורי אופטימיזציה סביב סוגריים מקיפים של ביטויים מבוצעים בשלב זה, יחד עם כמה אופטימיזציות על מחרוזות.
ראוי לציין כי מחרוזות תיעוד (docstrings) מושמטות ואינן נגישות למהדר. אפילו אופטימיזציות כמו איגום מחרוזות (string interning) אינן מיושמות על מחרוזות תיעוד.
מעברי המהדר¶
כמו מהדרים רבים, MicroPython מהדר את כל הקוד ל-bytecode של 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);
}
המהדר מהדר את הקוד בארבעה מעברים: scope (היקף), stack size (גודל מחסנית), code size (גודל קוד) ו-emit (פליטה). כל מעבר מריץ את אותו קוד C על אותו מבנה נתוני AST, כאשר דברים שונים מחושבים בכל פעם על בסיס תוצאות המעבר הקודם.
מעבר ראשון¶
במעבר הראשון, המהדר לומד על המזהים הידועים (משתנים) וההיקף שלהם, בין אם הוא גלובלי, מקומי, נסגר עליו (closed over) וכו«. באותו מעבר הפולט (bytecode או קוד נייטיב) מחשב גם את מספר התוויות הנדרשות עבור הקוד הנפלט.
// 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 וגודל הקוד עבור ה-bytecode או קוד הנייטיב. לאחר המעבר השלישי גודל הקוד אינו יכול להשתנות, אחרת תוויות הקפיצה יהיו שגויות.
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);
}
...
}
ממש לפני מעבר שתיים מתבצעת בחירה של סוג הקוד שייפלט, שיכול להיות נייטיב או bytecode.
// 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;
}
אפשרות ה-bytecode היא ברירת המחדל, אך דבר ייחודי שכדאי לציין לגבי אפשרות קוד הנייטיב הוא שקיימת אפשרות נוספת באמצעות VIPER. ראה את הסעיף פליטת קוד נייטיב לפרטים נוספים על הערות viper.
קיימת גם תמיכה ב-קוד אסמבלי מוטמע (inline assembly), שבו הוראות אסמבלי נכתבות כקריאות לפונקציות Python אך נפלטות ישירות כקוד המכונה המתאים. למַאַסֵמְבְּלֵר זה יש שלושה מעברים בלבד (scope, code size, emit) והוא משתמש במימוש שונה, לא בפונקציה compile_scope. ראה את עיון במאסמבלר המוטמע לפרטים נוספים.
מעבר רביעי¶
המעבר הרביעי פולט את הקוד הסופי שניתן להריץ, בין אם bytecode במכונה הווירטואלית, או קוד נייטיב ישירות על ידי ה-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);
}
}
פליטת bytecode¶
הוראות בקוד Python מתאימות בדרך כלל ל-bytecode נפלט, לדוגמה a + b מייצר ”push a“ ואז ”push b“ ואז ”binary op add“. חלק מההוראות אינן פולטות דבר אלא משפיעות על דברים אחרים כמו היקף המשתנים, לדוגמה global a.
המימוש של פונקציה הפולטת bytecode נראה דומה לזה:
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() שכל הפונקציות חייבות לקרוא לה כדי לפלוט bytecode.
פליטת קוד נייטיב¶
בדומה לאופן שבו מיוצר 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 מכיוון, לדוגמה, שמספרים שלמים הופכים למספרים שלמים נייטיביים ויכולים לגלוש (בניגוד למספרים שלמים של Python המתרחבים אוטומטית לדיוק שרירותי).