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, ba, b, *r*r, c, da, b, *r, c, d*r*
メソッド定義の仮引数の前半部分に似ていますね。
aやbの箇所にはどのような要素が書けるかをみてみましょう。
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_MASGNはnd_headに左辺の先頭部分を、nd_valueに右辺を保持していることがわかります。
*rがあるときはnd_argsにノードが追加されます。
また*rの後ろにさらにノードが続く場合には、NODE_POSTARGでラップされます。
NODE_POSTARGはnd_1stに*rに相当するノードを、nd_2ndにc, 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_assignやset_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_targetやaryset_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からMultiTargetNode、MultiTargetNodeから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部分を対応した
- 左辺にはいわゆる変数以外のものも書くことができるので、"何か"としています。↩