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


Ruby Parser開発日誌 (24-29) - parse.yが生成するノードを変える ー match(=~演算子)とlambda

29日目: =~演算子とlambda

前回は正規表現リテラルをやったので、今回は関連するトピックとして=~に取り組みます。 また時間があればlambdaについても対応したいと思います。

=~演算子

通常a =~ bは他の+などと同様に=~メソッドの呼び出しになります。

# @ NODE_CALL (id: 3, line: 1, location: (1,0)-(1,6))*
# +- nd_mid: :=~
# +- nd_recv:
# |   @ NODE_VCALL (id: 0, line: 1, location: (1,0)-(1,1))
# |   +- nd_mid: :a
# +- nd_args:
#     @ NODE_LIST (id: 2, line: 1, location: (1,5)-(1,6))
#     +- as.nd_alen: 1
#     +- nd_head:
#     |   @ NODE_VCALL (id: 1, line: 1, location: (1,5)-(1,6))
#     |   +- nd_mid: :b
#     +- nd_next:
#         (null node)
a =~ b

しかし左辺や右辺に正規表現リテラルがくると生成されるノードが変わります1

# @ NODE_MATCH2 (id: 2, line: 1, location: (1,0)-(1,15))*
# +- nd_recv:
# |   @ NODE_REGX (id: 0, line: 1, location: (1,0)-(1,5))
# |   +- string: /re1/
# |   +- opening_loc: (1,0)-(1,1)
# |   +- content_loc: (1,1)-(1,4)
# |   +- closing_loc: (1,4)-(1,5)
# +- nd_value:
#     @ NODE_STR (id: 1, line: 1, location: (1,9)-(1,15))
#     +- string: "str1"
/re1/ =~ "str1"

# @ NODE_MATCH3 (id: 8, line: 2, location: (2,0)-(2,18))*
# +- nd_recv:
# |   @ NODE_DREGX (id: 7, line: 2, location: (2,10)-(2,18))
# |   +- string: ""
# |   +- nd_next->nd_head:
# |   |   @ NODE_EVSTR (id: 5, line: 2, location: (2,11)-(2,17))
# |   |   +- nd_body:
# |   |   |   @ NODE_VCALL (id: 4, line: 2, location: (2,13)-(2,16))
# |   |   |   +- nd_mid: :var
# |   |   +- opening_loc: (2,11)-(2,13)
# |   |   +- closing_loc: (2,16)-(2,17)
# |   +- nd_next->nd_next:
# |       (null node)
# +- nd_value:
#     @ NODE_STR (id: 3, line: 2, location: (2,0)-(2,6))
#     +- string: "str2"
"str2" =~ /#{var}/

さてノードの書き換え後はというと、これらは全てCallNodeで表現するようになります。

# @ CallNode (location: (1,0)-(1,15))
# +-- receiver:
# |   @ RegularExpressionNode (location: (1,0)-(1,5))
# |   +-- unescaped: "re1"
# +-- name: :=~
# +-- arguments:
# |   @ ArgumentsNode (location: (1,9)-(1,15))
# |   +-- arguments: (length: 1)
# |       +-- @ StringNode (location: (1,9)-(1,15))
# |           +-- unescaped: "str1"
# +-- block: nil
/re1/ =~ "str1"

# @ CallNode (location: (2,0)-(2,18))
# +-- receiver:
# |   @ StringNode (location: (2,0)-(2,6))
# |   +-- unescaped: "str2"
# +-- name: :=~
# +-- arguments:
# |   @ ArgumentsNode (location: (2,10)-(2,18))
# |   +-- arguments: (length: 1)
# |       +-- @ InterpolatedRegularExpressionNode (location: (2,10)-(2,18))
# +-- block: nil
"str2" =~ /#{var}/

a =~ bに対応する生成規則のアクションではmatch_op関数を呼んでノードを生成しているので、この関数を修正します。

arg | arg tMATCH arg
        {
            $$ = match_op(p, $1, $3, &@2, &@$);
        /*% ripper: binary!($:1, ID2VAL(idEqTilde), $:3) %*/
        }

// Before
static NODE*
match_op(struct parser_params *p, NODE *node1, NODE *node2, const YYLTYPE *op_loc, const YYLTYPE *loc)
{
    NODE *n;
    int line = op_loc->beg_pos.lineno;

    value_expr(p, node1);
    value_expr(p, node2);

    if ((n = last_expr_once_body(node1)) != 0) {
        switch (nd_type(n)) {
          case NODE_DREGX:
            {
                NODE *match = NEW_MATCH2(node1, node2, loc);
                nd_set_line(match, line);
                return match;
            }

          case NODE_REGX:
            {
                const VALUE lit = rb_node_regx_string_val(n);
                if (!NIL_P(lit)) {
                    NODE *match = NEW_MATCH2(node1, node2, loc);
                    RNODE_MATCH2(match)->nd_args = reg_named_capture_assign(p, lit, loc, assignable);
                    nd_set_line(match, line);
                    return match;
                }
            }
        }
    }

    if ((n = last_expr_once_body(node2)) != 0) {
        NODE *match3;

        switch (nd_type(n)) {
          case NODE_DREGX:
            match3 = NEW_MATCH3(node2, node1, loc);
            return match3;
        }
    }

    n = NEW_CALL(node1, tMATCH, NEW_LIST(node2, &node2->nd_loc), loc);
    nd_set_line(n, line);
    return n;
}

// After
static rb_node_t*
match_op(struct parser_params *p, rb_node_t *node1, rb_node_t *node2, const YYLTYPE *op_loc, const YYLTYPE *loc)
{
    rb_node_t *n;
    int line = op_loc->beg_pos.lineno;

    value_expr(p, node1);
    value_expr(p, node2);

    n = NEW_RB_CALL(node1, tMATCH, NEW_RB_ARGUMENTS(node2, &node2->location), 0, loc);
    nd_set_line(n, line);

    return n;
}

=~演算子コンパイルする

ノードを書き換えたのでcompile.cも修正が必要でしょう。 念のためcompile.cを修正する前の段階で生成されるバイトコードを確認しておきます。

# Before
#
# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(1,13)>
# 0000 putobject                              /re/                      (   1)[Li]
# 0002 putchilledstring                       "str"
# 0004 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]
# 0006 leave

# After
#
# == disasm: #<ISeq:<main>@../../test.rb:1 (1,0)-(1,13)>
# 0000 putobject                              /re/                      (   1)[Li]
# 0002 putchilledstring                       "str"
# 0004 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]
# 0006 leave
/re/ =~ "str"

ん?

# Before
#
# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(16,16)>
# 0000 putobject                              /re/                      (  16)[Li]
# 0002 putobject                              ""
# 0004 putself
# 0005 opt_send_without_block                 <calldata!mid:var, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0007 dup
# 0008 objtostring                            <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>
# 0010 anytostring
# 0011 concatstrings                          2
# 0013 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]
# 0015 leave

# After
#
# == disasm: #<ISeq:<main>@../../test.rb:1 (1,0)-(16,16)>
# 0000 putobject                              /re/                      (  16)[Li]
# 0002 putself
# 0003 opt_send_without_block                 <calldata!mid:var, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0005 dup
# 0006 objtostring                            <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>
# 0008 anytostring
# 0009 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]
# 0011 leave
/re/ =~ "#{var}"

んん?

# Before
#
# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(26,13)>
# 0000 putchilledstring                       "str"                     (  26)[Li]
# 0002 putobject                              /re/
# 0004 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]
# 0006 leave

# After
#
# == disasm: #<ISeq:<main>@../../test.rb:1 (1,0)-(26,13)>
# 0000 putchilledstring                       "str"                     (  26)[Li]
# 0002 putobject                              /re/
# 0004 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]
# 0006 leave
"str" =~ /re/


# Before
#
# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(16,17)>
# 0000 putchilledstring                       "str"                     (  16)[Li]
# 0002 putobject                              ""
# 0004 putself
# 0005 opt_send_without_block                 <calldata!mid:var, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0007 dup
# 0008 objtostring                            <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>
# 0010 anytostring
# 0011 toregexp                               0, 2
# 0014 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]
# 0016 leave

# After
#
# == disasm: #<ISeq:<main>@../../test.rb:1 (1,0)-(16,17)>
# 0000 putchilledstring                       "str"                     (  16)[Li]
# 0002 putself
# 0003 opt_send_without_block                 <calldata!mid:var, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0005 dup
# 0006 objtostring                            <calldata!mid:to_s, argc:0, FCALL|ARGS_SIMPLE>
# 0008 anytostring
# 0009 toregexp                               0, 1
# 0012 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]
# 0014 leave
"str" =~ /#{var}/

式展開を含む正規表現の部分のバイトコードこそ微妙に違いますが、基本的にはcompile.cへの修正がなくても期待するバイトコードが生成されています。

大きく2つの疑問が出てくると思います。

  1. opt_regexpmatch2はどこから出てきたのか
  2. NODE_MATCH2NODE_MATCH3の差異はどこにいったのか

1つめの疑問についてはcompile.cのiseq_specialized_instruction関数が関係しています。 この関数がメソッドの種類に応じて最適化された命令を生成しています。

static int
iseq_specialized_instruction(rb_iseq_t *iseq, INSN *iobj)
{
...
#define SP_INSN(opt) insn_set_specialized_instruction(iseq, iobj, BIN(opt_##opt))
        if (vm_ci_simple(ci)) {
            switch (vm_ci_argc(ci)) {
              case 0:
                switch (vm_ci_mid(ci)) {
                  case idLength: SP_INSN(length); return COMPILE_OK;
                  case idSize:     SP_INSN(size);    return COMPILE_OK;
                  case idEmptyP: SP_INSN(empty_p);return COMPILE_OK;
                  case idNilP:   SP_INSN(nil_p);  return COMPILE_OK;
                  case idSucc:     SP_INSN(succ);    return COMPILE_OK;
                  case idNot:  SP_INSN(not);      return COMPILE_OK;
                }
                break;
              case 1:
                switch (vm_ci_mid(ci)) {
                  case idPLUS:     SP_INSN(plus);    return COMPILE_OK;
                  case idMINUS:    SP_INSN(minus);  return COMPILE_OK;
                  case idMULT:     SP_INSN(mult);    return COMPILE_OK;
                  case idDIV:  SP_INSN(div);     return COMPILE_OK;
                  case idMOD:  SP_INSN(mod);     return COMPILE_OK;
                  case idEq:   SP_INSN(eq);      return COMPILE_OK;
                  case idNeq:  SP_INSN(neq);     return COMPILE_OK;
                  case idEqTilde:SP_INSN(regexpmatch2);return COMPILE_OK;
...

2つめの疑問の答えは、そもそも差がないということになります。 書き換え前の世界ではNODE_MATCH2NODE_MATCH3はどちらもcompile_match関数がコンパイルをしていました。 この関数をよくみるとrecvvalが逆転していて、結果としてどちらもケースも=~の左辺をレシーバー、右辺を引数としてバイトコードを生成していることになります。

case NODE_MATCH:
case NODE_MATCH2:
case NODE_MATCH3:
  CHECK(compile_match(iseq, ret, node, popped, type));
break;

static int
compile_match(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, int popped, const enum node_type type)
{
    DECL_ANCHOR(recv);
    DECL_ANCHOR(val);

    INIT_ANCHOR(recv);
    INIT_ANCHOR(val);
    switch ((int)type) {
      case NODE_MATCH:
        {
            VALUE re = rb_node_regx_string_val(node);
            RB_OBJ_SET_FROZEN_SHAREABLE(re);
            ADD_INSN1(recv, node, putobject, re);
            ADD_INSN2(val, node, getspecial, INT2FIX(0),
                      INT2FIX(0));
        }
        break;
      case NODE_MATCH2: // /re1/ =~ "str1"
        // NODE_REGX
        CHECK(COMPILE(recv, "receiver", RNODE_MATCH2(node)->nd_recv));
        // NODE_STR
        CHECK(COMPILE(val, "value", RNODE_MATCH2(node)->nd_value));
        break;
      case NODE_MATCH3: // "str2" =~ /#{var}/
        // NODE_STR
        CHECK(COMPILE(recv, "receiver", RNODE_MATCH3(node)->nd_value));
        // NODE_DREGX
        CHECK(COMPILE(val, "value", RNODE_MATCH3(node)->nd_recv));
        break;
    }

    ADD_SEQ(ret, recv);
    ADD_SEQ(ret, val);
    ADD_SEND(ret, node, idEqTilde, INT2FIX(1));

    if (nd_type_p(node, NODE_MATCH2) && RNODE_MATCH2(node)->nd_args) {
        compile_named_capture_assign(iseq, ret, RNODE_MATCH2(node)->nd_args);
    }

    if (popped) {
        ADD_INSN(ret, node, pop);
    }
    return COMPILE_OK;
}

ということでcompile.cの変更は今回は必要ないようです。

正規表現による変数のキャプチャ

さてcompile.cの変更が不要と言いましたが、そんなことはありません。

Ruby正規表現では特定の書き方をするとローカル変数を宣言し、マッチの結果を変数に代入することができます。

/(?<v1>\d+reg)(?<v2>exp\d+)/ =~ "!123regexp456!"

p v1
#=> "123reg"
p v2
#=> "exp456"

自然にこうなるわけはないので、どこかで誰かが頑張っているはずです。

/(?<v1>\d+reg)(?<v2>exp\d+)/ =~ "!123regexp456!"のノードがどうなっているかみてみるとnd_argsというフィールドにローカル変数の代入を表すノードが並んでいることがわかります。

# @ NODE_MATCH2 (id: 2, line: 1, location: (1,0)-(1,48))*
# +- nd_recv:
# |   @ NODE_REGX (id: 0, line: 1, location: (1,0)-(1,28))
# |   +- string: /(?<v1>\d+reg)(?<v2>exp\d+)/
# +- nd_value:
# |   @ NODE_STR (id: 1, line: 1, location: (1,32)-(1,48))
# |   +- string: "!123regexp456!"
# +- nd_args:
#     @ NODE_BLOCK (id: 7, line: 1, location: (1,0)-(1,48))
#     +- nd_head (1):
#     |   @ NODE_LASGN (id: 3, line: 1, location: (1,0)-(1,48))
#     |   +- nd_vid: :v1
#     |   +- nd_value:
#     |       @ NODE_SYM (id: 4, line: 1, location: (1,0)-(1,48))
#     |       +- string: :v1
#     +- nd_head (2):
#         @ NODE_LASGN (id: 8, line: 1, location: (1,0)-(1,48))
#         +- nd_vid: :v2
#         +- nd_value:
#             @ NODE_SYM (id: 9, line: 1, location: (1,0)-(1,48))
#             +- string: :v2

バイトコードはというと=~メソッドを実行したあとに、:$~変数をチェックしてマッチに成功している場合はv1 = $~[:v1]; v2 = $~[:v2]相当の代入を行います。 マッチに成功していないときはv1 = nil; v2 = nil相当の代入をします。

== disasm: #<ISeq:<main>@test.rb:1 (1,0)-(4,4)>
# local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
# [ 2] v1@0       [ 1] v2@1

# Regexp#=~ の部分
# 0000 putobject                              /(?<v1>\d+reg)(?<v2>exp\d+)/(   1)[Li]
# 0002 putchilledstring                       "!123regexp456!"
# 0004 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]

# 正規表現にマッチしているかのチェック
# 0006 getglobal                              :$~
# 0008 dup
# 0009 branchunless                           26

# マッチしていた場合はキャプチャしているv1とv2にマッチした文字列を代入する
# 0011 dup
# 0012 putobject                              :v1
# 0014 opt_aref                               <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr]
# 0016 setlocal_WC_0                          v1@0
# 0018 putobject                              :v2
# 0020 opt_aref                               <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr]
# 0022 setlocal_WC_0                          v2@1
# 0024 jump                                   33

# マッチしていない場合にはv1とv2にnilを代入する
# 0026 pop
# 0027 putnil
# 0028 setlocal_WC_0                          v1@0
# 0030 putnil
# 0031 setlocal_WC_0                          v2@1
# 0033 pop

書き換え後はというとMatchWriteNodeという専用のノードのしたに/re/ =~ "str"のメソッド呼び出しとローカル変数を表すノードが置かれるようになります。

# +-- @ MatchWriteNode (location: (1,0)-(1,48))
# |   +-- call:
# |   |   @ CallNode (location: (1,0)-(1,48))
# |   |   +-- receiver:
# |   |   |   @ RegularExpressionNode (location: (1,0)-(1,28))
# |   |   |   +-- unescaped: "(?<v1>\\d+reg)(?<v2>exp\\d+)"
# |   |   +-- name: :=~
# |   |   +-- arguments:
# |   |   |   @ ArgumentsNode (location: (1,32)-(1,48))
# |   |   |   +-- arguments: (length: 1)
# |   |   |       +-- @ StringNode (location: (1,32)-(1,48))
# |   |   |           +-- unescaped: "!123regexp456!"
# |   |   +-- block: nil
# |   +-- targets: (length: 2)
# |       +-- @ LocalVariableTargetNode (location: (1,0)-(1,28))
# |       |   +-- name: :v1
# |       |   +-- depth: 0
# |       +-- @ LocalVariableTargetNode (location: (1,0)-(1,28))
# |           +-- name: :v2
# |           +-- depth: 0

parse.yではmatch_op関数に分岐を加えます。 正規表現による変数のキャプチャは左辺が正規表現リテラルでかつ式展開を含まないときだけに起きるので、左辺のノードの種類をチェックして分岐します。

static rb_node_t*
match_op(struct parser_params *p, rb_node_t *node1, rb_node_t *node2, const YYLTYPE *op_loc, const YYLTYPE *loc)
{
    rb_node_t *n;
    int line = op_loc->beg_pos.lineno;

    value_expr(p, node1);
    value_expr(p, node2);

    n = NEW_RB_CALL(node1, tMATCH, NEW_RB_ARGUMENTS(node2, &node2->location), 0, loc);
    nd_set_line(n, line);

    if (RB_NODE_TYPE_P(node1, RB_REGULAR_EXPRESSION_NODE)) {
        const VALUE lit = rb_node_regx_string_val2(node1);
        if (!NIL_P(lit)) {
            rb_array_node_t *nd_ary = reg_named_capture_assign(p, lit, loc, assignable);
            if (nd_ary) {
                n = NEW_RB_MATCH_WRITE((rb_call_node_t *)n, nd_ary, loc);
                nd_set_line(n, line);
            }
        }
    }

    return n;
}

compile.cではcompile_match関数のなかでキャプチャがあるときに/reg/ =~ "str"の部分をコンパイルしたあとにcompile_named_capture_assign関数を呼んでいました。 ノードの書き換えでMatchWriteNodeが導入されたので、iseq_compile_each0関数でcompile_named_capture_assign関数を利用してMatchWriteNodeコンパイルするようにします。

case RB_MATCH_WRITE_NODE: {
  CHECK(COMPILE(ret, "match_write/call", (rb_node_t *)RB_NODE_MATCH_WRITE(node)->call));
  CHECK(compile_named_capture_assign(iseq, ret, node, &RB_NODE_MATCH_WRITE(node)->targets));
  if (popped) {
      ADD_INSN(ret, node, pop);
  }
  break;
}

もともとのcompile_named_capture_assign関数は以下のような実装になっています(一部コメントを足しています)。

static void
compile_named_capture_assign(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node)
{
    const NODE *vars;
    LINK_ELEMENT *last;
    int line = nd_line(node);
    const NODE *line_node = node;
    LABEL *fail_label = NEW_LABEL(line), *end_label = NEW_LABEL(line);

    // 正規表現にマッチしているかのチェック
#if !(defined(NAMED_CAPTURE_BY_SVAR) && NAMED_CAPTURE_BY_SVAR-0)
    ADD_INSN1(ret, line_node, getglobal, ID2SYM(idBACKREF));
#else
    ADD_INSN2(ret, line_node, getspecial, INT2FIX(1) /* '~' */, INT2FIX(0));
#endif
    ADD_INSN(ret, line_node, dup);
    ADD_INSNL(ret, line_node, branchunless, fail_label);

    // マッチしていた場合
    for (vars = node; vars; vars = RNODE_BLOCK(vars)->nd_next) {
        INSN *cap;
        if (RNODE_BLOCK(vars)->nd_next) {
            ADD_INSN(ret, line_node, dup);
        }
        last = ret->last;

        // var = :varのコンパイル
        //
        // branchunless   fail_label   <-- last
        // putobject      :var
        // setlocal_WC_0  var@0
        NO_CHECK(COMPILE_POPPED(ret, "capture", RNODE_BLOCK(vars)->nd_head));

        last = last->next; /* putobject :var */

        // opt_aref  <calldata!mid:[], argc:1, ARGS_SIMPLE>
        cap = new_insn_send(iseq, nd_line(line_node), nd_node_id(line_node), idAREF, INT2FIX(1),
                            NULL, INT2FIX(0), NULL);

        // branchunless   fail_label
        // putobject      :var         <-- last
        // opt_aref       #[], argc:1  <-- inserted
        // setlocal_WC_0  var@0
        ELEM_INSERT_PREV(last->next, (LINK_ELEMENT *)cap);
#if !defined(NAMED_CAPTURE_SINGLE_OPT) || NAMED_CAPTURE_SINGLE_OPT-0
        if (!RNODE_BLOCK(vars)->nd_next && vars == node) {
            /* only one name */
            DECL_ANCHOR(nom);

            INIT_ANCHOR(nom);
            ADD_INSNL(nom, line_node, jump, end_label);
            ADD_LABEL(nom, fail_label);
# if 0              /* $~ must be MatchData or nil */
            ADD_INSN(nom, line_node, pop);
            ADD_INSN(nom, line_node, putnil);
# endif
            ADD_LABEL(nom, end_label);
            (nom->last->next = cap->link.next)->prev = nom->last;
            (cap->link.next = nom->anchor.next)->prev = &cap->link;
            return;
        }
#endif
    }
    ADD_INSNL(ret, line_node, jump, end_label);
    ADD_LABEL(ret, fail_label);

    // マッチしていない場合
    ADD_INSN(ret, line_node, pop);
    for (vars = node; vars; vars = RNODE_BLOCK(vars)->nd_next) {
        last = ret->last;

        // var = :varのコンパイル
        //
        // jump           end_label
        // pop                       <-- last
        // putobject      :var
        // setlocal_WC_0  var@0
        NO_CHECK(COMPILE_POPPED(ret, "capture", RNODE_BLOCK(vars)->nd_head));

        // jump           end_label
        // pop
        // putnil                    <-- last
        // setlocal_WC_0  var@0
        last = last->next; /* putobject :var */
        ((INSN*)last)->insn_id = BIN(putnil);
        ((INSN*)last)->operand_size = 0;
    }
    ADD_LABEL(ret, end_label);
}

おもしろいのは変数へ代入するバイトコードを生成している箇所です。

# マッチした場合
# 0012 putobject                              :v1
# 0014 opt_aref                               <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr]
# 0016 setlocal_WC_0                          v1@0

# マッチしない場合
# 0027 putnil
# 0028 setlocal_WC_0                          v1@0

変数への代入を表す部分のノードはv1 = :v1という形をしています。

#     |   @ NODE_LASGN (id: 3, line: 1, location: (1,0)-(1,48))
#     |   +- nd_vid: :v1
#     |   +- nd_value:
#     |       @ NODE_SYM (id: 4, line: 1, location: (1,0)-(1,48))
#     |       +- string: :v1

これを素直にコンパイルするとputobjectsetlocalの2つの命令が生成されます。

# putobject      :v1
# setlocal_WC_0  v1@0

compile_named_capture_assign関数ではELEM_INSERT_PREVマクロを使ってこの2つの命令の間にopt_aref命令(MatchData #[]の呼び出し)を差し込むことでマッチしたときのバイトコードを生成します。 またputobjectputnilに置き換えることでマッチしなかったときのバイトコードを生成します。

# マッチしたときのバイトコード
# putobject      :v1
# opt_aref       <calldata!mid:[]>  <-- ここにopt_arefを差し込んだ
# setlocal_WC_0  v1@0

# マッチしないときのバイトコード
# putnil              <--- putobject :v1を置き換えた
# setlocal_WC_0  v1@0

ノードの書き換え後はTargeNodeとなり右辺の値を持たなくなるので、素直に:v1putobjectし、MatchData #[]を呼び出し、setlocalでローカル変数に代入するようにします。

    for (size_t i = 0; i < RB_NODE_LIST_LEN(list); i++) {
        INSN *cap;
        vars = list->nodes[i];
        EXPECT_NODE("compile_named_capture_assign/target", vars, RB_LOCAL_VARIABLE_TARGET_NODE, COMPILE_NG);
        ID id = RB_NODE_LOCAL_VARIABLE_TARGET(vars)->name;
        if (i != (RB_NODE_LIST_LEN(list) - 1)) {
            ADD_INSN(ret, line_node, dup);
        }
        ADD_INSN1(ret, line_node, putobject, ID2SYM(id));
        ADD_SEND_R(ret, line_node, idAREF, INT2FIX(1), NULL, INT2FIX(0), NULL);
        CHECK(compile_lasgn_lhs(iseq, ret, line_node, id));
    }

    for (size_t i = 0; i < RB_NODE_LIST_LEN(list); i++) {
        vars = list->nodes[i];
        ID id = RB_NODE_LOCAL_VARIABLE_TARGET(vars)->name;
        ADD_INSN(ret, line_node, putnil);
        CHECK(compile_lasgn_lhs(iseq, ret, line_node, id));
    }

キャプチャが1つのときの最適化

/(?<v1>\d+reg)(exp\d+)/のようにキャプチャが1つしかないときはバイトコードレベルの最適化をしています。

/(?<v1>\d+reg)(?<v2>exp\d+)/をベースに考えると以下のようなバイトコードが想定されます。

# 0000 putobject                              /(?<v1>\d+reg)(?<v2>exp\d+)/(   2)[Li]
# 0002 putchilledstring                       "!123regexp456!"
# 0004 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]
# 0006 getglobal                              :$~
# 0008 dup
# 0009 branchunless                           25

# 0012 putobject                              :v1
# 0014 opt_aref                               <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr]
# 0016 setlocal_WC_0                          v1@0
# 0024 leave

# 0025 pop
# 0026 putnil
# 0027 setlocal_WC_0                          v1@0
# 0032 leave

しかし実際に生成されるバイトコードは以下のようになっています。 これはマッチしなかったときに$~nilであることを利用してgetglobal :$~した結果をそのままsetlocalで使うことにより、popputnilを減らすという最適化です。

# 0000 putobject                              /(?<v1>\d+reg)(exp\d+)/   (   1)[Li]
# 0002 putchilledstring                       "!123regexp456!"
# 0004 opt_regexpmatch2                       <calldata!mid:=~, argc:1, ARGS_SIMPLE>[CcCr]
# 0006 getglobal                              :$~
# 0008 dup
# 0009 branchunless                           15
# 0011 putobject                              :v1
# 0013 opt_aref                               <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr]
# 0015 setlocal_WC_0                          v1@0
# 0017 leave

コンパイラではopt_aref命令とsetlocal命令の間にjump命令とlabelを追加することでこの最適化を行っています。

# getglobal      :$~
# dup
# branchunless   fail_label
# putobject      :v1
# opt_aref       <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr]
# ここにjump命令とlabelを追加する
# setlocal_WC_0  v1@0


# getglobal      :$~
# dup
# branchunless   fail_label
# putobject      :v1
# opt_aref       <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr]
# jump           end_label
# fail_label:
# end_label:
# setlocal_WC_0  v1@0

ビルドして実行してみましょう。

/(?<v1>\d+reg)(exp\d+)/ =~ "!123regexp456!"
p v1
#=> "123reg"

/(?<v2>\d+reg)(?<v3>exp\d+)/ =~ "!123regexp456!"
p v2
#=> "123reg"
p v3
#=> "exp456"

lambda

Rubyでは-> x, y { p x, y }でlambdaを生成することができます2lambda {|x, y| p x, y }Kernel#lambdaメソッドの呼び出しですが、-> ...リテラルであり、NODE_LAMBDAという専用のノードが割り当てられています。

ノードの書き換え後はLambdaNodeというノードになりますが、構造はとくに変わりません。 そのためparse.yの書き換えもほとんど必要ありません。

@@ -5610,8 +5616,8 @@ lambda            : tLAMBDA[lpar]
                         nd_args = args_with_numbered(p, $args, max_numparam, it_id);
                         {
                             YYLTYPE loc = code_loc_gen(&@args, &@body);
-                            $$ = NEW_LAMBDA(nd_args, $body->node, &loc, &@lpar, &$body->opening_loc, &$body->closing_loc);
-                            nd_set_line(RNODE_LAMBDA($$)->nd_body, @body.end_pos.lineno);
+                            $$ = NEW_RB_LAMBDA(nd_args, $body->node, &loc, &@lpar, &$body->opening_loc, &$body->closing_loc);
+                            nd_set_line(RB_NODE_LAMBDA($$)->body, @body.end_pos.lineno);
                             nd_set_line($$, @args.end_pos.lineno);
                             nd_set_first_loc($$, @1.beg_pos);
                             xfree($body);

lambdaをコンパイルする

-> x, y { p x, y }コンパイルすると2つのISeqが作られます。 lambdaのbodyに対応するISeqと、そのISeqをオペランドとしてRubyVMFrozenCore#lambdaを呼び出すISeqです。

# == disasm: #<ISeq:<main>@test.rb:1 (1,0)-(1,18)>
# 0000 putspecialobject                       1                         (   1)[Li]
# 0002 send                                   <calldata!mid:lambda, argc:0, FCALL>, block in <main>
# 0005 leave

# == disasm: #<ISeq:block in <main>@test.rb:1 (1,3)-(1,18)>
# local table (size: 2, argc: 2 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
# [ 2] x@0<Arg>   [ 1] y@1<Arg>
# 0000 putself                                                          (   1)[LiBc]
# 0001 getlocal_WC_0                          x@0
# 0003 getlocal_WC_0                          y@1
# 0005 opt_send_without_block                 <calldata!mid:p, argc:2, FCALL|ARGS_SIMPLE>
# 0007 leave                                  [Br]
-> x, y { p x, y }

こちらも既存の処理を流用できます。

      case RB_LAMBDA_NODE: {
        /* compile same as lambda{...} */
        const rb_iseq_t *block = NEW_CHILD_ISEQ(node, make_name_for_block(iseq), ISEQ_TYPE_BLOCK, line);
        VALUE argc = INT2FIX(0);

        ADD_INSN1(ret, node, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE));
        ADD_CALL_WITH_BLOCK(ret, node, idLambda, argc, block);
        RB_OBJ_WRITTEN(iseq, Qundef, (VALUE)block);

        if (popped) {
            ADD_INSN(ret, node, pop);
        }
        break;
      }

minirubyをビルドして実行してみましょう。

l = lambda {|x, y| p x, y }
l.call(true, false)
#=> true
#=> false

まとめ

今日の成果です。


  1. NODE_MATCH2NODE_MATCH3があるということはNODE_MATCH1はあるのか?と疑問を持つかもしれませんが、if /re/のように条件式に正規表現リテラルを書くとNODE_MATCHが生成されます。これは$_との暗黙のマッチになるためです。NODE_MATCHのケースは条件式周りをいじる時に取り組むことにします。
  2. インスタンスとしてはProcクラスのオブジェクト。



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

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