コンパイラ¶
MicroPythonにおけるコンパイル処理は、次の手順で構成されます。
字句解析器(lexer)は、MicroPythonプログラムを構成するテキストストリームをトークンへ変換します。
次に構文解析器(parser)が、トークンを抽象構文木(解析木)へ変換します。
そして解析木に基づいて、バイトコードまたはネイティブコードが生成されます。
ここでの説明のために、Pythonで次のように使用できる単純な言語機能 add1 を追加してみます。
>>> add1 3
4
>>>
add1 文は整数を引数として受け取り、それに 1 を加算します。
文法規則の追加¶
MicroPythonの文法は CPythonの文法 をベースにしており、py/grammar.h で定義されています。この文法は、MicroPythonのソースファイルを解析するために使用されます。
文法規則を定義するために知っておくべきマクロは2つあります。DEF_RULE と DEF_RULE_NC です。DEF_RULE ではコンパイル関数を関連付けて規則を定義でき、DEF_RULE_NC にはコンパイル関数(NC: no compile)がありません。
新しい add1 文に対するコンパイル関数付きの単純な文法定義は、次のようになります。
DEF_RULE(add1_stmt, c(add1_stmt), and(2), tok(KW_ADD1), rule(testlist))
2番目の引数 c(add1_stmt) は、この規則を実行可能なコードに変換するために py/compile.c に実装すべき、対応するコンパイル関数です。
3番目の必須引数には or または and を指定できます。これは文に関連付けられるノードの数を指定します。例えばこの場合、add1 文はアセンブリ言語のADD1に似ています。これは1つの数値引数を取ります。したがって add1_stmt には2つのノードが関連付けられます。1つのノードは文そのもの、すなわち KW_ADD1 に対応するリテラル add1 のためのもので、もう1つはその引数、つまりトップレベルの式規則である testlist 規則のためのものです。
注釈
ここでの add1 規則はあくまで一例であり、標準のMicroPython文法の一部ではありません。
この例の4番目の引数は、規則に関連付けられたトークン 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 で定義されたトークンが関連付けられている必要があります。_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 におけるこれらのキーワードの順序は、py/lexer.h で定義された列挙型のトークンの順序と一致している必要があります。
構文解析¶
構文解析の段階では、構文解析器が字句解析器によって生成されたトークンを受け取り、それらを抽象構文木(AST)すなわち 解析木 へ変換します。構文解析器の実装は py/parse.c で定義されています。
構文解析器はまた、シンボルテーブル が行うのと同様に、構文解析のさまざまな場面で使用される定数のテーブルを保持します。
この段階では、論理演算、二項演算、単項演算などほとんどの演算における整数に対する 定数畳み込み や、式を囲む括弧に対する最適化的な強化など、いくつかの最適化が実行されるほか、文字列に対するいくつかの最適化も行われます。
docstring は破棄され、コンパイラからアクセスできなくなる点に注目してください。文字列インターン化 のような最適化さえも、docstring には適用されません。
コンパイラのパス¶
多くのコンパイラと同様に、MicroPythonはすべてのコードを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);
}
コンパイラはコードを4つのパスでコンパイルします。スコープ、スタックサイズ、コードサイズ、そして生成(emit)です。各パスは同じCコードを同じASTデータ構造に対して実行し、前のパスの結果に基づいて毎回異なるものを計算します。
第1パス¶
第1パスでは、コンパイラは既知の識別子(変数)と、それがグローバル、ローカル、クロージャによる包含などのいずれであるかというスコープについて学習します。同じパスで、生成器(バイトコードまたはネイティブコード)は生成されるコードに必要なラベルの数も計算します。
// 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);
}
}
}
...
}
第2パスと第3パス¶
第2パスと第3パスでは、バイトコードまたはネイティブコードのためのPythonスタックサイズとコードサイズを計算します。第3パスの後はコードサイズを変更できません。そうでないとジャンプラベルが不正になるためです。
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);
}
...
}
第2パスの直前に、生成するコードの種類の選択が行われ、ネイティブまたはバイトコードのいずれかを選べます。
// 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;
}
バイトコードオプションがデフォルトですが、ネイティブコードオプションについて特筆すべき点は、VIPER を介した別のオプションがあることです。viperアノテーションの詳細については、ネイティブコードの生成 のセクションを参照してください。
また インラインアセンブリコード もサポートされており、アセンブリ命令をPython関数呼び出しとして記述しますが、対応するマシンコードとして直接生成されます。このアセンブラはパスが3つ(スコープ、コードサイズ、生成)しかなく、compile_scope 関数ではなく別の実装を使用します。詳細については インラインアセンブラのリファレンス を参照してください。
第4パス¶
第4パスでは、実行可能な最終コードを生成します。これは仮想マシン上のバイトコード、または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);
}
}
バイトコードの生成¶
Pythonコード内の文は通常、生成されるバイトコードに対応します。例えば a + b は「push a」、次に「push b」、次に「binary op add」を生成します。一部の文は何も生成せず、代わりに変数のスコープのような他のものに影響を与えます。例えば global a です。
バイトコードを生成する関数の実装は、次のようになります。
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() のラッパーです。
ネイティブコードの生成¶
バイトコードが生成されるのと同様に、各コード文に対応する関数が 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アノテーションにより、複数の型の変数を扱うことができます。デフォルトではすべての変数はPythonオブジェクトですが、viperを使うと変数をネイティブ整数やポインタのようなマシン型変数として宣言することもできます。viperはPythonのスーパーセットと考えることができ、通常のPythonオブジェクトは通常どおり扱われる一方で、ネイティブのマシン変数は演算に直接マシン命令を使用することで最適化された方法で扱われます。viper型付けはPythonとの等価性を損なう可能性があります。例えば整数がネイティブ整数になりオーバーフローし得るためです(自動的に任意精度に拡張されるPythonの整数とは異なります)。