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


Ruby Parser開発日誌 (24-2) - parse.yが生成するノードを変える ー 変数と代入

2日目: 変数と代入

1日目にいくつかのリテラルとif/unlessのノードを移行しました。 数値リテラルや文字列リテラルなどほかのリテラルや、whileなどの他の制御構文に関するノードを置き換えてもいいのですが、2日目はちょっとそれらとは違う方向のノードを置き換えていきたいと思います。 ということで今回は変数と代入をやっていきたいと思います。

どの変数が簡単か

はじめに変数の種類を見ておきましょう。 ノードの一覧を眺めてもいいのですが、今回はparse.yにあるgettableという関数をみましょう。

static NODE*
gettable(struct parser_params *p, ID id, const YYLTYPE *loc)
{
    ...
    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;
        }
# if WARN_PAST_SCOPE
        if (!p->ctxt.in_defined && RTEST(ruby_verbose) && past_dvar_p(p, id)) {
            rb_warning1("possible reference to past scope - %"PRIsWARN, rb_id2str(id));
        }
# endif
        /* method call without arguments */
        if (dyna_in_block(p) && id == rb_intern("it") && !(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 = internal_id(p);
                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;
}

ざっと以下のような変数があることがわかりますね。

定数も変数といえば変数か...という気持ちになったところで、どの変数が簡単かを考えてみます。

普段一番よく使うしローカル変数が一番簡単なはず!と思ってはいけません。 ちゃんとgettableのなかの分岐の長さをみましたか? という冗談はさておき1、それぞれをコンパイルした結果を観察してみましょう。

$ ruby --parser=parse.y --dump=i -e 'lv = 1; lv'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,10)>
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] lv@0
0000 putobject_INT2FIX_1_                                             (   1)[Li]
0001 setlocal_WC_0                          lv@0
0003 getlocal_WC_0                          lv@0
0005 leave

$ ruby --parser=parse.y --dump=i -e '$gv'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,3)>
0000 getglobal                              :$gv                      (   1)[Li]
0002 leave

$ ruby --parser=parse.y --dump=i -e '@iv'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,3)>
0000 getinstancevariable                    :@iv, <is:0>              (   1)[Li]
0003 leave

$ ruby --parser=parse.y --dump=i -e 'Const'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,5)>
0000 opt_getconstant_path                   <ic:0 Const>              (   1)[Li]
0002 leave

$ ruby --parser=parse.y --dump=i -e '@@cv'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,4)>
0000 getclassvariable                       :@@cv, <is:0>             (   1)[Li]
0003 leave

ローカル変数のときだけlocal tableというものが出現しています。 詳しくは「Rubyのしくみ Ruby Under a Microscope」などをみていただくとして、ローカル変数はスコープに付随するlocal tableに格納されます。 ということでローカル変数を取り扱うさいには、iseqのlocal tableのことを考えないといけません。 といってもこの辺の処理はcompile.cにすでにあるはずです2

では何が面倒かというと、いわゆるdynamic variable (DVAR)というやつが問題になってきます。 ためしに次のスクリプトバイトコードを見てみましょう3

sum = 0

10.times do |i|
  j = i
  sum += j
end
$ ruby --parser=parse.y --dump=i test.rb

== disasm: #<ISeq:<main>@test.rb:1 (1,0)-(6,3)>
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] sum@0
0000 putobject_INT2FIX_0_                                             (   1)[Li]
0001 setlocal_WC_0                          sum@0
0003 putobject                              10                        (   3)[Li]
0005 send                                   <calldata!mid:times, argc:0>, block in <main>
0008 leave

== disasm: #<ISeq:block in <main>@test.rb:3 (3,9)-(6,3)>
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] i@0<AmbiguousArg>[ 1] j@1
0000 getlocal_WC_0                          i@0                       (   4)[LiBc]
0002 setlocal_WC_0                          j@1
0004 getlocal_WC_1                          sum@0                     (   5)[Li]
0006 getlocal_WC_0                          j@1
0008 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0010 dup
0011 setlocal_WC_1                          sum@0
0013 leave                                                            (   6)[Br]

注目すべきところは

  1. ブロックの外側と内側に対応するように、2つのiseqがつくられている
  2. sumはブロックの外側のiseqのlocal tableで管理しているのに対して、ijはブロックのiseqで管理している
  3. 外側のiseqでsumを触るときはsetlocal_WC_0という命令をつかっているのに対して、ブロックのiseqで触るときはsetlocal_WC_1という命令をつかっている

このWC_0WC_1といった数値がどのくらい外側のlocal tableを見に行くかを表しています。

一方のノードはというと、もともとのノードではLVARDVARというノードを使いわけていますし、書き換え後のLOCAL_VARIABLE_READ_NODEにはuint32_t depthというフィールドがあります。

$ ruby --parser=parse.y --dump=p test.rb
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (id: 19, line: 1, location: (1,0)-(6,3))
# +- nd_tbl: :sum
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_BLOCK (id: 17, line: 1, location: (1,0)-(6,3))
#     +- nd_head (1):
#     |   @ NODE_LASGN (id: 0, line: 1, location: (1,0)-(1,7))*
#     |   +- nd_vid: :sum
#     |   +- nd_value:
#     |       @ NODE_INTEGER (id: 1, line: 1, location: (1,6)-(1,7))
#     |       +- val: 0
#     +- nd_head (2):
#         @ NODE_ITER (id: 16, line: 3, location: (3,0)-(6,3))*
#         +- nd_iter:
#         |   @ NODE_CALL (id: 3, line: 3, location: (3,0)-(3,8))
#         |   +- nd_mid: :times
#         |   +- nd_recv:
#         |   |   @ NODE_INTEGER (id: 2, line: 3, location: (3,0)-(3,2))
#         |   |   +- val: 10
#         |   +- nd_args:
#         |       (null node)
#         +- nd_body:
#             @ NODE_SCOPE (id: 15, line: 3, location: (3,9)-(6,3))
#             +- nd_tbl: :i,:j
#             +- nd_args:
#             |   @ NODE_ARGS (id: 5, line: 3, location: (3,13)-(3,14))
#             |   +- nd_ainfo.forwarding: 0 (no forwarding)
#             |   +- nd_ainfo.pre_args_num: 1
#             |   +- nd_ainfo.pre_init:
#             |   |   (null node)
#             |   +- nd_ainfo.post_args_num: 0
#             |   +- nd_ainfo.post_init:
#             |   |   (null node)
#             |   +- nd_ainfo.first_post_arg: (null)
#             |   +- nd_ainfo.rest_arg: (null)
#             |   +- nd_ainfo.block_arg: (null)
#             |   +- nd_ainfo.opt_args:
#             |   |   (null node)
#             |   +- nd_ainfo.kw_args:
#             |   |   (null node)
#             |   +- nd_ainfo.kw_rest_arg:
#             |       (null node)
#             +- nd_body:
#                 @ NODE_BLOCK (id: 13, line: 4, location: (4,2)-(5,10))
#                 +- nd_head (1):
#                 |   @ NODE_DASGN (id: 6, line: 4, location: (4,2)-(4,7))*
#                 |   +- nd_vid: :j
#                 |   +- nd_value:
#                 |       @ NODE_DVAR (id: 7, line: 4, location: (4,6)-(4,7))
#                 |       +- nd_vid: :i
#                 +- nd_head (2):
#                     @ NODE_DASGN (id: 8, line: 5, location: (5,2)-(5,10))*
#                     +- nd_vid: :sum
#                     +- nd_value:
#                         @ NODE_CALL (id: 12, line: 5, location: (5,2)-(5,10))
#                         +- nd_mid: :+
#                         +- nd_recv:
#                         |   @ NODE_DVAR (id: 10, line: 5, location: (5,2)-(5,5))
#                         |   +- nd_vid: :sum
#                         +- nd_args:
#                             @ NODE_LIST (id: 11, line: 5, location: (5,9)-(5,10))
#                             +- as.nd_alen: 1
#                             +- nd_head:
#                             |   @ NODE_DVAR (id: 9, line: 5, location: (5,9)-(5,10))
#                             |   +- nd_vid: :j
#                             +- nd_next:
#                                 (null node)

$ ruby --parser=prism --dump=p test.rb
@ ProgramNode (location: (1,0)-(6,3))
+-- locals: [:sum]
+-- statements:
    @ StatementsNode (location: (1,0)-(6,3))
    +-- body: (length: 2)
        +-- @ LocalVariableWriteNode (location: (1,0)-(1,7))
        |   +-- name: :sum
        |   +-- depth: 0
        |   +-- name_loc: (1,0)-(1,3) = "sum"
        |   +-- value:
        |   |   @ IntegerNode (location: (1,6)-(1,7))
        |   |   +-- IntegerBaseFlags: decimal
        |   |   +-- value: 0
        |   +-- operator_loc: (1,4)-(1,5) = "="
        +-- @ CallNode (location: (3,0)-(6,3))
            +-- CallNodeFlags: nil
            +-- receiver:
            |   @ IntegerNode (location: (3,0)-(3,2))
            |   +-- IntegerBaseFlags: decimal
            |   +-- value: 10
            +-- call_operator_loc: (3,2)-(3,3) = "."
            +-- name: :times
            +-- message_loc: (3,3)-(3,8) = "times"
            +-- opening_loc: nil
            +-- arguments: nil
            +-- closing_loc: nil
            +-- equal_loc: nil
            +-- block:
                @ BlockNode (location: (3,9)-(6,3))
                +-- locals: [:i, :j]
                +-- parameters:
                |   @ BlockParametersNode (location: (3,12)-(3,15))
                |   +-- parameters:
                |   |   @ ParametersNode (location: (3,13)-(3,14))
                |   |   +-- requireds: (length: 1)
                |   |   |   +-- @ RequiredParameterNode (location: (3,13)-(3,14))
                |   |   |       +-- ParameterFlags: nil
                |   |   |       +-- name: :i
                |   |   +-- optionals: (length: 0)
                |   |   +-- rest: nil
                |   |   +-- posts: (length: 0)
                |   |   +-- keywords: (length: 0)
                |   |   +-- keyword_rest: nil
                |   |   +-- block: nil
                |   +-- locals: (length: 0)
                |   +-- opening_loc: (3,12)-(3,13) = "|"
                |   +-- closing_loc: (3,14)-(3,15) = "|"
                +-- body:
                |   @ StatementsNode (location: (4,2)-(5,10))
                |   +-- body: (length: 2)
                |       +-- @ LocalVariableWriteNode (location: (4,2)-(4,7))
                |       |   +-- name: :j
                |       |   +-- depth: 0
                |       |   +-- name_loc: (4,2)-(4,3) = "j"
                |       |   +-- value:
                |       |   |   @ LocalVariableReadNode (location: (4,6)-(4,7))
                |       |   |   +-- name: :i
                |       |   |   +-- depth: 0
                |       |   +-- operator_loc: (4,4)-(4,5) = "="
                |       +-- @ LocalVariableOperatorWriteNode (location: (5,2)-(5,10))
                |           +-- name_loc: (5,2)-(5,5) = "sum"
                |           +-- binary_operator_loc: (5,6)-(5,8) = "+="
                |           +-- value:
                |           |   @ LocalVariableReadNode (location: (5,9)-(5,10))
                |           |   +-- name: :j
                |           |   +-- depth: 0
                |           +-- name: :sum
                |           +-- binary_operator: :+
                |           +-- depth: 1
                +-- opening_loc: (3,9)-(3,11) = "do"
                +-- closing_loc: (6,0)-(6,3) = "end"

話が長くなりましたが、実際はこの辺のことを10秒くらい考えて、ローカル変数は後回しにしようという決意を固めます。 ということでローカル変数を除いた4つの変数のノードを置き換えていきましょう。

parse.yの変更

parse.yの変更箇所はgettable関数に閉じています。 新しいノードを生成するマクロを定義して、gettableの該当箇所を書き換えます。

@@ -13320,11 +13363,11 @@ gettable(struct parser_params *p, ID id, const YYLTYPE *loc)
       case ID_GLOBAL:
         return NEW_GVAR(id, loc);
       case ID_INSTANCE:
-        return NEW_IVAR(id, loc);
+        return NEW_RB_INSTANCE_VARIABLE_READ(id, loc);
       case ID_CONST:
-        return NEW_CONST(id, loc);
+        return NEW_RB_CONSTANT_READ(id, loc);
       case ID_CLASS:
-        return NEW_CVAR(id, loc);
+        return NEW_RB_CLASS_VARIABLE_READ(id, loc);
     }
     compile_error(p, "identifier %"PRIsVALUE" is not valid to get", rb_id2str(id));
     return 0;

compile.cの変更

ノードの置き換えが1対1に対応しているので、compile.cの変更も基本的にはキャストの変更とアクセスするフィールド名の変更ですみます。 インスタンス変数へのアクセスを例にあげてみましたが、ほとんど差分がないことがわかると思います。

// Before
case NODE_IVAR:{
  debugi("nd_vid", RNODE_IVAR(node)->nd_vid);
  if (!popped) {
      ADD_INSN2(ret, node, getinstancevariable,
                ID2SYM(RNODE_IVAR(node)->nd_vid),
                get_ivar_ic_value(iseq, RNODE_IVAR(node)->nd_vid));
  }
  break;
}

// After
case RB_INSTANCE_VARIABLE_READ_NODE: {
  debugi("name", RB_NODE_INSTANCE_VARIABLE_READ(node)->name);
  if (!popped) {
      ADD_INSN2(ret, node, getinstancevariable,
                ID2SYM(RB_NODE_INSTANCE_VARIABLE_READ(node)->name),
                get_ivar_ic_value(iseq, RB_NODE_INSTANCE_VARIABLE_READ(node)->name));
  }
  break;
}

なんか... 代入の種類が多い...

つい本音が漏れましたが、変数の代入って種類が多いんですよ。 先ほどあげた5つの変数への代入以外にもこういった代入があります。

  1. multiple assignment(a, b = foo)
  2. attribute assignment(struct.field = foo)
  3. operator assignment 1(ary[1] += foo)
  4. operator assignment 2(struct.field += foo)
  5. operator assignment and(foo &&= bar)
  6. operator assignment or(foo ||= bar)

attribute assignmentやoperator assignmentはコンパイル結果が長くなりそうですし、multiple assignmentにいたってはどういうコンパイル結果になるかパッと思いつきません。 使うときはどれも無意識に使うから全く気にならないんですけどね。 これらの複雑な代入は後回しにしましょう。

それから定数への代入も後回しにします。 定数に代入するところはsetconstantを使えばいいのですが、他の変数とちがって定数のパスはネストすることができるので、代入の左辺がやや複雑なのです。

# インスタンス変数はネストできないが
@iv = 1

# 定数は `::` を用いることでネストできる
A::B::C::D = 1

ということで以下の3つに関してノードを書き換えていくことにします。

parse.yの変更

parse.yにおける代入のノードの扱いは2つのステップからなります。

  1. 代入を表すノードを生成する
  2. 1で作ったノードに右辺に相当するノードを設定する
// @iv = 1
// ステップ2: 1で作ったノードに右辺に相当するノードを設定する
arg : lhs '=' arg_rhs
        {
            $$ = node_assign(p, (NODE *)$lhs, $rhs, $lex_ctxt, &@$);
        }
    ;

// ステップ1: 代入を表すノードを生成する
lhs : user_or_keyword_variable
        {
            $$ = assignable(p, $1, 0, &@$);
        }
    ;

// user_variableは@ivなど
// keyword_variableはnilやselfなど
user_or_keyword_variable : user_variable
                         | keyword_variable
                         ;

ステップ1ではassignableという関数が呼び出されます。 assignableは渡されたidに応じて適切なノードを生成して返します。 書き換える対象のノードについて、生成するマクロを変更すればよいでしょう。

static NODE*
assignable(struct parser_params *p, ID id, NODE *val, const YYLTYPE *loc)
{
    const char *err = 0;
    int node_type = assignable0(p, id, &err);
    switch (node_type) {
      case NODE_DASGN: return NEW_DASGN(id, val, loc);
      case NODE_LASGN: return NEW_LASGN(id, val, loc);
      case NODE_GASGN: return NEW_GASGN(id, val, loc);
      case NODE_IASGN: return NEW_IASGN(id, val, loc);
      case NODE_CDECL: return NEW_CDECL(id, val, 0, p->ctxt.shareable_constant_value, loc);
      case NODE_CVASGN: return NEW_CVASGN(id, val, loc);
    }
/* TODO: FIXME */
#ifndef RIPPER
    if (err) yyerror1(loc, err);
#else
    if (err) set_value(assign_error(p, err, p->s_lvalue));
#endif
    return NEW_ERROR(loc);
}

assignable0idに応じたノードの種別を判定するための関数で、self = 1のような入力をエラーにする役割も担っています。 こちらも書き換えるノードについて、ノードのタイプを変更すればよいでしょう。

static int
assignable0(struct parser_params *p, ID id, const char **err)
{
    if (!id) return -1;
    switch (id) {
      case keyword_self:
        *err = "Can't change the value of self";
        return -1;
      case keyword_nil:
        *err = "Can't assign to nil";
        return -1;
      ...
    }
    switch (id_type(id)) {
      case ID_LOCAL:
        if (dyna_in_block(p)) {
            if (p->max_numparam > NO_PARAM && NUMPARAM_ID_P(id)) {
                compile_error(p, "Can't assign to numbered parameter _%d",
                              NUMPARAM_ID_TO_IDX(id));
                return -1;
            }
            if (dvar_curr(p, id)) return NODE_DASGN;
            if (dvar_defined(p, id)) return NODE_DASGN;
            if (local_id(p, id)) return NODE_LASGN;
            dyna_var(p, id);
            return NODE_DASGN;
        }
        else {
            if (!local_id(p, id)) local_var(p, id);
            return NODE_LASGN;
        }
        break;
      case ID_GLOBAL: return NODE_GASGN;
      case ID_INSTANCE: return NODE_IASGN;
      case ID_CONST:
        if (!p->ctxt.in_def) return NODE_CDECL;
        *err = "dynamic constant assignment";
        return -1;
      case ID_CLASS: return NODE_CVASGN;
      default:
        compile_error(p, "identifier %"PRIsVALUE" is not valid to set", rb_id2str(id));
    }
    return -1;
}

ステップ2ではnode_assignという関数が呼ばれます。 基本的にはset_nd_valueという関数に処理を丸投げしているので、それぞれの関数でノードのタイプとキャスト用のマクロを書き換えればよいでしょう。 NODE_ATTRASGN(struct.field = fooの形をした代入)がチラッと見えましたが、コメントアウトするなどしてお茶を濁します。

static NODE *
node_assign(struct parser_params *p, NODE *lhs, NODE *rhs, struct lex_context ctxt, const YYLTYPE *loc)
{
    if (!lhs) return 0;

    switch (nd_type(lhs)) {
      case NODE_CDECL:
      case NODE_GASGN:
      case NODE_IASGN:
      case NODE_LASGN:
      case NODE_DASGN:
      case NODE_MASGN:
      case NODE_CVASGN:
        set_nd_value(p, lhs, rhs);
        nd_set_loc(lhs, loc);
        break;

      case NODE_ATTRASGN:
        RNODE_ATTRASGN(lhs)->nd_args = arg_append(p, RNODE_ATTRASGN(lhs)->nd_args, rhs, loc);
        nd_set_loc(lhs, loc);
        break;

      default:
        /* should not happen */
        break;
    }

    return lhs;
}

static void
set_nd_value(struct parser_params *p, NODE *node, NODE *rhs)
{
    switch (nd_type(node)) {
      case NODE_CDECL:
        RNODE_CDECL(node)->nd_value = rhs;
        break;
      case NODE_GASGN:
        RNODE_GASGN(node)->nd_value = rhs;
        break;
      case NODE_IASGN:
        RNODE_IASGN(node)->nd_value = rhs;
        break;
      case NODE_LASGN:
        RNODE_LASGN(node)->nd_value = rhs;
        break;
      case NODE_DASGN:
        RNODE_DASGN(node)->nd_value = rhs;
        break;
      case NODE_MASGN:
        RNODE_MASGN(node)->nd_value = rhs;
        break;
      case NODE_CVASGN:
        RNODE_CVASGN(node)->nd_value = rhs;
        break;
      default:
        compile_error(p, "set_nd_value: unexpected node: %s", parser_node_name(nd_type(node)));
        break;
    }
}

compile.cの変更

書き換えの前後でノードが1対1に対応しているので、compile.cもキャストするためのマクロやアクセスするフィールド名を変えるだけで済みます。 参考までにインスタンス変数への代入に関して書き換えの前後のコードを並べておきます。

// Before
case NODE_IASGN:{
  CHECK(COMPILE(ret, "lvalue", RNODE_IASGN(node)->nd_value));
  if (!popped) {
      ADD_INSN(ret, node, dup);
  }
  ADD_INSN2(ret, node, setinstancevariable,
            ID2SYM(RNODE_IASGN(node)->nd_vid),
            get_ivar_ic_value(iseq,RNODE_IASGN(node)->nd_vid));
  break;
}

// After
case RB_INSTANCE_VARIABLE_WRITE_NODE: {
  CHECK(COMPILE(ret, "lvalue", RB_NODE_INSTANCE_VARIABLE_WRITE(node)->value));
  if (!popped) {
      ADD_INSN(ret, node, dup);
  }
  ADD_INSN2(ret, node, setinstancevariable,
            ID2SYM(RB_NODE_INSTANCE_VARIABLE_WRITE(node)->name),
            get_ivar_ic_value(iseq, RB_NODE_INSTANCE_VARIABLE_WRITE(node)->name));
  break;
}

これらの変更によって変数への代入と参照ができるようになりました。 一部で位置情報がおかしくなっていますが、最後にまとめて直せばよいでしょう。

$ ./miniruby --parser=parse.y --dump=p,i -e '@iv = self; @iv'
@ ProgramNode (location: (1,0)-(1,15))
+-- locals: []
+-- statements:
    @ StatementsNode (location: (1,0)-(1,15))
    +-- body: (length: 2)
        +-- @ InstanceVariableWriteNode (location: (1,0)-(1,10))*
        |   +-- name: :@iv
        |   +-- name_loc: (0,4294967295)-(0,4294967295) = ""
        |   +-- value:
        |   |   @ SelfNode (location: (1,6)-(1,10))
        |   +-- operator_loc: (0,4294967295)-(0,4294967295) = ""
        +-- @ InstanceVariableReadNode (location: (1,12)-(1,15))*
            +-- name: :@iv
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,15)>
0000 putself                                                          (   1)[Li]
0001 setinstancevariable                    :@iv, <is:0>
0004 getinstancevariable                    :@iv, <is:1>
0007 leave

おまけ: iseqの命令を調べる

iseqを眺めていると「これってどういう命令だっけ」とか、「オペランドが複数あるけどそれぞれなにを意味していたっけ」などといった疑問が生じることがあります。 生成されるバイトコードが等しいことをもってノードの置き換えを進めていく今回の作業においては、そのような疑問は完全に邪念でしかないのですが、手元にソースコードがある以上は手が滑ってバイトコードを調べてしまうこともあるでしょう。

というわけでgetlocal_WC_0を調べてみることにしましょう。 バイトコードについて調べるときはまずinsns.defというファイルを眺めます。 getlocal_WC_0という命令はここにはなくて、代わりにgetlocalという命令が見つかります。

/**********************************************************/
/* deal with variables                                    */
/**********************************************************/

/* Get local variable (pointed by `idx' and `level').
     'level' indicates the nesting depth from the current block.
 */
DEFINE_INSN
getlocal
(lindex_t idx, rb_num_t level)
()
(VALUE val)
{
    val = *(vm_get_ep(GET_EP(), level) - idx);
    RB_DEBUG_COUNTER_INC(lvar_get);
    (void)RB_DEBUG_COUNTER_INC_IF(lvar_get_dynamic, level > 0);
}

簡潔にして十分な説明が書いてあることが多いので、ちゃんとコメントを読みます。

この命令はindex(idx)とブロックの深さ(level)を受け取って、ローカル変数を返すようです。 あるスコープのローカル変数は並べて置いてあるため、indexを用いて指定することができます。 ブロックの深さというのはさきほどDVARでみたように、いまいるブロックの外側にあるブロックに触ることがあるので、そこまでの距離を表しています。

命令のオペランドlevelがあるということは、ブロックの深さについてはコンパイル時に計算しておく必要があります。 先送りしてよかったですね。

RB_DEBUG_COUNTER_INCRB_DEBUG_COUNTER_INC_IFはきっとデバッグ用のカウンターを増やす処理でしょうから、この命令のキモは1行目でしょう。 GET_EP()でその時点のEnvironment Pointerを取得して、vm_get_eplevelの分だけEnvironment Pointerを戻るのでしょう。 そしてEnvironment Pointerはローカル変数の配列の先頭へのポインタになっているから、idx分を調整することでお目当ての変数(のポインタ)が手に入るというようになっていることでしょう。 この辺はあまり詳細に立ち入らず「そういう実装になっていないと動かないはずなので、そうなっているはず」という気持ちで読んでいます。

ちょっと脱線をしている気もしますが、getlocal_WC_0がどこで定義されているか調べましょう。 insns.defはそのままではCのコードとして扱えません。ということはこのファイルを元にしてCのファイルを生成しているはずです。 その手の生成物はビルドを行っているディレクトリに作られることが多いので、そちらを検索してみます。 insns_info.inc, insns.incなどいくつかのファイルがヒットしますが、vm.incというファイルに命令の具体的な内容が書いてあるようです。

デバッグ用のマクロや、どの命令でも共通したセットアップ処理などを読み飛ばすとこんな感じのコードになっています。 getlocalでは2つ目のオペランドlevelを初期化するのに対して、getlocal_WC_0では0getlocal_WC_1では1levelを初期化しています。 勝手な想像ですが、GET_OPERANDを呼び出すコストを軽減するというより、バイトコードオペランドを節約するための工夫かなと思います。

// vm.inc

/* insn getlocal(idx, level)()(val) */
INSN_ENTRY(getlocal)
{
    /* ###  Declare and assign variables. ### */
    lindex_t idx = (lindex_t)GET_OPERAND(1);
    rb_num_t level = (rb_num_t)GET_OPERAND(2);
    const bool MAYBE_UNUSED(leaf) = INSN_ATTR(leaf);
    VALUE val;

    /* ### Here we do the instruction body. ### */
#   line 81 "../../insns.def"
{
    val = *(vm_get_ep(GET_EP(), level) - idx);
    RB_DEBUG_COUNTER_INC(lvar_get);
    (void)RB_DEBUG_COUNTER_INC_IF(lvar_get_dynamic, level > 0);
}
#   line 95 "vm.inc"
}

/* insn getlocal_WC_0(idx)()(val) */
INSN_ENTRY(getlocal_WC_0)
{
    /* ###  Declare and assign variables. ### */
#line 10 "../../defs/opt_operand.def"
    const rb_num_t level = 0;
#line 4641 "vm.inc"
    lindex_t idx = (lindex_t)GET_OPERAND(1);
    const bool MAYBE_UNUSED(leaf) = INSN_ATTR(leaf);
    VALUE val;

    /* ### Here we do the instruction body. ### */
#   line 81 "../../insns.def"
{
    val = *(vm_get_ep(GET_EP(), level) - idx);
    RB_DEBUG_COUNTER_INC(lvar_get);
    (void)RB_DEBUG_COUNTER_INC_IF(lvar_get_dynamic, level > 0);
}
#   line 4661 "vm.inc"
}

/* insn getlocal_WC_1(idx)()(val) */
INSN_ENTRY(getlocal_WC_1)
{
    /* ###  Declare and assign variables. ### */
#line 11 "../../defs/opt_operand.def"
    const rb_num_t level = 1;
#line 4687 "vm.inc"
    lindex_t idx = (lindex_t)GET_OPERAND(1);
    const bool MAYBE_UNUSED(leaf) = INSN_ATTR(leaf);
    VALUE val;

    /* ### Here we do the instruction body. ### */
#   line 81 "../../insns.def"
{
    val = *(vm_get_ep(GET_EP(), level) - idx);
    RB_DEBUG_COUNTER_INC(lvar_get);
    (void)RB_DEBUG_COUNTER_INC_IF(lvar_get_dynamic, level > 0);
}
#   line 4707 "vm.inc"
}

さて、これらの命令の差分は"defs/opt_operand.def"というファイルで定義されているようです。 getlocal *, 0という定義の*はWildCardということなので、最終的にgetlocal_WC_0という命令が生成されることになるようです。

#
# configuration file for operand union optimization
#
# format:
#   [insn name] op1, op2 ...
#
#  wildcard: *
#

getlocal *, 0
getlocal *, 1
setlocal *, 0
setlocal *, 1

putobject INT2FIX(0)
putobject INT2FIX(1)

__END__

putobject Qtrue
putobject Qfalse

おまけ2: vm.incが生成される仕組みを調べる

脱線ついでに"defs/opt_operand.def"からvm.incが生成される仕組みについても見ておきましょう。 defs/opt_operand.defでgrepをすると"tool/ruby_vm/loaders/opt_operand_def.rb"というファイルがヒットします。 そもそも"tool/ruby_vm/"とは...? という気持ちになるので、ディレクトリを見てみましょう。

controllers, helpers, models, viewsといったなにか見覚えのあるディレクトリが並んでいます。 controllersのなかにはapplication_controller.rbというファイルが置かれていて、「なんだRailsか」という気持ちになります。

$ ls tool/ruby_vm
controllers helpers     loaders     models      scripts     views

真面目な話をすると、loadersに置かれているファイルが定義ファイルを読み込んで、modelでラップして、viewsに置いてあるテンプレートファイルをもとにCのファイルを生成しているようです。 ということはloaderが定義ファイルのparseをしているんだろうなと思ってみてみると、やっぱり文法定義が書かれていました4

# tool/ruby_vm/loaders/opt_operand_def.rb
require_relative '../helpers/scanner'

json    = []
scanner = RubyVM::Scanner.new '../../../defs/opt_operand.def'
path    = scanner.__FILE__
grammar = %r/
    (?<comment> \# .+? \n                      ){0}
    (?<ws>      \g<comment> | \s               ){0}
    (?<insn>    \w+                            ){0}
    (?<paren>   \( (?: \g<paren> | [^()]+)* \) ){0}
    (?<expr>    (?: \g<paren> | [^(),\ \n] )+  ){0}
    (?<remain>  \g<expr>                       ){0}
    (?<arg>     \g<expr>                       ){0}
    (?<extra>   , \g<ws>* \g<remain>           ){0}
    (?<args>    \g<arg> \g<extra>*             ){0}
    (?<decl>    \g<insn> \g<ws>+ \g<args> \n   ){0}
/mx

まとめ

今日の成果です。

あきらかに寄り道が多いので、次回は寄り道せずに進みたいものです。


  1. ID_LOCALのときはit_1を考慮する必要があるので分岐が複雑になっているだけで、基本的にはLVAR(ローカル変数)、DVAR(ダイナミック変数)、VCALL(メソッド呼び出し)のいずれかになると理解すればOKです。問題はDVARというやつにあって... 続きは本編で。
  2. 具体的にはiseq_set_local_tableという関数があります。
  3. make runrubymake runで実行するデフォルトのファイル名がtest.rbなので、test.rbを使いがち。
  4. 文法の定義はBNFで行いたいけど、parser generatorをもってくるのは大がかりなので正規表現で記述する。わかります...



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

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