38日目: パターンマッチングはじめました
前回はcase whenをやったので、今回からパターンマッチングに取り組みたいと思います。
おそらく1回で終わらないと思うので、数回に分けて対応していければいいなと考えています。
パターンマッチングの文法を洗い出す
パターンマッチングは豊富な文法要素をもっています。
まずはそれを種類ごとに整理しておきましょう。
パターンマッチング全体の構造として3つの異なる文法が用意されています。
case expr in pattern ...
expr in pattern
expr => pattern
それぞれ具体的なコードは以下の通りです。
case ary
in [0, 1]
expr1
in [1, 2]
expr2
else
expr_else
end
ary in [1, 2]
ary => [1, 2]
パターンについても複数の異なるパターンが用意されています。
多くない...?
- Value pattern
- Variable pattern
- Array pattern
- Hash pattern
- Find pattern
- Alternative pattern
- As pattern
それぞれ具体的には以下のようなコードになります。
a = 1
case obj
in String
expr
in ^a
expr
in v
expr
in [1, 2]
expr
in {k: :v}
expr
in [*a, b, c, *d]
expr
in [1, 2] | {k: :v}
expr
in Integer => a, Integer
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の変更
書き換え前後のノードを見比べてみます。
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($$, &@$);
}
}
;
p_cases : opt_else
{
if ($1) {
$$ = NEW_RB_ARRAY($1, $$);
}
}
| p_case_body
;
大枠の構造ができたので、次にパターンの部分についてみていきます。
今回のケースでいうと、in Stringやin Integerの部分はConstantReadNodeとして表現されています。
この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"
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);
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) {
ADD_INSN(head, line_node, putnil);
ADD_INSN(head, line_node, putnil);
ADD_INSN1(head, line_node, putobject, Qfalse);
ADD_INSN(head, line_node, putnil);
}
ADD_INSN(head, line_node, putnil);
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) {
ADD_INSN(head, line_node, putnil);
ADD_INSN(head, line_node, putnil);
ADD_INSN1(head, line_node, putobject, Qfalse);
ADD_INSN(head, line_node, putnil);
}
ADD_INSN(head, line_node, putnil);
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;
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);
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);
NOTE
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);
NOTE
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がないときについてはあとで見ることにします。
if (node) {
ADD_LABEL(cond_seq, elselabel);
ADD_INSN(cond_seq, line_node, pop);
ADD_INSN(cond_seq, line_node, pop);
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;
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);
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 RB_CONSTANT_READ_NODE:
case RB_CONSTANT_PATH_NODE:
CHECK(COMPILE(ret, "case in literal", node));
if (in_single_pattern) {
ADD_INSN1(ret, line_node, dupn, INT2FIX(2));
}
ADD_INSN1(ret, line_node, checkmatch, INT2FIX(VM_CHECKMATCH_TYPE_CASE));
if (in_single_pattern) {
CHECK(iseq_compile_pattern_set_eqq_errmsg(iseq, ret, node, base_index + 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")
p m(1)
p m([])
よさそうですね。
elseがないケース
ここで一旦case .... inの全体の構造に戻って、elseがないときのことを考えてみましょう。
elseがなくて、いずれのパターンにもマッチしないときはNoMatchingPatternError例外が投げられます。
case :sym
in String
:expr_str
in Integer
:expr_int
end
このコードに対応するバイトコードは以下のようになっています。
これはRubyVMFrozenCore#raise(NoMatchingPatternError, :sym)を実行して例外を投げていると言えます。
compile.cの該当する部分は以下のようになっています。
ここはノードの種類に依存していないので書き換えなしで動くはずです。
static int
compile_case3(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const orig_node, int popped)
{
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
single_patternのとき
compile_case3関数にはsingle_pattern(パターンが1つ)のときの分岐が存在します。
まずはバイトコードを眺めてみましょう。
case :sym
in String
:expr_str
end
このコードからバイトコードを生成すると以下のようなバイトコードが生成されます。
なんか異様に長いんだが??
パターンが2つのときのほうがバイトコードが短くて面食らいますね。
順を追ってみていきましょう。
まず最初にスタックに5つの領域を確保します。
これは後々使うタイミングで説明します。
次にString === :symを実行して結果に応じてjumpします。
このバイトコードによってスタックがどうなるかを確認しておきましょう(最初に確保した部分は変わらないので一旦無視します)。
String
:sym
String
:sym
:sym
false
false
String
:sym
:sym
false
String
:sym
:sym
今回のケースではStirng === :symがfalseなので、ここではjumpしません。
続く0018から0035では主にエラーメッセージの構築を行います。
:sym
String
"%p === %p does not return true"
RubyVMFrozenCore
false
String
:sym
:sym
"String === :sym does not return true"
false
String
:sym
:sym
nil
"String === :sym does not return true"
false
nil
nil
#sprintfをつかってエラーメッセージを構築したら、最初に確保したスタックのうち下から4つ目の領域にメッセージをコピーします。
この領域はerror_stringとあるように、例外に渡すメッセージのための領域のようです。
その後key_error_pをfalseにするために一時的にスタックにfalseを積んで、popでエラーメッセージの構築に使った領域を捨てます。
false
"String === :sym does not return true"
false
String
:sym
:sym
nil
"String === :sym does not return true"
false
nil
nil
false
String
:sym
:sym
0036以降の命令はStirng === :symがマッチしたときも、マッチしていないときも実行されます。
そのため0035 popまで終わった時点でもともとジャンプしてきた0016 branchif 36の時点とスタックが同じになるように調整されています(最初に確保した5つの領域の値は変わっていることもありますが)。
0036から0046をみていきましょう。
...
setnとpopをつかってStringと:symを捨てます。
false
:sym
nil
"String === :sym does not return true"
false
nil
nil
0040 branchifはString === :symの結果をみて分岐することを意味しています。
0088以降のバイトコードはinのbodyに当たる命令列なので、0088にジャンプしたあとはbodyを評価してcase全体を抜けることになります。
マッチが成功しない場合をみていきましょう。
RubyVMFrozenCoreをスタックに積んだのち、最初に確保した領域からkey_error_pをコピーして0046 branchif 64を行います。
false
RubyVMFrozenCore
:sym
nil
"String === :sym does not return true"
false
nil
nil
key_error_pの値によってNoMatchingPatternErrorを投げるかNoMatchingPatternKeyErrorを投げるかが変わります。
single_patternのときもin pattern bodyのpatternやbodyに依存せず例外を発生させる部分のバイトコードを生成することができます。
そのためcompile_case3関数の修正は特に必要ないでしょう。
patternが1つのケースを実行してみます。
case :sym
in String
:expr_str
end
良さそうですね
まとめ
今日の成果です。
- パターンマッチングの外観を眺めて整理した
- 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
今回はここまで!