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 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

次回はFind patternの対応をする予定です。