以下の内容はhttps://yui-knk.hatenablog.com/entry/2025/12/12/212854より取得しました。


Ruby Parser開発日誌 (24-1) - parse.yが生成するノードを変える ー もしくは大規模なコードの書き換えをどう進めるか

不思議なもので、ここ数年は12月になるとCRuby向けに大きめなパッチを書くことが続いています。 今年はparse.yが生成するノードを一気に置き換えるかなという気持ちになり、粛々と作業をしています。 parserの生成するノードを変更するということは、当然そのノードを扱うコンパイラ(ファイルでいうとcompile.c)も書き換える必要があります。 できればparserだけいじって生きていきたいんだけどなと思ったりもしますが、コンパイラコンパイラでいじってみるといろいろ知らなかったことがあって面白いんですよ。

という話をとあるコミッターにしたところ、「その感動や楽しさ、その時に考えたことをちゃんと記事にして公開しないとダメですよ。パッチが完成してから記事をブラッシュアップして公開しようとか考えていると思うんですが、それだと新鮮さがなくなるのでダメです。その日その日の出来事をちゃんと記事にして日々公開するのです」と無茶苦茶なことを言われてしまいました。

そんなこんなで記事を書くことになりました。 日々の作業日誌という面が強いので、あっちに行ったり、こっちに行ったりするかと思います。 また通常のブログ記事よりは説明を端折ったりすることもあるかと思いますが、なにかの参考になれば幸いです。

0日目: ゴールを設定する

なにはともあれ作業を始める前にゴールを考えておきます。 ゴールは達成が簡単でかつ、見栄えがするものだと良いでしょう。 ということで、以下の2点を達成することを目指します。

  1. Rubyの世界から触ることができるノードがPrismノードになっている
  2. ruby/rubyのCIが通る

1日目: シンプルなリテラルと制御構文

最も簡単なスクリプト

それでは書き換えをはじめていきましょう。 Rubyオブジェクト指向プログラミング言語なのだから、まずはクラス定義のノードを変更しようとか、メソッド定義こそが重要なのだからそのノードを変更しようとか、そういう難しいことをすると途中で挫折するリスクが高まります。 変数も難しいのでいきなり手をつけてはダメです。

なので最初の一歩として可能な限り簡単なRubyスクリプトを対象にノードを変更してみましょう。 可能な限り簡単なスクリプトというと、つまり空の文字列ですがこれだとちょっと味気がないですね。 なので最も要素の少ないリテラルであるnilを対象にします。 このくらいの欲なら許されるでしょう。

parse.yの変更

実際にnilからどのような構文木が生成されるか確認してみると、NilNode以外にもProgramNodeStatementsNodeが生成されます。 ProgramNodeは生成される構文木のルートとなるノードで、StatementsNodeは複数のノードを束ねる働きをします。 nilを扱いたいだけなのにいきなり3種類のノードが出てきました。 しかしこれはどのようなスクリプトでも出てくるノードのなので諦めて3種類のノードと向き合うことにします。

$ ruby --parser=prism --dump=p -e 'nil'
@ ProgramNode (location: (1,0)-(1,3))
+-- locals: []
+-- statements:
    @ StatementsNode (location: (1,0)-(1,3))
    +-- body: (length: 1)
        +-- @ NilNode (location: (1,0)-(1,3))

それぞれのノードを生成するためのマクロ(と対応する関数)をparse.yに定義して、そちらを使うように書き換えていきます。

  • NEW_RB_PROGRAM
  • NEW_RB_STATEMENTS
  • NEW_RB_NIL

parse.yの生成規則でプログラム全体を表すのはprogramという規則なので、いままでNODE_SCOPEを作っていたところでProgramNodeを作るようにします。

@@ -3176,7 +3201,8 @@ program           :  {
                             node = remove_begin(node);
                             void_expr(p, node);
                         }
-                        p->eval_tree = NEW_SCOPE(0, block_append(p, p->eval_tree, $2), NULL, &@$);
+                        // p->eval_tree = NEW_SCOPE(0, block_append(p, p->eval_tree, $2), NULL, &@$);
+                        p->eval_tree = NEW_RB_PROGRAM($2, &@$);
                     /*% ripper[final]: program!($:2) %*/
                         local_pop(p);
                     }

NilNodegettableという関数の中で生成されているので、そこを書きえます。

@@ -12987,7 +13134,8 @@ gettable(struct parser_params *p, ID id, const YYLTYPE *loc)
       case keyword_self:
         return NEW_SELF(loc);
       case keyword_nil:
-        return NEW_NIL(loc);
+        // return NEW_NIL(loc);
+        return NEW_RB_NIL(loc);
       case keyword_true:
         return NEW_TRUE(loc);
       case keyword_false:

compile.cの変更

ノードを変更したのでコンパイラも変更しないといけません。 compile.cではノードの種類に応じてrb_iseq_compile_nodeもしくはiseq_compile_each0を変更することになります。 ノードのなかにはiseq(InstructionSequence)を作るものと作らないものがあります。 iseqをつくるノードの場合はrb_iseq_compile_node、そうでない場合はiseq_compile_each0コンパイルの処理が書かれています。 今回のケースではProgramNodeではiseqをつくり、StatementsNodeNilNodeでは新しくiseqはつくりません1

iseqを作った場合には引数とローカル変数を格納する領域(struct rb_iseq_constant_bodystruct rb_iseq_parameters param)を設定する必要があります。 ProgramNodeにはローカル変数はありますが、引数はありません。 なのでローカル変数をlocalsフィールドから取得してiseq_set_local_tableに渡します。

// rb_iseq_compile_node

/* iseq type of top, method, class, block */
switch (nd_type(node)) {
  case RB_PROGRAM_NODE:
    {
        const rb_program_node_t *cast = (const rb_program_node_t *) node;
        locals = cast->locals;
        body = (NODE *) cast->statements;
        break;
    }
...
}
iseq_set_local_table(iseq, locals, args);
iseq_set_arguments(iseq, ret, args);
iseq_set_parameters_lvar_state(iseq);

NilNodeは既存のコードをそのまま流用できます。

// iseq_compile_each0

// Before
case NODE_NIL:{
  if (!popped) {
      ADD_INSN(ret, node, putnil);
  }
  break;
}

// After
case RB_NIL_NODE: {
  if (!popped) {
      ADD_INSN(ret, node, putnil);
  }
  break;
}

最後に残ったStatementsNodeですが、これはもともとのNODE_BLOCKに対応しています。 ブロックといっても10.times do |i| expr endのブロックではなく、コードブロックを表していたノードです2

$ ruby --parser=parse.y --dump=p -e 'nil; nil'
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (id: 4, line: 1, location: (1,0)-(1,8))
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_BLOCK (id: 2, line: 1, location: (1,0)-(1,8))
#     +- nd_head (1):
#     |   @ NODE_NIL (id: 0, line: 1, location: (1,0)-(1,3))*
#     +- nd_head (2):
#         @ NODE_NIL (id: 1, line: 1, location: (1,5)-(1,8))*

ruby --parser=prism --dump=p -e 'nil; nil'
@ ProgramNode (location: (1,0)-(1,8))
+-- locals: []
+-- statements:
    @ StatementsNode (location: (1,0)-(1,8))
    +-- body: (length: 2)
        +-- @ NilNode (location: (1,0)-(1,3))
        +-- @ NilNode (location: (1,5)-(1,8))

データ構造的な違いとしてはNODE_BLOCKがリスト構造であるのに対して、StatementsNodeは配列で他のノードを保持していることです。 NODE_BLOCKでは実際のノード(例えばNODE_NIL)をnd_head、次の要素をラップしているNODE_BLOCKnd_nextで管理しています。 一方でStatementsNodebodyが他のノードへのポインタの配列になっています。

またstatementがまったくないときのノードの構造にも違いがあって、NODE_BLOCKの場合は要素がないときはそもそもNODE_BLOCKが作られないのに対して、StatementsNodeの場合はbodyが空配列であるノードが作られます。 要素がないときはputnilコンパイルされてほしいことを踏まえると、StatementsNodeコンパイルするコードは以下のようになります。 body->sizeに応じて分岐しているのは、空配列のときのケースを考慮しているためです。

最後の要素かどうかで呼び出しているマクロが異なるのはpopしてよいかどうかが最後の要素とそれ以外で異なるからです。 pop(compile.cでいうpopped引数)についてはまたどこかで詳しく取り上げるつもりです。 生成されるバイトコードを比較して差がなければよいので、コードでわからないところが多少あっても一旦飲み込んで進むことも大切です。 しばらくいじっていると、そのうち答えがわかるものです。

      case RB_STATEMENTS_NODE: {
        // A list of statements.
        const rb_statements_node_t *cast = (const rb_statements_node_t *) node;
        const rb_node_list2_t *body = &cast->body;

        if (body && body->size > 0) {
            for (size_t index = 0; index < body->size - 1; index++) {
                COMPILE_POPPED(ret, "statements body", body->nodes[index]);
            }
            CHECK(COMPILE_(ret, "statements body", body->nodes[body->size - 1], popped));
        }
        else {
            ADD_INSN(ret, node, putnil);
        }
        break;
      }

参考にまでにNODE_BLOCKコンパイルするコードものせておきます。 こちらはcompile_blockという関数に切り出されています。 StatementsNodeの処理を関数に切り出すかどうかは今後の成り行きに任せます。

static int
compile_block(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, int popped)
{
    while (node && nd_type_p(node, NODE_BLOCK)) {
        CHECK(COMPILE_(ret, "BLOCK body", RNODE_BLOCK(node)->nd_head,
                       (RNODE_BLOCK(node)->nd_next ? 1 : popped)));
        node = RNODE_BLOCK(node)->nd_next;
    }
    if (node) {
        CHECK(COMPILE_(ret, "BLOCK next", RNODE_BLOCK(node)->nd_next, popped));
    }
    return COMPILE_OK;
}

このような変更をすることで無事nilに対してバイトコードが生成できるようになりました。

$ ./miniruby --parser=parse.y --dump=p,i -e 'nil'
@ ProgramNode (location: (1,0)-(1,3))
+-- locals: []
+-- statements:
    @ StatementsNode (location: (1,0)-(1,3))
    +-- body: (length: 1)
        +-- @ NilNode (location: (1,0)-(1,3))*
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,3)>
0000 putnil                                                           (   1)[Li]
0001 leave

その他のシンプルなリテラル (self, true, false, __FILE__, __LINE__, __ENCODING__)

nil以外にもself, true, falseなどはノードが値を持たないので、nilと同様に書き換えることができます。 __FILE__に相当するSourceFileNodeはファイル名をfilepathというフィールドに保持しているので、変更前のノードと同様にノードの生成時にファイル名を渡すようにします。

static rb_source_file_node_t *
rb_new_node_source_file_new(struct parser_params *p, VALUE str, const YYLTYPE *loc)
{
    rb_source_file_node_t *n = RB_NEW_NODE_NEWNODE((enum rb_node_type)RB_SOURCE_FILE_NODE, rb_source_file_node_t, loc);
    n->filepath = rb_str_to_parser_string(p, str);

    return n;
}

そのほかコンパイラの処理などはとくに難しい箇所はないので割愛します。

ifとunless

リテラルばかり実装していても飽きてしまうので、この辺で制御構文に取り組んでみましょう。

parse.yの変更

ifunlessに対応するノードはparse.yのnew_ifnew_unlessという関数で作られています。

static NODE*
new_if(struct parser_params *p, NODE *cc, NODE *left, NODE *right, const YYLTYPE *loc, const YYLTYPE* if_keyword_loc, const YYLTYPE* then_keyword_loc, const YYLTYPE* end_keyword_loc)
{
    if (!cc) return right;
    cc = cond0(p, cc, COND_IN_COND, loc, true);
    return newline_node(NEW_IF(cc, left, right, loc, if_keyword_loc, then_keyword_loc, end_keyword_loc));
}

ここでcond0という関数をみてみます。 if 1; e; end # => -e:1: warning: literal in conditionといった警告をだすために条件部分のノードの種類に応じて分岐していることがわかります。

static NODE*
cond0(struct parser_params *p, NODE *node, enum cond_type type, const YYLTYPE *loc, bool top)
{
    if (node == 0) return 0;
    if (!(node = nd_once_body(node))) return 0;
    assign_in_cond(p, node);

    switch (nd_type(node)) {
      case NODE_BEGIN:
        RNODE_BEGIN(node)->nd_body = cond0(p, RNODE_BEGIN(node)->nd_body, type, loc, top);
        break;

      case NODE_DSTR:
      case NODE_EVSTR:
      case NODE_STR:
      case NODE_FILE:
        SWITCH_BY_COND_TYPE(type, warn, "string ");
        break;
      ...

数秒考えたのですが、現時点では未対応のノードが多いので一旦cond0を呼ばないようにしてnew_if2を実装し、ここは先に進むようにしておきましょう。 作業量が多いので手が止まらないようにするのが大事。

static rb_node_t *
new_if2(struct parser_params *p, rb_node_t *cc, rb_statements_node_t *left, rb_node_t *right, const YYLTYPE *loc, const YYLTYPE* if_keyword_loc, const YYLTYPE* then_keyword_loc, const YYLTYPE* end_keyword_loc)
{
    if (!cc) return right;
    // TODO
    // cc = cond0(p, cc, COND_IN_COND, loc, true);
    return rb_new_node_newline_node(NEW_RB_IF(cc, left, right, loc, if_keyword_loc, then_keyword_loc, end_keyword_loc));
}

compile.cの変更

compile.cではcompile_ifという関数でifunlessの両方を扱っています。 ifunlessは条件式の値に応じて実行するブランチが逆なだけなので、1つの関数で扱うのは合理的でしょう。 compile_ifのなかでキャストする先の構造体を新しいノードの構造体に書き換えておけばいいでしょう。

一つ注意が必要なのは条件式がNODE_BLOCKかチェックしている箇所です。

static int
compile_if(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped, const enum node_type type)
{
    const NODE *const node_body = type == NODE_IF ? RNODE_IF(node)->nd_body : RNODE_UNLESS(node)->nd_else;
    const NODE *const node_else = type == NODE_IF ? RNODE_IF(node)->nd_else : RNODE_UNLESS(node)->nd_body;

    const int line = nd_line(node);
    const NODE *line_node = node;
    DECL_ANCHOR(cond_seq);
    LABEL *then_label, *else_label, *end_label;
    VALUE branches = Qfalse;

    INIT_ANCHOR(cond_seq);
    then_label = NEW_LABEL(line);
    else_label = NEW_LABEL(line);
    end_label = 0;

    NODE *cond = RNODE_IF(node)->nd_cond;
    // ここ
    if (nd_type(cond) == NODE_BLOCK) {
        cond = RNODE_BLOCK(cond)->nd_head;
    }
    ...

条件式がNODE_BLOCK、つまり複数の式からなるケースを考えてみます。 たとえばif (m1; m2) then 1 else 2 endのようなコードをみてみましょう。

$ ruby --parser=parse.y --dump=p -e 'if (m1; m2) then 1 else 2 end'
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (id: 8, line: 1, location: (1,0)-(1,29))
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_IF (id: 7, line: 1, location: (1,0)-(1,29))*
#     +- nd_cond:
#     |   @ NODE_BLOCK (id: 4, line: 1, location: (1,3)-(1,11))
#     |   +- nd_head (1):
#     |       @ NODE_BLOCK (id: 2, line: 1, location: (1,4)-(1,10))
#     |       +- nd_head (1):
#     |       |   @ NODE_VCALL (id: 0, line: 1, location: (1,4)-(1,6))*
#     |       |   +- nd_mid: :m1
#     |       +- nd_head (2):
#     |           @ NODE_VCALL (id: 1, line: 1, location: (1,8)-(1,10))*
#     |           +- nd_mid: :m2
#     +- nd_body:
#     |   @ NODE_INTEGER (id: 5, line: 1, location: (1,17)-(1,18))*
#     |   +- val: 1
#     +- nd_else:
#     |   @ NODE_INTEGER (id: 6, line: 1, location: (1,24)-(1,25))*
#     |   +- val: 2
#     +- if_keyword_loc: (1,0)-(1,2)
#     +- then_keyword_loc: (1,12)-(1,16)
#     +- end_keyword_loc: (1,26)-(1,29)

たしかにNODE_BLOCKのなかにNODE_BLOCKがネストする構造になっています。 compile_ifのコードはこのネストを解消するためのコードだとわかります。 書き換えた後はParenthesesNodeによってラップされるようになるので、コンパイルParenthesesNodeコンパイル処理にまかせてcompile_ifではとくにネストを解消しなくていいかなと思います3

ruby --parser=prism --dump=p -e 'if (m1; m2) then 1 else 2 end'
@ ProgramNode (location: (1,0)-(1,29))
+-- locals: []
+-- statements:
    @ StatementsNode (location: (1,0)-(1,29))
    +-- body: (length: 1)
        +-- @ IfNode (location: (1,0)-(1,29))
            +-- if_keyword_loc: (1,0)-(1,2) = "if"
            +-- predicate:
            |   @ ParenthesesNode (location: (1,3)-(1,11))
            |   +-- ParenthesesNodeFlags: multiple_statements
            |   +-- body:
            |   |   @ StatementsNode (location: (1,4)-(1,10))
            |   |   +-- body: (length: 2)
            |   |       +-- @ CallNode (location: (1,4)-(1,6))
            |   |       |   +-- CallNodeFlags: variable_call, ignore_visibility
            |   |       |   +-- receiver: nil
            |   |       |   +-- call_operator_loc: nil
            |   |       |   +-- name: :m1
            |   |       |   +-- message_loc: (1,4)-(1,6) = "m1"
            |   |       |   +-- opening_loc: nil
            |   |       |   +-- arguments: nil
            |   |       |   +-- closing_loc: nil
            |   |       |   +-- equal_loc: nil
            |   |       |   +-- block: nil
            |   |       +-- @ CallNode (location: (1,8)-(1,10))
            |   |           +-- CallNodeFlags: variable_call, ignore_visibility
            |   |           +-- receiver: nil
            |   |           +-- call_operator_loc: nil
            |   |           +-- name: :m2
            |   |           +-- message_loc: (1,8)-(1,10) = "m2"
            |   |           +-- opening_loc: nil
            |   |           +-- arguments: nil
            |   |           +-- closing_loc: nil
            |   |           +-- equal_loc: nil
            |   |           +-- block: nil
            |   +-- opening_loc: (1,3)-(1,4) = "("
            |   +-- closing_loc: (1,10)-(1,11) = ")"
            +-- then_keyword_loc: (1,12)-(1,16) = "then"
            +-- statements:
            |   @ StatementsNode (location: (1,17)-(1,18))
            |   +-- body: (length: 1)
            |       +-- @ IntegerNode (location: (1,17)-(1,18))
            |           +-- IntegerBaseFlags: decimal
            |           +-- value: 1
            +-- subsequent:
            |   @ ElseNode (location: (1,19)-(1,29))
            |   +-- else_keyword_loc: (1,19)-(1,23) = "else"
            |   +-- statements:
            |   |   @ StatementsNode (location: (1,24)-(1,25))
            |   |   +-- body: (length: 1)
            |   |       +-- @ IntegerNode (location: (1,24)-(1,25))
            |   |           +-- IntegerBaseFlags: decimal
            |   |           +-- value: 2
            |   +-- end_keyword_loc: (1,26)-(1,29) = "end"
            +-- end_keyword_loc: (1,26)-(1,29) = "end"

compile_ifcompile_branch_conditionという関数を呼ぶのですが、これがノードの種類に応じて分岐する関数になっています。 触り始めるとハマりそうな気がするのでここも一旦見なかったことにして先にすすみましょう。

static int
compile_branch_condition(rb_iseq_t *iseq, LINK_ANCHOR *ret, const NODE *cond,
                         LABEL *then_label, LABEL *else_label)
{
    int ok;
    DECL_ANCHOR(ignore);

  again:
    switch (nd_type(cond)) {
      case NODE_AND:
        CHECK(ok = compile_logical(iseq, ret, RNODE_AND(cond)->nd_1st, NULL, else_label));
        cond = RNODE_AND(cond)->nd_2nd;
        if (ok == COMPILE_SINGLE) {
            INIT_ANCHOR(ignore);
            ret = ignore;
            then_label = NEW_LABEL(nd_line(cond));
        }
        goto again;
      case NODE_OR:
        CHECK(ok = compile_logical(iseq, ret, RNODE_OR(cond)->nd_1st, then_label, NULL));
        cond = RNODE_OR(cond)->nd_2nd;
        if (ok == COMPILE_SINGLE) {
            INIT_ANCHOR(ignore);
            ret = ignore;
            else_label = NEW_LABEL(nd_line(cond));
        }
        goto again;
      case NODE_SYM:
      case NODE_LINE:
      ...:

elseを表すNode

else句のstatementsはElseNodeでラップされています。 else句をコンパイルする処理をcompile_ifに書いてもいいのですが、case ... else ... endelseなどもElseNodeになることを考えると、iseq_compile_each0ElseNodeコンパイルする処理を書いたほうがあとで楽そうです。

elseNULLのときはiseq_compile_eachで、elseのstatementsが空のときはiseq_compile_each0StatementsNodeの処理でputnilに変換されるので、ElseNodeの処理ではとくにstatementsの中をチェックしなくてよいでしょう。

      case RB_ELSE_NODE: {
        const rb_else_node_t *cast = (const rb_else_node_t *) node;
        CHECK(COMPILE_(ret, "else", (const NODE *) cast->statements, popped));
        break;
      }

これでifコンパイルできるようになりました。

$ ./miniruby --parser=parse.y --dump=p,i -e 'if self; true else false end'
@ ProgramNode (location: (1,0)-(1,28))
+-- locals: []
+-- statements:
    @ StatementsNode (location: (1,0)-(1,28))
    +-- body: (length: 1)
        +-- @ IfNode (location: (1,0)-(1,28))*
            +-- if_keyword_loc: (1,0)-(1,2) = ""
            +-- predicate:
            |   @ SelfNode (location: (1,3)-(1,7))
            +-- then_keyword_loc: (1,7)-(1,8) = ""
            +-- statements:
            |   @ StatementsNode (location: (1,9)-(1,13))
            |   +-- body: (length: 1)
            |       +-- @ TrueNode (location: (1,9)-(1,13))*
            +-- subsequent:
            |   @ ElseNode (location: (1,14)-(1,24))
            |   +-- else_keyword_loc: (1,14)-(1,18) = ""
            |   +-- statements:
            |   |   @ StatementsNode (location: (1,19)-(1,24))
            |   |   +-- body: (length: 1)
            |   |       +-- @ FalseNode (location: (1,19)-(1,24))*
            |   +-- end_keyword_loc: nil
            +-- end_keyword_loc: (1,25)-(1,28) = ""
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,28)>
0000 putself                                                          (   1)[Li]
0001 branchunless                           6
0003 putobject                              true
0005 leave
0006 putobject                              false
0008 leave

Cコンパイラの出す警告

こんな雑に変更を加えていって問題がおきないのか心配になったかもしれません。 もちろんCコンパイラは大量の警告を出してきます。 特に多いのがincompatible pointer typesで、これは新旧のノードがあちらこちらで混ざっているため発生します。 具体的にはparse.yとcompile.cでそれぞれ300件前後の警告がでます。 とは言っても正しく変更を加えたところだけを通るスクリプトを与えていれば問題は起きないはずなので、いまのところは警告を気にせず作業を進めていきましょう。

まとめ

今日の成果です。

今後も気をつけていきたいことは以下のとおりです。

  • まずゴールを決めるのが大事
  • 最初の一歩は小さくするのがよい
  • ハマりそうな箇所は一旦無視して進める
  • 多少の警告は気にしない

  1. prism_compile.cではrb_iseq_compile_nodeに相当するpm_scope_node_initStatementsNodeをあつかっているのですが、現時点での理解ではStatementsNodeはiseqを作らないと思うので、一旦iseq_compile_each0で処理するようにしておきます。
  2. do ... endのほうのブロックはNODE_ITERというノードで表現しています。ITERはiterationの略でしょう。
  3. parse.yでprimary: tLPAREN compstmt(stmts) ')'が無条件でNODE_BLOCKでラップするようになっているので、たとえばif ((m1; m2)) then 1 else 2 endのようなコードではNODE_BLOCKが3重にネストするようになっています。ということはもとのcompile_ifの実装でもNODE_BLOCKのネストを展開しきれずに、NODE_BLOCKiseq_compile_each0に処理させるケースがある気がします。だいぶ寄り道をしたのでこの辺で実装に戻りましょう。



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

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