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


Ruby Parser開発日誌 (24-6) - parse.yが生成するノードを変える ー 引数を渡したい(parser編)

6日目: メソッド呼び出し時の引数

前回は引数がないケースでのメソッド呼び出しに対応しました。 やっぱり引数を渡したい!ということで、今日は引数周りをいじっていきます。

引数に関する生成規則

引数を構成する基本的なパーツはargs, assocs, block_argの3つです。

argsa1などの要素の列です。 *a*のようなsplatもここに含まれます。

assocsk => vk: vといった要素の列です。 **kw**のようなsplatもここに含まれます。

block_arg&blkもしくは&のことです。

f(a, b)cmd 1, 2のようなカッコの有無などいくつかのバリエーションに対応するように、引数に関して複数の生成規則があります。 その中心となるのはcall_argsという規則です。

call_args       : value_expr(command)
                | def_endless_method(endless_command)
                | args opt_block_arg
                | assocs opt_block_arg
                | args ',' assocs opt_block_arg
                | block_arg
                ;

commanddef_endless_method(endless_command)など大変に興味深い規則がありますが、まずはargs ',' assocs opt_block_argに注目すると良いでしょう。 f(a, *b, c: :d, **kw, &blk)と書いたときに対応するのがこの規則です。

今回の書き換えでは基本的にはArgumentsNodeargumentsというフィールドに引数を詰め込んでいくことになります。 pre, post, restなど配置できる順番が細かく決まっている仮引数に比べると、実引数は要素の列という側面が強いといえます。

Arrayの中身は引数?

書き換えを行う前にargsが他にどのような生成規則で使われているかをみてみると、配列リテラルの要素列にも使われていることがわかります。

primary     | tLBRACK aref_args ']'

aref_args   : none
            | args trailer
            | args ',' assocs trailer
            | assocs trailer
            ;

trailer     : '\n'?
            | ','
            ;

配列リテラルに対応するノードはArrayNodeです。 なのでargs自体はArrayNodeで管理するようにして、call_argsArrayNodeからArgumentsNodeに変換するようにしましょう。

static rb_arguments_node_t *
array2arguments(struct parser_params *p, rb_array_node_t *nd_ary)
{
    rb_arguments_node_t *nd_args = NEW_RB_ARGUMENTS0(rb_nd_code_loc((rb_node_t *)nd_ary));
    rb_node_list_move(&nd_args->arguments, &nd_ary->elements);

    return nd_args;
}

call_args   : value_expr(command)
                 {
                     // 要素が1つのときは、ここでArgumentsNodeを作成する
                     $$ = NEW_RB_ARGUMENTS($1, &@$);
                 }
            | args opt_block_arg
                {
                    // 要素が複数あるときはArrayNodeからArgumentsNodeに変換する
                    $$ = array2arguments(p, $1);
                    $$ = arg_blk_pass2(p, $$, $2);
                }

キーワード引数

キーワード引数に対応するassocsもまた引数とハッシュリテラルの両方で使われる生成規則です。 書き換え後は、ハッシュリテラルの場合はHashNodeelementsに各要素が並ぶのに対して、引数の場合はKeywordHashNodeelementsに各要素が並ぶという構造になります。

primary     | tLBRACE assoc_list '}' // `{k1: v}`のようなハッシュリテラルのこと

assoc_list  : none
            | assocs trailer
            ;

call_args   | args ',' assocs opt_block_arg // `1, k1: v, &blk`のような引数列のこと
               {
                   $$ = $3 ? arg_append(p, $1, new_hash(p, $3, &@3), &@$) : $1;
                   $$ = arg_blk_pass($$, $4);
               /*% ripper: args_add_block!(args_add!($:1, bare_assoc_hash!($:3)), $:4) %*/
               }

配列のときと同様にcall_argsのアクションでKeywordHashNodeに変換することになります。 既存のコードではnew_hashという関数にassocsを渡すようになっているので、new_hashのなかで変換をすればよいでしょう。

ここで一点注意が必要なのは、ArgumentsNodeには実は階層があるということです。 abなどの引数はArgumentsNodeargumentsに展開されますが、k1: 1などはKeywordHashNodeにまとめたうえでArgumentsNodeにおかれます。 文法的にはargsassocsなので、両方をそれぞれ構造化する、もしくはどちらもフラットにargumentsにおくのが自然に思えます。 コンパイル時の都合でKeywordHashNodeに特殊なフラグを置きたかったのかもしれません1

$ ruby --dump=p --parser=prism  -e 'm(a, b, k1: 1, k2: 2)'
        +-- @ CallNode (location: (1,0)-(1,21))
            +-- arguments:
            |   @ ArgumentsNode (location: (1,2)-(1,20))
            |   +-- arguments: (length: 3)
            |       +-- @ CallNode (location: (1,2)-(1,3))
            |       |   +-- name: :a
            |       +-- @ CallNode (location: (1,5)-(1,6))
            |       |   +-- name: :b
            |       +-- @ KeywordHashNode (location: (1,8)-(1,20))
            |           +-- KeywordHashNodeFlags: symbol_keys
            |           +-- elements: (length: 2)
            |               +-- @ AssocNode (location: (1,8)-(1,13))
            |               |   +-- key:
            |               |   |   @ SymbolNode (location: (1,8)-(1,11))
            |               |   |   +-- unescaped: "k1"
            |               |   +-- value:
            |               |   |   @ IntegerNode (location: (1,12)-(1,13))
            |               |   |   +-- IntegerBaseFlags: decimal
            |               |   |   +-- value: 1
            |               |   +-- operator_loc: nil
            |               +-- @ AssocNode (location: (1,15)-(1,20))
            |                   +-- key:
            |                   |   @ SymbolNode (location: (1,15)-(1,18))
            |                   |   +-- unescaped: "k2"
            |                   +-- value:
            |                   |   @ IntegerNode (location: (1,19)-(1,20))
            |                   |   +-- IntegerBaseFlags: decimal
            |                   |   +-- value: 2
            |                   +-- operator_loc: nil

ブロック引数の位置

ブロック引数を表すノードはBlockArgumentNodeです。 それ自体は対応する生成規則block_argのアクションを書き換えればよいので、特に問題ないでしょう。

問題はブロック引数が格納される場所です。

$ ruby --dump=p --parser=prism  -e 'm(a, &blk)'
        +-- @ CallNode (location: (1,0)-(1,10))
            +-- name: :m
            +-- arguments:
            |   @ ArgumentsNode (location: (1,2)-(1,3))
            |   +-- ArgumentsNodeFlags: nil
            |   +-- arguments: (length: 1)
            |       +-- @ CallNode (location: (1,2)-(1,3))
            |           +-- name: :a
            +-- block:
                @ BlockArgumentNode (location: (1,5)-(1,9))
                +-- expression:
                |   @ CallNode (location: (1,6)-(1,9))
                |   +-- name: :blk
                +-- operator_loc: (1,5)-(1,6) = "&"

CallNodeblockにおかれるようです。

ArgumentsNodeの外じゃん...

do ... endの形式でブロックを渡したときも同様にCallNodeblockにおかれるようです。

$ ruby --dump=p --parser=prism  -e 'm(a) do expr end'
        +-- @ CallNode (location: (1,0)-(1,16))
            +-- name: :m
            +-- arguments:
            |   @ ArgumentsNode (location: (1,2)-(1,3))
            |   +-- ArgumentsNodeFlags: nil
            |   +-- arguments: (length: 1)
            |       +-- @ CallNode (location: (1,2)-(1,3))
            |           +-- name: :a
            +-- block:
                @ BlockNode (location: (1,5)-(1,16))
                +-- body:
                |   @ StatementsNode (location: (1,8)-(1,12))
                |   +-- body: (length: 1)
                |       +-- @ CallNode (location: (1,8)-(1,12))
                |           +-- name: :expr
                +-- opening_loc: (1,5)-(1,7) = "do"
                +-- closing_loc: (1,13)-(1,16) = "end"

入力されたスクリプトの構造と完全に一致する構文木を目指しているわけではないのかもしれません。 あまり気にしても仕方ないので、ここはそういうものだということにして先に進みましょう。

実装上の問題として、BlockArgumentNodeをどうやってCallNodeに渡すかを考えなくてはいけません。 メソッド呼び出しに関連する生成規則はおおよそ以下のような構造をしています。 method_callfcallというのがCallNodeなので、call_argsに出現するBlockArgumentNodemethod_callまで持っていく必要があります。

method_call : fcall paren_args // f(a, k1: 1, &blk). CallNodeはここにあらわれるが、paren_argsはArgumentsNode

paren_args  : '(' opt_call_args rparen // `(a, k1: 1, &blk)`

call_args   | args ',' assocs opt_block_arg // `a, k1: 1, &blk`. ここにBlockArgumentNodeがあらわれるが、call_argsはArgumentsNodeを返す

ぱっと思いつく方法は以下の4つです。

  1. parser_params構造体に管理させる
  2. %unionに新しい構造体を入れる
  3. parser内部でだけ使う専用のノードを用意する
  4. args(ArrayNode)に入れる

それぞれ検討していきましょう。

最初の方法はparser_params構造体に管理させるというものです。

parse.yの実装ではほぼ全ての場所でparser_params構造体にアクセスすることが可能です。 といっても特別なことをしているわけではなく、生成規則のアクションも含めてあらゆる場所に引数としてparser_params構造体を渡しているというシンプルな話です。 parse.yを読んでいるとpというとても短い変数をあらゆるところで見ると思います。 このpparser_params構造体へのポインタになっています。 ということはparser_params構造体にフィールドを増やせば実質的に全ての箇所でそのデータに触れることができます。

例えば構文解析中にはそのスコープで定義・使用されているローカル変数を管理する必要があります。 ローカル変数の定義や使用やスコープ内部のあらゆるところで発生する可能性があります。 このような理由からparser_params構造体にstruct local_vars *lvtbl;というフィールドを用意して、そこでローカル変数を管理するようになっています。

今回のケースではローカル変数ほど大域で利用するものではないので、よりスコープを狭くできる別の方法を考えたいところです。

2つ目の方法はparserのsemantic value用の領域を使うというものです。 %unionでparserのsemantic valueの型を定義しています。 例えばここにArgumentsNodeのポインタとBlockArgumentNodeのポインタを含んだ構造体を追加することで、call_argsのsemantic valueとしてArgumentsNodeBlockArgumentNodeを設定することができます。

生成されるparserでは%unionに対応する共用体の配列を、関数内部のローカル変数に確保します。 可能なら共用体のサイズが大きくなるのは避けたいところです。

3つ目の方法はparser内部でだけ使用するノードを定義して、そこでArgumentsNodeのポインタとBlockArgumentNodeのポインタを管理するという方法です。 実際メソッド定義におけるメソッド名などを管理するための一時的なデータ構造として、DEF_TEMPというノードがあったりします。

ノードのメモリ管理は一括して行っているで煩雑な処理を書くことなく使うことができます。 悪くない方法だと思います。

4つ目の方法はcall_argsargs(ArrayNode)の末尾にBlockArgumentNodeを追加し、method_callで取り出すという方法です。 ブロック引数が存在する場合、文法上それは必ず引数列の末尾にあらわれます。 そのため取り出す側でも単純にArrayNodeの末尾をみて判断すればすみます。

手軽さを重視して、今回は4つ目の方法を用いることにします。

arg_blk_pass2関数を用いて、opt_block_argargsの末尾に追加します。

call_args   | args ',' assocs opt_block_arg
               {
                   $$ = array2arguments(p, $1);
                   $$ = $3 ? arg_append2(p, $$, new_hash(p, $3, &@3), &@$) : $$;
                   $$ = arg_blk_pass2(p, $$, $4);
               }


static rb_node_t *
arg_blk_pass2(struct parser_params *p, rb_arguments_node_t *nd_args, rb_node_t *node)
{
    if (!node) return nd_args;

    rb_node_list_append(&nd_args->arguments, node);
    return nd_args;
}

method_call生成規則などのようにCallNodeを扱うところではmethod_add_arguments関数を呼び出し、ブロック引数がある場合には末尾から取り出してCallNodeblockにセットします。

method_call : fcall paren_args
                {
                    $$ = (rb_node_t *)method_add_arguments(p, $1, $2, &@$);
                    nd_set_last_loc($1, @2.end_pos);
                }

static rb_call_node_t *
method_add_arguments(struct parser_params *p, rb_call_node_t *nd_call, rb_arguments_node_t *nd_args, const YYLTYPE *loc)
{
    rb_node_t *last;

    if (nd_args && (last = rb_node_list_last(&nd_args->arguments)) && nd_type_p(last, RB_BLOCK_ARGUMENT_NODE)) {
        nd_call->block = (rb_node_t *)rb_node_list_pop(&nd_args->arguments);
    }
    nd_call->arguments = nd_args;

    return nd_call;
}

ブロックを伴うメソッド呼び出し

ブロックを伴うメソッド呼び出しについても書き換えておきましょう。 ブロックに対応するノードはBlockNode、その仮引数全体はBlockParametersNodeで表現します。

ブロックのほうは{ ... }に相当するbrace_bodyと、do ... endに相当するdo_bodyという生成規則があるので、それぞれのアクションで生成するノードを変更すればよいでしょう。

@@ -5596,7 +5622,7 @@ do_body   :   {
                         p->max_numparam = $max_numparam;
                         p->it_id = $it_id;
                         $args = args_with_numbered(p, $args, max_numparam, it_id);
-                        $$ = NEW_ITER($args, $bodystmt, &@$);
+                        $$ = NEW_RB_BLOCK($args, $bodystmt, &@$);
                     /*% ripper: do_block!($:args, $:bodystmt) %*/
                         CMDARG_POP();
                         restore_block_exit(p, $allow_exits);

仮引数全体に相当するのはblock_param_defという生成規則なので、そこでBlockParametersNodeを生成するようにします。

@@ -5329,7 +5349,7 @@ block_param_def   : '|' opt_block_param opt_bv_decl '|'
                     {
                         p->max_numparam = ORDINAL_PARAM;
                         p->ctxt.in_argdef = 0;
-                        $$ = $2;
+                        $$ = NEW_RB_BLOCK_PARAMETERS($2, $3, &@$);
                     /*% ripper: block_var!($:2, $:3) %*/
                     }
                 ;

メソッド定義の書き換えをするときに、block_param_defのもととなるblock_paramParametersNodeを返すように変更してあるので、今回はとくに変更する必要はありません。

block_param : f_arg ',' f_opt_arg(primary_value) ',' f_rest_arg opt_args_tail(block_args_tail)
                {
                    $$ = new_args2(p, $1, $3, $5, 0, $6, &@$);
                /*% ripper: params!($:1, $:3, $:5, Qnil, *$:6[0..2]) %*/
                }

ブロックローカル変数

さてRubyのブロックにはブロックローカル変数というものがあります。 |; v2|のように;のあとに変数を定義すると、そのブロックの内部だけで有効な変数を定義することができます。

n = nil
3.times {|i| n = i }
p n
# => 2

n = nil
3.times {|i;n| n = i}
p n
# => nil

今回の書き換えにより、このブロックローカル変数に対応するノードとしてBlockLocalVariableNodeが追加されました。 対応する生成規則であるbvarBlockLocalVariableNodeを生成し、ブロックローカル変数列を表すbv_declsではそれらをArrayNodeに追加するようにします。

@@ -5349,20 +5369,27 @@ opt_bv_decl     : '\n'?
                     }
                 | '\n'? ';' bv_decls '\n'?
                     {
-                        $$ = 0;
+                        $$ = $3;
                     /*% ripper: $:3 %*/
                     }
                 ;

 bv_decls       : bvar
+                    {
+                        $$ = NEW_RB_ARRAY($1, &NULL_LOC);
                     /*% ripper[brace]: rb_ary_new3(1, $:1) %*/
+                    }
                 | bv_decls ',' bvar
+                    {
+                        $$ = node_array_append(p, $1, $3, &NULL_LOC);
                     /*% ripper[brace]: rb_ary_push($:1, $:3) %*/
+                    }
                 ;

 bvar           : tIDENTIFIER
                     {
                         new_bv(p, $1);
+                        $$ = (rb_node_t*)NEW_RB_BLOCK_LOCAL_VARIABLE($1, &@$);
                     /*% ripper: $:1 %*/
                     }

最終的にはBlockParametersNodeの生成時にブロックローカル変数列をArrayNodeからBlockParametersNodeに受け渡すようにします。

static rb_block_parameters_node_t *
rb_new_node_block_parameters_new(struct parser_params *p, rb_parameters_node_t *nd_params, rb_array_node_t *block_locals, const YYLTYPE *loc)
{
    rb_block_parameters_node_t *n = RB_NEW_NODE_NEWNODE((enum rb_node_type)RB_BLOCK_PARAMETERS_NODE, rb_block_parameters_node_t, loc);
    n->parameters = nd_params;
    block_locals ? rb_node_list_move(&n->locals, &block_locals->elements) : rb_node_list_init(&n->locals);
    n->opening_loc = NULL_LOC;
    n->closing_loc = NULL_LOC;

    return n;
}

itとnumbered parameters

ブロックに特有の変数としてitとnumbered parameters(_1など)があります。 それぞれItParametersNodeNumberedParametersNodeが対応します。 do_body生成規則を眺めると、args_with_numberedという関数がブロックの仮引数を受け取ってなにか処理をしているようです。

do_body    :   {
                        $$ = dyna_push(p);
                        CMDARG_PUSH(0);
                    }[dyna]<vars>
                  max_numparam numparam it_id allow_exits
                  opt_block_param_def[args] bodystmt
                    {
                        int max_numparam = p->max_numparam;
                        ID it_id = p->it_id;
                        p->max_numparam = $max_numparam;
                        p->it_id = $it_id;
                        $args = args_with_numbered(p, $args, max_numparam, it_id);
                        $$ = NEW_ITER($args, $bodystmt, &@$);
                    /*% ripper: do_block!($:args, $:bodystmt) %*/
                        CMDARG_POP();
                        restore_block_exit(p, $allow_exits);
                        numparam_pop(p, $numparam);
                        dyna_pop(p, $dyna);
                    }
                ;

これを書き換えてItParametersNode(if (it_id)のとき)やNumberedParametersNode(elseなのでmax_numparam > NO_PARAMのとき)を生成するようにします。

-static rb_node_args_t *
+static rb_node_t *
 args_with_numbered(struct parser_params *p, rb_node_args_t *args, int max_numparam, ID it_id)
 {
+    rb_node_t *ret = (rb_node_t *)args;
+
     if (max_numparam > NO_PARAM || it_id) {
-        if (!args) {
-            YYLTYPE loc = RUBY_INIT_YYLLOC();
-            args = new_args_tail2(p, 0, 0, 0, 0);
-            nd_set_loc(RNODE(args), &loc);
+        YYLTYPE loc = RUBY_INIT_YYLLOC();
+
+        if (it_id) {
+            ret = NEW_RB_IT_PARAMETERS(&loc);
+        }
+        else {
+            ret = NEW_RB_NUMBERED_PARAMETERS(max_numparam, &loc);
         }
-        args->nd_ainfo.pre_args_num = it_id ? 1 : max_numparam;
     }
-    return args;
+    return ret;
 }

BlockNodeCallNodeに紐づける

仮引数も含めてブロック関連の処理を書き直したので、最後にBlockNodeCallNodeへ紐づけましょう。 method_add_blockという関数が用意されているので、これを修正します2

-static NODE *method_add_block(struct parser_params*p, NODE *m, NODE *b, const YYLTYPE *loc) {RNODE_ITER(b)->nd_iter = m; b->nd_loc = *loc; return b;}
+static rb_call_node_t *method_add_block(struct parser_params *p, rb_call_node_t *m, rb_block_node_t *b, const YYLTYPE *loc) {m->block = b; return m;}

method_add_argumentsblock_dup_check

メソッド呼び出しに対して、&のついた引数とブロックの両方を渡すことはできません。

m(&blk) {}
# => -e:1: both block arg and actual block given
./miniruby: compile error (SyntaxError)

それら両方が指定されていないかチェックするのがblock_dup_checkという関数です。 これまでは&blkargumentsが管理していたので、block_dup_checkにはargumentsblockを渡していましたが、今回の書き換えでCallNode&blkを管理するようになりました。 この変更にあわせてblock_dup_checkCallNodeBlockNodeを受け取るように変更します。

                 | fcall command_args cmd_brace_block
                     {
-                        block_dup_check(p, $2, $3);
-                        $1->arguments = $2;
+                        method_add_arguments(p, $1, $2, &@$);
+                        block_dup_check(p, $1, $3);
                         $$ = method_add_block(p, (NODE *)$1, $3, &@$);
                         fixpos($$, RNODE($1));
                         nd_set_last_loc($1, @2.end_pos);

今後は以下の順番でCallNodeをセットアップすることにします。

  1. method_add_argumentsを呼び出して&blkCallNodeに移動する
  2. block_dup_checkを呼び出して&blkBlockNodeの両方が渡されていないかチェックする
  3. (BlockNode)がある場合にはmethod_add_blockを呼び出してCallNodeにセットする

block_dup_checkmethod_add_blockはセットなので、method_add_blockの中でblock_dup_checkを呼ぶようにリファクタリングしてもいいかもしれません。

これで無事にメソッド呼び出しに引数を渡すことができるようになりました。

$ ./miniruby --dump=p -e 'm1(true, *rest, k1: false, **kw, &blk)'

        +-- @ CallNode (location: (1,1)-(38,2))*
            +-- receiver: nil
            +-- arguments:
            |   @ ArgumentsNode (location: (1,3)-(1,14))
            |   +-- arguments: (length: 3)
            |       +-- @ TrueNode (location: (1,3)-(1,7))
            |       +-- @ SplatNode (location: (1,9)-(1,14))
            |       +-- @ KeywordHashNode (location: (1,16)-(1,31))
            |           +-- KeywordHashNodeFlags: nil
            |           +-- elements: (length: 2)
            |               +-- @ AssocNode (location: (1,16)-(1,25))
            |               +-- @ AssocSplatNode (location: (1,27)-(1,31))
            +-- block:
                @ BlockArgumentNode (location: (1,33)-(1,37))
                +-- expression:
                |   @ CallNode (location: (1,34)-(1,37))
                |   +-- name: :blk
                +-- operator_loc: (1,33)-(1,34) = ""

$ ./miniruby --dump=p  -e 'm1 {|a|}; m2 {it}; m3 {_2}'

        +-- @ CallNode (location: (1,0)-(1,2))*
        |   +-- receiver: nil
        |   +-- name: :m1
        |   +-- block:
        |       @ BlockNode (location: (1,4)-(1,7))
        |       +-- locals: [:a]
        |       +-- parameters:
        |       |   @ BlockParametersNode (location: (1,4)-(1,7))
        |       |   +-- parameters:
        |       |   |   @ ParametersNode (location: (0,-1)-(0,-1))
        |       |   |   +-- requireds: (length: 1)
        |       |   |   |   +-- @ RequiredParameterNode (location: (0,-1)-(0,-1))
        |       |   |   |       +-- ParameterFlags: nil
        |       |   |   |       +-- name: :a
        |       |   |   +-- optionals: (length: 0)
        |       |   |   +-- rest: nil
        |       |   |   +-- posts: (length: 0)
        |       |   |   +-- keywords: (length: 0)
        |       |   |   +-- keyword_rest: nil
        |       |   |   +-- block: nil
        |       |   +-- locals: (length: 0)
        |       +-- body:
        |       |   @ StatementsNode (location: (1,7)-(1,7))
        +-- @ CallNode (location: (1,10)-(1,12))*
        |   +-- receiver: nil
        |   +-- name: :m2
        |   +-- block:
        |       @ BlockNode (location: (1,14)-(1,16))
        |       +-- locals: [:(null)]
        |       +-- parameters:
        |       |   @ ItParametersNode (location: (1,17)-(1,17))
        |       +-- body:
        |       |   @ StatementsNode (location: (1,14)-(1,16))
        |       |   +-- body: (length: 1)
        |       |       +-- @ ItLocalVariableReadNode (location: (1,14)-(1,16))*
        +-- @ CallNode (location: (1,19)-(1,21))*
            +-- receiver: nil
            +-- name: :m3
            +-- block:
                @ BlockNode (location: (1,23)-(1,25))
                +-- locals: [:_1, :_2]
                +-- parameters:
                |   @ NumberedParametersNode (location: (1,26)-(1,26))
                |   +-- maximum: 2
                +-- body:
                |   @ StatementsNode (location: (1,23)-(1,25))
                |   +-- body: (length: 1)
                |       +-- @ LocalVariableReadNode (location: (1,23)-(1,25))*
                |           +-- name: :_2
                |           +-- depth: 0

おまけ: 暗黙に定義されるitとnumbered parameters

itとnumbered parametersは仮引数として定義する必要がありません。 これらはブロックの中で使用されている場合に自動的にBlockNodeparametersにノードが生えます。 どのようにして無からItParametersNodeNumberedParametersNodeが生まれるのかを調べてみましょう。

今回変更を加えたargs_with_numberedとその呼び出し元をもう一度みてみましょう。

static rb_node_t *
args_with_numbered(struct parser_params *p, rb_node_args_t *args, int max_numparam, ID it_id)
{
    rb_node_t *ret = (rb_node_t *)args;

    if (max_numparam > NO_PARAM || it_id) {
        YYLTYPE loc = RUBY_INIT_YYLLOC();

        if (it_id) {
            ret = NEW_RB_IT_PARAMETERS(&loc);
        }
        else {
            ret = NEW_RB_NUMBERED_PARAMETERS(max_numparam, &loc);
        }
    }
    return ret;
}

do_body     :   {
                        $$ = dyna_push(p);
                        CMDARG_PUSH(0);
                    }[dyna]<vars>
                  max_numparam numparam it_id allow_exits
                  opt_block_param_def[args] bodystmt
                    {
                        int max_numparam = p->max_numparam;
                        ID it_id = p->it_id;
                        ...
                        nd_args = args_with_numbered(p, $args, max_numparam, it_id);
                        $$ = NEW_RB_BLOCK(nd_args, $bodystmt, &@$);
                    }
                ;

do body endのうち、bodyの部分の解析が終わったタイミングで、p->max_numparamp->it_idを引数としてargs_with_numberedが呼び出されます。 関数の中ではp->max_numparamp->it_idの状態に応じてItParametersNodeを返すか、NumberedParametersNodeを返すか、argsを返すかを決定します。

p->it_idgettable関数の中でitという変数を認識したときにセットされます。 do_bodyという生成規則のアクションからみると、bodystmt構文解析したさいにitという変数が使われているかどうかはp->it_idを見れば確認することができます。

p->max_numparamNO_PARAM = 0で初期化されます。 仮引数が定義されていることがわかるとORDINAL_PARAM = -1が、_1のような変数が使用されていることがわかると変数の名前に応じて1から9の値がそれぞれセットされます。

enum {
    ORDINAL_PARAM = -1,
    NO_PARAM = 0,
    NUMPARAM_MAX = 9,
};

このようにしてargs_with_numbered関数ではp->max_numparamp->it_idをチェックすることで、itおよびnumbered parametersが使用されているかを判断して、事後的に仮引数用のノードをつくることができるのです。

まとめ

今日の成果です。

  • メソッド呼び出し時の引数に関して、parser側の書き換えをした

次回は引数のあるメソッド呼び出しをコンパイルできるようにしていきます。


  1. フラグを置きたかった場合でもArgumentsNodeに置くことで、構造をフラットにすることはできるような気がしますが。
  2. 関数の定義が短いためか、他の関数のプロトタイプ宣言に混ざって関数定義が書かれています。実際の定義がファイルの下のほうにあるのではと思って探し回るというのをよくやります。



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

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