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


Ruby Parser開発日誌 (24-21) - parse.yが生成するノードを変える ー retry, while, until

21日目: 制御構文に対応する

前回はbegin ... rescue ... endに対応したので、今回はretryをはじめとする制御構文を書き換えていきましょう。

retryに対応する

rertyは引数を取らないため、そのノードもシンプルです。

begin
  p :begin
rescue
  p :rescue
  retry
end

# Before
# @ NODE_RETRY (id: 6, line: 5, location: (5,2)-(5,7))*

# After
# RetryNode (location: (5,2)-(5,7))

parse.yの書き換えも難しい部分は特にありません。

@@ -5062,7 +5064,7 @@ primary           : inline_primary
                           case after_ensure: yyerror1(&@1, "Invalid retry after ensure"); break;
                         }
                     }
-                    $$ = NEW_RETRY(&@$);
+                    $$ = NEW_RB_RETRY(&@$);
                 /*% ripper: retry! %*/
                 }
             ;

compile.cも既存のcompile_retry関数をそのまま使うことができます。

@@ -11615,6 +11616,10 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no
         break;
       }

+      case RB_RETRY_NODE: {
+        CHECK(compile_retry(iseq, ret, node, popped));
+        break;
+      }
       case RB_BEGIN_NODE: {
         CHECK(compile_begin(iseq, ret, node, popped));
         break;

compile_retry関数では現在のISeqがrescueに対応するISeqであることを確認して、putnilthrow命令を生成するだけです。

static int
compile_retry(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped)
{
    const NODE *line_node = node;

    if (ISEQ_BODY(iseq)->type == ISEQ_TYPE_RESCUE) {
        ADD_INSN(ret, line_node, putnil);
        ADD_INSN1(ret, line_node, throw, INT2FIX(TAG_RETRY));

        if (popped) {
            ADD_INSN(ret, line_node, pop);
        }
    }
    else {
        COMPILE_ERROR(ERROR_ARGS "Invalid retry");
        return COMPILE_NG;
    }
    return COMPILE_OK;
}

コンパイル結果もよさそうです。

begin
  p :begin
rescue
  p :rescue
  retry
end

# == catch table
# | catch type: rescue st: 0000 ed: 0005 sp: 0000 cont: 0006
# | == disasm: #<ISeq:rescue in <main>@../../test.rb:3 (3,0)-(5,7)>
# | 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                    (   3)
# | 0002 putobject                              StandardError
# | 0004 checkmatch                             3
# | 0006 branchunless                           18
# | 0008 putself                                                          (   4)[LiRs]
# | 0009 putobject                              :rescue
# | 0011 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# | 0013 pop
# | 0014 putnil                                                           (   5)[Li]
# | 0015 throw                                  4
# | 0017 leave                                                            (   3)
# | 0018 getlocal_WC_0                          "$!"@0
# | 0020 throw                                  0

nextに対応する

次にnextに対応していきましょう。 ところでnextwhileの中とブロックの中のときでコンパイル結果が変わりますが、そもそもまだwhileuntilをサポートしていないことに今気がつきました。

whileとuntilに対応する

ということで、先にwhileuntilを書き換えましょう。

whileには以下のように3つの異なる書き方があります。 後置whileかつbegin ... endのケースでは最初に1度begin ... endを実行してからloopに入ります。

while false
  p :while1
end

p :while2 while false

begin
  p :while3
end while false

#=> :while3

書き換え前後のノードは以下の通りです。

while false
  p :while1
end

#  @ NODE_WHILE (id: 4, line: 1, location: (1,0)-(3,3))*
#  +- nd_state: 1 (while-end)
#  +- nd_cond:
#  +- nd_body:

# +-- @ WhileNode (location: (1,0)-(3,3))
# |   +-- LoopFlags: nil
# |   +-- predicate:
# |   +-- statements:

p :while2 while false

#  @ NODE_WHILE (id: 9, line: 5, location: (5,0)-(5,21))*
#  +- nd_state: 1 (while-end)
#  +- nd_cond:
#  +- nd_body:

# +-- @ WhileNode (location: (5,0)-(5,21))
# |   +-- LoopFlags: nil
# |   +-- predicate:
# |   +-- statements:

begin
  p :while3
end while false

#  @ NODE_WHILE (id: 17, line: 7, location: (7,0)-(9,15))*
#  +- nd_state: 0 (begin-end-while)
#  +- nd_cond:
#  +- nd_body:

# +-- @ WhileNode (location: (7,0)-(9,15))
# |   +-- LoopFlags: begin_modifier
# |   +-- predicate:
# |   +-- statements:

後置rescueの場合には専用のノードを割り当てていたのと対照的に、書き換え後も後置whileかどうかに関わらず常にWhileNodeを使うようです。 その結果、本来1つのstatemetしかとれない後置whileの場合でも、StatementsNodeでラップする必要があることに注意します。

また3番目のケースのwhileであるかどうかは、begin_modifierというフラグで判別するようです。

parse.yでは使用するマクロを書き換えることで対応できます。

@@ -3692,22 +3700,22 @@ stmt            : keyword_alias fitem {SET_LEX_STATE(EXPR_FNAME|EXPR_FITEM);} fitem
                 | stmt modifier_while expr_value
                     {
                         clear_block_exit(p, false);
-                        if ($1 && nd_type_p($1, NODE_BEGIN)) {
-                            $$ = NEW_WHILE(cond(p, $3, &@3), RNODE_BEGIN($1)->nd_body, 0, &@$, &@2, &NULL_LOC);
+                        if ($1 && nd_type_p($1, RB_BEGIN_NODE)) {
+                            $$ = NEW_RB_WHILE(cond(p, $3, &@3), NEW_RB_STATEMENTS($1, &@1), RB_LOOP_FLAGS_BEGIN_MODIFIER, &@$, &@2, &NULL_LOC);
                         }
                         else {
-                            $$ = NEW_WHILE(cond(p, $3, &@3), $1, 1, &@$, &@2, &NULL_LOC);
+                            $$ = NEW_RB_WHILE(cond(p, $3, &@3), NEW_RB_STATEMENTS($1, &@1), 0, &@$, &@2, &NULL_LOC);
                         }
                     /*% ripper: while_mod!($:3, $:1) %*/

whileとuntilをコンパイルする

まずは生成されるバイトコードをみてみましょう。

while cond
  p :while1
end

# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(3,3)>

# whileの条件にjumpする
# 0000 jump                                   12                        (   1)[Li]
# 0002 putnil
# 0003 pop
# 0004 jump                                   12

# whileのbody
# 0006 putself                                                          (   2)[Li]
# 0007 putobject                              :while1
# 0009 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0011 pop

# whileの条件をチェックして次のloopに入る、もしくはloopを抜ける
# 0012 putself                                                          (   1)
# 0013 opt_send_without_block                 <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0015 branchif                               6

# while loopより後の部分
# 0017 putnil
# 0018 leave                                                            (   2)

bodyの部分のバイトコードのあとにloopを継続するかどうかをチェックするバイトコードが置かれていて、一番最初は継続のチェックをするバイトコードまでjumpしていることがわかります。

begin ... end while condの場合は、最初のjump先が条件式の部分(0012)ではなくbodyの部分(0006)になります。

begin
  p :while3
end while cond

# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(3,14)>
# 0000 jump                                   6                         (   1)[Li]
# 0002 putnil
# 0003 pop
# 0004 jump                                   12
# 0006 putself                                                          (   2)[Li]
# 0007 putobject                              :while3
# 0009 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0011 pop
# 0012 putself                                                          (   3)
# 0013 opt_send_without_block                 <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0015 branchif                               6
# 0017 putnil                                                           (   1)
# 0018 leave                                                            (   2)

whileにしろuntilにしろ、それらのノードのコンパイルcompile_loop関数が行っています。 書き換え自体は構造体の変化に合わせてマクロを変える程度なので、ここでは書き換え前の関数を眺めるに留めます。

static int
compile_loop(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped, const enum node_type type)
{
    const int line = (int)nd_line(node);
    const NODE *line_node = node;

    LABEL *prev_start_label = ISEQ_COMPILE_DATA(iseq)->start_label;
    LABEL *prev_end_label = ISEQ_COMPILE_DATA(iseq)->end_label;
    LABEL *prev_redo_label = ISEQ_COMPILE_DATA(iseq)->redo_label;
    int prev_loopval_popped = ISEQ_COMPILE_DATA(iseq)->loopval_popped;
    VALUE branches = Qfalse;

    struct iseq_compile_data_ensure_node_stack enl;

    // 各種labelを設定する
    LABEL *next_label = ISEQ_COMPILE_DATA(iseq)->start_label = NEW_LABEL(line); /* next  */
    LABEL *redo_label = ISEQ_COMPILE_DATA(iseq)->redo_label = NEW_LABEL(line);  /* redo  */
    LABEL *break_label = ISEQ_COMPILE_DATA(iseq)->end_label = NEW_LABEL(line);  /* break */
    LABEL *end_label = NEW_LABEL(line);
    LABEL *adjust_label = NEW_LABEL(line);

    LABEL *next_catch_label = NEW_LABEL(line);
    LABEL *tmp_label = NULL;

    ISEQ_COMPILE_DATA(iseq)->loopval_popped = 0;
    // while自体のコンパイルには影響しないのでまた今度
    push_ensure_entry(iseq, &enl, NULL, NULL);

    // `begin ... end while`かどうかで最初のjump先を変えている
    if (RNODE_WHILE(node)->nd_state == 1) {
        ADD_INSNL(ret, line_node, jump, next_label);
    }
    else {
        tmp_label = NEW_LABEL(line);
        ADD_INSNL(ret, line_node, jump, tmp_label);
    }
    ADD_LABEL(ret, adjust_label);
    ADD_INSN(ret, line_node, putnil);
    ADD_LABEL(ret, next_catch_label);
    ADD_INSN(ret, line_node, pop);
    ADD_INSNL(ret, line_node, jump, next_label);
    if (tmp_label) ADD_LABEL(ret, tmp_label);

    ADD_LABEL(ret, redo_label);
    branches = decl_branch_base(iseq, PTR2NUM(node), nd_code_loc(node), type == NODE_WHILE ? "while" : "until");

    const NODE *const coverage_node = RNODE_WHILE(node)->nd_body ? RNODE_WHILE(node)->nd_body : node;
    add_trace_branch_coverage(
        iseq,
        ret,
        nd_code_loc(coverage_node),
        nd_node_id(coverage_node),
        0,
        "body",
        branches);

    // body部分をコンパイルする
    CHECK(COMPILE_POPPED(ret, "while body", RNODE_WHILE(node)->nd_body));
    ADD_LABEL(ret, next_label); /* next */

    // condition部分をコンパイルする
    // whileとuntilで条件がtrueのときのjump先が逆になる
    if (type == NODE_WHILE) {
        CHECK(compile_branch_condition(iseq, ret, RNODE_WHILE(node)->nd_cond,
                                       redo_label, end_label));
    }
    else {
        /* until */
        CHECK(compile_branch_condition(iseq, ret, RNODE_WHILE(node)->nd_cond,
                                       end_label, redo_label));
    }

    ADD_LABEL(ret, end_label);
    ADD_ADJUST_RESTORE(ret, adjust_label);

    // 書き換え後はflagの有無になるのでこの分岐は消える
    if (UNDEF_P(RNODE_WHILE(node)->nd_state)) {
        /* ADD_INSN(ret, line_node, putundef); */
        COMPILE_ERROR(ERROR_ARGS "unsupported: putundef");
        return COMPILE_NG;
    }
    else {
        ADD_INSN(ret, line_node, putnil);
    }

    ADD_LABEL(ret, break_label);    /* break */

    if (popped) {
        ADD_INSN(ret, line_node, pop);
    }

    // break, next, redo用のcatch tableを生成する
    ADD_CATCH_ENTRY(CATCH_TYPE_BREAK, redo_label, break_label, NULL,
                    break_label);
    ADD_CATCH_ENTRY(CATCH_TYPE_NEXT, redo_label, break_label, NULL,
                    next_catch_label);
    ADD_CATCH_ENTRY(CATCH_TYPE_REDO, redo_label, break_label, NULL,
                    ISEQ_COMPILE_DATA(iseq)->redo_label);

    // 各種labelを関数呼び出し前の状態にもどす
    ISEQ_COMPILE_DATA(iseq)->start_label = prev_start_label;
    ISEQ_COMPILE_DATA(iseq)->end_label = prev_end_label;
    ISEQ_COMPILE_DATA(iseq)->redo_label = prev_redo_label;
    ISEQ_COMPILE_DATA(iseq)->loopval_popped = prev_loopval_popped;
    ISEQ_COMPILE_DATA(iseq)->ensure_node_stack = ISEQ_COMPILE_DATA(iseq)->ensure_node_stack->prev;
    return COMPILE_OK;
}

基本的には生成されるバイトコードの順に沿って、bodyとconditionをコンパイルしています。 難しいのはISEQ_COMPILE_DATA(iseq)のセットアップやpush_ensure_entryの部分ですが、これはnextなどをコンパイルするときに使用するので、そのときがきたら詳しくみることにします。

compile.c書き換え後のバイトコードを確認します。

while cond
  p :while1
end
# 0000 jump                                   12                        (   1)[Li]
# 0002 putnil
# 0003 pop
# 0004 jump                                   12
# 0006 putself                                                          (   2)[Li]
# 0007 putobject                              :while1
# 0009 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0011 pop
# 0012 putself                                                          (   1)
# 0013 opt_send_without_block                 <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0015 branchif                               6
# 0017 putnil
# 0018 pop

begin
  p :while3
end while cond
# 0019 jump                                   25                        (   5)[Li]
# 0021 putnil
# 0022 pop
# 0023 jump                                   31
# 0025 putself                                                          (   6)[Li]
# 0026 putobject                              :while3
# 0028 opt_send_without_block                 <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE>
# 0030 pop
# 0031 putself                                                          (   7)
# 0032 opt_send_without_block                 <calldata!mid:cond, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0034 branchif                               25
# 0036 putnil                                                           (   5)
# 0037 leave                                                            (   6)

良さそうですね。

まとめ

今日の成果です。

  • retryコンパイルできるようにした
  • whileとuntilを書き換えてないことに気がついたので、その対応をした

次回こそnext, break, redoに取り組みたいと思います。




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

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