以下の内容はhttps://yui-knk.hatenablog.com/entry/2026/01/25/225639より取得しました。


Ruby Parser開発日誌 (24-23) - parse.yが生成するノードを変える ー superとyield

23日目: superとyieldに対応する

前回はbreak, next, redoといった制御構文に対応したので、今回はsuperとyieldに取り組みます。

superのノードを書き換える

Rubyではsuper 1のように引数を渡したときと、superのように引数を渡さないときで挙動がだいぶ変わります。 そのためノードもSUPERZSUPERというように、それぞれ専用のノードが割り当てられています。 ノードの書き換え後もSUPERFORWARDING_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ないしForwardingSuperNodeblockフィールドに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だけでなくSuperNodeForwardingSuperNodeを扱えるように修正します。

  • method_add_block: 引数として渡されたノードにブロックノードを設定する
  • method_add_arguments: ArgumentsNodeから&blkノードを取り出し、引数として渡されたノードのblockフィールドに追加する
  • block_dup_check: &blkdo ... 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;
}

書き換え後のノードを確認しておきましょう。 ブロックがSuperNodeForwardingSuperNodeの子ノードになっていることがわかります。

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関数がSUPERZSUPERの両方を処理していました。 ノードの書き換え後もそれを踏襲します。 修正自体はマクロやアクセスするフィールド名の変更が主なので割愛します。

ノードの書き換え前後で大きく変わるのはブロックの扱いです。 メソッド呼び出しを表すCallNodeのときもそうだったのですが、いままではブロックがある場合にはNODE_CALLNODE_SUPERNODE_ITERの子ノードとして保持される構造になっていました。 ノードの書き換え後はCallNodeSuperNodeといったノードがブロックに相当するノードを持つようになります。

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の対応をした

ここまでの進捗

前回の進捗確認から少し間が空いたのであらためて進捗を確認しておきましょう。

yui-knk.hatenablog.com

前回と比べて対応した機能は以下のとおりです。

  • 19-20日目: rescue / ensure
  • 21日目: retry, while, until
  • 22日目: break, next, redo
  • 23日目: super, yield
  • いつの間にか実装していた: return

冬休みが終わってしまい、まとまった時間をとるのが難しくはなっていますが、まあまあ順調に進んでいるのではないでしょうか。 機能の一覧とその進捗は以下のとおりです。 引き続き頑張っていきましょう。

  • リテラル
    • nil
    • true
    • false
    • self
    • __LINE__
    • __FILE__
    • __ENCODING__
    • array
    • hash
    • string
    • integer
    • float
    • rational
    • imaginary
    • regex
    • range
    • interpolationを含むstring
    • interpolationを含むsymbol
    • interpolationを含むregex
    • lambda
  • 変数の参照と代入
  • 定義
    • メソッド定義
      • 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)
  • 制御構文
    • if
    • unless
    • flipflop
    • case when
    • while / until
    • for
    • retry / rescue / ensure
    • && / || / and / or
    • yield
    • return
    • super
    • break / next / redo
    • BEGIN
    • END
  • パターンマッチング
  • その他
    • alias
    • undef
    • defined?



以上の内容はhttps://yui-knk.hatenablog.com/entry/2026/01/25/225639より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14