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


Ruby Parser開発日誌 (24-36) - parse.yが生成するノードを変える ー constant declaration with operator

36日目: A::B ||= 1

前回はa += 1の形式の代入をやりました。 今回は左辺が定数のときの代入をやっていきます。

これまでと同様に演算子の種類によって生成するノードが変わります。

  • A += 1: ConstantOperatorWriteNode
  • A ||= 1: ConstantOrWriteNode
  • A &&= 1: ConstantAndWriteNode

また定数に特有の話としてConstant(A += 1)とConstantPath(::A += 1A::B += 1)を使いわける必要もあります。

parse.yを変更する

まず変更するのはnew_const_op_assign関数の実装です。 この関数はA::B += 1::A += 1を解析するときに呼ばれます。

%rule op_asgn(rhs) <node>
                | primary_value tCOLON2 tCONSTANT tOP_ASGN lex_ctxt rhs
                    {
                        YYLTYPE loc = code_loc_gen(&@primary_value, &@tCONSTANT);
                        $$ = new_const_op_assign(p, NEW_COLON2($primary_value, $tCONSTANT, &loc, &@tCOLON2, &@tCONSTANT), $tOP_ASGN, $rhs, $lex_ctxt, &@$);
                    /*% ripper: opassign!(const_path_field!($:1, $:3), $:4, $:6) %*/
                    }
                | tCOLON3 tCONSTANT tOP_ASGN lex_ctxt rhs
                    {
                        YYLTYPE loc = code_loc_gen(&@tCOLON3, &@tCONSTANT);
                        $$ = new_const_op_assign(p, NEW_COLON3($tCONSTANT, &loc, &@tCOLON3, &@tCONSTANT), $tOP_ASGN, $rhs, $lex_ctxt, &@$);
                    /*% ripper: opassign!(top_const_field!($:2), $:3, $:5) %*/
                    }

前回、前々回と同様に演算子の種類とノードの種類に応じて生成するノードを変えます。 またshareable_constant_valueマジックコメントがあるときのために最後にnew_shareable_constant関数を呼び出して、必要に応じてShareableConstantNodeでラップするようにしておきます。

static rb_node_t *
new_const_op_assign(struct parser_params *p, rb_node_t *lhs, ID op, rb_node_t *rhs, struct lex_context ctxt, const YYLTYPE *loc)
{
    rb_node_t *asgn;

    if (lhs) {
        if (op == tOROP) {
            switch (nd_type(lhs)) {
              case RB_CONSTANT_READ_NODE: {
                ...
              }
              case RB_CONSTANT_PATH_NODE: {
                ...
              }
              ...
            }
        }
        else if (op == tANDOP) {
            ...
        }
        else {
            ...
        }

        asgn = new_shareable_constant(p, asgn);
    }
    else {
        asgn = NEW_ERROR(loc);
    }
    return asgn;
}

そのほかnew_op_assign関数も修正しておきます。 これはA += 1などのときに以下の生成規則のアクションが適用されるからです。

%rule op_asgn(rhs) <node>
                : var_lhs tOP_ASGN lex_ctxt rhs
                    {
                        $$ = new_op_assign(p, $var_lhs, $tOP_ASGN, $rhs, $lex_ctxt, &@$);
                    /*% ripper: opassign!($:1, $:2, $:4) %*/
                    }

shareable_constant_valueマジックコメントがある場合、new_op_assign関数にはShareableConstantNodeが渡ってくるので、ShareableConstantNodeについてもケアする必要があります。

@@ -17274,6 +17352,16 @@ new_op_assign(struct parser_params *p, rb_node_t *lhs, ID op, rb_node_t *rhs, st
                 asgn = NEW_RB_GLOBAL_VARIABLE_OR_WRITE(cast->name, rhs, loc);
                 break;
               }
+              case RB_CONSTANT_WRITE_NODE: {
+                rb_constant_write_node_t *cast = (rb_constant_write_node_t *)lhs;
+                asgn = NEW_RB_CONSTANT_OR_WRITE(cast->name, rhs, loc);
+                break;
+              }
+              case RB_SHAREABLE_CONSTANT_NODE: {
+                asgn = lhs;
+                RB_NODE_SHAREABLE_CONSTANT(lhs)->write = new_op_assign(p, RB_NODE_SHAREABLE_CONSTANT(lhs)->write, op, rhs, ctxt, loc);
+                break;
+              }
               default:

compile.cを変更する

ここで一度、書き換え前後のノードと書き換え前のcompile.cで使っている関数を整理しておきます。

Before After compile (Before)
A += 1 NODE_CDECL ConstantOperatorWriteNode iseq_compile_each0
A ||= 1 NODE_OP_ASGN_OR ConstantOrWriteNode compile_op_log
A &&= 1 NODE_OP_ASGN_AND ConstantAndWriteNode compile_op_log
::A += 1 NODE_OP_CDECL ConstantPathOperatorWriteNode compile_op_cdecl
::A ||= 1 NODE_OP_CDECL ConstantPathOrWriteNode compile_op_cdecl
::A &&= 1 NODE_OP_CDECL ConstantPathAndWriteNode compile_op_cdecl
A::B += 1 NODE_OP_CDECL ConstantPathOperatorWriteNode compile_op_cdecl
A::B ||= 1 NODE_OP_CDECL ConstantPathOrWriteNode compile_op_cdecl
A::B &&= 1 NODE_OP_CDECL ConstantPathAndWriteNode compile_op_cdecl

使用している関数に注目して、以下の3つのグループに分けて書き換えを考えることにします。

  • ConstantOperatorWriteNode
  • ConstantOrWriteNodeConstantAndWriteNode
  • その他

A += 1をコンパイルする

ノードの書き換え前は以下のようにiseq_compile_each0関数のなかに直接コンパイルするためのロジックが書かれていました。

case NODE_CDECL:{
  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;
}

今回対象としているケースはA += 1であり、この場合常にnd_vidが設定されているため、実際はifの方の分岐だけを考えればよいでしょう。 shareable_constant_valueマジックコメントがあるときのことも考えて、今回はcompile_constant_operator_write関数として処理を切り出します。

まずshareable_constant_valueマジックコメントがない、もしくはnoneのときのことを考えましょう。

== disasm: #<ISeq:<main>@test.rb:1 (1,0)-(2,6)>
# A + 1の部分
#
# 0000 opt_getconstant_path                   <ic:0 A>                  (   2)[Li]
# 0002 putobject_INT2FIX_1_
# 0003 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
# 0005 dup

# 代入の部分
#
# 0006 putspecialobject                       3
# 0008 setconstant                            :A
# 0010 leave

# shareable_constant_value: none
A += 1

他の+=と同様に右辺を計算する部分と代入をする部分からなるバイトコードが生成されます。 さてこれまではノードのレベルで右辺がA + 1の形になっていたので、そのまま右辺をコンパイルすれば期待するバイトコードが生成されていました。

# @ NODE_CDECL (id: 0, line: 2, location: (2,0)-(2,6))*
# +- nd_vid: :A
# +- nd_else: not used
# +- shareability: none
# +- nd_value:
#     @ NODE_CALL (id: 4, line: 2, location: (2,0)-(2,6))
#     +- nd_mid: :+
#     +- nd_recv:
#     |   @ NODE_CONST (id: 2, line: 2, location: (2,0)-(2,1))
#     |   +- nd_vid: :A
#     +- nd_args:
#         @ NODE_LIST (id: 3, line: 2, location: (2,5)-(2,6))
#         +- as.nd_alen: 1
#         +- nd_head:
#         |   @ NODE_INTEGER (id: 1, line: 2, location: (2,5)-(2,6))
#         |   +- val: 1
#         +- nd_next:
#             (null node)

しかしノードの書き換え後はvalue: 1になっているため、自前でA + 1を組み立てる必要があります。

# @ ConstantOperatorWriteNode (location: (2,0)-(2,6))
# +-- name: :A
# +-- name_loc: (2,0)-(2,1) = "A"
# +-- binary_operator_loc: (2,2)-(2,4) = "+="
# +-- value:
# |   @ IntegerNode (location: (2,5)-(2,6))
# |   +-- IntegerBaseFlags: decimal
# |   +-- value: 1
# +-- binary_operator: :+

これらを踏まえてcompile_constant_operator_write関数を実装すると以下のようになります。

static int
compile_constant_operator_write(rb_iseq_t *iseq, LINK_ANCHOR *const ret, enum rb_parser_shareability shareable, const NODE *const node, int popped)
{
    CHECK(compile_constant_read(iseq, ret, node, RB_NODE_CONSTANT_OPERATOR_WRITE(node)->name));
    CHECK(COMPILE(ret, "const op asgn value", RB_NODE_CONSTANT_OPERATOR_WRITE(node)->value));
    ADD_SEND_R(ret, node, RB_NODE_CONSTANT_OPERATOR_WRITE(node)->binary_operator, INT2FIX(1), NULL, INT2FIX(0), NULL);

    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_OPERATOR_WRITE(node)->name));
    return COMPILE_OK;
}

それではshareable_constant_valueが指定されているケースを考えてみましょう。 shareable_constant_valueにはliteral, experimental_copy, experimental_everythingがあるので、それぞれ生成されるバイトコードを確認します。

まずliteralの場合です。 このときA = RubyVMFrozenCore#ensure_shareable(A + 1, "A")に相当する命令が生成されます。 そのため0000 putspecialobject 1RubyVMFrozenCoreをスタックに積み、0007 putobject "A"0009 opt_send_without_block <calldata!mid:ensure_shareable, argc:2, ARGS_SIMPLE>#ensure_shareableを呼び出す命令が追加されています。

# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(2,6)>
# 0000 putspecialobject                       1                         (   2)[Li]
# 0002 opt_getconstant_path                   <ic:0 A>
# 0004 putobject_INT2FIX_1_
# 0005 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
# 0007 putobject                              "A"
# 0009 opt_send_without_block                 <calldata!mid:ensure_shareable, argc:2, ARGS_SIMPLE>
# 0011 dup
# 0012 putspecialobject                       3
# 0014 setconstant                            :A
# 0016 leave

# shareable_constant_value: literal
A += 1

experimental_copyの場合はA = RubyVMFrozenCore#make_shareable_copy(A + 1)に相当する命令が生成されます。

# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(2,6)>
# 0000 putspecialobject                       1                         (   2)[Li]
# 0002 opt_getconstant_path                   <ic:0 A>
# 0004 putobject_INT2FIX_1_
# 0005 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
# 0007 opt_send_without_block                 <calldata!mid:make_shareable_copy, argc:1, ARGS_SIMPLE>
# 0009 dup
# 0010 putspecialobject                       3
# 0012 setconstant                            :A
# 0014 leave

# shareable_constant_value: experimental_copy
A += 1

同様にexperimental_everythingの場合はA = RubyVMFrozenCore#make_shareable(A + 1)に相当する命令が生成されます。

# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(2,6)>
# 0000 putspecialobject                       1                         (   2)[Li]
# 0002 opt_getconstant_path                   <ic:0 A>
# 0004 putobject_INT2FIX_1_
# 0005 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
# 0007 opt_send_without_block                 <calldata!mid:make_shareable, argc:1, ARGS_SIMPLE>
# 0009 dup
# 0010 putspecialobject                       3
# 0012 setconstant                            :A
# 0014 leave

# shareable_constant_value: experimental_everything
A += 1

これまではノードのnd_value(右辺)がA + 1の形をしていたのでcompile_shareable_constant_value関数を呼び出すだけで、マジックコメントの値に応じたバイトコードを生成することができました。 しかし今回のノードの書き換えでvalue(右辺)が1になっているため、compile_shareable_constant_value関数にメソッド呼び出しのノードを渡したときと同じ挙動を実装する必要があります。

これらを踏まえてcompile_constant_operator_write関数を修正すると以下のようになります1

 static int
 compile_constant_operator_write(rb_iseq_t *iseq, LINK_ANCHOR *const ret, enum rb_parser_shareability shareable, const NODE *const node, int popped)
 {
+    if (shareable != rb_parser_shareable_none) {
+      ADD_INSN1(ret, node, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE));
+    }
     CHECK(compile_constant_read(iseq, ret, node, RB_NODE_CONSTANT_OPERATOR_WRITE(node)->name));
     CHECK(COMPILE(ret, "const op asgn value", RB_NODE_CONSTANT_OPERATOR_WRITE(node)->value));
     ADD_SEND_R(ret, node, RB_NODE_CONSTANT_OPERATOR_WRITE(node)->binary_operator, INT2FIX(1), NULL, INT2FIX(0), NULL);

+    switch (shareable) {
+      case rb_parser_shareable_none:
+        break;
+      case rb_parser_shareable_literal: {
+        VALUE path = const_decl_path(node);
+        ADD_INSN1(ret, node, putobject, path);
+        RB_OBJ_WRITTEN(iseq, Qundef, path);
+        ADD_SEND_WITH_FLAG(ret, node, rb_intern("ensure_shareable"), INT2FIX(2), INT2FIX(VM_CALL_ARGS_SIMPLE));
+        break;
+      }
+      case rb_parser_shareable_copy:
+        ADD_SEND_WITH_FLAG(ret, node, rb_intern("make_shareable_copy"), INT2FIX(1), INT2FIX(VM_CALL_ARGS_SIMPLE));
+        break;
+      case rb_parser_shareable_everything:
+        ADD_SEND_WITH_FLAG(ret, node, rb_intern("make_shareable"), INT2FIX(1), INT2FIX(VM_CALL_ARGS_SIMPLE));
+        break;
+    }
+
     if (!popped) {
         ADD_INSN(ret, node, dup);
     }

A ||= 1A &&= 1をコンパイルする

次にA ||= 1A &&= 1のコンパイルですが、ここでは引き続きcompile_op_log関数に処理を任せる方針で実装してみます。 定数のアクセスの仕方やマジックコメントが有効な際に右辺の値に対してRubyVMFrozenCoreのメソッドを呼ぶといった違いはあれど、生成されるバイトコードのフロー自体はa ||= 1などと同じ構造になるためです。

compile_op_log関数の変更点を1つずつみていきましょう。

まずは関数の引数です。 ShareableConstantNodeのときを考慮してenum rb_parser_shareability shareableを1つ増やします。

 static int
-compile_op_log(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, ID name, const NODE *const nd_value, int popped, bool op_and)
+compile_op_log(rb_iseq_t *iseq, LINK_ANCHOR *const ret, enum rb_parser_shareability shareable, const NODE *const node, ID name, const NODE *const n
d_value, int popped, bool op_and)

A ||= 1A = A || 1であることを踏まえて、右辺のAに対応するバイトコードを生成するようにします。

       case RB_GLOBAL_VARIABLE_OR_WRITE_NODE:
         ADD_INSN1(ret, node, getglobal, ID2SYM(name));
         break;
+      case RB_CONSTANT_AND_WRITE_NODE:
+      case RB_CONSTANT_OR_WRITE_NODE:
+        CHECK(compile_constant_read(iseq, ret, node, name));
+        break;
       default:
         UNKNOWN_NODE("compile_op_log", node, COMPILE_NG);
     }

右辺の演算子の後の値については必要に応じてRubyVMFrozenCoreのメソッドを呼ぶ必要があるため、ノードの種類に応じて分岐します。

-    CHECK(COMPILE(ret, "NODE_OP_ASGN_AND/OR#nd_value", nd_value));
+
+    if (nd_type_p(node, RB_CONSTANT_AND_WRITE_NODE) || nd_type_p(node, RB_CONSTANT_OR_WRITE_NODE)) {
+        CHECK(compile_shareable_constant_value(iseq, ret, shareable, node, nd_value));
+    }
+    else {
+        CHECK(COMPILE(ret, "NODE_OP_ASGN_AND/OR#nd_value", nd_value));
+    }
+

最後に定数へ代入するためのバイトコードを生成します。

       case RB_GLOBAL_VARIABLE_OR_WRITE_NODE:
         ADD_INSN1(ret, node, setglobal, ID2SYM(name));
         break;
+      case RB_CONSTANT_AND_WRITE_NODE:
+      case RB_CONSTANT_OR_WRITE_NODE:
+        ADD_INSN1(ret, node, putspecialobject,
+                  INT2FIX(VM_SPECIAL_OBJECT_CONST_BASE));
+        ADD_INSN1(ret, node, setconstant, ID2SYM(name));
+        break;
       default:
         UNKNOWN_NODE("compile_op_log", node, COMPILE_NG);

その他をコンパイルする

ConstantPathOperatorWriteNode, ConstantPathOrWriteNode, ConstantPathAndWriteNodeについてはcompile_op_cdecl関数を修正することで対応します。

複数種類のノードを扱うためにcompile_op_cdecl関数の呼び出し元で以下の3つの要素を取り出してcompile_op_cdecl関数に渡すようにします。

  • 左辺を表すnd_head
  • 演算子を表すnd_aid
  • 右辺を表すnd_value
 static int
-compile_op_cdecl(rb_iseq_t *iseq, LINK_ANCHOR *const ret, enum rb_parser_shareability shareability, const NODE *const node, ID nd_aid, int popped)
+compile_op_cdecl(rb_iseq_t *iseq, LINK_ANCHOR *const ret, enum rb_parser_shareability shareability, const NODE *const node, const rb_constant_path_node_t *const nd_head, ID nd_aid, const NODE *const nd_value, int popped)

関数の内部の修正については特筆すべきこともないので省略します。

最後にいくつか簡単な例を実行して動作を確認しておきます。

A = 1
p A
#=> 1

A += 1
p A
#=> 2

A ||= 10
p A
#=> 2

A &&= 12
p A
#=> 12

::A = 1
p ::A
#=> 1

::A += 1
p ::A
#=> 2

::A ||= 10
p ::A
#=> 2

::A &&= 12
p ::A
#=> 12

module A
end

A::B = 1
p A::B
#=> 1

A::B += 1
p A::B
#=> 2

A::B ||= 10
p A::B
#=> 2

A::B &&= 12
p A::B
#=> 12

良さそうです。

まとめ

今日の成果です。

  • constant declaration with operator (A::B ||= 1)に対応した

これでメソッド呼び出しに関する構文は全て終わったので、次回は制御構文のうち未対応のものをやっていきたいと思います。


  1. このあたりcompile_ensure_shareable_node関数とcompile_make_shareable_node関数をうまく整理すると共通化できるような気がしなくもないが...



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

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