38日目: パターンマッチングはじめました
前回はcase whenをやったので、今回からパターンマッチングに取り組みたいと思います。
おそらく1回で終わらないと思うので、数回に分けて対応していければいいなと考えています。
パターンマッチングの文法を洗い出す
パターンマッチングは豊富な文法要素をもっています。 まずはそれを種類ごとに整理しておきましょう。
パターンマッチング全体の構造として3つの異なる文法が用意されています。
case expr in pattern ...expr in patternexpr => 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
パターンについても複数の異なるパターンが用意されています。 多くない...?
- Value pattern
- Variable pattern
- Array pattern
- Hash pattern
- Find pattern
- Alternative pattern
- 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つの差異があることがわかります。
NODE_CASE3の代わりにCaseMatchNodeを使うNODE_INがリスト構造ではなくなるelseにあたるノードが直接CaseMatchNodeに紐づく
これは前回取り組んだcase whenのケースと同じような構造の変化です。
parse.yのアクションに関していうと、p_case_bodyでInNodeを常に配列の先頭に入れるようにして、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 Stringやin 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 ::Integerやin A::Bを書くこともできます。
それぞれ期待されるノードはConstantPathNodeとConstantPathNode(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つずつ順番にコンパイルしていきます。
bodyはbody_seqに、patternはcond_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 === :symがfalseなので、ここでは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_pをfalseにするために一時的にスタックに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
setnとpopをつかって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 branchifはString === :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 bodyのpatternやbodyに依存せず例外を発生させる部分のバイトコードを生成することができます。
そのため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 patternexpr => patternValue 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など)
- p_primitive (
- Variable pattern
- Array pattern
- Hash pattern
- Find pattern
- Alternative pattern
- As pattern
- 後置ifと後置unless
今回はここまで!