39日目: Value patternに対応する
前回はcase ... in String ... endの対応をしました。
今回はString以外のValue patternに取り組みます。
文法の面からみるとValue patternは以下のように分類できます。
このうちp_const (A, ::A, A::Bなど)は前回対応したので、残りを実装していきましょう。
- 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など)
- p_primitive (
p_primitiveとrange_expr
p_primitiveには"str", 1, :symが、range_exprには1...3などが含まれます。
parse.yでは以下の生成規則が対応しています。
ここではとくにアクションが設定されていないので、inline_primaryという生成規則が返す値がそのままp_primitiveの値になります。
p_primitive : inline_primary
...
;
書き換え後のInNodeにおけるpatternではStringNodeやIntegerNodeがそのまま設定されています。
そのためparse.yのアクションを変更する必要はありません。
case v # @ InNode (location: (2,0)-(3,7)) # +-- pattern: # | @ StringNode (location: (2,3)-(2,8)) # | +-- unescaped: "str" in "str" :expr # @ InNode (location: (4,0)-(5,7)) # +-- pattern: # | @ IntegerNode (location: (4,3)-(4,4)) # | +-- value: 1 in 1 :expr # @ InNode (location: (6,0)-(7,7)) # +-- pattern: # | @ SymbolNode (location: (6,3)-(6,7)) # | +-- unescaped: "sym" in :sym :expr end
次にcompile.cについてです。
これらのvalue patternのときに生成される命令をみてみるとputobjectにコンパイルされていることがわかります。
# 0000 putnil ( 2)[Li] # 0001 putself ( 1) # 0002 opt_send_without_block <calldata!mid:v, argc:0, FCALL|VCALL|ARGS_SIMPLE> # in "str" # # 0004 dup ( 2) # 0005 putchilledstring "str" # 0007 checkmatch 2 # 0009 branchif 36 # in 1 # # 0011 dup ( 4) # 0012 putobject_INT2FIX_1_ # 0013 checkmatch 2 # 0015 branchif 41 # in :sym # # 0017 dup ( 6) # 0018 putobject :sym # 0020 checkmatch 2 # 0022 branchif 46 # ...
これは通常のコンパイルに任せればいいので、iseq_compile_pattern_each関数では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_ARYPTN: { ... break; } case NODE_FNDPTN: { ... break; } case NODE_HSHPTN: { ... break; } case RB_SYMBOL_NODE: case RB_REGULAR_EXPRESSION_NODE: case RB_SOURCE_LINE_NODE: case RB_INTEGER_NODE: case RB_FLOAT_NODE: case RB_RATIONAL_NODE: case RB_IMAGINARY_NODE: case RB_SOURCE_FILE_NODE: case RB_SOURCE_ENCODING_NODE: case RB_STRING_NODE: case RB_X_STRING_NODE: case RB_INTERPOLATED_STRING_NODE: case RB_INTERPOLATED_SYMBOL_NODE: case RB_INTERPOLATED_REGULAR_EXPRESSION_NODE: case RB_ARRAY_NODE: case RB_LAMBDA_NODE: case RB_RANGE_NODE: case RB_CONSTANT_READ_NODE: case RB_LOCAL_VARIABLE_READ_NODE: case RB_INSTANCE_VARIABLE_READ_NODE: case RB_CLASS_VARIABLE_READ_NODE: case RB_GLOBAL_VARIABLE_READ_NODE: case RB_TRUE_NODE: case RB_FALSE_NODE: case RB_SELF_NODE: case RB_NIL_NODE: case RB_CONSTANT_PATH_NODE: 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; }
動作を確認しておきます。
def m(v) case v in "str" :expr_str in 1 :expr_int in :sym :expr_sym in 1...10 :expr_range end end p m("str") #=> :expr_str p m(1) #=> :expr_int p m(:sym) #=> :expr_sym p m(5) #=> :expr_range
p_var_refとvariable pinning
p_var_refは2つの生成規則からなります。
p_var_ref : '^' tIDENTIFIER { NODE *n = gettable(p, $2, &@$); if (!n) { n = NEW_ERROR(&@$); } else if (!(nd_type_p(n, NODE_LVAR) || nd_type_p(n, NODE_DVAR))) { compile_error(p, "%"PRIsVALUE": no such local variable", rb_id2str($2)); } $$ = n; /*% ripper: var_ref!($:2) %*/ } | '^' nonlocal_var { if (!($$ = gettable(p, $2, &@$))) $$ = NEW_ERROR(&@$); /*% ripper: var_ref!($:2) %*/ } ;
パターンマッチングのパターンに変数の値を使うときは^をつける必要があります。
in a ...と書くと、それはaに対する代入になるためです。
def m(v) a = 0 case v in a a else :else end end def m2(v) a = 0 case v in ^a a else :else end end p m(0) #=> 0 p m(1) #=> 1 p m2(0) #=> 0 p m2(1) #=> :else
ノードとしてはPinnedVariableNodeでラップすることになります。
def m(v) a = 0 case v # @ InNode (location: (5,2)-(6,5)) # +-- pattern: # | @ PinnedVariableNode (location: (5,5)-(5,7)) # | +-- variable: # | | @ LocalVariableReadNode (location: (5,6)-(5,7)) # | | +-- name: :a # | | +-- depth: 0 # | +-- operator_loc: (5,5)-(5,6) = "^" in ^a a # @ InNode (location: (15,2)-(16,6)) # +-- pattern: # | @ PinnedVariableNode (location: (15,5)-(15,8)) # | +-- variable: # | | @ InstanceVariableReadNode (location: (15,6)-(15,8)) # | | +-- name: :@b # | +-- operator_loc: (15,5)-(15,6) = "^" in ^@b @b else :else end end
parse.yでは素直にPinnedVariableNodeを生成するだけです。
@@ -6380,15 +6380,24 @@ p_var_ref : '^' tIDENTIFIER if (!n) { n = NEW_ERROR(&@$); } - else if (!(nd_type_p(n, NODE_LVAR) || nd_type_p(n, NODE_DVAR))) { + else if (!(nd_type_p(n, RB_LOCAL_VARIABLE_READ_NODE))) { compile_error(p, "%"PRIsVALUE": no such local variable", rb_id2str($2)); } + else { + n = NEW_RB_PINNED_VARIABLE(n, &@1, &@$); + } $$ = n; /*% ripper: var_ref!($:2) %*/ } | '^' nonlocal_var { - if (!($$ = gettable(p, $2, &@$))) $$ = NEW_ERROR(&@$); + NODE *n = gettable(p, $2, &@$); + if (!n) { + $$ = NEW_ERROR(&@$); + } + else { + $$ = NEW_RB_PINNED_VARIABLE(n, &@1, &@$); + } /*% ripper: var_ref!($:2) %*/ } ;
compile.cではPinnedVariableNodeのvariableを取り出してコンパイルします。
@@ -8339,6 +8339,12 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c // ADD_INSNL(ret, line_node, jump, unmatched); // break; // } + case RB_PINNED_VARIABLE_NODE: + node = RB_NODE_PINNED_VARIABLE(node)->variable; + goto compile_value; case RB_SYMBOL_NODE: case RB_REGULAR_EXPRESSION_NODE: case RB_SOURCE_LINE_NODE: ... compile_value: 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;
^varを含むコードを実行してみます。
def m(v) a = 0 @b = 1 case v in ^a :a in ^@b :@b else :else end end p m(0) # #=> :a p m(1) # #=> :@b p m(2) # #=> :else
p_expr_ref
p_expr_refという生成規則は^(cmd 1, 2)のように^()の中にexprを書くことができるというルールです。
書き換え前はNODE_BLOCKで、書き換え後はPinnedExpressionNodeで表現しています。
def m(v) c = true case v # Before # # @ NODE_IN (id: 14, line: 5, location: (5,2)-(8,9)) # +- nd_head: # | @ NODE_BLOCK (id: 11, line: 5, location: (5,5)-(5,19)) # | +- nd_head (1): # | @ NODE_IF (id: 10, line: 5, location: (5,7)-(5,18))* # After # # @ InNode (location: (5,2)-(6,9)) # +-- pattern: # | @ PinnedExpressionNode (location: (5,5)-(5,19)) # | +-- expression: # | | @ IfNode (location: (5,7)-(5,18)) in ^(c ? :t : :f) :expr else :else end end
parse.yでは生成するノードを変更します。
p_expr_ref : '^' tLPAREN expr_value rparen
{
- $$ = NEW_BLOCK($3, &@$);
+ $$ = NEW_RB_PINNED_EXPRESSION($3, &@$, &@1, &@2, &@4);
/*% ripper: begin!($:3) %*/
}
;
compile.cではPinnedVariableNodeと同様にexpressionを取り出してコンパイルするようにします。
@@ -8339,6 +8339,12 @@ iseq_compile_pattern_each(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *c // ADD_INSNL(ret, line_node, jump, unmatched); // break; // } case RB_PINNED_VARIABLE_NODE: node = RB_NODE_PINNED_VARIABLE(node)->variable; goto compile_value; + case RB_PINNED_EXPRESSION_NODE: + node = RB_NODE_PINNED_EXPRESSION(node)->expression; + goto compile_value; case RB_SYMBOL_NODE: case RB_REGULAR_EXPRESSION_NODE: case RB_SOURCE_LINE_NODE:
動作確認します。
def m(v) c = true case v in ^(c ? :t : :f) :expr else :else end end p m(:t) # # #=> :expr p m(:f) # # #=> :else p m(:e) # # #=> :else
まとめ
今日の成果です。
- Value patternのうちConst以外の残りに対応した
パターンマッチング全体の進捗は以下の通りです。
Value patternp_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