コンパイラ¶
MicroPython のコンパイルは以下の手順で行われます。
語彙解析器は MicroPython プログラムを構成するテキストのストリームをトークンに変換します。
次に構文解析器が、トークンを抽象的な構文(構文木)に変換します。
次に構文木に基づいてバイトコードまたはネイティブコードを出力します。
この説明のために、Python に単純な言語機能 add1
追加してみることにします。
>>> add1 3
4
>>>
add1
文は、引数として整数を受け取り、それに 1
を加算します。
文法規則の追加¶
MicroPython の文法は CPythonの文法 をベースにしていて、それは py/grammar.h に定義されています。この文法はMicroPython のソースファイルを構文解析するために使われます。
文法規則を定義するために知っておかなければならないマクロが2つあります: DEF_RULE
と DEF_RULE_NC
です。 DEF_RULE
はコンパイル関数を関連付けた規則を定義でき、DEF_RULE_NC
はそのためのコンパイル関数を定義しないものです。
新しい 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つは文そのもの、すなわちリテラル add1
に対応する KW_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))
残りの引数も同じ意味を持ちます。コンパイル関数のないルールは、この規則をノードとして持つ可能性のあるすべての規則において明示的に処理されなければなりません。このようなコンパイル関数の無い規則は、通常、単一の規則では表現できない複雑な文法構造の下位部分を表現するために用います。
注釈
マクロ 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
で定義した enum 中のトークンの順序と一致させなければなりません。
構文解析¶
構文解析のステージでは、構文解析器が語彙解析器で生成したトークンを受け取り、抽象構文木(AST: Abstract Syntax Tree)に変換します(抽象構文木は パースツリー と呼ぶこともあります)。構文解析の実装は 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つのパスでコードをコンパイルします。各パスは同じASTデータ構造に対して同じCコードを実行し、前のパスの結果に基づいて異なるものが算出されます。
第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 の整数と異なります)。