以下の内容はhttps://yui-knk.hatenablog.com/entry/2026/01/08/000300より取得しました。


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

16日目: 定数の参照と代入

しばらくmultiple assignmentの実装が続いたので、今回はちょっと目先を変えて定数の参照と代入をやっていきます。

ノードを眺める

いつものように書き換え前後のノードの確認からはじめましょう。

書き換え前 書き換え後
A, A::BA NODE_CONST ConstantReadNode
::A, ::A::B::A NODE_COLON3 ConstantPathNode
A::B, ::A::B::B NODE_COLON2 ConstantPathNode

一番左の定数は::の有無でノードが変わりますが、それ以降の定数は常にNODE_COLON2(書き換え後はConstantPathNode)になるという構造のようです。

図にすると以下のようになっており、書き換えの前後を問わず最も右の定数が頂点にきて、そのnd_head(もしくはparent)フィールドに左側のノードが設定されます。

NODE_COLON3NODE_COLON2ConstantPathNodeに統合されるので、parentフィールドの有無を考慮してマクロを書き直せばよいでしょう。

-#define NEW_COLON2(c,i,loc,d_loc,n_loc) (NODE *)rb_node_colon2_new(p,c,i,loc,d_loc,n_loc)
-#define NEW_COLON3(i,loc,d_loc,n_loc) (NODE *)rb_node_colon3_new(p,i,loc,d_loc,n_loc)
+// #define NEW_COLON2(c,i,loc,d_loc,n_loc) (NODE *)rb_node_colon2_new(p,c,i,loc,d_loc,n_loc)
+// #define NEW_COLON3(i,loc,d_loc,n_loc) (NODE *)rb_node_colon3_new(p,i,loc,d_loc,n_loc)

+#define NEW_CONSTANT_PATH(c,i,loc,d_loc,n_loc) rb_new_constant_path(p,c,i,loc,d_loc,n_loc)
+#define NEW_COLON2(c,i,loc,d_loc,n_loc) NEW_CONSTANT_PATH(c,i,loc,d_loc,n_loc)
+#define NEW_COLON3(i,loc,d_loc,n_loc) NEW_CONSTANT_PATH(0,i,loc,d_loc,n_loc)

これだけでうまく書き換えができているようです。

$ ./miniruby --parser=parse.y --dump=p -e '::A::B'
+-- @ ConstantPathNode (location: (1,0)-(1,6))*
    +-- parent:
    |   @ ConstantPathNode (location: (1,0)-(1,3))
    |   +-- parent: nil
    |   +-- name: :A
    +-- name: :B

$ ./miniruby --parser=parse.y --dump=p -e 'A::B'
+-- @ ConstantPathNode (location: (1,0)-(1,4))*
    +-- parent:
    |   @ ConstantReadNode (location: (1,0)-(1,1))
    |   +-- name: :A
    +-- name: :B

ほんと...?

定数の参照をコンパイルする

一発で通ったので気をよくしてコンパイルをしていきましょう。

まずはバイトコードの確認です。

# 0000 opt_getconstant_path                   <ic:0 A>                  (   1)[Li]
# 0002 leave
A

# 0000 opt_getconstant_path                   <ic:0 ::A>                (   1)[Li]
# 0002 leave
::A

# 0000 opt_getconstant_path                   <ic:0 A::B>               (   1)[Li]
# 0002 leave
A::B

# 0000 opt_getconstant_path                   <ic:0 ::A::B>             (   1)[Li]
# 0002 leave
::A::B

どれも最適化されている...

opt_getconstant_pathはoptimizedな命令のことでしょう。

最適化されていないものを見たいときは変数やメソッド呼び出しを挟んでみましょう。

# 0000 putself                                                          (   1)[Li]
# 0001 opt_send_without_block                 <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0003 putobject                              false
# 0005 getconstant                            :B
# 0007 opt_send_without_block                 <calldata!mid:c, argc:0, ARGS_SIMPLE>
# 0009 putobject                              false
# 0011 getconstant                            :D
# 0013 leave
a::B::c::D

よし(よしとは?)。

getconstant命令はスタックの上から2つの値と自身のオペランド1つを使って定数を取得する命令です。 a::Bの部分でいえば、aメソッドの戻り値とフラグ(false)と:Bを使ってaの戻り値から:Bを検索して返します。

一方でopt_getconstant_path命令はオペランドICを使って定数を取得する命令です。 ICは以下のように定義された構造体に対するポインタです。

const ID *segmentsとしてidの配列を持っています。 opt_getconstant_path命令を生成するときにcompile.cでsegmentsを計算します。

/* inline cache */
typedef struct iseq_inline_constant_cache *IC;

struct iseq_inline_constant_cache {
    struct iseq_inline_constant_cache_entry *entry;

    /**
     * A null-terminated list of ids, used to represent a constant's path
     * idNULL is used to represent the :: prefix, and 0 is used to donate the end
     * of the list.
     *
     * For example
     *   FOO        {rb_intern("FOO"), 0}
     *   FOO::BAR   {rb_intern("FOO"), rb_intern("BAR"), 0}
     *   ::FOO      {idNULL, rb_intern("FOO"), 0}
     *   ::FOO::BAR {idNULL, rb_intern("FOO"), rb_intern("BAR"), 0}
     */
    const ID *segments;
};

バイトコードを確認したのでcompile.cを書き換えていきましょう。 書き換える前は3つのノードがあったので、compile.cでもそれら3つのノードに対してコンパイル処理が書かれているはずです。

case NODE_CONST:{
  debugi("nd_vid", RNODE_CONST(node)->nd_vid);

  if (ISEQ_COMPILE_DATA(iseq)->option->inline_const_cache) {
      body->ic_size++;
      VALUE segments = rb_ary_new_from_args(1, ID2SYM(RNODE_CONST(node)->nd_vid));
      RB_OBJ_SET_FROZEN_SHAREABLE(segments);
      ADD_INSN1(ret, node, opt_getconstant_path, segments);
      RB_OBJ_WRITTEN(iseq, Qundef, segments);
  }
  else {
      ADD_INSN(ret, node, putnil);
      ADD_INSN1(ret, node, putobject, Qtrue);
      ADD_INSN1(ret, node, getconstant, ID2SYM(RNODE_CONST(node)->nd_vid));
  }

  if (popped) {
      ADD_INSN(ret, node, pop);
  }
  break;
}
case NODE_COLON2:
  CHECK(compile_colon2(iseq, ret, node, popped));
  break;
case NODE_COLON3:
  CHECK(compile_colon3(iseq, ret, node, popped));
  break;

NODE_COLON2NODE_COLON3についてはそれぞれ専用の関数を呼び出しているので、そちらをみます。

static int
compile_colon2(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped)
{
    if (rb_is_const_id(RNODE_COLON2(node)->nd_mid)) {
        /* constant */
        VALUE segments;
        if (ISEQ_COMPILE_DATA(iseq)->option->inline_const_cache &&
                (segments = collect_const_segments(iseq, node))) {
            ISEQ_BODY(iseq)->ic_size++;
            ADD_INSN1(ret, node, opt_getconstant_path, segments);
            RB_OBJ_WRITTEN(iseq, Qundef, segments);
        }
        else {
            /* constant */
            DECL_ANCHOR(pref);
            DECL_ANCHOR(body);

            INIT_ANCHOR(pref);
            INIT_ANCHOR(body);
            CHECK(compile_const_prefix(iseq, node, pref, body));
            if (LIST_INSN_SIZE_ZERO(pref)) {
                ADD_INSN(ret, node, putnil);
                ADD_SEQ(ret, body);
            }
            else {
                ADD_SEQ(ret, pref);
                ADD_SEQ(ret, body);
            }
        }
    }
    else {
        /* function call */
        ADD_CALL_RECEIVER(ret, node);
        CHECK(COMPILE(ret, "colon2#nd_head", RNODE_COLON2(node)->nd_head));
        ADD_CALL(ret, node, RNODE_COLON2(node)->nd_mid, INT2FIX(1));
    }
    if (popped) {
        ADD_INSN(ret, node, pop);
    }
    return COMPILE_OK;
}

static int
compile_colon3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped)
{
    debugi("colon3#nd_mid", RNODE_COLON3(node)->nd_mid);

    /* add cache insn */
    if (ISEQ_COMPILE_DATA(iseq)->option->inline_const_cache) {
        ISEQ_BODY(iseq)->ic_size++;
        VALUE segments = rb_ary_new_from_args(2, ID2SYM(idNULL), ID2SYM(RNODE_COLON3(node)->nd_mid));
        RB_OBJ_SET_FROZEN_SHAREABLE(segments);
        ADD_INSN1(ret, node, opt_getconstant_path, segments);
        RB_OBJ_WRITTEN(iseq, Qundef, segments);
    }
    else {
        ADD_INSN1(ret, node, putobject, rb_cObject);
        ADD_INSN1(ret, node, putobject, Qtrue);
        ADD_INSN1(ret, node, getconstant, ID2SYM(RNODE_COLON3(node)->nd_mid));
    }

    if (popped) {
        ADD_INSN(ret, node, pop);
    }
    return COMPILE_OK;
}

3種類のノードの全てのケースでISEQ_COMPILE_DATA(iseq)->option->inline_const_cacheをチェックして分岐しています。 inline_const_cacheが無効になっている場合にinline cacheを使うわけにもいかないので、それはそう。

NODE_CONSTNODE_COLON3ではinline cacheが有効かどうかだけをチェックするのに対して、NODE_COLON2ではcollect_const_segments関数の戻り値もチェックしています。

collect_const_segments関数はidの配列であるsegmentsを構築しますが、途中でNODE_CONST, NODE_COLON2, NODE_COLON3以外のノードを見つけた場合はfalseを返して最適化ではないほうのパスに分岐させます1

static VALUE
collect_const_segments(rb_iseq_t *iseq, const NODE *node)
{
    VALUE arr = rb_ary_new();
    for (;;) {
        switch (nd_type(node)) {
          case NODE_CONST:
            rb_ary_unshift(arr, ID2SYM(RNODE_CONST(node)->nd_vid));
            RB_OBJ_SET_SHAREABLE(arr);
            return arr;
          case NODE_COLON3:
            rb_ary_unshift(arr, ID2SYM(RNODE_COLON3(node)->nd_mid));
            rb_ary_unshift(arr, ID2SYM(idNULL));
            RB_OBJ_SET_SHAREABLE(arr);
            return arr;
          case NODE_COLON2:
            rb_ary_unshift(arr, ID2SYM(RNODE_COLON2(node)->nd_mid));
            node = RNODE_COLON2(node)->nd_head;
            break;
          default:
            return Qfalse;
        }
    }
}

NODE_CONSTNODE_COLON3の場合はそれ以上左側に要素がないことが文法上決定しているため、collect_const_segments関数で行っているようなチェックはとくに必要ありません。

余談ですが、compile_colon2関数の最初にnd_midの内容で定数かメソッド呼び出しか分岐している箇所がありますが、これってメソッド呼び出しになるケースあるんですかね。 まぁ今回は気にせず進みましょう。

基本的には構造体の変更に合わせてマクロなどを修正するいつものやつをやっていきます。 NODE_COLON2NODE_COLON3は1つのノードに統合されましたが、parentフィールドの有無で判別できるので既存の処理の流れなどには手をつけないようにparentの有無で分岐するようにしました。

+      case RB_CONSTANT_PATH_NODE: {
+        if (RB_NODE_CONSTANT_PATH(node)->parent) {
+            CHECK(compile_colon2(iseq, ret, node, popped));
+        }
+        else {
+            CHECK(compile_colon3(iseq, ret, node, popped));
+        }
+        break;
+      }
+

いい感じにコンパイルできていそうです。

$ ./miniruby --parser=parse.y --dump=i -e 'A; B::C; ::D; ::D::E; f::G'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,26)>
0000 opt_getconstant_path                   <ic:0 A>                  (   1)[Li]
0002 pop
0003 opt_getconstant_path                   <ic:1 B::C>
0005 pop
0006 opt_getconstant_path                   <ic:2 ::D>
0008 pop
0009 opt_getconstant_path                   <ic:3 ::D::E>
0011 pop
0012 putself
0013 opt_send_without_block                 <calldata!mid:f, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0015 putobject                              false
0017 getconstant                            :G
0019 leave

定数の代入を書き換える

順調なので定数に対する代入もやってしまいましょう。

書き換え前のノードは以下のようになっており、定数の代入にはNODE_CDECLというノードを使っています。 A = 0のように左辺の定数がネストしていないもので、かつ::が使われていないときはnd_vidフィールドに、それ以外のケースではnd_elseというフィールドに左辺のノード(もしくは定数を示すid)が入っています。

A = 0
#     |   @ NODE_CDECL (id: 0, line: 1, location: (1,0)-(1,5))*
#     |   +- nd_vid: :A
#     |   +- nd_else: not used
#     |   +- shareability: none
#     |   +- nd_value:
#     |       @ NODE_INTEGER (id: 1, line: 1, location: (1,4)-(1,5))
#     |       +- val: 0


B::C = 1
#     |   @ NODE_CDECL (id: 4, line: 1, location: (1,7)-(1,15))*
#     |   +- nd_vid: 0 (see extension field)
#     |   +- nd_else:
#     |   |   @ NODE_COLON2 (id: 3, line: 1, location: (1,7)-(1,11))
#     |   |   +- nd_mid: :C
#     |   |   +- nd_head:
#     |   |   |   @ NODE_CONST (id: 2, line: 1, location: (1,7)-(1,8))
#     |   |   |   +- nd_vid: :B
#     |   +- nd_value:
#     |       @ NODE_INTEGER (id: 5, line: 1, location: (1,14)-(1,15))
#     |       +- val: 1


::D = 2
#     |   @ NODE_CDECL (id: 9, line: 1, location: (1,17)-(1,24))*
#     |   +- nd_vid: 0 (see extension field)
#     |   +- nd_else:
#     |   |   @ NODE_COLON3 (id: 8, line: 1, location: (1,17)-(1,20))
#     |   |   +- nd_mid: :D
#     |   +- nd_value:
#     |       @ NODE_INTEGER (id: 10, line: 1, location: (1,23)-(1,24))
#     |       +- val: 2


E::F = 3
#     |   @ NODE_CDECL (id: 14, line: 1, location: (1,26)-(1,34))*
#     |   +- nd_vid: 0 (see extension field)
#     |   +- nd_else:
#     |   |   @ NODE_COLON2 (id: 13, line: 1, location: (1,26)-(1,30))
#     |   |   +- nd_mid: :F
#     |   |   +- nd_head:
#     |   |   |   @ NODE_CONST (id: 12, line: 1, location: (1,26)-(1,27))
#     |   |   |   +- nd_vid: :E
#     |   +- nd_value:
#     |       @ NODE_INTEGER (id: 15, line: 1, location: (1,33)-(1,34))
#     |       +- val: 3


g::H = 4
#         @ NODE_CDECL (id: 19, line: 1, location: (1,36)-(1,44))*
#         +- nd_vid: 0 (see extension field)
#         +- nd_else:
#         |   @ NODE_COLON2 (id: 18, line: 1, location: (1,36)-(1,40))
#         |   +- nd_mid: :H
#         |   +- nd_head:
#         |   |   @ NODE_VCALL (id: 17, line: 1, location: (1,36)-(1,37))
#         |   |   +- nd_mid: :g
#         +- nd_value:
#             @ NODE_INTEGER (id: 20, line: 1, location: (1,43)-(1,44))
#             +- val: 4

書き換え後はA = 0のケースはConstantWriteNodeで、それ以外はConstantPathWriteNodeで表現します。 NODE_CDECLnd_vidフィールドとnd_elseフィールドを使い分けて表現していたものが、それぞれ異なるノードで表現するようになるということです。

A = 0
# +-- @ ConstantWriteNode (location: (1,0)-(1,5))
# |   +-- name: :A
# |   +-- name_loc: (1,0)-(1,1) = "A"
# |   +-- value:
# |   |   @ IntegerNode (location: (1,4)-(1,5))
# |   |   +-- IntegerBaseFlags: decimal
# |   |   +-- value: 0


B::C = 1
# +-- @ ConstantPathWriteNode (location: (1,7)-(1,15))
# |   +-- target:
# |   |   @ ConstantPathNode (location: (1,7)-(1,11))
# |   |   +-- parent:
# |   |   |   @ ConstantReadNode (location: (1,7)-(1,8))
# |   |   |   +-- name: :B
# |   |   +-- name: :C
# |   +-- value:
# |       @ IntegerNode (location: (1,14)-(1,15))
# |       +-- IntegerBaseFlags: decimal
# |       +-- value: 1


::D = 2
# +-- @ ConstantPathWriteNode (location: (1,17)-(1,24))
# |   +-- target:
# |   |   @ ConstantPathNode (location: (1,17)-(1,20))
# |   |   +-- parent: nil
# |   |   +-- name: :D
# |   +-- value:
# |       @ IntegerNode (location: (1,23)-(1,24))
# |       +-- IntegerBaseFlags: decimal
# |       +-- value: 2


E::F = 3
# +-- @ ConstantPathWriteNode (location: (1,26)-(1,34))
# |   +-- target:
# |   |   @ ConstantPathNode (location: (1,26)-(1,30))
# |   |   +-- parent:
# |   |   |   @ ConstantReadNode (location: (1,26)-(1,27))
# |   |   |   +-- name: :E
# |   |   +-- name: :F
# |   +-- value:
# |       @ IntegerNode (location: (1,33)-(1,34))
# |       +-- IntegerBaseFlags: decimal
# |       +-- value: 3


g::H = 4
# +-- @ ConstantPathWriteNode (location: (1,36)-(1,44))
#     +-- target:
#     |   @ ConstantPathNode (location: (1,36)-(1,40))
#     |   +-- parent:
#     |   |   @ CallNode (location: (1,36)-(1,37))
#     |   |   +-- CallNodeFlags: variable_call, ignore_visibility
#     |   |   +-- receiver: nil
#     |   |   +-- call_operator_loc: nil
#     |   |   +-- name: :g
#     |   |   +-- arguments: nil
#     |   |   +-- block: nil
#     |   +-- name: :H
#     +-- value:
#         @ IntegerNode (location: (1,43)-(1,44))
#         +-- IntegerBaseFlags: decimal
#         +-- value: 4

parserのほうはとくに目新しいこともないのでサクッと実装して、コンパイラをみていきます。 参照のときはいままで2つのノードで表現したていたものを1つのノードで表現するようになったのに対して、代入はいままで1つのノードで表現していたものが文法に応じて2つの異なるノードで表現されるようになります。

ということでノードのフィールドの値による分岐から、ノードの種類による分岐に書き換えればいいでしょう。

// Before
case NODE_CDECL:{
  // nd_vidの有無で大きく分岐していた
  if (RNODE_CDECL(node)->nd_vid) {
      CHECK(compile_shareable_constant_value(iseq, ret, RNODE_CDECL(node)->shareability, node, RNODE_CDECL(node)->nd_value));

      if (!popped) {
          ADD_INSN(ret, node, dup);
      }

      ADD_INSN1(ret, node, putspecialobject,
                INT2FIX(VM_SPECIAL_OBJECT_CONST_BASE));
      ADD_INSN1(ret, node, setconstant, ID2SYM(RNODE_CDECL(node)->nd_vid));
  }
  else {
      compile_cpath(ret, iseq, RNODE_CDECL(node)->nd_else);
      CHECK(compile_shareable_constant_value(iseq, ret, RNODE_CDECL(node)->shareability, node, RNODE_CDECL(node)->nd_value));
      ADD_INSN(ret, node, swap);

      if (!popped) {
          ADD_INSN1(ret, node, topn, INT2FIX(1));
          ADD_INSN(ret, node, swap);
      }

      ADD_INSN1(ret, node, setconstant, ID2SYM(get_node_colon_nd_mid(RNODE_CDECL(node)->nd_else)));
  }
  break;
}

// After
// ノードのタイプで分岐するようになる
case RB_CONSTANT_WRITE_NODE: {
  CHECK(compile_shareable_constant_value(iseq, ret, rb_parser_shareable_none, node, RB_NODE_CONSTANT_WRITE(node)->value));
  if (!popped) {
      ADD_INSN(ret, node, dup);
  }

  ADD_INSN1(ret, node, putspecialobject,
            INT2FIX(VM_SPECIAL_OBJECT_CONST_BASE));
  ADD_INSN1(ret, node, setconstant, ID2SYM(RB_NODE_CONSTANT_WRITE(node)->name));
  break;
}
case RB_CONSTANT_PATH_WRITE_NODE: {
  compile_cpath(ret, iseq, RB_NODE_CONSTANT_PATH_WRITE(node)->target);
  CHECK(compile_shareable_constant_value(iseq, ret, rb_parser_shareable_none, node, RB_NODE_CONSTANT_PATH_WRITE(node)->value));
  ADD_INSN(ret, node, swap);

  if (!popped) {
      ADD_INSN1(ret, node, topn, INT2FIX(1));
      ADD_INSN(ret, node, swap);
  }

  ADD_INSN1(ret, node, setconstant, ID2SYM(RB_NODE_CONSTANT_PATH_WRITE(node)->target->name));
  break;
}

いろいろなパターンの左辺をコンパイルしてみましょう。 よさそうですね。

$ ./miniruby --parser=parse.y --dump=i -e 'A = :a; B::C = :b; ::D = :c; E::F = :d; g::H = :e'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,49)>
0000 putobject                              :a                        (   1)[Li]
0002 putspecialobject                       3
0004 setconstant                            :A
0006 opt_getconstant_path                   <ic:0 B>
0008 putobject                              :b
0010 swap
0011 setconstant                            :C
0013 putobject                              Object
0015 putobject                              :c
0017 swap
0018 setconstant                            :D
0020 opt_getconstant_path                   <ic:1 E>
0022 putobject                              :d
0024 swap
0025 setconstant                            :F
0027 putself
0028 opt_send_without_block                 <calldata!mid:g, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0030 putobject                              :e
0032 swap
0033 topn                                   1
0035 swap
0036 setconstant                            :H
0038 leave

実は1つ問題がありまして...

さてここで一つ問題が発生しました。 compile_shareable_constant_value関数の第三引数に注目してください。

// Before
CHECK(compile_shareable_constant_value(iseq, ret, RNODE_CDECL(node)->shareability, node, RNODE_CDECL(node)->nd_value));

// After
CHECK(compile_shareable_constant_value(iseq, ret, rb_parser_shareable_none, node, RB_NODE_CONSTANT_WRITE(node)->value));
CHECK(compile_shareable_constant_value(iseq, ret, rb_parser_shareable_none, node, RB_NODE_CONSTANT_PATH_WRITE(node)->value));

おわかりいただけたでしょうか。 ConstantWriteNodeConstantPathWriteNodeshareable_constant_valueマジックコメントに関する情報を持っていないのです。

ではどうしているかというとConstantWriteNodeをラップするShareableConstantNodeというのがマジックコメントの値によっては出現します。

# shareable_constant_value: experimental_everything
C1 = r1

+-- @ ShareableConstantNode (location: (2,0)-(2,7))
#     +-- ShareableConstantNodeFlags: experimental_everything
#     +-- write:
#         @ ConstantWriteNode (location: (2,0)-(2,7))
#         +-- name: :C1
#         +-- value:
#         |   @ CallNode (location: (2,5)-(2,7))
#         |   +-- CallNodeFlags: variable_call, ignore_visibility
#         |   +-- receiver: nil
#         |   +-- call_operator_loc: nil
#         |   +-- name: :r1
#         |   +-- arguments: nil
#         |   +-- block: nil

どうしてこうなった...

この辺の対応は次回にしましょう。 ということで今日はここまで。

まとめ

今日の成果です。

  • 定数の参照と代入に対応した

次回はShareableConstantNodeの対応と、可能ならmultiple assignmentの左辺に定数があるケースの対応をやっていきます。


  1. QfalseRubyfalseを表しますが、その値は0なのでC言語の偽としても振る舞います。



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

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