23日目: superとyieldに対応する
前回はbreak, next, redoといった制御構文に対応したので、今回はsuperとyieldに取り組みます。
superのノードを書き換える
Rubyではsuper 1のように引数を渡したときと、superのように引数を渡さないときで挙動がだいぶ変わります。
そのためノードもSUPERとZSUPERというように、それぞれ専用のノードが割り当てられています。
ノードの書き換え後もSUPERとFORWARDING_SUPERというように、専用のノードが割り当てられているので、それらのノードを使うように書き換えます。
1点注意が必要なのは書き換え後のノードはどちらもblockというフィールドをもつということです。
以下のようなコードのノードを考えてみましょう。
def m super arg1 do expr1 end super do expr2 end end
書き換え前はブロックがある場合にはNODE_BLOCKというノードがNODE_SUPERないしはNODE_ZSUPERを子ノードとして持つという構造でした。
# @ NODE_BLOCK (id: 13, line: 2, location: (2,2)-(8,5)) # +- nd_head (1): # | @ NODE_ITER (id: 8, line: 2, location: (2,2)-(4,5))* # | +- nd_iter: # | | @ NODE_SUPER (id: 5, line: 2, location: (2,2)-(2,12)) # | | +- nd_args: # | | | @ NODE_LIST (id: 4, line: 2, location: (2,8)-(2,12)) # | | | +- as.nd_alen: 1 # | | | +- nd_head: # | | | | @ NODE_VCALL (id: 3, line: 2, location: (2,8)-(2,12)) # | | | | +- nd_mid: :arg1 # | | | +- nd_next: # | | | (null node) # | | +- keyword_loc: (2,2)-(2,7) # | | +- lparen_loc: (0,-1)-(0,-1) # | | +- rparen_loc: (0,-1)-(0,-1) # | +- nd_body: # | @ NODE_SCOPE (id: 7, line: 2, location: (2,13)-(4,5)) # | +- nd_tbl: (empty) # | +- nd_args: # | | (null node) # | +- nd_body: # | @ NODE_VCALL (id: 6, line: 3, location: (3,4)-(3,9))* # | +- nd_mid: :expr1 # +- nd_head (2): # @ NODE_ITER (id: 12, line: 6, location: (6,2)-(8,5))* # +- nd_iter: # | @ NODE_ZSUPER (id: 9, line: 6, location: (6,2)-(6,7)) # +- nd_body: # @ NODE_SCOPE (id: 11, line: 6, location: (6,8)-(8,5)) # +- nd_tbl: (empty) # +- nd_args: # | (null node) # +- nd_body: # @ NODE_VCALL (id: 10, line: 7, location: (7,4)-(7,9))* # +- nd_mid: :expr2
書き換え後はSuperNodeないしForwardingSuperNodeのblockフィールドにBlockNodeが置かれます。
| +-- @ SuperNode (location: (2,2)-(4,5)) | | +-- arguments: | | | @ ArgumentsNode (location: (2,8)-(2,12)) | | | +-- ArgumentsNodeFlags: nil | | | +-- arguments: (length: 1) | | | +-- @ CallNode (location: (2,8)-(2,12)) | | | +-- receiver: nil | | | +-- name: :arg1 | | | +-- arguments: nil | | | +-- block: nil | | +-- block: | | @ BlockNode (location: (2,13)-(4,5)) | | +-- locals: [] | | +-- parameters: nil | | +-- body: | | | @ StatementsNode (location: (3,4)-(3,9)) | | | +-- body: (length: 1) | | | +-- @ CallNode (location: (3,4)-(3,9)) | | | +-- receiver: nil | | | +-- name: :expr1 | | | +-- arguments: nil | | | +-- block: nil | +-- @ ForwardingSuperNode (location: (6,2)-(8,5)) | +-- block: | @ BlockNode (location: (6,8)-(8,5)) | +-- locals: [] | +-- parameters: nil | +-- body: | | @ StatementsNode (location: (7,4)-(7,9)) | | +-- body: (length: 1) | | +-- @ CallNode (location: (7,4)-(7,9)) | | +-- receiver: nil | | +-- name: :expr2 | | +-- arguments: nil | | +-- block: nil
parse.yでブロックを扱う以下の関数でCallNodeだけでなくSuperNodeとForwardingSuperNodeを扱えるように修正します。
method_add_block: 引数として渡されたノードにブロックノードを設定するmethod_add_arguments:ArgumentsNodeから&blkノードを取り出し、引数として渡されたノードのblockフィールドに追加するblock_dup_check:&blkとdo ... endの両方が渡されていないかチェックする
雰囲気がわかるように修正後のmethod_add_block関数を掲載しておきます。
static rb_node_t * method_add_block(struct parser_params *p, rb_node_t *m, rb_block_node_t *b, const YYLTYPE *loc) { switch (RB_NODE_TYPE(m)) { case RB_CALL_NODE: RB_NODE_CALL(m)->block = b; break; case RB_SUPER_NODE: RB_NODE_SUPER(m)->block = b; break; case RB_FORWARDING_SUPER_NODE: RB_NODE_FORWARDING_SUPER(m)->block = b; break; default: rb_bug("unexpected node: %s", rb_node_type_to_str(RB_NODE_TYPE(m))); UNREACHABLE_RETURN(0); } return m; }
書き換え後のノードを確認しておきましょう。
ブロックがSuperNodeやForwardingSuperNodeの子ノードになっていることがわかります。
def m super arg1 do expr1 end super do expr2 end end # @ StatementsNode (location: (2,2)-(6,7)) # +-- body: (length: 2) # +-- @ SuperNode (location: (2,2)-(2,12))* # | +-- arguments: # | | @ ArgumentsNode (location: (2,8)-(2,12)) # | | +-- arguments: (length: 1) # | | +-- @ CallNode (location: (2,8)-(2,12)) # | | +-- receiver: nil # | | +-- name: :arg1 # | | +-- arguments: nil # | | +-- block: nil # | +-- block: # | @ BlockNode (location: (2,15)-(3,9)) # | +-- locals: [] # | +-- parameters: nil # | +-- body: # | | @ BeginNode (location: (3,4)-(3,9)) # | | +-- begin_keyword_loc: nil # | | +-- statements: # | | | @ StatementsNode (location: (3,4)-(3,9)) # | | | +-- body: (length: 1) # | | | +-- @ CallNode (location: (3,4)-(3,9))* # | | | +-- receiver: nil # | | | +-- name: :expr1 # | | | +-- arguments: nil # | | | +-- block: nil # | | +-- rescue_clause: nil # | | +-- else_clause: nil # | | +-- ensure_clause: nil # +-- @ ForwardingSuperNode (location: (6,2)-(6,7))* # +-- block: # @ BlockNode (location: (6,10)-(7,9)) # +-- locals: [] # +-- parameters: nil # +-- body: # | @ BeginNode (location: (7,4)-(7,9)) # | +-- begin_keyword_loc: nil # | +-- statements: # | | @ StatementsNode (location: (7,4)-(7,9)) # | | +-- body: (length: 1) # | | +-- @ CallNode (location: (7,4)-(7,9))* # | | +-- receiver: nil # | | +-- name: :expr2 # | | +-- arguments: nil # | | +-- block: nil # | +-- rescue_clause: nil # | +-- else_clause: nil # | +-- ensure_clause: nil
superをコンパイルする
ノードの書き換え前はcompile_super関数がSUPERとZSUPERの両方を処理していました。
ノードの書き換え後もそれを踏襲します。
修正自体はマクロやアクセスするフィールド名の変更が主なので割愛します。
ノードの書き換え前後で大きく変わるのはブロックの扱いです。
メソッド呼び出しを表すCallNodeのときもそうだったのですが、いままではブロックがある場合にはNODE_CALLやNODE_SUPERがNODE_ITERの子ノードとして保持される構造になっていました。
ノードの書き換え後はCallNodeやSuperNodeといったノードがブロックに相当するノードを持つようになります。
SuperNodeの場合も以前実装をしたCallNode同様に一度compile_iter関数に渡すようにします。
case RB_CALL_NODE: { if (rb_node_get_fl(node) & RB_CALL_NODE_FLAGS_ATTRIBUTE_WRITE) { CHECK(compile_attrasgn(iseq, ret, node, popped)); } else if (compile_iter(iseq, ret, node, type, popped) == COMPILE_NG) { goto ng; } break; } case RB_SUPER_NODE: case RB_FORWARDING_SUPER_NODE: { if (compile_iter(iseq, ret, node, type, popped) == COMPILE_NG) { goto ng; } break; }
compile_iter関数ではCallNode同様にブロックの有無で呼び出す関数を切り替えます。
static int compile_iter(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, const enum node_type type, int popped) { switch (type) { // case RB_FOR_NODE: case RB_CALL_NODE: { if (block_node_p(RB_NODE_CALL(node)->block)) { return compile_iter0(iseq, ret, node, type, popped); } else { return compile_call(iseq, ret, node, type, popped); } break; } case RB_SUPER_NODE: { if (block_node_p(RB_NODE_SUPER(node)->block)) { return compile_iter0(iseq, ret, node, type, popped); } else { return compile_super(iseq, ret, node, type, popped); } break; } case RB_FORWARDING_SUPER_NODE: { if (block_node_p(RB_NODE_FORWARDING_SUPER(node)->block)) { return compile_iter0(iseq, ret, node, type, popped); } else { return compile_super(iseq, ret, node, type, popped); } break; } ...
ブロックがある場合の処理を行うcompile_iter0関数ではブロックを別のISeqとしてコンパイルしてからcompile_super関数を呼び出します。
static int compile_iter0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, const enum node_type type, int popped) { const int line = nd_line(node); const NODE *line_node = node; const rb_iseq_t *prevblock = ISEQ_COMPILE_DATA(iseq)->current_block; LABEL *retry_label = NEW_LABEL(line); LABEL *retry_end_l = NEW_LABEL(line); const rb_iseq_t *child_iseq; ADD_LABEL(ret, retry_label); switch (type) { case RB_CALL_NODE: { EXPECT_NODE("compile_iter0", RB_NODE_CALL(node)->block, RB_BLOCK_NODE, COMPILE_NG); ISEQ_COMPILE_DATA(iseq)->current_block = child_iseq = NEW_CHILD_ISEQ(RB_NODE_CALL(node)->block, make_name_for_block(iseq), ISEQ_TYPE_BLOCK, line); CHECK(compile_call(iseq, ret, node, type, 0)); break; } case RB_SUPER_NODE: { EXPECT_NODE("compile_iter0", RB_NODE_SUPER(node)->block, RB_BLOCK_NODE, COMPILE_NG); ISEQ_COMPILE_DATA(iseq)->current_block = child_iseq = NEW_CHILD_ISEQ(RB_NODE_SUPER(node)->block, make_name_for_block(iseq), ISEQ_TYPE_BLOCK, line); CHECK(compile_super(iseq, ret, node, type, 0)); break; } case RB_FORWARDING_SUPER_NODE: { EXPECT_NODE("compile_iter0", RB_NODE_FORWARDING_SUPER(node)->block, RB_BLOCK_NODE, COMPILE_NG); ISEQ_COMPILE_DATA(iseq)->current_block = child_iseq = NEW_CHILD_ISEQ(RB_NODE_FORWARDING_SUPER(node)->block, make_name_for_block(iseq), ISEQ_TYPE_BLOCK, line); CHECK(compile_super(iseq, ret, node, type, 0)); break; } } ...
superとzsuperのコンパイル結果を確認します。 よさそうですね。
# == disasm: #<ISeq:<main>@../../test.rb:1 (1,0)-(9,3)> # 0000 definemethod :m, m ( 1)[Li] # 0003 putobject :m # 0005 leave # == disasm: #<ISeq:m@../../test.rb:1 (1,0)-(9,3)> # 0000 putself ( 2)[LiCa] # 0001 putself # 0002 opt_send_without_block <calldata!mid:arg1, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0004 invokesuper <calldata!argc:1, FCALL|SUPER>, block in m # 0007 pop # 0008 putself ( 6)[Li] # 0009 invokesuper <calldata!argc:0, FCALL|SUPER|ZSUPER>, block in m # 0012 leave ( 1)[Re] # == disasm: #<ISeq:block in m@../../test.rb:2 (2,15)-(3,9)> # 0000 putself ( 3)[LiBc] # 0001 opt_send_without_block <calldata!mid:expr1, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0003 leave [Br] # == disasm: #<ISeq:block in m@../../test.rb:6 (6,10)-(7,9)> # 0000 putself ( 7)[LiBc] # 0001 opt_send_without_block <calldata!mid:expr2, argc:0, FCALL|VCALL|ARGS_SIMPLE> # 0003 leave [Br] def m super arg1 do expr1 end super do expr2 end end
yieldのノードを書きかえる
yieldはyieldのほかにyield 1, 2のように引数を渡すこともできます。
yield do ... endと書くことはできません。
シンプルな構造でノードの書き換え前後でその基本構造は変わりません。
parse.yの変更は言ってしまえば呼び出すマクロの変更だけですみます。
| k_yield '(' call_args rparen
{
- $$ = NEW_YIELD($3, &@$, &@1, &@2, &@4);
+ $$ = NEW_RB_YIELD($3, &@$, &@1, &@2, &@4);
/*% ripper: yield!(paren!($:3)) %*/
}
yieldのノードをコンパイルする
コンパイルについてもcompile_yield関数の中で使うマクロを変更するなど、軽微な修正ですみます。
生成されるバイトコードを確認して終了です。
def m yield yield nil end # == disasm: #<ISeq:<main>@../../test.rb:1 (1,0)-(4,3)> # 0000 definemethod :m, m ( 1)[Li] # 0003 putobject :m # 0005 leave # # == disasm: #<ISeq:m@../../test.rb:1 (1,0)-(4,3)> # 0000 invokeblock <calldata!argc:0, ARGS_SIMPLE>( 2)[LiCa] # 0002 pop # 0003 putnil ( 3)[Li] # 0004 invokeblock <calldata!argc:1, ARGS_SIMPLE> # 0006 leave ( 1)[Re]
まとめ
今日の成果です。
- superの対応をした
- yieldの対応をした
ここまでの進捗
前回の進捗確認から少し間が空いたのであらためて進捗を確認しておきましょう。
前回と比べて対応した機能は以下のとおりです。
- 19-20日目: rescue / ensure
- 21日目: retry, while, until
- 22日目: break, next, redo
- 23日目: super, yield
- いつの間にか実装していた: return
冬休みが終わってしまい、まとまった時間をとるのが難しくはなっていますが、まあまあ順調に進んでいるのではないでしょうか。 機能の一覧とその進捗は以下のとおりです。 引き続き頑張っていきましょう。
- リテラル
- 変数の参照と代入
- 定義
- メソッド定義
multiple assignment (def m((a, b)))が未対応- forwarding (
...)が未対応
クラス定義 / モジュール定義 / シングルトンクラス定義
- メソッド定義
- メソッド呼び出し
いわゆる通常のメソッド呼び出し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
yieldreturnsuperbreak / next / redo- BEGIN
- END
- パターンマッチング
- その他
- alias
- undef
- defined?