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


Ruby Parser開発日誌 (24-17) - parse.yが生成するノードを変える ー 定数 (shareable constantとmultiple assignment)

17日目: shareable constantと定数へのmultiple assignment

前回は定数の参照と代入に関するノードを書き換えました。 その途中でshareable_constant_valueマジックコメントの有無によって構文木の構造が変わることに気がついたのでした。

今回はshareable_constant_valueマジックコメントの対応と、multiple assignmentの左辺に定数がある場合の対応をしていきます。 これが終われば定数に関係する部分は完了するはずです。

shareable_constant_valueマジックコメント

shareable_constant_valueマジックコメントはRactorに関係するマジックコメントで、定数に代入されるオブジェクトをfreezeしたりdeep coypしたりします。

# shareable_constant_value: none
C1 = [1, 2]
p C1.frozen?
# => false

# shareable_constant_value: literal
C2 = [1, 2]
p C2.frozen?
# => true

その時点で有効なshareable_constant_valueの値によって生成されるバイトコードが変化するため、コンパイラshareable_constant_valueの値を知っている必要があります。

ノードの書き換え前はNODE_CDECLshareabilityというフィールドがあり、そこにshareable_constant_valueの値を持たせていました。

#     +- nd_head (1):
#     |   @ NODE_CDECL (id: 0, line: 2, location: (2,0)-(2,11))*
#     |   +- nd_vid: :C1
#     |   +- nd_else: not used
#     |   +- shareability: none
#     |   +- nd_value:
#     |       @ NODE_LIST (id: 2, line: 2, location: (2,5)-(2,11))
#     |       +- as.nd_alen: 2
#     |       +- nd_head:
#     |       |   @ NODE_INTEGER (id: 1, line: 2, location: (2,6)-(2,7))
#     |       |   +- val: 1
#     |       +- nd_head:
#     |       |   @ NODE_INTEGER (id: 3, line: 2, location: (2,9)-(2,10))
#     |       |   +- val: 2
#     |       +- nd_next:
#     |           (null node)

#     +- nd_head (3):
#     |   @ NODE_CDECL (id: 11, line: 7, location: (7,0)-(7,11))*
#     |   +- nd_vid: :C2
#     |   +- nd_else: not used
#     |   +- shareability: literal
#     |   +- nd_value:
#     |       @ NODE_LIST (id: 13, line: 7, location: (7,5)-(7,11))
#     |       +- as.nd_alen: 2
#     |       +- nd_head:
#     |       |   @ NODE_INTEGER (id: 12, line: 7, location: (7,6)-(7,7))
#     |       |   +- val: 1
#     |       +- nd_head:
#     |       |   @ NODE_INTEGER (id: 14, line: 7, location: (7,9)-(7,10))
#     |       |   +- val: 2
#     |       +- nd_next:
#     |           (null node)

書き換え後は# shareable_constant_value: none以外のときに、ShareableConstantNodeというノードがConstantWriteNodeなどをもつという構造になります。

@ StatementsNode (location: (2,0)-(8,12))
+-- body: (length: 4)
    +-- @ ConstantWriteNode (location: (2,0)-(2,11))
    |   +-- name: :C1
    |   +-- name_loc: (2,0)-(2,2) = "C1"
    |   +-- value:
    |   |   @ ArrayNode (location: (2,5)-(2,11))
    |   |   +-- ArrayNodeFlags: nil
    |   |   +-- elements: (length: 2)
    |   |   |   +-- @ IntegerNode (location: (2,6)-(2,7))
    |   |   |   |   +-- IntegerBaseFlags: decimal
    |   |   |   |   +-- value: 1
    |   |   |   +-- @ IntegerNode (location: (2,9)-(2,10))
    |   |   |       +-- IntegerBaseFlags: decimal
    |   |   |       +-- value: 2

    +-- @ ShareableConstantNode (location: (7,0)-(7,11))
    |   +-- ShareableConstantNodeFlags: literal
    |   +-- write:
    |       @ ConstantWriteNode (location: (7,0)-(7,11))
    |       +-- name: :C2
    |       +-- name_loc: (7,0)-(7,2) = "C2"
    |       +-- value:
    |       |   @ ArrayNode (location: (7,5)-(7,11))
    |       |   +-- ArrayNodeFlags: nil
    |       |   +-- elements: (length: 2)
    |       |   |   +-- @ IntegerNode (location: (7,6)-(7,7))
    |       |   |   |   +-- IntegerBaseFlags: decimal
    |       |   |   |   +-- value: 1
    |       |   |   +-- @ IntegerNode (location: (7,9)-(7,10))
    |       |   |       +-- IntegerBaseFlags: decimal
    |       |   |       +-- value: 2

# shareable_constant_value: noneのときもShareableConstantNodeでラップするか、ConstantWriteNodeにフラグを持たせるかしたほうが構造が共通化できて見通しが良い気がするんですが、こういう設計のほうがツールなどを作るときに扱いやすいのでしょうか。

まぁ、気にしても仕方ないので進みましょう。

parse.yを変更する

parse.yではnew_shareable_constantという補助的な関数を用意して、ConstantWriteNodeなどを生成したあとで必要に応じてShareableConstantNodeでラップするようにします。

+static rb_node_t *
+new_shareable_constant(struct parser_params *p, rb_node_t *node)
+{
+    if (p->ctxt.shareable_constant_value == rb_parser_shareable_none) return node;
+    return NEW_RB_SHAREABLE_CONSTANT(node, p->ctxt.shareable_constant_value, rb_nd_code_loc(node));
+}
+

 static int
 assignable0(struct parser_params *p, ID id, const char **err)
 {
@@ -14742,7 +14776,7 @@ assignable(struct parser_params *p, ID id, rb_node_t *val, const YYLTYPE *loc)
       case NODE_LASGN: return NEW_LASGN(id, val, loc); // case NODE_DASGN:
       case RB_GLOBAL_VARIABLE_WRITE_NODE: return NEW_RB_GLOBAL_VARIABLE_WRITE(id, val, loc);
       case RB_INSTANCE_VARIABLE_WRITE_NODE: return NEW_RB_INSTANCE_VARIABLE_WRITE(id, val, loc);
-      case RB_CONSTANT_WRITE_NODE: return NEW_RB_CONSTANT_WRITE(id, val, p->ctxt.shareable_constant_value, loc);
+      case RB_CONSTANT_WRITE_NODE: return new_shareable_constant(p, NEW_RB_CONSTANT_WRITE(id, val, p->ctxt.shareable_constant_value, loc));
       case RB_CLASS_VARIABLE_WRITE_NODE: return NEW_RB_CLASS_VARIABLE_WRITE(id, val, loc);
     }

 static rb_node_t *
 const_decl(struct parser_params *p, rb_node_t *path, const YYLTYPE *loc)
 {
     if (p->ctxt.in_def) {
@@ -16244,7 +16279,7 @@ const_decl(struct parser_params *p, rb_node_t *path, const YYLTYPE *loc)
         set_value(assign_error(p, "dynamic constant assignment", p->s_lvalue));
 #endif
     }
-    return NEW_RB_CONSTANT_PATH_WRITE((path), 0, p->ctxt.shareable_constant_value, loc);
+    return new_shareable_constant(p, NEW_RB_CONSTANT_PATH_WRITE((path), 0, p->ctxt.shareable_constant_value, loc));
 }

compile.cを変更する

compile.cではConstantWriteNodeなどの処理を関数に切り出して、ShareableConstantNodeでも使いまわせるようにします。 ShareableConstantNodeがないConstantWriteNodeというのは、enum rb_parser_shareability shareableを常にrb_parser_shareable_noneとしてcompile_shareable_constant_value関数を呼び出すことと同じです。

@@ -11533,27 +11588,27 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no
         break;
       }
       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));
+        CHECK(compile_constant_write(iseq, ret, rb_parser_shareable_none, RB_NODE_CONSTANT_WRITE(node), popped));
         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);
+        CHECK(compile_constant_path_write(iseq, ret, rb_parser_shareable_none, RB_NODE_CONSTANT_PATH_WRITE(node), popped));
+        break;
+      }
+      case RB_SHAREABLE_CONSTANT_NODE: {
+        enum rb_parser_shareability shareable = const_node_shareability(RB_NODE_SHAREABLE_CONSTANT(node));
+        const NODE *n = RB_NODE_SHAREABLE_CONSTANT(node)->write;

-        if (!popped) {
-            ADD_INSN1(ret, node, topn, INT2FIX(1));
-            ADD_INSN(ret, node, swap);
+        switch (nd_type(n)) {
+          case RB_CONSTANT_WRITE_NODE:
+            CHECK(compile_constant_write(iseq, ret, shareable, RB_NODE_CONSTANT_WRITE(n), popped));
+            break;
+          case RB_CONSTANT_PATH_WRITE_NODE:
+            CHECK(compile_constant_path_write(iseq, ret, shareable, RB_NODE_CONSTANT_PATH_WRITE(n), popped));
+            break;
+          default:
+            rb_bug("unexpected node: %s", ruby_node_name(nd_type(n)));
         }
-
-        ADD_INSN1(ret, node, setconstant, ID2SYM(RB_NODE_CONSTANT_PATH_WRITE(node)->target->name));
         break;
       }

ビルドして# shareable_constant_value: literalで試してみます。 よさそうですね。

# shareable_constant_value: literal
C1 = [false, :s.to_s]
# 0000 putspecialobject                       1                         (   2)[Li]
# 0002 putobject                              false
# 0004 putspecialobject                       1
# 0006 putobject                              :s
# 0008 opt_send_without_block                 <calldata!mid:to_s, argc:0, ARGS_SIMPLE>
# 0010 putobject                              "C1"
# 0012 opt_send_without_block                 <calldata!mid:ensure_shareable, argc:2, ARGS_SIMPLE>
# 0014 newarray                               2
# 0016 opt_send_without_block                 <calldata!mid:make_shareable, argc:1, ARGS_SIMPLE>
# 0018 putspecialobject                       3
# 0020 setconstant                            :C1

C1::C2 = [false, :s.to_s]
# 0022 opt_getconstant_path                   <ic:0 C1>                 (   3)[Li]
# 0024 putspecialobject                       1
# 0026 putobject                              false
# 0028 putspecialobject                       1
# 0030 putobject                              :s
# 0032 opt_send_without_block                 <calldata!mid:to_s, argc:0, ARGS_SIMPLE>
# 0034 putobject                              "C1::C2"
# 0036 opt_send_without_block                 <calldata!mid:ensure_shareable, argc:2, ARGS_SIMPLE>
# 0038 newarray                               2
# 0040 opt_send_without_block                 <calldata!mid:make_shareable, argc:1, ARGS_SIMPLE>
# 0042 swap
# 0043 setconstant                            :C2

::C1::C3 = [false, :s.to_s]
# 0045 opt_getconstant_path                   <ic:1 ::C1>               (   4)[Li]
# 0047 putspecialobject                       1
# 0049 putobject                              false
# 0051 putspecialobject                       1
# 0053 putobject                              :s
# 0055 opt_send_without_block                 <calldata!mid:to_s, argc:0, ARGS_SIMPLE>
# 0057 putobject                              "::C1::C3"
# 0059 opt_send_without_block                 <calldata!mid:ensure_shareable, argc:2, ARGS_SIMPLE>
# 0061 newarray                               2
# 0063 opt_send_without_block                 <calldata!mid:make_shareable, argc:1, ARGS_SIMPLE>
# 0065 swap
# 0066 setconstant                            :C3

c::C2::C3 = [false, :s.to_s]
# 0068 putself                                                          (   5)[Li]
# 0069 opt_send_without_block                 <calldata!mid:c, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0071 putobject                              false
# 0073 getconstant                            :C2
# 0075 putspecialobject                       1
# 0077 putobject                              false
# 0079 putspecialobject                       1
# 0081 putobject                              :s
# 0083 opt_send_without_block                 <calldata!mid:to_s, argc:0, ARGS_SIMPLE>
# 0085 putobject                              "...::C2::C3"
# 0087 opt_send_without_block                 <calldata!mid:ensure_shareable, argc:2, ARGS_SIMPLE>
# 0089 newarray                               2
# 0091 opt_send_without_block                 <calldata!mid:make_shareable, argc:1, ARGS_SIMPLE>
# 0093 swap
# 0094 setconstant                            :C3

C1::c::C2::C3 = [false, :s.to_s]
# 0096 opt_getconstant_path                   <ic:2 C1>                 (   6)[Li]
# 0098 opt_send_without_block                 <calldata!mid:c, argc:0, ARGS_SIMPLE>
# 0100 putobject                              false
# 0102 getconstant                            :C2
# 0104 putspecialobject                       1
# 0106 putobject                              false
# 0108 putspecialobject                       1
# 0110 putobject                              :s
# 0112 opt_send_without_block                 <calldata!mid:to_s, argc:0, ARGS_SIMPLE>
# 0114 putobject                              "...::C2::C3"
# 0116 opt_send_without_block                 <calldata!mid:ensure_shareable, argc:2, ARGS_SIMPLE>
# 0118 newarray                               2
# 0120 opt_send_without_block                 <calldata!mid:make_shareable, argc:1, ARGS_SIMPLE>
# 0122 swap
# 0123 topn                                   1
# 0125 swap
# 0126 setconstant                            :C3
# 0128 leave

定数へのmultiple assignment

A, B = fooのようにmultiple assignmentのうち左辺が定数のケースを対応していなかったのでやっていきます。

書き換え前後のノードは以下のとおりです。

書き換え前 書き換え後
::A CDECL(COLON3) ConstantPathTarget
B CDECL ConstantTarget
::C::D CDECL(COLON2(COLON3)) ConstantPathTarget(ConstantPath)
E::F CDECL(COLON2(CONST)) ConstantPathTarget(ConstantRead)
g::H CDECL(COLON2(VCALL)) ConstantPathTarget(Call)

Bのケースはassignable_target関数で対応します。

@@ -14797,7 +14823,7 @@ assignable_target(struct parser_params *p, ID id, const YYLTYPE *loc)
       case NODE_LASGN: return NEW_LTARGET(id, loc); // case NODE_DASGN:
       case RB_GLOBAL_VARIABLE_WRITE_NODE: return NEW_RB_GLOBAL_VARIABLE_TARGET(id, loc);
       case RB_INSTANCE_VARIABLE_WRITE_NODE: return NEW_RB_INSTANCE_VARIABLE_TARGET(id, loc);
-      // case NODE_CDECL: return NEW_CDECL(id, 0, p->ctxt.shareable_constant_value, loc);
+      case RB_CONSTANT_WRITE_NODE: return NEW_RB_CONSTANT_TARGET(id, loc);
       case RB_CLASS_VARIABLE_WRITE_NODE: return NEW_RB_CLASS_VARIABLE_TARGET(id, loc);
     }

その他のケースはTargetノードを生成するconst_decl_targetという関数を用意します。 これまではmultiple assignmentのときも通常の代入同様const_decl関数でノードを作成していました。 それらの共通部分はcheck_dynamic_const_decl関数として切り出します。

-static rb_node_t *
-const_decl(struct parser_params *p, rb_node_t *path, const YYLTYPE *loc)
+static void
+check_dynamic_const_decl(struct parser_params *p, const YYLTYPE *loc)
 {
     if (p->ctxt.in_def) {
 #ifndef RIPPER
@@ -16279,7 +16303,20 @@ const_decl(struct parser_params *p, rb_node_t *path, const YYLTYPE *loc)
         set_value(assign_error(p, "dynamic constant assignment", p->s_lvalue));
 #endif
     }
-    return new_shareable_constant(p, NEW_RB_CONSTANT_PATH_WRITE((path), 0, p->ctxt.shareable_constant_value, loc));
+}
+
+static rb_node_t *
+const_decl(struct parser_params *p, rb_node_t *path, const YYLTYPE *loc)
+{
+    check_dynamic_const_decl(p, loc);
+    return new_shareable_constant(p, NEW_RB_CONSTANT_PATH_WRITE(path, 0, loc));
+}
+
+static rb_node_t *
+const_decl_target(struct parser_params *p, rb_node_t *path, ID id, const YYLTYPE *loc)
+{
+    check_dynamic_const_decl(p, loc);
+    return NEW_RB_CONSTANT_PATH_TARGET(path, id, loc);
 }

@@ -4087,12 +4092,12 @@ mlhs_node       : user_or_keyword_variable
                 | primary_value tCOLON2 tCONSTANT
                     {
                     /*% ripper: const_path_field!($:1, $:3) %*/
-                        $$ = const_decl(p, NEW_COLON2($1, $3, &@$, &@2, &@3), &@$);
+                        $$ = const_decl_target(p, $1, $3, &@$);
                     }
                 | tCOLON3 tCONSTANT
                     {
                     /*% ripper: top_const_field!($:2) %*/
-                        $$ = const_decl(p, NEW_COLON3($2, &@$, &@1, &@2), &@$);
+                        $$ = const_decl_target(p, 0, $2, &@$);
                     }

生成されるノードを確認します。 よさそうですね。

$ ./miniruby --parser=parse.y --dump=p -e  '::A, B, ::C::D, E::F, g::H  = foo'
  +-- @ MultiWriteNode (location: (1,0)-(1,33))*
      +-- lefts: (length: 5)
      |   +-- @ ConstantPathTargetNode (location: (1,0)-(1,3))
      |   |   +-- parent: nil
      |   |   +-- name: :A
      |   +-- @ ConstantTargetNode (location: (1,5)-(1,6))
      |   |   +-- name: :B
      |   +-- @ ConstantPathTargetNode (location: (1,8)-(1,14))
      |   |   +-- parent:
      |   |   |   @ ConstantPathNode (location: (1,8)-(1,11))
      |   |   |   +-- parent: nil
      |   |   |   +-- name: :C
      |   |   +-- name: :D
      |   +-- @ ConstantPathTargetNode (location: (1,16)-(1,20))
      |   |   +-- parent:
      |   |   |   @ ConstantReadNode (location: (1,16)-(1,17))
      |   |   |   +-- name: :E
      |   |   +-- name: :F
      |   +-- @ ConstantPathTargetNode (location: (1,22)-(1,26))
      |       +-- parent:
      |       |   @ CallNode (location: (1,22)-(1,23))
      |       |   +-- CallNodeFlags: variable_call
      |       |   +-- receiver: nil
      |       |   +-- name: :g
      |       |   +-- arguments: nil
      |       |   +-- block: nil
      |       +-- name: :H
      +-- rest: nil
      +-- rights: (length: 0)
      +-- value:
          @ CallNode (location: (1,30)-(1,33))
          +-- CallNodeFlags: variable_call
          +-- receiver: nil
          +-- call_operator_loc: nil
          +-- name: :foo
          +-- arguments: nil
          +-- block: nil

compile.cを変更する

さて久しぶりのmultiple assignmentですが、その左辺の要素はcompile_massign_lhs関数でコンパイルされるのでした。

定数のコンパイルに該当する部分をみてみると、!RNODE_CDECL(node)->nd_vidつまりB以外のケースでは特別な対応をし、Bのケースはiseq_compile_each0関数に任せていることがわかります。

      case NODE_CDECL:
        if (!RNODE_CDECL(node)->nd_vid) {
            /* Special handling only needed for expr::C, not for C */
            INSN *iobj;

            CHECK(COMPILE_POPPED(pre, "masgn lhs (NODE_CDECL)", node));

            LINK_ELEMENT *insn_element = LAST_ELEMENT(pre);
            iobj = (INSN *)insn_element; /* setconstant insn */
            ELEM_REMOVE((LINK_ELEMENT *)get_prev_insn((INSN *)get_prev_insn(iobj)));
            ELEM_REMOVE((LINK_ELEMENT *)get_prev_insn(iobj));
            ELEM_REMOVE(insn_element);
            pre->last = iobj->link.prev;
            ADD_ELEM(lhs, (LINK_ELEMENT *)iobj);

            if (!add_masgn_lhs_node(state, lhs_pos, node, 1, (INSN *)LAST_ELEMENT(lhs))) {
                return COMPILE_NG;
            }

            ADD_INSN(post, node, pop);
            break;
        }
        /* Fallthrough */
      default: {
        DECL_ANCHOR(anchor);
        INIT_ANCHOR(anchor);
        CHECK(COMPILE_POPPED(anchor, "masgn lhs", node));
        ELEM_REMOVE(FIRST_ELEMENT(anchor));
        ADD_SEQ(lhs, anchor);
      }

それぞれ順番にみていきましょう。

B以外のケース

まずはifの分岐のなかに入るケースです。 ::A, ::C::D, E::F, g::Hなどが該当します。

このときcompile_massign_lhs関数ではノードをコンパイルして取得したバイトコードprelhsの2つのanchor用に分割します。 ::A, _ = barを例に確認してみましょう。

multiple assignmentではNODE_CDECLnd_valueがnullになるので、右辺をコンパイルするとputnilが生成されます。 そこで擬似的にA = nilバイトコードも掲載しておきます。

LAST_ELEMENTELEM_REMOVEを利用してputobject Objectsetconstant :Aに分割し、それぞれをpre anchorとlhs anchorに結合するとちょうどmultiple assignmentに必要なバイトコードになります。 不要なputnilswapの部分は必ずこの2つの命令になるのでsetconstant :Aからみて1つ前の命令と2つ前の命令を削除すれば帳尻があいます。

::A = nil
# 0000 putobject                              Object                    (   1)[Li]
# 0002 putnil
# 0003 swap
# 0004 setconstant                            :A

::A, _ = bar
# pre anchor
# 0006 putobject                              Object

# rhs anchor
# 0008 putself
# 0009 opt_send_without_block                 <calldata!mid:bar, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0011 dup
# 0012 expandarray                            2, 0
# 0015 topn                                   3

# lhs anchor
# 0017 setconstant                            :A
# 0019 setlocal_WC_0                          _@0

# 0021 setn                                   1

# post anchor
# 0023 pop
# 0024 leave

この方法は定数のパスが長くなっても有効です。 ::A::B::c::D, _ = barを例に確認してみましょう。

たしかに末尾から順番にsetconstant :D, swap, putnilが並んでいることがわかります。

::A::B::c::D = nil
# 0000 opt_getconstant_path                   <ic:0 ::A::B>             (   1)[Li]
# 0002 opt_send_without_block                 <calldata!mid:c, argc:0, ARGS_SIMPLE>
# 0004 putnil
# 0005 swap
# 0006 setconstant                            :D

::A::B::c::D, _ = bar
# pre anchor
# 0008 opt_getconstant_path                   <ic:1 ::A::B>
# 0010 opt_send_without_block                 <calldata!mid:c, argc:0, ARGS_SIMPLE>

# rhs anchor
# 0012 putself
# 0013 opt_send_without_block                 <calldata!mid:bar, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0015 dup
# 0016 expandarray                            2, 0
# 0019 topn                                   3

# lhs anchor
# 0021 setconstant                            :D
# 0023 setlocal_WC_0                          _@0

# 0025 setn                                   1

# post anchor
# 0027 pop
# 0028 leave

さて元のコードでは!RNODE_CDECL(node)->nd_vidで定数のパスの構造を判断していました。 書き換え後はノードの種別(ConstantTargetNode(B)かConstantPathTargetNode(B以外)か)で判別可能です。

@@ -6196,29 +6196,28 @@ compile_massign_lhs(rb_iseq_t *iseq, LINK_ANCHOR *const pre, LINK_ANCHOR *const
         ADD_SEQ(lhs, nest_lhs);
         break;
       }
-      case NODE_CDECL:
-        if (!RNODE_CDECL(node)->nd_vid) {
-            /* Special handling only needed for expr::C, not for C */
-            INSN *iobj;
-
-            CHECK(COMPILE_POPPED(pre, "masgn lhs (NODE_CDECL)", node));
+      case RB_CONSTANT_PATH_TARGET_NODE: {
+        // if (!RNODE_CDECL(node)->nd_vid)
+        /* Special handling only needed for expr::C, not for C */
+        INSN *iobj;

-            LINK_ELEMENT *insn_element = LAST_ELEMENT(pre);
-            iobj = (INSN *)insn_element; /* setconstant insn */
-            ELEM_REMOVE((LINK_ELEMENT *)get_prev_insn((INSN *)get_prev_insn(iobj)));
-            ELEM_REMOVE((LINK_ELEMENT *)get_prev_insn(iobj));
-            ELEM_REMOVE(insn_element);
-            pre->last = iobj->link.prev;
-            ADD_ELEM(lhs, (LINK_ELEMENT *)iobj);
+        CHECK(COMPILE_POPPED(pre, "masgn lhs (RB_CONSTANT_PATH_TARGET_NODE)", node));

-            if (!add_masgn_lhs_node(state, lhs_pos, node, 1, (INSN *)LAST_ELEMENT(lhs))) {
-                return COMPILE_NG;
-            }
+        LINK_ELEMENT *insn_element = LAST_ELEMENT(pre);
+        iobj = (INSN *)insn_element; /* setconstant insn */
+        ELEM_REMOVE((LINK_ELEMENT *)get_prev_insn((INSN *)get_prev_insn(iobj)));
+        ELEM_REMOVE((LINK_ELEMENT *)get_prev_insn(iobj));
+        ELEM_REMOVE(insn_element);
+        pre->last = iobj->link.prev;
+        ADD_ELEM(lhs, (LINK_ELEMENT *)iobj);

-            ADD_INSN(post, node, pop);
-            break;
+        if (!add_masgn_lhs_node(state, lhs_pos, node, 1, (INSN *)LAST_ELEMENT(lhs))) {
+            return COMPILE_NG;
         }
-        /* Fallthrough */
+
+        ADD_INSN(post, node, pop);
+        break;
+      }
       default: {
         DECL_ANCHOR(anchor);
         INIT_ANCHOR(anchor);

コンパイラを書き換えたのでバイトコードを生成してみましょう。

$ ./miniruby --parser=parse.y --dump=i -e  '::A, B, ::C::D, E::F, g::H  = foo'
-e:1: iseq_compile_each: unknown node (RB_CONSTANT_PATH_TARGET_NODE)
-e: compile error (SyntaxError)

ConstantPathTargetNodeコンパイル方法がわからないと言われてしまいました。 ConstantPathTargetNodeコンパイルは右辺がnullなConstantPathWriteNodeと同じになるようにしたいので、compile_constant_path_write関数を拡張して両方を扱えるようにします。

ConstantPathWriteNodeの場合はtargetフィールドに定数参照用のノードが入っているのに対して、ConstantPathTargetNodeは自身がtargetになるという違いがあり多少煩雑ですが、まあいいでしょう。

-      case RB_CONSTANT_PATH_WRITE_NODE: {
-        CHECK(compile_constant_path_write(iseq, ret, rb_parser_shareable_none, RB_NODE_CONSTANT_PATH_WRITE(node), popped));
+      case RB_CONSTANT_PATH_WRITE_NODE:
+      case RB_CONSTANT_PATH_TARGET_NODE: {
+        CHECK(compile_constant_path_write(iseq, ret, rb_parser_shareable_none, node, popped));
         break;
       }

 static int
-compile_constant_path_write(rb_iseq_t *iseq, LINK_ANCHOR *const ret, enum rb_parser_shareability shareable, const rb_constant_path_write_node_t *co
nst node, int popped)
+compile_constant_path_write(rb_iseq_t *iseq, LINK_ANCHOR *const ret, enum rb_parser_shareability shareable, const NODE *const node, int popped)
 {
-    compile_cpath(ret, iseq, node->target);
-    CHECK(compile_shareable_constant_value(iseq, ret, shareable, node, node->value));
+    NODE *target, *nd_value;
+    ID name;
+
+    switch (nd_type(node)) {
+      case RB_CONSTANT_PATH_WRITE_NODE:
+        target = (NODE *)RB_NODE_CONSTANT_PATH_WRITE(node)->target;
+        nd_value = RB_NODE_CONSTANT_PATH_WRITE(node)->value;
+        name = RB_NODE_CONSTANT_PATH_WRITE(node)->target->name;
+        break;
+      case RB_CONSTANT_PATH_TARGET_NODE:
+        target = (NODE *)node;
+        nd_value = NULL;
+        name = RB_NODE_CONSTANT_PATH_TARGET(node)->name;
+        break;
+      default:
+        rb_bug("unexpected node: %s", ruby_node_name(nd_type(node)));
+    }
+
+    compile_cpath(ret, iseq, target);
+    CHECK(compile_shareable_constant_value(iseq, ret, shareable, node, nd_value));
     ADD_INSN(ret, node, swap);

     if (!popped) {
@@ -10894,7 +10938,7 @@ compile_constant_path_write(rb_iseq_t *iseq, LINK_ANCHOR *const ret, enum rb_par
         ADD_INSN(ret, node, swap);
     }

-    ADD_INSN1(ret, node, setconstant, ID2SYM(node->target->name));
+    ADD_INSN1(ret, node, setconstant, ID2SYM(name));
     return COMPILE_OK;
 }

ConstantTargetNodeも同様にConstantWriteNodeの処理に寄せておきます。

-      case RB_CONSTANT_WRITE_NODE: {
-        CHECK(compile_constant_write(iseq, ret, rb_parser_shareable_none, RB_NODE_CONSTANT_WRITE(node), popped));
+      case RB_CONSTANT_WRITE_NODE:
+      case RB_CONSTANT_TARGET_NODE: {
+        CHECK(compile_constant_write(iseq, ret, rb_parser_shareable_none, node, popped));
         break;
       }

 static int
-compile_constant_write(rb_iseq_t *iseq, LINK_ANCHOR *const ret, enum rb_parser_shareability shareable, const rb_constant_write_node_t *const node, int popped)
+compile_constant_write(rb_iseq_t *iseq, LINK_ANCHOR *const ret, enum rb_parser_shareability shareable, const NODE *const node, int popped)
 {
-    CHECK(compile_shareable_constant_value(iseq, ret, shareable, node, node->value));
+    NODE *nd_value;
+    ID name;
+
+    switch (nd_type(node)) {
+      case RB_CONSTANT_WRITE_NODE:
+        nd_value = RB_NODE_CONSTANT_WRITE(node)->value;
+        name = RB_NODE_CONSTANT_WRITE(node)->name;
+        break;
+      case RB_CONSTANT_TARGET_NODE:
+        nd_value = NULL;
+        name = RB_NODE_CONSTANT_TARGET(node)->name;
+        break;
+      default:
+        rb_bug("unexpected node: %s", ruby_node_name(nd_type(node)));
+    }
+
+    CHECK(compile_shareable_constant_value(iseq, ret, shareable, 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(node->name));
+    ADD_INSN1(ret, node, setconstant, ID2SYM(name));
     return COMPILE_OK;
 }

再度コンパイルをしてみます。 よさそうですね。

$ ./miniruby --parser=parse.y --dump=i -e  '::A, B, ::C::D, E::F, g::H  = foo'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,33)>
0000 putobject                              Object                    (   1)[Li]
0002 opt_getconstant_path                   <ic:0 ::C>
0004 opt_getconstant_path                   <ic:1 E>
0006 putself
0007 opt_send_without_block                 <calldata!mid:g, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0009 putself
0010 opt_send_without_block                 <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0012 dup
0013 expandarray                            5, 0
0016 topn                                   9
0018 setconstant                            :A
0020 putspecialobject                       3
0022 setconstant                            :B
0024 topn                                   6
0026 setconstant                            :D
0028 topn                                   4
0030 setconstant                            :F
0032 topn                                   2
0034 setconstant                            :H
0036 setn                                   4
0038 pop
0039 pop
0040 pop
0041 pop
0042 leave

まとめ

今日の成果です。

  • shareable_constant_valueマジックコメントが指定されたときの定数への代入を対応した
  • 左辺に定数が含まれるmultiple assignmentを対応した



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

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