Prevodilac (kompajler)¶
Proces prevođenja u MicroPythonu obuhvaća sljedeće korake:
Leksički analizator pretvara tok teksta od kojeg se sastoji MicroPython program u tokene.
Parser zatim pretvara tokene u apstraktnu sintaksu (stablo parsiranja).
Zatim se na temelju stabla parsiranja generira bajtkod ili izvorni (native) kod.
Za potrebe ove rasprave dodat ćemo jednostavnu jezičnu značajku add1 koja se u Pythonu može koristiti ovako:
>>> add1 3
4
>>>
Naredba add1 uzima cijeli broj kao argument i dodaje mu 1.
Dodavanje gramatičkog pravila¶
Gramatika MicroPythona temelji se na CPython gramatici i definirana je u py/grammar.h. Ova se gramatika koristi za parsiranje MicroPython izvornih datoteka.
Postoje dva makroa koja trebate poznavati za definiranje gramatičkog pravila: DEF_RULE i DEF_RULE_NC. DEF_RULE vam omogućuje definiranje pravila s pridruženom funkcijom prevođenja, dok DEF_RULE_NC nema funkciju prevođenja (NC).
Jednostavna definicija gramatike s funkcijom prevođenja za našu novu naredbu add1 izgleda ovako:
DEF_RULE(add1_stmt, c(add1_stmt), and(2), tok(KW_ADD1), rule(testlist))
Drugi argument c(add1_stmt) je odgovarajuća funkcija prevođenja koja bi trebala biti implementirana u py/compile.c kako bi se ovo pravilo pretvorilo u izvršivi kod.
Treći obavezni argument može biti or ili and. To određuje broj čvorova pridruženih naredbi. Na primjer, u ovom slučaju, naša naredba add1 slična je ADD1 u asemblerskom jeziku. Ona uzima jedan brojčani argument. Stoga add1_stmt ima dva pridružena čvora. Jedan čvor je za samu naredbu, tj. literal add1 koji odgovara KW_ADD1, a drugi za njezin argument, pravilo testlist koje je izraz najviše razine.
Napomena
Pravilo add1 ovdje je samo primjer i nije dio standardne MicroPython gramatike.
Četvrti argument u ovom primjeru je token pridružen pravilu, KW_ADD1. Ovaj token treba biti definiran u leksičkom analizatoru uređivanjem py/lexer.h.
Definiranje istog pravila bez funkcije prevođenja postiže se korištenjem makroa DEF_RULE_NC i izostavljanjem argumenta funkcije prevođenja:
DEF_RULE_NC(add1_stmt, and(2), tok(KW_ADD1), rule(testlist))
Preostali argumenti zadržavaju isto značenje. Pravilo bez funkcije prevođenja moraju eksplicitno obraditi sva pravila koja mogu imati to pravilo kao čvor. Takva NC-pravila obično se koriste za izražavanje poddijelova složene gramatičke strukture koja se ne može izraziti u jednom pravilu.
Napomena
Makroi DEF_RULE i DEF_RULE_NC uzimaju i druge argumente. Za detaljnije razumijevanje podržanih parametara pogledajte py/grammar.h.
Dodavanje leksičkog tokena¶
Svako pravilo definirano u gramatici trebalo bi imati pridružen token koji je definiran u py/lexer.h. Dodajte ovaj token uređivanjem enuma _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;
Zatim uredite i py/lexer.c kako biste dodali tekst literala nove ključne riječi:
static const char *const tok_kw[] = {
...
"or",
"pass",
"raise",
"return",
"try",
"while",
"with",
"yield",
"add1",
...
};
Primijetite da je ključna riječ imenovana ovisno o tome kako želite da glasi. Radi dosljednosti, držite se standarda imenovanja u skladu s tim.
Napomena
Redoslijed ovih ključnih riječi u py/lexer.c mora odgovarati redoslijedu tokena u enumu definiranom u py/lexer.h.
Parsiranje¶
U fazi parsiranja parser uzima tokene koje proizvodi leksički analizator i pretvara ih u apstraktno sintaksno stablo (AST) ili stablo parsiranja. Implementacija parsera definirana je u py/parse.c.
Parser također održava tablicu konstanti za korištenje u različitim aspektima parsiranja, slično onome što radi tablica simbola.
Tijekom ove faze provodi se nekoliko optimizacija poput sažimanja konstanti na cijelim brojevima za većinu operacija, npr. logičke, binarne, unarne itd., te optimizacijska poboljšanja oko zagrada u izrazima, zajedno s nekim optimizacijama nad nizovima.
Vrijedi napomenuti da se docstringovi odbacuju i nisu dostupni prevodiocu. Čak se ni optimizacije poput internalizacije nizova ne primjenjuju na docstringove.
Prolazi prevodioca¶
Kao i mnogi prevodioci, MicroPython prevodi sav kod u MicroPython bajtkod ili izvorni kod. Funkcionalnost koja to postiže implementirana je u py/compile.c. Najvažnija metoda koju biste trebali poznavati je ova:
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);
}
Prevodilac prevodi kod u četiri prolaza: doseg (scope), veličina stoga, veličina koda i emitiranje. Svaki prolaz pokreće isti C kod nad istom AST strukturom podataka, pri čemu se svaki put izračunavaju različite stvari na temelju rezultata prethodnog prolaza.
Prvi prolaz¶
U prvom prolazu prevodilac uči o poznatim identifikatorima (varijablama) i njihovom dosegu, koji može biti globalni, lokalni, zatvoreni (closed over) itd. U istom prolazu emiter (bajtkoda ili izvornog koda) također izračunava broj oznaka potrebnih za emitirani kod.
// 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);
}
}
}
...
}
Drugi i treći prolaz¶
Drugi i treći prolaz uključuju izračunavanje veličine Python stoga i veličine koda za bajtkod ili izvorni kod. Nakon trećeg prolaza veličina koda se ne može mijenjati, inače će oznake skokova biti netočne.
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);
}
...
}
Neposredno prije drugog prolaza odabire se vrsta koda koji će se emitirati, a to može biti izvorni kod ili bajtkod.
// 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;
}
Bajtkod je zadana opcija, no jedinstveno je za opciju izvornog koda da postoji još jedna opcija putem VIPER. Pogledajte odjeljak Emitiranje izvornog koda za više pojedinosti o viper anotacijama.
Postoji i podrška za ugrađeni asemblerski kod (inline assembly), gdje se asemblerske instrukcije pišu kao pozivi Python funkcija, ali se emitiraju izravno kao odgovarajući strojni kod. Ovaj asembler ima samo tri prolaza (doseg, veličina koda, emitiranje) i koristi drugačiju implementaciju, ne funkciju compile_scope. Pogledajte referencu za ugrađeni asembler za više pojedinosti.
Četvrti prolaz¶
Četvrti prolaz emitira konačni kod koji se može izvršiti, bilo bajtkod u virtualnom stroju, bilo izvorni kod izravno od strane CPU-a.
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);
}
}
Emitiranje bajtkoda¶
Naredbe u Python kodu obično odgovaraju emitiranom bajtkodu, na primjer a + b generira „push a”, zatim „push b”, zatim „binary op add”. Neke naredbe ne emitiraju ništa, već umjesto toga utječu na druge stvari poput dosega varijabli, na primjer global a.
Implementacija funkcije koja emitira bajtkod izgleda otprilike ovako:
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);
}
Ovdje za primjer koristimo izraze s unarnim operatorom, ali pojedinosti implementacije slične su za druge naredbe/izraze. Metoda emit_write_bytecode_byte() omotač je oko glavne funkcije emit_get_cur_to_write_bytecode() koju sve funkcije moraju pozvati za emitiranje bajtkoda.
Emitiranje izvornog koda¶
Slično kao što se generira bajtkod, za svaku naredbu koda trebala bi postojati odgovarajuća funkcija u 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]);
}
}
Razlika je ovdje u tome što moramo obraditi viper tipizaciju. Viper anotacije omogućuju nam obradu više od jedne vrste varijable. Prema zadanim postavkama sve su varijable Python objekti, ali uz viper varijabla se može deklarirati i kao varijabla strojnog tipa, poput izvornog cijelog broja ili pokazivača. Viper se može shvatiti kao nadskup Pythona, gdje se uobičajeni Python objekti obrađuju kao i inače, dok se izvorne strojne varijable obrađuju na optimiziran način korištenjem izravnih strojnih instrukcija za operacije. Viper tipizacija može narušiti ekvivalentnost s Pythonom jer, na primjer, cijeli brojevi postaju izvorni cijeli brojevi i mogu se preliti (za razliku od Python cijelih brojeva koji se automatski proširuju na proizvoljnu preciznost).