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


Ruby Parser開発日誌 (24-5) - parse.yが生成するノードを変える ー メソッドを呼び出したい

5日目: メソッド呼び出し

メソッド定義ができるようになると、次は定義したメソッドを呼び出してみたくなるものです。 というわけで今日はメソッド呼び出しに取り組みます。

メソッド呼び出しに関連するノード

Rubyにはいろいろな形式のメソッド呼び出しがあります。 まずは既存のノードを眺めていきます。

書き換え前 書き換え後
obj.m(a, b) NODE_CALL CallNode
1 + 2 NODE_OPCALL CallNode
f(a, b) NODE_FCALL CallNode
f NODE_VCALL CallNode
obj&.m(a, b) NODE_QCALL CallNode

このほかにもstruct.field = foo(NODE_ATTRASGN)やary[1] += foo(NODE_OP_ASGN1)といったコードもメソッド呼び出しを含んでいますが、まずはベーシックな上記5つのメソッド呼び出しに取り組むことにします。

書き換え前は5種類のノードを使い分けています。 レシーバーが明示されているかどうかでコンパイル結果は変わるだろうし、引数がない場合は引数の処理をスキップできるだろうし、&.の場合はnilチェックして分岐する命令を入れるだろうし、ノードが分かれているとそういう区別がノードレベルでできるのでしょう。 NODE_OPCALLNODE_CALLから分離してあるのは引数が1つのケースでなにか省略できるものがあるのかもしれません。 parse.yやcompile.cを読む前の段階ではこのくらいを想像します。

これらのノードは書き換え後はすべてCallNodeで表現することになります。

parse.yを変更していく

parse.yでは5つのノードを作り分けているので、それぞれのノードに対してノード生成用のマクロが定義されています。

#define NEW_CALL(r,m,a,loc) (NODE *)rb_node_call_new(p,r,m,a,loc)
#define NEW_OPCALL(r,m,a,loc) (NODE *)rb_node_opcall_new(p,r,m,a,loc)
#define NEW_FCALL(m,a,loc) rb_node_fcall_new(p,m,a,loc)
#define NEW_VCALL(m,loc) (NODE *)rb_node_vcall_new(p,m,loc)
#define NEW_QCALL0(r,m,a,loc) (NODE *)rb_node_qcall_new(p,r,m,a,loc)

全てがCallNodeになるとはいえ、書き換え前と同じような使い心地になるように5種類のマクロを用意しておきます。 書き換えの前後で見た目が同じになるようにしておくことで、認知的な負荷を減らすのが目的です。 今回の書き換えはおそらく千行を超えるdiffになると思うので、将来の自分が見返したときに見やすいようにするのが大事だと思います1

#define NEW_RB_CALL(r,m,a,loc) (rb_node_t *)rb_new_node_call_new(p,r,m,a,loc)
#define NEW_RB_OPCALL(r,m,a,loc) NEW_RB_CALL(r,m,a,loc)
#define NEW_RB_FCALL(m,a,loc) (rb_call_node_t *)NEW_RB_CALL(0,m,a,loc)
#define NEW_RB_VCALL(m,loc) NEW_RB_CALL(0,m,0,loc)
#define NEW_RB_QCALL0(r,m,a,loc) NEW_RB_CALL(r,m,a,loc)

余談ですが生成規則のアクションではNEW_CALLNEW_RB_QCALL0をラップしたnew_qcallという関数を呼ぶことが多いので、マクロを変更したといってもアクションをいじる箇所は思ったよりも少なくすみます。

#define CALL_Q_P(q) ((q) == tANDDOT)
#define NEW_QCALL(q,r,m,a,loc) (CALL_Q_P(q) ? NEW_QCALL0(r,m,a,loc) : NEW_CALL(r,m,a,loc))
// 新しいノード用に用意した
#define NEW_RB_QCALL(q,r,m,a,loc) (CALL_Q_P(q) ? NEW_RB_QCALL0(r,m,a,loc) : NEW_RB_CALL(r,m,a,loc))

static NODE *
new_qcall(struct parser_params* p, ID atype, NODE *recv, ID mid, NODE *args, const YYLTYPE *op_loc, const YYLTYPE *loc)
{
    NODE *qcall = NEW_QCALL(atype, recv, mid, args, loc); // NEW_RB_QCALLに書き換える
    nd_set_line(qcall, op_loc->beg_pos.lineno);
    return qcall;
}

さてノード生成用のマクロが用意できたので、具体的な生成規則を見ていきましょう。

VCALLを書き換える

まずは一番要素の少ないvcall、つまりfのようにレシーバーも引数もない形でのメソッド呼び出しからいきましょう。 NEW_VCALLを検索するとヒットするのはgettable関数内部の一箇所のみです。

static NODE*
gettable(struct parser_params *p, ID id, const YYLTYPE *loc)
{
    ID *vidp = NULL;
    NODE *node;
    ...
    switch (id_type(id)) {
      case ID_LOCAL:
        if (dyna_in_block(p) && dvar_defined_ref(p, id, &vidp)) {
            if (NUMPARAM_ID_P(id) && (numparam_nested_p(p) || it_used_p(p))) return 0;
            if (vidp) *vidp |= LVAR_USED;
            node = NEW_DVAR(id, loc);
            return node;
        }
        if (local_id_ref(p, id, &vidp)) {
            if (vidp) *vidp |= LVAR_USED;
            node = NEW_LVAR(id, loc);
            return node;
        }
        if (dyna_in_block(p) && NUMPARAM_ID_P(id) &&
            parser_numbered_param(p, NUMPARAM_ID_TO_IDX(id))) {
            if (numparam_nested_p(p) || it_used_p(p)) return 0;
            node = NEW_DVAR(id, loc);
            struct local_vars *local = p->lvtbl;
            if (!local->numparam.current) local->numparam.current = node;
            return node;
        }
        /* method call without arguments */
        if (dyna_in_block(p) && id == idIt && !(DVARS_TERMINAL_P(p->lvtbl->args) || DVARS_TERMINAL_P(p->lvtbl->args->prev))) {
            if (numparam_used_p(p)) return 0;
            if (p->max_numparam == ORDINAL_PARAM) {
                compile_error(p, "ordinary parameter is defined");
                return 0;
            }
            if (!p->it_id) {
                p->it_id = idItImplicit;
                vtable_add(p->lvtbl->args, p->it_id);
            }
            NODE *node = NEW_DVAR(p->it_id, loc);
            if (!p->lvtbl->it) p->lvtbl->it = node;
            return node;
        }
        return NEW_VCALL(id, loc);
      case ID_GLOBAL:
        return NEW_GVAR(id, loc);
      case ID_INSTANCE:
        return NEW_IVAR(id, loc);
      case ID_CONST:
        return NEW_CONST(id, loc);
      case ID_CLASS:
        return NEW_CVAR(id, loc);
    }
    compile_error(p, "identifier %"PRIsVALUE" is not valid to get", rb_id2str(id));
    return 0;
}

この関数は与えられたidに応じてローカル変数やインスタンス変数のノードをなどを返す関数でした。 fだけをみてもローカル変数なのかメソッド呼び出しなのか分からないので、gettableでローカル変数ではないと判断されたときにVCALLノードを作っているんですね。 NEW_RB_VCALLを使うように書き換えます。

うまくいっているようにみえます。

$ ./miniruby --parser=parse.y --dump=p -e 'f'
@ ProgramNode (location: (1,0)-(1,1))
+-- locals: []
+-- statements:
    @ StatementsNode (location: (1,0)-(1,1))
    +-- body: (length: 1)
        +-- @ CallNode (location: (1,0)-(1,1))*
            +-- CallNodeFlags: nil
            +-- receiver: nil
            +-- call_operator_loc: nil
            +-- name: :f
            +-- message_loc: nil
            +-- opening_loc: nil
            +-- arguments: nil
            +-- closing_loc: nil
            +-- equal_loc: nil
            +-- block: nil

FCALLを書き換える

続いてはfcall、つまりf(a, b)のようにレシーバーがなくて、引数があるメソッド呼び出しです。 2つのアクションと1つの関数で使われています。

生成規則のほうはfcallprimaryです。

fcall       : operation
                    {
                        $$ = NEW_FCALL($1, 0, &@$);
                    }
            ;


primary     | tFID
                {
                    $$ = (NODE *)NEW_FCALL($1, 0, &@$);
                }

fcallは後ろに引数を取る記法のなかで、メソッド呼び出し部分を表しています。

// cmd 1, 2
command     : fcall command_args

// m(1, 2)
method_call : fcall paren_args

primaryのほうのtFIDとはなんでしょうか。 ttokenのことなので、lexerをみます。 lexerの補助関数であるparse_ident(identifierをlexする関数)に関連する処理が書いてあります。

static enum yytokentype
parse_ident(struct parser_params *p, int c, int cmd_state)
{
    enum yytokentype result;
    bool is_ascii = true;
    const enum lex_state_e last_state = p->lex.state;
    ID ident;
    int enforce_keyword_end = 0;

    do {
        if (!ISASCII(c)) is_ascii = false;
        if (tokadd_mbchar(p, c) == -1) return 0;
        c = nextc(p);
    } while (parser_is_identchar(p));
    if ((c == '!' || c == '?') && !peek(p, '=')) {
        result = tFID;
        tokadd(p, c);
    }

要するにident?ident!のときにtFIDが返ります。 ?!は変数名に使えないので、この時点で変数ではない(つまりメソッド呼び出しである)ことがわかります。

完全に余談ですが、!peek(p, '=')とはなんでしょうか。 peekはlexerに次の1バイトをチェックしてもらう関数です2。 つまり!?の次に=が来ないことをチェックしています。

!のほうは!=という二項演算子があるためでしょう。

# これは`v != 1`
v!=1

# これは`v! = 1`でSyntaxError
v! =1

?のほうはちょっとよく分からなくて、?=という二項演算子はありません。 なので?=で文字リテラルだと解釈されます。 ほかに?が絡む構文だと三項演算子がありますが、その場合はcond?=とはならないはずです。 もしこの辺について詳しい方がいたら、ご一報いただけると幸いです。

# これは`v(?=)`
v?=

# これは`v?(a)`
v?a

アクションの書き換えは特に難しいことはありません。

fcall       : operation
                    {
                        $$ = NEW_RB_FCALL($1, 0, &@$);
                    }
            ;

primary     | tFID
                {
                    $$ = (rb_node_t *)NEW_RB_FCALL($1, 0, &@$);
                }

関数ではparser_append_optionsという関数がNEW_FCALLを呼び出しています。

static NODE *
parser_append_options(struct parser_params *p, NODE *node)
{
    static const YYLTYPE default_location = {{1, 0}, {1, 0}};
    const YYLTYPE *const LOC = &default_location;

    if (p->do_print) {
        NODE *print = (NODE *)NEW_FCALL(rb_intern("print"),
                                NEW_LIST(NEW_GVAR(idLASTLINE, LOC), LOC),
                                LOC);
        node = block_append(p, node, print);
    }

    if (p->do_loop) {
        NODE *irs = NEW_LIST(NEW_GVAR(rb_intern("$/"), LOC), LOC);

        if (p->do_split) {
            ID ifs = rb_intern("$;");
            ID fields = rb_intern("$F");
            NODE *args = NEW_LIST(NEW_GVAR(ifs, LOC), LOC);
            NODE *split = NEW_GASGN(fields,
                                    NEW_CALL(NEW_GVAR(idLASTLINE, LOC),
                                             rb_intern("split"), args, LOC),
                                    LOC);
            node = block_append(p, split, node);
        }
        if (p->do_chomp) {
            NODE *chomp = NEW_SYM(rb_str_new_cstr("chomp"), LOC);
            chomp = list_append(p, NEW_LIST(chomp, LOC), NEW_TRUE(LOC));
            irs = list_append(p, irs, NEW_HASH(chomp, LOC));
        }

        node = NEW_WHILE((NODE *)NEW_FCALL(idGets, irs, LOC), node, 1, LOC, &NULL_LOC, &NULL_LOC);
    }

    return node;
}

rubyコマンドには-n-pというオプションがありますが、それらが与えられたときに生成される構文木に細工をするのがparser_append_options関数です。

$ ruby --parser=parse.y --dump=p -n -e 'puts $_'

# @ NODE_SCOPE (id: 3, line: 1, location: (1,0)-(1,7))
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_WHILE (id: 7, line: 1, location: (1,0)-(1,0))
#     +- nd_state: 1 (while-end)
#     +- nd_cond:
#     |   @ NODE_FCALL (id: 6, line: 1, location: (1,0)-(1,0))
#     |   +- nd_mid: :gets
#     |   +- nd_args:
#     |       @ NODE_LIST (id: 5, line: 1, location: (1,0)-(1,0))
#     |       +- as.nd_alen: 1
#     |       +- nd_head:
#     |       |   @ NODE_GVAR (id: 4, line: 1, location: (1,0)-(1,0))
#     |       |   +- nd_vid: :$/
#     |       +- nd_next:
#     |           (null node)
#     +- nd_body:
#     |   @ NODE_FCALL (id: 0, line: 1, location: (1,0)-(1,7))*
#     |   +- nd_mid: :puts
#     |   +- nd_args:
#     |       @ NODE_LIST (id: 2, line: 1, location: (1,5)-(1,7))
#     |       +- as.nd_alen: 1
#     |       +- nd_head:
#     |       |   @ NODE_GVAR (id: 1, line: 1, location: (1,5)-(1,7))
#     |       |   +- nd_vid: :$_
#     |       +- nd_next:
#     |           (null node)
#     +- keyword_loc: (0,-1)-(0,-1)
#     +- closing_loc: (0,-1)-(0,-1)

puts $_と書いただけなのに、NODE_WHILEgetsメソッドの呼び出しに関するノードが生成されています。 この辺の実装方法は今回のノード書き換えで変わると思うので3、今はあまり深く考えずにNEW_RB_FCALLを呼ぶように変更しておきましょう。

FCALLかVCALLか。それが問題だ

さて書き換え後のアクションをもう一度見てみます。

#define NEW_RB_FCALL(m,a,loc) (rb_call_node_t *)NEW_RB_CALL(0,m,a,loc)
#define NEW_RB_VCALL(m,loc) NEW_RB_CALL(0,m,0,loc)

fcall       : operation
                    {
                        $$ = NEW_RB_FCALL($1, 0, &@$);
                    }
            ;

primary     | tFID
                {
                    $$ = (rb_node_t *)NEW_RB_FCALL($1, 0, &@$);
                }

NEW_RB_FCALLの第二引数が0というのはNEW_RB_VCALLと変わらないわけです。 いままではFCALLノードとVCALLノードのように生成されるノードが異なっていたため、ノードのtypeをみることで区別がついていたわけですが、今回の変更ではどちらもCallNodeで表現するようになるので後から区別がつかなくなります。

これって区別がつかなくて大丈夫なんですよね? なんか嫌な予感がするので、compile.cをちょっとだけ読みます。

杞憂であってくれ...

NODE_FCALLもしくはNODE_VCALLという文字列で検索していくと、こういうコードが見つかります。

    switch ((int)type) {
      case NODE_VCALL:
        flag |= VM_CALL_VCALL;
        /* VCALL is funcall, so fall through */
      case NODE_FCALL:
        flag |= VM_CALL_FCALL;
    }

あ...

いや、まてまてまて、このフラグはきっと使ってないのでしょう。

$ git grep VM_CALL_VCALL
...
vm_insnhelper.c:    if (vm_ci_flag(ci) & VM_CALL_VCALL && !(vm_ci_flag(ci) & VM_CALL_FORWARDING)) stat |= MISSING_VCALL;

$ git grep MISSING_VCALL
vm_eval.c:    else if (last_call_status & MISSING_VCALL) {
...

おーん?

    else if (last_call_status & MISSING_VCALL) {
        format = rb_fstring_lit("undefined local variable or method '%1$s' for %3$s%4$s");
        exc = rb_eNameError;
    }

あー。。。

$ ruby --parser=parse.y -e 'm'
-e:1:in '<main>': undefined local variable or method 'm' for main (NameError)

m
^

$ ruby --parser=parse.y -e 'm?'
-e:1:in '<main>': undefined method 'm?' for main (NoMethodError)

m?
^^

エラーメッセージも例外のクラスも違います。 これではいけませんね。 CallNodeに色をつけないとだめですね。

VCALLノードの生成時にはRB_CALL_NODE_FLAGS_VARIABLE_CALLというフラグを渡すようにします。

#define NEW_RB_VCALL(m,loc) NEW_RB_CALL(0,m,0,RB_CALL_NODE_FLAGS_VARIABLE_CALL,loc)

QCALLを書き換える

QCALL、つまり&.を用いたメソッド呼び出しですが、これも他のメソッド呼び出しと区別できる必要があります。 &.の場合はレシーバーがnilかどうかのチェックをしてからメソッドを呼び出すはずだからです。 実際に生成されるバイトコードをみてみると、o.m(0000から0005)には存在しないbranchnilという命令が、o&.m(0006から0012)には含まれています。

$ ruby --parser=parse.y --dump=i -e 'o.m; o&.m'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,9)>
0000 putself                                                          (   1)[Li]
0001 opt_send_without_block                 <calldata!mid:o, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0003 opt_send_without_block                 <calldata!mid:m, argc:0, ARGS_SIMPLE>
0005 pop
0006 putself
0007 opt_send_without_block                 <calldata!mid:o, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0009 dup
0010 branchnil                              14
0012 opt_send_without_block                 <calldata!mid:m, argc:0, ARGS_SIMPLE>
0014 leave

RB_CALL_NODE_FLAGS_SAFE_NAVIGATIONというフラグを渡すようにします。

#define NEW_RB_QCALL0(r,m,a,loc) NEW_RB_CALL(r,m,a,RB_CALL_NODE_FLAGS_SAFE_NAVIGATION,loc)

OPCALLを書き換える

1 + 2といった二項演算や-""といった単項演算はNODE_OPCALLとして表現されてきました。 基本的にはレシーバーとメソッド名、そして二項演算の場合には引数があるという構造なので、通常のメソッド呼び出しと区別する必要はないように思えます。

念の為NODE_OPCALLというタイプに依存している箇所がないかparse.yを見ておきましょう。

static void
void_expr(struct parser_params *p, NODE *node)
{
    const char *useless = 0;

    if (!RTEST(ruby_verbose)) return;

    if (!node || !(node = nd_once_body(node))) return;
    switch (nd_type(node)) {
      case NODE_OPCALL:
        switch (RNODE_OPCALL(node)->nd_mid) {
          case '+':
          case '-':
          case '*':
            ...
            useless = rb_id2name(RNODE_OPCALL(node)->nd_mid);
            break;
        }
        break;

      case NODE_LVAR:
        ...
        break;
    }

    if (useless) {
        rb_warn1L(nd_line(node), "possibly useless use of %s in void context", WARN_S(useless));
    }
}

ん...?

$ ruby --parser=parse.y -wc -e '1 + 2; m'
-e:1: warning: possibly useless use of + in void context
Syntax OK

$ ruby --parser=parse.y -wc -e '1.+ 2; m'
Syntax OK

ああーーーー。

区別する必要があるんですねぇ。なるほどです。

で、そんなフラグはないわけだが...

/**
 * Flags for call nodes.
 */
typedef enum rb_call_node_flags {
    /** &. operator */
    RB_CALL_NODE_FLAGS_SAFE_NAVIGATION = 4,

    /** a call that could have been a local variable */
    RB_CALL_NODE_FLAGS_VARIABLE_CALL = 8,

    /** a call that is an attribute write, so the value being written should be returned */
    RB_CALL_NODE_FLAGS_ATTRIBUTE_WRITE = 16,

    /** a call that ignores method visibility */
    RB_CALL_NODE_FLAGS_IGNORE_VISIBILITY = 32,
} rb_call_node_flags_t;

prismの該当箇所を眺めてみると、call_operator_locmessage_locの有無をチェックしています。

うーん、まぁ。そういうこともあるか...

        case PM_CALL_NODE: {
            const pm_call_node_t *cast = (const pm_call_node_t *) node;
            if (cast->call_operator_loc.start != NULL || cast->message_loc.start == NULL) break;

            const pm_constant_t *message = pm_constant_pool_id_to_constant(&parser->constant_pool, cast->name);
            switch (message->length) {
                case 1:
                    switch (message->start[0]) {
                        case '+':

call_operator_locがないケースでmessage_locもないケースってどういうケースなんだ...

// call_operator_locがなくて、message_locがある。警告を出すのでよい
$ ruby --parser=prism --dump=p,i -e '1 + 2'
@ ProgramNode (location: (1,0)-(1,5))
+-- locals: []
+-- statements:
    @ StatementsNode (location: (1,0)-(1,5))
    +-- body: (length: 1)
        +-- @ CallNode (location: (1,0)-(1,5))
            +-- CallNodeFlags: nil
            +-- receiver:
            |   @ IntegerNode (location: (1,0)-(1,1))
            |   +-- IntegerBaseFlags: decimal
            |   +-- value: 1
            +-- call_operator_loc: nil
            +-- name: :+
            +-- message_loc: (1,2)-(1,3) = "+"

// call_operator_locがある
$ ruby --parser=prism --dump=p -e 'o.(1)'
        +-- @ CallNode (location: (1,0)-(1,5))
            +-- call_operator_loc: (1,1)-(1,2) = "."
            +-- name: :call
            +-- message_loc: nil

// call_operator_locがなくて、message_locがある。さらにnameをチェックすることで警告を出さないようにしている
$ ruby --parser=prism --dump=p -e 'a[1]'
        +-- @ CallNode (location: (1,0)-(1,4))
            +-- call_operator_loc: nil
            +-- name: :[]
            +-- message_loc: (1,1)-(1,4) = "[1]"
            +-- opening_loc: (1,1)-(1,2) = "["

うーん。わからない。 正直OPCALLもフラグを立てたらいいのではないかと思うけど、一旦同じ判定方法で実装しておくことにします。

ここまででレシーバー、メソッド名、各種フラグに対応できました。

self.m
+-- @ CallNode (location: (1,0)-(1,6))*
|   +-- CallNodeFlags: nil
|   +-- receiver:
|   |   @ SelfNode (location: (1,0)-(1,4))
|   +-- name: :m

true + false
+-- @ CallNode (location: (2,0)-(2,12))*
|   +-- CallNodeFlags: nil
|   +-- receiver:
|   |   @ TrueNode (location: (2,0)-(2,4))
|   +-- name: :+

f(false)
+-- @ CallNode (location: (3,3)-(8,1))*
|   +-- CallNodeFlags: nil
|   +-- receiver: nil
|   +-- name: :f

f
+-- @ CallNode (location: (4,0)-(4,1))*
|   +-- CallNodeFlags: variable_call
|   +-- receiver: nil
|   +-- name: :f

self&.m
+-- @ CallNode (location: (5,0)-(5,7))*
    +-- CallNodeFlags: safe_navigation
    +-- receiver:
    |   @ SelfNode (location: (5,0)-(5,4))
    +-- name: :m

compile.c

引数に関する変更をparse.yに入れてないですが、長くなってきたので今の段階で一度compile.cを変更します。

既存のコードをみてみるとcompile_call_precheck_freezeという特殊ケースでの最適化はあるものの、基本的には処理をcompile_callに任せているようです。

case NODE_CALL:   /* obj.foo */
case NODE_OPCALL: /* foo[] */
  if (compile_call_precheck_freeze(iseq, ret, node, node, popped) == TRUE) {
      break;
  }
case NODE_QCALL: /* obj&.foo */
case NODE_FCALL: /* foo() */
case NODE_VCALL: /* foo (variable or call) */
  if (compile_call(iseq, ret, node, type, node, popped, false) == COMPILE_NG) {
      goto ng;
  }
  break;

compile_callはというと、ノードの種類で分岐していることがわかります。

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)
{
    /* receiver */
    if (!assume_receiver) {
        if (type == NODE_CALL || type == NODE_OPCALL || type == NODE_QCALL) {
            ...
            if (type == NODE_QCALL) {
                else_label = qcall_branch_start(iseq, recv, &branches, node, line_node);
            }
        }
        else if (type == NODE_FCALL || type == NODE_VCALL) {
            ADD_CALL_RECEIVER(recv, line_node);
        }
    }

    /* args */
    if (type != NODE_VCALL) {
        argc = setup_args(iseq, args, get_nd_args(node), &flag, &keywords);
        CHECK(!NIL_P(argc));
    }
    else {
        argc = INT2FIX(0);
    }

    ...

    return COMPILE_OK;
}

あんまりいろいろ書き換えたくないので、新しくenumを定義して書き換え前と同様の種類分けができるようにしてみます。

enum node_call_type {
    N_CALL,   // obj.m
    N_OPCALL, // 1 + 2
    N_FCALL,  // f(1)
    N_VCALL,  // f
    N_QCALL   // obj&.m
};

static enum node_call_type
get_node_call_type(rb_call_node_t *node)
{
    if (rb_node_get_fl(node) & RB_CALL_NODE_FLAGS_VARIABLE_CALL) return N_VCALL;
    if (rb_node_get_fl(node) & RB_CALL_NODE_FLAGS_SAFE_NAVIGATION) return N_QCALL;
    if (!node->receiver) return N_FCALL;
    if (NULL_LOC_P(&node->call_operator_loc) && !NULL_LOC_P(&node->message_loc)) return N_OPCALL;
    return N_CALL;
}

RB_CALL_NODEの分岐のなかでさらにnode_call_typeの種類に応じて分岐をすることで、既存のコンパイル処理を流用できるようになります。

// Before
case NODE_CALL:   /* obj.foo */
case NODE_OPCALL: /* foo[] */
  if (compile_call_precheck_freeze(iseq, ret, node, node, popped) == TRUE) {
      break;
  }
case NODE_QCALL: /* obj&.foo */
case NODE_FCALL: /* foo() */
case NODE_VCALL: /* foo (variable or call) */
  if (compile_call(iseq, ret, node, type, node, popped, false) == COMPILE_NG) {
      goto ng;
  }
  break;

// After
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;
}

compile_callのなかでもnode_call_typeに応じて処理が変わるので、外側で計算したnode_call_typeを渡すようにします。 既存の処理を流用することでcompile_callおよびiseq_compile_each0のdiffはごくわずかですみます。

 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)
+compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, const enum node_type type, const NODE *const line_node, const enum node_call_type call_type, int popped, bool assume_receiver)
 {
     /* call:  obj.method(...)
      * fcall: func(...)
@@ -9663,7 +9663,7 @@ compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, co
 
     /* receiver */
     if (!assume_receiver) {
-        if (type == NODE_CALL || type == NODE_OPCALL || type == NODE_QCALL) {
+        if (call_type == N_CALL || call_type == N_OPCALL || call_type == N_QCALL) {
             int idx, level;
 
             if (mid == idCall &&
@@ -9679,17 +9679,17 @@ compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, co
                 CHECK(COMPILE(recv, "recv", get_nd_recv(node)));
             }
 
-            if (type == NODE_QCALL) {
+            if (call_type == N_QCALL) {
                 else_label = qcall_branch_start(iseq, recv, &branches, node, line_node);
             }
         }
-        else if (type == NODE_FCALL || type == NODE_VCALL) {
+        else if (call_type == N_FCALL || call_type == N_VCALL) {
             ADD_CALL_RECEIVER(recv, line_node);
         }
     }
 
     /* args */
-    if (type != NODE_VCALL) {
+    if (call_type != N_VCALL) {
         argc = setup_args(iseq, args, get_nd_args(node), &flag, &keywords);
         CHECK(!NIL_P(argc));
     }
@@ -9714,11 +9714,11 @@ compile_call(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, co
     debugp_param("call args argc", argc);
     debugp_param("call method", ID2SYM(mid));
 
-    switch ((int)type) {
-      case NODE_VCALL:
+    switch ((int)call_type) {
+      case N_VCALL:
         flag |= VM_CALL_VCALL;
         /* VCALL is funcall, so fall through */
-      case NODE_FCALL:
+      case N_FCALL:
         flag |= VM_CALL_FCALL;
     }
 
@@ -11082,6 +11082,26 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no
         break;
       }
 
+      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;
+      }
+
       case RB_ARRAY_NODE: {
         CHECK(compile_array(iseq, ret, node, &RB_NODE_ARRAY(node)->elements, popped, TRUE) >= 0);
         break;

動作確認

ここまでできたら実際にバイトコードを生成してみましょう。 とはいってもまだ引数の処理をなにもしていないのでobj.m(false)などを書くことはできません4。 うまいことVCALLになるような入力を選びます5

$ ruby --parser=parse.y --dump=i -e 'm'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,1)>
0000 putself                                                          (   1)[Li]
0001 opt_send_without_block                 <calldata!mid:m, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0003 leave

$ ./miniruby --parser=parse.y --dump=i -e 'm'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,1)>
0000 putself                                                          (   1)[Li]
0001 opt_send_without_block                 <calldata!mid:m, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0003 leave

良さそうです。

こうなると実際にメソッドを実行したくなりますね。

./miniruby --parser=parse.y -e 'p'
# 引数がないので特に何も表示されない

悲しい...

引数がなくても実行したことがわかるメソッドがないか考えると、raiseというメソッドがあることを思い出しました。

$ ./miniruby --parser=parse.y -e 'raise'
-e:1:in '<main>': unhandled exception

え、すごく嬉しい。 raise、今日から一番好きなメソッドはraiseです。

まとめ

今日の成果です。

  • 引数のないメソッド呼び出しができるようになった
  • 実際に#raiseを呼び出して実行できることが確認できた!!!

  1. だいたいこういう大規模な変更は入れてから半年くらいたったときに何か別の問題を踏んだ人によってgit blameされて、「この変更ってなんでいれたの?」と詳細なことを聞かれるんですよ。なのでいまから自分が見返したときに苦労しないようにしておくのが大事。
  2. ソースコードがマルチバイト文字を含むこともあるので1文字ではないこともあります。
  3. 新しい構文木の方針として、与えられたインプットから乖離したようなノードを生成しないという方針があるという理解をしているので、オプションを渡すと生成されるノードが変わるという方向には行かないんじゃないかと思っています。
  4. まだ移行が完了していないコードを実行すると、「assertionでこける > SEGVする > 意味不明な挙動をする」(左から順に運がよい)のいずれかをひくことになります。今回はassertionでこけました。
  5. VCALLは文法上引数がないことが確定するのでcompile.cでも引数のセットアップ処理をまるごとスキップしてくれます。



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

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