Derleyici¶
MicroPython’daki derleme süreci aşağıdaki adımları içerir:
Sözcüksel çözümleyici (lexer), bir MicroPython programını oluşturan metin akışını belirteçlere (token) dönüştürür.
Ardından ayrıştırıcı (parser), belirteçleri soyut bir söz dizimine (ayrıştırma ağacı) dönüştürür.
Daha sonra ayrıştırma ağacına dayalı olarak bytecode veya yerel (native) kod üretilir.
Bu tartışma kapsamında, Python’da şu şekilde kullanılabilecek basit bir dil özelliği olan add1 ekleyeceğiz:
>>> add1 3
4
>>>
add1 deyimi bir tam sayıyı argüman olarak alır ve ona 1 ekler.
Bir dil bilgisi kuralı ekleme¶
MicroPython’un dil bilgisi CPython dil bilgisine dayanır ve py/grammar.h içinde tanımlıdır. Bu dil bilgisi, MicroPython kaynak dosyalarını ayrıştırmak için kullanılan şeydir.
Bir dil bilgisi kuralı tanımlamak için bilmeniz gereken iki makro vardır: DEF_RULE ve DEF_RULE_NC. DEF_RULE, ilişkili bir derleme işlevine sahip bir kural tanımlamanıza olanak tanırken, DEF_RULE_NC bunun için derleme (NC) işlevine sahip değildir.
Yeni add1 deyimimiz için derleme işlevine sahip basit bir dil bilgisi tanımı aşağıdaki gibi görünür:
DEF_RULE(add1_stmt, c(add1_stmt), and(2), tok(KW_ADD1), rule(testlist))
İkinci argüman c(add1_stmt), bu kuralı çalıştırılabilir koda dönüştürmek için py/compile.c içinde uygulanması gereken karşılık gelen derleme işlevidir.
Gerekli üçüncü argüman or veya and olabilir. Bu, bir deyimle ilişkili düğüm sayısını belirtir. Örneğin, bu durumda add1 deyimimiz montaj dilindeki ADD1’e benzer. Sayısal bir argüman alır. Bu nedenle add1_stmt ile ilişkili iki düğüm vardır. Bir düğüm deyimin kendisi içindir, yani KW_ADD1‘e karşılık gelen add1 değişmezi, diğeri ise argümanı içindir; bu da üst düzey ifade kuralı olan bir testlist kuralıdır.
Not
Buradaki add1 kuralı yalnızca bir örnektir ve standart MicroPython dil bilgisinin bir parçası değildir.
Bu örnekteki dördüncü argüman, kuralla ilişkili belirteç olan KW_ADD1‘dir. Bu belirteç, py/lexer.h düzenlenerek sözcüksel çözümleyicide (lexer) tanımlanmalıdır.
Aynı kuralı derleme işlevi olmadan tanımlamak, DEF_RULE_NC makrosu kullanılarak ve derleme işlevi argümanı atlanarak elde edilir:
DEF_RULE_NC(add1_stmt, and(2), tok(KW_ADD1), rule(testlist))
Kalan argümanlar aynı anlamı taşır. Derleme işlevi olmayan bir kural, bu kurala bir düğüm olarak sahip olabilecek tüm kurallar tarafından açıkça ele alınmalıdır. Bu tür NC kuralları genellikle tek bir kuralda ifade edilemeyen karmaşık bir dil bilgisi yapısının alt parçalarını ifade etmek için kullanılır.
Not
DEF_RULE ve DEF_RULE_NC makroları başka argümanlar da alır. Desteklenen parametreleri derinlemesine anlamak için py/grammar.h dosyasına bakın.
Bir sözcüksel belirteç ekleme¶
Dil bilgisinde tanımlanan her kuralın, py/lexer.h içinde tanımlanan ilişkili bir belirteci olmalıdır. Bu belirteci, _mp_token_kind_t enum’unu düzenleyerek ekleyin:
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;
Ardından yeni anahtar sözcük değişmez metnini eklemek için py/lexer.c dosyasını da düzenleyin:
static const char *const tok_kw[] = {
...
"or",
"pass",
"raise",
"return",
"try",
"while",
"with",
"yield",
"add1",
...
};
Anahtar sözcüğün, olmasını istediğiniz şeye göre adlandırıldığına dikkat edin. Tutarlılık için adlandırma standardını buna göre koruyun.
Not
py/lexer.c içindeki bu anahtar sözcüklerin sırası, py/lexer.h içinde tanımlanan enum’daki belirteçlerin sırasıyla eşleşmelidir.
Ayrıştırma¶
Ayrıştırma aşamasında ayrıştırıcı (parser), sözcüksel çözümleyici (lexer) tarafından üretilen belirteçleri alır ve bunları soyut bir söz dizimi ağacına (AST) veya ayrıştırma ağacına dönüştürür. Ayrıştırıcının uygulaması py/parse.c içinde tanımlıdır.
Ayrıştırıcı ayrıca, bir sembol tablosunun yaptığına benzer şekilde, ayrıştırmanın farklı yönlerinde kullanılmak üzere bir sabitler tablosu da tutar.
Bu aşamada, çoğu işlem için tam sayılar üzerinde sabit katlama (constant folding) (örneğin mantıksal, ikili, tekli vb.) ve ifadelerin etrafındaki parantezler üzerinde iyileştirici geliştirmeler gibi çeşitli optimizasyonlar, dizeler üzerindeki bazı optimizasyonlarla birlikte gerçekleştirilir.
Docstring‘lerin atıldığını ve derleyici tarafından erişilemediğini belirtmekte fayda var. Docstring‘lere dize özdeşleştirme (string interning) gibi optimizasyonlar bile uygulanmaz.
Derleyici geçişleri¶
Birçok derleyici gibi MicroPython da tüm kodu MicroPython bytecode’una veya yerel (native) koda derler. Bunu gerçekleştiren işlevsellik py/compile.c içinde uygulanmıştır. Bilmeniz gereken en ilgili yöntem şudur:
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);
}
Derleyici kodu dört geçişte derler: kapsam (scope), yığın boyutu (stack size), kod boyutu (code size) ve üretim (emit). Her geçiş, aynı AST veri yapısı üzerinde aynı C kodunu çalıştırır ve bir önceki geçişin sonuçlarına göre her seferinde farklı şeyler hesaplanır.
İlk geçiş¶
İlk geçişte derleyici, bilinen tanımlayıcıları (değişkenleri) ve bunların kapsamını (global, yerel, kapanış üzerinden vb.) öğrenir. Aynı geçişte üretici (bytecode veya yerel kod) ayrıca üretilen kod için gereken etiket sayısını da hesaplar.
// 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);
}
}
}
...
}
İkinci ve üçüncü geçişler¶
İkinci ve üçüncü geçişler, bytecode veya yerel kod için Python yığın boyutunun ve kod boyutunun hesaplanmasını içerir. Üçüncü geçişten sonra kod boyutu değişemez, aksi takdirde atlama etiketleri yanlış olur.
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);
}
...
}
İkinci geçişten hemen önce, üretilecek kodun türü için bir seçim yapılır; bu, yerel kod veya bytecode olabilir.
// 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 seçeneği varsayılandır, ancak yerel kod seçeneği için belirtilmesi gereken benzersiz bir nokta, VIPER aracılığıyla başka bir seçeneğin bulunmasıdır. Viper açıklamaları hakkında daha fazla ayrıntı için Yerel kod üretme bölümüne bakın.
Ayrıca satır içi montaj kodu için de destek vardır; burada montaj komutları Python işlev çağrıları olarak yazılır ancak doğrudan karşılık gelen makine kodu olarak üretilir. Bu derleyicinin yalnızca üç geçişi vardır (kapsam, kod boyutu, üretim) ve compile_scope işlevini değil, farklı bir uygulamayı kullanır. Daha fazla ayrıntı için satır içi derleyici referansına bakın.
Dördüncü geçiş¶
Dördüncü geçiş, çalıştırılabilen nihai kodu üretir; bu, sanal makinedeki bytecode veya doğrudan CPU tarafından çalıştırılan yerel kod olabilir.
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 üretme¶
Python kodundaki deyimler genellikle üretilen bytecode’a karşılık gelir; örneğin a + b önce “push a”, ardından “push b”, ardından “binary op add” üretir. Bazı deyimler hiçbir şey üretmez, bunun yerine değişkenlerin kapsamı gibi başka şeyleri etkiler; örneğin global a.
Bytecode üreten bir işlevin uygulaması şuna benzer:
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);
}
Burada örnek olarak tekli operatör ifadelerini kullanıyoruz, ancak uygulama ayrıntıları diğer deyimler/ifadeler için benzerdir. emit_write_bytecode_byte() yöntemi, bytecode üretmek için tüm işlevlerin çağırması gereken ana işlev olan emit_get_cur_to_write_bytecode() etrafında bir sarmalayıcıdır.
Yerel kod üretme¶
Bytecode’un nasıl üretildiğine benzer şekilde, her kod deyimi için py/emitnative.c içinde karşılık gelen bir işlev bulunmalıdır:
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]);
}
}
Buradaki fark, viper tiplemesini ele almak zorunda olmamızdır. Viper açıklamaları, birden fazla değişken türünü ele almamıza olanak tanır. Varsayılan olarak tüm değişkenler Python nesneleridir, ancak viper ile bir değişken, yerel bir tam sayı veya işaretçi gibi makine tiplemeli bir değişken olarak da bildirilebilir. Viper, Python’un bir üst kümesi olarak düşünülebilir; burada normal Python nesneleri her zamanki gibi ele alınırken, yerel makine değişkenleri işlemler için doğrudan makine komutları kullanılarak optimize edilmiş bir şekilde ele alınır. Viper tiplemesi Python eşdeğerliğini bozabilir çünkü, örneğin, tam sayılar yerel tam sayılara dönüşür ve taşabilir (otomatik olarak keyfi hassasiyete genişleyen Python tam sayılarının aksine).