以下の内容はhttps://yui-knk.hatenablog.com/entry/2026/03/01/114534より取得しました。


Ruby Parser開発日誌 (24-39) - parse.yが生成するノードを変える ー パターンマッチング その2 (Value patternとVariable pinning)

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

p_primitiveには"str", 1, :symが、range_exprには1...3などが含まれます。 parse.yでは以下の生成規則が対応しています。 ここではとくにアクションが設定されていないので、inline_primaryという生成規則が返す値がそのままp_primitiveの値になります。

p_primitive     : inline_primary
                ...
                ;

書き換え後のInNodeにおけるpatternではStringNodeIntegerNodeがそのまま設定されています。 そのため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ではPinnedVariableNodevariableを取り出してコンパイルします。

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



以上の内容はhttps://yui-knk.hatenablog.com/entry/2026/03/01/114534より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14