Ruby Parser開発日誌 (24-42) - parse.yが生成するノードを変える ー パターンマッチング その5 (Hash pattern)
42日目: Hash patternに対応する
前回はArray patternに対応をしました。 今回はHash patternに取り組みます。
Hash pattern
Hash patternもまた読んで字の如くハッシュを表したパターンです。
def m(v) case v in {k: :v, k2: :v2} p :a in {k: :v} p :b else p :c end end m({k: :v}) #=> :b m({k: :v, k2: :v2}) #=> :a m({k: :v, k3: :v3}) #=> :b m({k: :v0}) #=> :c
書き換え前後のノードを比較するとフィールド名は異なるものの、3つのフィールドが存在することがわかります。 Array patternと同様にフィールド名に気をつけて書き直せば良さそうです。
def m(v) case v # Before # # @ NODE_HSHPTN (id: 10, line: 3, location: (3,6)-(3,11)) # +- nd_pconst: # | (null node) # +- nd_pkwargs: # | @ NODE_HASH (id: 9, line: 3, location: (3,6)-(3,11)) # | +- nd_brace: 0 (keyword argument) # | +- nd_head: # | @ NODE_LIST (id: 7, line: 3, location: (3,6)-(3,11)) # +- nd_pkwrestarg: # (null node) # After # # @ HashPatternNode (location: (3,5)-(3,12)) # +-- constant: nil # +-- elements: (length: 1) # | +-- @ AssocNode (location: (3,6)-(3,11)) # +-- rest: nil in {k: :v} p :a end end
それぞれのフィールドの意味は以下のとおりです。
| Before | After | |
|---|---|---|
| nd_pconst | constant | in H(k: :v) の H |
| nd_pkwargs | elements | in {k: :v} の {k: :v} |
| nd_pkwrestarg | rest | in {k: :v, **rest} の **rest |
ところでrestの部分には4つの形式があります。
| Before | After | |
|---|---|---|
in {k: :v} |
nil | nil |
in {k: :v, **rest} |
NODE_LASGN |
AssocSplatNode |
in {k: :v, **} |
nil | AssocSplatNode (valueがnil) |
in {k: :v, **nil} |
NODE_SPECIAL_NO_REST_KEYWORD |
NoKeywordsParameterNode |
ちなみにin {k: :v}とin k: :vではHashPatternNodeのopening_locとclosing_locを除き差異はありません。
parse.yを書き換える
parse.yの書き換えは基本的に生成する構造体を変更するだけです。
特筆すべき点をあえてあげるなら、**restや**、**nilを表す生成規則がIDではなくNODE *を生成するようになり、その結果new_hash_pattern_tail関数内部での分岐が消えることくらいかと思います。
p_kwrest : kwrest_mark tIDENTIFIER
{
- $$ = $2;
+ $$ = NEW_RB_ASSOC_SPLAT(assignable_target(p, $2, &@2), &@$, &@1);
/*% ripper: var_field!($:2) %*/
}
| kwrest_mark
{
- $$ = 0;
+ $$ = NEW_RB_ASSOC_SPLAT(0, &@$, &@1);
/*% ripper: Qnil %*/
}
;
p_kwnorest : kwrest_mark keyword_nil
{
- $$ = 0;
+ $$ = NEW_RB_NO_KEYWORDS_PARAMETER(&@$, &@1, &@2);
}
;
p_any_kwrest : p_kwrest
| p_kwnorest
{
- $$ = idNil;
/*% ripper: var_field!(ID2VAL(idNil)) %*/
}
;
-static NODE*
-new_hash_pattern_tail(struct parser_params *p, NODE *kw_args, ID kw_rest_arg, const YYLTYPE *loc)
+static rb_node_t *
+new_hash_pattern_tail(struct parser_params *p, rb_node_t *kw_args, rb_node_t *kw_rest_arg, const YYLTYPE *loc)
{
- NODE *node, *kw_rest_arg_node;
-
- if (kw_rest_arg == idNil) {
- kw_rest_arg_node = NODE_SPECIAL_NO_REST_KEYWORD;
- }
- else if (kw_rest_arg) {
- kw_rest_arg_node = assignable(p, kw_rest_arg, 0, loc);
- }
- else {
- kw_rest_arg_node = NULL;
- }
-
- node = NEW_HSHPTN(0, kw_args, kw_rest_arg_node, loc);
-
- return node;
+ return NEW_RB_HASH_PATTERN(0, kw_args, kw_rest_arg, loc);
}
compile.cを書き換える
compile.cを書き換える前に生成されるべきバイトコードを確認しておきましょう。
# 0000 putnil ( 2)[LiCa] # 0001 getlocal_WC_0 v@0 # 0003 dup ( 3) # 0004 dup # 0005 putobject :deconstruct_keys # 0007 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0009 branchunless 50 # 0011 duparray [:k] # 0013 opt_send_without_block <calldata!mid:deconstruct_keys, argc:1, ARGS_SIMPLE> # 0015 dup # 0016 checktype T_HASH # 0018 branchunless 41 # 0020 dup # 0021 putobject :k # 0023 opt_send_without_block <calldata!mid:key?, argc:1, ARGS_SIMPLE> # 0025 branchunless 50 # 0027 dup # 0028 putobject :k # 0030 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0032 putobject :v # 0034 checkmatch 2 # 0036 branchunless 50 # 0038 pop # 0039 jump 107 # 0041 putspecialobject 1 # 0043 putobject TypeError # 0045 putobject "deconstruct_keys must return Hash" # 0047 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0049 pop # 0050 pop # 0051 dup ( 5) # 0052 dup # 0053 putobject :deconstruct_keys # 0055 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0057 branchunless 98 # 0059 duparray [:k2] # 0061 opt_send_without_block <calldata!mid:deconstruct_keys, argc:1, ARGS_SIMPLE> # 0063 dup # 0064 checktype T_HASH # 0066 branchunless 89 # 0068 dup # 0069 putobject :k2 # 0071 opt_send_without_block <calldata!mid:key?, argc:1, ARGS_SIMPLE> # 0073 branchunless 98 # 0075 dup # 0076 putobject :k2 # 0078 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0080 putobject :v2 # 0082 checkmatch 2 # 0084 branchunless 98 # 0086 pop # 0087 jump 115 # 0089 putspecialobject 1 # 0091 putobject TypeError # 0093 putobject "deconstruct_keys must return Hash" # 0095 opt_send_without_block <calldata!mid:core#raise, argc:2, ARGS_SIMPLE> # 0097 pop # 0098 pop # 0099 pop ( 2) # 0100 pop # 0101 putself ( 8)[Li] # 0102 putobject :c # 0104 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0106 leave ( 10)[Re] # 0107 adjuststack 2 ( 3) # 0109 putself ( 4)[Li] # 0110 putobject :a # 0112 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0114 leave ( 10)[Re] # 0115 adjuststack 2 ( 5) # 0117 putself ( 6)[Li] # 0118 putobject :b # 0120 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0122 leave ( 10)[Re] def m(v) case v in {k: :v} p :a in {k2: :v2} p :b else p :c end end
Array patternと同様にはじめにスタックの調整をしてcase vの部分を評価します。
# `compile_case3` function # # 0000 putnil ( 2)[LiCa] # 0001 getlocal_WC_0 v@0 # 0003 dup ( 3)
つぎにvが#deconstruct_keysメソッドを実装しているか確認して、実装しているなら#deconstruct_keysを呼び出し、結果がhashのオブジェクトかどうかを確認します。
# 0004 dup # 0005 putobject :deconstruct_keys # 0007 opt_send_without_block <calldata!mid:respond_to?, argc:1, ARGS_SIMPLE> # 0009 branchunless 50 # 0011 duparray [:k] # 0013 opt_send_without_block <calldata!mid:deconstruct_keys, argc:1, ARGS_SIMPLE> # 0015 dup # 0016 checktype T_HASH # 0018 branchunless 41
hashであることを確認したら:kをキーにもつか、キーにもつ場合は:vと一致するかをチェックします。
チェックが通ったらbodyの部分を実行します。
# 0020 dup # 0021 putobject :k # 0023 opt_send_without_block <calldata!mid:key?, argc:1, ARGS_SIMPLE> # 0025 branchunless 50 # 0027 dup # 0028 putobject :k # 0030 opt_aref <calldata!mid:[], argc:1, ARGS_SIMPLE>[CcCr] # 0032 putobject :v # 0034 checkmatch 2 # 0036 branchunless 50 # 0038 pop # 0039 jump 107 # ... # 0107 adjuststack 2 ( 3) # 0109 putself ( 4)[Li] # 0110 putobject :a # 0112 opt_send_without_block <calldata!mid:p, argc:1, FCALL|ARGS_SIMPLE> # 0114 leave ( 10)[Re]
Array patternと同様にノードの構造が大きく変わるわけではないので、基本的には構造体名やフィールド名の変更にあわせてcompile.cを変更していきます。
一点注意が必要なのがin {k: :v, **}のケースです。
変更前はin {k: :v}と同じノードが生成されていましたが、今回の変更でAssocSplatNode(ただしvalueがnil)というノードが付与されます。
Array patternではin [1, 2]と書いた場合、それは長さが2の配列を意味します。
一方Hash patternでin {k: :v}と書いた場合、それは少なくとも:kをkey、:vをvalueとして持つハッシュを意味します。
def m(v) case v in {k: :v} p :a else p :b end end m({k: :v}) #=> a m({k: :v, k2: :v2}) #=> a
そのためin {k: :v}とin {k: :v, **}でノードを同じにしておくとコンパイル時に都合がよかったのです。
今回の変更でin {k: :v, **}がAssocSplatNodeを持つようになったので、restの有無で分岐するコードは少し修正が必要になります。
// Before if (RNODE_HSHPTN(node)->nd_pkwrestarg) { ADD_SEND(ret, line_node, rb_intern("dup"), INT2FIX(0)); } // After const NODE *nd_rest = RB_NODE_HASH_PATTERN(node)->rest; bool has_rest = nd_rest && !(nd_type_p(nd_rest, RB_ASSOC_SPLAT_NODE) && RB_NODE_ASSOC_SPLAT(nd_rest)->value == NULL); if (has_rest) { ADD_SEND(ret, line_node, rb_intern("dup"), INT2FIX(0)); }
余談ですがin {k: :v, **rest}の場合はキーのチェックの際に#[]ではなく#deleteを使うようになったり、restに対してsetlocalをしたりします。
またin {k: :v, **nil}の場合はキーのチェックの際に#deleteを使い、最後に#empty?でハッシュが空になっているかチェックします。
以上に注意してcompile.cを修正し、挙動を確認します。
def m(v) case v in {k: :v, k2: :v2} p :a in {k: :v} p :b else p :c end end m({k: :v}) #=> :b m({k: :v, k2: :v2}) #=> :a m({k: :v, k3: :v3}) #=> :b m({k: :v0}) #=> :c
よさそうですね。
まとめ
- Hash patternに対応した
パターンマッチング全体の進捗は以下の通りです。
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 patternArray patternHash pattern- Find pattern
- Alternative pattern
- As pattern
後置ifと後置unless
次回はFind patternの対応をする予定です。