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


Ruby Parser開発日誌 (24-22) - parse.yが生成するノードを変える ー break, next, redo

22日目: break, next, redoに対応する

前回はretryの書き換えをしたところで、whileやuntilの対応をまだしていなかったことに気がついたので、それらloopの対応をしたのでした。 今回はloopやブロックに対する制御構文であるbreak, next, redoに取り組みます。

parse.yの書き換え

break, next, redoに対応するノードの書き換え自体はさほど難しくないのですが、問題はそれらの管理方法にあります。

Rubyではbreak, next, redoを書くことができる場所と、できない場所が存在します。

10.times do
  break
end
# OK

while true
  break
end
# OK

break
#=> Invalid break (SyntaxError)

parse.yではこれらのinvalidなbreakなどをリスト構造で管理しています。

実際にそれぞれのキーワードに対応する生成規則をみると、add_block_exitという関数を呼び出していることがわかります。 この関数は特定の条件下でstruct parser_paramsにあるexitsに、breaknextのノードをappendしています。

| keyword_break
    {
        $$ = add_block_exit(p, NEW_BREAK(0, &@$, &@1));
    }
| keyword_next
    {
        $$ = add_block_exit(p, NEW_NEXT(0, &@$, &@1));
    }
| keyword_redo
    {
        $$ = add_block_exit(p, NEW_REDO(&@$, &@1));
    }


static NODE *
add_block_exit(struct parser_params *p, NODE *node)
{
    if (!node) {
        compile_error(p, "unexpected null node");
        return 0;
    }
    switch (nd_type(node)) {
      case NODE_BREAK: case NODE_NEXT: case NODE_REDO: break;
      default:
        compile_error(p, "add_block_exit: unexpected node: %s", parser_node_name(nd_type(node)));
        return node;
    }
    if (!p->ctxt.in_defined) {
        rb_node_exits_t *exits = p->exits;
        if (exits) {
            RNODE_EXITS(exits->nd_stts)->nd_chain = node;
            exits->nd_stts = node;
        }
    }
    return node;
}

p->exitsinit_block_exit関数の呼び出しによって初期化され、allow_block_exit関数が呼び出されると0が代入されます。 そしてclear_block_exit関数はp->exitsbreakなどのノードがappendされているかチェックして、それらのノードが存在する場合にはsyntax errorにします。

static void
clear_block_exit(struct parser_params *p, bool error)
{
    rb_node_exits_t *exits = p->exits;
    if (!exits) return;
    if (error) {
        for (NODE *e = RNODE(exits); (e = RNODE_EXITS(e)->nd_chain) != 0; ) {
            switch (nd_type(e)) {
              case NODE_BREAK:
                yyerror1(&e->nd_loc, "Invalid break");
                break;
              case NODE_NEXT:
                yyerror1(&e->nd_loc, "Invalid next");
                break;
              case NODE_REDO:
                yyerror1(&e->nd_loc, "Invalid redo");
                break;
              default:
                yyerror1(&e->nd_loc, "unexpected node");
                goto end_checks; /* no nd_chain */
            }
        }
      end_checks:;
    }
    exits->nd_stts = RNODE(exits);
    exits->nd_chain = 0;
}

NODE_EXITSの構造を変える

書き換え前はNODE_EXITS, NODE_BREAK, NODE_NEXT, NODE_REDOはどれも同じ構造をしていて、いずれのノードにもnd_chainというフィールドがありました。 そのためこれらのノードをつかってリスト構造をつくることができたわけです。

typedef struct RNode_EXITS {
    NODE node;

    struct RNode *nd_chain;
    struct RNode *nd_stts;
    rb_code_location_t keyword_loc;
} rb_node_exits_t, rb_node_break_t, rb_node_next_t, rb_node_redo_t;

しかし書き換え後のNODE_BREAK, NODE_NEXT, NODE_REDOにはnd_chainに相当するフィールドはありません。 そこでNODE_EXITSの構造体を変更して、NODE_EXITSだけでリストを作るようにしましょう。

構造が決まれば書き換えは自明なので割愛します。

書き換え後のノードは問題なさそうです。

# @ CallNode (location: (1,0)-(1,7))*
# +-- receiver:
# |   @ CallNode (location: (1,0)-(1,1))
# |   +-- receiver: nil
# |   +-- name: :i
# |   +-- arguments: nil
# |   +-- block: nil
# +-- name: :times
# +-- arguments: nil
# +-- block:
#     @ BlockNode (location: (1,10)-(2,7))
#     +-- locals: []
#     +-- parameters: nil
#     +-- body:
#     |   @ BeginNode (location: (2,2)-(2,7))
#     |   +-- statements:
#     |   |   @ StatementsNode (location: (2,2)-(2,7))
#     |   |   +-- body: (length: 1)
#     |   |       +-- @ BreakNode (location: (2,2)-(2,7))*
#     |   |           +-- arguments: nil
#     |   +-- rescue_clause: nil
#     |   +-- else_clause: nil
#     |   +-- ensure_clause: nil
i.times do
  break
end

# @ WhileNode (location: (5,0)-(7,3))*
# +-- predicate:
# |   @ TrueNode (location: (5,6)-(5,10))
# +-- statements:
#     @ StatementsNode (location: (6,2)-(6,6))
#     +-- body: (length: 1)
#         +-- @ NextNode (location: (6,2)-(6,6))*
#             +-- arguments: nil
while true
  next
end

バイトコードを確認する

break, next, redoコンパイルでは考慮すべきことが大きく2つあります。

  1. whileの中にいるのか、ブロックの中にいるのか、それ以外か
  2. ensurerescueの有無

breakを例にそれぞれ確認しておきましょう。

以下のコードから分かるように、while内のbreakjump命令に、ブロック内のbreakthrow命令にコンパイルされます。

# 0000 jump                                   4                         (  17)[Li]
# 0002 putnil
# 0003 pop
# 0004 putself                                                          (  18)[Li]
# 0005 putobject                              :a
# 0007 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0009 pop
# 0010 putnil                                                           (  19)[Li]
# 0011 jump                                   22
# 0013 putself                                                          (  20)[Li]
# 0014 putobject                              :b
# 0016 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0018 pop
# 0019 jump                                   4                         (  17)
# 0021 putnil
# 0022 pop
# 0023 putself                                                          (  23)[Li]
# 0024 putobject                              :c
# 0026 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0028 leave
while true
  p :a
  break
  p :b
end

p :c
# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(7,4)>
# == catch table
# | catch type: break  st: 0000 ed: 0005 sp: 0000 cont: 0005
# | == disasm: #<ISeq:block in <main>@test.rb:1 (1,9)-(5,3)>
# | == catch table
# | | catch type: redo   st: 0001 ed: 0016 sp: 0000 cont: 0001
# | | catch type: next   st: 0001 ed: 0016 sp: 0000 cont: 0016
# | |------------------------------------------------------------------------
# | 0000 nop                                                              (   1)[Bc]
# | 0001 putself                                                          (   2)[Li]
# | 0002 putobject                              :c
# | 0004 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# | 0006 pop
# | 0007 putnil                                                           (   3)[Li]
# | 0008 throw                                  2
# | 0010 pop
# | 0011 putself                                                          (   4)[Li]
# | 0012 putobject                              :d
# | 0014 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# | 0016 leave                                                            (   5)[Br]
# |------------------------------------------------------------------------
# 0000 putobject                              10                        (   1)[Li]
# 0002 send                                   <calldata!mid:times, argc:0>, block in <main>
# 0005 pop
# 0006 putself                                                          (   7)[Li]
# 0007 putobject                              :e
# 0009 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0011 leave
10.times do
  p :c
  break
  p :d
end

p :e

次に、ensurerescueが関係するケースをみておきます。

ensureがある場合には例外が発生しなかった場合にもensureを実行する必要があるため、p :aを実行したあとにp :ensureを実行してからjump(break)するというバイトコードが生成されます(0011から0016を参照)。

# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(11,4)>
# == catch table
# | catch type: ensure st: 0004 ed: 0011 sp: 0000 cont: 0031
# | == disasm: #<ISeq:ensure in <main>@test.rb:7 (7,4)-(7,13)>
# | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
# | [ 1] "$!"@0
# | 0000 putself                                                          (   7)[Li]
# | 0001 putobject                              :ensure
# | 0003 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# | 0005 pop
# | 0006 getlocal_WC_0                          "$!"@0
# | 0008 throw                                  0
# | catch type: ensure st: 0017 ed: 0025 sp: 0000 cont: 0031
# | catch type: break  st: 0004 ed: 0034 sp: 0000 cont: 0034
# | catch type: next   st: 0004 ed: 0034 sp: 0000 cont: 0003
# | catch type: redo   st: 0004 ed: 0034 sp: 0000 cont: 0004
# |------------------------------------------------------------------------
# 0000 jump                                   4                         (   1)[Li]
# 0002 putnil
# 0003 pop
# 0004 putself                                                          (   3)[Li]
# 0005 putobject                              :a
# 0007 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0009 pop
# 0010 putnil                                                           (   4)[Li]
# 0011 putself                                                          (   7)[Li]
# 0012 putobject                              :ensure
# 0014 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0016 pop
# 0017 jump                                   34                        (   4)
# 0019 putself                                                          (   5)[Li]
# 0020 putobject                              :b
# 0022 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0024 pop
# 0025 putself                                                          (   7)[Li]
# 0026 putobject                              :ensure
# 0028 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0030 pop
# 0031 jump                                   4                         (   1)
# 0033 putnil
# 0034 pop
# 0035 putself                                                          (  11)[Li]
# 0036 putobject                              :c
# 0038 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0040 leave

while true
  begin
    p :a
    break
    p :b
  ensure
    p :ensure
  end
end

p :c

一方でensurerescueがある場合にはthrow(0011)をするバイトコードが生成されます。

# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(13,4)>
# == catch table
# | catch type: rescue st: 0004 ed: 0019 sp: 0000 cont: 0020
# | == disasm: #<ISeq:rescue in <main>@test.rb:6 (6,2)-(7,13)>
# | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
# | [ 1] "$!"@0
# | 0000 getlocal_WC_0                          "$!"@0                    (   7)
# | 0002 putobject                              StandardError
# | 0004 checkmatch                             3
# | 0006 branchunless                           14
# | 0008 putself                                [LiRs]
# | 0009 putobject                              :rescue
# | 0011 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# | 0013 leave
# | 0014 getlocal_WC_0                          "$!"@0
# | 0016 throw                                  0
# | catch type: retry  st: 0019 ed: 0020 sp: 0000 cont: 0004
# | catch type: ensure st: 0004 ed: 0021 sp: 0000 cont: 0027
# | == disasm: #<ISeq:ensure in <main>@test.rb:9 (9,4)-(9,13)>
# | local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
# | [ 1] "$!"@0
# | 0000 putself                                                          (   9)[Li]
# | 0001 putobject                              :ensure
# | 0003 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# | 0005 pop
# | 0006 getlocal_WC_0                          "$!"@0
# | 0008 throw                                  0
# | catch type: break  st: 0004 ed: 0030 sp: 0000 cont: 0030
# | catch type: next   st: 0004 ed: 0030 sp: 0000 cont: 0003
# | catch type: redo   st: 0004 ed: 0030 sp: 0000 cont: 0004
# |------------------------------------------------------------------------
# 0000 jump                                   4                         (   1)[Li]
# 0002 putnil
# 0003 pop
# 0004 putself                                                          (   3)[Li]
# 0005 putobject                              :a
# 0007 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0009 pop
# 0010 putnil                                                           (   4)[Li]
# 0011 throw                                  32770
# 0013 pop
# 0014 putself                                                          (   5)[Li]
# 0015 putobject                              :b
# 0017 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0019 nop                                                              (   6)
# 0020 pop
# 0021 putself                                                          (   9)[Li]
# 0022 putobject                              :ensure
# 0024 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0026 pop
# 0027 jump                                   4                         (   1)
# 0029 putnil
# 0030 pop
# 0031 putself                                                          (  13)[Li]
# 0032 putobject                              :c
# 0034 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0036 leave

while true
  begin
    p :a
    break
    p :b
  rescue
    p :rescue
  ensure
    p :ensure
  end
end

p :c

以上をふまえてcompile_break関数を眺めてみます。

static int
compile_break(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped)
{
    const NODE *line_node = node;
    unsigned long throw_flag = 0;

    if (ISEQ_COMPILE_DATA(iseq)->redo_label != 0 && can_add_ensure_iseq(iseq)) {
        /* while/until */
        LABEL *splabel = NEW_LABEL(0);
        ADD_LABEL(ret, splabel);
        ADD_ADJUST(ret, line_node, ISEQ_COMPILE_DATA(iseq)->redo_label);
        CHECK(COMPILE_(ret, "break val (while/until)", RNODE_BREAK(node)->nd_stts,
                       ISEQ_COMPILE_DATA(iseq)->loopval_popped));
        add_ensure_iseq(ret, iseq, 0);
        ADD_INSNL(ret, line_node, jump, ISEQ_COMPILE_DATA(iseq)->end_label);
        ADD_ADJUST_RESTORE(ret, splabel);

        if (!popped) {
            ADD_INSN(ret, line_node, putnil);
        }
    }
    else {
        const rb_iseq_t *ip = iseq;

        while (ip) {
            if (!ISEQ_COMPILE_DATA(ip)) {
                ip = 0;
                break;
            }

            if (ISEQ_COMPILE_DATA(ip)->redo_label != 0) {
                throw_flag = VM_THROW_NO_ESCAPE_FLAG;
            }
            else if (ISEQ_BODY(ip)->type == ISEQ_TYPE_BLOCK) {
                throw_flag = 0;
            }
            else if (ISEQ_BODY(ip)->type == ISEQ_TYPE_EVAL) {
                COMPILE_ERROR(ERROR_ARGS "Can't escape from eval with break");
                return COMPILE_NG;
            }
            else {
                ip = ISEQ_BODY(ip)->parent_iseq;
                continue;
            }

            /* escape from block */
            CHECK(COMPILE(ret, "break val (block)", RNODE_BREAK(node)->nd_stts));
            ADD_INSN1(ret, line_node, throw, INT2FIX(throw_flag | TAG_BREAK));
            if (popped) {
                ADD_INSN(ret, line_node, pop);
            }
            return COMPILE_OK;
        }
        COMPILE_ERROR(ERROR_ARGS "Invalid break");
        return COMPILE_NG;
    }
    return COMPILE_OK;
}

whileコンパイル中かどうかはISEQ_COMPILE_DATA(iseq)->redo_labelをチェックすることでわかります。 またブロックのコンパイル中かどうかは、それにあわせてISEQ_COMPILE_DATA(iseq)->end_labelをチェックすることで判別可能です1

ensurerescueについてはcan_add_ensure_iseq関数でチェックしています。

static bool
can_add_ensure_iseq(const rb_iseq_t *iseq)
{
    struct iseq_compile_data_ensure_node_stack *e;
    if (ISEQ_COMPILE_DATA(iseq)->in_rescue && (e = ISEQ_COMPILE_DATA(iseq)->ensure_node_stack) != NULL) {
        while (e) {
            if (e->ensure_node) return false;
            e = e->prev;
        }
    }
    return true;
}

ensure_node_stackはwhileのコンパイル時とensureをもつstatementsのコンパイル時にpush_ensure_entry関数が呼び出されることで設定されますが、whileの場合はensure_nodeがNULL、ensureの場合はノードがセットされます。 なのでensure_nodeを見ればensureかどうか判別できます。

またrescueがある場合、begin ... rescueまでの部分をコンパイルする際にはin_rescueがtrueになっていますので、この値をチェックすることでrescueの有無がわかります。

compile.cを書き換える

while内部かブロック内部か、rescueensureがあるのかに応じたバイトコードの書き分けは必要ですが、ノードの変更に伴う修正はほとんど必要ありません。 たとえばcompile_break関数であればbreak ......の部分のコンパイルにさいしてアクセスするフィールドが変わるのに対応するだけです。

@@ -9082,7 +9082,7 @@ compile_break(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, i
         LABEL *splabel = NEW_LABEL(0);
         ADD_LABEL(ret, splabel);
         ADD_ADJUST(ret, line_node, ISEQ_COMPILE_DATA(iseq)->redo_label);
-        CHECK(COMPILE_(ret, "break val (while/until)", RNODE_BREAK(node)->nd_stts,
+        CHECK(COMPILE_(ret, "break val (while/until)", RNODE(RB_NODE_BREAK(node)->arguments),
                        ISEQ_COMPILE_DATA(iseq)->loopval_popped));
         add_ensure_iseq(ret, iseq, 0);
         ADD_INSNL(ret, line_node, jump, ISEQ_COMPILE_DATA(iseq)->end_label);
@@ -9117,7 +9117,7 @@ compile_break(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, i
             }

             /* escape from block */
-            CHECK(COMPILE(ret, "break val (block)", RNODE_BREAK(node)->nd_stts));
+            CHECK(COMPILE(ret, "break val (block)", RNODE(RB_NODE_BREAK(node)->arguments)));
             ADD_INSN1(ret, line_node, throw, INT2FIX(throw_flag | TAG_BREAK));
             if (popped) {
                 ADD_INSN(ret, line_node, pop);

compile_nextにも同様の修正を入れます2

minirubyをbuildしてバイトコードが問題なく生成されていることを確認します。

while true
  p :a
  break
  p :b
end

num.times do
  p :c
  break
  p :d
end

== disasm: #<ISeq:<main>@../../test.rb:1 (1,0)-(11,3)>
# == catch table
# | catch type: break  st: 0004 ed: 0022 sp: 0000 cont: 0022
# | catch type: next   st: 0004 ed: 0022 sp: 0000 cont: 0003
# | catch type: redo   st: 0004 ed: 0022 sp: 0000 cont: 0004
# | catch type: break  st: 0023 ed: 0029 sp: 0000 cont: 0029
# | == disasm: #<ISeq:block in <main>@../../test.rb:7 (7,12)-(10,6)>
# | == catch table
# | | catch type: redo   st: 0001 ed: 0016 sp: 0000 cont: 0001
# | | catch type: next   st: 0001 ed: 0016 sp: 0000 cont: 0016
# | |------------------------------------------------------------------------
# | 0000 nop                                                              (   7)[Bc]
# | 0001 putself                                                          (   8)[Li]
# | 0002 putobject                              :c
# | 0004 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# | 0006 pop
# | 0007 putnil                                                           (   9)[Li]
# | 0008 throw                                  2
# | 0010 pop
# | 0011 putself                                                          (  10)[Li]
# | 0012 putobject                              :d
# | 0014 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# | 0016 leave                                  [Br]
# |------------------------------------------------------------------------
# 0000 jump                                   4                         (   1)[Li]
# 0002 putnil
# 0003 pop
# 0004 putself                                                          (   2)[Li]
# 0005 putobject                              :a
# 0007 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0009 pop
# 0010 putnil                                                           (   3)[Li]
# 0011 jump                                   22
# 0013 putself                                                          (   4)[Li]
# 0014 putobject                              :b
# 0016 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0018 pop
# 0019 jump                                   4                         (   1)
# 0021 putnil
# 0022 pop
# 0023 putself                                                          (   7)[Li]
# 0024 opt_send_without_block                 <calldata!mid:num, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0026 send                                   <calldata!mid:times, argc:0>, block in <main>
# 0029 leave

まとめ

今日の成果です。

  • break, next, redoに対応した

次回はsuperとyieldの対応をしたいと思います。


  1. 詳しくはcompile_next関数をみてください
  2. redoは引数をとらないので、修正が必要ありません



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

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