Kompiler¶
Proses kompilasi dalam MicroPython melibatkan langkah-langkah berikut:
Lexer mengubah aliran teks yang membentuk program MicroPython menjadi token.
Parser kemudian mengubah token menjadi sintaks abstrak (pohon parse).
Kemudian bytecode atau kode native dipancarkan berdasarkan pohon parse.
Untuk keperluan diskusi ini, kita akan menambahkan fitur bahasa sederhana add1 yang dapat digunakan dalam Python sebagai:
>>> add1 3
4
>>>
Pernyataan add1 mengambil bilangan bulat sebagai argumen dan menambahkan 1 ke dalamnya.
Menambahkan aturan tata bahasa¶
Tata bahasa MicroPython didasarkan pada tata bahasa CPython dan didefinisikan dalam py/grammar.h. Tata bahasa ini yang digunakan untuk mengurai file sumber MicroPython.
Ada dua makro yang perlu Anda ketahui untuk mendefinisikan aturan tata bahasa: DEF_RULE dan DEF_RULE_NC. DEF_RULE memungkinkan Anda mendefinisikan aturan dengan fungsi kompilasi yang terkait, sementara DEF_RULE_NC tidak memiliki fungsi kompilasi (NC) untuk itu.
Definisi tata bahasa sederhana dengan fungsi kompilasi untuk pernyataan add1 baru kita terlihat seperti berikut:
DEF_RULE(add1_stmt, c(add1_stmt), and(2), tok(KW_ADD1), rule(testlist))
Argumen kedua c(add1_stmt) adalah fungsi kompilasi yang sesuai yang harus diimplementasikan dalam py/compile.c untuk mengubah aturan ini menjadi kode yang dapat dieksekusi.
Argumen ketiga yang diperlukan dapat berupa or atau and. Ini menentukan jumlah simpul yang terkait dengan suatu pernyataan. Misalnya, dalam kasus ini, pernyataan add1 kita mirip dengan ADD1 dalam bahasa assembly. Ia mengambil satu argumen numerik. Oleh karena itu, add1_stmt memiliki dua simpul yang terkait dengannya. Satu simpul untuk pernyataan itu sendiri, yaitu literal add1 yang sesuai dengan KW_ADD1, dan yang lainnya untuk argumennya, sebuah aturan testlist yang merupakan aturan ekspresi tingkat atas.
Catatan
Aturan add1 di sini hanyalah contoh dan bukan bagian dari tata bahasa standar MicroPython.
Argumen keempat dalam contoh ini adalah token yang terkait dengan aturan, KW_ADD1. Token ini harus didefinisikan dalam lexer dengan mengedit py/lexer.h.
Mendefinisikan aturan yang sama tanpa fungsi kompilasi dapat dilakukan dengan menggunakan makro DEF_RULE_NC dan menghilangkan argumen fungsi kompilasi:
DEF_RULE_NC(add1_stmt, and(2), tok(KW_ADD1), rule(testlist))
Argumen-argumen yang tersisa memiliki arti yang sama. Aturan tanpa fungsi kompilasi harus ditangani secara eksplisit oleh semua aturan yang mungkin memiliki aturan ini sebagai simpul. Aturan NC semacam itu biasanya digunakan untuk mengekspresikan sub-bagian dari struktur tata bahasa yang rumit yang tidak dapat diekspresikan dalam satu aturan.
Catatan
Makro DEF_RULE dan DEF_RULE_NC mengambil argumen lainnya. Untuk pemahaman mendalam tentang parameter yang didukung, lihat py/grammar.h.
Menambahkan token leksikal¶
Setiap aturan yang didefinisikan dalam tata bahasa harus memiliki token yang terkait dengannya yang didefinisikan dalam py/lexer.h. Tambahkan token ini dengan mengedit 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;
Kemudian edit juga py/lexer.c untuk menambahkan teks literal kata kunci baru:
static const char *const tok_kw[] = {
...
"or",
"pass",
"raise",
"return",
"try",
"while",
"with",
"yield",
"add1",
...
};
Perhatikan bahwa kata kunci diberi nama tergantung pada apa yang Anda inginkan. Untuk konsistensi, pertahankan standar penamaan sesuai.
Catatan
Urutan kata kunci ini dalam py/lexer.c harus sesuai dengan urutan token dalam enum yang didefinisikan dalam py/lexer.h.
Parsing¶
Pada tahap parsing, parser mengambil token yang dihasilkan oleh lexer dan mengubahnya menjadi pohon sintaks abstrak (AST) atau pohon parse. Implementasi untuk parser didefinisikan dalam py/parse.c.
Parser juga mempertahankan tabel konstanta untuk digunakan dalam berbagai aspek parsing, mirip dengan apa yang dilakukan oleh tabel simbol.
Beberapa optimasi seperti constant folding pada bilangan bulat untuk sebagian besar operasi misalnya logis, biner, unari, dll, dan peningkatan optimasi pada tanda kurung di sekitar ekspresi dilakukan selama fase ini, bersama dengan beberapa optimasi pada string.
Perlu dicatat bahwa docstring dibuang dan tidak dapat diakses oleh kompiler. Bahkan optimasi seperti string interning tidak diterapkan pada docstring.
Pass kompiler¶
Seperti banyak kompiler, MicroPython mengkompilasi semua kode menjadi bytecode MicroPython atau kode native. Fungsionalitas yang mencapai ini diimplementasikan dalam py/compile.c. Metode yang paling relevan yang perlu Anda ketahui adalah:
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);
}
Kompiler mengkompilasi kode dalam empat pass: scope, ukuran stack, ukuran kode, dan emit. Setiap pass menjalankan kode C yang sama pada struktur data AST yang sama, dengan hal-hal yang berbeda dihitung setiap kali berdasarkan hasil pass sebelumnya.
Pass pertama¶
Pada pass pertama, kompiler mempelajari identifier (variabel) yang dikenal dan scope-nya, apakah global, lokal, closed over, dll. Dalam pass yang sama, emitter (bytecode atau kode native) juga menghitung jumlah label yang diperlukan untuk kode yang dipancarkan.
// 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);
}
}
}
...
}
Pass kedua dan ketiga¶
Pass kedua dan ketiga melibatkan penghitungan ukuran stack Python dan ukuran kode untuk bytecode atau kode native. Setelah pass ketiga, ukuran kode tidak dapat berubah, jika tidak, label jump akan salah.
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);
}
...
}
Tepat sebelum pass dua, ada pemilihan untuk jenis kode yang akan dipancarkan, yang bisa berupa native atau 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;
}
Opsi bytecode adalah default, tetapi sesuatu yang unik untuk dicatat untuk opsi kode native adalah bahwa ada opsi lain melalui VIPER. Lihat bagian Memancarkan kode native untuk detail lebih lanjut tentang anotasi viper.
Ada juga dukungan untuk kode assembly inline, di mana instruksi assembly ditulis sebagai pemanggilan fungsi Python tetapi dipancarkan langsung sebagai kode mesin yang sesuai. Assembler ini hanya memiliki tiga pass (scope, ukuran kode, emit) dan menggunakan implementasi yang berbeda, bukan fungsi compile_scope. Lihat referensi assembler inline untuk detail lebih lanjut.
Pass keempat¶
Pass keempat memancarkan kode akhir yang dapat dieksekusi, baik bytecode dalam mesin virtual, maupun kode native langsung oleh 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);
}
}
Memancarkan bytecode¶
Pernyataan dalam kode Python biasanya sesuai dengan bytecode yang dipancarkan, misalnya a + b menghasilkan "push a" kemudian "push b" kemudian "binary op add". Beberapa pernyataan tidak memancarkan apa pun tetapi sebaliknya mempengaruhi hal-hal lain seperti scope variabel, misalnya global a.
Implementasi fungsi yang memancarkan bytecode terlihat seperti ini:
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);
}
Kita menggunakan ekspresi operator unari sebagai contoh di sini tetapi detail implementasinya serupa untuk pernyataan/ekspresi lainnya. Metode emit_write_bytecode_byte() adalah pembungkus di sekitar fungsi utama emit_get_cur_to_write_bytecode() yang harus dipanggil oleh semua fungsi untuk memancarkan bytecode.
Memancarkan kode native¶
Mirip dengan cara bytecode dihasilkan, harus ada fungsi yang sesuai dalam py/emitnative.c untuk setiap pernyataan kode:
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]);
}
}
Perbedaannya di sini adalah bahwa kita harus menangani pengetikan viper. Anotasi viper memungkinkan kita menangani lebih dari satu jenis variabel. Secara default semua variabel adalah objek Python, tetapi dengan viper sebuah variabel juga dapat dideklarasikan sebagai variabel bertipe mesin seperti bilangan bulat native atau pointer. Viper dapat dianggap sebagai superset dari Python, di mana objek Python normal ditangani seperti biasa, sementara variabel mesin native ditangani dengan cara yang dioptimalkan dengan menggunakan instruksi mesin langsung untuk operasi. Pengetikan viper dapat merusak kesetaraan Python karena, misalnya, bilangan bulat menjadi bilangan bulat native dan dapat overflow (tidak seperti bilangan bulat Python yang secara otomatis diperluas ke presisi arbitrer).