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


Ruby Parser開発日誌 (24-38) - parse.yが生成するノードを変える ー パターンマッチング その1 (定数とのマッチ)

38日目: パターンマッチングはじめました

前回はcase whenをやったので、今回からパターンマッチングに取り組みたいと思います。 おそらく1回で終わらないと思うので、数回に分けて対応していければいいなと考えています。

パターンマッチングの文法を洗い出す

パターンマッチングは豊富な文法要素をもっています。 まずはそれを種類ごとに整理しておきましょう。

パターンマッチング全体の構造として3つの異なる文法が用意されています。

  1. case expr in pattern ...
  2. expr in pattern
  3. expr => pattern

それぞれ具体的なコードは以下の通りです。

# その1
case ary
in [0, 1]
  expr1
in [1, 2]
  expr2
else
  expr_else
end

# その2
ary in [1, 2]

# その3
ary => [1, 2]
# その2

パターンについても複数の異なるパターンが用意されています。 多くない...?

  1. Value pattern
  2. Variable pattern
  3. Array pattern
  4. Hash pattern
  5. Find pattern
  6. Alternative pattern
  7. As pattern

それぞれ具体的には以下のようなコードになります。

a = 1

case obj
in String # Value pattern
  expr
in ^a # Value pattern (variable pinning)
  expr
in v # Variable pattern
  expr
in [1, 2] # Array pattern
  expr
in {k: :v} # Hash pattern
  expr
in [*a, b, c, *d] # Find pattern
  expr
in [1, 2] | {k: :v} # Alternative pattern
  expr
in Integer => a, Integer # As pattern
  expr
else
  expr_else
end

一度に全部をやるのは難しそうなので、今回はつぎの2つの点に絞って実装していきます。

  • case expr in pattern ...の形式
  • Value pattern (の一部)

まずは以下のコードをコンパイルするところまで進めてみます。

case "str"
in String
  expr_str
in Integer
  expr_int
else
  expr_else
end

parserの変更

書き換え前後のノードを見比べてみます。

# Before
#
# @ NODE_CASE3
# +- nd_head:
# |   @ NODE_STR ("str")
# +- nd_body:
# |   @ NODE_IN (String)
# |   |   @ NODE_IN (Integer)
# |   |   |   @ NODE_VCALL (expr_else)

# After
#
# @ CaseMatchNode (location: (1,0)-(8,3))
# +-- predicate:
# |   @ StringNode ("str")
# +-- conditions: (length: 2)
# |   +-- @ InNode (String)
# |   +-- @ InNode (Integer)
# +-- else_clause:
# |   @ ElseNode (location: (6,0)-(8,3))
# |   +-- statements:
# |   |   @ StatementsNode (expr_else)

case "str"
in String
  expr_str
in Integer
  expr_int
else
  expr_else
end

ここで3つの差異があることがわかります。

  1. NODE_CASE3の代わりにCaseMatchNodeを使う
  2. NODE_INがリスト構造ではなくなる
  3. elseにあたるノードが直接CaseMatchNodeに紐づく

これは前回取り組んだcase whenのケースと同じような構造の変化です。

parse.yのアクションに関していうと、p_case_bodyInNodeを常に配列の先頭に入れるようにして、CaseMatchNodeをつくるときにElseNodeがあれば取り出してelse_clauseに設定します。 このあたりは前回と同様の変更です。

p_case_body     : keyword_in
                  p_in_kwarg[ctxt] p_pvtbl p_pktbl
                  p_top_expr[expr] then
                    {
                        pop_pktbl(p, $p_pktbl);
                        pop_pvtbl(p, $p_pvtbl);
                        p->ctxt.in_kwarg = $ctxt.in_kwarg;
                        p->ctxt.in_alt_pattern = $ctxt.in_alt_pattern;
                        p->ctxt.capture_in_pattern = $ctxt.capture_in_pattern;
                    }
                  compstmt(stmts)
                  p_cases[cases]
                    {
                        $$ = NEW_RB_IN($expr, $compstmt, &@$, &@keyword_in, &@then);
                        if ($cases) {
                            $$ = node_array_prepend(p, $cases, $$, &@$);
                        }
                        else {
                            $$ = NEW_RB_ARRAY($$, &@$);
                        }
                    /*% ripper: in!($:expr, $:compstmt, $:cases) %*/
                    }
                ;

p_cases         : opt_else
                    {
                        if ($1) {
                            $$ = NEW_RB_ARRAY($1, $$);
                        }
                    }
                | p_case_body
                ;

大枠の構造ができたので、次にパターンの部分についてみていきます。 今回のケースでいうと、in Stringin Integerの部分はConstantReadNodeとして表現されています。

# conditions: (length: 2)
# +-- @ InNode (location: (2,0)-(3,10))
# |   +-- pattern:
# |   |   @ ConstantReadNode (location: (2,3)-(2,9))
# |   |   +-- name: :String
# +-- @ InNode (location: (4,0)-(5,10))
#     +-- pattern:
#     |   @ ConstantReadNode (location: (4,3)-(4,10))
#     |   +-- name: :Integer

このinのあとのStringは生成規則を辿っていくとp_constというルールで定義されています。

p_case_body     : keyword_in
                  p_in_kwarg[ctxt] p_pvtbl p_pktbl
                  p_top_expr[expr] then
                  compstmt(stmts)
                  p_cases[cases]

p_top_expr      : p_top_expr_body
                | p_top_expr_body modifier_if expr_value
                | p_top_expr_body modifier_unless expr_value
                ;

p_top_expr_body : p_expr
                | p_expr ','
                | p_expr ',' p_args
                | p_find
                | p_args_tail
                | p_kwargs
                ;

p_expr          : p_as
                ;

p_as            : p_expr tASSOC p_variable
                | p_alt
                ;


p_alt           : p_alt[left] '|'[alt] p_expr_basic[right]
                | p_expr_basic
                ;


p_expr_basic    : p_value
                | p_variable
                | p_const p_lparen[p_pktbl] p_args rparen
                | p_const p_lparen[p_pktbl] p_find rparen
                | p_const p_lparen[p_pktbl] p_kwargs rparen
                ...
                ;

p_value         : p_primitive
                | range_expr(p_primitive)
                | p_var_ref
                | p_expr_ref
                | p_const
                ;

p_const         : tCOLON3 cname
                    {
                        $$ = NEW_COLON3($2, &@$, &@1, &@2);
                    }
                | p_const tCOLON2 cname
                    {
                        $$ = NEW_COLON2($1, $3, &@$, &@2, &@3);
                    }
                | tCONSTANT
                   {
                        $$ = gettable(p, $1, &@$);
                   }
                ;

定数に対するgettable関数はConstantReadNodeを返すので期待するノードが生成されていることがわかります。 in Stringの他にもin ::Integerin A::Bを書くこともできます。 それぞれ期待されるノードはConstantPathNodeConstantPathNode(ConstantReadNode)(ネストしている)ですが、これらは今のアクションが生成するノードと一致しているため、value patternにおける定数のケースではとくにアクションを修正する必要がないことがわかります。

生成されるバイトコードを確認する

サンプルコードから生成されるバイトコードを先にみておきましょう。 基本的な構造は

  • case ...の部分のバイトコード
  • in ...のチェックとジャンプをするバイトコード
  • else bodyのbodyの部分のバイトコード
  • in ... bodyのbodyの部分のバイトコード

となっており、これも前回やったcase whenとほぼ同じ構造になっています。 一番最初にputnilしているのは実際にこれを使うときに説明しようとおもいます。

# `case "str"`の部分
#
# 0000 putnil                                                           (   2)[Li]
# 0001 putchilledstring                       "str"                     (   1)

# `String === "str"`によるチェック
#
# 0003 dup                                                              (   2)
# 0004 opt_getconstant_path                   <ic:0 String>
# 0006 checkmatch                             2
# 0008 branchif                               23

# `Integer === "str"`によるチェック
#
# 0010 dup                                                              (   4)
# 0011 opt_getconstant_path                   <ic:1 Integer>
# 0013 checkmatch                             2
# 0015 branchif                               29

# `else`のケース
#
# 0017 pop                                                              (   7)
# 0018 pop
# 0019 putself                                [Li]
# 0020 opt_send_without_block                 <calldata!mid:expr_else, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0022 leave

# `String === "str"`のケース
#
# 0023 adjuststack                            2                         (   2)
# 0025 putself                                                          (   3)[Li]
# 0026 opt_send_without_block                 <calldata!mid:expr_str, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0028 leave                                                            (   7)

# ` === "str"`のケース
#
# 0029 adjuststack                            2                         (   4)
# 0031 putself                                                          (   5)[Li]
# 0032 opt_send_without_block                 <calldata!mid:expr_int, argc:0, FCALL|VCALL|ARGS_SIMPLE>
# 0034 leave

case "str"
in String
  expr_str
in Integer
  expr_int
else
  expr_else
end

compile.cを変更する

compile.cではcompile_case3という関数があるので、それを修正していくことにします。

      case RB_CASE_MATCH_NODE: {
        CHECK(compile_case3(iseq, ret, node, popped));
        break;
      }

compile_case3関数ではhead, body_seq, cond_seqという3つのアンカーを用意してバイトコードを生成していきます。 この辺はcase whenのときと同じです。

static int
compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_node, int popped)
{
    DECL_ANCHOR(head);
    DECL_ANCHOR(body_seq);
    DECL_ANCHOR(cond_seq);

    INIT_ANCHOR(head);
    INIT_ANCHOR(body_seq);
    INIT_ANCHOR(cond_seq);

    ADD_SEQ(ret, head);  /* case VAL */

    ADD_SEQ(ret, cond_seq);
    ADD_SEQ(ret, body_seq);
    ADD_LABEL(ret, endlabel);
    return COMPILE_OK;
}

まずcase "str"の部分のコンパイルですが、これは最初にいくつかスタックを確保しておきます。 確保する数はパターンが一個かどうかで変わりますが、おそらく一個のときだけは決め打ちで確保できるのでしょう。 その後nd_headをコンパイルしています。

static int
compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_node, int popped)
{
    const NODE *node = orig_node;
    bool single_pattern;

    node = RNODE_CASE3(node)->nd_body;
    EXPECT_NODE("NODE_CASE3", node, NODE_IN, COMPILE_NG);
    single_pattern = !RNODE_IN(node)->nd_next;

    if (single_pattern) {
        /* allocate stack for ... */
        ADD_INSN(head, line_node, putnil); /* key_error_key */
        ADD_INSN(head, line_node, putnil); /* key_error_matchee */
        ADD_INSN1(head, line_node, putobject, Qfalse); /* key_error_p */
        ADD_INSN(head, line_node, putnil); /* error_string */
    }
    ADD_INSN(head, line_node, putnil); /* allocate stack for cached #deconstruct value */

    CHECK(COMPILE(head, "case base", RNODE_CASE3(orig_node)->nd_head));

in ...の部分がconditionsフィールドに移動したことを踏まえて書き換えると以下のようになります。

static int
compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_node, int popped)
{
    const NODE *node = orig_node;
    const rb_node_list2_t *conditions = &RB_NODE_CASE_MATCH(node)->conditions;
    bool single_pattern;

    single_pattern = RB_NODE_LIST_LEN(conditions) == 1;

    if (single_pattern) {
        /* allocate stack for ... */
        ADD_INSN(head, line_node, putnil); /* key_error_key */
        ADD_INSN(head, line_node, putnil); /* key_error_matchee */
        ADD_INSN1(head, line_node, putobject, Qfalse); /* key_error_p */
        ADD_INSN(head, line_node, putnil); /* error_string */
    }
    ADD_INSN(head, line_node, putnil); /* allocate stack for cached #deconstruct value */

    CHECK(COMPILE(head, "case base", RB_NODE_CASE_MATCH(orig_node)->predicate));

続いてin pattern bodyを1つずつ順番にコンパイルしていきます。 bodybody_seqに、patterncond_seqにそれぞれ追記していきます。

パターンをコンパイルするのはiseq_compile_pattern_each関数なので、あとでiseq_compile_pattern_each関数も修正します。

    while (type == NODE_IN) {
        LABEL *l1;

        // `in pattern body`のうち`body`をコンパイルする
        if (branch_id) {
            ADD_INSN(body_seq, line_node, putnil);
        }
        l1 = NEW_LABEL(line);
        ADD_LABEL(body_seq, l1);
        ADD_INSN1(body_seq, line_node, adjuststack, INT2FIX(single_pattern ? 6 : 2));

        const NODE *const coverage_node = RNODE_IN(node)->nd_body ? RNODE_IN(node)->nd_body : node;
        add_trace_branch_coverage(
            iseq,
            body_seq,
            nd_code_loc(coverage_node),
            nd_node_id(coverage_node),
            branch_id++,
            "in",
            branches);

        CHECK(COMPILE_(body_seq, "in body", RNODE_IN(node)->nd_body, popped));
        ADD_INSNL(body_seq, line_node, jump, endlabel);

        // `in pattern body`のうち`pattern`をコンパイルする
        pattern = RNODE_IN(node)->nd_head;
        if (pattern) {
            int pat_line = nd_line(pattern);
            LABEL *next_pat = NEW_LABEL(pat_line);
            ADD_INSN (cond_seq, pattern, dup); /* dup case VAL */
            // NOTE: set base_index (it's "under" the matchee value, so it's position is 2)
            CHECK(iseq_compile_pattern_each(iseq, cond_seq, pattern, l1, next_pat, single_pattern, false, 2, true));
            ADD_LABEL(cond_seq, next_pat);
            LABEL_UNREMOVABLE(next_pat);
        }
        else {
            COMPILE_ERROR(ERROR_ARGS "unexpected node");
            return COMPILE_NG;
        }

        node = RNODE_IN(node)->nd_next;
        if (!node) {
            break;
        }
        type = nd_type(node);
        line = nd_line(node);
        line_node = node;
    }

NODE_INがリンク構造から配列の要素に変わったことを踏まえて、forによるループに変更します。

    for (size_t i = 0; i < RB_NODE_LIST_LEN(conditions); i++) {
        LABEL *l1;
        const NODE *nd_cond = conditions->nodes[i];
        EXPECT_NODE("NODE_CASE3", nd_cond, RB_IN_NODE, COMPILE_NG);
        type = nd_type(nd_cond);
        line = nd_line(nd_cond);
        line_node = nd_cond;

        if (branch_id) {
            ADD_INSN(body_seq, line_node, putnil);
        }
        l1 = NEW_LABEL(line);
        ADD_LABEL(body_seq, l1);
        ADD_INSN1(body_seq, line_node, adjuststack, INT2FIX(single_pattern ? 6 : 2));

        const NODE *const coverage_node = RB_NODE_IN(nd_cond)->statements ? (const NODE *const)RB_NODE_IN(nd_cond)->statements : nd_cond;
        add_trace_branch_coverage(
            iseq,
            body_seq,
            nd_code_loc(coverage_node),
            nd_node_id(coverage_node),
            branch_id++,
            "in",
            branches);

        CHECK(COMPILE_(body_seq, "in body", RB_NODE_IN(nd_cond)->statements, popped));
        ADD_INSNL(body_seq, line_node, jump, endlabel);

        pattern = RB_NODE_IN(nd_cond)->pattern;
        if (pattern) {
            int pat_line = nd_line(pattern);
            LABEL *next_pat = NEW_LABEL(pat_line);
            ADD_INSN (cond_seq, pattern, dup); /* dup case VAL */
            // NOTE: set base_index (it's "under" the matchee value, so it's position is 2)
            CHECK(iseq_compile_pattern_each(iseq, cond_seq, pattern, l1, next_pat, single_pattern, false, 2, true));
            ADD_LABEL(cond_seq, next_pat);
            LABEL_UNREMOVABLE(next_pat);
        }
        else {
            COMPILE_ERROR(ERROR_ARGS "unexpected node");
            return COMPILE_NG;
        }
    }

最後にelseがあればその部分をコンパイルして終了です。 elseがないときについてはあとで見ることにします。

    /* else */
    if (node) {
        ADD_LABEL(cond_seq, elselabel);
        ADD_INSN(cond_seq, line_node, pop);
        ADD_INSN(cond_seq, line_node, pop); /* discard cached #deconstruct value */
        add_trace_branch_coverage(iseq, cond_seq, nd_code_loc(node), nd_node_id(node), branch_id, "else", branches);
        CHECK(COMPILE_(cond_seq, "else", node, popped));
        ADD_INSNL(cond_seq, line_node, jump, endlabel);
        ADD_INSN(cond_seq, line_node, putnil);
        if (popped) {
            ADD_INSN(cond_seq, line_node, putnil);
        }
    }
    else {
        ...
    }

NODE_INの最後の要素として存在していたelseに相当するノードがelse_clauseに分離されたことを踏まえて書き直します。

    const rb_else_node_t *const nd_else = RB_NODE_CASE_MATCH(node)->else_clause;

    /* else */
    if (nd_else) {
        node = (NODE *)nd_else;
        line_node = node;

        ADD_LABEL(cond_seq, elselabel);
        ADD_INSN(cond_seq, line_node, pop);
        ADD_INSN(cond_seq, line_node, pop); /* discard cached #deconstruct value */
        add_trace_branch_coverage(iseq, cond_seq, nd_code_loc(node), nd_node_id(node), branch_id, "else", branches);
        CHECK(COMPILE_(cond_seq, "else", node, popped));
        ADD_INSNL(cond_seq, line_node, jump, endlabel);
        ADD_INSN(cond_seq, line_node, putnil);
        if (popped) {
            ADD_INSN(cond_seq, line_node, putnil);
        }
    }
    else {
        ...
    }

さて大枠を書き換えたのでiseq_compile_pattern_each関数にいきましょう。 この関数はin pattern ...patternの部分をコンパイルするための関数で、内部はpatternのノードで分岐しています。

static int
iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, LABEL *matched, LABEL *unmatched, bool in_single_pattern, bool in_alt_pattern, int base_index, bool use_deconstructed_cache)
{
    const int line = nd_line(node);
    const NODE *line_node = node;

    switch (nd_type(node)) {
      case NODE_ARYPTN: {
        ...
        break;
      }
      case NODE_FNDPTN: {
        ...
        break;
      }
      case NODE_HSHPTN: {
        ...
        break;
      }
      case NODE_SYM:
      case NODE_REGX:
      case NODE_LINE:
      case NODE_INTEGER:
      ...
      default:
        UNKNOWN_NODE("NODE_IN", node, COMPILE_NG);
    }
    return COMPILE_OK;
}

今回は定数に関するノードをコンパイルできるようにしましょう。 といっても、基本的には既存のコンパイルの枠組み(COMPILE)に載せるだけなので、作業としてはcase ...のところを書き換えるだけです。

static int
iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, LABEL *matched, LABEL *unmatched, bool in_single_pattern, bool in_alt_pattern, int base_index, bool use_deconstructed_cache)
{
    const int line = nd_line(node);
    const NODE *line_node = node;

    switch (nd_type(node)) {
      ...
      // case NODE_FALSE:
      // case NODE_SELF:
      // case NODE_NIL:
      case RB_CONSTANT_READ_NODE:
      case RB_CONSTANT_PATH_NODE:  
      // case NODE_BEGIN:
      // case NODE_BLOCK:
      // case NODE_ONCE:
        CHECK(COMPILE(ret, "case in literal", node)); // (1)
        if (in_single_pattern) {
            ADD_INSN1(ret, line_node, dupn, INT2FIX(2));
        }
        ADD_INSN1(ret, line_node, checkmatch, INT2FIX(VM_CHECKMATCH_TYPE_CASE)); // (2)
        if (in_single_pattern) {
            CHECK(iseq_compile_pattern_set_eqq_errmsg(iseq, ret, node, base_index + 2 /* (1), (2) */));
        }
        ADD_INSNL(ret, line_node, branchif, matched);
        ADD_INSNL(ret, line_node, jump, unmatched);
        break;
      ...
      default:
        UNKNOWN_NODE("NODE_IN", node, COMPILE_NG);
    }
    return COMPILE_OK;
}

ここまでで一度minirubyをbuildして動作確認します。

def m(val)
  case val
  in String
    :expr_str
  in Integer
    :expr_int
  else
    :expr_else
  end
end

p m("str")
#=> :expr_str
p m(1)
#=> :expr_int
p m([])
#=> :expr_else

よさそうですね。

elseがないケース

ここで一旦case .... inの全体の構造に戻って、elseがないときのことを考えてみましょう。 elseがなくて、いずれのパターンにもマッチしないときはNoMatchingPatternError例外が投げられます。

case :sym
in String
  :expr_str
in Integer
  :expr_int
end
#=> test.rb:1:in '<main>': sym (NoMatchingPatternError)

このコードに対応するバイトコードは以下のようになっています。 これはRubyVMFrozenCore#raise(NoMatchingPatternError, :sym)を実行して例外を投げていると言えます。

# 0000 putnil                                                           (   2)[Li]
# 0001 putobject                              :sym                      (   1)
# 0003 dup                                                              (   2)
# 0004 opt_getconstant_path                   <ic:0 String>
# 0006 checkmatch                             2
# 0008 branchif                               29
# 0010 dup                                                              (   4)
# 0011 opt_getconstant_path                   <ic:1 Integer>
# 0013 checkmatch                             2
# 0015 branchif                               34

# どのパターンにもマッチしないとき
#
# 0017 putspecialobject                       1                         (   1)
# 0019 putobject                              NoMatchingPatternError
# 0021 topn                                   2
# 0023 opt_send_without_block                 <calldata!mid:core#raise, argc:2, ARGS_SIMPLE>
# 0025 adjuststack                            3
# 0027 putnil
# 0028 leave                                                            (   5)

# `String`にマッチしたとき
#
# 0029 adjuststack                            2                         (   2)
# 0031 putobject                              :expr_str                 (   3)[Li]
# 0033 leave                                                            (   5)

# `Integer`にマッチしたとき
#
# 0034 adjuststack                            2                         (   4)
# 0036 putobject                              :expr_int                 (   5)[Li]
# 0038 leave

compile.cの該当する部分は以下のようになっています。 ここはノードの種類に依存していないので書き換えなしで動くはずです。

static int
compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_node, int popped)
{
    /* else */
    if (nd_else) {
        ...
    }
    else {
        debugs("== else (implicit)\n");
        ADD_LABEL(cond_seq, elselabel);
        add_trace_branch_coverage(iseq, cond_seq, nd_code_loc(orig_node), nd_node_id(orig_node), branch_id, "else", branches);
        ADD_INSN1(cond_seq, orig_node, putspecialobject, INT2FIX(VM_SPECIAL_OBJECT_VMCORE));

        if (single_pattern) {
            ...
        }
        else {
            ADD_INSN1(cond_seq, orig_node, putobject, rb_eNoMatchingPatternError);
            ADD_INSN1(cond_seq, orig_node, topn, INT2FIX(2));
            ADD_SEND(cond_seq, orig_node, id_core_raise, INT2FIX(2));
        }
        ADD_INSN1(cond_seq, orig_node, adjuststack, INT2FIX(single_pattern ? 7 : 3));
        if (!popped) {
            ADD_INSN(cond_seq, orig_node, putnil);
        }
        ADD_INSNL(cond_seq, orig_node, jump, endlabel);
        ADD_INSN1(cond_seq, orig_node, dupn, INT2FIX(single_pattern ? 5 : 1));
        if (popped) {
            ADD_INSN(cond_seq, line_node, putnil);
        }
    }

    ADD_SEQ(ret, cond_seq);
    ADD_SEQ(ret, body_seq);
    ADD_LABEL(ret, endlabel);
    return COMPILE_OK;
}

実行してみると期待したとおりの例外が発生することがわかります。

case :sym
in String
  :expr_str
in Integer
  :expr_int
end
#=> ../../test.rb:1:in '<main>': sym (NoMatchingPatternError)

single_patternのとき

compile_case3関数にはsingle_pattern(パターンが1つ)のときの分岐が存在します。 まずはバイトコードを眺めてみましょう。

case :sym
in String
  :expr_str
end

このコードからバイトコードを生成すると以下のようなバイトコードが生成されます。

# 0000 putnil                                                           (   2)[Li]
# 0001 putnil
# 0002 putobject                              false
# 0004 putnil
# 0005 putnil
# 0006 putobject                              :sym                      (   1)
# 0008 dup                                                              (   2)
# 0009 opt_getconstant_path                   <ic:0 String>
# 0011 dupn                                   2
# 0013 checkmatch                             2
# 0015 dup
# 0016 branchif                               36
# 0018 putspecialobject                       1
# 0020 putobject                              "%p === %p does not return true"
# 0022 topn                                   3
# 0024 topn                                   5
# 0026 opt_send_without_block                 <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE>
# 0028 setn                                   6
# 0030 putobject                              false
# 0032 setn                                   8
# 0034 pop
# 0035 pop
# 0036 setn                                   2
# 0038 pop
# 0039 pop
# 0040 branchif                               88
# 0042 putspecialobject                       1                         (   1)
# 0044 topn                                   4
# 0046 branchif                               64
# 0048 putobject                              NoMatchingPatternError
# 0050 putspecialobject                       1
# 0052 putobject                              "%p: %s"
# 0054 topn                                   4
# 0056 topn                                   7
# 0058 opt_send_without_block                 <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE>
# 0060 opt_send_without_block                 <calldata!mid:core#raise, argc:2, ARGS_SIMPLE>
# 0062 jump                                   84
# 0064 putobject                              NoMatchingPatternKeyError
# 0066 putspecialobject                       1
# 0068 putobject                              "%p: %s"
# 0070 topn                                   4
# 0072 topn                                   7
# 0074 opt_send_without_block                 <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE>
# 0076 topn                                   7
# 0078 topn                                   9
# 0080 opt_send_without_block                 <calldata!mid:new, argc:3, kw:[#<Symbol:0x000000000023310c>,#<Symbol:0x000000000021f10c>], KWARG>
# 0082 opt_send_without_block                 <calldata!mid:core#raise, argc:1, ARGS_SIMPLE>
# 0084 adjuststack                            7
# 0086 putnil
# 0087 leave                                                            (   3)
# 0088 adjuststack                            6                         (   2)
# 0090 putobject                              :expr_str                 (   3)[Li]
# 0092 leave

なんか異様に長いんだが?? パターンが2つのときのほうがバイトコードが短くて面食らいますね。 順を追ってみていきましょう。

まず最初にスタックに5つの領域を確保します。 これは後々使うタイミングで説明します。

# stackに領域を確保する
#
# 0000 putnil # key_error_key
# 0001 putnil # key_error_matchee
# 0002 putobject false # key_error_p
# 0004 putnil # error_string
# 0005 putnil # cached #deconstruct value

次にString === :symを実行して結果に応じてjumpします。

# `String === :sym`をチェックする
#
# 0006 putobject                              :sym                      (   1)
# 0008 dup                                                              (   2)
# 0009 opt_getconstant_path                   <ic:0 String>
# 0011 dupn                                   2
# 0013 checkmatch                             2
# 0015 dup
# 0016 branchif                               36

このバイトコードによってスタックがどうなるかを確認しておきましょう(最初に確保した部分は変わらないので一旦無視します)。

# `0011 dupn 2`まで
String
:sym
String
:sym
:sym

# `0015 dup`まで
false
false
String
:sym
:sym

# `0016 branchif 36`まで
false
String
:sym
:sym

今回のケースではStirng === :symfalseなので、ここではjumpしません。

# マッチしなかったとき
#
# 0018 putspecialobject                       1
# 0020 putobject                              "%p === %p does not return true"
# 0022 topn                                   3
# 0024 topn                                   5
# 0026 opt_send_without_block                 <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE>
# 0028 setn                                   6
# 0030 putobject                              false
# 0032 setn                                   8
# 0034 pop
# 0035 pop

続く0018から0035では主にエラーメッセージの構築を行います。

# `0024 topn 5`まで
:sym
String
"%p === %p does not return true"
RubyVMFrozenCore

false
String
:sym
:sym

# `0028 setn 6`まで
"String === :sym does not return true"

false
String
:sym
:sym

nil # cached #deconstruct value
"String === :sym does not return true" # error_string
false # key_error_p
nil # key_error_matchee
nil # key_error_key

#sprintfをつかってエラーメッセージを構築したら、最初に確保したスタックのうち下から4つ目の領域にメッセージをコピーします。 この領域はerror_stringとあるように、例外に渡すメッセージのための領域のようです。

その後key_error_pfalseにするために一時的にスタックにfalseを積んで、popでエラーメッセージの構築に使った領域を捨てます。

# `0032 setn 8`まで
false
"String === :sym does not return true"

false
String
:sym
:sym

nil # cached #deconstruct value
"String === :sym does not return true" # error_string
false # key_error_p <- ここを`0032 setn 8`で更新する
nil # key_error_matchee
nil # key_error_key

# `0035 pop`まで
false
String
:sym
:sym

0036以降の命令はStirng === :symがマッチしたときも、マッチしていないときも実行されます。 そのため0035 popまで終わった時点でもともとジャンプしてきた0016 branchif 36の時点とスタックが同じになるように調整されています(最初に確保した5つの領域の値は変わっていることもありますが)。

0036から0046をみていきましょう。

#
# マッチしたときはここに飛んでくる
#
# 0036 setn                                   2
# 0038 pop
# 0039 pop
# 0040 branchif                               88
# 0042 putspecialobject                       1                         (   1)
# 0044 topn                                   4
# 0046 branchif                               64
...
# `in String :expr_str`のbodyの部分
#
# 0088 adjuststack                            6                         (   2)
# 0090 putobject                              :expr_str                 (   3)[Li]
# 0092 leave

setnpopをつかってString:symを捨てます。

# `0039 pop`まで
false # String === :sym の結果
:sym

nil # cached #deconstruct value
"String === :sym does not return true" # error_string
false # key_error_p
nil # key_error_matchee
nil # key_error_key

0040 branchifString === :symの結果をみて分岐することを意味しています。 0088以降のバイトコードはinのbodyに当たる命令列なので、0088にジャンプしたあとはbodyを評価してcase全体を抜けることになります。

マッチが成功しない場合をみていきましょう。 RubyVMFrozenCoreをスタックに積んだのち、最初に確保した領域からkey_error_pをコピーして0046 branchif 64を行います。

# `0044 topn 4`まで
false # key_error_p
RubyVMFrozenCore
:sym

nil # cached #deconstruct value
"String === :sym does not return true" # error_string
false # key_error_p
nil # key_error_matchee
nil # key_error_key

key_error_pの値によってNoMatchingPatternErrorを投げるかNoMatchingPatternKeyErrorを投げるかが変わります。

# key_error_p == false
#
# 0048 putobject                              NoMatchingPatternError
# 0050 putspecialobject                       1
# 0052 putobject                              "%p: %s"
# 0054 topn                                   4
# 0056 topn                                   7
# 0058 opt_send_without_block                 <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE>
# 0060 opt_send_without_block                 <calldata!mid:core#raise, argc:2, ARGS_SIMPLE>
# 0062 jump                                   84

# key_error_p == true
#
# 0064 putobject                              NoMatchingPatternKeyError
# 0066 putspecialobject                       1
# 0068 putobject                              "%p: %s"
# 0070 topn                                   4
# 0072 topn                                   7
# 0074 opt_send_without_block                 <calldata!mid:core#sprintf, argc:3, ARGS_SIMPLE>
# 0076 topn                                   7
# 0078 topn                                   9
# 0080 opt_send_without_block                 <calldata!mid:new, argc:3, kw:[#<Symbol:0x000000000023310c>,#<Symbol:0x000000000021f10c>], KWARG>
# 0082 opt_send_without_block                 <calldata!mid:core#raise, argc:1, ARGS_SIMPLE>
#
# key_error_p == falseのときは最後にここに飛んでくる
#
# 0084 adjuststack                            7
# 0086 putnil
# 0087 leave                                                            (   3)

single_patternのときもin pattern bodypatternbodyに依存せず例外を発生させる部分のバイトコードを生成することができます。 そのためcompile_case3関数の修正は特に必要ないでしょう。

patternが1つのケースを実行してみます。

case :sym
in String
  :expr_str
end
#=> ../../test.rb:1:in '<main>': :sym: String === :sym does not return true (NoMatchingPatternError)

良さそうですね

まとめ

今日の成果です。

  • パターンマッチングの外観を眺めて整理した
  • Value patternのうちConstの対応をした(in String ...)

しばらくパターンマッチングが続くと思うので、ブレイクダウンした結果と現在の進捗をまとめておきます。

  • case expr in pattern ...
  • expr in pattern
  • expr => pattern

  • Value pattern

    • p_primitive ("str", 1, :symなど)
    • range_expr (1...3など)
    • p_var_ref (^varなど)
    • p_expr_ref (^(cmd 1, 2)など)
    • p_const (A, ::A, A::Bなど)
  • Variable pattern
  • Array pattern
  • Hash pattern
  • Find pattern
  • Alternative pattern
  • As pattern
  • 後置ifと後置unless

今回はここまで!




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

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