以下の内容はhttps://yui-knk.hatenablog.com/entry/2025/12/27/115614より取得しました。


Ruby Parser開発日誌 (24-9) - parse.yが生成するノードを変える ー 引数を渡したい(compiler編 ブロック引数の巻)

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の下にargumentsblockが同じ階層のものとして並んでいます。

setup_args関数は引数のノードを受け取る関数ですが、今回の変更をうけてblockに関するノードも受け取るようにします。 CallNodeを受け取るようにしないのは、setup_argsCallNode以外のケースでも呼ばれるためです。 呼び出しもとではいま扱っているノードがなんであるかわかっているはずなので、再度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>がブロック)。 このようなバイトコードを生成するためには、以下のような手順を踏んでいるはずと推測できます。

  1. doブロックを新しいISeqとしてコンパイルする
  2. 1で作ったISeqをsend命令のオペランドにとして命令を生成する

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_ITERNODE_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つのステップからなります。

  1. 共通の前処理としてラベルを貼る
  2. ノードごとの処理
  3. 共通の後処理としてCATCH_ENTRYを追加する

ノードごとの処理のうち、メソッド呼び出しの場合を詳しくみると

  1. ブロックの中身(nd_body)をコンパイルして、current_blockにセットする
  2. メソッド呼び出し(nd_iter)をコンパイルする

メソッド呼び出しのコンパイル時にはブロックに相当するISeqをcurrent_blockから取得して命令に埋め込んでいるはずです。

ものすごく簡略化すると、compile_callcurrent_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つのことを遵守したいと思っています。

  1. 既存の処理の順番を変えない
  2. NODE_ITERNODE_FORで処理を分けない

1番目について。 そもそもこのブログは"Ruby Parser開発日誌"であり、コンパイラの開発日誌ではありません。 筆者はparse.yこそチョットわかりますが、compile.cは基本的にど素人です。 今回のノードの変更であれば、CallNodeblockの種類に応じてcompile_callのなかで分岐し、current_blockを経由しないで渡せばよさそうというのは思いつきます。 が、これが本当に正しいのかはいろいろとコードを読まないと自信がもてません。

2番目について。 こちらも同じような理由に起因しますが、NODE_ITERNODE_FORで共通している処理が今後どのくらい同じであり続けるのか見通しが立つほど詳しくはありません。 その判断を可能な限り遅延するためにも、できるのであれば既存の処理の流れを維持したいところです。

ブロックのあるメソッド呼び出しについて、書き換え前の処理の流れを整理しておきましょう。

  1. NODE_ITERを引数としてiseq_compile_each0が呼ばれる
  2. NODE_ITERを引数としてcompile_iterが呼ばれる
    1. 前処理が行われる
    2. ブロック部分のコンパイルをしてcurrent_blockに代入する
    3. NODE_CALLを引数としてiseq_compile_each0が呼ばれる。これはcompile_callを呼ぶ
      1. NODE_CALLコンパイルする
      2. current_blockを含めてsend命令を生成する
    4. 後処理が行われる

ブロックのないメソッド呼び出しについても、書き換え前の処理の流れを整理しておきましょう。

  1. NODE_CALLを引数としてiseq_compile_each0が呼ばれる
  2. NODE_CALLを引数としてcompile_callが呼ばれる
    1. NODE_CALLコンパイルする
    2. current_blockを含めてsend命令を生成する

書き換え後はブロックの有無にかかわらずCallNodeを引数としてiseq_compile_each0が呼ばれることを踏まえると

  1. CallNodeを引数としてiseq_compile_each0が呼ばれる
  2. CallNodeを引数としてcompile_iterが呼ばれる
    1. blockBlockNodeの場合
      1. compile_iter相当の前処理が行われる
      2. ブロック部分のコンパイルをしてcurrent_blockに代入する
      3. CallNodeを引数としてcompile_callが呼ばれる
        1. CallNodeコンパイルする
        2. current_blockを含めてsend命令を生成する
      4. 後処理が行われる
    2. blockBlockNodeではない場合(&blkのときと、ブロックがない時の2パターンがある)
      1. CallNodeを引数としてcompile_callが呼ばれる

という流れになるかと思います。

念のためcurrent_blockを読んでいる関数を調べてみると、3つの関数がヒットします。

  • compile_call_precheck_freeze
  • compile_call
  • compile_super

compile_call_precheck_freezeCallNodeの特殊なケースの最適化用の関数で、ブロックが渡されてないことを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_freezecompile_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_supercompile_iterの中にいれます。

CallNodeを例にすると以下のような呼び出しになります。

  • iseq_compile_each0
    • compile_iter
      • do expr endがあるとき
      • それ以外
        • 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_itercompile_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 endcompile_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);
            ...
}

BlockParametersNodeparametersフィールドにParametersNodeが入っているので、そちらを解析すればよいです。 ついでなのでItParametersNodeNumberedParametersNodeも対応しておきましょう。 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を引数に渡せるようになった
  • 引数のないブロックをメソッド呼び出しに渡せるようになった

次回はローカル変数の対応をして引数のあるブロックをメソッドに渡せるようにしていきます。


  1. for i ary do expr endary.each do |i| expr endなので。
  2. ADD_SEND_Rの実態はnew_insn_sendです。new_insn_sendは基本的にはsend命令を生成します。opt_send_without_blockは?と思うかもしれませんが、それはiseq_specialized_instructionという関数で条件を満たした場合に命令の書き換えを行い、opt_send_without_blockに変換されます。
  3. 数値リテラルにまだ対応してないので、シンボルから文字の配列を作ってイテレートしています。数値リテラルはよ...



以上の内容はhttps://yui-knk.hatenablog.com/entry/2025/12/27/115614より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14