compile.cの変更
Python/compile.cを探る
compile.c
の改良を試みるため、まずはcompile.c
の中身を探っていく。VSCodeの検索機能を用いてbreak
を探すと、3024行に次のようなcompiler_break
が見つかる。
Python/compile.c: L3024
static int compiler_break(struct compiler *c) { struct fblockinfo *loop = NULL; /* Emit instruction with line number */ ADDOP(c, NOP); if (!compiler_unwind_fblock_stack(c, 0, &loop)) { return 0; } if (loop == NULL) { return compiler_error(c, "'break' outside loop"); } if (!compiler_unwind_fblock(c, loop, 0)) { return 0; } ADDOP_JUMP(c, JUMP_ABSOLUTE, loop->fb_exit); NEXT_BLOCK(c); return 1; }
これは明らかにbreak
の動作を決めている関数である。中身を見ていくと、
compiler_unwind_fblock_stack
という関数があり、ここで何らかの処理をしているcompiler_error
のエラー文を見ると、ループが存在しない場合のエラー処理をしているcompiler_unwind_fblock
で何らかの処理をしているADDOP_JUMP
でbreak
の動作をするジャンプ命令のバイトコードを生成NEXT_BLOCK
で次のブロックに移動
だと読み取れる。また、compiler_break
で検索すると、compiler_visit_stmt
という関数内で、3566行に、
Python/compile.c: compiler_visit_stmt L3566
case Break_kind: return compiler_break(c);
という場所があるので、compiler_break
と似た関数を作る場合は、ここも忘れずに同様な記述をする必要がある。
compiler_break
だけでは動作がよく分からないので、compiler_unwind_fblock_stack
の中を見ていく。
Python/compile.c: L1871
/** Unwind block stack. If loop is not NULL, then stop when the first loop is encountered. */ static int compiler_unwind_fblock_stack(struct compiler *c, int preserve_tos, struct fblockinfo **loop) { if (c->u->u_nfblocks == 0) { return 1; } struct fblockinfo *top = &c->u->u_fblock[c->u->u_nfblocks-1]; if (loop != NULL && (top->fb_type == WHILE_LOOP || top->fb_type == FOR_LOOP)) { *loop = top; return 1; } struct fblockinfo copy = *top; c->u->u_nfblocks--; if (!compiler_unwind_fblock(c, ©, preserve_tos)) { return 0; } if (!compiler_unwind_fblock_stack(c, preserve_tos, loop)) { return 0; } c->u->u_fblock[c->u->u_nfblocks] = copy; c->u->u_nfblocks++; return 1; }
中身を見ていくと、
c->u->u_nfblocks
はもっとも外側のframe block(for
while
try
など)から数えたframe blockの数c->u->u_fblock
はframe blockを、もっとも外側をc->u->u_fblock[0]
, 現在地(break
の場合はbreak
がある位置)をc->u->u_fblock[c->u->u_nfblocks-1]
として積んでいる配列top
に現在のframe blockの情報を渡すtop
を参照し、for
やwhile
ならばloop
にtop
を渡し関数を出る(loop
に渡されたものを抜ける処理がbreak
である)for
やwhile
でなかった場合、c->u->u_nfblocks
を1下げることで、一つ外側のframe blockについてcompiler_unwind_fblock
とcompiler_unwind_fblock_stack
をもう一度実行している(for
やwhile
でなければbreak
で抜けることができないので、より外側のframe blockを探しにいくイメージ)
といった構造になっている。ここから、多重ループを抜け出すbreak
を実現するには、このcompiler_unwind_fblock_stack
をうまく改造することが中心の作業となることが予想される。特に、loop
に渡すものをうまく制御できれば、我々が実装したい複数ループを抜けるbreak
を実装できると思われる。
次に、compiler_unwind_fblock
を見ていく。(話の流れ的に書いたけど、長いし変更するわけでもないからいらないかも?)
Python/compile.c: L1769
/* Unwind a frame block. If preserve_tos is true, the TOS before * popping the blocks will be restored afterwards, unless another * return, break or continue is found. In which case, the TOS will * be popped. */ static int compiler_unwind_fblock(struct compiler *c, struct fblockinfo *info, int preserve_tos) { switch (info->fb_type) { case WHILE_LOOP: case EXCEPTION_HANDLER: case ASYNC_COMPREHENSION_GENERATOR: return 1; case FOR_LOOP: /* Pop the iterator */ if (preserve_tos) { ADDOP(c, ROT_TWO); } ADDOP(c, POP_TOP); return 1; case TRY_EXCEPT: ADDOP(c, POP_BLOCK); return 1; case FINALLY_TRY: /* This POP_BLOCK gets the line number of the unwinding statement */ ADDOP(c, POP_BLOCK); if (preserve_tos) { if (!compiler_push_fblock(c, POP_VALUE, NULL, NULL, NULL)) { return 0; } } /* Emit the finally block */ VISIT_SEQ(c, stmt, info->fb_datum); if (preserve_tos) { compiler_pop_fblock(c, POP_VALUE, NULL); } /* The finally block should appear to execute after the * statement causing the unwinding, so make the unwinding * instruction artificial */ c->u->u_lineno = -1; return 1; case FINALLY_END: if (preserve_tos) { ADDOP(c, ROT_FOUR); } ADDOP(c, POP_TOP); ADDOP(c, POP_TOP); ADDOP(c, POP_TOP); if (preserve_tos) { ADDOP(c, ROT_FOUR); } ADDOP(c, POP_EXCEPT); return 1; case WITH: case ASYNC_WITH: SET_LOC(c, (stmt_ty)info->fb_datum); ADDOP(c, POP_BLOCK); if (preserve_tos) { ADDOP(c, ROT_TWO); } if(!compiler_call_exit_with_nones(c)) { return 0; } if (info->fb_type == ASYNC_WITH) { ADDOP(c, GET_AWAITABLE); ADDOP_LOAD_CONST(c, Py_None); ADDOP(c, YIELD_FROM); } ADDOP(c, POP_TOP); /* The exit block should appear to execute after the * statement causing the unwinding, so make the unwinding * instruction artificial */ c->u->u_lineno = -1; return 1; case HANDLER_CLEANUP: if (info->fb_datum) { ADDOP(c, POP_BLOCK); } if (preserve_tos) { ADDOP(c, ROT_FOUR); } ADDOP(c, POP_EXCEPT); if (info->fb_datum) { ADDOP_LOAD_CONST(c, Py_None); compiler_nameop(c, info->fb_datum, Store); compiler_nameop(c, info->fb_datum, Del); } return 1; case POP_VALUE: if (preserve_tos) { ADDOP(c, ROT_TWO); } ADDOP(c, POP_TOP); return 1; } Py_UNREACHABLE(); }
これは、frame blockにはfor
やwhile
のほかに、try
, with
などいろいろなものが入っているので、それぞれに対して別々の処理を行なっている。今回私たちが実装するのはbreak
の挙動に関するものなので、for
などのframe block自体をいじる必要はなく、ここは変更を加える必要はないと判断した。
compile.cの変更
これからcompile.c
に変更、追加をしていく。実装の方向性は、
- 例えば
break 3
のような形で、抜け出したいループの数を受け取る(この場合は3つのループを抜ける) - その引数の数だけ
compiler_unwind_fblock_stack
と同様の処理を実行する - 引数が0の場合は、全てのループを抜ける
- 引数がない(
break
のみ)の場合は、通常のbreak
として動作する
といったイメージである。まずはcompiler_unwind_fblock_stack
を参考にして作成したcompiler_unwind_fblock_stack_count
を次に示す。 compiler_unwind_fblock_stack
のすぐ下に書き加えれば良い。(compiler_unwind_fblock_stack
は必要なので、手を加えてはならない)
static int compiler_unwind_fblock_stack_count(struct compiler *c, int count, struct fblockinfo **loop) { if (c->u->u_nfblocks == 0 || count <= 0) { return 1; } struct fblockinfo *top = &c->u->u_fblock[c->u->u_nfblocks-1]; if (loop != NULL && (top->fb_type == WHILE_LOOP || top->fb_type == FOR_LOOP)) { count--; if (count <= 0){ *loop = top; return 1; } } struct fblockinfo copy = *top; c->u->u_nfblocks--; if (!compiler_unwind_fblock(c, ©, 0)) { return 0; } if (!compiler_unwind_fblock_stack_count(c, count, loop)) { return 0; } c->u->u_fblock[c->u->u_nfblocks] = copy; c->u->u_nfblocks++; return 1; }
関数の内容について、compiler_unwind_fblock_stack
との違いを中心に説明する。
- 引数が
int preserve_tos
が消えint count
を追加
これは、break
を実行する際にはpreserve_tos
の値が常に0であったので、わざわざ引数にする必要はないと判断したため削除した。count
は「ループを抜ける数」を表す引数である。
- 最初の
if
文の条件に|| count<= 0
を追加
これは、元々あったc->u->u_nfblocks == 0
が「そもそもループが存在しない」ことを表しており、loop
をNULL
のまま関数を終えることでcompiler_break
のエラー処理に誘導するためのif
文である。よってcount
が0以下(抜けたいループの数が0以下)も、エラー処理に誘導するためif
分の条件に追加した。
- 2つ目の
if
文の中で、count
を減らし、0以下になったらloop
にtop
を渡し抜ける
元々はtop
が示すものがwhile
やfor
などループを表すものであれば、すぐにloop
にtop
を渡し関数を抜けていた。1つのループを抜けるならそれで良いが、複数ループを抜けるならloop
に渡さずスルーする必要がある。そのため、top
がloop
やwhile
ならばcount
を1減らし、count
の値が0でなければスルー、0であればその時のtop
が抜けたいループなので、loop
に渡して関数を抜けるように変更した。
- 再帰呼び出しの部分を
compiler_unwind_fblock_stack_count
へ変更
再帰にするための変更である。再帰にすることで、count
を減らしながら目的のループを探すことができる。
以上の変更によって、compiler_unwind_fblock_stack_count
を実行すれば、loop
が抜け出したいループを示すようになる。
ここで、break
の引数が0の場合は全てのループを抜ける処理にしたい。そのためのcompiler_unwind_fblock_stack_all
を別に作る。それを以下に示す。
static int compiler_unwind_fblock_stack_all(struct compiler *c, struct fblockinfo **loop) { if (c->u->u_nfblocks == 0) { return 1; } int count = 0; for (int i = 0; i < (c->u->u_nfblocks); ++i){ if (c->u->u_fblock[i].fb_type == WHILE_LOOP || c->u->u_fblock[i].fb_type == FOR_LOOP){ count++; } } return compiler_unwind_fblock_stack_count(c, count, loop); }
これは、c->u->u_fblock
の中で、while
とfor
の数を数え上げ、それをcompiler_unwind_fblock_stack_count
のcount
として渡している。こうすることで、全てのループを抜ける処理を可能としている。
つぎに、このcompiler_unwind_fblock_stack_count
を用いたcompiler_breaknew
を以下に示す。compiler_break
を消す必要はなく、その直後に書き加えれば良い。
static int compiler_breaknew(struct compiler *c, stmt_ty s){ struct fblockinfo *loop = NULL; ADDOP(c, NOP); int count = PyLong_AS_LONG(s->v.Breaknew.value->v.Constant.value); if (count == 0) { if (!compiler_unwind_fblock_stack_all(c, &loop)) { return 0; } } else { if (!compiler_unwind_fblock_stack_count(c, count, &loop)) { return 0; } } if (loop == NULL) { return compiler_error(c, "'break' not properly in loop"); } if (!compiler_unwind_fblock(c, loop, 0)) { return 0; } ADDOP_JUMP(c, JUMP_ABSOLUTE, loop->fb_exit); NEXT_BLOCK(c); return 1; }
関数の内容の説明を上から順にしていく。
int count = PyLong_AS_LONG(s->v.Breaknew.value->v.Constant.value);
の追加
これは、s->v.Breaknew.value->v.Constant.value
が、例えばbreak 3
とした時の3のような引数を表しており、PyLong_AS_LONG
で特殊な形の整数に変換している。要するにbreak
の引数をcount
として受け取っている。
count
の値による処理の違い
count
が0の時は、全てのループを抜ける処理をするため、compiler_unwind_fblock_stack_all
を呼び足している。0でない時は、compiler_unwind_fblock_stack_count
を呼び出して、指定した回数だけループを抜けるようにする。
- 以上の処理の結果、
loop == NULL
のままの場合のエラー処理
compile_break
の時は、「ループが存在しない」場合にこのエラーを用いたため、"'break' outside loop"
というエラー文であったが、compile_breaknew
の場合は、「break
の引数が適切でない(負、ループの数より多いなど)」の場合にもこのエラーを用いることになるので、より広い意味を表すように"'break' not properly in loop"
という表現にしている。
以降はcompiler_break
と同じ処理である。
最後に、compiler_visit_stmt
内のcase Break_kind
の後に以下のcase Breaknew_kind
を追加する。わかりやすくするため、case Break_kind
の部分も書いておく。(上にあげた変更を行なっていれば、おそらく3638行あたりに追加することになる)
case Break_kind: return compiler_break(c); case Breaknew_kind: return compiler_breaknew(c, s);
compiler_breaknew
には引数がc
だけでなくs
もあることに気をつける。
以上で、compile.c
に関する変更が完了した。