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


Ruby Parser開発日誌 (24-40) - parse.yが生成するノードを変える ー パターンマッチング その3 (Variable patternと後置if, unless)

40日目: Variable patternと後置if, unlessに対応する

前回はValue patternの対応をしました。 今回はVariable patternと後置if, unlessに取り組みます。

Variable pattern

Variable patternもしくはvariable captureとは、パターンマッチングによるローカル変数の束縛のことです。 以下のコードではa = v相当の処理が行われます。

def m(v)
  case v

  # Before
  #
  # @ NODE_IN (id: 8, line: 3, location: (3,2)-(6,9))
  # +- nd_head:
  # |   @ NODE_LASGN (id: 5, line: 3, location: (3,5)-(3,6))
  # |   +- nd_vid: :a
  # |   +- nd_value:
  # |       (null node)

  # After
  #
  # @ InNode (location: (3,2)-(4,5))
  # +-- pattern:
  # |   @ LocalVariableTargetNode (location: (3,5)-(3,6))
  # |   +-- name: :a
  # |   +-- depth: 0
  in a
    a
  else
    :else
  end
end

対応するノードは書き換え前がNODE_LASGNで、書き換え後がLocalVariableTargetNodeです。 parse.yではアクションで呼び出す関数を変更してTargetNodeが生成されるようにします。

@@ -6372,7 +6372,7 @@ p_variable        : tIDENTIFIER
                     {
                         error_duplicate_pattern_variable(p, $1, &@1);
                     /*% ripper: var_field!($:1) %*/
-                        $$ = assignable(p, $1, 0, &@$);
+                        $$ = assignable_target(p, $1, &@$);
                     }
                 ;

Variable patternの場合のバイトコードはsetlocaljumpからなります。

# `case v`
#
# 0000 putnil                                                           (   3)[LiCa]
# 0001 getlocal_WC_0                          v@0                       (   2)
# 0003 dup                                                              (   3)

# `in a`
#
# 0004 setlocal_WC_0                          a@1
# 0006 jump                                   13

# `else`
#
# 0008 pop                                                              (   6)
# 0009 pop
# 0010 putobject                              :else[Li]
# 0012 leave                                                            (   8)[Re]

# `in a`のbody
#
# 0013 adjuststack                            2                         (   3)
# 0015 getlocal_WC_0                          a@1                       (   4)[Li]
# 0017 leave                                                            (   8)[Re]
def m(v)
  case v
  in a
    a
  else
    :else
  end
end

書き換え前のコンパイラをみると以下の3つを行なっています。

  1. Alternative patternのときに変数の束縛を禁止するためのチェックをする
  2. setlocal命令を生成するためにindexとlevelを解決する
  3. setlocal命令とjump命令を生成する
      case NODE_LASGN: {
        struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq);
        ID id = RNODE_LASGN(node)->nd_vid;
        int idx = ISEQ_BODY(body->local_iseq)->local_table_size - get_local_var_idx(iseq, id);

        if (in_alt_pattern) {
            const char *name = rb_id2name(id);
            if (name && strlen(name) > 0 && name[0] != '_') {
                COMPILE_ERROR(ERROR_ARGS "illegal variable in alternative pattern (%"PRIsVALUE")",
                              rb_id2str(id));
                return COMPILE_NG;
            }
        }

        ADD_SETLOCAL(ret, line_node, idx, get_lvar_level(iseq));
        ADD_INSNL(ret, line_node, jump, matched);
        break;
      }
      case NODE_DASGN: {
        int idx, lv, ls;
        ID id = RNODE_DASGN(node)->nd_vid;

        idx = get_dyna_var_idx(iseq, id, &lv, &ls);

        if (in_alt_pattern) {
            const char *name = rb_id2name(id);
            if (name && strlen(name) > 0 && name[0] != '_') {
                COMPILE_ERROR(ERROR_ARGS "illegal variable in alternative pattern (%"PRIsVALUE")",
                              rb_id2str(id));
                return COMPILE_NG;
            }
        }

        if (idx < 0) {
            COMPILE_ERROR(ERROR_ARGS "NODE_DASGN: unknown id (%"PRIsVALUE")",
                          rb_id2str(id));
            return COMPILE_NG;
        }
        ADD_SETLOCAL(ret, line_node, ls - idx, lv);
        ADD_INSNL(ret, line_node, jump, matched);
        break;
      }

ノードの修正にあわせてcompile_lasgn_lhs関数を使うように書き換えます。

      case RB_LOCAL_VARIABLE_TARGET_NODE: {
        ID id = RB_NODE_LOCAL_VARIABLE_TARGET(node)->name;

        if (in_alt_pattern) {
            const char *name = rb_id2name(id);
            if (name && strlen(name) > 0 && name[0] != '_') {
                COMPILE_ERROR(ERROR_ARGS "illegal variable in alternative pattern (%"PRIsVALUE")",
                              rb_id2str(id));
                return COMPILE_NG;
            }
        }

        CHECK(compile_lasgn_lhs(iseq, ret, node, id));
        ADD_INSNL(ret, line_node, jump, matched);
        break;
      }

buildして動作を確認します。

def m(v)
  case v
  in a
    a
  else
    :else
  end
end

p m(:t)
#=> :t
p m(:f)
#=> :f
p m(:e)
#=> :e

後置ifと後置unless

パターンマッチングではpatternのあとにifやunlessを書くことができます。

def m(v, cond)
  case v

  in a if cond
    a
  else
    :else
  end
end

p m(:t, true)
#=> :t
p m(:t, false)
#=> :else

書き換え前のノードはNODE_IF、書き換え後のノードはIfNodeです。 書き換え前後で構造が変化しないのでparse.yの修正は不要です。

  # Before
  #
  # @ NODE_IN (id: 11, line: 3, location: (3,2)-(6,9))
  # +- nd_head:
  # |   @ NODE_IF (id: 8, line: 3, location: (3,5)-(3,14))*
  # |   +- nd_cond:
  # |   |   @ NODE_LVAR (id: 7, line: 3, location: (3,10)-(3,14))
  # |   |   +- nd_vid: :cond
  # |   +- nd_body:
  # |   |   @ NODE_LASGN (id: 6, line: 3, location: (3,5)-(3,6))
  # |   |   +- nd_vid: :a
  # |   |   +- nd_value:
  # |   |       (null node)

  # After
  #
  # @ InNode (location: (3,2)-(4,5))
  # +-- pattern:
  # |   @ IfNode (location: (3,5)-(3,14))
  # |   +-- predicate:
  # |   |   @ LocalVariableReadNode (location: (3,10)-(3,14))
  # |   |   +-- name: :cond
  # |   +-- statements:
  # |   |   @ StatementsNode (location: (3,5)-(3,6))
  # |   |   +-- body: (length: 1)
  # |   |       +-- @ LocalVariableTargetNode (location: (3,5)-(3,6))
  # |   |           +-- name: :a
  in a if cond

生成されるバイトコードはifのbodyに当たるsetlocal aを実行してから条件部分のgetlocal condbranchifを行うようになっています。

# `case v`
#
# 0000 putnil                                                           (   3)[LiCa]
# 0001 getlocal_WC_0                          v@0                       (   2)
# 0003 dup                                                              (   3)

# `in a if cond`
#
# 0004 setlocal_WC_0                          a@2
# 0006 getlocal_WC_0                          cond@1
# 0008 branchif                               15

# `else ...`
#
# 0010 pop                                                              (   6)
# 0011 pop
# 0012 putobject                              :else[Li]
# 0014 leave                                                            (   8)[Re]

# `in a if cond`のbody
#
# 0015 adjuststack                            2                         (   3)
# 0017 getlocal_WC_0                          a@2                       (   4)[Li]
# 0019 leave                                                            (   8)[Re]
def m(v, cond)
  case v
  in a if cond
    a
  else
    :else
  end
end

compile.cの変更は構造体に合わせて修正をするだけです。

      case RB_IF_NODE:
      case RB_UNLESS_NODE: {
        LABEL *match_failed;
        match_failed = unmatched;
        CHECK(iseq_compile_pattern_match(iseq, ret, (NODE *)RB_NODE_IF(node)->statements, unmatched, in_single_pattern, in_alt_pattern, base_index, use_deconstructed_cache));
        CHECK(COMPILE(ret, "case in if", RB_NODE_IF(node)->predicate));
        if (in_single_pattern) {
            LABEL *match_succeeded;
            match_succeeded = NEW_LABEL(line);

            ADD_INSN(ret, line_node, dup);
            if (nd_type_p(node, NODE_IF)) {
                ADD_INSNL(ret, line_node, branchif, match_succeeded);
            }
            else {
                ADD_INSNL(ret, line_node, branchunless, match_succeeded);
            }

            ADD_INSN1(ret, line_node, putobject, rb_fstring_lit("guard clause does not return true")); // (1)
            ADD_INSN1(ret, line_node, setn, INT2FIX(base_index + CASE3_BI_OFFSET_ERROR_STRING + 1 /* (1) */)); // (2)
            ADD_INSN1(ret, line_node, putobject, Qfalse);
            ADD_INSN1(ret, line_node, setn, INT2FIX(base_index + CASE3_BI_OFFSET_KEY_ERROR_P + 2 /* (1), (2) */));

            ADD_INSN(ret, line_node, pop);
            ADD_INSN(ret, line_node, pop);

            ADD_LABEL(ret, match_succeeded);
        }
        if (nd_type_p(node, RB_IF_NODE)) {
            ADD_INSNL(ret, line_node, branchunless, match_failed);
        }
        else {
            ADD_INSNL(ret, line_node, branchif, match_failed);
        }
        ADD_INSNL(ret, line_node, jump, matched);
        break;
      }

実際にコードを実行して挙動を確認します。

def m(v, cond)
  case v
  in a if cond
    a
  else
    :else
  end
end

p m(:t, true)
#=> :t
p m(:t, false)
#=> :else

まとめ

  • Variable patternに対応した
  • 後置ifと後置unlessに対応した

パターンマッチング全体の進捗は以下の通りです。

  • 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/02/155905より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

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