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


Ruby Parser開発日誌 (24-12) - parse.yが生成するノードを変える ー multiple assignment (parser編)

12日目: multiple assignment

attr assignmentに続いて今回はmultiple assignmentをやっていこうとおもいます。

multiple assignmentというのはa, b = fooのように左辺に2つ以上の何かがある代入のことです1。 multiple assignmentに類似した概念はメソッドやブロックの仮引数、forといった他の文法要素にも登場します。

今回は手始めに代入に絞ってparserとコンパイラの変更をしていきましょう。

文法からみるmultiple assignment

正直なことを言うとmultiple assignmentについては仕様を含め、あまりちゃんと理解していません。 そこでまずは観察をしてみます。

mlhs_basic  : mlhs_head
            | mlhs_head mlhs_item
            | mlhs_head tSTAR mlhs_node
            | mlhs_head tSTAR mlhs_node ',' mlhs_items(mlhs_item)
            | mlhs_head tSTAR
            | mlhs_head tSTAR ',' mlhs_items(mlhs_item)
            | tSTAR mlhs_node
            | tSTAR mlhs_node ',' mlhs_items(mlhs_item)
            | tSTAR
            | tSTAR ',' mlhs_items(mlhs_item)
            ;

なるほど、multiple assignmentには例えば以下のような書き方があるようです。

  • a, b
  • a, b, *r
  • *r, c, d
  • a, b, *r, c, d
  • *r
  • *

メソッド定義の仮引数の前半部分に似ていますね。 abの箇所にはどのような要素が書けるかをみてみましょう。

mlhs_head   : mlhs_item ','
            | mlhs_head mlhs_item ','
            ;

mlhs_item   : mlhs_node
            | tLPAREN mlhs_inner rparen // !!??
            ;

mlhs_node   : user_or_keyword_variable
            | primary_value '[' opt_call_args rbracket
            | primary_value call_op ident_or_const
            | primary_value tCOLON2 tIDENTIFIER
            | primary_value tCOLON2 tCONSTANT
            | tCOLON3 tCONSTANT
            | backref // ERROR
            ;

基本となるのはmlhs_nodeです。 user_or_keyword_variableというのはローカル変数やインスタンス変数だとおもってください。 そのほかにもa[0]struct.fieldも書くことができるようです。

なるほど?

もう一つ注意が必要なのがmlhs_item | tLPAREN mlhs_inner rparenです。

mlhs_inner  : mlhs_basic
            | tLPAREN mlhs_inner rparen

これと組み合わせて考えると、*以外の場所であれば無限に()でネストすることができるようです。

なるほど??

# 複雑な例
(a0, (a1, (a2[0], *r0)), *r1, b0, (*r2, b1, (struct.field, b2))) = foo

これはたしかにRubyスクリプトとしてvalidなようです。

ノードからみるmultiple assignment

生成されるノードを眺めます。

a, b = foo
#     @ NODE_MASGN (id: 4, line: 1, location: (1,0)-(1,10))*
#     +- nd_value:
#     |   @ NODE_VCALL (id: 5, line: 1, location: (1,7)-(1,10))
#     |   +- nd_mid: :foo
#     +- nd_head:
#     |   @ NODE_LIST (id: 1, line: 1, location: (1,0)-(1,4))
#     |   +- as.nd_alen: 2
#     |   +- nd_head:
#     |   |   @ NODE_LASGN (id: 0, line: 1, location: (1,0)-(1,1))
#     |   |   +- nd_vid: :a
#     |   |   +- nd_value:
#     |   |       (null node)
#     |   +- nd_head:
#     |   |   @ NODE_LASGN (id: 2, line: 1, location: (1,3)-(1,4))
#     |   |   +- nd_vid: :b
#     |   |   +- nd_value:
#     |   |       (null node)
#     |   +- nd_next:
#     |       (null node)
#     +- nd_args:
#         (null node)

a, *r = foo
#     @ NODE_MASGN (id: 3, line: 1, location: (1,0)-(1,11))*
#     +- nd_value:
#     |   @ NODE_VCALL (id: 4, line: 1, location: (1,8)-(1,11))
#     |   +- nd_mid: :foo
#     +- nd_head:
#     |   @ NODE_LIST (id: 1, line: 1, location: (1,0)-(1,1))
#     |   +- as.nd_alen: 1
#     |   +- nd_head:
#     |   |   @ NODE_LASGN (id: 0, line: 1, location: (1,0)-(1,1))
#     |   |   +- nd_vid: :a
#     |   |   +- nd_value:
#     |   |       (null node)
#     |   +- nd_next:
#     |       (null node)
#     +- nd_args:
#         @ NODE_LASGN (id: 2, line: 1, location: (1,4)-(1,5))
#         +- nd_vid: :r
#         +- nd_value:
#             (null node)

a, b, *r, c, d = foo
#     @ NODE_MASGN (id: 10, line: 5, location: (5,0)-(5,20))*
#     +- nd_value:
#     |   @ NODE_VCALL (id: 11, line: 5, location: (5,17)-(5,20))
#     |   +- nd_mid: :foo
#     +- nd_head:
#     |   @ NODE_LIST (id: 1, line: 5, location: (5,0)-(5,4))
#     |   +- as.nd_alen: 2
#     |   +- nd_head:
#     |   |   @ NODE_LASGN (id: 0, line: 5, location: (5,0)-(5,1))
#     |   |   +- nd_vid: :a
#     |   |   +- nd_value:
#     |   |       (null node)
#     |   +- nd_head:
#     |   |   @ NODE_LASGN (id: 2, line: 5, location: (5,3)-(5,4))
#     |   |   +- nd_vid: :b
#     |   |   +- nd_value:
#     |   |       (null node)
#     |   +- nd_next:
#     |       (null node)
#     +- nd_args:
#         @ NODE_POSTARG (id: 9, line: 5, location: (5,0)-(5,14))
#         +- nd_1st:
#         |   @ NODE_LASGN (id: 4, line: 5, location: (5,7)-(5,8))
#         |   +- nd_vid: :r
#         |   +- nd_value:
#         |       (null node)
#         +- nd_2nd:
#             @ NODE_LIST (id: 6, line: 5, location: (5,10)-(5,14))
#             +- as.nd_alen: 2
#             +- nd_head:
#             |   @ NODE_LASGN (id: 5, line: 5, location: (5,10)-(5,11))
#             |   +- nd_vid: :c
#             |   +- nd_value:
#             |       (null node)
#             +- nd_head:
#             |   @ NODE_LASGN (id: 7, line: 5, location: (5,13)-(5,14))
#             |   +- nd_vid: :d
#             |   +- nd_value:
#             |       (null node)
#             +- nd_next:
#                 (null node)

代入全体を表すNODE_MASGNnd_headに左辺の先頭部分を、nd_valueに右辺を保持していることがわかります。 *rがあるときはnd_argsにノードが追加されます。 また*rの後ろにさらにノードが続く場合には、NODE_POSTARGでラップされます。 NODE_POSTARGnd_1st*rに相当するノードを、nd_2ndc, dに相当するノードをもちます。

書き換え後のノード

書き換え後のMultiWriteNodeはこれら右辺と左辺の情報を全てもちます。

a, b, *r, c, d = foo
# +-- @ MultiWriteNode (location: (5,0)-(5,20))
#     +-- lefts: (length: 2)
#     |   +-- @ LocalVariableTargetNode (location: (5,0)-(5,1))
#     |   |   +-- name: :a
#     |   +-- @ LocalVariableTargetNode (location: (5,3)-(5,4))
#     |       +-- name: :b
#     +-- rest:
#     |   @ SplatNode (location: (5,6)-(5,8))
#     |   +-- operator_loc: (5,6)-(5,7) = "*"
#     |   +-- expression:
#     |       @ LocalVariableTargetNode (location: (5,7)-(5,8))
#     |       +-- name: :r
#     +-- rights: (length: 2)
#     |   +-- @ LocalVariableTargetNode (location: (5,10)-(5,11))
#     |   |   +-- name: :c
#     |   +-- @ LocalVariableTargetNode (location: (5,13)-(5,14))
#     |       +-- name: :d
#     +-- value:
#         @ CallNode (location: (5,17)-(5,20))
#         +-- CallNodeFlags: variable_call, ignore_visibility
#         +-- receiver: nil
#         +-- name: :foo
#         +-- arguments: nil
#         +-- block: nil

parse.yをいじる

ノードの書き換え前後でそこまで大きく構造が変わるわけではないので、そこまで難しくないでしょう。 mlhs_headではArrayNodeを作るようにして、mlhs_basicではMultiWriteNodeを作るようにします。

@@ -4009,12 +4012,12 @@ mlhs_item       : mlhs_node

 mlhs_head      : mlhs_item ','
                     {
-                        $$ = NEW_LIST($1, &@1);
+                        $$ = NEW_RB_ARRAY($1, &@1);
                     /*% ripper: mlhs_add!(mlhs_new!, $:1) %*/
                     }
                 | mlhs_head mlhs_item ','
                     {
-                        $$ = list_append(p, $1, $2);
+                        $$ = node_array_append(p, $1, $2, &NULL_LOC);
                     /*% ripper: mlhs_add!($:1, $:2) %*/
                     }
                 ;

@@ -3949,52 +3952,52 @@ mlhs_inner      : mlhs_basic

 mlhs_basic     : mlhs_head
                     {
-                        $$ = NEW_MASGN($1, 0, &@$);
+                        $$ = NEW_RB_MULTI_WRITE($1, 0, 0, &@$);
                     /*% ripper: $:1 %*/
                     }

minirubyをbuildしてノードを出力してみます。

$ ./miniruby --parser=parse.y --dump=p -e  "a, b, *r, c, d = foo"
./miniruby: [BUG] unexpected node: RB_MULTI_WRITE_NODE
...
/Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(rb_bug+0x28) [0x102bab4b8] ../../error.c:1116
/Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(node_assign+0x118) [0x102cb8cb0] parse.y:14873
/Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(ruby_yyparse+0x1750) [0x102caaff0] parse.y:3687
...

えーっと...

代入周りを久しぶりにいじったので忘れていましたが、node_assignset_nd_valueを修正する必要があるのでした。

@@ -2138,9 +2140,9 @@ set_nd_value(struct parser_params *p, rb_node_t *node, rb_node_t *rhs)
       // case NODE_DASGN:
         RB_NODE_LOCAL_VARIABLE_WRITE(node)->value = rhs;
         break;
-      // case NODE_MASGN:
-      //   RNODE_MASGN(node)->nd_value = rhs;
-      //   break;
+      case RB_MULTI_WRITE_NODE:
+        RB_NODE_MULTI_WRITE(node)->value = rhs;
+        break;
       case RB_CLASS_VARIABLE_WRITE_NODE:
         RB_NODE_CLASS_VARIABLE_WRITE(node)->value = rhs;
         break;

@@ -14837,9 +14855,9 @@ node_assign(struct parser_params *p, rb_node_t *lhs, rb_node_t *rhs, struct lex_
     switch (RB_NODE_TYPE(lhs)) {
       // case NODE_CDECL:
       case RB_GLOBAL_VARIABLE_WRITE_NODE:
-      case NODE_LASGN:
+      case NODE_LASGN: // NODE_DASGN
       // case NODE_DASGN:
-      // case NODE_MASGN:
+      case RB_MULTI_WRITE_NODE:
       case RB_CLASS_VARIABLE_WRITE_NODE:
         set_nd_value(p, lhs, rhs);
         rb_nd_set_loc(lhs, loc);

再度チャレンジ。

$ ./miniruby --parser=parse.y --dump=p -e  "a, b, *r, c, d = foo"
-e: [BUG] Segmentation fault at 0x0000000000000000
...
/usr/lib/system/libsystem_platform.dylib(_sigtramp+0x38) [0x19722e584]
/Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(prettyprint_node+0x1c) [0x10080c0c4] parser_prettyprint.c:35
/Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(prettyprint_node+0x11930) [0x10081d9d8] parser_prettyprint.c:6194
...

今度はなに...

case RB_LOCAL_VARIABLE_WRITE_NODE: {
    ...
    // value
    {
        pm_buffer_concat(output_buffer, prefix_buffer);
        pm_buffer_append_string(output_buffer, "+-- value:", 10);
        pm_buffer_append_byte(output_buffer, '\n');

        size_t prefix_length = prefix_buffer->length;
        pm_buffer_append_string(prefix_buffer, "|   ", 4);
        pm_buffer_concat(output_buffer, prefix_buffer);
        // L:6194
        prettyprint_node(output_buffer, (rb_node_t *) cast->value, prefix_buffer);
        prefix_buffer->length = prefix_length;
    }

multiple assignmentなんだから右辺(value)はnullになるでしょうに...

ん?

ちょっとノードを確かめてみます。

$ ruby --parser=prism --dump=p -e  "a, b, *r, c, d = foo"

+-- @ MultiWriteNode (location: (1,0)-(1,20))
    +-- lefts: (length: 2)
    |   +-- @ LocalVariableTargetNode (location: (1,0)-(1,1))
    |   |   +-- name: :a
    |   |   +-- depth: 0
    |   +-- @ LocalVariableTargetNode (location: (1,3)-(1,4))
    |       +-- name: :b
    |       +-- depth: 0
    +-- rest:
    |   @ SplatNode (location: (1,6)-(1,8))
    |   +-- operator_loc: (1,6)-(1,7) = "*"
    |   +-- expression:
    |       @ LocalVariableTargetNode (location: (1,7)-(1,8))
    |       +-- name: :r
    |       +-- depth: 0

LocalVariableWriteNodeではなくLocalVariableTargetNodeというノードが使われていますね。 LocalVariableTargetNodeにはvalueのフィールドがありません。

なるほどなぁ?

Target Node

念のため左辺に書くことができる要素について、どのようなノードが割り振られているか確認しておきます。

単一代入 複数代入
a LocalVariableWriteNode LocalVariableTargetNode
@a InstanceVariableWriteNode InstanceVariableTargetNode
@@a ClassVariableWriteNode ClassVariableTargetNode
$a GlobalVariableWriteNode GlobalVariableTargetNode
a[0] CallNode IndexTargetNode
s.f CallNode CallTargetNode
A ConstantWriteNode ConstantTargetNode

a[0]については単一代入のときは他のメソッド呼び出し同様にCallNodeを使いますが、複数代入のときはIndexTargetNodeという専用のノードを使うようです。 どちらのケースもattribute_writeというフラグは立つようなのでCallTargetNodeで表現できない理由がなにかあるのか気になりますが、深入りせずに進みましょう。

ちなみにs.fは単一代入のときはattribute_writeフラグがたちますが、複数代入のときは特にフラグはたたないようです。

またその他の細かい差異としてCallTargetNodeにはblockフィールドがありませんが、IndexTargetNodeにはblockフィールドがあります。

parse.yの実装としてはassignable_targetaryset_targetといった専用の関数を用意すればいいでしょう。

@@ -4026,22 +4041,22 @@ mlhs_head       : mlhs_item ','
 mlhs_node      : user_or_keyword_variable
                     {
                     /*% ripper: var_field!($:1) %*/
-                        $$ = assignable(p, $1, 0, &@$);
+                        $$ = assignable_target(p, $1, 0, &@$);
                     }
                 | primary_value '[' opt_call_args rbracket
                     {
-                        $$ = aryset(p, $1, $3, &@$);
+                        $$ = aryset_target(p, $1, $3, &@$);
                     /*% ripper: aref_field!($:1, $:3) %*/
                     }
                 | primary_value call_op ident_or_const
                     {
                         anddot_multiple_assignment_check(p, &@2, $2);
-                        $$ = attrset(p, $1, $2, $3, &@$);
+                        $$ = attrset_target(p, $1, $2, $3, &@$);
                     /*% ripper: field!($:1, $:2, $:3) %*/
                     }

再度実行してみます。 良さそうですね。

$ ./miniruby --parser=parse.y --dump=p -e  "a, @b, *r, @@c, \$d, e[nil], f.g = foo"
+-- @ MultiWriteNode (location: (1,0)-(1,37))*
    +-- lefts: (length: 2)
    |   +-- @ LocalVariableTargetNode (location: (1,0)-(1,1))
    |   |   +-- name: :a
    |   +-- @ InstanceVariableTargetNode (location: (1,3)-(1,5))
    |       +-- name: :@b
    +-- rest:
    |   @ SplatNode (location: (0,-1)-(0,-1))
    |   +-- operator_loc: (1,7)-(1,8) = ""
    |   +-- expression:
    |       @ LocalVariableTargetNode (location: (1,8)-(1,9))
    |       +-- name: :r
    +-- rights: (length: 4)
    |   +-- @ ClassVariableTargetNode (location: (1,11)-(1,14))
    |   |   +-- name: :@@c
    |   +-- @ GlobalVariableTargetNode (location: (1,16)-(1,18))
    |   |   +-- name: :$d
    |   +-- @ IndexTargetNode (location: (1,20)-(1,26))
    |   |   +-- receiver:
    |   |   |   @ CallNode (location: (1,20)-(1,21))
    |   |   |   +-- name: :e
    |   |   +-- arguments:
    |   |   |   @ ArgumentsNode (location: (1,22)-(1,25))
    |   |   |   +-- ArgumentsNodeFlags: nil
    |   |   |   +-- arguments: (length: 1)
    |   |   |       +-- @ NilNode (location: (1,22)-(1,25))
    |   |   +-- block: nil
    |   +-- @ CallTargetNode (location: (1,28)-(1,31))
    |       +-- receiver:
    |       |   @ CallNode (location: (1,28)-(1,29))
    |       |   +-- name: :f
    |       +-- name: :g=
    +-- value:
        @ CallNode (location: (1,34)-(1,37))
        +-- name: :foo

Multi Target Node

multiple assignmentがネストできるため、MultiWriteNodeにもTarget版のノードがあります。

a, (b, c) = foo
# +-- @ MultiWriteNode (location: (1,0)-(1,15))
#     +-- lefts: (length: 2)
#     |   +-- @ LocalVariableTargetNode (location: (1,0)-(1,1))
#     |   |   +-- name: :a
#     |   |   +-- depth: 0
#     |   +-- @ MultiTargetNode (location: (1,3)-(1,9))
#     |       +-- lefts: (length: 2)
#     |       |   +-- @ LocalVariableTargetNode (location: (1,4)-(1,5))
#     |       |   |   +-- name: :b
#     |       |   |   +-- depth: 0
#     |       |   +-- @ LocalVariableTargetNode (location: (1,7)-(1,8))
#     |       |       +-- name: :c
#     |       |       +-- depth: 0
#     |       +-- rest: nil
#     |       +-- rights: (length: 0)
#     +-- rest: nil
#     +-- rights: (length: 0)
#     +-- value:
#         @ CallNode (location: (1,12)-(1,15))
#         +-- name: :foo

MultiWriteNodeからMultiTargetNodeMultiTargetNodeからMultiWriteNodeへ変換する関数をそれぞれ用意して対応する生成規則のアクションでノードを変換すればいいでしょう。

@@ -3937,15 +3956,18 @@ command         : fcall command_args       %prec tLOWEST
 mlhs           : mlhs_basic
                 | tLPAREN mlhs_inner rparen
                     {
-                        $$ = $2;
+                        $$ = multi_target2multi_write(p, $2);
                     /*% ripper: mlhs_paren!($:2) %*/
                     }
                 ;

 mlhs_inner     : mlhs_basic
+                    {
+                        $$ = multi_write2multi_target(p, $1);
+                    }

よさそうです。

$ ./miniruby --parser=parse.y --dump=p -e  "(((a, b))) = foo"
# +-- @ MultiWriteNode (location: (1,0)-(1,16))*
#     +-- lefts: (length: 1)
#     |   +-- @ MultiTargetNode (location: (1,2)-(1,8))
#     |       +-- lefts: (length: 1)
#     |       |   +-- @ MultiTargetNode (location: (1,3)-(1,7))
#     |       |       +-- lefts: (length: 2)
#     |       |       |   +-- @ LocalVariableTargetNode (location: (1,3)-(1,4))
#     |       |       |   |   +-- name: :a
#     |       |       |   |   +-- depth: 0
#     |       |       |   +-- @ LocalVariableTargetNode (location: (1,6)-(1,7))
#     |       |       |       +-- name: :b
#     |       |       |       +-- depth: 0

$ ./miniruby --parser=parse.y --dump=p -e  "a, (b, c) = foo"
# +-- @ MultiWriteNode (location: (1,0)-(1,15))*
#     +-- lefts: (length: 2)
#     |   +-- @ LocalVariableTargetNode (location: (1,0)-(1,1))
#     |   |   +-- name: :a
#     |   |   +-- depth: 0
#     |   +-- @ MultiTargetNode (location: (1,4)-(1,8))
#     |       +-- lefts: (length: 2)
#     |       |   +-- @ LocalVariableTargetNode (location: (1,4)-(1,5))
#     |       |   |   +-- name: :b
#     |       |   |   +-- depth: 0
#     |       |   +-- @ LocalVariableTargetNode (location: (1,7)-(1,8))
#     |       |       +-- name: :c
#     |       |       +-- depth: 0

バイトコードコンパイラの話を始めると長くなりそうなので、今日はここまで。

まとめ

今日の成果です。

  • multiple assignmentのparser部分を対応した

  1. 左辺にはいわゆる変数以外のものも書くことができるので、"何か"としています。



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

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