11日目: attr assignment
早いものですでに11日目です。 ここらで一度進捗を確認してみましょう。
ざっと以下のような要素を実装する必要があります。 どうでしょう... 30%くらい実装できたんですかね。 思ったより大変な気がしてきました。
- リテラル
- 変数の参照と代入
- 定義
- メソッド定義
- multiple assignment (
def m((a, b)))が未対応 - forwarding (
...)が未対応
- multiple assignment (
- クラス定義 / モジュール定義 / シングルトンクラス定義
- メソッド定義
- メソッド呼び出し
いわゆる通常のメソッド呼び出し- attr assignment (
struct.field = foo) - array assignment with operator (
ary[1] += foo) - attr assignment with operator (
struct.field += foo) - assignment with && / || operator (
foo &&= bar) - constant declaration with operator (
A::B ||= 1)
- 制御構文
ifunless- flipflop
- case when
- while / until
- for
- retry / rescue / ensure
- && / || / and / or
- yield
- return
- super
- break / next / redo
- BEGIN
- END
- パターンマッチング
- その他
- alias
- undef
- defined?
今日は引き続きメソッド呼び出し関連ということでattr assignmentに取り組みたいと思います。 余力があればaliasやundefあたりもさくっと実装して進捗を出したいところでもあります。
attr assignment
struct.field = fooの形をした代入をattr assignmentと呼んでいます。
これは実際にはfield=というメソッドをfooを引数として呼び出すことになります。
書き換え前後のノードを確認する
書き換え前のノードはNODE_ATTRASGNです。
細かくみるとattr assignmentには3つの異なる形式があります。
struct.field = foostruct&.field = fooa[i] = foo
最初の2つはattrset関数、最後の1つはaryset関数で生成しています。
static NODE * attrset(struct parser_params *p, NODE *recv, ID atype, ID id, const YYLTYPE *loc) { if (!CALL_Q_P(atype)) id = rb_id_attrset(id); return NEW_ATTRASGN(recv, id, 0, loc); } static NODE * aryset(struct parser_params *p, NODE *recv, NODE *idx, const YYLTYPE *loc) { aryset_check(p, idx); return NEW_ATTRASGN(recv, tASET, idx, loc); }
attrsetではCALL_Q_P、つまり&.なのか.なのかで生成するメソッドのidを変更しています。
.の場合は:field=、&.の場合は:fieldという形のidがNODE_ATTRASGNに設定されます。
arysetではaryset_checkという関数でa[args] = 1のargsの部分をチェックしています。
aryset_checkの最後の方を読むと、この関数はキーワード引数やブロック引数が渡されてないことを確認するための関数であることがわかります。
static void aryset_check(struct parser_params *p, NODE *args) { NODE *block = 0, *kwds = 0; if (args && nd_type_p(args, NODE_BLOCK_PASS)) { block = RNODE_BLOCK_PASS(args)->nd_body; args = RNODE_BLOCK_PASS(args)->nd_head; } if (args && nd_type_p(args, NODE_ARGSCAT)) { args = RNODE_ARGSCAT(args)->nd_body; } if (args && nd_type_p(args, NODE_ARGSPUSH)) { kwds = RNODE_ARGSPUSH(args)->nd_body; } else { for (NODE *next = args; next && nd_type_p(next, NODE_LIST); next = RNODE_LIST(next)->nd_next) { kwds = RNODE_LIST(next)->nd_head; } } if (kwds && nd_type_p(kwds, NODE_HASH) && !RNODE_HASH(kwds)->nd_brace) { yyerror1(&kwds->nd_loc, "keyword arg given in index assignment"); } if (block) { yyerror1(&block->nd_loc, "block arg given in index assignment"); } }
なぜarysetでは&.のチェックが不要で、attrsetではキーワード引数やブロック引数のチェックが不要なのでしょうか。
そのような組み合わせは文法定義上ありえないからです。
arysetは以下のような生成規則から呼び出されます。
ここで'['は終端記号であり&[のような形にはなりません。
なので&.のチェックは不要です。
lhs | primary_value '[' opt_call_args rbracket { $$ = aryset(p, $1, $3, &@$); /*% ripper: aref_field!($:1, $:3) %*/ }
一方でattrsetは以下のような生成規則から呼び出されます。
今度は引数に当たる部分がありません。
なのでキーワード引数やブロック引数をそもそも渡すことができません。
lhs | primary_value tCOLON2 tIDENTIFIER
{
$$ = attrset(p, $1, idCOLON2, $3, &@$);
/*% ripper: field!($:1, $:2, $:3) %*/
}
ここまでを踏まえて書き換え前後のノードについてまとめておきます。
| 前 | nd_mid | 後 | name | |
|---|---|---|---|---|
struct.field = foo |
ATTRASGN | :field= |
CallNode | :field= |
struct&.field = foo |
ATTRASGN | :field |
CallNode | :field= |
a[i] = foo |
ATTRASGN | :[]= |
CallNode | :[]= |
parse.yを変更する
まずNEW_ATTRASGNマクロに対応するNEW_RB_ATTRASGNマクロを定義します。
他のメソッド呼び出しと同様にNEW_RB_CALLのラッパーとして定義するのが良いでしょう。
+#define NEW_RB_ATTRASGN(r,m,a,loc) NEW_RB_CALL(r,m,a,0,loc)
aryset関数は型の調整だけなので割愛します。
attrset関数は&.かどうかに関係なく:field=形式のidを使用するようになるので、CALL_Q_Pによる分岐を削除します。
static NODE * -attrset(struct parser_params *p, NODE *recv, ID atype, ID id, const YYLTYPE *loc) +attrset(struct parser_params *p, rb_node_t *recv, ID atype, ID id, const YYLTYPE *loc) { - if (!CALL_Q_P(atype)) id = rb_id_attrset(id); - return NEW_ATTRASGN(recv, id, 0, loc); + return NEW_ATTRASGN(recv, rb_id_attrset(id), 0, loc); }
aryset_checkは引数のrb_arguments_node_tが持っているリストをループするコードに書き換えればいいでしょう。
static void aryset_check(struct parser_params *p, rb_arguments_node_t *args) { rb_node_t *block = 0, *kwds = 0; rb_node_list2_t *list = &args->arguments; for (size_t i = 0; i < RB_NODE_LIST_LEN(list); i++) { const rb_node_t *node = list->nodes[i]; switch (nd_type(node)) { case RB_KEYWORD_HASH_NODE: block = node; break; case RB_BLOCK_ARGUMENT_NODE: kwds = node; break; default: break; } } if (kwds) { yyerror1(&kwds->location, "keyword arg given in index assignment"); } if (block) { yyerror1(&block->location, "block arg given in index assignment"); } }
minirubyをビルドして生成されるノードを確認します。
$ ./miniruby --parser=parse.y -e 'a[i] = v' ./miniruby: [BUG] unexpected node: RB_CALL_NODE ... /Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(rb_bug+0x28) [0x102d7b70c] ../../error.c:1116 /Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(node_assign+0xdc) [0x102e88db8] parse.y:14843 /Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(ruby_yyparse+0x33e0) [0x102e7ced4] parse.y:3182 ....
[BUG]で落ちました。
えーっと?
static rb_node_t * node_assign(struct parser_params *p, rb_node_t *lhs, rb_node_t *rhs, struct lex_context ctxt, const YYLTYPE *loc) { if (!lhs) return 0; switch (RB_NODE_TYPE(lhs)) { // case NODE_CDECL: case RB_GLOBAL_VARIABLE_WRITE_NODE: case NODE_LASGN: // case NODE_DASGN: // case NODE_MASGN: case RB_CLASS_VARIABLE_WRITE_NODE: set_nd_value(p, lhs, rhs); rb_nd_set_loc(lhs, loc); break; // case NODE_ATTRASGN: // RNODE_ATTRASGN(lhs)->nd_args = arg_append(p, RNODE_ATTRASGN(lhs)->nd_args, rhs, loc); // nd_set_loc(lhs, loc); // break; default: rb_bug("unexpected node: %s", rb_node_type_to_str(RB_NODE_TYPE(lhs))); UNREACHABLE_RETURN(0); } return lhs; }
代入式全体のparseが終わった時に右辺の値を左辺のCallNodeのargumentsに追加する必要があるのでした。
例えばstruct.field = fooであればstruct.fieldまでをparseしたタイミングでCallNodeを生成しているので、= fooまでparseしたときにCallNodeのargumentsにfooを追加する必要があるということです。
こんな感じに書き換えればいいでしょう。
static rb_node_t * node_assign(struct parser_params *p, rb_node_t *lhs, rb_node_t *rhs, struct lex_context ctxt, const YYLTYPE *loc) { ... case RB_CALL_NODE: // TODO: Assert call node is NODE_ATTRASGN node_arg_append(p, RB_NODE_CALL(lhs)->arguments, rhs, loc); rb_nd_set_loc(lhs, loc); break; default: rb_bug("unexpected node: %s", rb_node_type_to_str(RB_NODE_TYPE(lhs))); UNREACHABLE_RETURN(0); ... }
再度minirubyをビルドして、試してみます。
$ ./miniruby --parser=parse.y -e 'a[i] = v' -e:1:in '<main>': undefined local variable or method 'a' for main (NameError)
ん? 実行できてる? なんか動いてしまいましたね。
$ ./miniruby --parser=parse.y -e 's.f = nil' ./miniruby: [BUG] Segmentation fault at 0x0000000000000020 ... /usr/lib/system/libsystem_platform.dylib(_sigtramp+0x38) [0x19722e584] /Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(rb_node_list_append+0x20) [0x104277bbc] ../../parser_node_list.c:55 /Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(rb_node_list_append+0x20) [0x104277bbc] ../../parser_node_list.c:55 /Users/kaneko.y/source/ruby/ruby/build/dev/miniruby(node_arg_append+0x2c) [0x104274f30] parse.y:14693 ...
うん?
あー。attrsetのときはrb_arguments_node_t *をNULLで初期化しているのでした。
static rb_node_t * attrset(struct parser_params *p, rb_node_t *recv, ID atype, ID id, const YYLTYPE *loc) { return NEW_RB_ATTRASGN(recv, rb_id_attrset(id), 0, loc); }
attrset関数を呼んだ場合、その後右辺の値がargumentsに追加されることは確定しています。
ということでattrset関数のなかで空のargumentsを生成しておくのでいいでしょう。
static rb_node_t * attrset(struct parser_params *p, rb_node_t *recv, ID atype, ID id, const YYLTYPE *loc) { return NEW_RB_ATTRASGN(recv, rb_id_attrset(id), NEW_RB_ARGUMENTS0(&NULL_LOC), loc); }
再度minirubyをビルドして試しましょう。
$ ./miniruby --parser=parse.y -e 's.f = nil' -e:1:in '<main>': undefined local variable or method 's' for main (NameError)
やっぱりなぜか実行までできる...
attr assignmentをコンパイルする
一応生成されるバイトコードをみてみましょう。
# Before ruby --parser=parse.y --dump=i -e "h[:k]= :v" == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,9)> 0000 putnil ( 1)[Li] 0001 putself 0002 opt_send_without_block <calldata!mid:h, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0004 putobject :k 0006 putobject :v 0008 setn 3 0010 opt_aset <calldata!mid:[]=, argc:2, ARGS_SIMPLE>[CcCr] 0012 pop 0013 leave # After ./miniruby --parser=parse.y --dump=i -e "h[:k]= :v" == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,9)> 0000 putself ( 1)[Li] 0001 opt_send_without_block <calldata!mid:h, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0003 putobject :k 0005 putobject :v 0007 opt_aset <calldata!mid:[]=, argc:2, ARGS_SIMPLE>[CcCr] 0009 leave
全然違いますね。うん、知ってた。
正しいバイトコードを眺めます。
0000 putnil ( 1)[Li] # なにこれ? 0001 putself 0002 opt_send_without_block <calldata!mid:h, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0004 putobject :k 0006 putobject :v 0008 setn 3 # なにこれ? 0010 opt_aset <calldata!mid:[]=, argc:2, ARGS_SIMPLE>[CcCr] 0012 pop # なにこれ? 0013 leave
スタックの気持ちになってみます。
# 0001 putself まで self nil # 0002 opt_send_without_blockまで h の戻り値 nil # 0006 putobject :v まで :v :k h の戻り値 nil
setnは初めてみる命令なので定義を見ておきます。
英語の説明がなんとなく間違っている気がしますが、これはstack topの値(val)をstackの上からn番目にコピーする命令のようです。
ここでstack topはn = 0で表します。
/* set Nth stack entry to stack top */ DEFINE_INSN setn (rb_num_t n) (..., VALUE val) (VALUE val) // attr rb_snum_t sp_inc = 0; { TOPN(n) = val; }
スタックの操作を引き続き考えていきます。
# 0008 setn 3 まで :v :k h の戻り値 :v # 0010 opt_aset まで []= の戻り値 :v # 0012 pop まで :v
ということで=の右辺にある:vが最終的にスタックに残ります1。
これはもしかして... ということで簡単なスクリプトを書いて確認します。
class A def []=(*a) false end end a = A.new p(a[0] = 1) #=> 1
ということで、Rubyは#m=というメソッドを呼び出した時に、メソッドの戻り値ではなく=の右辺が式の値になるということがわかりました2。
compile.cを変更する
compile.cのもともとの実装をみるとNODE_ATTRASGNとそれ以外のメソッド呼び出しで関数が分かれていることがわかります。
それはそう... という感じがします。
case NODE_CALL: /* obj.foo */ case NODE_OPCALL: /* foo[] */ if (compile_call_precheck_freeze(iseq, ret, node, node, popped) == TRUE) { break; } case NODE_QCALL: /* obj&.foo */ case NODE_FCALL: /* foo() */ case NODE_VCALL: /* foo (variable or call) */ if (compile_call(iseq, ret, node, type, node, popped, false) == COMPILE_NG) { goto ng; } break; ... case NODE_ATTRASGN: CHECK(compile_attrasgn(iseq, ret, node, popped)); break;
attr assignmentの場合は文法上ブロックが渡せないことや、生成されるバイトコードがCallNodeとは結構違うことを考え、処理する関数を分けるというアプローチを引き続き採用することにします。
さてattr assignmentのときのノードもCallNodeに統一したので、コンパイラでattr assignmentかどうか区別する方法が必要です。
CALL_NODE_FLAGS_ATTRIBUTE_WRITEというフラグがあるようなので、このフラグを立てるようにparse.yを修正します。
またs&.f= 1のときにCALL_NODE_FLAGS_SAFE_NAVIGATIONがたつようにもしておきます。
#define NEW_RB_ATTRASGN(r,m,a,fl,loc) NEW_RB_CALL(r,m,a,fl|RB_CALL_NODE_FLAGS_ATTRIBUTE_WRITE,loc) static rb_node_t * attrset(struct parser_params *p, rb_node_t *recv, ID atype, ID id, const YYLTYPE *loc) { int flags = CALL_Q_P(atype) ? RB_CALL_NODE_FLAGS_SAFE_NAVIGATION : 0; return NEW_RB_ATTRASGN(recv, rb_id_attrset(id), NEW_RB_ARGUMENTS0(&NULL_LOC), flags, loc); }
compile.cはCallNodeを処理する先頭で分岐すればよいでしょう。
compile_attrasgnの書き換えはいつものように構造体が変化したことによるマクロの書き換えが主なので割愛します。
case RB_CALL_NODE: { if (rb_node_get_fl(node) & RB_CALL_NODE_FLAGS_ATTRIBUTE_WRITE) { CHECK(compile_attrasgn(iseq, ret, RB_NODE_CALL(node), popped)); } else if (compile_iter(iseq, ret, node, type, popped) == COMPILE_NG) { goto ng; } break; }
正しくコンパイルできるようになりました。
$ ./miniruby --parser=parse.y --dump=i -e "a[:k] = :v" == disasm: #<ISeq:<main>@-e:1 (1,0)-(1,10)> 0000 putnil ( 1)[Li] 0001 putself 0002 opt_send_without_block <calldata!mid:a, argc:0, FCALL|VCALL|ARGS_SIMPLE> 0004 putobject :k 0006 putobject :v 0008 setn 3 0010 opt_aset <calldata!mid:[]=, argc:2, ARGS_SIMPLE>[CcCr] 0012 pop 0013 leave
おまけ: いままでどうやってa.b = 1とa&.b = 1を区別していたか
通常のメソッド呼び出しではQCALLという専用のノードでa&.bというメソッド呼び出しを表現していました。
しかしa&.b = 1用のノードはなく、a.b = 1もa&.b = 1もNODE_ATTRASGNというノードを用いていました。
ではどのようにしてこの2つを判別していたかというと、ノードに設定されたメソッド名をみて判断していました。
a.b = 1のときはnd_mid: :b=と=がついているのに対して、c&.d = 2のときはnd_mid: :dと=がついていません。
$ ruby --parser=parse.y --dump=p -e "a.b = 1; c&.d = 2" # | @ NODE_ATTRASGN (id: 1, line: 1, location: (1,0)-(1,7))* # | +- nd_recv: # | | @ NODE_VCALL (id: 0, line: 1, location: (1,0)-(1,1)) # | | +- nd_mid: :a # | +- nd_mid: :b= # +- nd_head (2): # @ NODE_ATTRASGN (id: 5, line: 1, location: (1,9)-(1,17))* # +- nd_recv: # | @ NODE_VCALL (id: 4, line: 1, location: (1,9)-(1,10)) # | +- nd_mid: :c # +- nd_mid: :d
これはNODE_ATTRASGNをつくるときに&.かどうかでrb_id_attrsetを呼び出すかを変えることで実現しています。
// parse.y static NODE * attrset(struct parser_params *p, NODE *recv, ID atype, ID id, const YYLTYPE *loc) { if (!CALL_Q_P(atype)) id = rb_id_attrset(id); return NEW_ATTRASGN(recv, id, 0, loc); }
compile.cではrb_is_attrset_idという関数を使ってmidをチェックして&.かどうかの判断をし、rb_id_attrsetを呼び出してmidに=を付与したidを取得してバイトコードに埋め込むようなっています。
static int compile_attrasgn(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped) { ... if (!rb_is_attrset_id(mid)) { /* safe nav attr */ mid = rb_id_attrset(mid); else_label = qcall_branch_start(iseq, recv, &branches, node, node); } ... return COMPILE_OK; }
まとめ
今日の成果です。
a.b = 1やa[0] = 1といったattr assignmentと呼ばれる文法の対応をした
余裕はなったのでaliasやundefなどはまた今度ということで。