9日目: ブロック引数をコンパイルする
通常の引数、キーワード引数に対応したので、残りはブロックとブロック引数です。
Rubyでは2つの書き方でメソッドにブロックを渡すことができます。
obj.m(&blk)とobj.m do expr endです。
対応するバイトコードやコンパイラの処理はそれぞれ異なるので、1つずつみていきましょう。
&blkに対応する
&blk形式の場合のバイトコードは他の引数のときとそこまで変わりません。
# 0000 putself ( 1)[Li] # 0001 putobject_INT2FIX_1_ # 0002 putself # 0003 opt_send_without_block <calldata!mid:blk, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0005 send <calldata!mid:m, argc:1, ARGS_BLOCKARG|FCALL>, nil # 0008 leave m(1, &blk)
blkをコンパイルしてスタックに積んで(0003)、メソッドmを呼び出します(0005)。
いくつか注意すべき点がありますが、いままで実装してきた引数とおおよそ同じバイトコードになるということがわかりますね。
opt_send_without_block命令ではなくてsend命令になるargcには&blkはカウントされないARGS_BLOCKARGというフラグが渡っている
ノードの構造は書き換えの前後で少し変わります。
m(1, &blk)
# Before
# @ NODE_FCALL (id: 0, line: 1, location: (1,0)-(1,10))*
# +- nd_mid: :m
# +- nd_args:
# @ NODE_BLOCK_PASS (id: 4, line: 1, location: (1,2)-(1,9))
# +- forwarding: 0 (no forwarding)
# +- nd_head:
# | @ NODE_LIST (id: 2, line: 1, location: (1,2)-(1,3))
# | +- as.nd_alen: 1
# | +- nd_head:
# | | @ NODE_INTEGER (id: 1, line: 1, location: (1,2)-(1,3))
# | | +- val: 1
# | +- nd_next:
# | (null node)
# +- nd_body:
# | @ NODE_VCALL (id: 3, line: 1, location: (1,6)-(1,9))
# | +- nd_mid: :blk
# +- operator_loc: (1,5)-(1,6)
# After
+-- @ CallNode (location: (1,0)-(1,10))
+-- receiver: nil
+-- name: :m
+-- arguments:
| @ ArgumentsNode (location: (1,2)-(1,3))
| +-- ArgumentsNodeFlags: nil
| +-- arguments: (length: 1)
| +-- @ IntegerNode (location: (1,2)-(1,3))
| +-- IntegerBaseFlags: decimal
| +-- value: 1
+-- block:
@ BlockArgumentNode (location: (1,5)-(1,9))
+-- expression:
| @ CallNode (location: (1,6)-(1,9))
| +-- CallNodeFlags: variable_call, ignore_visibility
| +-- receiver: nil
| +-- name: :blk
| +-- arguments: nil
| +-- block: nil
書き換え前はメソッド呼び出しを表すNODE_FCALLの下にNODE_BLOCK_PASSがあり、その他の引数はNODE_BLOCK_PASSの下に置かれています。
書き換え後はCallNodeの下にargumentsとblockが同じ階層のものとして並んでいます。
setup_args関数は引数のノードを受け取る関数ですが、今回の変更をうけてblockに関するノードも受け取るようにします。
CallNodeを受け取るようにしないのは、setup_argsがCallNode以外のケースでも呼ばれるためです。
呼び出しもとではいま扱っているノードがなんであるかわかっているはずなので、再度setup_argsでノードの種別を判定する必要もないでしょう。
// Before static VALUE setup_args(rb_iseq_t *iseq, LINK_ANCHOR *const args, const rb_arguments_node_t *argn, unsigned int *flag, struct rb_callinfo_kwarg **keywords) // After static VALUE setup_args(rb_iseq_t *iseq, LINK_ANCHOR *const args, const rb_arguments_node_t *argn, const rb_block_argument_node_t *block, unsigned int *flag, struct rb_callinfo_kwarg **keywords)
ブロック周りの処理をみていきましょう。
if (argn && nd_type_p(argn, NODE_BLOCK_PASS)) { DECL_ANCHOR(arg_block); INIT_ANCHOR(arg_block); if (RNODE_BLOCK_PASS(argn)->forwarding && ISEQ_BODY(ISEQ_BODY(iseq)->local_iseq)->param.flags.forwardable) { int idx = ISEQ_BODY(ISEQ_BODY(iseq)->local_iseq)->local_table_size;// - get_local_var_idx(iseq, idDot3); RUBY_ASSERT(nd_type_p(RNODE_BLOCK_PASS(argn)->nd_head, NODE_ARGSPUSH)); const NODE * arg_node = RNODE_ARGSPUSH(RNODE_BLOCK_PASS(argn)->nd_head)->nd_head; int argc = 0; // Only compile leading args: // foo(x, y, ...) // ^^^^ if (nd_type_p(arg_node, NODE_ARGSCAT)) { argc += setup_args_core(iseq, args, RNODE_ARGSCAT(arg_node)->nd_head, &dup_rest, flag, keywords); } *flag |= VM_CALL_FORWARDING; ADD_GETLOCAL(args, argn, idx, get_lvar_level(iseq)); setup_args_splat_mut(flag, dup_rest, initial_dup_rest); return INT2FIX(argc); } else { *flag |= VM_CALL_ARGS_BLOCKARG; NO_CHECK(COMPILE(arg_block, "block", RNODE_BLOCK_PASS(argn)->nd_body)); } if (LIST_INSN_SIZE_ONE(arg_block)) { LINK_ELEMENT *elem = FIRST_ELEMENT(arg_block); if (IS_INSN(elem)) { INSN *iobj = (INSN *)elem; if (iobj->insn_id == BIN(getblockparam)) { iobj->insn_id = BIN(getblockparamproxy); } } } ret = INT2FIX(setup_args_core(iseq, args, RNODE_BLOCK_PASS(argn)->nd_head, &dup_rest, flag, keywords)); ADD_SEQ(args, arg_block); } else { ret = INT2FIX(setup_args_core(iseq, args, argn, &dup_rest, flag, keywords)); }
最初の分岐は新しく追加した引数をみるだけなので簡単です。
// Before if (argn && nd_type_p(argn, NODE_BLOCK_PASS)) { // After if (block) {
次にforwardingという文字列が見えますね。
まったくなにもやってないなぁ...
ということでif (false && ...とでもして、一旦スキップします。
setup_args_coreを呼び出すときの引数はRNODE_BLOCK_PASSから取得するのではなく、関数の引数であるargnを渡せばいいでしょう。
// Before ret = INT2FIX(setup_args_core(iseq, args, RNODE_BLOCK_PASS(argn)->nd_head, &dup_rest, flag, keywords)); // After ret = INT2FIX(setup_args_core(iseq, args, argn, &dup_rest, flag, keywords));
このくらいの書き換えで動くことがわかります。
$ ./miniruby --dump=i --parser=parse.y -e 'm(nil, &blk)' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,12)> 0000 putself ( 1)[Li] 0001 putnil 0002 putself 0003 opt_send_without_block <calldata!mid:blk, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0005 send <calldata!mid:m, argc:1, ARGS_BLOCKARG|FCALL>, nil 0008 leave
do expr endに対応する
doブロックを渡した時のバイトコードをみてみます。 いままでとは変わって2つのISeqが表示されます。
m(1) do expr end # == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,16)> # 0000 putself ( 1)[Li] # 0001 putobject_INT2FIX_1_ # 0002 send <calldata!mid:m, argc:1, FCALL>, block in <main> # 0005 leave m(1) block に相当する # == disasm: #<ISeq:block in <main>@-e:1 (1,5)-(1,16)> # 0000 putself ( 1)[LiBc] # 0001 opt_send_without_block <calldata!mid:expr, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0003 leave [Br] do expr end に相当する
doブロックの内容は別のISeqとしてコンパイルされ、そのブロックがmを呼び出すsend命令に渡されていることがわかります(block in <main>がブロック)。
このようなバイトコードを生成するためには、以下のような手順を踏んでいるはずと推測できます。
doブロックに関しては、書き換え前後でノードの構造が大きく変わります。
m(1) do expr end
# Before
# @ NODE_ITER (id: 5, line: 1, location: (1,0)-(1,16))*
# +- nd_iter:
# | @ NODE_FCALL (id: 0, line: 1, location: (1,0)-(1,4))
# | +- nd_mid: :m
# +- nd_body:
# @ NODE_SCOPE (id: 4, line: 1, location: (1,5)-(1,16))
# +- nd_tbl: (empty)
# +- nd_args:
# | (null node)
# +- nd_body:
# @ NODE_VCALL (id: 3, line: 1, location: (1,8)-(1,12))*
# +- nd_mid: :expr
# After
+-- @ CallNode (location: (1,0)-(1,16))
+-- receiver: nil
+-- name: :m
+-- arguments:
| @ ArgumentsNode (location: (1,2)-(1,3))
| +-- ArgumentsNodeFlags: nil
| +-- arguments: (length: 1)
| +-- @ IntegerNode (location: (1,2)-(1,3))
| +-- IntegerBaseFlags: decimal
| +-- value: 1
+-- block:
@ BlockNode (location: (1,5)-(1,16))
+-- locals: []
+-- parameters: nil
+-- body:
| @ StatementsNode (location: (1,8)-(1,12))
| +-- body: (length: 1)
| +-- @ CallNode (location: (1,8)-(1,12))
| +-- CallNodeFlags: variable_call, ignore_visibility
| +-- receiver: nil
| +-- name: :expr
| +-- arguments: nil
| +-- block: nil
NODE_ITERの下にNODE_FCALLがいるという構造から、CallNodeの下にBlockNodeがいるという構造に変わります。
compile.cの今の処理ではcompile_iterという関数がNODE_ITERとNODE_FORを扱っています1。
static int compile_iter(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { // 共通の前処理 ADD_LABEL(ret, retry_label); // NODEごとの処理 if (nd_type_p(node, NODE_FOR)) { CHECK(COMPILE(ret, "iter caller (for)", RNODE_FOR(node)->nd_iter)); ISEQ_COMPILE_DATA(iseq)->current_block = child_iseq = NEW_CHILD_ISEQ(RNODE_FOR(node)->nd_body, make_name_for_block(iseq), ISEQ_TYPE_BLOCK, line); ADD_SEND_WITH_BLOCK(ret, line_node, idEach, INT2FIX(0), child_iseq); } else { ISEQ_COMPILE_DATA(iseq)->current_block = child_iseq = NEW_CHILD_ISEQ(RNODE_ITER(node)->nd_body, make_name_for_block(iseq), ISEQ_TYPE_BLOCK, line); CHECK(COMPILE(ret, "iter caller", RNODE_ITER(node)->nd_iter)); } // 共通の後処理 { // We need to put the label "retry_end_l" immediately after the last "send" instruction. // This because vm_throw checks if the break cont is equal to the index of next insn of the "send". // (Otherwise, it is considered "break from proc-closure". See "TAG_BREAK" handling in "vm_throw_start".) // // Normally, "send" instruction is at the last. // However, qcall under branch coverage measurement adds some instructions after the "send". // // Note that "invokesuper", "invokesuperforward" appears instead of "send". INSN *iobj; LINK_ELEMENT *last_elem = LAST_ELEMENT(ret); iobj = IS_INSN(last_elem) ? (INSN*) last_elem : (INSN*) get_prev_insn((INSN*) last_elem); while (!IS_INSN_ID(iobj, send) && !IS_INSN_ID(iobj, invokesuper) && !IS_INSN_ID(iobj, sendforward) && !IS_INSN_ID(iobj, invokesuperforward)) { iobj = (INSN*) get_prev_insn(iobj); } ELEM_INSERT_NEXT(&iobj->link, (LINK_ELEMENT*) retry_end_l); // LINK_ANCHOR has a pointer to the last element, but ELEM_INSERT_NEXT does not update it // even if we add an insn to the last of LINK_ANCHOR. So this updates it manually. if (&iobj->link == LAST_ELEMENT(ret)) { ret->last = (LINK_ELEMENT*) retry_end_l; } } if (popped) { ADD_INSN(ret, line_node, pop); } ISEQ_COMPILE_DATA(iseq)->current_block = prevblock; ADD_CATCH_ENTRY(CATCH_TYPE_BREAK, retry_label, retry_end_l, child_iseq, retry_end_l); return COMPILE_OK; }
ざっくり3つのステップからなります。
- 共通の前処理としてラベルを貼る
- ノードごとの処理
- 共通の後処理としてCATCH_ENTRYを追加する
ノードごとの処理のうち、メソッド呼び出しの場合を詳しくみると
メソッド呼び出しのコンパイル時にはブロックに相当するISeqをcurrent_blockから取得して命令に埋め込んでいるはずです。
ものすごく簡略化すると、compile_callはcurrent_blockからブロックのISeqを取り出してADD_SEND_Rに渡しています2。
static int compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, const enum node_type type, const NODE *const line_node, int popped, bool assume_receiver) { const rb_iseq_t *parent_block = ISEQ_COMPILE_DATA(iseq)->current_block; ISEQ_COMPILE_DATA(iseq)->current_block = NULL; /* receiver */ ... /* args */ ... ADD_SEQ(ret, recv); ADD_SEQ(ret, args); LABEL *not_basic_new = NEW_LABEL(nd_line(node)); LABEL *not_basic_new_finish = NEW_LABEL(nd_line(node)); if (inline_new) { ... } else { ADD_SEND_R(ret, line_node, mid, argc, parent_block, INT2FIX(flag), keywords); } qcall_branch_end(iseq, ret, else_label, branches, node, line_node); if (popped) { ADD_INSN(ret, line_node, pop); } return COMPILE_OK; }
さて今回のNODE_ITERの書き換えでは、2つのことを遵守したいと思っています。
- 既存の処理の順番を変えない
NODE_ITERとNODE_FORで処理を分けない
1番目について。
そもそもこのブログは"Ruby Parser開発日誌"であり、コンパイラの開発日誌ではありません。
筆者はparse.yこそチョットわかりますが、compile.cは基本的にど素人です。
今回のノードの変更であれば、CallNodeのblockの種類に応じてcompile_callのなかで分岐し、current_blockを経由しないで渡せばよさそうというのは思いつきます。
が、これが本当に正しいのかはいろいろとコードを読まないと自信がもてません。
2番目について。
こちらも同じような理由に起因しますが、NODE_ITERとNODE_FORで共通している処理が今後どのくらい同じであり続けるのか見通しが立つほど詳しくはありません。
その判断を可能な限り遅延するためにも、できるのであれば既存の処理の流れを維持したいところです。
ブロックのあるメソッド呼び出しについて、書き換え前の処理の流れを整理しておきましょう。
NODE_ITERを引数としてiseq_compile_each0が呼ばれるNODE_ITERを引数としてcompile_iterが呼ばれる
ブロックのないメソッド呼び出しについても、書き換え前の処理の流れを整理しておきましょう。
NODE_CALLを引数としてiseq_compile_each0が呼ばれるNODE_CALLを引数としてcompile_callが呼ばれるNODE_CALLをコンパイルするcurrent_blockを含めてsend命令を生成する
書き換え後はブロックの有無にかかわらずCallNodeを引数としてiseq_compile_each0が呼ばれることを踏まえると
CallNodeを引数としてiseq_compile_each0が呼ばれるCallNodeを引数としてcompile_iterが呼ばれる
という流れになるかと思います。
念のためcurrent_blockを読んでいる関数を調べてみると、3つの関数がヒットします。
compile_call_precheck_freezecompile_callcompile_super
compile_call_precheck_freezeはCallNodeの特殊なケースの最適化用の関数で、ブロックが渡されてないことをISEQ_COMPILE_DATA(iseq)->current_block == NULLでチェックしています。
static int compile_call_precheck_freeze(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const rb_call_node_t *const node, const NODE *line_node, int popped) { /* optimization shortcut * "literal".freeze -> opt_str_freeze("literal") */ NODE *recv = node->receiver; if (recv && (nd_type_p(recv, RB_STRING_NODE) || nd_type_p(recv, RB_SOURCE_FILE_NODE)) && (get_node_call_nd_mid(node) == idFreeze || get_node_call_nd_mid(node) == idUMinus) && get_nd_args(node) == NULL && ISEQ_COMPILE_DATA(iseq)->current_block == NULL && ISEQ_COMPILE_DATA(iseq)->option->specialized_instruction) { VALUE str = get_string_value(recv); if (get_node_call_nd_mid(node) == idUMinus) { ADD_INSN2(ret, line_node, opt_str_uminus, str, new_callinfo(iseq, idUMinus, 0, 0, NULL, FALSE)); } else { ADD_INSN2(ret, line_node, opt_str_freeze, str, new_callinfo(iseq, idFreeze, 0, 0, NULL, FALSE)); } RB_OBJ_WRITTEN(iseq, Qundef, str); if (popped) { ADD_INSN(ret, line_node, pop); } return TRUE; } return FALSE; }
手元のコードではiseq_compile_each0のなかで分岐しているので、compile_call_precheck_freezeとcompile_callをラップした関数にして、呼び出す前にブロックの処理をするように変更します。
case RB_CALL_NODE: { rb_call_node_t *cast = RB_NODE_CALL(node); enum node_call_type call_type = get_node_call_type(cast); switch (call_type) { case N_CALL: /* obj.foo */ case N_OPCALL: /* foo[] */ if (compile_call_precheck_freeze(iseq, ret, cast, node, popped) == TRUE) { break; } case N_QCALL: /* obj&.foo */ case N_FCALL: /* foo() */ case N_VCALL: /* foo (variable or call) */ if (compile_call(iseq, ret, node, type, node, call_type, popped, false) == COMPILE_NG) { goto ng; } } break; }
superもブロックをとれるので、compile_superもcompile_iterの中にいれます。
CallNodeを例にすると以下のような呼び出しになります。
iseq_compile_each0compile_iterdo expr endがあるときcompile_iter0- ブロックのコンパイルをする
compile_call
- それ以外
compile_call
compile_callは最適化を試して、できない場合には通常のメソッド呼び出しとしてコンパイルします。
compile_call_coreを分離しているのは、defined?をコンパイルするときにcompile_call_precheck_freezeを経由しないパスがあるためです。
static int compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, const enum node_type type, int popped) { rb_call_node_t *cast = RB_NODE_CALL(node); enum node_call_type call_type = get_node_call_type(cast); switch (call_type) { case N_CALL: /* obj.foo */ case N_OPCALL: /* foo[] */ if (compile_call_precheck_freeze(iseq, ret, cast, node, popped) == TRUE) { break; } case N_QCALL: /* obj&.foo */ case N_FCALL: /* foo() */ case N_VCALL: /* foo (variable or call) */ return compile_call_core(iseq, ret, node, type, node, call_type, popped, false); } return COMPILE_OK; }
compile_iterとcompile_iter0の両方でノードの種類に応じて分岐するのがイマイチですが、処理の流れを変えたくないのでこの辺が自分の実力かなぁという気持ちです。
static int compile_iter0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, const enum node_type type, int popped) { ... ADD_LABEL(ret, retry_label); switch (type) { // case RB_FOR_NODE: { // CHECK(COMPILE(ret, "iter caller (for)", RNODE_FOR(node)->nd_iter)); // ISEQ_COMPILE_DATA(iseq)->current_block = child_iseq = // NEW_CHILD_ISEQ(RNODE_FOR(node)->nd_body, make_name_for_block(iseq), // ISEQ_TYPE_BLOCK, line); // ADD_SEND_WITH_BLOCK(ret, line_node, idEach, INT2FIX(0), child_iseq); // } case RB_CALL_NODE: { EXPECT_NODE("compile_iter0", RB_NODE_CALL(node)->block, RB_BLOCK_NODE, COMPILE_NG); ISEQ_COMPILE_DATA(iseq)->current_block = child_iseq = NEW_CHILD_ISEQ(RB_NODE_CALL(node)->block, make_name_for_block(iseq), ISEQ_TYPE_BLOCK, line); CHECK(compile_call(iseq, ret, node, type, popped)); } } ... } static int compile_iter(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, const enum node_type type, int popped) { switch (type) { // case RB_FOR_NODE: case RB_CALL_NODE: { if (block_node_p(RB_NODE_CALL(node)->block)) { return compile_iter0(iseq, ret, node, type, popped); } else { return compile_call(iseq, ret, node, type, popped); } break; } // case RB_SUPER_NODE: // case RB_FORWARDING_SUPER_NODE: default: rb_bug("unexpected node: %s", ruby_node_name(nd_type(node))); } }
setup_argsでもblockを扱いますが、do expr endはcompile_iterであつかうので、setup_argsではBlockArgumentNode(&blk)のときのみ分岐に入るようにします。
@@ -7156,11 +7176,12 @@ setup_args(rb_iseq_t *iseq, LINK_ANCHOR *const args, const rb_arguments_node_t * } initial_dup_rest = dup_rest; - if (argn && nd_type_p(argn, NODE_BLOCK_PASS)) { + // BlockNode is also stored in `block` however it should be handled by `compile_iter` + if (block && nd_type_p(block, RB_BLOCK_ARGUMENT_NODE)) { DECL_ANCHOR(arg_block); INIT_ANCHOR(arg_block);
実際にブロックを渡してメソッドを呼び出してみましょう3。
$ ./miniruby --parser=parse.y -e ':abc.to_s.chars.count.times do |i| p :r end' -e: -e:1: iseq_set_arguments: RB_PARAMETERS_NODE is expected, but RB_BLOCK_PARAMETERS_NODE (SyntaxError)
ああーーーー。 ブロックの引数は別のノードか...
iseq_set_argumentsを修正する
Block Parametersが未対応だと怒られたのでiseq_set_argumentsを修正します。
static int iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const optargs, const NODE *const node_args) { debugs("iseq_set_arguments: %s\n", node_args ? "" : "0"); if (node_args) { struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq); rb_parameters_node_t *args; NODE *rest = 0; int last_comma = 0; rb_block_parameter_node_t *block = 0; int arg_size; switch (nd_type(node_args)) { case RB_IT_PARAMETERS_NODE: body->param.lead_num = body->param.size = 1; break; case RB_NUMBERED_PARAMETERS_NODE: body->param.lead_num = body->param.size = RB_NODE_NUMBERED_PARAMETERS(node_args)->maximum; break; case RB_PARAMETERS_NODE: args = RB_NODE_PARAMETERS(node_args); goto parameters; case RB_BLOCK_PARAMETERS_NODE: args = RB_NODE_BLOCK_PARAMETERS(node_args)->parameters; parameters: { // body->param.flags.ruby2_keywords = args->ruby2_keywords; body->param.lead_num = arg_size = (int)RB_NODE_LIST_LEN(&args->requireds); if (body->param.lead_num > 0) body->param.flags.has_lead = TRUE; debugs(" - argc: %d\n", body->param.lead_num); ... }
BlockParametersNodeはparametersフィールドにParametersNodeが入っているので、そちらを解析すればよいです。
ついでなのでItParametersNodeとNumberedParametersNodeも対応しておきましょう。
itの場合はいわば10.times do |it| it endのように1つ仮引数があるのと同じなのでbody->param.lead_num = body->param.size = 1;にします。
_2の場合はいわばobj.m do |_1, _2| endのように2つの仮引数があるのと同じなのでbody->param.lead_num = body->param.size = maximum;にします。
いろいろなブロックを渡してみましょう。
$ ./miniruby --parser=parse.y --dump=i -e 'm do end' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,8)> 0000 putself ( 1)[Li] 0001 send <calldata!mid:m, argc:0, FCALL>, block in <main> 0004 leave == disasm: #<ISeq:block in <main>@-e:1 (1,4)-(1,4)> 0000 putnil ( 1)[Bc] 0001 leave [Br] $ ./miniruby --parser=parse.y --dump=i -e 'm do |i| end' == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,12)> 0000 putself ( 1)[Li] 0001 send <calldata!mid:m, argc:0, FCALL>, block in <main> 0004 leave == disasm: #<ISeq:block in <main>@-e:1 (1,4)-(1,8)> local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] i@0<AmbiguousArg> 0000 putnil ( 1)[Bc] 0001 leave [Br]
よさそうです。
$ ./miniruby --parser=parse.y --dump=i -e 'm do it end' -e: -e:1: iseq_compile_each: unknown node (RB_IT_LOCAL_VARIABLE_READ_NODE) (SyntaxError)
あー。たしかに...
$ ./miniruby --parser=parse.y --dump=i -e 'm do _1 end' -e: [BUG] Segmentation fault at 0x00000007fffffff8 ... /usr/lib/system/libsystem_platform.dylib(_sigtramp+0x38) [0x19722e584] /Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(iseq_add_getlocal+0x194) [0x1008b8d24] ../../compile.c:2027 /Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(compile_lvar+0x8c) [0x1008d5aac] ../../compile.c:9441 /Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(iseq_compile_each0+0x8b4) [0x1008d29b8] ../../compile.c:11439 ...
ん???
$ ./miniruby --parser=parse.y --dump=i -e 'm do |i| i end' -e: -e:1: iseq_compile_each: unknown node () (SyntaxError)
そうでしたDVAR、つまりブロックの中などになるローカル変数のサポートをまだしていないのでした。
ということで今日は引数のないブロックまでサポートすることができました。
$ ./miniruby --parser=parse.y -e ':sym.tap do p :a end' :a
まとめ
今日の成果です。
&blkを引数に渡せるようになった- 引数のないブロックをメソッド呼び出しに渡せるようになった
次回はローカル変数の対応をして引数のあるブロックをメソッドに渡せるようにしていきます。