22日目: break, next, redoに対応する
前回はretryの書き換えをしたところで、whileやuntilの対応をまだしていなかったことに気がついたので、それらloopの対応をしたのでした。 今回はloopやブロックに対する制御構文であるbreak, next, redoに取り組みます。
parse.yの書き換え
break, next, redoに対応するノードの書き換え自体はさほど難しくないのですが、問題はそれらの管理方法にあります。
Rubyではbreak, next, redoを書くことができる場所と、できない場所が存在します。
10.times do break end # OK while true break end # OK break #=> Invalid break (SyntaxError)
parse.yではこれらのinvalidなbreakなどをリスト構造で管理しています。

実際にそれぞれのキーワードに対応する生成規則をみると、add_block_exitという関数を呼び出していることがわかります。
この関数は特定の条件下でstruct parser_paramsにあるexitsに、breakやnextのノードをappendしています。
| keyword_break
{
$$ = add_block_exit(p, NEW_BREAK(0, &@$, &@1));
}
| keyword_next
{
$$ = add_block_exit(p, NEW_NEXT(0, &@$, &@1));
}
| keyword_redo
{
$$ = add_block_exit(p, NEW_REDO(&@$, &@1));
}
static NODE *
add_block_exit(struct parser_params *p, NODE *node)
{
if (!node) {
compile_error(p, "unexpected null node");
return 0;
}
switch (nd_type(node)) {
case NODE_BREAK: case NODE_NEXT: case NODE_REDO: break;
default:
compile_error(p, "add_block_exit: unexpected node: %s", parser_node_name(nd_type(node)));
return node;
}
if (!p->ctxt.in_defined) {
rb_node_exits_t *exits = p->exits;
if (exits) {
RNODE_EXITS(exits->nd_stts)->nd_chain = node;
exits->nd_stts = node;
}
}
return node;
}
p->exitsはinit_block_exit関数の呼び出しによって初期化され、allow_block_exit関数が呼び出されると0が代入されます。
そしてclear_block_exit関数はp->exitsにbreakなどのノードがappendされているかチェックして、それらのノードが存在する場合にはsyntax errorにします。
static void clear_block_exit(struct parser_params *p, bool error) { rb_node_exits_t *exits = p->exits; if (!exits) return; if (error) { for (NODE *e = RNODE(exits); (e = RNODE_EXITS(e)->nd_chain) != 0; ) { switch (nd_type(e)) { case NODE_BREAK: yyerror1(&e->nd_loc, "Invalid break"); break; case NODE_NEXT: yyerror1(&e->nd_loc, "Invalid next"); break; case NODE_REDO: yyerror1(&e->nd_loc, "Invalid redo"); break; default: yyerror1(&e->nd_loc, "unexpected node"); goto end_checks; /* no nd_chain */ } } end_checks:; } exits->nd_stts = RNODE(exits); exits->nd_chain = 0; }
NODE_EXITSの構造を変える
書き換え前はNODE_EXITS, NODE_BREAK, NODE_NEXT, NODE_REDOはどれも同じ構造をしていて、いずれのノードにもnd_chainというフィールドがありました。
そのためこれらのノードをつかってリスト構造をつくることができたわけです。
typedef struct RNode_EXITS { NODE node; struct RNode *nd_chain; struct RNode *nd_stts; rb_code_location_t keyword_loc; } rb_node_exits_t, rb_node_break_t, rb_node_next_t, rb_node_redo_t;
しかし書き換え後のNODE_BREAK, NODE_NEXT, NODE_REDOにはnd_chainに相当するフィールドはありません。
そこでNODE_EXITSの構造体を変更して、NODE_EXITSだけでリストを作るようにしましょう。

構造が決まれば書き換えは自明なので割愛します。
書き換え後のノードは問題なさそうです。
# @ CallNode (location: (1,0)-(1,7))* # +-- receiver: # | @ CallNode (location: (1,0)-(1,1)) # | +-- receiver: nil # | +-- name: :i # | +-- arguments: nil # | +-- block: nil # +-- name: :times # +-- arguments: nil # +-- block: # @ BlockNode (location: (1,10)-(2,7)) # +-- locals: [] # +-- parameters: nil # +-- body: # | @ BeginNode (location: (2,2)-(2,7)) # | +-- statements: # | | @ StatementsNode (location: (2,2)-(2,7)) # | | +-- body: (length: 1) # | | +-- @ BreakNode (location: (2,2)-(2,7))* # | | +-- arguments: nil # | +-- rescue_clause: nil # | +-- else_clause: nil # | +-- ensure_clause: nil i.times do break end # @ WhileNode (location: (5,0)-(7,3))* # +-- predicate: # | @ TrueNode (location: (5,6)-(5,10)) # +-- statements: # @ StatementsNode (location: (6,2)-(6,6)) # +-- body: (length: 1) # +-- @ NextNode (location: (6,2)-(6,6))* # +-- arguments: nil while true next end
バイトコードを確認する
break, next, redoのコンパイルでは考慮すべきことが大きく2つあります。
whileの中にいるのか、ブロックの中にいるのか、それ以外かensureとrescueの有無
breakを例にそれぞれ確認しておきましょう。
以下のコードから分かるように、while内のbreakはjump命令に、ブロック内のbreakはthrow命令にコンパイルされます。
# 0000 jump 4 ( 17)[Li] # 0002 putnil # 0003 pop # 0004 putself ( 18)[Li] # 0005 putobject :a # 0007 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0009 pop # 0010 putnil ( 19)[Li] # 0011 jump 22 # 0013 putself ( 20)[Li] # 0014 putobject :b # 0016 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0018 pop # 0019 jump 4 ( 17) # 0021 putnil # 0022 pop # 0023 putself ( 23)[Li] # 0024 putobject :c # 0026 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0028 leave while true p :a break p :b end p :c
# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(7,4)> # == catch table # | catch type: break st: 0000 ed: 0005 sp: 0000 cont: 0005 # | == disasm: #<ISeq:block in <main>@test.rb:1 (1,9)-(5,3)> # | == catch table # | | catch type: redo st: 0001 ed: 0016 sp: 0000 cont: 0001 # | | catch type: next st: 0001 ed: 0016 sp: 0000 cont: 0016 # | |------------------------------------------------------------------------ # | 0000 nop ( 1)[Bc] # | 0001 putself ( 2)[Li] # | 0002 putobject :c # | 0004 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # | 0006 pop # | 0007 putnil ( 3)[Li] # | 0008 throw 2 # | 0010 pop # | 0011 putself ( 4)[Li] # | 0012 putobject :d # | 0014 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # | 0016 leave ( 5)[Br] # |------------------------------------------------------------------------ # 0000 putobject 10 ( 1)[Li] # 0002 send <calldata!mid:times, argc:0>, block in <main> # 0005 pop # 0006 putself ( 7)[Li] # 0007 putobject :e # 0009 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0011 leave 10.times do p :c break p :d end p :e
次に、ensureやrescueが関係するケースをみておきます。
ensureがある場合には例外が発生しなかった場合にもensureを実行する必要があるため、p :aを実行したあとにp :ensureを実行してからjump(break)するというバイトコードが生成されます(0011から0016を参照)。
# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(11,4)> # == catch table # | catch type: ensure st: 0004 ed: 0011 sp: 0000 cont: 0031 # | == disasm: #<ISeq:ensure in <main>@test.rb:7 (7,4)-(7,13)> # | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) # | [ 1] "$!"@0 # | 0000 putself ( 7)[Li] # | 0001 putobject :ensure # | 0003 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # | 0005 pop # | 0006 getlocal_WC_0 "$!"@0 # | 0008 throw 0 # | catch type: ensure st: 0017 ed: 0025 sp: 0000 cont: 0031 # | catch type: break st: 0004 ed: 0034 sp: 0000 cont: 0034 # | catch type: next st: 0004 ed: 0034 sp: 0000 cont: 0003 # | catch type: redo st: 0004 ed: 0034 sp: 0000 cont: 0004 # |------------------------------------------------------------------------ # 0000 jump 4 ( 1)[Li] # 0002 putnil # 0003 pop # 0004 putself ( 3)[Li] # 0005 putobject :a # 0007 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0009 pop # 0010 putnil ( 4)[Li] # 0011 putself ( 7)[Li] # 0012 putobject :ensure # 0014 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0016 pop # 0017 jump 34 ( 4) # 0019 putself ( 5)[Li] # 0020 putobject :b # 0022 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0024 pop # 0025 putself ( 7)[Li] # 0026 putobject :ensure # 0028 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0030 pop # 0031 jump 4 ( 1) # 0033 putnil # 0034 pop # 0035 putself ( 11)[Li] # 0036 putobject :c # 0038 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0040 leave while true begin p :a break p :b ensure p :ensure end end p :c
一方でensureとrescueがある場合にはthrow(0011)をするバイトコードが生成されます。
# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(13,4)> # == catch table # | catch type: rescue st: 0004 ed: 0019 sp: 0000 cont: 0020 # | == disasm: #<ISeq:rescue in <main>@test.rb:6 (6,2)-(7,13)> # | 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 ( 7) # | 0002 putobject StandardError # | 0004 checkmatch 3 # | 0006 branchunless 14 # | 0008 putself [LiRs] # | 0009 putobject :rescue # | 0011 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # | 0013 leave # | 0014 getlocal_WC_0 "$!"@0 # | 0016 throw 0 # | catch type: retry st: 0019 ed: 0020 sp: 0000 cont: 0004 # | catch type: ensure st: 0004 ed: 0021 sp: 0000 cont: 0027 # | == disasm: #<ISeq:ensure in <main>@test.rb:9 (9,4)-(9,13)> # | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) # | [ 1] "$!"@0 # | 0000 putself ( 9)[Li] # | 0001 putobject :ensure # | 0003 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # | 0005 pop # | 0006 getlocal_WC_0 "$!"@0 # | 0008 throw 0 # | catch type: break st: 0004 ed: 0030 sp: 0000 cont: 0030 # | catch type: next st: 0004 ed: 0030 sp: 0000 cont: 0003 # | catch type: redo st: 0004 ed: 0030 sp: 0000 cont: 0004 # |------------------------------------------------------------------------ # 0000 jump 4 ( 1)[Li] # 0002 putnil # 0003 pop # 0004 putself ( 3)[Li] # 0005 putobject :a # 0007 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0009 pop # 0010 putnil ( 4)[Li] # 0011 throw 32770 # 0013 pop # 0014 putself ( 5)[Li] # 0015 putobject :b # 0017 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0019 nop ( 6) # 0020 pop # 0021 putself ( 9)[Li] # 0022 putobject :ensure # 0024 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0026 pop # 0027 jump 4 ( 1) # 0029 putnil # 0030 pop # 0031 putself ( 13)[Li] # 0032 putobject :c # 0034 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0036 leave while true begin p :a break p :b rescue p :rescue ensure p :ensure end end p :c
以上をふまえてcompile_break関数を眺めてみます。
static int compile_break(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { const NODE *line_node = node; unsigned long throw_flag = 0; if (ISEQ_COMPILE_DATA(iseq)->redo_label != 0 && can_add_ensure_iseq(iseq)) { /* while/until */ LABEL *splabel = NEW_LABEL(0); ADD_LABEL(ret, splabel); ADD_ADJUST(ret, line_node, ISEQ_COMPILE_DATA(iseq)->redo_label); CHECK(COMPILE_(ret, "break val (while/until)", RNODE_BREAK(node)->nd_stts, ISEQ_COMPILE_DATA(iseq)->loopval_popped)); add_ensure_iseq(ret, iseq, 0); ADD_INSNL(ret, line_node, jump, ISEQ_COMPILE_DATA(iseq)->end_label); ADD_ADJUST_RESTORE(ret, splabel); if (!popped) { ADD_INSN(ret, line_node, putnil); } } else { const rb_iseq_t *ip = iseq; while (ip) { if (!ISEQ_COMPILE_DATA(ip)) { ip = 0; break; } if (ISEQ_COMPILE_DATA(ip)->redo_label != 0) { throw_flag = VM_THROW_NO_ESCAPE_FLAG; } else if (ISEQ_BODY(ip)->type == ISEQ_TYPE_BLOCK) { throw_flag = 0; } else if (ISEQ_BODY(ip)->type == ISEQ_TYPE_EVAL) { COMPILE_ERROR(ERROR_ARGS "Can't escape from eval with break"); return COMPILE_NG; } else { ip = ISEQ_BODY(ip)->parent_iseq; continue; } /* escape from block */ CHECK(COMPILE(ret, "break val (block)", RNODE_BREAK(node)->nd_stts)); ADD_INSN1(ret, line_node, throw, INT2FIX(throw_flag | TAG_BREAK)); if (popped) { ADD_INSN(ret, line_node, pop); } return COMPILE_OK; } COMPILE_ERROR(ERROR_ARGS "Invalid break"); return COMPILE_NG; } return COMPILE_OK; }
whileのコンパイル中かどうかはISEQ_COMPILE_DATA(iseq)->redo_labelをチェックすることでわかります。
またブロックのコンパイル中かどうかは、それにあわせてISEQ_COMPILE_DATA(iseq)->end_labelをチェックすることで判別可能です1。
ensureとrescueについてはcan_add_ensure_iseq関数でチェックしています。
static bool can_add_ensure_iseq(const rb_iseq_t *iseq) { struct iseq_compile_data_ensure_node_stack *e; if (ISEQ_COMPILE_DATA(iseq)->in_rescue && (e = ISEQ_COMPILE_DATA(iseq)->ensure_node_stack) != NULL) { while (e) { if (e->ensure_node) return false; e = e->prev; } } return true; }
ensure_node_stackはwhileのコンパイル時とensureをもつstatementsのコンパイル時にpush_ensure_entry関数が呼び出されることで設定されますが、whileの場合はensure_nodeがNULL、ensureの場合はノードがセットされます。
なのでensure_nodeを見ればensureかどうか判別できます。
またrescueがある場合、begin ... rescueまでの部分をコンパイルする際にはin_rescueがtrueになっていますので、この値をチェックすることでrescueの有無がわかります。
compile.cを書き換える
while内部かブロック内部か、rescueやensureがあるのかに応じたバイトコードの書き分けは必要ですが、ノードの変更に伴う修正はほとんど必要ありません。
たとえばcompile_break関数であればbreak ...の...の部分のコンパイルにさいしてアクセスするフィールドが変わるのに対応するだけです。
@@ -9082,7 +9082,7 @@ compile_break(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, i LABEL *splabel = NEW_LABEL(0); ADD_LABEL(ret, splabel); ADD_ADJUST(ret, line_node, ISEQ_COMPILE_DATA(iseq)->redo_label); - CHECK(COMPILE_(ret, "break val (while/until)", RNODE_BREAK(node)->nd_stts, + CHECK(COMPILE_(ret, "break val (while/until)", RNODE(RB_NODE_BREAK(node)->arguments), ISEQ_COMPILE_DATA(iseq)->loopval_popped)); add_ensure_iseq(ret, iseq, 0); ADD_INSNL(ret, line_node, jump, ISEQ_COMPILE_DATA(iseq)->end_label); @@ -9117,7 +9117,7 @@ compile_break(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, i } /* escape from block */ - CHECK(COMPILE(ret, "break val (block)", RNODE_BREAK(node)->nd_stts)); + CHECK(COMPILE(ret, "break val (block)", RNODE(RB_NODE_BREAK(node)->arguments))); ADD_INSN1(ret, line_node, throw, INT2FIX(throw_flag | TAG_BREAK)); if (popped) { ADD_INSN(ret, line_node, pop);
compile_nextにも同様の修正を入れます2。
minirubyをbuildしてバイトコードが問題なく生成されていることを確認します。
while true p :a break p :b end num.times do p :c break p :d end == disasm: #<ISeq:<main>@../../test.rb:1 (1,0)-(11,3)> # == catch table # | catch type: break st: 0004 ed: 0022 sp: 0000 cont: 0022 # | catch type: next st: 0004 ed: 0022 sp: 0000 cont: 0003 # | catch type: redo st: 0004 ed: 0022 sp: 0000 cont: 0004 # | catch type: break st: 0023 ed: 0029 sp: 0000 cont: 0029 # | == disasm: #<ISeq:block in <main>@../../test.rb:7 (7,12)-(10,6)> # | == catch table # | | catch type: redo st: 0001 ed: 0016 sp: 0000 cont: 0001 # | | catch type: next st: 0001 ed: 0016 sp: 0000 cont: 0016 # | |------------------------------------------------------------------------ # | 0000 nop ( 7)[Bc] # | 0001 putself ( 8)[Li] # | 0002 putobject :c # | 0004 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # | 0006 pop # | 0007 putnil ( 9)[Li] # | 0008 throw 2 # | 0010 pop # | 0011 putself ( 10)[Li] # | 0012 putobject :d # | 0014 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # | 0016 leave [Br] # |------------------------------------------------------------------------ # 0000 jump 4 ( 1)[Li] # 0002 putnil # 0003 pop # 0004 putself ( 2)[Li] # 0005 putobject :a # 0007 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0009 pop # 0010 putnil ( 3)[Li] # 0011 jump 22 # 0013 putself ( 4)[Li] # 0014 putobject :b # 0016 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0018 pop # 0019 jump 4 ( 1) # 0021 putnil # 0022 pop # 0023 putself ( 7)[Li] # 0024 opt_send_without_block <calldata!mid:num, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0026 send <calldata!mid:times, argc:0>, block in <main> # 0029 leave
まとめ
今日の成果です。
- break, next, redoに対応した
次回はsuperとyieldの対応をしたいと思います。