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


Ruby Parser開発日誌 (24-24) - parse.yが生成するノードを変える ー aliasとback_refとnth_ref

24日目: aliasとback_refとnth_ref

ずっと制御構文をやっていたので、たまには気分を変えてaliasの書き換えをやっていきたいと思います1

aliasのノードを書き換える

書き換え前の世界ではグローバル変数のaliasを表すNODE_VALIASとそれ以外のaliasを表すNODE_ALIASという2つのノードがありました。

# @ NODE_ALIAS (id: 2, line: 1, location: (1,0)-(1,11))*
# +- nd_1st:
# |   @ NODE_SYM (id: 0, line: 1, location: (1,6)-(1,8))
# |   +- string: :a
# +- nd_2nd:
# |   @ NODE_SYM (id: 1, line: 1, location: (1,9)-(1,11))
# |   +- string: :b
alias :a :b

# @ NODE_VALIAS (id: 3, line: 2, location: (2,0)-(2,11))*
# +- nd_alias: :$a
# +- nd_orig: :$b
alias $a $b

# @ NODE_VALIAS (id: 7, line: 3, location: (3,0)-(3,11))*
# +- nd_alias: :$a
# +- nd_orig: :$&
alias $a $&

書き換え後はグローバル変数の場合はAliasGlobalVariableNode、それ以外の場合はAliasMethodNodeというノードを使うことになります。 ノードの構造が変わるわけではないので、書き換え自体はさほど難しくありません。

グローバル変数のaliasを表すノードのフィールドについては注意が必要です。 NODE_VALIASではnd_alias, nd_origともに型がIDでしたが、AliasGlobalVariableNodeではどちらもノードへのポインターになります。 そのためAliasGlobalVariableNodeの生成時にIDをGlobalVariableReadNodeでラップするようにします。

                 | keyword_alias tGVAR tGVAR
                     {
-                        $$ = NEW_VALIAS($2, $3, &@$, &@1);
+                        $$ = NEW_RB_ALIAS_GLOBAL_VARIABLE(NEW_RB_GLOBAL_VARIABLE_READ($2, &@2), NEW_RB_GLOBAL_VARIABLE_READ($3, &@3), &@$, &@1);
                     /*% ripper: var_alias!($:2, $:3) %*/
                     }

ここで同時にback ref(バックレファレンス)2に関するノードも変更しておきましょう。 back refを表すNODE_BACK_REFでは&'といった文字をlong nd_nthというフィールドで保持しています3。 書き換え後のBackReferenceReadNodeに合わせてIDに変換してからノードを生成するようにしておきます。

                 | keyword_alias tGVAR tBACK_REF
                     {
-                        char buf[2];
-                        buf[0] = '$';
-                        buf[1] = (char)RNODE_BACK_REF($3)->nd_nth;
-                        $$ = NEW_VALIAS($2, rb_intern2(buf, 2), &@$, &@1);
+                        $$ = NEW_RB_ALIAS_GLOBAL_VARIABLE(NEW_RB_GLOBAL_VARIABLE_READ($2, &@2), $3, &@$, &@1);
                     /*% ripper: var_alias!($:2, $:3) %*/
                     }

+static rb_back_reference_read_node_t *
+rb_new_node_back_reference_read_new(struct parser_params *p, long nd_nth, const YYLTYPE *loc)
+{
+    char buf[2];
+    buf[0] = '$';
+    buf[1] = (char)nd_nth;
+
+    rb_back_reference_read_node_t *n = RB_NEW_NODE_NEWNODE((enum rb_node_type)RB_BACK_REFERENCE_READ_NODE, rb_back_reference_read_node_t, loc);
+    n->name = rb_intern2(buf, 2);
+
+    return n;
+}

ここまでで一度ノードを確認しておきましょう。

alias :a :b
alias $a $b
alias $a $&

# @ StatementsNode (location: (1,0)-(3,11))
# +-- body: (length: 3)
#     +-- @ AliasMethodNode (location: (1,0)-(1,11))*
#     |   +-- new_name:
#     |   |   @ SymbolNode (location: (1,6)-(1,8))
#     |   |   +-- SymbolFlags: nil
#     |   |   +-- opening_loc: (0,0)-(0,0) = ""
#     |   |   +-- value_loc: (0,0)-(0,0) = ""
#     |   |   +-- closing_loc: (0,0)-(0,0) = ""
#     |   |   +-- unescaped: "a"
#     |   +-- old_name:
#     |   |   @ SymbolNode (location: (1,9)-(1,11))
#     |   |   +-- SymbolFlags: nil
#     |   |   +-- opening_loc: (0,0)-(0,0) = ""
#     |   |   +-- value_loc: (0,0)-(0,0) = ""
#     |   |   +-- closing_loc: (0,0)-(0,0) = ""
#     |   |   +-- unescaped: "b"
#     |   +-- keyword_loc: (1,0)-(1,5) = ""
#     +-- @ AliasGlobalVariableNode (location: (2,0)-(2,11))*
#     |   +-- new_name:
#     |   |   @ GlobalVariableReadNode (location: (2,6)-(2,8))
#     |   |   +-- name: :$a
#     |   +-- old_name:
#     |   |   @ GlobalVariableReadNode (location: (2,9)-(2,11))
#     |   |   +-- name: :$b
#     |   +-- keyword_loc: (2,0)-(2,5) = ""
#     +-- @ AliasGlobalVariableNode (location: (3,0)-(3,11))*
#         +-- new_name:
#         |   @ GlobalVariableReadNode (location: (3,6)-(3,8))
#         |   +-- name: :$a
#         +-- old_name:
#         |   @ BackReferenceReadNode (location: (3,9)-(3,11))
#         |   +-- name: :$&
#         +-- keyword_loc: (3,0)-(3,5) = ""

aliasとバックレファレンスをコンパイルする

aliasに関するバイトコードaliasに続く値をそれぞれスタックにつんで、特殊なメソッドを呼び出すだけです。 呼び出すメソッドがグローバル変数の場合はset_variable_alias、それ以外の場合はset_method_aliasという違いはありますが、シンプルな構造だと思います。

グローバル変数の場合はスタックにはシンボルを積むことになるので、GlobalVariableReadNodeBackReferenceReadNodeからnameを取り出すための補助的な関数を用意します。 そのほかは書き換え前と変わりません。

      case RB_ALIAS_METHOD_NODE: {
        ADD_INSN1(ret, node, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE));
        ADD_INSN1(ret, node, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_CBASE));
        CHECK(COMPILE(ret, "alias arg1", RB_NODE_ALIAS_METHOD(node)->new_name));
        CHECK(COMPILE(ret, "alias arg2", RB_NODE_ALIAS_METHOD(node)->old_name));
        ADD_SEND(ret, node, id_core_set_method_alias, INT2FIX(3));

        if (popped) {
            ADD_INSN(ret, node, pop);
        }
        break;
      }
      case RB_ALIAS_GLOBAL_VARIABLE_NODE: {
        ADD_INSN1(ret, node, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE));
        ADD_INSN1(ret, node, putobject, ID2SYM(get_valias_id(RB_NODE_ALIAS_GLOBAL_VARIABLE(node)->new_name)));
        ADD_INSN1(ret, node, putobject, ID2SYM(get_valias_id(RB_NODE_ALIAS_GLOBAL_VARIABLE(node)->old_name)));
        ADD_SEND(ret, node, id_core_set_variable_alias, INT2FIX(2));

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


static VALUE
get_valias_id(const NODE *node)
{
    switch (nd_type(node)) {
      case RB_GLOBAL_VARIABLE_READ_NODE:
        return RB_NODE_GLOBAL_VARIABLE_READ(node)->name;
      case RB_BACK_REFERENCE_READ_NODE:
        return RB_NODE_BACK_REFERENCE_READ(node)->name;
      default:
        rb_bug("unexpected node: %s", ruby_node_name(nd_type(node)));
    }
}

BackReferenceReadNodeそのものもコンパイルもできるようにしておきましょう。 もともとはlongで持っていた情報がIDになっているので、一度文字列に戻してからcharを取得する必要があります。

// Before
case NODE_BACK_REF:{
  if (!popped) {
      ADD_INSN2(ret, node, getspecial, INT2FIX(1) /* '~' */,
                INT2FIX(0x01 | (RNODE_BACK_REF(node)->nd_nth << 1)));
  }
  break;
}

// After
case RB_BACK_REFERENCE_READ_NODE: {
  if (!popped) {
      const char *str = rb_id2name(RB_NODE_BACK_REFERENCE_READ(node)->name);
      ADD_INSN2(ret, node, getspecial, INT2FIX(1) /* '~' */,
                INT2FIX(0x01 | ((long)str[1] << 1)));
  }
  break;
}

getspecial命令の実装であるところのvm_getspecial関数をみるとわかるのですが、1bit分シフトをして下位1bitの値でnth ref4とback refの区別がつくようにしています(下位1bitがたっている場合がback ref)。

一度コンパイル結果を確認しておきましょう。

# 0000 putspecialobject                       1                         (   1)[Li]
# 0002 putspecialobject                       2
# 0004 putobject                              :a
# 0006 putobject                              :b
# 0008 opt_send_without_block                 <calldata!mid:core#set_method_alias, argc:3, ARGS_SIMPLE>
# 0010 pop
alias :a :b

# 0011 putspecialobject                       1                         (   2)[Li]
# 0013 putobject                              :$a
# 0015 putobject                              :$b
# 0017 opt_send_without_block                 <calldata!mid:core#set_variable_alias, argc:2, ARGS_SIMPLE>
# 0019 pop
alias $a $b

# 0020 putspecialobject                       1                         (   3)[Li]
# 0022 putobject                              :$a
# 0024 putobject                              :$&
# 0026 opt_send_without_block                 <calldata!mid:core#set_variable_alias, argc:2, ARGS_SIMPLE>
# 0028 leave
alias $a $&

# 0029 putself                                                          (   4)[Li]
# 0030 getspecial                             1, 77
# 0033 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0035 leave
p $&

nth refに対応する

back refの対応をしたので、ついでにnth refについてもやっておきましょう。 nth ref、つまり$1などですが、これにはNODE_NTH_REFというノードが使われています。 書き換え後のノードはNumberedReferenceReadNodeで、どちらも1の部分をフィールドに保持しています。

$1
# @ NODE_NTH_REF (id: 0, line: 1, location: (1,0)-(1,2))*
# +- nd_nth: $1

# @ NumberedReferenceReadNode (location: (1,0)-(1,2))
# +-- number: 1

BackReferenceReadNodeと異なり、NumberedReferenceReadNodenumberuint32_tと整数型なのでIDと文字列間の変換が不要です。

BackReferenceReadNodeのときはINT2FIX(0x01 | ((long)str[1] << 1))0x01との論理和をとっていましたが、NumberedReferenceReadNodeではINT2FIX(RB_NODE_NUMBERED_REFERENCE_READ(node)->number << 1)と1ビットシフトするだけになっているのがわかります。

// Before
case NODE_NTH_REF:{
  if (!popped) {
      if (!RNODE_NTH_REF(node)->nd_nth) {
          ADD_INSN(ret, node, putnil);
          break;
      }
      ADD_INSN2(ret, node, getspecial, INT2FIX(1) /* '~'  */,
                INT2FIX(RNODE_NTH_REF(node)->nd_nth << 1));
  }
  break;
}

// After
case RB_NUMBERED_REFERENCE_READ_NODE: {
  if (!popped) {
      if (!RB_NODE_NUMBERED_REFERENCE_READ(node)->number) {
          ADD_INSN(ret, node, putnil);
          break;
      }
      ADD_INSN2(ret, node, getspecial, INT2FIX(1) /* '~'  */,
                INT2FIX(RB_NODE_NUMBERED_REFERENCE_READ(node)->number << 1));
  }
  break;
}

コンパイル結果もとくに問題なさそうです。

# 0000 getspecial                             1, 2                      (   1)[Li]
# 0003 leave
$1

いい時間になったので今日はこのへんで。

まとめ

今日の成果です。

  • aliasの対応をした
  • back ref($&など)の対応をした
  • nth ref(&1など)の対応をした

  1. 平日でパッと成果を積みたかったというのもありますね...
  2. 最後に成功した正規表現のマッチに関連する特殊な変数のことで、具体的には$&, $`, $', $+の4つです。
  3. たかだか1文字なので...
  4. 最後に成功した正規表現のマッチのうちカッコの部分が格納されている変数のことで、$1からはじまります。



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

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