A fordító¶
A MicroPythonban a fordítási folyamat a következő lépésekből áll:
A lexer a MicroPython programot alkotó szövegfolyamot tokenekké alakítja.
A parser ezután a tokeneket absztrakt szintaxissá (elemzőfa) alakítja.
Ezt követően az elemzőfa alapján bájtkód vagy natív kód jön létre.
A jelen tárgyalás céljából egy egyszerű nyelvi elemet, az add1 utasítást fogjuk hozzáadni, amelyet a Pythonban így lehet használni:
>>> add1 3
4
>>>
Az add1 utasítás egy egészet vár argumentumként, és hozzáad 1-et.
Nyelvtani szabály hozzáadása¶
A MicroPython nyelvtana a CPython nyelvtanon alapul, és a py/grammar.h fájlban van definiálva. Ezt a nyelvtant használja a rendszer a MicroPython forrásfájlok elemzéséhez.
Egy nyelvtani szabály definiálásához két makrót kell ismerned: DEF_RULE és DEF_RULE_NC. A DEF_RULE lehetővé teszi egy szabály definiálását egy hozzá tartozó fordítófüggvénnyel, míg a DEF_RULE_NC esetén nincs fordító (NC) függvény.
Egy egyszerű nyelvtani definíció fordítófüggvénnyel az új add1 utasításunkhoz a következőképpen néz ki:
DEF_RULE(add1_stmt, c(add1_stmt), and(2), tok(KW_ADD1), rule(testlist))
A második argumentum, a c(add1_stmt), a megfelelő fordítófüggvény, amelyet a py/compile.c fájlban kell megvalósítani, hogy ezt a szabályt végrehajtható kóddá alakítsa.
A harmadik kötelező argumentum lehet or vagy and. Ez adja meg az utasításhoz tartozó csomópontok számát. Ebben az esetben például az add1 utasításunk hasonló az assembly nyelvi ADD1 utasításhoz. Egyetlen numerikus argumentumot vesz fel. Ezért az add1_stmt szabályhoz két csomópont tartozik. Az egyik csomópont magához az utasításhoz tartozik, azaz a KW_ADD1 tokennek megfelelő add1 literálhoz, a másik pedig az argumentumához, egy testlist szabályhoz, amely a legfelső szintű kifejezésszabály.
Megjegyzés
Az itt szereplő add1 szabály csak példa, és nem része a szabványos MicroPython nyelvtannak.
Ebben a példában a negyedik argumentum a szabályhoz tartozó token, a KW_ADD1. Ezt a tokent a lexerben kell definiálni a py/lexer.h szerkesztésével.
Ugyanezen szabály fordítófüggvény nélküli definiálását a DEF_RULE_NC makró használatával és a fordítófüggvény argumentumának elhagyásával érheted el:
DEF_RULE_NC(add1_stmt, and(2), tok(KW_ADD1), rule(testlist))
A többi argumentum jelentése ugyanaz marad. A fordítófüggvény nélküli szabályt explicit módon kell kezelnie minden szabálynak, amely ezt a szabályt csomópontként tartalmazhatja. Az ilyen NC-szabályokat általában egy bonyolult nyelvtani szerkezet olyan részeinek kifejezésére használják, amelyek egyetlen szabállyal nem fejezhetők ki.
Megjegyzés
A DEF_RULE és DEF_RULE_NC makrók más argumentumokat is fogadnak. A támogatott paraméterek mélyreható megértéséhez lásd: py/grammar.h.
Lexikai token hozzáadása¶
A nyelvtanban definiált minden szabályhoz tartoznia kell egy tokennek, amely a py/lexer.h fájlban van definiálva. Ezt a tokent a _mp_token_kind_t enum szerkesztésével add hozzá:
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;
Ezután a py/lexer.c fájlt is szerkeszd, hogy hozzáadd az új kulcsszó literális szövegét:
static const char *const tok_kw[] = {
...
"or",
"pass",
"raise",
"return",
"try",
"while",
"with",
"yield",
"add1",
...
};
Vedd észre, hogy a kulcsszó neve attól függ, hogy minek szeretnéd nevezni. A következetesség érdekében tartsd be a megfelelő elnevezési szabványt.
Megjegyzés
A py/lexer.c fájlban ezeknek a kulcsszavaknak a sorrendjének meg kell egyeznie a py/lexer.h fájlban definiált enumban szereplő tokenek sorrendjével.
Elemzés¶
Az elemzési szakaszban a parser a lexer által előállított tokeneket veszi, és absztrakt szintaxisfává (AST) vagy elemzőfává alakítja. A parser implementációja a py/parse.c fájlban van definiálva.
A parser egy konstanstáblát is karbantart az elemzés különböző aspektusaihoz, hasonlóan ahhoz, amit egy szimbólumtábla tesz.
Ebben a fázisban számos optimalizálás történik, mint például a konstans-összevonás egészeken a legtöbb művelet esetén, pl. logikai, bináris, unáris stb., valamint a kifejezések körüli zárójelekre vonatkozó optimalizáló fejlesztések, néhány karakterláncokra vonatkozó optimalizálással együtt.
Érdemes megjegyezni, hogy a docstring-eket eldobja a rendszer, és nem hozzáférhetők a fordító számára. Még az olyan optimalizálások, mint a karakterlánc-internálás sem alkalmazódnak a docstring-ekre.
Fordítási menetek¶
Sok fordítóhoz hasonlóan a MicroPython minden kódot MicroPython bájtkóddá vagy natív kóddá fordít. Az ezt megvalósító funkcionalitás a py/compile.c fájlban van implementálva. A legfontosabb metódus, amelyet ismerned kell, a következő:
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);
}
A fordító négy menetben fordítja le a kódot: hatókör, veremméret, kódméret és kibocsátás. Minden menet ugyanazt a C kódot futtatja le ugyanazon az AST adatstruktúrán, és minden alkalommal más dolgokat számít ki az előző menet eredményei alapján.
Első menet¶
Az első menetben a fordító megismeri az ismert azonosítókat (változókat) és azok hatókörét, legyen az globális, lokális, lezárt (closed over) stb. Ugyanebben a menetben a kibocsátó (bájtkód vagy natív kód) kiszámítja a kibocsátott kódhoz szükséges címkék számát is.
// 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);
}
}
}
...
}
Második és harmadik menet¶
A második és harmadik menet a Python veremméretének és a bájtkód vagy natív kód kódméretének kiszámítását foglalja magában. A harmadik menet után a kódméret nem változhat, különben az ugrási címkék hibásak lesznek.
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);
}
...
}
Közvetlenül a második menet előtt történik a kibocsátandó kód típusának kiválasztása, amely lehet natív vagy bájtkód.
// 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;
}
A bájtkód az alapértelmezett opció, de a natív kód opcióval kapcsolatban egyedi megjegyzendő dolog, hogy létezik egy másik opció a VIPER révén. A viper annotációkról bővebben lásd a Natív kód kibocsátása szakaszt.
Támogatott az inline assembly kód is, ahol az assembly utasításokat Python függvényhívásokként írjuk le, de közvetlenül a megfelelő gépi kódként bocsátódnak ki. Ennek az assemblernek csak három menete van (hatókör, kódméret, kibocsátás), és eltérő implementációt használ, nem a compile_scope függvényt. Bővebben lásd az inline assembler hivatkozást.
Negyedik menet¶
A negyedik menet bocsátja ki a végleges, végrehajtható kódot, amely vagy bájtkód a virtuális gépben, vagy natív kód közvetlenül a CPU számára.
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);
}
}
Bájtkód kibocsátása¶
A Python kódban szereplő utasítások általában megfelelnek a kibocsátott bájtkódnak, például az a + b a „push a”, majd a „push b”, majd a „binary op add” utasításokat generálja. Egyes utasítások nem bocsátanak ki semmit, hanem más dolgokat befolyásolnak, például a változók hatókörét, mint például a global a.
A bájtkódot kibocsátó függvény implementációja az alábbihoz hasonlóan néz ki:
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);
}
Itt példaként az unáris operátoros kifejezéseket használjuk, de az implementáció részletei más utasítások/kifejezések esetén hasonlóak. Az emit_write_bytecode_byte() metódus egy burkoló a fő emit_get_cur_to_write_bytecode() függvény körül, amelyet minden függvénynek meg kell hívnia a bájtkód kibocsátásához.
Natív kód kibocsátása¶
Ahhoz hasonlóan, ahogy a bájtkód generálódik, a py/emitnative.c fájlban kell lennie egy megfelelő függvénynek minden kódutasításhoz:
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]);
}
}
A különbség itt az, hogy kezelnünk kell a viper típusolást. A viper annotációk lehetővé teszik, hogy egynél több típusú változót kezeljünk. Alapértelmezés szerint minden változó Python objektum, de a viperrel egy változó deklarálható gépi típusú változóként is, például natív egészként vagy mutatóként. A viper a Python szuperhalmazaként fogható fel, ahol a normál Python objektumok a szokásos módon kezelődnek, míg a natív gépi változókat optimalizált módon kezeli, közvetlen gépi utasításokat használva a műveletekhez. A viper típusolás megtörheti a Python-ekvivalenciát, mivel például az egészek natív egészekké válnak, és túlcsordulhatnak (ellentétben a Python egészekkel, amelyek automatikusan tetszőleges pontosságúra bővülnek).