21日目: 制御構文に対応する
前回はbegin ... rescue ... endに対応したので、今回はretryをはじめとする制御構文を書き換えていきましょう。
retryに対応する
rertyは引数を取らないため、そのノードもシンプルです。
begin p :begin rescue p :rescue retry end # Before # @ NODE_RETRY (id: 6, line: 5, location: (5,2)-(5,7))* # After # RetryNode (location: (5,2)-(5,7))
parse.yの書き換えも難しい部分は特にありません。
@@ -5062,7 +5064,7 @@ primary : inline_primary case after_ensure: yyerror1(&@1, "Invalid retry after ensure"); break; } } - $$ = NEW_RETRY(&@$); + $$ = NEW_RB_RETRY(&@$); /*% ripper: retry! %*/ } ;
compile.cも既存のcompile_retry関数をそのまま使うことができます。
@@ -11615,6 +11616,10 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no break; } + case RB_RETRY_NODE: { + CHECK(compile_retry(iseq, ret, node, popped)); + break; + } case RB_BEGIN_NODE: { CHECK(compile_begin(iseq, ret, node, popped)); break;
compile_retry関数では現在のISeqがrescueに対応するISeqであることを確認して、putnilとthrow命令を生成するだけです。
static int compile_retry(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { const NODE *line_node = node; if (ISEQ_BODY(iseq)->type == ISEQ_TYPE_RESCUE) { ADD_INSN(ret, line_node, putnil); ADD_INSN1(ret, line_node, throw, INT2FIX(TAG_RETRY)); if (popped) { ADD_INSN(ret, line_node, pop); } } else { COMPILE_ERROR(ERROR_ARGS "Invalid retry"); return COMPILE_NG; } return COMPILE_OK; }
コンパイル結果もよさそうです。
begin p :begin rescue p :rescue retry end # == catch table # | catch type: rescue st: 0000 ed: 0005 sp: 0000 cont: 0006 # | == disasm: #<ISeq:rescue in <main>@../../test.rb:3 (3,0)-(5,7)> # | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) # | [ 1] "$!"@0 # | 0000 getlocal_WC_0 "$!"@0 ( 3) # | 0002 putobject StandardError # | 0004 checkmatch 3 # | 0006 branchunless 18 # | 0008 putself ( 4)[LiRs] # | 0009 putobject :rescue # | 0011 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # | 0013 pop # | 0014 putnil ( 5)[Li] # | 0015 throw 4 # | 0017 leave ( 3) # | 0018 getlocal_WC_0 "$!"@0 # | 0020 throw 0
nextに対応する
次にnextに対応していきましょう。
ところでnextはwhileの中とブロックの中のときでコンパイル結果が変わりますが、そもそもまだwhileやuntilをサポートしていないことに今気がつきました。
whileとuntilに対応する
ということで、先にwhileとuntilを書き換えましょう。
whileには以下のように3つの異なる書き方があります。
後置whileかつbegin ... endのケースでは最初に1度begin ... endを実行してからloopに入ります。
while false p :while1 end p :while2 while false begin p :while3 end while false #=> :while3
書き換え前後のノードは以下の通りです。
while false p :while1 end # @ NODE_WHILE (id: 4, line: 1, location: (1,0)-(3,3))* # +- nd_state: 1 (while-end) # +- nd_cond: # +- nd_body: # +-- @ WhileNode (location: (1,0)-(3,3)) # | +-- LoopFlags: nil # | +-- predicate: # | +-- statements: p :while2 while false # @ NODE_WHILE (id: 9, line: 5, location: (5,0)-(5,21))* # +- nd_state: 1 (while-end) # +- nd_cond: # +- nd_body: # +-- @ WhileNode (location: (5,0)-(5,21)) # | +-- LoopFlags: nil # | +-- predicate: # | +-- statements: begin p :while3 end while false # @ NODE_WHILE (id: 17, line: 7, location: (7,0)-(9,15))* # +- nd_state: 0 (begin-end-while) # +- nd_cond: # +- nd_body: # +-- @ WhileNode (location: (7,0)-(9,15)) # | +-- LoopFlags: begin_modifier # | +-- predicate: # | +-- statements:
後置rescueの場合には専用のノードを割り当てていたのと対照的に、書き換え後も後置whileかどうかに関わらず常にWhileNodeを使うようです。
その結果、本来1つのstatemetしかとれない後置whileの場合でも、StatementsNodeでラップする必要があることに注意します。
また3番目のケースのwhileであるかどうかは、begin_modifierというフラグで判別するようです。
parse.yでは使用するマクロを書き換えることで対応できます。
@@ -3692,22 +3700,22 @@ stmt : keyword_alias fitem {SET_LEX_STATE(EXPR_FNAME|EXPR_FITEM);} fitem | stmt modifier_while expr_value { clear_block_exit(p, false); - if ($1 && nd_type_p($1, NODE_BEGIN)) { - $$ = NEW_WHILE(cond(p, $3, &@3), RNODE_BEGIN($1)->nd_body, 0, &@$, &@2, &NULL_LOC); + if ($1 && nd_type_p($1, RB_BEGIN_NODE)) { + $$ = NEW_RB_WHILE(cond(p, $3, &@3), NEW_RB_STATEMENTS($1, &@1), RB_LOOP_FLAGS_BEGIN_MODIFIER, &@$, &@2, &NULL_LOC); } else { - $$ = NEW_WHILE(cond(p, $3, &@3), $1, 1, &@$, &@2, &NULL_LOC); + $$ = NEW_RB_WHILE(cond(p, $3, &@3), NEW_RB_STATEMENTS($1, &@1), 0, &@$, &@2, &NULL_LOC); } /*% ripper: while_mod!($:3, $:1) %*/
whileとuntilをコンパイルする
まずは生成されるバイトコードをみてみましょう。
while cond p :while1 end # == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(3,3)> # whileの条件にjumpする # 0000 jump 12 ( 1)[Li] # 0002 putnil # 0003 pop # 0004 jump 12 # whileのbody # 0006 putself ( 2)[Li] # 0007 putobject :while1 # 0009 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0011 pop # whileの条件をチェックして次のloopに入る、もしくはloopを抜ける # 0012 putself ( 1) # 0013 opt_send_without_block <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0015 branchif 6 # while loopより後の部分 # 0017 putnil # 0018 leave ( 2)

bodyの部分のバイトコードのあとにloopを継続するかどうかをチェックするバイトコードが置かれていて、一番最初は継続のチェックをするバイトコードまでjumpしていることがわかります。
begin ... end while condの場合は、最初のjump先が条件式の部分(0012)ではなくbodyの部分(0006)になります。
begin p :while3 end while cond # == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(3,14)> # 0000 jump 6 ( 1)[Li] # 0002 putnil # 0003 pop # 0004 jump 12 # 0006 putself ( 2)[Li] # 0007 putobject :while3 # 0009 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0011 pop # 0012 putself ( 3) # 0013 opt_send_without_block <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0015 branchif 6 # 0017 putnil ( 1) # 0018 leave ( 2)
whileにしろuntilにしろ、それらのノードのコンパイルはcompile_loop関数が行っています。
書き換え自体は構造体の変化に合わせてマクロを変える程度なので、ここでは書き換え前の関数を眺めるに留めます。
static int compile_loop(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped, const enum node_type type) { const int line = (int)nd_line(node); const NODE *line_node = node; LABEL *prev_start_label = ISEQ_COMPILE_DATA(iseq)->start_label; LABEL *prev_end_label = ISEQ_COMPILE_DATA(iseq)->end_label; LABEL *prev_redo_label = ISEQ_COMPILE_DATA(iseq)->redo_label; int prev_loopval_popped = ISEQ_COMPILE_DATA(iseq)->loopval_popped; VALUE branches = Qfalse; struct iseq_compile_data_ensure_node_stack enl; // 各種labelを設定する LABEL *next_label = ISEQ_COMPILE_DATA(iseq)->start_label = NEW_LABEL(line); /* next */ LABEL *redo_label = ISEQ_COMPILE_DATA(iseq)->redo_label = NEW_LABEL(line); /* redo */ LABEL *break_label = ISEQ_COMPILE_DATA(iseq)->end_label = NEW_LABEL(line); /* break */ LABEL *end_label = NEW_LABEL(line); LABEL *adjust_label = NEW_LABEL(line); LABEL *next_catch_label = NEW_LABEL(line); LABEL *tmp_label = NULL; ISEQ_COMPILE_DATA(iseq)->loopval_popped = 0; // while自体のコンパイルには影響しないのでまた今度 push_ensure_entry(iseq, &enl, NULL, NULL); // `begin ... end while`かどうかで最初のjump先を変えている if (RNODE_WHILE(node)->nd_state == 1) { ADD_INSNL(ret, line_node, jump, next_label); } else { tmp_label = NEW_LABEL(line); ADD_INSNL(ret, line_node, jump, tmp_label); } ADD_LABEL(ret, adjust_label); ADD_INSN(ret, line_node, putnil); ADD_LABEL(ret, next_catch_label); ADD_INSN(ret, line_node, pop); ADD_INSNL(ret, line_node, jump, next_label); if (tmp_label) ADD_LABEL(ret, tmp_label); ADD_LABEL(ret, redo_label); branches = decl_branch_base(iseq, PTR2NUM(node), nd_code_loc(node), type == NODE_WHILE ? "while" : "until"); const NODE *const coverage_node = RNODE_WHILE(node)->nd_body ? RNODE_WHILE(node)->nd_body : node; add_trace_branch_coverage( iseq, ret, nd_code_loc(coverage_node), nd_node_id(coverage_node), 0, "body", branches); // body部分をコンパイルする CHECK(COMPILE_POPPED(ret, "while body", RNODE_WHILE(node)->nd_body)); ADD_LABEL(ret, next_label); /* next */ // condition部分をコンパイルする // whileとuntilで条件がtrueのときのjump先が逆になる if (type == NODE_WHILE) { CHECK(compile_branch_condition(iseq, ret, RNODE_WHILE(node)->nd_cond, redo_label, end_label)); } else { /* until */ CHECK(compile_branch_condition(iseq, ret, RNODE_WHILE(node)->nd_cond, end_label, redo_label)); } ADD_LABEL(ret, end_label); ADD_ADJUST_RESTORE(ret, adjust_label); // 書き換え後はflagの有無になるのでこの分岐は消える if (UNDEF_P(RNODE_WHILE(node)->nd_state)) { /* ADD_INSN(ret, line_node, putundef); */ COMPILE_ERROR(ERROR_ARGS "unsupported: putundef"); return COMPILE_NG; } else { ADD_INSN(ret, line_node, putnil); } ADD_LABEL(ret, break_label); /* break */ if (popped) { ADD_INSN(ret, line_node, pop); } // break, next, redo用のcatch tableを生成する ADD_CATCH_ENTRY(CATCH_TYPE_BREAK, redo_label, break_label, NULL, break_label); ADD_CATCH_ENTRY(CATCH_TYPE_NEXT, redo_label, break_label, NULL, next_catch_label); ADD_CATCH_ENTRY(CATCH_TYPE_REDO, redo_label, break_label, NULL, ISEQ_COMPILE_DATA(iseq)->redo_label); // 各種labelを関数呼び出し前の状態にもどす ISEQ_COMPILE_DATA(iseq)->start_label = prev_start_label; ISEQ_COMPILE_DATA(iseq)->end_label = prev_end_label; ISEQ_COMPILE_DATA(iseq)->redo_label = prev_redo_label; ISEQ_COMPILE_DATA(iseq)->loopval_popped = prev_loopval_popped; ISEQ_COMPILE_DATA(iseq)->ensure_node_stack = ISEQ_COMPILE_DATA(iseq)->ensure_node_stack->prev; return COMPILE_OK; }
基本的には生成されるバイトコードの順に沿って、bodyとconditionをコンパイルしています。
難しいのはISEQ_COMPILE_DATA(iseq)のセットアップやpush_ensure_entryの部分ですが、これはnextなどをコンパイルするときに使用するので、そのときがきたら詳しくみることにします。
compile.c書き換え後のバイトコードを確認します。
while cond p :while1 end # 0000 jump 12 ( 1)[Li] # 0002 putnil # 0003 pop # 0004 jump 12 # 0006 putself ( 2)[Li] # 0007 putobject :while1 # 0009 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0011 pop # 0012 putself ( 1) # 0013 opt_send_without_block <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0015 branchif 6 # 0017 putnil # 0018 pop begin p :while3 end while cond # 0019 jump 25 ( 5)[Li] # 0021 putnil # 0022 pop # 0023 jump 31 # 0025 putself ( 6)[Li] # 0026 putobject :while3 # 0028 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0030 pop # 0031 putself ( 7) # 0032 opt_send_without_block <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0034 branchif 25 # 0036 putnil ( 5) # 0037 leave ( 6)
良さそうですね。
まとめ
今日の成果です。
retryをコンパイルできるようにした- whileとuntilを書き換えてないことに気がついたので、その対応をした
次回こそnext, break, redoに取り組みたいと思います。