34日目: ary[1] += fooとstruct.field += foo
前回は匿名引数とforwardingというメソッド呼び出しに関係する部分をやりました。
今回はary[1] += fooやstruct.field += fooという形式のメソッド呼び出しについて取り組んでいきたいと思います。
ary[1] += fooの場合
この形式のメソッド呼び出しでは最終的に以下の3つを異なるものとして扱う必要があると思います。
ary[1] += fooary[1] ||= fooary[1] &&= foo
というのもこれらは以下のような意味になるわけですが、とくに1と2 & 3では生成されるバイトコードが異なるはずだからです。 1はメソッド呼び出しと代入ですが、2と3ではメソッド呼び出しの結果をみて分岐する必要があります。
ary[1] = ary[1] + fooary[1] = ary[1] || fooary[1] = ary[1] && foo
そのあたりに注意しながら進めていきましょう。
parse.yの変更
ノードの書き換え前はいずれのケースもNODE_OP_ASGN1で表現していて、それぞれの違いはnd_midの部分で区別をしています。
# @ NODE_OP_ASGN1 (id: 4, line: 1, location: (1,0)-(1,13))* # +- nd_recv: # | @ NODE_VCALL (id: 0, line: 1, location: (1,0)-(1,3)) # | +- nd_mid: :ary # +- nd_mid: :+ # +- nd_index: # | @ NODE_LIST (id: 2, line: 1, location: (1,4)-(1,5)) # | +- as.nd_alen: 1 # | +- nd_head: # | | @ NODE_INTEGER (id: 1, line: 1, location: (1,4)-(1,5)) # | | +- val: 1 # +- nd_rvalue: # | @ NODE_VCALL (id: 3, line: 1, location: (1,10)-(1,13)) # | +- nd_mid: :foo ary[1] += foo # @ NODE_OP_ASGN1 (id: 9, line: 2, location: (2,0)-(2,13))* # +- nd_recv: # | @ NODE_VCALL (id: 5, line: 2, location: (2,0)-(2,3)) # | +- nd_mid: :ary # +- nd_mid: :- # +- nd_index: # | @ NODE_LIST (id: 7, line: 2, location: (2,4)-(2,5)) # | +- as.nd_alen: 1 # | +- nd_head: # | | @ NODE_INTEGER (id: 6, line: 2, location: (2,4)-(2,5)) # | | +- val: 1 # +- nd_rvalue: # | @ NODE_VCALL (id: 8, line: 2, location: (2,10)-(2,13)) # | +- nd_mid: :foo ary[1] -= foo # @ NODE_OP_ASGN1 (id: 16, line: 3, location: (3,0)-(3,14))* # +- nd_recv: # | @ NODE_VCALL (id: 12, line: 3, location: (3,0)-(3,3)) # | +- nd_mid: :ary # +- nd_mid: :|| # +- nd_index: # | @ NODE_LIST (id: 14, line: 3, location: (3,4)-(3,5)) # | +- as.nd_alen: 1 # | +- nd_head: # | | @ NODE_INTEGER (id: 13, line: 3, location: (3,4)-(3,5)) # | | +- val: 1 # +- nd_rvalue: # | @ NODE_VCALL (id: 15, line: 3, location: (3,11)-(3,14)) # | +- nd_mid: :foo ary[1] ||= foo # @ NODE_OP_ASGN1 (id: 22, line: 4, location: (4,0)-(4,14))* # +- nd_recv: # | @ NODE_VCALL (id: 18, line: 4, location: (4,0)-(4,3)) # | +- nd_mid: :ary # +- nd_mid: :&& # +- nd_index: # | @ NODE_LIST (id: 20, line: 4, location: (4,4)-(4,5)) # | +- as.nd_alen: 1 # | +- nd_head: # | | @ NODE_INTEGER (id: 19, line: 4, location: (4,4)-(4,5)) # | | +- val: 1 # +- nd_rvalue: # | @ NODE_VCALL (id: 21, line: 4, location: (4,11)-(4,14)) # | +- nd_mid: :foo ary[1] &&= foo
ノードの書き換え後は+=や-=はIndexOperatorWriteNode、||=はIndexOrWriteNode、&&=はIndexAndWriteNodeで表すようになります。
parse.yではnew_ary_op_assign関数でNODE_OP_ASGN1を生成しています。
%rule op_asgn(rhs) <node> | primary_value '['[lbracket] opt_call_args rbracket tOP_ASGN lex_ctxt rhs { $$ = new_ary_op_assign(p, $primary_value, $opt_call_args, $tOP_ASGN, $rhs, &@opt_call_args, &@$, &NULL_LOC, &@lbracket, &@rbracket, &@tOP_ASGN); /*% ripper: opassign!(aref_field!($:1, $:3), $:5, $:7) %*/ }
この関数を変更して、$tOP_ASGNに応じて生成するノードを変えればよいでしょう。
static rb_node_t * new_ary_op_assign(struct parser_params *p, rb_node_t *ary, rb_arguments_node_t *args, ID op, rb_node_t *rhs, const YYLTYPE *args_loc, const YYLTYPE *loc, const YYLTYPE *call_operator_loc, const YYLTYPE *opening_loc, const YYLTYPE *closing_loc, const YYLTYPE *binary_operator_loc) { rb_node_t *asgn; aryset_check(p, args); switch (op) { case idOROP: asgn = NEW_RB_INDEX_OR_WRITE(ary, args, rhs, loc, call_operator_loc, opening_loc, closing_loc, binary_operator_loc); break; case idANDOP: asgn = NEW_RB_INDEX_AND_WRITE(ary, args, rhs, loc, call_operator_loc, opening_loc, closing_loc, binary_operator_loc); break; default : asgn = NEW_RB_INDEX_OPERATOR_WRITE(ary, op, args, rhs, loc, call_operator_loc, opening_loc, closing_loc, binary_operator_loc); break; } fixpos(asgn, ary); return asgn; }
バイトコードを眺める
コンパイラに変更を加える前にバイトコードを見ておきましょう。
# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(4,13)> # 0000 putnil ( 4)[Li] # 0001 putself # 0002 opt_send_without_block <calldata!mid:ary, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0004 putobject_INT2FIX_1_ # 0005 dupn 2 # 0007 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0009 putself # 0010 opt_send_without_block <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0012 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr] # 0014 setn 3 # 0016 opt_aset <calldata!mid:[]=, argc:2, ARGS_SIMPLE>[CcCr] # 0018 pop # 0019 leave ary[1] += foo
それぞれのステップにおけるスタックの状態を考えていきます。
まず初めにnilをスタックに積み、その上に左辺に当たるaryと1を積みます。
このnilはあとで式全体の値を保持するのに使います。
# `0004 putobject_INT2FIX_1_`まで 1 ary nil
ary[1] += fooというのはary[1] = ary[1] + fooのことであり、この右辺にあるary[1]を評価します。
後にary[1] =のメソッド呼び出しをすることになるので、ここではdupnをつかってスタックをコピーしておきます。
# `0005 dupn 2`まで 1 ary 1 ary nil # `0007 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr]`まで ary[1] 1 ary nil
ary[1] = ary[1] + fooのfooとary[1] + fooの評価をします。
この時点で式全体の戻り値が確定するので、最初にスタックにおいたnilをsetnを用いて置き換えます。
# `0010 opt_send_without_block <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE>`まで foo ary[1] 1 ary nil # `0012 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]`まで ary[1] + foo 1 ary nil # `0014 setn 3`まで ary[1] + foo 1 ary ary[1] + foo
最後にary[1] =の部分を評価し、popを用いてスタックの状態を調整して完了です。
# `0016 opt_aset <calldata!mid:[]=, argc:2, ARGS_SIMPLE>[CcCr]`まで # ary.[]=(1, ary[1] + foo) と同等 ary.[]=(1, ary[1] + foo) ary[1] + foo # `0018 pop`まで # 右辺が最終的な式の評価値になる ary[1] + foo
ではary[1] ||= fooの場合はどうなるでしょうか。
# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(1,14)> # 0000 putnil ( 1)[Li] # 0001 putself # 0002 opt_send_without_block <calldata!mid:ary, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0004 putobject_INT2FIX_1_ # 0005 dupn 2 # 0007 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0009 dup # 0010 branchif 22 # 0012 pop # 0013 putself # 0014 opt_send_without_block <calldata!mid:foo, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0016 setn 3 # 0018 opt_aset <calldata!mid:[]=, argc:2, ARGS_SIMPLE>[CcCr] # 0020 pop # 0021 leave # 0022 setn 3 # 0024 adjuststack 3 # 0026 leave ary[1] ||= foo
||なのでary[1]の値がnilやfalseのときだけ代入が行われます。
そのためary[1]を評価したあと0010 branchif 22でその値をチェックして分岐しています。
もしary[1]がfalsyならばそのまま0012 popに進み、先ほど同じような命令を実行して0021 leaveで抜けます。
ary[1]がtruthyの場合は0022 setn 3以降の命令が実行されます。
0010 branchif 22の時点でのスタックは以下のようになっています。
ary[1] 1 ary nil
ary[1] = ary[1] || fooでary[1]がtruthyの場合、式全体の値はary[1]になります。
そこでsetnでary[1]をnilの場所にコピーし、adjuststackでスタックの高さを調整します。
# `0022 setn 3`まで ary[1] 1 ary ary[1] # `0024 adjuststack 3`まで ary[1]
compile.cの変更
バイトコードはそれなりに複雑ですがcompile.cの変更は局所的です。
今までは3つのパターン全てがNODE_OP_ASGN1というノードで表現されてきました。
なのでそのノードをコンパイルするcompile_op_asgn1関数の中に&&=や||=などidに基づいた分岐があります。
ノードの書き換え後は3つのパターンで異なるノードを使いわけるようになるので、compile_op_asgn1関数の外でidの解決を行うようにします。
static int -compile_op_asgn1(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) +compile_op_asgn1(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, const NODE *const nd_recv, ID id, const rb_arguments_node_t *cons t nd_index, const NODE *const nd_rvalue, int popped) { const int line = nd_line(node); VALUE argc; unsigned int flag = 0; int asgnflag = 0; - ID id = RNODE_OP_ASGN1(node)->nd_mid; /* * a[x] (op)= y + case RB_INDEX_OPERATOR_WRITE_NODE: { + rb_index_operator_write_node_t *cast = (rb_index_operator_write_node_t *)node; + CHECK(compile_op_asgn1(iseq, ret, node, cast->receiver, cast->binary_operator, cast->arguments, cast->value, popped)); + break; + } + case RB_INDEX_OR_WRITE_NODE: { + rb_index_or_write_node_t *cast = (rb_index_or_write_node_t *)node; + CHECK(compile_op_asgn1(iseq, ret, node, cast->receiver, idOROP, cast->arguments, cast->value, popped)); + break; + } + case RB_INDEX_AND_WRITE_NODE: { + rb_index_and_write_node_t *cast = (rb_index_and_write_node_t *)node; + CHECK(compile_op_asgn1(iseq, ret, node, cast->receiver, idANDOP, cast->arguments, cast->value, popped)); + break; + }
minirubyをビルドして実行してみます。
a = [0, 1] a[0] += 2 p a #=> [2, 1] b = [true, false] b[0] ||= 0 b[1] ||= 1 p b #=> [true, 1] c = [true, false] c[0] &&= 0 c[1] &&= 1 p c #=> [0, false]
良さそうです。
struct.field += fooの場合
続いてstruct.field += fooのケースをやっていきましょう。
具体的には以下の3つのパターンがあります。
s.f += foo s.f &&= foo s.f ||= foo
このケースもary[1] += fooと同様に書き換え前は3つのパターン全てをNODE_OP_ASGN2という1つのノードで表現していました。
また書き換え後はary[1] += fooと同様に、CallOperatorWriteNode, CallAndWriteNode, CallOrWriteNodeの3種類のノードを使い分けることになります。
parseの変更
NODE_OP_ASGN2はnew_attr_op_assign関数で生成しているので、この関数を修正してidの種類に応じて生成するノードを切り替えます。
static rb_node_t * new_attr_op_assign(struct parser_params *p, rb_node_t *lhs, ID atype, ID attr, ID op, rb_node_t *rhs, const YYLTYPE *loc, const YYLTYPE *call_operator_loc, const YYLTYPE *message_loc, const YYLTYPE *binary_operator_loc) { rb_node_t *asgn; switch (op) { case idOROP: asgn = NEW_RB_CALL_OR_WRITE(lhs, attr, rhs, loc, call_operator_loc, message_loc, binary_operator_loc); break; case idANDOP: asgn = NEW_RB_CALL_AND_WRITE(lhs, attr, rhs, loc, call_operator_loc, message_loc, binary_operator_loc); break; default: { int flags = CALL_Q_P(atype) ? RB_CALL_NODE_FLAGS_SAFE_NAVIGATION : 0; asgn = NEW_RB_CALL_OPERATOR_WRITE(lhs, flags, attr, op, rhs, loc, call_operator_loc, message_loc, binary_operator_loc); break; } } fixpos(asgn, lhs); return asgn; }
ここで注意点が2つあります。
1つはwrite_nameというフィールドについてです。
例えばs.f += fooというコードがあるとき、そのバイトコードではs.fの呼び出しとs.f=の呼び出しを行うことになります。
ノードの書き換え前はnd_vid: :fとしてfのシンボルだけをノードに持たせて、:f=はコンパイル時に計算していました。
ノードの書き換え後はread_name: :fとwrite_name: :f=として両方のシンボルをノードに持たせることになります。
もうひとつはs.f += fooとs&.f += fooをどのようにして区別するかです。
ノードの書き換え前はbool nd_aidというフィールドがノードにあり、それをみて.なのか&.なのかを判断していました。
ノードの書き換え後はCallOperatorWriteNodeのフラグにsafe_navigationが立っているかどうかで判断するようになります。
コンパイラを修正する
コンパイラの修正はarray assignment with operatorのときと同様にcompile_op_asgn2関数の呼び出し元で必要な情報を渡すように書き換えます。
@@ -11897,6 +11901,21 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no CHECK(compile_op_asgn1(iseq, ret, node, cast->receiver, idANDOP, cast->arguments, cast->value, popped)); break; } + case RB_CALL_OPERATOR_WRITE_NODE: { + rb_call_operator_write_node_t *cast = (rb_call_operator_write_node_t *)node; + CHECK(compile_op_asgn2(iseq, ret, node, cast->receiver, cast->read_name, cast->write_name, cast->binary_operator, cast->value, popped)); + break; + } + case RB_CALL_OR_WRITE_NODE: { + rb_call_or_write_node_t *cast = (rb_call_or_write_node_t *)node; + CHECK(compile_op_asgn2(iseq, ret, node, cast->receiver, cast->read_name, cast->write_name, idOROP, cast->value, popped)); + break; + } + case RB_CALL_AND_WRITE_NODE: { + rb_call_and_write_node_t *cast = (rb_call_and_write_node_t *)node; + CHECK(compile_op_asgn2(iseq, ret, node, cast->receiver, cast->read_name, cast->write_name, idANDOP, cast->value, popped)); + break; + }
いくつかコードを実行して確認してみましょう。
class S attr_accessor :f end s = nil s&.f += 1 p s #=> nil s = S.new p s.f #=> nil s.f = 0 s.f += 1 p s.f #=> 1 s&.f += 2 p s.f #=> 3
&&=や||=も確認します。
class S attr_accessor :f end s = S.new p s.f #=> nil s.f &&= 1 p s.f #=> nil s.f ||= 1 p s.f #=> 1
良さそうです。
まとめ
今日の成果です。
- array assignment with operator (
ary[1] += foo)に対応した - attr assignment with operator (
s.f += foo)に対応した