15日目: 仮引数にもあるmultiple assignment
さて、multiple assignmentでおそらく最も複雑な部分を実装したので今回は仮引数部分のmultiple assignmentに取り組みます1。 以下のコードのように、Rubyではメソッド定義などの仮引数のところにもmultiple assignmentを書くことができます。
def m((a, b), c) p a p b p c end m([1, 2], 3) #=> 1 #=> 2 #=> 3
そこまで詳しい部分でもないので、いつものように文法、ノード、バイトコードを眺めつつ進めていきましょう。
文法とノードを眺める
まずはざっと文法を眺めておきます。 パッとみてわかる特徴は以下の2点でしょうか。
def m((a, (b, c)), d); endのように()をネストすることができるaやbのところにはローカル変数の形のものしか書けない
二つめの点は、代入としてのmultiple assignmentが左辺に様々な変数や一部の形式のメソッド呼び出しをとれたことを考えるとだいぶシンプルです2。
// 仮引数の個々の部分 f_arg_item : f_arg_asgn | tLPAREN f_margs rparen ; // "()"のなかに複数の要素を書くことができる // *argも書くことができる f_margs : mlhs_items(f_marg) | mlhs_items(f_marg) ',' f_rest_marg ... ; // "()"がネストできる! f_marg : f_norm_arg | tLPAREN f_margs rparen ; // 要素としてはローカル変数しか書けない f_norm_arg : f_bad_arg | tIDENTIFIER ;
細部を変えながら書き換え前のノードを眺めておきましょう。
$ ruby --parser=parse.y --dump=p -e 'def m((a,b)); end' # @ NODE_DEFN (id: 1, line: 1, location: (1,0)-(1,17))* # +- nd_mid: :m # +- nd_defn: # @ NODE_SCOPE (id: 11, line: 1, location: (1,0)-(1,17)) # +- nd_tbl: (internal variable: 0x34359214071),:a,:b # +- nd_args: # | @ NODE_ARGS (id: 9, line: 1, location: (1,6)-(1,11)) # | +- nd_ainfo.forwarding: 0 (no forwarding) # | +- nd_ainfo.pre_args_num: 1 # | +- nd_ainfo.pre_init: # | | @ NODE_MASGN (id: 6, line: 1, location: (1,7)-(1,10)) # | | +- nd_value: # | | | @ NODE_LVAR (id: 7, line: 1, location: (1,7)-(1,7)) # | | | +- nd_vid: (internal variable: 0x34359214071) # | | +- nd_head: # | | | @ NODE_LIST (id: 3, line: 1, location: (1,7)-(1,10)) # | | | +- as.nd_alen: 2 # | | | +- nd_head: # | | | | @ NODE_LASGN (id: 2, line: 1, location: (1,7)-(1,8)) # | | | | +- nd_vid: :a # | | | | +- nd_value: # | | | | (null node) # | | | +- nd_head: # | | | | @ NODE_LASGN (id: 4, line: 1, location: (1,9)-(1,10)) # | | | | +- nd_vid: :b # | | | | +- nd_value: # | | | | (null node) # | | | +- nd_next: # | | | (null node) # | | +- nd_args: # | | (null node) # | +- nd_ainfo.post_args_num: 0 # | +- nd_ainfo.post_init: # | | (null node) # | +- nd_ainfo.first_post_arg: (null) # | +- nd_ainfo.rest_arg: (null) # | +- nd_ainfo.block_arg: (null) # | +- nd_ainfo.opt_args: # | | (null node) # | +- nd_ainfo.kw_args: # | | (null node) # | +- nd_ainfo.kw_rest_arg: # | (null node) # +- nd_body: # (null node)
nd_ainfo.pre_initという部分にNODE_MASGNというノードがセットされます。
NODE_MASGNというノードは代入のときと同じノードです。
NODE_MASGNのnd_valueにはinternal variableを参照するノードがセットされています。
例えて言うならa, b = internal_idといったところでしょうか。
また全く同じ数値のidがnd_tblにも設定されています。
これは以下のような書き換えを考えるとよいと思います。
(a,b)という仮引数は、multiple assignmentを表す適当な仮引数iを用意して、iをaとbに分解することと同じようです。
def m((a, b)) end def m(i) a, b = i end
nd_ainfo.pre_initのほかにnd_ainfo.post_initというフィールドもあります。
仮引数においてpostというのは*r(rest)のあとのことなので、以下のようなスクリプトを考えます。
$ ruby --parser=parse.y --dump=p -e 'def m(*r, (a,b)); end' # @ NODE_DEFN (id: 1, line: 1, location: (1,0)-(1,21))* # +- nd_mid: :m # +- nd_defn: # @ NODE_SCOPE (id: 11, line: 1, location: (1,0)-(1,21)) # +- nd_tbl: :r,(internal variable: 0x34359214063),:a,:b # +- nd_args: # | @ NODE_ARGS (id: 9, line: 1, location: (1,6)-(1,15)) # | +- nd_ainfo.forwarding: 0 (no forwarding) # | +- nd_ainfo.pre_args_num: 0 # | +- nd_ainfo.pre_init: # | | (null node) # | +- nd_ainfo.post_args_num: 1 # | +- nd_ainfo.post_init: # | | @ NODE_MASGN (id: 6, line: 1, location: (1,11)-(1,14)) # | | +- nd_value: # | | | @ NODE_LVAR (id: 7, line: 1, location: (1,11)-(1,11)) # | | | +- nd_vid: (internal variable: 0x34359214063) # | | +- nd_head: # | | | @ NODE_LIST (id: 3, line: 1, location: (1,11)-(1,14)) # | | | +- as.nd_alen: 2 # | | | +- nd_head: # | | | | @ NODE_LASGN (id: 2, line: 1, location: (1,11)-(1,12)) # | | | | +- nd_vid: :a # | | | | +- nd_value: # | | | | (null node) # | | | +- nd_head: # | | | | @ NODE_LASGN (id: 4, line: 1, location: (1,13)-(1,14)) # | | | | +- nd_vid: :b # | | | | +- nd_value: # | | | | (null node) # | | | +- nd_next: # | | | (null node) # | | +- nd_args: # | | (null node) # | +- nd_ainfo.first_post_arg: (internal variable: 0x34359214063) # | +- nd_ainfo.rest_arg: :r # | +- nd_ainfo.block_arg: (null) # | +- nd_ainfo.opt_args: # | | (null node) # | +- nd_ainfo.kw_args: # | | (null node) # | +- nd_ainfo.kw_rest_arg: # | (null node) # +- nd_body: # (null node)
nd_ainfo.pre_initではなくnd_ainfo.post_initにNODE_MASGNがセットされました。
multiple assignmentがネストするケースについてもみておきましょう。 特徴は以下の2点でしょう。
NODE_MASGNの下にNODE_MASGNがネストする構造になっているnd_tblのinternal variableは1つのまま
このネスト構造はa, (b, c) = iと同じ構造になっています。
$ ruby --parser=parse.y --dump=p -e 'def m((a,(b, c))); end' # @ NODE_DEFN (id: 1, line: 1, location: (1,0)-(1,22))* # +- nd_mid: :m # +- nd_defn: # @ NODE_SCOPE (id: 15, line: 1, location: (1,0)-(1,22)) # +- nd_tbl: (internal variable: 0x34359214063),:a,:b,:c # +- nd_args: # | @ NODE_ARGS (id: 13, line: 1, location: (1,6)-(1,16)) # | +- nd_ainfo.forwarding: 0 (no forwarding) # | +- nd_ainfo.pre_args_num: 1 # | +- nd_ainfo.pre_init: # | | @ NODE_MASGN (id: 10, line: 1, location: (1,7)-(1,15)) # | | +- nd_value: # | | | @ NODE_LVAR (id: 11, line: 1, location: (1,7)-(1,7)) # | | | +- nd_vid: (internal variable: 0x34359214063) # | | +- nd_head: # | | | @ NODE_LIST (id: 3, line: 1, location: (1,7)-(1,14)) # | | | +- as.nd_alen: 2 # | | | +- nd_head: # | | | | @ NODE_LASGN (id: 2, line: 1, location: (1,7)-(1,8)) # | | | | +- nd_vid: :a # | | | | +- nd_value: # | | | | (null node) # | | | +- nd_head: # | | | | @ NODE_MASGN (id: 8, line: 1, location: (1,10)-(1,14)) # | | | | +- nd_value: # | | | | | (null node) # | | | | +- nd_head: # | | | | | @ NODE_LIST (id: 5, line: 1, location: (1,10)-(1,14)) # | | | | | +- as.nd_alen: 2 # | | | | | +- nd_head: # | | | | | | @ NODE_LASGN (id: 4, line: 1, location: (1,10)-(1,11)) # | | | | | | +- nd_vid: :b # | | | | | | +- nd_value: # | | | | | | (null node) # | | | | | +- nd_head: # | | | | | | @ NODE_LASGN (id: 6, line: 1, location: (1,13)-(1,14)) # | | | | | | +- nd_vid: :c # | | | | | | +- nd_value: # | | | | | | (null node) # | | | | | +- nd_next: # | | | | | (null node) # | | | | +- nd_args: # | | | | (null node) # | | | +- nd_next: # | | | (null node) # | | +- nd_args: # | | (null node) # | +- nd_ainfo.post_args_num: 0 # | +- nd_ainfo.post_init: # | | (null node) # | +- nd_ainfo.first_post_arg: (null) # | +- nd_ainfo.rest_arg: (null) # | +- nd_ainfo.block_arg: (null) # | +- nd_ainfo.opt_args: # | | (null node) # | +- nd_ainfo.kw_args: # | | (null node) # | +- nd_ainfo.kw_rest_arg: # | (null node) # +- nd_body: # (null node)
最後に複数のmultiple assignmentが並んでいるケースをみておきます。 特徴は以下の2点です。
nd_tblには2つのinternal variableがセットされているnd_ainfo.pre_initにはNODE_MASGNが2つ同じ階層に並んでいる
$ ruby --parser=parse.y --dump=p -e 'def m((a, b), (c, d)); end' # @ NODE_DEFN (id: 1, line: 1, location: (1,0)-(1,26))* # +- nd_mid: :m # +- nd_defn: # @ NODE_SCOPE (id: 20, line: 1, location: (1,0)-(1,26)) # +- nd_tbl: (internal variable: 0x34359214071),(internal variable: 0x34359214047),:a,:b,:c,:d # +- nd_args: # | @ NODE_ARGS (id: 18, line: 1, location: (1,6)-(1,20)) # | +- nd_ainfo.forwarding: 0 (no forwarding) # | +- nd_ainfo.pre_args_num: 2 # | +- nd_ainfo.pre_init: # | | @ NODE_BLOCK (id: 16, line: 1, location: (1,7)-(1,19)) # | | +- nd_head (1): # | | | @ NODE_MASGN (id: 6, line: 1, location: (1,7)-(1,11)) # | | | +- nd_value: # | | | | @ NODE_LVAR (id: 7, line: 1, location: (1,7)-(1,7)) # | | | | +- nd_vid: (internal variable: 0x34359214071) # | | | +- nd_head: # | | | | @ NODE_LIST (id: 3, line: 1, location: (1,7)-(1,11)) # | | | | +- as.nd_alen: 2 # | | | | +- nd_head: # | | | | | @ NODE_LASGN (id: 2, line: 1, location: (1,7)-(1,8)) # | | | | | +- nd_vid: :a # | | | | | +- nd_value: # | | | | | (null node) # | | | | +- nd_head: # | | | | | @ NODE_LASGN (id: 4, line: 1, location: (1,10)-(1,11)) # | | | | | +- nd_vid: :b # | | | | | +- nd_value: # | | | | | (null node) # | | | | +- nd_next: # | | | | (null node) # | | | +- nd_args: # | | | (null node) # | | +- nd_head (2): # | | @ NODE_MASGN (id: 13, line: 1, location: (1,15)-(1,19)) # | | +- nd_value: # | | | @ NODE_LVAR (id: 14, line: 1, location: (1,15)-(1,15)) # | | | +- nd_vid: (internal variable: 0x34359214047) # | | +- nd_head: # | | | @ NODE_LIST (id: 10, line: 1, location: (1,15)-(1,19)) # | | | +- as.nd_alen: 2 # | | | +- nd_head: # | | | | @ NODE_LASGN (id: 9, line: 1, location: (1,15)-(1,16)) # | | | | +- nd_vid: :c # | | | | +- nd_value: # | | | | (null node) # | | | +- nd_head: # | | | | @ NODE_LASGN (id: 11, line: 1, location: (1,18)-(1,19)) # | | | | +- nd_vid: :d # | | | | +- nd_value: # | | | | (null node) # | | | +- nd_next: # | | | (null node) # | | +- nd_args: # | | (null node) # | +- nd_ainfo.post_args_num: 0 # | +- nd_ainfo.post_init: # | | (null node) # | +- nd_ainfo.first_post_arg: (null) # | +- nd_ainfo.rest_arg: (null) # | +- nd_ainfo.block_arg: (null) # | +- nd_ainfo.opt_args: # | | (null node) # | +- nd_ainfo.kw_args: # | | (null node) # | +- nd_ainfo.kw_rest_arg: # | (null node) # +- nd_body: # (null node)
parse.yを書き換える
ノードの書き換え後は他の必須引数と同様にParametersNodeのrequiredsもしくはpostsにMultiTargetNodeが並ぶ構造をとります。
$ ruby --parser=prism --dump=p -e 'def m((a, *b, c), d, *r, e, (f, *g, h)); end'
+-- @ DefNode (location: (1,0)-(1,44))
+-- name: :m
+-- receiver: nil
+-- parameters:
| @ ParametersNode (location: (1,6)-(1,38))
| +-- requireds: (length: 2)
| | +-- @ MultiTargetNode (location: (1,6)-(1,16))
| | | +-- lefts: (length: 1)
| | | | +-- @ RequiredParameterNode (location: (1,7)-(1,8))
| | | | +-- name: :a
| | | +-- rest:
| | | | @ SplatNode (location: (1,10)-(1,12))
| | | | +-- expression:
| | | | @ RequiredParameterNode (location: (1,11)-(1,12))
| | | | +-- name: :b
| | | +-- rights: (length: 1)
| | | | +-- @ RequiredParameterNode (location: (1,14)-(1,15))
| | | | +-- name: :c
| | +-- @ RequiredParameterNode (location: (1,18)-(1,19))
| | +-- name: :d
| +-- optionals: (length: 0)
| +-- rest:
| | @ RestParameterNode (location: (1,21)-(1,23))
| | +-- name: :r
| +-- posts: (length: 2)
| | +-- @ RequiredParameterNode (location: (1,25)-(1,26))
| | | +-- name: :e
| | +-- @ MultiTargetNode (location: (1,28)-(1,38))
| | +-- lefts: (length: 1)
| | | +-- @ RequiredParameterNode (location: (1,29)-(1,30))
| | | +-- name: :f
| | +-- rest:
| | | @ SplatNode (location: (1,32)-(1,34))
| | | +-- expression:
| | | @ RequiredParameterNode (location: (1,33)-(1,34))
| | | +-- name: :g
| | +-- rights: (length: 1)
| | | +-- @ RequiredParameterNode (location: (1,36)-(1,37))
| | | +-- name: :h
| +-- keywords: (length: 0)
| +-- keyword_rest: nil
| +-- block: nil
+-- body: nil
+-- locals: [:a, :b, :c, :d, :r, :e, :f, :g, :h]
基本的には代入のときと同じ構造をしているのでmlhs_basicやmlhsといった生成規則を参考にアクションを書き換えていきます。
例えば()の中の要素を表すf_margsでは生成するノードを変更します。
f_margs : mlhs_items(f_marg)
{
- $$ = NEW_MASGN($1, 0, &@$);
+ $$ = NEW_RB_MULTI_TARGET($1, 0, 0, &@$);
/*% ripper: $:1 %*/
}
| mlhs_items(f_marg) ',' f_rest_marg
{
- $$ = NEW_MASGN($1, $3, &@$);
+ $$ = NEW_RB_MULTI_TARGET($1, $3, 0, &@$);
/*% ripper: mlhs_add_star!($:1, $:3) %*/
}
ひとつ困った箇所があります。
それは個々の要素を表すf_margのアクションです。
f_marg : f_norm_arg
{
- $$ = assignable(p, $1, 0, &@$);
- mark_lvar_used(p, $$);
+ // $$ = assignable(p, $1, 0, &@$);
+ // TODO: local_var ?
+ // local_var(p, $1);
+ // mark_lvar_used(p, $$);
+ arg_var(p, $1);
+ $$ = NEW_RB_REQUIRED_PARAMETER($1, &NULL_LOC);
}
これまではidを引数としてassignable関数を呼び出してNODE_LASGNノードを取得し、そのノードをmark_lvar_usedに渡すことで変数が使用されていないという警告を抑制していました。
assignableもmark_lvar_usedもRequiredParameterNodeには対応していないので、どうしましょう。
というかdef m((a, *b, c), d, *r, e, (f, *g, h)); endのaやbなどはローカル変数ではなく仮引数です。
assignable関数ではなくarg_var関数であれば引数がIDですし、受け取ったidを仮引数として扱ってくれます。
ここでは一旦arg_varを使うようにしてみます。
生成されるノードを確認します。 よさそうですね。
$ ./miniruby --parser=parse.y --dump=p -e "def m((a, *b, c)); end"
+-- @ DefNode (location: (1,0)-(1,22))*
+-- name: :m
+-- receiver: nil
+-- parameters:
| @ ParametersNode (location: (0,-1)-(0,-1))
| +-- requireds: (length: 1)
| | +-- @ MultiTargetNode (location: (1,7)-(1,15))
| | +-- lefts: (length: 1)
| | | +-- @ RequiredParameterNode (location: (0,-1)-(0,-1))
| | | +-- name: :a
| | +-- rest:
| | | @ SplatNode (location: (0,-1)-(0,-1))
| | | +-- expression:
| | | @ RequiredParameterNode (location: (0,-1)-(0,-1))
| | | +-- name: :b
| | +-- rights: (length: 1)
| | | +-- @ RequiredParameterNode (location: (0,-1)-(0,-1))
| | | +-- name: :c
| +-- optionals: (length: 0)
| +-- rest: nil
| +-- posts: (length: 0)
| +-- keywords: (length: 0)
| +-- keyword_rest: nil
| +-- block: nil
+-- body:
| @ StatementsNode (location: (1,17)-(1,17))
| +-- body: (length: 0)
+-- locals: [:a, :b, :c, :(null)]
compile.cを変更する
一度現時点で生成されるISeqを確認しておきましょう。
# Before == disasm: #<ISeq:m@-e:1 (1,0)-(1,22)> local table (size: 4, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 4] ?@0<Arg> [ 3] a@1 [ 2] b@2 [ 1] c@3 0000 getlocal_WC_0 ?@0 ( 1) 0002 expandarray 1, 1 0005 setlocal_WC_0 a@1 0007 expandarray 1, 3 0010 setlocal_WC_0 b@2 0012 setlocal_WC_0 c@3 0014 putnil [Ca] 0015 leave [Re] # After == disasm: #<ISeq:m@-e:1 (1,0)-(1,22)> local table (size: 4, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 4] a@0<Arg> [ 3] b@1 [ 2] c@2 [ 1] ?@3 0000 putnil ( 1)[Ca] 0001 leave [Re]
まだバイトコードを生成するロジックを直していないのでバイトコードに差があるのはよいとして、問題はlocal tableの変数の順番が変わっていることです。
local tableは仮引数、ローカル変数の順番に変数が並びます。
parse.yでassignableとmark_lvar_usedを使っていた箇所をarg_varにしたため、a, b, cが仮引数になって前に移動したということです。
これではいけませんね。
あまり既存のロジックに手を入れないようにしたいので、assignable_target関数を呼び出したうえでノードの変換をアクションで行うようにします。
f_marg : f_norm_arg { $$ = assignable_target(p, $1, &@$); mark_lvar_used(p, $$); if (!RB_NODE_TYPE_P($$, RB_LOCAL_VARIABLE_TARGET_NODE)) rb_bug("unexpected node: %s", ruby_node_name(nd_type($$)));; $$ = local_variable_target2required_parameter(p, $$); }
再度minirubyをbuildして確認します。 今度はよさそうですね。
$ ./miniruby --parser=parse.y --dump=p,i -e "def m((a, *b, c)); end"
+-- @ DefNode (location: (1,0)-(1,22))*
+-- name: :m
+-- receiver: nil
+-- parameters:
| @ ParametersNode (location: (0,-1)-(0,-1))
| +-- requireds: (length: 1)
| | +-- @ MultiTargetNode (location: (1,7)-(1,15))
| | +-- lefts: (length: 1)
| | | +-- @ RequiredParameterNode (location: (1,7)-(1,8))
| | | +-- name: :a
| | +-- rest:
| | | @ SplatNode (location: (0,-1)-(0,-1))
| | | +-- expression:
| | | @ RequiredParameterNode (location: (1,10)-(1,12))
| | | +-- name: :b
| | +-- rights: (length: 1)
| | | +-- @ RequiredParameterNode (location: (1,14)-(1,15))
| | | +-- name: :c
| +-- optionals: (length: 0)
| +-- rest: nil
| +-- posts: (length: 0)
| +-- keywords: (length: 0)
| +-- keyword_rest: nil
| +-- block: nil
+-- body:
| @ StatementsNode (location: (1,17)-(1,17))
| +-- body: (length: 0)
+-- locals: [:(null), :a, :b, :c]
== disasm: #<ISeq:m@-e:1 (1,0)-(1,22)>
local table (size: 4, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 4] ?@0<Arg> [ 3] a@1 [ 2] b@2 [ 1] c@3
0000 putnil ( 1)[Ca]
0001 leave [Re]
これはRequiredなのだろうか?
ここまでやって1つ気がついたのですが、MultiTargetNodeの下にいるRequiredParameterNodeは別に必須引数ではないんですよね。
必須でもないし、内部的には引数とも言い難い。
例えば以下のメソッドは1つの必須引数を持ちます。 ですがその引数を展開したときに要素が足らなくてもエラーにはなりません。
def m((a, *b, c)) p a p b p c end m() #=> wrong number of arguments (given 0, expected 1) (ArgumentError) m(1) #=> 1 #=> [] #=> nil m([]) #=> nil #=> [] #=> nil
ノードでいえばMultiTargetNodeは確かに必須ですが、その下にあるノードは特段必須というわけではありません。
またここまでみてきたようにa, b, cはISeqのレベルでみれば仮引数ではなくローカル変数として扱われます。
RequiredParameterNodeになっているところはLocalVariableTargetNodeのほうが適切な気がします。
compile.cを変更する (2回目)
といっても仕方ないので、compile.cに手を加えていきましょう。
どこに手を入れるかというと3日目にコメントアウトした部分です。
このパターンはさきほどのオプショナル引数にNODE_LASGNがセットされているパターンによく似ています。 ということでmultiple assignmentのノードを対応するまではコメントアウトしておきましょう。
ノードの書き換え前はpre_initとpost_initにノードが入っていましたが、書き換えによりrequiredsやpostsに他の仮引数のノードと一緒に保存されるようになりました。
そのためrequiredsやpostsに入っているノードを辿りながら、MultiTargetNodeのときはコンパイルをするというロジックに書き換えます。
diff --git a/compile.c b/compile.c index 72e860600d..a0e8a8e684 100644 --- a/compile.c +++ b/compile.c @@ -2342,7 +2342,19 @@ iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const optargs, const NODE *cons iseq_calc_param_size(iseq); body->param.size = arg_size; - // TODO: def m4((a, b), c, (d, e), f = false, (g, h)); end + // Generate byte codes for multiple assignment like below. + // + // `def m4((a, b), c, (d, e), f = false, (g, h)); end` + for (size_t i = 0; i < RB_NODE_LIST_LEN(&args->requireds); i++) { + const NODE *n = args->requireds.nodes[i]; + if (!nd_type_p(n, RB_MULTI_TARGET_NODE)) continue; + NO_CHECK(COMPILE_POPPED(optargs, "init arguments (m)", n)); + } + for (size_t i = 0; i < RB_NODE_LIST_LEN(&args->posts); i++) { + const NODE *n = args->posts.nodes[i]; + if (!nd_type_p(n, RB_MULTI_TARGET_NODE)) continue; + NO_CHECK(COMPILE_POPPED(optargs, "init arguments (p)", n)); + } // if (args->pre_init) { /* m_init */ // NO_CHECK(COMPILE_POPPED(optargs, "init arguments (m)", args->pre_init)); // }
バイトコードを生成してみましょう。
$ ./miniruby --parser=parse.y --dump=i -e "def m((a, *b, c)); end" -e: -e:1: iseq_compile_each: unknown node (RB_MULTI_TARGET_NODE) (SyntaxError)
だめですね...
複数代入のときはiseq_compile_each0関数でMultiWriteNodeを扱うものの、その中身(とくにMultiTargetNode)はcompile_massign_lhs関数とcompile_massign0関数で処理するようになっているのでした。
そのためiseq_compile_each0関数はMultiTargetNodeの扱い方を知らないのです。
面倒だな...
MultiTargetNodeをコンパイルする
ノードを書き換える前の世界では複数代入のNODE_MASGNにしろ、仮引数のNODE_MASGNにしろcompile_massign関数で扱っていました。
ノードの書き換え後もこれを踏襲して一番外側のMultiTargetNodeはcompile_massign関数で扱うようにしてみましょう。
MultiWriteNodeもしくはMultiTargetNodeを受け取ってleftsやrestを返す補助的な関数をいくつか用意しましたが、基本的な変更は以下の1点だけです。
- case RB_MULTI_WRITE_NODE: { + case RB_MULTI_WRITE_NODE: + case RB_MULTI_TARGET_NODE: { bool prev_in_masgn = ISEQ_COMPILE_DATA(iseq)->in_masgn; ISEQ_COMPILE_DATA(iseq)->in_masgn = true; - compile_massign(iseq, ret, RB_NODE_MULTI_WRITE(node), popped); + compile_massign(iseq, ret, node, popped); ISEQ_COMPILE_DATA(iseq)->in_masgn = prev_in_masgn; break; }
今度こそどうでしょうか。
$ ./miniruby --parser=parse.y --dump=i -e "def m((a, *b, c)); end" -e: -e:1: iseq_compile_each: unknown node (RB_REQUIRED_PARAMETER_NODE) (SyntaxError)
ああーーー。
LocalVariableTargetNodeにしていればこんなことには...
RequiredParameterNodeをコンパイルする
RequiredParameterNodeのコンパイルをどこで行うか、二つの選択肢があります。
現時点でRequiredParameterNodeが出てくるのはMultiTargetNodeの以下だけです。
なのでcompile_massign_lhs関数で処理するのはあり得る選択肢でしょう。
一方で、いままでそれはNODE_LASGNとして表現されていて、iseq_compile_each0関数で処理されていたことを考えるとiseq_compile_each0関数で扱うというのもあり得る選択肢でしょう。
コンパイル処理のほとんどがLocalVariableTargetNodeなどと同じになりそうなので、iseq_compile_each0関数で扱うようにしてみます。
case RB_LOCAL_VARIABLE_WRITE_NODE: // LASGN and DASGN - case RB_LOCAL_VARIABLE_TARGET_NODE: { + case RB_LOCAL_VARIABLE_TARGET_NODE: + case RB_REQUIRED_PARAMETER_NODE: { int idx, lv, ls; ID id; const NODE *valn = NULL; - if (nd_type_p(node, RB_LOCAL_VARIABLE_WRITE_NODE)) { + switch (nd_type(node)) { + case RB_LOCAL_VARIABLE_WRITE_NODE: id = RB_NODE_LOCAL_VARIABLE_WRITE(node)->name; valn = RB_NODE_LOCAL_VARIABLE_WRITE(node)->value; - } - else { + break; + case RB_LOCAL_VARIABLE_TARGET_NODE: id = RB_NODE_LOCAL_VARIABLE_TARGET(node)->name; + break; + case RB_REQUIRED_PARAMETER_NODE: + id = RB_NODE_REQUIRED_PARAMETER(node)->name; + break; + default: + break; } CHECK(COMPILE(ret, "dvalue", valn)); debugi("dassn id", rb_id2str(id) ? id : '*');
今度こそどうだ。
# After $ ./miniruby --parser=parse.y --dump=i -e "def m((a, *b, c)); end" == disasm: #<ISeq:m@-e:1 (1,0)-(1,22)> local table (size: 4, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 4] ?@0<Arg> [ 3] a@1 [ 2] b@2 [ 1] c@3 0000 putnil ( 1) 0001 expandarray 1, 1 0004 setlocal_WC_0 a@1 0006 expandarray 1, 3 0009 setlocal_WC_0 b@2 ( 1) 0011 setlocal_WC_0 c@3 0013 putnil [Ca] 0014 leave [Re] # Before $ ruby --parser=parse.y --dump=i -e "def m((a, *b, c)); end" == disasm: #<ISeq:m@-e:1 (1,0)-(1,22)> local table (size: 4, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 4] ?@0<Arg> [ 3] a@1 [ 2] b@2 [ 1] c@3 0000 getlocal_WC_0 ?@0 ( 1) 0002 expandarray 1, 1 0005 setlocal_WC_0 a@1 0007 expandarray 1, 3 0010 setlocal_WC_0 b@2 0012 setlocal_WC_0 c@3 0014 putnil [Ca] 0015 leave [Re]
バイトコードは生成されましたが、一番最初の命令が書き換え前と変わってしまいました。
書き換え前は右辺を表すNODE_MASGNのnd_valueにinternal idへのローカル変数アクセスを表すノードが付与されていました。
@ NODE_MASGN (id: 8, line: 1, location: (1,7)-(1,15)) +- nd_value: | @ NODE_LVAR (id: 9, line: 1, location: (1,7)-(1,7)) | +- nd_vid: (internal variable: 0x34359214063) +- nd_head: | @ NODE_LIST (id: 3, line: 1, location: (1,7)-(1,8)) | +- as.nd_alen: 1 | +- nd_head: | | @ NODE_LASGN (id: 2, line: 1, location: (1,7)-(1,8)) | | +- nd_vid: :a | | +- nd_value: | | (null node) | +- nd_next: | (null node) +- nd_args:
しかし書き換え後のMultiTargetNodeには右辺を表すフィールドが存在しません。
そのためNULLがCOMPILEに渡されて、putnil命令が生成されるようになりました。
static int compile_massign0(rb_iseq_t *iseq, LINK_ANCHOR *const pre, LINK_ANCHOR *const rhs, LINK_ANCHOR *const lhs, LINK_ANCHOR *const post, const NODE *const node, struct masgn_state *state, int popped) { const NODE *rhsn = get_node_multi_value(node); ... if (!state->nested) { // rhsn = NULLがコンパイルされてputnil命令が作られる NO_CHECK(COMPILE(rhs, "normal masgn rhs", rhsn)); } if (!popped) { ADD_INSN(rhs, node, dup); } ADD_INSN2(rhs, node, expandarray, INT2FIX(llen), INT2FIX(lhs_splat)); return COMPILE_OK; }
getlocal命令を生成する
さて正しいバイトコードを生成するためには2つの点で修正が必要です。
MultiTargetNodeの右辺3に対応するputnil命令の生成を止める- 代わりに仮引数を対象とする
getlocal命令を生成する
1についてはcompile_massign0関数の該当箇所でノードの種別をチェックするようにすればよいでしょう。
@@ -6262,7 +6346,7 @@ compile_massign0(rb_iseq_t *iseq, LINK_ANCHOR *const pre, LINK_ANCHOR *const rhs } } - if (!state->nested) { + if (!state->nested && nd_type_p(node, RB_MULTI_WRITE_NODE)) { NO_CHECK(COMPILE(rhs, "normal masgn rhs", rhsn)); }
余談ですが、おそらくこのstate->nestedによるチェックは削除することができるはずです。
このチェックはもともと複数代入の一番外側とそれ以外で共通したNODE_MASGNを使っていたときに、内側のNODE_MASGNのnd_valueフィールド(値がNULLになっている)に対するコンパイルをスキップするための仕組みのはずです。
書き換え後は一番外側はMultiWriteNode、それ以外はMultiTargetNodeになっているのでノードの種別で判断できるはずです。
まあどこかのタイミングで整理すればよいでしょう。
2については一工夫必要です。
いままではNODE_MASGNのnd_valueがinternal idを直接保持していました。
そのためlocal_tableからそのidを探し、local_table内での位置をgetlocalのオペランドにすることができました。
ノードの書き換え後に使用しているMultiTargetNodeはidを保持していません。
ここで使うことができる情報を整理してみます。
- idがinternalなidかどうかは
is_internal_idという関数で判別が可能 local_tableにinternal idが入るケースは 1. multiple assignmentがあるとき。2. キーワード引数があるとき。のいずれか- multiple assignmentのときはmultiple assignmentとinternal idの順番は一致する
- キーワード引数はpost引数より必ずあとに出現する
ということはlocal_tableからinternal idだけを取り出した場合、それは各multiple assignmentに対応する順番に並んでおり、たとえmultiple assignment以外のinternal idがあったとしても、それはmultiple assignmentのidよりも後ろにあることが保証されているはずです4。

ということで左からn番目のinternl idの位置を返す関数を用意し、getlocal命令のオペランドの計算にはその関数の戻り値を使うようにします。
static int local_internal_id_idx(rb_iseq_t *iseq, int n) { ID *ids = ISEQ_BODY(iseq)->local_table; for (int i = 0; i < ISEQ_BODY(iseq)->local_table_size; i++) { if (is_internal_id(ids[i])) { if (n <= 0) { return ISEQ_BODY(iseq)->local_table_size - i; } else { n--; } } } rb_bug("unknown index: %d", n); } static int iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const optargs, const NODE *const node_args) { ... for (size_t i = 0; i < RB_NODE_LIST_LEN(&args->requireds); i++) { const NODE *n = args->requireds.nodes[i]; if (!nd_type_p(n, RB_MULTI_TARGET_NODE)) continue; ADD_GETLOCAL(optargs, n, local_internal_id_idx(iseq, internal_id_idx), 0); NO_CHECK(COMPILE_POPPED(optargs, "init arguments (m)", n)); internal_id_idx++; } ...
minirubyをbuildして生成されるバイトコードを確認します。 うまく動いていそうです。
$ ./miniruby --parser=parse.y --dump=i -e "def m((a, *b, c), *r, (d, e), k1:); end" == disasm: #<ISeq:m@-e:1 (1,0)-(1,39)> local table (size: 10, argc: 1 [opts: 0, rest: 1, post: 1, block: -1, kw: 1@1, kwrest: -1]) [10] ?@0<Arg> [ 9] r@1<Rest> [ 8] ?@2<Post> [ 7] k1@3 [ 6] ?@4 [ 5] a@5 [ 4] b@6 [ 3] c@7 [ 2] d@8 [ 1] e@9 0000 getlocal_WC_0 ?@0 ( 1) # (a, *b, c)用の引数 0002 expandarray 1, 1 0005 setlocal_WC_0 a@5 0007 expandarray 1, 3 0010 setlocal_WC_0 b@6 ( 1) 0012 setlocal_WC_0 c@7 0014 getlocal_WC_0 ?@2 # (d, e)用の引数 0016 expandarray 2, 0 0019 setlocal_WC_0 d@8 0021 setlocal_WC_0 e@9 0023 putnil [Ca] 0024 leave [Re]
他の方法としてinternal idかどうかに関わらず直接local tableにおけるindexを計算するという方法もあります。 postのindexを計算するときにrestの有無で1ずらすかどうかが変わる以外は考慮することはないはずなので、そちらの方法のほうがわかりやすいかもしれません。
おまけ: 引数にデフォルト値があるときの実行順序
ふと思ってバイトコードを見たのですが、multiple assignmentの展開って、以下のようにオプショナル引数のデフォルト値の評価が終わったあとに行われるんですね。
opt = fook1: bar(a, *b, c)(f, g)
def m((a, *b, c), opt = foo, *r, (f, g), k1: bar) end # opt = foo 0000 putself ( 1) 0001 opt_send_without_block <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0003 setlocal_WC_0 opt@1 # k1: bar 0005 checkkeyword 8, 0 0008 branchif 15 0010 putself 0011 opt_send_without_block <calldata!mid:bar, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0013 setlocal_WC_0 k1@4 # (a, *b, c) 0015 getlocal_WC_0 ?@0 0017 expandarray 1, 1 0020 setlocal_WC_0 a@6 0022 expandarray 1, 3 0025 setlocal_WC_0 b@7 0027 setlocal_WC_0 c@8 # (f, g) 0029 getlocal_WC_0 ?@3 0031 expandarray 2, 0 0034 setlocal_WC_0 f@9 0036 setlocal_WC_0 g@10 0038 putnil [Ca] 0039 leave [Re]
まとめ
今日の成果です。
- 仮引数のmultiple assignmentを対応した
ようやく全てのパターンの仮引数に対応したようです。めでたい。