Repository: ruby/prism
Pull request #1796 (2023-11-10 01:55:46)

Build the ability to format errors
Previously, we only supported error messages that were constant strings. This works for the most part, but there are some times where we want to include some part of the source in the error message to make it better.

For example, instead of "Token is reserved" it's better to write "_1 is reserved".

To do this, we now support allocating error messages at runtime that are built around format strings.

I don't actually think this should be the real solution here. Ideally we would provide the ability for these fields to be described so that we could template out some nice Ruby syntax error classes. But for now, this gets us closer to passing Ruby spec so I'd like to build this intermediate step.

cc @enebo this should allow us to start fixing some specific error message assertions. Now that we have this, would you be able to open an issue with the error messages that are not lining up?

enebo: @kddnewton sure.  I will start opening issues next week.  In many cases it is not even the dynamic aspect you are fixing here but just more explicit english grammar (like adding an article in prism which was omitted in original warn/error).

Patch:
diff --git a/bin/encodings b/bin/encodings
index 77de0b75..2a6eb629 100755
--- a/bin/encodings
+++ b/bin/encodings
@@ -63,11 +63,13 @@ when "ibm852"       then table(Encoding::IBM852)
 when "ibm855"       then table(Encoding::IBM855)
 when "ibm857"       then table(Encoding::IBM857)
 when "ibm860"       then table(Encoding::IBM860)
+when "ibm863"       then table(Encoding::IBM863)
 when "ibm861"       then table(Encoding::IBM861)
 when "ibm862"       then table(Encoding::IBM862)
 when "ibm864"       then table(Encoding::IBM864)
 when "ibm865"       then table(Encoding::IBM865)
 when "ibm866"       then table(Encoding::IBM866)
+when "ibm869"       then table(Encoding::IBM869)
 when "iso-8859-1"   then table(Encoding::ISO8859_1)
 when "iso-8859-2"   then table(Encoding::ISO8859_2)
 when "iso-8859-3"   then table(Encoding::ISO8859_3)
diff --git a/config.yml b/config.yml
index 6f12a513..ef12f7a7 100644
--- a/config.yml
+++ b/config.yml
@@ -768,6 +768,26 @@ nodes:
 
           foo => [bar => baz]
                  ^^^^^^^^^^^^
+  - name: CaseMatchNode
+    fields:
+      - name: predicate
+        type: node?
+      - name: conditions
+        type: node[]
+      - name: consequent
+        type: node?
+        kind: ElseNode
+      - name: case_keyword_loc
+        type: location
+      - name: end_keyword_loc
+        type: location
+    comment: |
+      Represents the use of a case statement for pattern matching.
+
+          case true
+          in false
+          end
+          ^^^^^^^^^
   - name: CaseNode
     fields:
       - name: predicate
@@ -1880,8 +1900,8 @@ nodes:
       - name: call
         type: node
         kind: CallNode
-      - name: locals
-        type: constant[]
+      - name: targets
+        type: node[]
     comment: |
       Represents writing local variables using a regular expression match with
       named capture groups.
diff --git a/docs/encoding.md b/docs/encoding.md
index 4ff8aa9c..9abbcf9c 100644
--- a/docs/encoding.md
+++ b/docs/encoding.md
@@ -34,6 +34,7 @@ The key of the comment can be either "encoding" or "coding". The value of the co
 * `IBM864`
 * `IBM865`
 * `IBM866`
+* `IBM869`
 * `ISO-8859-1`
 * `ISO-8859-2`
 * `ISO-8859-3`
diff --git a/include/prism/diagnostic.h b/include/prism/diagnostic.h
index c963289c..33a70229 100644
--- a/include/prism/diagnostic.h
+++ b/include/prism/diagnostic.h
@@ -30,6 +30,13 @@ typedef struct {
 
     /** The message associated with the diagnostic. */
     const char *message;
+
+    /**
+     * Whether or not the memory related to the message of this diagnostic is
+     * owned by this diagnostic. If it is, it needs to be freed when the
+     * diagnostic is freed.
+     */
+    bool owned;
 } pm_diagnostic_t;
 
 /**
@@ -73,6 +80,7 @@ typedef enum {
     PM_ERR_CANNOT_PARSE_STRING_PART,
     PM_ERR_CASE_EXPRESSION_AFTER_CASE,
     PM_ERR_CASE_EXPRESSION_AFTER_WHEN,
+    PM_ERR_CASE_MATCH_MISSING_PREDICATE,
     PM_ERR_CASE_MISSING_CONDITIONS,
     PM_ERR_CASE_TERM,
     PM_ERR_CLASS_IN_METHOD,
@@ -248,7 +256,8 @@ typedef enum {
 } pm_diagnostic_id_t;
 
 /**
- * Append a diagnostic to the given list of diagnostics.
+ * Append a diagnostic to the given list of diagnostics that is using shared
+ * memory for its message.
  *
  * @param list The list to append to.
  * @param start The start of the diagnostic.
@@ -258,6 +267,19 @@ typedef enum {
  */
 bool pm_diagnostic_list_append(pm_list_t *list, const uint8_t *start, const uint8_t *end, pm_diagnostic_id_t diag_id);
 
+/**
+ * Append a diagnostic to the given list of diagnostics that is using a format
+ * string for its message.
+ *
+ * @param list The list to append to.
+ * @param start The start of the diagnostic.
+ * @param end The end of the diagnostic.
+ * @param diag_id The diagnostic ID.
+ * @param ... The arguments to the format string for the message.
+ * @return Whether the diagnostic was successfully appended.
+ */
+bool pm_diagnostic_list_append_format(pm_list_t *list, const uint8_t *start, const uint8_t *end, pm_diagnostic_id_t diag_id, ...);
+
 /**
  * Deallocate the internal state of the given diagnostic list.
  *
diff --git a/include/prism/enc/pm_encoding.h b/include/prism/enc/pm_encoding.h
index 7279dfd7..15db4abc 100644
--- a/include/prism/enc/pm_encoding.h
+++ b/include/prism/enc/pm_encoding.h
@@ -159,6 +159,7 @@ extern pm_encoding_t pm_encoding_ascii;
 extern pm_encoding_t pm_encoding_ascii_8bit;
 extern pm_encoding_t pm_encoding_big5;
 extern pm_encoding_t pm_encoding_big5_hkscs;
+extern pm_encoding_t pm_encoding_big5_uao;
 extern pm_encoding_t pm_encoding_cp51932;
 extern pm_encoding_t pm_encoding_cp850;
 extern pm_encoding_t pm_encoding_cp852;
@@ -176,9 +177,11 @@ extern pm_encoding_t pm_encoding_ibm857;
 extern pm_encoding_t pm_encoding_ibm860;
 extern pm_encoding_t pm_encoding_ibm861;
 extern pm_encoding_t pm_encoding_ibm862;
+extern pm_encoding_t pm_encoding_ibm863;
 extern pm_encoding_t pm_encoding_ibm864;
 extern pm_encoding_t pm_encoding_ibm865;
 extern pm_encoding_t pm_encoding_ibm866;
+extern pm_encoding_t pm_encoding_ibm869;
 extern pm_encoding_t pm_encoding_iso_8859_1;
 extern pm_encoding_t pm_encoding_iso_8859_2;
 extern pm_encoding_t pm_encoding_iso_8859_3;
diff --git a/prism.gemspec b/prism.gemspec
index 5f3c666e..4b0f87d4 100644
--- a/prism.gemspec
+++ b/prism.gemspec
@@ -85,7 +85,6 @@ Gem::Specification.new do |spec|
     "lib/prism/visitor.rb",
     "src/diagnostic.c",
     "src/enc/pm_big5.c",
-    "src/enc/pm_big5_hkscs.c",
     "src/enc/pm_cp51932.c",
     "src/enc/pm_euc_jp.c",
     "src/enc/pm_gbk.c",
diff --git a/src/diagnostic.c b/src/diagnostic.c
index c1d930a4..7da8806b 100644
--- a/src/diagnostic.c
+++ b/src/diagnostic.c
@@ -87,6 +87,7 @@ static const char* const diagnostic_messages[PM_DIAGNOSTIC_ID_LEN] = {
     [PM_ERR_CANNOT_PARSE_STRING_PART]           = "Cannot parse the string part",
     [PM_ERR_CASE_EXPRESSION_AFTER_CASE]         = "Expected an expression after `case`",
     [PM_ERR_CASE_EXPRESSION_AFTER_WHEN]         = "Expected an expression after `when`",
+    [PM_ERR_CASE_MATCH_MISSING_PREDICATE]       = "Expected a predicate for a case matching statement",
     [PM_ERR_CASE_MISSING_CONDITIONS]            = "Expected a `when` or `in` clause after `case`",
     [PM_ERR_CASE_TERM]                          = "Expected an `end` to close the `case` statement",
     [PM_ERR_CLASS_IN_METHOD]                    = "Unexpected class definition in a method body",
@@ -201,7 +202,7 @@ static const char* const diagnostic_messages[PM_DIAGNOSTIC_ID_LEN] = {
     [PM_ERR_PARAMETER_NAME_REPEAT]              = "Repeated parameter name",
     [PM_ERR_PARAMETER_NO_DEFAULT]               = "Expected a default value for the parameter",
     [PM_ERR_PARAMETER_NO_DEFAULT_KW]            = "Expected a default value for the keyword parameter",
-    [PM_ERR_PARAMETER_NUMBERED_RESERVED]        = "Token reserved for a numbered parameter",
+    [PM_ERR_PARAMETER_NUMBERED_RESERVED]        = "%.2s is reserved for a numbered parameter",
     [PM_ERR_PARAMETER_ORDER]                    = "Unexpected parameter order",
     [PM_ERR_PARAMETER_SPLAT_MULTI]              = "Unexpected multiple `*` splat parameters",
     [PM_ERR_PARAMETER_STAR]                     = "Unexpected parameter `*`",
@@ -260,8 +261,10 @@ static const char* const diagnostic_messages[PM_DIAGNOSTIC_ID_LEN] = {
 static const char*
 pm_diagnostic_message(pm_diagnostic_id_t diag_id) {
     assert(diag_id < PM_DIAGNOSTIC_ID_LEN);
+
     const char *message = diagnostic_messages[diag_id];
     assert(message);
+
     return message;
 }
 
@@ -273,7 +276,57 @@ pm_diagnostic_list_append(pm_list_t *list, const uint8_t *start, const uint8_t *
     pm_diagnostic_t *diagnostic = (pm_diagnostic_t *) calloc(sizeof(pm_diagnostic_t), 1);
     if (diagnostic == NULL) return false;
 
-    *diagnostic = (pm_diagnostic_t) { .start = start, .end = end, .message = pm_diagnostic_message(diag_id) };
+    *diagnostic = (pm_diagnostic_t) {
+        .start = start,
+        .end = end,
+        .message = pm_diagnostic_message(diag_id),
+        .owned = false
+    };
+
+    pm_list_append(list, (pm_list_node_t *) diagnostic);
+    return true;
+}
+
+/**
+ * Append a diagnostic to the given list of diagnostics that is using a format
+ * string for its message.
+ */
+bool
+pm_diagnostic_list_append_format(pm_list_t *list, const uint8_t *start, const uint8_t *end, pm_diagnostic_id_t diag_id, ...) {
+    va_list arguments;
+    va_start(arguments, diag_id);
+
+    const char *format = pm_diagnostic_message(diag_id);
+    int result = vsnprintf(NULL, 0, format, arguments);
+    va_end(arguments);
+
+    if (result < 0) {
+        return false;
+    }
+
+    pm_diagnostic_t *diagnostic = (pm_diagnostic_t *) calloc(sizeof(pm_diagnostic_t), 1);
+    if (diagnostic == NULL) {
+        return false;
+    }
+
+    size_t length = (size_t) (result + 1);
+    char *message = (char *) malloc(length);
+    if (message == NULL) {
+        free(diagnostic);
+        return false;
+    }
+
+    va_start(arguments, diag_id);
+    vsnprintf(message, length, format, arguments);
+    va_end(arguments);
+
+    *diagnostic = (pm_diagnostic_t) {
+        .start = start,
+        .end = end,
+        .message = message,
+        .owned = true
+    };
+
     pm_list_append(list, (pm_list_node_t *) diagnostic);
     return true;
 }
@@ -287,8 +340,9 @@ pm_diagnostic_list_free(pm_list_t *list) {
 
     for (node = list->head; node != NULL; node = next) {
         next = node->next;
-
         pm_diagnostic_t *diagnostic = (pm_diagnostic_t *) node;
+
+        if (diagnostic->owned) free((void *) diagnostic->message);
         free(diagnostic);
     }
 }
diff --git a/src/enc/pm_big5.c b/src/enc/pm_big5.c
index fc5c4e73..5e9924b4 100644
--- a/src/enc/pm_big5.c
+++ b/src/enc/pm_big5.c
@@ -15,6 +15,22 @@ pm_encoding_big5_char_width(const uint8_t *b, ptrdiff_t n) {
     return 0;
 }
 
+static size_t
+pm_encoding_big5_star_char_width(const uint8_t *b, ptrdiff_t n) {
+    // These are the single byte characters.
+    if (*b < 0x80) {
+        return 1;
+    }
+
+    // These are the double byte characters.
+    if ((n > 1) && (b[0] >= 0x87 && b[0] <= 0xFE) &&
+                   ((b[1] >= 0x40 && b[1] <= 0x7E) || (b[1] >= 0xA1 && b[1] <= 0xFE))) {
+        return 2;
+    }
+
+    return 0;
+}
+
 static size_t
 pm_encoding_big5_alpha_char(const uint8_t *b, ptrdiff_t n) {
     if (pm_encoding_big5_char_width(b, n) == 1) {
@@ -24,6 +40,15 @@ pm_encoding_big5_alpha_char(const uint8_t *b, ptrdiff_t n) {
     }
 }
 
+static size_t
+pm_encoding_big5_star_alpha_char(const uint8_t *b, ptrdiff_t n) {
+    if (pm_encoding_big5_star_char_width(b, n) == 1) {
+        return pm_encoding_ascii_alpha_char(b, n);
+    } else {
+        return 0;
+    }
+}
+
 static size_t
 pm_encoding_big5_alnum_char(const uint8_t *b, ptrdiff_t n) {
     if (pm_encoding_big5_char_width(b, n) == 1) {
@@ -33,6 +58,15 @@ pm_encoding_big5_alnum_char(const uint8_t *b, ptrdiff_t n) {
     }
 }
 
+static size_t
+pm_encoding_big5_star_alnum_char(const uint8_t *b, ptrdiff_t n) {
+    if (pm_encoding_big5_star_char_width(b, n) == 1) {
+        return pm_encoding_ascii_alnum_char(b, n);
+    } else {
+        return 0;
+    }
+}
+
 static bool
 pm_encoding_big5_isupper_char(const uint8_t *b, ptrdiff_t n) {
     if (pm_encoding_big5_char_width(b, n) == 1) {
@@ -42,6 +76,15 @@ pm_encoding_big5_isupper_char(const uint8_t *b, ptrdiff_t n) {
     }
 }
 
+static bool
+pm_encoding_big5_star_isupper_char(const uint8_t *b, ptrdiff_t n) {
+    if (pm_encoding_big5_star_char_width(b, n) == 1) {
+        return pm_encoding_ascii_isupper_char(b, n);
+    } else {
+        return false;
+    }
+}
+
 /** Big5 encoding */
 pm_encoding_t pm_encoding_big5 = {
     .name = "big5",
@@ -51,3 +94,23 @@ pm_encoding_t pm_encoding_big5 = {
     .isupper_char = pm_encoding_big5_isupper_char,
     .multibyte = true
 };
+
+/** Big5-HKSCS encoding */
+pm_encoding_t pm_encoding_big5_hkscs = {
+    .name = "big5-hkscs",
+    .char_width = pm_encoding_big5_star_char_width,
+    .alnum_char = pm_encoding_big5_star_alnum_char,
+    .alpha_char = pm_encoding_big5_star_alpha_char,
+    .isupper_char = pm_encoding_big5_star_isupper_char,
+    .multibyte = true
+};
+
+/** Big5-UAO encoding */
+pm_encoding_t pm_encoding_big5_uao = {
+    .name = "big5-uao",
+    .char_width = pm_encoding_big5_star_char_width,
+    .alnum_char = pm_encoding_big5_star_alnum_char,
+    .alpha_char = pm_encoding_big5_star_alpha_char,
+    .isupper_char = pm_encoding_big5_star_isupper_char,
+    .multibyte = true
+};
diff --git a/src/enc/pm_big5_hkscs.c b/src/enc/pm_big5_hkscs.c
deleted file mode 100644
index e6a13737..00000000
--- a/src/enc/pm_big5_hkscs.c
+++ /dev/null
@@ -1,54 +0,0 @@
-#include "prism/enc/pm_encoding.h"
-
-static size_t
-pm_encoding_big5_hkscs_char_width(const uint8_t *b, ptrdiff_t n) {
-    // These are the single byte characters.
-    if (*b < 0x80) {
-        return 1;
-    }
-
-    // These are the double byte characters.
-    if ((n > 1) && (b[0] >= 0x87 && b[0] <= 0xFE) &&
-                   ((b[1] >= 0x40 && b[1] <= 0x7E) || (b[1] >= 0xA1 && b[1] <= 0xFE))) {
-        return 2;
-    }
-
-    return 0;
-}
-
-static size_t
-pm_encoding_big5_hkscs_alpha_char(const uint8_t *b, ptrdiff_t n) {
-    if (pm_encoding_big5_hkscs_char_width(b, n) == 1) {
-        return pm_encoding_ascii_alpha_char(b, n);
-    } else {
-        return 0;
-    }
-}
-
-static size_t
-pm_encoding_big5_hkscs_alnum_char(const uint8_t *b, ptrdiff_t n) {
-    if (pm_encoding_big5_hkscs_char_width(b, n) == 1) {
-        return pm_encoding_ascii_alnum_char(b, n);
-    } else {
-        return 0;
-    }
-}
-
-static bool
-pm_encoding_big5_hkscs_isupper_char(const uint8_t *b, ptrdiff_t n) {
-    if (pm_encoding_big5_hkscs_char_width(b, n) == 1) {
-        return pm_encoding_ascii_isupper_char(b, n);
-    } else {
-        return false;
-    }
-}
-
-/** Big5 encoding */
-pm_encoding_t pm_encoding_big5_hkscs = {
-    .name = "big5-hkscs",
-    .char_width = pm_encoding_big5_hkscs_char_width,
-    .alnum_char = pm_encoding_big5_hkscs_alnum_char,
-    .alpha_char = pm_encoding_big5_hkscs_alpha_char,
-    .isupper_char = pm_encoding_big5_hkscs_isupper_char,
-    .multibyte = true
-};
diff --git a/src/enc/pm_tables.c b/src/enc/pm_tables.c
index d0414590..70a44720 100644
--- a/src/enc/pm_tables.c
+++ b/src/enc/pm_tables.c
@@ -360,6 +360,30 @@ static uint8_t pm_encoding_ibm862_table[256] = {
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
 };
 
+/**
+ * Each element of the following table contains a bitfield that indicates a
+ * piece of information about the corresponding IBM863 character.
+ */
+static uint8_t pm_encoding_ibm863_table[256] = {
+//  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x
+    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, // 3x
+    0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // 4x
+    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, // 5x
+    0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 6x
+    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, // 7x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ax
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Bx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Cx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Dx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ex
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
+};
+
 /**
  * Each element of the following table contains a bitfield that indicates a
  * piece of information about the corresponding IBM864 character.
@@ -432,6 +456,30 @@ static uint8_t pm_encoding_ibm866_table[256] = {
     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
 };
 
+/**
+ * Each element of the following table contains a bitfield that indicates a
+ * piece of information about the corresponding IBM869 character.
+ */
+static uint8_t pm_encoding_ibm869_table[256] = {
+//  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x
+    2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, // 3x
+    0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // 4x
+    7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 0, 0, 0, 0, // 5x
+    0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 6x
+    3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, // 7x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9x
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ax
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Bx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Cx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Dx
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Ex
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Fx
+};
+
 /**
  * Each element of the following table contains a bitfield that indicates a
  * piece of information about the corresponding ISO-8859-1 character.
@@ -1375,9 +1423,11 @@ PRISM_ENCODING_TABLE(ibm857)
 PRISM_ENCODING_TABLE(ibm860)
 PRISM_ENCODING_TABLE(ibm861)
 PRISM_ENCODING_TABLE(ibm862)
+PRISM_ENCODING_TABLE(ibm863)
 PRISM_ENCODING_TABLE(ibm864)
 PRISM_ENCODING_TABLE(ibm865)
 PRISM_ENCODING_TABLE(ibm866)
+PRISM_ENCODING_TABLE(ibm869)
 PRISM_ENCODING_TABLE(iso_8859_1)
 PRISM_ENCODING_TABLE(iso_8859_2)
 PRISM_ENCODING_TABLE(iso_8859_3)
@@ -1577,6 +1627,16 @@ pm_encoding_t pm_encoding_ibm862 = {
     .multibyte = false
 };
 
+/** IBM863 */
+pm_encoding_t pm_encoding_ibm863 = {
+    .name = "IBM863",
+    .char_width = pm_encoding_single_char_width,
+    .alnum_char = pm_encoding_ibm863_alnum_char,
+    .alpha_char = pm_encoding_ibm863_alpha_char,
+    .isupper_char = pm_encoding_ibm863_isupper_char,
+    .multibyte = false
+};
+
 /** IBM864 */
 pm_encoding_t pm_encoding_ibm864 = {
     .name = "IBM864",
@@ -1607,6 +1667,16 @@ pm_encoding_t pm_encoding_ibm866 = {
     .multibyte = false
 };
 
+/** IBM869 */
+pm_encoding_t pm_encoding_ibm869 = {
+    .name = "IBM869",
+    .char_width = pm_encoding_single_char_width,
+    .alnum_char = pm_encoding_ibm869_alnum_char,
+    .alpha_char = pm_encoding_ibm869_alpha_char,
+    .isupper_char = pm_encoding_ibm869_isupper_char,
+    .multibyte = false
+};
+
 /** ISO-8859-1 */
 pm_encoding_t pm_encoding_iso_8859_1 = {
     .name = "ISO-8859-1",
diff --git a/src/prism.c b/src/prism.c
index 2607a84c..3b70ae11 100644
--- a/src/prism.c
+++ b/src/prism.c
@@ -479,6 +479,11 @@ pm_parser_err(pm_parser_t *parser, const uint8_t *start, const uint8_t *end, pm_
     pm_diagnostic_list_append(&parser->error_list, start, end, diag_id);
 }
 
+/**
+ * Append an error to the list of errors on the parser using a format string.
+ */
+#define PM_PARSER_ERR_FORMAT(parser, start, end, diag_id, ...) pm_diagnostic_list_append_format(&parser->error_list, start, end, diag_id, __VA_ARGS__)
+
 /**
  * Append an error to the list of errors on the parser using the location of the
  * current token.
@@ -489,12 +494,10 @@ pm_parser_err_current(pm_parser_t *parser, pm_diagnostic_id_t diag_id) {
 }
 
 /**
- * Append an error to the list of errors on the parser using the given location.
+ * Append an error to the list of errors on the parser using the given location
+ * using a format string.
  */
-static inline void
-pm_parser_err_location(pm_parser_t *parser, const pm_location_t *location, pm_diagnostic_id_t diag_id) {
-    pm_parser_err(parser, location->start, location->end, diag_id);
-}
+#define PM_PARSER_ERR_LOCATION_FORMAT(parser, location, diag_id, ...) pm_diagnostic_list_append_format(&parser->error_list, (location)->start, (location)->end, diag_id, __VA_ARGS__)
 
 /**
  * Append an error to the list of errors on the parser using the location of the
@@ -505,6 +508,12 @@ pm_parser_err_node(pm_parser_t *parser, const pm_node_t *node, pm_diagnostic_id_
     pm_parser_err(parser, node->location.start, node->location.end, diag_id);
 }
 
+/**
+ * Append an error to the list of errors on the parser using the location of the
+ * given node and a format string.
+ */
+#define PM_PARSER_ERR_NODE_FORMAT(parser, node, diag_id, ...) pm_diagnostic_list_append_format(&parser->error_list, node->location.start, node->location.end, diag_id, __VA_ARGS__)
+
 /**
  * Append an error to the list of errors on the parser using the location of the
  * previous token.
@@ -523,6 +532,12 @@ pm_parser_err_token(pm_parser_t *parser, const pm_token_t *token, pm_diagnostic_
     pm_parser_err(parser, token->start, token->end, diag_id);
 }
 
+/**
+ * Append an error to the list of errors on the parser using the location of the
+ * given token and a format string.
+ */
+#define PM_PARSER_ERR_TOKEN_FORMAT(parser, token, diag_id, ...) pm_diagnostic_list_append_format(&parser->error_list, token->start, token->end, diag_id, __VA_ARGS__)
+
 /**
  * Append a warning to the list of warnings on the parser.
  */
@@ -796,6 +811,27 @@ typedef struct {
     pm_node_t *block;
 } pm_arguments_t;
 
+/**
+ * Retrieve the end location of a `pm_arguments_t` object.
+ */
+static inline const uint8_t *
+pm_arguments_end(pm_arguments_t *arguments) {
+    if (arguments->block != NULL) {
+        const uint8_t *end = arguments->block->location.end;
+        if (arguments->closing_loc.start != NULL && arguments->closing_loc.end > end) {
+            end = arguments->closing_loc.end;
+        }
+        return end;
+    }
+    if (arguments->closing_loc.start != NULL) {
+        return arguments->closing_loc.end;
+    }
+    if (arguments->arguments != NULL) {
+        return arguments->arguments->base.location.end;
+    }
+    return arguments->closing_loc.end;
+}
+
 /**
  * Check that we're not about to attempt to attach a brace block to a call that
  * has arguments without parentheses.
@@ -1611,11 +1647,7 @@ pm_call_node_aref_create(pm_parser_t *parser, pm_node_t *receiver, pm_arguments_
     pm_call_node_t *node = pm_call_node_create(parser);
 
     node->base.location.start = receiver->location.start;
-    if (arguments->block != NULL) {
-        node->base.location.end = arguments->block->location.end;
-    } else {
-        node->base.location.end = arguments->closing_loc.end;
-    }
+    node->base.location.end = pm_arguments_end(arguments);
 
     node->receiver = receiver;
     node->message_loc.start = arguments->opening_loc.start;
@@ -1664,15 +1696,11 @@ pm_call_node_call_create(pm_parser_t *parser, pm_node_t *receiver, pm_token_t *o
     pm_call_node_t *node = pm_call_node_create(parser);
 
     node->base.location.start = receiver->location.start;
-    if (arguments->block != NULL) {
-        node->base.location.end = arguments->block->location.end;
-    } else if (arguments->closing_loc.start != NULL) {
-        node->base.location.end = arguments->closing_loc.end;
-    } else if (arguments->arguments != NULL) {
-        node->base.location.end = arguments->arguments->base.location.end;
-    } else {
-        node->base.location.end = message->end;
+    const uint8_t *end = pm_arguments_end(arguments);
+    if (end == NULL) {
+        end = message->end;
     }
+    node->base.location.end = end;
 
     node->receiver = receiver;
     node->call_operator_loc = PM_OPTIONAL_LOCATION_TOKEN_VALUE(operator);
@@ -1699,15 +1727,7 @@ pm_call_node_fcall_create(pm_parser_t *parser, pm_token_t *message, pm_arguments
     pm_call_node_t *node = pm_call_node_create(parser);
 
     node->base.location.start = message->start;
-    if (arguments->block != NULL) {
-        node->base.location.end = arguments->block->location.end;
-    } else if (arguments->closing_loc.start != NULL) {
-        node->base.location.end = arguments->closing_loc.end;
-    } else if (arguments->arguments != NULL) {
-        node->base.location.end = arguments->arguments->base.location.end;
-    } else {
-        node->base.location.end = arguments->closing_loc.end;
-    }
+    node->base.location.end = pm_arguments_end(arguments);
 
     node->message_loc = PM_OPTIONAL_LOCATION_TOKEN_VALUE(message);
     node->opening_loc = arguments->opening_loc;
@@ -1755,11 +1775,7 @@ pm_call_node_shorthand_create(pm_parser_t *parser, pm_node_t *receiver, pm_token
     pm_call_node_t *node = pm_call_node_create(parser);
 
     node->base.location.start = receiver->location.start;
-    if (arguments->block != NULL) {
-        node->base.location.end = arguments->block->location.end;
-    } else {
-        node->base.location.end = arguments->closing_loc.end;
-    }
+    node->base.location.end = pm_arguments_end(arguments);
 
     node->receiver = receiver;
     node->call_operator_loc = PM_OPTIONAL_LOCATION_TOKEN_VALUE(operator);
@@ -2112,7 +2128,7 @@ pm_capture_pattern_node_create(pm_parser_t *parser, pm_node_t *value, pm_node_t
  * Allocate and initialize a new CaseNode node.
  */
 static pm_case_node_t *
-pm_case_node_create(pm_parser_t *parser, const pm_token_t *case_keyword, pm_node_t *predicate, pm_else_node_t *consequent, const pm_token_t *end_keyword) {
+pm_case_node_create(pm_parser_t *parser, const pm_token_t *case_keyword, pm_node_t *predicate, const pm_token_t *end_keyword) {
     pm_case_node_t *node = PM_ALLOC_NODE(parser, pm_case_node_t);
 
     *node = (pm_case_node_t) {
@@ -2124,7 +2140,7 @@ pm_case_node_create(pm_parser_t *parser, const pm_token_t *case_keyword, pm_node
             },
         },
         .predicate = predicate,
-        .consequent = consequent,
+        .consequent = NULL,
         .case_keyword_loc = PM_LOCATION_TOKEN_VALUE(case_keyword),
         .end_keyword_loc = PM_LOCATION_TOKEN_VALUE(end_keyword),
         .conditions = { 0 }
@@ -2138,7 +2154,7 @@ pm_case_node_create(pm_parser_t *parser, const pm_token_t *case_keyword, pm_node
  */
 static void
 pm_case_node_condition_append(pm_case_node_t *node, pm_node_t *condition) {
-    assert(PM_NODE_TYPE_P(condition, PM_WHEN_NODE) || PM_NODE_TYPE_P(condition, PM_IN_NODE));
+    assert(PM_NODE_TYPE_P(condition, PM_WHEN_NODE));
 
     pm_node_list_append(&node->conditions, condition);
     node->base.location.end = condition->location.end;
@@ -2162,6 +2178,60 @@ pm_case_node_end_keyword_loc_set(pm_case_node_t *node, const pm_token_t *end_key
     node->end_keyword_loc = PM_LOCATION_TOKEN_VALUE(end_keyword);
 }
 
+/**
+ * Allocate and initialize a new CaseMatchNode node.
+ */
+static pm_case_match_node_t *
+pm_case_match_node_create(pm_parser_t *parser, const pm_token_t *case_keyword, pm_node_t *predicate, const pm_token_t *end_keyword) {
+    pm_case_match_node_t *node = PM_ALLOC_NODE(parser, pm_case_match_node_t);
+
+    *node = (pm_case_match_node_t) {
+        {
+            .type = PM_CASE_MATCH_NODE,
+            .location = {
+                .start = case_keyword->start,
+                .end = end_keyword->end
+            },
+        },
+        .predicate = predicate,
+        .consequent = NULL,
+        .case_keyword_loc = PM_LOCATION_TOKEN_VALUE(case_keyword),
+        .end_keyword_loc = PM_LOCATION_TOKEN_VALUE(end_keyword),
+        .conditions = { 0 }
+    };
+
+    return node;
+}
+
+/**
+ * Append a new condition to a CaseMatchNode node.
+ */
+static void
+pm_case_match_node_condition_append(pm_case_match_node_t *node, pm_node_t *condition) {
+    assert(PM_NODE_TYPE_P(condition, PM_IN_NODE));
+
+    pm_node_list_append(&node->conditions, condition);
+    node->base.location.end = condition->location.end;
+}
+
+/**
+ * Set the consequent of a CaseMatchNode node.
+ */
+static void
+pm_case_match_node_consequent_set(pm_case_match_node_t *node, pm_else_node_t *consequent) {
+    node->consequent = consequent;
+    node->base.location.end = consequent->base.location.end;
+}
+
+/**
+ * Set the end location for a CaseMatchNode node.
+ */
+static void
+pm_case_match_node_end_keyword_loc_set(pm_case_match_node_t *node, const pm_token_t *end_keyword) {
+    node->base.location.end = end_keyword->end;
+    node->end_keyword_loc = PM_LOCATION_TOKEN_VALUE(end_keyword);
+}
+
 /**
  * Allocate a new ClassNode node.
  */
@@ -4023,30 +4093,45 @@ pm_token_is_numbered_parameter(const uint8_t *start, const uint8_t *end) {
 static inline void
 pm_refute_numbered_parameter(pm_parser_t *parser, const uint8_t *start, const uint8_t *end) {
     if (pm_token_is_numbered_parameter(start, end)) {
-        pm_parser_err(parser, start, end, PM_ERR_PARAMETER_NUMBERED_RESERVED);
+        PM_PARSER_ERR_FORMAT(parser, start, end, PM_ERR_PARAMETER_NUMBERED_RESERVED, start);
     }
 }
 
 /**
- * Allocate and initialize a new LocalVariableTargetNode node.
+ * Allocate and initialize a new LocalVariableTargetNode node with the given
+ * name and depth.
  */
 static pm_local_variable_target_node_t *
-pm_local_variable_target_node_create(pm_parser_t *parser, const pm_token_t *name) {
+pm_local_variable_target_node_create_values(pm_parser_t *parser, const pm_location_t *location, pm_constant_id_t name, uint32_t depth) {
     pm_local_variable_target_node_t *node = PM_ALLOC_NODE(parser, pm_local_variable_target_node_t);
-    pm_refute_numbered_parameter(parser, name->start, name->end);
 
     *node = (pm_local_variable_target_node_t) {
         {
             .type = PM_LOCAL_VARIABLE_TARGET_NODE,
-            .location = PM_LOCATION_TOKEN_VALUE(name)
+            .location = *location
         },
-        .name = pm_parser_constant_id_token(parser, name),
-        .depth = 0
+        .name = name,
+        .depth = depth
     };
 
     return node;
 }
 
+/**
+ * Allocate and initialize a new LocalVariableTargetNode node.
+ */
+static pm_local_variable_target_node_t *
+pm_local_variable_target_node_create(pm_parser_t *parser, const pm_token_t *name) {
+    pm_refute_numbered_parameter(parser, name->start, name->end);
+
+    return pm_local_variable_target_node_create_values(
+        parser,
+        &(pm_location_t) { .start = name->start, .end = name->end },
+        pm_parser_constant_id_token(parser, name),
+        0
+    );
+}
+
 /**
  * Allocate and initialize a new MatchPredicateNode node.
  */
@@ -4109,10 +4194,10 @@ pm_match_write_node_create(pm_parser_t *parser, pm_call_node_t *call) {
             .type = PM_MATCH_WRITE_NODE,
             .location = call->base.location
         },
-        .call = call
+        .call = call,
+        .targets = { 0 }
     };
 
-    pm_constant_id_list_init(&node->locals);
     return node;
 }
 
@@ -5121,16 +5206,9 @@ pm_super_node_create(pm_parser_t *parser, const pm_token_t *keyword, pm_argument
     assert(keyword->type == PM_TOKEN_KEYWORD_SUPER);
     pm_super_node_t *node = PM_ALLOC_NODE(parser, pm_super_node_t);
 
-    const uint8_t *end;
-    if (arguments->block != NULL) {
-        end = arguments->block->location.end;
-    } else if (arguments->closing_loc.start != NULL) {
-        end = arguments->closing_loc.end;
-    } else if (arguments->arguments != NULL) {
-        end = arguments->arguments->base.location.end;
-    } else {
+    const uint8_t *end = pm_arguments_end(arguments);
+    if (end == NULL) {
         assert(false && "unreachable");
-        end = NULL;
     }
 
     *node = (pm_super_node_t) {
@@ -5694,17 +5772,16 @@ pm_parser_scope_push_transparent(pm_parser_t *parser) {
 }
 
 /**
- * Check if the current scope has a given local variables.
+ * Check if any of the currently visible scopes contain a local variable
+ * described by the given constant id.
  */
 static int
-pm_parser_local_depth(pm_parser_t *parser, pm_token_t *token) {
-    pm_constant_id_t constant_id = pm_parser_constant_id_token(parser, token);
+pm_parser_local_depth_constant_id(pm_parser_t *parser, pm_constant_id_t constant_id) {
     pm_scope_t *scope = parser->current_scope;
     int depth = 0;
 
     while (scope != NULL) {
-        if (!scope->transparent &&
-                pm_constant_id_list_includes(&scope->locals, constant_id)) return depth;
+        if (!scope->transparent && pm_constant_id_list_includes(&scope->locals, constant_id)) return depth;
         if (scope->closed) break;
 
         scope = scope->previous;
@@ -5714,6 +5791,16 @@ pm_parser_local_depth(pm_parser_t *parser, pm_token_t *token) {
     return -1;
 }
 
+/**
+ * Check if any of the currently visible scopes contain a local variable
+ * described by the given token. This function implicitly inserts a constant
+ * into the constant pool.
+ */
+static inline int
+pm_parser_local_depth(pm_parser_t *parser, pm_token_t *token) {
+    return pm_parser_local_depth_constant_id(parser, pm_parser_constant_id_token(parser, token));
+}
+
 /**
  * Add a constant id to the local table of the current scope.
  */
@@ -6073,6 +6160,7 @@ parser_lex_magic_comment_encoding_value(pm_parser_t *parser, const uint8_t *star
                 ENCODING1("BINARY", pm_encoding_ascii_8bit);
                 ENCODING1("Big5", pm_encoding_big5);
                 ENCODING1("Big5-HKSCS", pm_encoding_big5_hkscs);
+                ENCODING1("Big5-UAO", pm_encoding_big5_uao);
                 break;
             case 'C': case 'c':
                 ENCODING1("CP437", pm_encoding_ibm437);
@@ -6089,8 +6177,10 @@ parser_lex_magic_comment_encoding_value(pm_parser_t *parser, const uint8_t *star
                 ENCODING1("CP864", pm_encoding_ibm864);
                 ENCODING1("CP865", pm_encoding_ibm865);
                 ENCODING1("CP866", pm_encoding_ibm866);
+                ENCODING1("CP869", pm_encoding_ibm869);
                 ENCODING1("CP874", pm_encoding_windows_874);
                 ENCODING1("CP878", pm_encoding_koi8_r);
+                ENCODING1("CP863", pm_encoding_ibm863);
                 ENCODING2("CP932", "csWindows31J", pm_encoding_windows_31j);
                 ENCODING1("CP936", pm_encoding_gbk);
                 ENCODING1("CP1250", pm_encoding_windows_1250);
@@ -6128,9 +6218,11 @@ parser_lex_magic_comment_encoding_value(pm_parser_t *parser, const uint8_t *star
                 ENCODING1("IBM860", pm_encoding_ibm860);
                 ENCODING1("IBM861", pm_encoding_ibm861);
                 ENCODING1("IBM862", pm_encoding_ibm862);
+                ENCODING1("IBM863", pm_encoding_ibm863);
                 ENCODING1("IBM864", pm_encoding_ibm864);
                 ENCODING1("IBM865", pm_encoding_ibm865);
                 ENCODING1("IBM866", pm_encoding_ibm866);
+                ENCODING1("IBM869", pm_encoding_ibm869);
                 ENCODING2("ISO-8859-1", "ISO8859-1", pm_encoding_iso_8859_1);
                 ENCODING2("ISO-8859-2", "ISO8859-2", pm_encoding_iso_8859_2);
                 ENCODING2("ISO-8859-3", "ISO8859-3", pm_encoding_iso_8859_3);
@@ -10499,7 +10591,7 @@ parse_target(pm_parser_t *parser, pm_node_t *target) {
             return target;
         case PM_LOCAL_VARIABLE_READ_NODE:
             if (pm_token_is_numbered_parameter(target->location.start, target->location.end)) {
-                pm_parser_err_node(parser, target, PM_ERR_PARAMETER_NUMBERED_RESERVED);
+                PM_PARSER_ERR_NODE_FORMAT(parser, target, PM_ERR_PARAMETER_NUMBERED_RESERVED, target->location.start);
             } else {
                 assert(sizeof(pm_local_variable_target_node_t) == sizeof(pm_local_variable_read_node_t));
                 target->type = PM_LOCAL_VARIABLE_TARGET_NODE;
@@ -14167,15 +14259,17 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power) {
 
             if (accept1(parser, PM_TOKEN_KEYWORD_END)) {
                 pm_parser_err_token(parser, &case_keyword, PM_ERR_CASE_MISSING_CONDITIONS);
-                return (pm_node_t *) pm_case_node_create(parser, &case_keyword, predicate, NULL, &parser->previous);
+                return (pm_node_t *) pm_case_node_create(parser, &case_keyword, predicate, &parser->previous);
             }
 
             // At this point we can create a case node, though we don't yet know if it
             // is a case-in or case-when node.
             pm_token_t end_keyword = not_provided(parser);
-            pm_case_node_t *case_node = pm_case_node_create(parser, &case_keyword, predicate, NULL, &end_keyword);
+            pm_node_t *node;
 
             if (match1(parser, PM_TOKEN_KEYWORD_WHEN)) {
+                pm_case_node_t *case_node = pm_case_node_create(parser, &case_keyword, predicate, &end_keyword);
+
                 // At this point we've seen a when keyword, so we know this is a
                 // case-when node. We will continue to parse the when nodes until we hit
                 // the end of the list.
@@ -14215,7 +14309,23 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power) {
 
                     pm_case_node_condition_append(case_node, (pm_node_t *) when_node);
                 }
+
+                // If we didn't parse any conditions (in or when) then we need
+                // to indicate that we have an error.
+                if (case_node->conditions.size == 0) {
+                    pm_parser_err_token(parser, &case_keyword, PM_ERR_CASE_MISSING_CONDITIONS);
+                }
+
+                node = (pm_node_t *) case_node;
             } else {
+                pm_case_match_node_t *case_node = pm_case_match_node_create(parser, &case_keyword, predicate, &end_keyword);
+
+                // If this is a case-match node (i.e., it is a pattern matching
+                // case statement) then we must have a predicate.
+                if (predicate == NULL) {
+                    pm_parser_err_token(parser, &case_keyword, PM_ERR_CASE_MATCH_MISSING_PREDICATE);
+                }
+
                 // At this point we expect that we're parsing a case-in node. We will
                 // continue to parse the in nodes until we hit the end of the list.
                 while (match1(parser, PM_TOKEN_KEYWORD_IN)) {
@@ -14269,14 +14379,16 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power) {
                     // Now that we have the full pattern and statements, we can create the
                     // node and attach it to the case node.
                     pm_node_t *condition = (pm_node_t *) pm_in_node_create(parser, pattern, statements, &in_keyword, &then_keyword);
-                    pm_case_node_condition_append(case_node, condition);
+                    pm_case_match_node_condition_append(case_node, condition);
                 }
-            }
 
-            // If we didn't parse any conditions (in or when) then we need to
-            // indicate that we have an error.
-            if (case_node->conditions.size == 0) {
-                pm_parser_err_token(parser, &case_keyword, PM_ERR_CASE_MISSING_CONDITIONS);
+                // If we didn't parse any conditions (in or when) then we need
+                // to indicate that we have an error.
+                if (case_node->conditions.size == 0) {
+                    pm_parser_err_token(parser, &case_keyword, PM_ERR_CASE_MISSING_CONDITIONS);
+                }
+
+                node = (pm_node_t *) case_node;
             }
 
             accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON);
@@ -14290,12 +14402,21 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power) {
                     else_node = pm_else_node_create(parser, &else_keyword, NULL, &parser->current);
                 }
 
-                pm_case_node_consequent_set(case_node, else_node);
+                if (PM_NODE_TYPE_P(node, PM_CASE_NODE)) {
+                    pm_case_node_consequent_set((pm_case_node_t *) node, else_node);
+                } else {
+                    pm_case_match_node_consequent_set((pm_case_match_node_t *) node, else_node);
+                }
             }
 
             expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_CASE_TERM);
-            pm_case_node_end_keyword_loc_set(case_node, &parser->previous);
-            return (pm_node_t *) case_node;
+            if (PM_NODE_TYPE_P(node, PM_CASE_NODE)) {
+                pm_case_node_end_keyword_loc_set((pm_case_node_t *) node, &parser->previous);
+            } else {
+                pm_case_match_node_end_keyword_loc_set((pm_case_match_node_t *) node, &parser->previous);
+            }
+
+            return node;
         }
         case PM_TOKEN_KEYWORD_BEGIN: {
             parser_lex(parser);
@@ -15762,53 +15883,68 @@ parse_regular_expression_named_captures(pm_parser_t *parser, const pm_string_t *
     pm_node_t *result;
 
     if (pm_regexp_named_capture_group_names(pm_string_source(content), pm_string_length(content), &named_captures, parser->encoding_changed, &parser->encoding) && (named_captures.length > 0)) {
-        // Since we should not create a MatchWriteNode when all capture names are invalid,
-        // creating a MatchWriteNode is delayed here.
+        // Since we should not create a MatchWriteNode when all capture names
+        // are invalid, creating a MatchWriteNode is delayed here.
         pm_match_write_node_t *match = NULL;
+        pm_constant_id_list_t names = { 0 };
 
         for (size_t index = 0; index < named_captures.length; index++) {
-            pm_string_t *name = &named_captures.strings[index];
+            pm_string_t *string = &named_captures.strings[index];
 
-            const uint8_t *source = pm_string_source(name);
-            size_t length = pm_string_length(name);
+            const uint8_t *source = pm_string_source(string);
+            size_t length = pm_string_length(string);
 
-            if (!name_is_identifier(parser, source, length)) {
-                continue;
-            }
+            pm_location_t location;
+            pm_constant_id_t name;
+
+            // If the name of the capture group isn't a valid identifier, we do
+            // not add it to the local table.
+            if (!name_is_identifier(parser, source, length)) continue;
 
-            pm_constant_id_t local;
             if (content->type == PM_STRING_SHARED) {
                 // If the unescaped string is a slice of the source, then we can
                 // copy the names directly. The pointers will line up.
-                local = pm_parser_local_add_location(parser, source, source + length);
+                location = (pm_location_t) { .start = source, .end = source + length };
+                name = pm_parser_constant_id_location(parser, location.start, location.end);
                 pm_refute_numbered_parameter(parser, source, source + length);
             } else {
                 // Otherwise, the name is a slice of the malloc-ed owned string,
                 // in which case we need to copy it out into a new string.
+                location = call->receiver->location;
+
                 void *memory = malloc(length);
                 if (memory == NULL) abort();
 
                 memcpy(memory, source, length);
-
-                // This silences clang analyzer warning about leak of memory pointed by `memory`.
-                // NOLINTNEXTLINE(clang-analyzer-*)
-                local = pm_parser_local_add_owned(parser, (const uint8_t *) memory, length);
+                name = pm_parser_constant_id_owned(parser, (const uint8_t *) memory, length);
 
                 if (pm_token_is_numbered_parameter(source, source + length)) {
                     const pm_location_t *location = &call->receiver->location;
-                    pm_parser_err_location(parser, location, PM_ERR_PARAMETER_NUMBERED_RESERVED);
+                    PM_PARSER_ERR_LOCATION_FORMAT(parser, location, PM_ERR_PARAMETER_NUMBERED_RESERVED, location->start);
                 }
             }
 
-            if (match == NULL) {
-                match = pm_match_write_node_create(parser, call);
-            }
+            if (name != 0) {
+                // We dont want to create duplicate targets if the capture name
+                // is duplicated.
+                if (pm_constant_id_list_includes(&names, name)) continue;
+                pm_constant_id_list_append(&names, name);
 
-            if (pm_constant_id_list_includes(&match->locals, local)) {
-                continue;
-            }
+                // Here we lazily create the MatchWriteNode since we know we're
+                // about to add a target.
+                if (match == NULL) match = pm_match_write_node_create(parser, call);
 
-            pm_constant_id_list_append(&match->locals, local);
+                // First, find the depth of the local that is being assigned.
+                int depth;
+                if ((depth = pm_parser_local_depth_constant_id(parser, name)) == -1) {
+                    pm_parser_local_add(parser, name);
+                }
+
+                // Next, create the local variable target and add it to the
+                // list of targets for the match.
+                pm_node_t *target = (pm_node_t *) pm_local_variable_target_node_create_values(parser, &location, name, depth == -1 ? 0 : (uint32_t) depth);
+                pm_node_list_append(&match->targets, target);
+            }
         }
 
         if (match != NULL) {
@@ -15816,6 +15952,8 @@ parse_regular_expression_named_captures(pm_parser_t *parser, const pm_string_t *
         } else {
             result = (pm_node_t *) call;
         }
+
+        pm_constant_id_list_free(&names);
     } else {
         result = (pm_node_t *) call;
     }


Test patch:
diff --git a/test/prism/encoding_test.rb b/test/prism/encoding_test.rb
index a059e227..75a6b027 100644
--- a/test/prism/encoding_test.rb
+++ b/test/prism/encoding_test.rb
@@ -23,9 +23,11 @@ module Prism
       Encoding::IBM860 =>       0x00...0x100,
       Encoding::IBM861 =>       0x00...0x100,
       Encoding::IBM862 =>       0x00...0x100,
+      Encoding::IBM863 =>       0x00...0x100,
       Encoding::IBM864 =>       0x00...0x100,
       Encoding::IBM865 =>       0x00...0x100,
       Encoding::IBM866 =>       0x00...0x100,
+      Encoding::IBM869 =>       0x00...0x100,
       Encoding::ISO_8859_1 =>   0x00...0x100,
       Encoding::ISO_8859_2 =>   0x00...0x100,
       Encoding::ISO_8859_3 =>   0x00...0x100,
@@ -63,6 +65,7 @@ module Prism
       Encoding::Windows_874 =>  0x00...0x100,
       Encoding::Big5 =>         0x00...0x10000,
       Encoding::Big5_HKSCS =>   0x00...0x10000,
+      Encoding::Big5_UAO =>     0x00...0x10000,
       Encoding::CP51932 =>      0x00...0x10000,
       Encoding::GBK =>          0x00...0x10000,
       Encoding::Shift_JIS =>    0x00...0x10000,
diff --git a/test/prism/errors_test.rb b/test/prism/errors_test.rb
index 070b36de..c6d35bae 100644
--- a/test/prism/errors_test.rb
+++ b/test/prism/errors_test.rb
@@ -563,15 +563,15 @@ module Prism
       end
       RUBY
       assert_errors expected, source, [
-        ["Token reserved for a numbered parameter", 8..10],
-        ["Token reserved for a numbered parameter", 14..16],
-        ["Token reserved for a numbered parameter", 20..22],
-        ["Token reserved for a numbered parameter", 26..28],
-        ["Token reserved for a numbered parameter", 32..34],
-        ["Token reserved for a numbered parameter", 40..42],
-        ["Token reserved for a numbered parameter", 46..48],
-        ["Token reserved for a numbered parameter", 52..54],
-        ["Token reserved for a numbered parameter", 58..60],
+        ["_1 is reserved for a numbered parameter", 8..10],
+        ["_2 is reserved for a numbered parameter", 14..16],
+        ["_3 is reserved for a numbered parameter", 20..22],
+        ["_4 is reserved for a numbered parameter", 26..28],
+        ["_5 is reserved for a numbered parameter", 32..34],
+        ["_6 is reserved for a numbered parameter", 40..42],
+        ["_7 is reserved for a numbered parameter", 46..48],
+        ["_8 is reserved for a numbered parameter", 52..54],
+        ["_9 is reserved for a numbered parameter", 58..60],
       ]
     end
 
@@ -937,7 +937,7 @@ module Prism
     end
 
     def test_case_without_when_clauses_errors_on_else_clause
-      expected = CaseNode(
+      expected = CaseMatchNode(
         SymbolNode(Location(), Location(), nil, "a"),
         [],
         ElseNode(Location(), nil, Location()),
@@ -1253,18 +1253,18 @@ module Prism
 
     def test_writing_numbered_parameter
       assert_errors expression("-> { _1 = 0 }"), "-> { _1 = 0 }", [
-        ["Token reserved for a numbered parameter", 5..7]
+        ["_1 is reserved for a numbered parameter", 5..7]
       ]
     end
 
     def test_targeting_numbered_parameter
       assert_errors expression("-> { _1, = 0 }"), "-> { _1, = 0 }", [
-        ["Token reserved for a numbered parameter", 5..7]
+        ["_1 is reserved for a numbered parameter", 5..7]
       ]
     end
 
     def test_defining_numbered_parameter
-      error_messages = ["Token reserved for a numbered parameter"]
+      error_messages = ["_1 is reserved for a numbered parameter"]
 
       assert_error_messages "def _1; end", error_messages
       assert_error_messages "def self._1; end", error_messages
@@ -1319,7 +1319,7 @@ module Prism
     def test_numbered_parameters_in_block_arguments
       source = "foo { |_1| }"
       assert_errors expression(source), source, [
-        ["Token reserved for a numbered parameter", 7..9],
+        ["_1 is reserved for a numbered parameter", 7..9],
       ]
     end
 
@@ -1405,7 +1405,7 @@ module Prism
         /(?<_1>)/ =~ a
       RUBY
 
-      message = "Token reserved for a numbered parameter"
+      message = "_1 is reserved for a numbered parameter"
       assert_errors expression(source), source, [
         [message, 5..7],
         [message, 13..15],
diff --git a/test/prism/fixtures/regex.txt b/test/prism/fixtures/regex.txt
index c27e022d..ef2f6d45 100644
--- a/test/prism/fixtures/regex.txt
+++ b/test/prism/fixtures/regex.txt
@@ -35,3 +35,6 @@ b>)/ =~ ""; ab
 /(?<abc>)(?<abc>)/ =~ ""; abc
 
 /(?<a b>)/ =~ ""
+
+a = 1
+tap { /(?<a>)/ =~ to_s }
diff --git a/test/prism/location_test.rb b/test/prism/location_test.rb
index 363e96fa..02a577a7 100644
--- a/test/prism/location_test.rb
+++ b/test/prism/location_test.rb
@@ -201,6 +201,13 @@ module Prism
       assert_location(CaseNode, "case foo; when bar; when baz; else; end")
     end
 
+    def test_CaseMatchNode
+      assert_location(CaseMatchNode, "case foo; in bar; end")
+      assert_location(CaseMatchNode, "case foo; in bar; else; end")
+      assert_location(CaseMatchNode, "case foo; in bar; in baz; end")
+      assert_location(CaseMatchNode, "case foo; in bar; in baz; else; end")
+    end
+
     def test_ClassNode
       assert_location(ClassNode, "class Foo end")
       assert_location(ClassNode, "class Foo < Bar; end")
diff --git a/test/prism/snapshots/case.txt b/test/prism/snapshots/case.txt
index 3655ce16..7b264c14 100644
--- a/test/prism/snapshots/case.txt
+++ b/test/prism/snapshots/case.txt
@@ -306,7 +306,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (38,0)-(38,4) = "case"
         │   └── end_keyword_loc: (38,21)-(38,24) = "end"
-        ├── @ CaseNode (location: (40,0)-(42,3))
+        ├── @ CaseMatchNode (location: (40,0)-(42,3))
         │   ├── predicate:
         │   │   @ MatchPredicateNode (location: (40,5)-(40,11))
         │   │   ├── value:
@@ -327,7 +327,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (40,0)-(40,4) = "case"
         │   └── end_keyword_loc: (42,0)-(42,3) = "end"
-        └── @ CaseNode (location: (44,0)-(44,22))
+        └── @ CaseMatchNode (location: (44,0)-(44,22))
             ├── predicate:
             │   @ MatchPredicateNode (location: (44,5)-(44,11))
             │   ├── value:
diff --git a/test/prism/snapshots/patterns.txt b/test/prism/snapshots/patterns.txt
index 0e8c31c8..ef2e26de 100644
--- a/test/prism/snapshots/patterns.txt
+++ b/test/prism/snapshots/patterns.txt
@@ -2778,7 +2778,7 @@
         │   │               ├── name: :bar
         │   │               └── depth: 1
         │   └── operator_loc: (125,4)-(125,6) = "in"
-        ├── @ CaseNode (location: (127,0)-(127,25))
+        ├── @ CaseMatchNode (location: (127,0)-(127,25))
         │   ├── predicate:
         │   │   @ CallNode (location: (127,5)-(127,8))
         │   │   ├── receiver: ∅
@@ -2802,7 +2802,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (127,0)-(127,4) = "case"
         │   └── end_keyword_loc: (127,22)-(127,25) = "end"
-        ├── @ CaseNode (location: (128,0)-(128,23))
+        ├── @ CaseMatchNode (location: (128,0)-(128,23))
         │   ├── predicate:
         │   │   @ CallNode (location: (128,5)-(128,8))
         │   │   ├── receiver: ∅
@@ -2825,7 +2825,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (128,0)-(128,4) = "case"
         │   └── end_keyword_loc: (128,20)-(128,23) = "end"
-        ├── @ CaseNode (location: (129,0)-(129,25))
+        ├── @ CaseMatchNode (location: (129,0)-(129,25))
         │   ├── predicate:
         │   │   @ CallNode (location: (129,5)-(129,8))
         │   │   ├── receiver: ∅
@@ -2847,7 +2847,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (129,0)-(129,4) = "case"
         │   └── end_keyword_loc: (129,22)-(129,25) = "end"
-        ├── @ CaseNode (location: (130,0)-(130,24))
+        ├── @ CaseMatchNode (location: (130,0)-(130,24))
         │   ├── predicate:
         │   │   @ CallNode (location: (130,5)-(130,8))
         │   │   ├── receiver: ∅
@@ -2872,7 +2872,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (130,0)-(130,4) = "case"
         │   └── end_keyword_loc: (130,21)-(130,24) = "end"
-        ├── @ CaseNode (location: (131,0)-(131,24))
+        ├── @ CaseMatchNode (location: (131,0)-(131,24))
         │   ├── predicate:
         │   │   @ CallNode (location: (131,5)-(131,8))
         │   │   ├── receiver: ∅
@@ -2897,7 +2897,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (131,0)-(131,4) = "case"
         │   └── end_keyword_loc: (131,21)-(131,24) = "end"
-        ├── @ CaseNode (location: (132,0)-(132,26))
+        ├── @ CaseMatchNode (location: (132,0)-(132,26))
         │   ├── predicate:
         │   │   @ CallNode (location: (132,5)-(132,8))
         │   │   ├── receiver: ∅
@@ -2923,7 +2923,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (132,0)-(132,4) = "case"
         │   └── end_keyword_loc: (132,23)-(132,26) = "end"
-        ├── @ CaseNode (location: (133,0)-(133,29))
+        ├── @ CaseMatchNode (location: (133,0)-(133,29))
         │   ├── predicate:
         │   │   @ CallNode (location: (133,5)-(133,8))
         │   │   ├── receiver: ∅
@@ -2949,7 +2949,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (133,0)-(133,4) = "case"
         │   └── end_keyword_loc: (133,26)-(133,29) = "end"
-        ├── @ CaseNode (location: (134,0)-(134,28))
+        ├── @ CaseMatchNode (location: (134,0)-(134,28))
         │   ├── predicate:
         │   │   @ CallNode (location: (134,5)-(134,8))
         │   │   ├── receiver: ∅
@@ -2975,7 +2975,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (134,0)-(134,4) = "case"
         │   └── end_keyword_loc: (134,25)-(134,28) = "end"
-        ├── @ CaseNode (location: (135,0)-(135,27))
+        ├── @ CaseMatchNode (location: (135,0)-(135,27))
         │   ├── predicate:
         │   │   @ CallNode (location: (135,5)-(135,8))
         │   │   ├── receiver: ∅
@@ -3002,7 +3002,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (135,0)-(135,4) = "case"
         │   └── end_keyword_loc: (135,24)-(135,27) = "end"
-        ├── @ CaseNode (location: (136,0)-(136,27))
+        ├── @ CaseMatchNode (location: (136,0)-(136,27))
         │   ├── predicate:
         │   │   @ CallNode (location: (136,5)-(136,8))
         │   │   ├── receiver: ∅
@@ -3028,7 +3028,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (136,0)-(136,4) = "case"
         │   └── end_keyword_loc: (136,24)-(136,27) = "end"
-        ├── @ CaseNode (location: (137,0)-(137,29))
+        ├── @ CaseMatchNode (location: (137,0)-(137,29))
         │   ├── predicate:
         │   │   @ CallNode (location: (137,5)-(137,8))
         │   │   ├── receiver: ∅
@@ -3054,7 +3054,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (137,0)-(137,4) = "case"
         │   └── end_keyword_loc: (137,26)-(137,29) = "end"
-        ├── @ CaseNode (location: (138,0)-(138,29))
+        ├── @ CaseMatchNode (location: (138,0)-(138,29))
         │   ├── predicate:
         │   │   @ CallNode (location: (138,5)-(138,8))
         │   │   ├── receiver: ∅
@@ -3084,7 +3084,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (138,0)-(138,4) = "case"
         │   └── end_keyword_loc: (138,26)-(138,29) = "end"
-        ├── @ CaseNode (location: (139,0)-(139,29))
+        ├── @ CaseMatchNode (location: (139,0)-(139,29))
         │   ├── predicate:
         │   │   @ CallNode (location: (139,5)-(139,8))
         │   │   ├── receiver: ∅
@@ -3114,7 +3114,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (139,0)-(139,4) = "case"
         │   └── end_keyword_loc: (139,26)-(139,29) = "end"
-        ├── @ CaseNode (location: (140,0)-(140,29))
+        ├── @ CaseMatchNode (location: (140,0)-(140,29))
         │   ├── predicate:
         │   │   @ CallNode (location: (140,5)-(140,8))
         │   │   ├── receiver: ∅
@@ -3145,7 +3145,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (140,0)-(140,4) = "case"
         │   └── end_keyword_loc: (140,26)-(140,29) = "end"
-        ├── @ CaseNode (location: (141,0)-(141,29))
+        ├── @ CaseMatchNode (location: (141,0)-(141,29))
         │   ├── predicate:
         │   │   @ CallNode (location: (141,5)-(141,8))
         │   │   ├── receiver: ∅
@@ -3176,7 +3176,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (141,0)-(141,4) = "case"
         │   └── end_keyword_loc: (141,26)-(141,29) = "end"
-        ├── @ CaseNode (location: (142,0)-(142,29))
+        ├── @ CaseMatchNode (location: (142,0)-(142,29))
         │   ├── predicate:
         │   │   @ CallNode (location: (142,5)-(142,8))
         │   │   ├── receiver: ∅
@@ -3203,7 +3203,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (142,0)-(142,4) = "case"
         │   └── end_keyword_loc: (142,26)-(142,29) = "end"
-        ├── @ CaseNode (location: (143,0)-(143,29))
+        ├── @ CaseMatchNode (location: (143,0)-(143,29))
         │   ├── predicate:
         │   │   @ CallNode (location: (143,5)-(143,8))
         │   │   ├── receiver: ∅
@@ -3230,7 +3230,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (143,0)-(143,4) = "case"
         │   └── end_keyword_loc: (143,26)-(143,29) = "end"
-        ├── @ CaseNode (location: (144,0)-(144,27))
+        ├── @ CaseMatchNode (location: (144,0)-(144,27))
         │   ├── predicate:
         │   │   @ CallNode (location: (144,5)-(144,8))
         │   │   ├── receiver: ∅
@@ -3257,7 +3257,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (144,0)-(144,4) = "case"
         │   └── end_keyword_loc: (144,24)-(144,27) = "end"
-        ├── @ CaseNode (location: (145,0)-(145,25))
+        ├── @ CaseMatchNode (location: (145,0)-(145,25))
         │   ├── predicate:
         │   │   @ CallNode (location: (145,5)-(145,8))
         │   │   ├── receiver: ∅
@@ -3279,7 +3279,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (145,0)-(145,4) = "case"
         │   └── end_keyword_loc: (145,22)-(145,25) = "end"
-        ├── @ CaseNode (location: (146,0)-(146,26))
+        ├── @ CaseMatchNode (location: (146,0)-(146,26))
         │   ├── predicate:
         │   │   @ CallNode (location: (146,5)-(146,8))
         │   │   ├── receiver: ∅
@@ -3301,7 +3301,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (146,0)-(146,4) = "case"
         │   └── end_keyword_loc: (146,23)-(146,26) = "end"
-        ├── @ CaseNode (location: (147,0)-(147,26))
+        ├── @ CaseMatchNode (location: (147,0)-(147,26))
         │   ├── predicate:
         │   │   @ CallNode (location: (147,5)-(147,8))
         │   │   ├── receiver: ∅
@@ -3323,7 +3323,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (147,0)-(147,4) = "case"
         │   └── end_keyword_loc: (147,23)-(147,26) = "end"
-        ├── @ CaseNode (location: (148,0)-(148,27))
+        ├── @ CaseMatchNode (location: (148,0)-(148,27))
         │   ├── predicate:
         │   │   @ CallNode (location: (148,5)-(148,8))
         │   │   ├── receiver: ∅
@@ -3345,7 +3345,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (148,0)-(148,4) = "case"
         │   └── end_keyword_loc: (148,24)-(148,27) = "end"
-        ├── @ CaseNode (location: (149,0)-(149,30))
+        ├── @ CaseMatchNode (location: (149,0)-(149,30))
         │   ├── predicate:
         │   │   @ CallNode (location: (149,5)-(149,8))
         │   │   ├── receiver: ∅
@@ -3368,7 +3368,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (149,0)-(149,4) = "case"
         │   └── end_keyword_loc: (149,27)-(149,30) = "end"
-        ├── @ CaseNode (location: (150,0)-(150,30))
+        ├── @ CaseMatchNode (location: (150,0)-(150,30))
         │   ├── predicate:
         │   │   @ CallNode (location: (150,5)-(150,8))
         │   │   ├── receiver: ∅
@@ -3390,7 +3390,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (150,0)-(150,4) = "case"
         │   └── end_keyword_loc: (150,27)-(150,30) = "end"
-        ├── @ CaseNode (location: (151,0)-(151,34))
+        ├── @ CaseMatchNode (location: (151,0)-(151,34))
         │   ├── predicate:
         │   │   @ CallNode (location: (151,5)-(151,8))
         │   │   ├── receiver: ∅
@@ -3412,7 +3412,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (151,0)-(151,4) = "case"
         │   └── end_keyword_loc: (151,31)-(151,34) = "end"
-        ├── @ CaseNode (location: (152,0)-(152,32))
+        ├── @ CaseMatchNode (location: (152,0)-(152,32))
         │   ├── predicate:
         │   │   @ CallNode (location: (152,5)-(152,8))
         │   │   ├── receiver: ∅
@@ -3445,7 +3445,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (152,0)-(152,4) = "case"
         │   └── end_keyword_loc: (152,29)-(152,32) = "end"
-        ├── @ CaseNode (location: (154,0)-(154,32))
+        ├── @ CaseMatchNode (location: (154,0)-(154,32))
         │   ├── predicate:
         │   │   @ CallNode (location: (154,5)-(154,8))
         │   │   ├── receiver: ∅
@@ -3481,7 +3481,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (154,0)-(154,4) = "case"
         │   └── end_keyword_loc: (154,29)-(154,32) = "end"
-        ├── @ CaseNode (location: (155,0)-(155,30))
+        ├── @ CaseMatchNode (location: (155,0)-(155,30))
         │   ├── predicate:
         │   │   @ CallNode (location: (155,5)-(155,8))
         │   │   ├── receiver: ∅
@@ -3516,7 +3516,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (155,0)-(155,4) = "case"
         │   └── end_keyword_loc: (155,27)-(155,30) = "end"
-        ├── @ CaseNode (location: (156,0)-(156,32))
+        ├── @ CaseMatchNode (location: (156,0)-(156,32))
         │   ├── predicate:
         │   │   @ CallNode (location: (156,5)-(156,8))
         │   │   ├── receiver: ∅
@@ -3550,7 +3550,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (156,0)-(156,4) = "case"
         │   └── end_keyword_loc: (156,29)-(156,32) = "end"
-        ├── @ CaseNode (location: (157,0)-(157,31))
+        ├── @ CaseMatchNode (location: (157,0)-(157,31))
         │   ├── predicate:
         │   │   @ CallNode (location: (157,5)-(157,8))
         │   │   ├── receiver: ∅
@@ -3587,7 +3587,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (157,0)-(157,4) = "case"
         │   └── end_keyword_loc: (157,28)-(157,31) = "end"
-        ├── @ CaseNode (location: (158,0)-(158,31))
+        ├── @ CaseMatchNode (location: (158,0)-(158,31))
         │   ├── predicate:
         │   │   @ CallNode (location: (158,5)-(158,8))
         │   │   ├── receiver: ∅
@@ -3624,7 +3624,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (158,0)-(158,4) = "case"
         │   └── end_keyword_loc: (158,28)-(158,31) = "end"
-        ├── @ CaseNode (location: (159,0)-(159,33))
+        ├── @ CaseMatchNode (location: (159,0)-(159,33))
         │   ├── predicate:
         │   │   @ CallNode (location: (159,5)-(159,8))
         │   │   ├── receiver: ∅
@@ -3662,7 +3662,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (159,0)-(159,4) = "case"
         │   └── end_keyword_loc: (159,30)-(159,33) = "end"
-        ├── @ CaseNode (location: (160,0)-(160,36))
+        ├── @ CaseMatchNode (location: (160,0)-(160,36))
         │   ├── predicate:
         │   │   @ CallNode (location: (160,5)-(160,8))
         │   │   ├── receiver: ∅
@@ -3700,7 +3700,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (160,0)-(160,4) = "case"
         │   └── end_keyword_loc: (160,33)-(160,36) = "end"
-        ├── @ CaseNode (location: (161,0)-(161,35))
+        ├── @ CaseMatchNode (location: (161,0)-(161,35))
         │   ├── predicate:
         │   │   @ CallNode (location: (161,5)-(161,8))
         │   │   ├── receiver: ∅
@@ -3738,7 +3738,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (161,0)-(161,4) = "case"
         │   └── end_keyword_loc: (161,32)-(161,35) = "end"
-        ├── @ CaseNode (location: (162,0)-(162,34))
+        ├── @ CaseMatchNode (location: (162,0)-(162,34))
         │   ├── predicate:
         │   │   @ CallNode (location: (162,5)-(162,8))
         │   │   ├── receiver: ∅
@@ -3777,7 +3777,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (162,0)-(162,4) = "case"
         │   └── end_keyword_loc: (162,31)-(162,34) = "end"
-        ├── @ CaseNode (location: (163,0)-(163,34))
+        ├── @ CaseMatchNode (location: (163,0)-(163,34))
         │   ├── predicate:
         │   │   @ CallNode (location: (163,5)-(163,8))
         │   │   ├── receiver: ∅
@@ -3815,7 +3815,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (163,0)-(163,4) = "case"
         │   └── end_keyword_loc: (163,31)-(163,34) = "end"
-        ├── @ CaseNode (location: (164,0)-(164,36))
+        ├── @ CaseMatchNode (location: (164,0)-(164,36))
         │   ├── predicate:
         │   │   @ CallNode (location: (164,5)-(164,8))
         │   │   ├── receiver: ∅
@@ -3853,7 +3853,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (164,0)-(164,4) = "case"
         │   └── end_keyword_loc: (164,33)-(164,36) = "end"
-        ├── @ CaseNode (location: (165,0)-(165,36))
+        ├── @ CaseMatchNode (location: (165,0)-(165,36))
         │   ├── predicate:
         │   │   @ CallNode (location: (165,5)-(165,8))
         │   │   ├── receiver: ∅
@@ -3895,7 +3895,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (165,0)-(165,4) = "case"
         │   └── end_keyword_loc: (165,33)-(165,36) = "end"
-        ├── @ CaseNode (location: (166,0)-(166,36))
+        ├── @ CaseMatchNode (location: (166,0)-(166,36))
         │   ├── predicate:
         │   │   @ CallNode (location: (166,5)-(166,8))
         │   │   ├── receiver: ∅
@@ -3937,7 +3937,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (166,0)-(166,4) = "case"
         │   └── end_keyword_loc: (166,33)-(166,36) = "end"
-        ├── @ CaseNode (location: (167,0)-(167,36))
+        ├── @ CaseMatchNode (location: (167,0)-(167,36))
         │   ├── predicate:
         │   │   @ CallNode (location: (167,5)-(167,8))
         │   │   ├── receiver: ∅
@@ -3980,7 +3980,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (167,0)-(167,4) = "case"
         │   └── end_keyword_loc: (167,33)-(167,36) = "end"
-        ├── @ CaseNode (location: (168,0)-(168,36))
+        ├── @ CaseMatchNode (location: (168,0)-(168,36))
         │   ├── predicate:
         │   │   @ CallNode (location: (168,5)-(168,8))
         │   │   ├── receiver: ∅
@@ -4023,7 +4023,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (168,0)-(168,4) = "case"
         │   └── end_keyword_loc: (168,33)-(168,36) = "end"
-        ├── @ CaseNode (location: (169,0)-(169,36))
+        ├── @ CaseMatchNode (location: (169,0)-(169,36))
         │   ├── predicate:
         │   │   @ CallNode (location: (169,5)-(169,8))
         │   │   ├── receiver: ∅
@@ -4062,7 +4062,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (169,0)-(169,4) = "case"
         │   └── end_keyword_loc: (169,33)-(169,36) = "end"
-        ├── @ CaseNode (location: (170,0)-(170,36))
+        ├── @ CaseMatchNode (location: (170,0)-(170,36))
         │   ├── predicate:
         │   │   @ CallNode (location: (170,5)-(170,8))
         │   │   ├── receiver: ∅
@@ -4101,7 +4101,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (170,0)-(170,4) = "case"
         │   └── end_keyword_loc: (170,33)-(170,36) = "end"
-        ├── @ CaseNode (location: (171,0)-(171,34))
+        ├── @ CaseMatchNode (location: (171,0)-(171,34))
         │   ├── predicate:
         │   │   @ CallNode (location: (171,5)-(171,8))
         │   │   ├── receiver: ∅
@@ -4140,7 +4140,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (171,0)-(171,4) = "case"
         │   └── end_keyword_loc: (171,31)-(171,34) = "end"
-        ├── @ CaseNode (location: (172,0)-(172,32))
+        ├── @ CaseMatchNode (location: (172,0)-(172,32))
         │   ├── predicate:
         │   │   @ CallNode (location: (172,5)-(172,8))
         │   │   ├── receiver: ∅
@@ -4174,7 +4174,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (172,0)-(172,4) = "case"
         │   └── end_keyword_loc: (172,29)-(172,32) = "end"
-        ├── @ CaseNode (location: (173,0)-(173,33))
+        ├── @ CaseMatchNode (location: (173,0)-(173,33))
         │   ├── predicate:
         │   │   @ CallNode (location: (173,5)-(173,8))
         │   │   ├── receiver: ∅
@@ -4208,7 +4208,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (173,0)-(173,4) = "case"
         │   └── end_keyword_loc: (173,30)-(173,33) = "end"
-        ├── @ CaseNode (location: (174,0)-(174,33))
+        ├── @ CaseMatchNode (location: (174,0)-(174,33))
         │   ├── predicate:
         │   │   @ CallNode (location: (174,5)-(174,8))
         │   │   ├── receiver: ∅
@@ -4242,7 +4242,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (174,0)-(174,4) = "case"
         │   └── end_keyword_loc: (174,30)-(174,33) = "end"
-        ├── @ CaseNode (location: (175,0)-(175,34))
+        ├── @ CaseMatchNode (location: (175,0)-(175,34))
         │   ├── predicate:
         │   │   @ CallNode (location: (175,5)-(175,8))
         │   │   ├── receiver: ∅
@@ -4276,7 +4276,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (175,0)-(175,4) = "case"
         │   └── end_keyword_loc: (175,31)-(175,34) = "end"
-        ├── @ CaseNode (location: (176,0)-(176,37))
+        ├── @ CaseMatchNode (location: (176,0)-(176,37))
         │   ├── predicate:
         │   │   @ CallNode (location: (176,5)-(176,8))
         │   │   ├── receiver: ∅
@@ -4311,7 +4311,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (176,0)-(176,4) = "case"
         │   └── end_keyword_loc: (176,34)-(176,37) = "end"
-        ├── @ CaseNode (location: (177,0)-(177,37))
+        ├── @ CaseMatchNode (location: (177,0)-(177,37))
         │   ├── predicate:
         │   │   @ CallNode (location: (177,5)-(177,8))
         │   │   ├── receiver: ∅
@@ -4345,7 +4345,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (177,0)-(177,4) = "case"
         │   └── end_keyword_loc: (177,34)-(177,37) = "end"
-        ├── @ CaseNode (location: (178,0)-(178,41))
+        ├── @ CaseMatchNode (location: (178,0)-(178,41))
         │   ├── predicate:
         │   │   @ CallNode (location: (178,5)-(178,8))
         │   │   ├── receiver: ∅
@@ -4379,7 +4379,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (178,0)-(178,4) = "case"
         │   └── end_keyword_loc: (178,38)-(178,41) = "end"
-        ├── @ CaseNode (location: (179,0)-(179,39))
+        ├── @ CaseMatchNode (location: (179,0)-(179,39))
         │   ├── predicate:
         │   │   @ CallNode (location: (179,5)-(179,8))
         │   │   ├── receiver: ∅
diff --git a/test/prism/snapshots/regex.txt b/test/prism/snapshots/regex.txt
index 722006c9..b69e5cc3 100644
--- a/test/prism/snapshots/regex.txt
+++ b/test/prism/snapshots/regex.txt
@@ -1,8 +1,8 @@
-@ ProgramNode (location: (1,0)-(37,16))
-├── locals: [:foo, :ab, :abc]
+@ ProgramNode (location: (1,0)-(40,24))
+├── locals: [:foo, :ab, :abc, :a]
 └── statements:
-    @ StatementsNode (location: (1,0)-(37,16))
-    └── body: (length: 19)
+    @ StatementsNode (location: (1,0)-(40,24))
+    └── body: (length: 21)
         ├── @ CallNode (location: (1,0)-(1,9))
         │   ├── receiver: ∅
         │   ├── call_operator_loc: ∅
@@ -116,7 +116,10 @@
         │   │   │   │   ├── block: ∅
         │   │   │   │   ├── flags: ∅
         │   │   │   │   └── name: :=~
-        │   │   │   └── locals: [:foo]
+        │   │   │   └── targets: (length: 1)
+        │   │   │       └── @ LocalVariableTargetNode (location: (11,5)-(11,8))
+        │   │   │           ├── name: :foo
+        │   │   │           └── depth: 0
         │   │   └── @ LocalVariableReadNode (location: (11,23)-(11,26))
         │   │       ├── name: :foo
         │   │       └── depth: 0
@@ -237,7 +240,10 @@
         │   │   ├── block: ∅
         │   │   ├── flags: ∅
         │   │   └── name: :=~
-        │   └── locals: [:ab]
+        │   └── targets: (length: 1)
+        │       └── @ LocalVariableTargetNode (location: (32,0)-(33,4))
+        │           ├── name: :ab
+        │           └── depth: 0
         ├── @ LocalVariableReadNode (location: (33,12)-(33,14))
         │   ├── name: :ab
         │   └── depth: 0
@@ -268,32 +274,96 @@
         │   │   ├── block: ∅
         │   │   ├── flags: ∅
         │   │   └── name: :=~
-        │   └── locals: [:abc]
+        │   └── targets: (length: 1)
+        │       └── @ LocalVariableTargetNode (location: (35,4)-(35,7))
+        │           ├── name: :abc
+        │           └── depth: 0
         ├── @ LocalVariableReadNode (location: (35,26)-(35,29))
         │   ├── name: :abc
         │   └── depth: 0
-        └── @ CallNode (location: (37,0)-(37,16))
-            ├── receiver:
-            │   @ RegularExpressionNode (location: (37,0)-(37,10))
-            │   ├── opening_loc: (37,0)-(37,1) = "/"
-            │   ├── content_loc: (37,1)-(37,9) = "(?<a b>)"
-            │   ├── closing_loc: (37,9)-(37,10) = "/"
-            │   ├── unescaped: "(?<a b>)"
-            │   └── flags: ∅
+        ├── @ CallNode (location: (37,0)-(37,16))
+        │   ├── receiver:
+        │   │   @ RegularExpressionNode (location: (37,0)-(37,10))
+        │   │   ├── opening_loc: (37,0)-(37,1) = "/"
+        │   │   ├── content_loc: (37,1)-(37,9) = "(?<a b>)"
+        │   │   ├── closing_loc: (37,9)-(37,10) = "/"
+        │   │   ├── unescaped: "(?<a b>)"
+        │   │   └── flags: ∅
+        │   ├── call_operator_loc: ∅
+        │   ├── message_loc: (37,11)-(37,13) = "=~"
+        │   ├── opening_loc: ∅
+        │   ├── arguments:
+        │   │   @ ArgumentsNode (location: (37,14)-(37,16))
+        │   │   ├── arguments: (length: 1)
+        │   │   │   └── @ StringNode (location: (37,14)-(37,16))
+        │   │   │       ├── flags: ∅
+        │   │   │       ├── opening_loc: (37,14)-(37,15) = "\""
+        │   │   │       ├── content_loc: (37,15)-(37,15) = ""
+        │   │   │       ├── closing_loc: (37,15)-(37,16) = "\""
+        │   │   │       └── unescaped: ""
+        │   │   └── flags: ∅
+        │   ├── closing_loc: ∅
+        │   ├── block: ∅
+        │   ├── flags: ∅
+        │   └── name: :=~
+        ├── @ LocalVariableWriteNode (location: (39,0)-(39,5))
+        │   ├── name: :a
+        │   ├── depth: 0
+        │   ├── name_loc: (39,0)-(39,1) = "a"
+        │   ├── value:
+        │   │   @ IntegerNode (location: (39,4)-(39,5))
+        │   │   └── flags: decimal
+        │   └── operator_loc: (39,2)-(39,3) = "="
+        └── @ CallNode (location: (40,0)-(40,24))
+            ├── receiver: ∅
             ├── call_operator_loc: ∅
-            ├── message_loc: (37,11)-(37,13) = "=~"
+            ├── message_loc: (40,0)-(40,3) = "tap"
             ├── opening_loc: ∅
-            ├── arguments:
-            │   @ ArgumentsNode (location: (37,14)-(37,16))
-            │   ├── arguments: (length: 1)
-            │   │   └── @ StringNode (location: (37,14)-(37,16))
-            │   │       ├── flags: ∅
-            │   │       ├── opening_loc: (37,14)-(37,15) = "\""
-            │   │       ├── content_loc: (37,15)-(37,15) = ""
-            │   │       ├── closing_loc: (37,15)-(37,16) = "\""
-            │   │       └── unescaped: ""
-            │   └── flags: ∅
+            ├── arguments: ∅
             ├── closing_loc: ∅
-            ├── block: ∅
+            ├── block:
+            │   @ BlockNode (location: (40,4)-(40,24))
+            │   ├── locals: []
+            │   ├── parameters: ∅
+            │   ├── body:
+            │   │   @ StatementsNode (location: (40,6)-(40,22))
+            │   │   └── body: (length: 1)
+            │   │       └── @ MatchWriteNode (location: (40,6)-(40,22))
+            │   │           ├── call:
+            │   │           │   @ CallNode (location: (40,6)-(40,22))
+            │   │           │   ├── receiver:
+            │   │           │   │   @ RegularExpressionNode (location: (40,6)-(40,14))
+            │   │           │   │   ├── opening_loc: (40,6)-(40,7) = "/"
+            │   │           │   │   ├── content_loc: (40,7)-(40,13) = "(?<a>)"
+            │   │           │   │   ├── closing_loc: (40,13)-(40,14) = "/"
+            │   │           │   │   ├── unescaped: "(?<a>)"
+            │   │           │   │   └── flags: ∅
+            │   │           │   ├── call_operator_loc: ∅
+            │   │           │   ├── message_loc: (40,15)-(40,17) = "=~"
+            │   │           │   ├── opening_loc: ∅
+            │   │           │   ├── arguments:
+            │   │           │   │   @ ArgumentsNode (location: (40,18)-(40,22))
+            │   │           │   │   ├── arguments: (length: 1)
+            │   │           │   │   │   └── @ CallNode (location: (40,18)-(40,22))
+            │   │           │   │   │       ├── receiver: ∅
+            │   │           │   │   │       ├── call_operator_loc: ∅
+            │   │           │   │   │       ├── message_loc: (40,18)-(40,22) = "to_s"
+            │   │           │   │   │       ├── opening_loc: ∅
+            │   │           │   │   │       ├── arguments: ∅
+            │   │           │   │   │       ├── closing_loc: ∅
+            │   │           │   │   │       ├── block: ∅
+            │   │           │   │   │       ├── flags: variable_call
+            │   │           │   │   │       └── name: :to_s
+            │   │           │   │   └── flags: ∅
+            │   │           │   ├── closing_loc: ∅
+            │   │           │   ├── block: ∅
+            │   │           │   ├── flags: ∅
+            │   │           │   └── name: :=~
+            │   │           └── targets: (length: 1)
+            │   │               └── @ LocalVariableTargetNode (location: (40,10)-(40,11))
+            │   │                   ├── name: :a
+            │   │                   └── depth: 1
+            │   ├── opening_loc: (40,4)-(40,5) = "{"
+            │   └── closing_loc: (40,23)-(40,24) = "}"
             ├── flags: ∅
-            └── name: :=~
+            └── name: :tap
diff --git a/test/prism/snapshots/seattlerb/case_in.txt b/test/prism/snapshots/seattlerb/case_in.txt
index c6150c41..9db7851e 100644
--- a/test/prism/snapshots/seattlerb/case_in.txt
+++ b/test/prism/snapshots/seattlerb/case_in.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(111,3))
     └── body: (length: 28)
-        ├── @ CaseNode (location: (1,0)-(3,3))
+        ├── @ CaseMatchNode (location: (1,0)-(3,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (1,5)-(1,7))
         │   │   ├── opening_loc: (1,5)-(1,6) = ":"
@@ -34,7 +34,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (1,0)-(1,4) = "case"
         │   └── end_keyword_loc: (3,0)-(3,3) = "end"
-        ├── @ CaseNode (location: (5,0)-(7,3))
+        ├── @ CaseMatchNode (location: (5,0)-(7,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (5,5)-(5,7))
         │   │   ├── opening_loc: (5,5)-(5,6) = ":"
@@ -64,7 +64,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (5,0)-(5,4) = "case"
         │   └── end_keyword_loc: (7,0)-(7,3) = "end"
-        ├── @ CaseNode (location: (9,0)-(11,3))
+        ├── @ CaseMatchNode (location: (9,0)-(11,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (9,5)-(9,7))
         │   │   ├── opening_loc: (9,5)-(9,6) = ":"
@@ -96,7 +96,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (9,0)-(9,4) = "case"
         │   └── end_keyword_loc: (11,0)-(11,3) = "end"
-        ├── @ CaseNode (location: (13,0)-(15,3))
+        ├── @ CaseMatchNode (location: (13,0)-(15,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (13,5)-(13,7))
         │   │   ├── opening_loc: (13,5)-(13,6) = ":"
@@ -126,7 +126,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (13,0)-(13,4) = "case"
         │   └── end_keyword_loc: (15,0)-(15,3) = "end"
-        ├── @ CaseNode (location: (17,0)-(19,3))
+        ├── @ CaseMatchNode (location: (17,0)-(19,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (17,5)-(17,7))
         │   │   ├── opening_loc: (17,5)-(17,6) = ":"
@@ -158,7 +158,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (17,0)-(17,4) = "case"
         │   └── end_keyword_loc: (19,0)-(19,3) = "end"
-        ├── @ CaseNode (location: (21,0)-(23,3))
+        ├── @ CaseMatchNode (location: (21,0)-(23,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (21,5)-(21,7))
         │   │   ├── opening_loc: (21,5)-(21,6) = ":"
@@ -181,7 +181,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (21,0)-(21,4) = "case"
         │   └── end_keyword_loc: (23,0)-(23,3) = "end"
-        ├── @ CaseNode (location: (25,0)-(27,3))
+        ├── @ CaseMatchNode (location: (25,0)-(27,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (25,5)-(25,7))
         │   │   ├── opening_loc: (25,5)-(25,6) = ":"
@@ -204,7 +204,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (25,0)-(25,4) = "case"
         │   └── end_keyword_loc: (27,0)-(27,3) = "end"
-        ├── @ CaseNode (location: (29,0)-(31,3))
+        ├── @ CaseMatchNode (location: (29,0)-(31,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (29,5)-(29,7))
         │   │   ├── opening_loc: (29,5)-(29,6) = ":"
@@ -227,7 +227,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (29,0)-(29,4) = "case"
         │   └── end_keyword_loc: (31,0)-(31,3) = "end"
-        ├── @ CaseNode (location: (33,0)-(35,3))
+        ├── @ CaseMatchNode (location: (33,0)-(35,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (33,5)-(33,7))
         │   │   ├── opening_loc: (33,5)-(33,6) = ":"
@@ -252,7 +252,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (33,0)-(33,4) = "case"
         │   └── end_keyword_loc: (35,0)-(35,3) = "end"
-        ├── @ CaseNode (location: (37,0)-(39,3))
+        ├── @ CaseMatchNode (location: (37,0)-(39,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (37,5)-(37,7))
         │   │   ├── opening_loc: (37,5)-(37,6) = ":"
@@ -270,7 +270,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (37,0)-(37,4) = "case"
         │   └── end_keyword_loc: (39,0)-(39,3) = "end"
-        ├── @ CaseNode (location: (41,0)-(43,3))
+        ├── @ CaseMatchNode (location: (41,0)-(43,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (41,5)-(41,7))
         │   │   ├── opening_loc: (41,5)-(41,6) = ":"
@@ -295,7 +295,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (41,0)-(41,4) = "case"
         │   └── end_keyword_loc: (43,0)-(43,3) = "end"
-        ├── @ CaseNode (location: (45,0)-(47,3))
+        ├── @ CaseMatchNode (location: (45,0)-(47,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (45,5)-(45,7))
         │   │   ├── opening_loc: (45,5)-(45,6) = ":"
@@ -317,7 +317,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (45,0)-(45,4) = "case"
         │   └── end_keyword_loc: (47,0)-(47,3) = "end"
-        ├── @ CaseNode (location: (49,0)-(51,3))
+        ├── @ CaseMatchNode (location: (49,0)-(51,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (49,5)-(49,7))
         │   │   ├── opening_loc: (49,5)-(49,6) = ":"
@@ -356,7 +356,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (49,0)-(49,4) = "case"
         │   └── end_keyword_loc: (51,0)-(51,3) = "end"
-        ├── @ CaseNode (location: (53,0)-(55,3))
+        ├── @ CaseMatchNode (location: (53,0)-(55,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (53,5)-(53,7))
         │   │   ├── opening_loc: (53,5)-(53,6) = ":"
@@ -396,7 +396,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (53,0)-(53,4) = "case"
         │   └── end_keyword_loc: (55,0)-(55,3) = "end"
-        ├── @ CaseNode (location: (57,0)-(59,3))
+        ├── @ CaseMatchNode (location: (57,0)-(59,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (57,5)-(57,7))
         │   │   ├── opening_loc: (57,5)-(57,6) = ":"
@@ -421,7 +421,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (57,0)-(57,4) = "case"
         │   └── end_keyword_loc: (59,0)-(59,3) = "end"
-        ├── @ CaseNode (location: (61,0)-(63,3))
+        ├── @ CaseMatchNode (location: (61,0)-(63,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (61,5)-(61,7))
         │   │   ├── opening_loc: (61,5)-(61,6) = ":"
@@ -461,7 +461,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (61,0)-(61,4) = "case"
         │   └── end_keyword_loc: (63,0)-(63,3) = "end"
-        ├── @ CaseNode (location: (65,0)-(67,3))
+        ├── @ CaseMatchNode (location: (65,0)-(67,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (65,5)-(65,7))
         │   │   ├── opening_loc: (65,5)-(65,6) = ":"
@@ -501,7 +501,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (65,0)-(65,4) = "case"
         │   └── end_keyword_loc: (67,0)-(67,3) = "end"
-        ├── @ CaseNode (location: (69,0)-(71,3))
+        ├── @ CaseMatchNode (location: (69,0)-(71,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (69,5)-(69,7))
         │   │   ├── opening_loc: (69,5)-(69,6) = ":"
@@ -552,7 +552,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (69,0)-(69,4) = "case"
         │   └── end_keyword_loc: (71,0)-(71,3) = "end"
-        ├── @ CaseNode (location: (73,0)-(75,3))
+        ├── @ CaseMatchNode (location: (73,0)-(75,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (73,5)-(73,7))
         │   │   ├── opening_loc: (73,5)-(73,6) = ":"
@@ -605,7 +605,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (73,0)-(73,4) = "case"
         │   └── end_keyword_loc: (75,0)-(75,3) = "end"
-        ├── @ CaseNode (location: (77,0)-(79,3))
+        ├── @ CaseMatchNode (location: (77,0)-(79,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (77,5)-(77,7))
         │   │   ├── opening_loc: (77,5)-(77,6) = ":"
@@ -635,7 +635,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (77,0)-(77,4) = "case"
         │   └── end_keyword_loc: (79,0)-(79,3) = "end"
-        ├── @ CaseNode (location: (81,0)-(83,3))
+        ├── @ CaseMatchNode (location: (81,0)-(83,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (81,5)-(81,7))
         │   │   ├── opening_loc: (81,5)-(81,6) = ":"
@@ -691,7 +691,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (81,0)-(81,4) = "case"
         │   └── end_keyword_loc: (83,0)-(83,3) = "end"
-        ├── @ CaseNode (location: (85,0)-(87,3))
+        ├── @ CaseMatchNode (location: (85,0)-(87,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (85,5)-(85,7))
         │   │   ├── opening_loc: (85,5)-(85,6) = ":"
@@ -714,7 +714,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (85,0)-(85,4) = "case"
         │   └── end_keyword_loc: (87,0)-(87,3) = "end"
-        ├── @ CaseNode (location: (89,0)-(91,3))
+        ├── @ CaseMatchNode (location: (89,0)-(91,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (89,5)-(89,7))
         │   │   ├── opening_loc: (89,5)-(89,6) = ":"
@@ -752,7 +752,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (89,0)-(89,4) = "case"
         │   └── end_keyword_loc: (91,0)-(91,3) = "end"
-        ├── @ CaseNode (location: (93,0)-(95,3))
+        ├── @ CaseMatchNode (location: (93,0)-(95,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (93,5)-(93,7))
         │   │   ├── opening_loc: (93,5)-(93,6) = ":"
@@ -790,7 +790,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (93,0)-(93,4) = "case"
         │   └── end_keyword_loc: (95,0)-(95,3) = "end"
-        ├── @ CaseNode (location: (97,0)-(99,3))
+        ├── @ CaseMatchNode (location: (97,0)-(99,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (97,5)-(97,7))
         │   │   ├── opening_loc: (97,5)-(97,6) = ":"
@@ -811,7 +811,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (97,0)-(97,4) = "case"
         │   └── end_keyword_loc: (99,0)-(99,3) = "end"
-        ├── @ CaseNode (location: (101,0)-(103,3))
+        ├── @ CaseMatchNode (location: (101,0)-(103,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (101,5)-(101,7))
         │   │   ├── opening_loc: (101,5)-(101,6) = ":"
@@ -837,7 +837,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (101,0)-(101,4) = "case"
         │   └── end_keyword_loc: (103,0)-(103,3) = "end"
-        ├── @ CaseNode (location: (105,0)-(107,3))
+        ├── @ CaseMatchNode (location: (105,0)-(107,3))
         │   ├── predicate:
         │   │   @ SymbolNode (location: (105,5)-(105,7))
         │   │   ├── opening_loc: (105,5)-(105,6) = ":"
@@ -868,7 +868,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (105,0)-(105,4) = "case"
         │   └── end_keyword_loc: (107,0)-(107,3) = "end"
-        └── @ CaseNode (location: (109,0)-(111,3))
+        └── @ CaseMatchNode (location: (109,0)-(111,3))
             ├── predicate:
             │   @ SymbolNode (location: (109,5)-(109,7))
             │   ├── opening_loc: (109,5)-(109,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_31.txt b/test/prism/snapshots/seattlerb/case_in_31.txt
index e753c3cc..7c1a73f5 100644
--- a/test/prism/snapshots/seattlerb/case_in_31.txt
+++ b/test/prism/snapshots/seattlerb/case_in_31.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_37.txt b/test/prism/snapshots/seattlerb/case_in_37.txt
index b272291f..a4c9aa9d 100644
--- a/test/prism/snapshots/seattlerb/case_in_37.txt
+++ b/test/prism/snapshots/seattlerb/case_in_37.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_42.txt b/test/prism/snapshots/seattlerb/case_in_42.txt
index fe69a9e3..bd77e493 100644
--- a/test/prism/snapshots/seattlerb/case_in_42.txt
+++ b/test/prism/snapshots/seattlerb/case_in_42.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(3,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(3,3))
+        └── @ CaseMatchNode (location: (1,0)-(3,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_42_2.txt b/test/prism/snapshots/seattlerb/case_in_42_2.txt
index f4e6806d..86375abc 100644
--- a/test/prism/snapshots/seattlerb/case_in_42_2.txt
+++ b/test/prism/snapshots/seattlerb/case_in_42_2.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(3,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(3,3))
+        └── @ CaseMatchNode (location: (1,0)-(3,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_47.txt b/test/prism/snapshots/seattlerb/case_in_47.txt
index 32b24fc9..4c38e1de 100644
--- a/test/prism/snapshots/seattlerb/case_in_47.txt
+++ b/test/prism/snapshots/seattlerb/case_in_47.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_67.txt b/test/prism/snapshots/seattlerb/case_in_67.txt
index 73649059..55b188a6 100644
--- a/test/prism/snapshots/seattlerb/case_in_67.txt
+++ b/test/prism/snapshots/seattlerb/case_in_67.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(3,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(3,3))
+        └── @ CaseMatchNode (location: (1,0)-(3,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_86.txt b/test/prism/snapshots/seattlerb/case_in_86.txt
index 7197e77b..ab010821 100644
--- a/test/prism/snapshots/seattlerb/case_in_86.txt
+++ b/test/prism/snapshots/seattlerb/case_in_86.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(3,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(3,3))
+        └── @ CaseMatchNode (location: (1,0)-(3,3))
             ├── predicate:
             │   @ ArrayNode (location: (1,5)-(1,13))
             │   ├── elements: (length: 2)
diff --git a/test/prism/snapshots/seattlerb/case_in_86_2.txt b/test/prism/snapshots/seattlerb/case_in_86_2.txt
index 9d3abbb3..c2b320df 100644
--- a/test/prism/snapshots/seattlerb/case_in_86_2.txt
+++ b/test/prism/snapshots/seattlerb/case_in_86_2.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(3,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(3,3))
+        └── @ CaseMatchNode (location: (1,0)-(3,3))
             ├── predicate:
             │   @ ArrayNode (location: (1,5)-(1,13))
             │   ├── elements: (length: 2)
diff --git a/test/prism/snapshots/seattlerb/case_in_array_pat_const.txt b/test/prism/snapshots/seattlerb/case_in_array_pat_const.txt
index 8316c51f..87752706 100644
--- a/test/prism/snapshots/seattlerb/case_in_array_pat_const.txt
+++ b/test/prism/snapshots/seattlerb/case_in_array_pat_const.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_array_pat_const2.txt b/test/prism/snapshots/seattlerb/case_in_array_pat_const2.txt
index 9f54b921..1b9c5703 100644
--- a/test/prism/snapshots/seattlerb/case_in_array_pat_const2.txt
+++ b/test/prism/snapshots/seattlerb/case_in_array_pat_const2.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_array_pat_paren_assign.txt b/test/prism/snapshots/seattlerb/case_in_array_pat_paren_assign.txt
index 0b903e8d..b16682a2 100644
--- a/test/prism/snapshots/seattlerb/case_in_array_pat_paren_assign.txt
+++ b/test/prism/snapshots/seattlerb/case_in_array_pat_paren_assign.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_const.txt b/test/prism/snapshots/seattlerb/case_in_const.txt
index 59880bc2..dac930a1 100644
--- a/test/prism/snapshots/seattlerb/case_in_const.txt
+++ b/test/prism/snapshots/seattlerb/case_in_const.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ ConstantReadNode (location: (1,5)-(1,10))
             │   └── name: :Array
diff --git a/test/prism/snapshots/seattlerb/case_in_else.txt b/test/prism/snapshots/seattlerb/case_in_else.txt
index c132443c..ba6655f7 100644
--- a/test/prism/snapshots/seattlerb/case_in_else.txt
+++ b/test/prism/snapshots/seattlerb/case_in_else.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(6,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(6,3))
+        └── @ CaseMatchNode (location: (1,0)-(6,3))
             ├── predicate:
             │   @ ConstantReadNode (location: (1,5)-(1,10))
             │   └── name: :Array
diff --git a/test/prism/snapshots/seattlerb/case_in_find.txt b/test/prism/snapshots/seattlerb/case_in_find.txt
index 191dee3a..0a34234a 100644
--- a/test/prism/snapshots/seattlerb/case_in_find.txt
+++ b/test/prism/snapshots/seattlerb/case_in_find.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(3,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(3,3))
+        └── @ CaseMatchNode (location: (1,0)-(3,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_find_array.txt b/test/prism/snapshots/seattlerb/case_in_find_array.txt
index df764a4b..3fce60c9 100644
--- a/test/prism/snapshots/seattlerb/case_in_find_array.txt
+++ b/test/prism/snapshots/seattlerb/case_in_find_array.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(3,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(3,3))
+        └── @ CaseMatchNode (location: (1,0)-(3,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_hash_pat.txt b/test/prism/snapshots/seattlerb/case_in_hash_pat.txt
index a8e27b47..174b61c0 100644
--- a/test/prism/snapshots/seattlerb/case_in_hash_pat.txt
+++ b/test/prism/snapshots/seattlerb/case_in_hash_pat.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_hash_pat_assign.txt b/test/prism/snapshots/seattlerb/case_in_hash_pat_assign.txt
index 4df1e565..d0673d1d 100644
--- a/test/prism/snapshots/seattlerb/case_in_hash_pat_assign.txt
+++ b/test/prism/snapshots/seattlerb/case_in_hash_pat_assign.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_hash_pat_paren_assign.txt b/test/prism/snapshots/seattlerb/case_in_hash_pat_paren_assign.txt
index a444e523..802f4d7e 100644
--- a/test/prism/snapshots/seattlerb/case_in_hash_pat_paren_assign.txt
+++ b/test/prism/snapshots/seattlerb/case_in_hash_pat_paren_assign.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_hash_pat_paren_true.txt b/test/prism/snapshots/seattlerb/case_in_hash_pat_paren_true.txt
index 05a111f0..c445b010 100644
--- a/test/prism/snapshots/seattlerb/case_in_hash_pat_paren_true.txt
+++ b/test/prism/snapshots/seattlerb/case_in_hash_pat_paren_true.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_hash_pat_rest.txt b/test/prism/snapshots/seattlerb/case_in_hash_pat_rest.txt
index 90e32521..83e247cb 100644
--- a/test/prism/snapshots/seattlerb/case_in_hash_pat_rest.txt
+++ b/test/prism/snapshots/seattlerb/case_in_hash_pat_rest.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(3,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(3,3))
+        └── @ CaseMatchNode (location: (1,0)-(3,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_hash_pat_rest_solo.txt b/test/prism/snapshots/seattlerb/case_in_hash_pat_rest_solo.txt
index 68503300..6b6147e2 100644
--- a/test/prism/snapshots/seattlerb/case_in_hash_pat_rest_solo.txt
+++ b/test/prism/snapshots/seattlerb/case_in_hash_pat_rest_solo.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(3,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(3,3))
+        └── @ CaseMatchNode (location: (1,0)-(3,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_if_unless_post_mod.txt b/test/prism/snapshots/seattlerb/case_in_if_unless_post_mod.txt
index 03f29769..dfc51b5e 100644
--- a/test/prism/snapshots/seattlerb/case_in_if_unless_post_mod.txt
+++ b/test/prism/snapshots/seattlerb/case_in_if_unless_post_mod.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(6,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(6,3))
+        └── @ CaseMatchNode (location: (1,0)-(6,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_multiple.txt b/test/prism/snapshots/seattlerb/case_in_multiple.txt
index 046cb618..fe6eb13f 100644
--- a/test/prism/snapshots/seattlerb/case_in_multiple.txt
+++ b/test/prism/snapshots/seattlerb/case_in_multiple.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(6,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(6,3))
+        └── @ CaseMatchNode (location: (1,0)-(6,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/case_in_or.txt b/test/prism/snapshots/seattlerb/case_in_or.txt
index 059ddcd3..11a706b2 100644
--- a/test/prism/snapshots/seattlerb/case_in_or.txt
+++ b/test/prism/snapshots/seattlerb/case_in_or.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/parse_pattern_019.txt b/test/prism/snapshots/seattlerb/parse_pattern_019.txt
index e3bf981d..196cb186 100644
--- a/test/prism/snapshots/seattlerb/parse_pattern_019.txt
+++ b/test/prism/snapshots/seattlerb/parse_pattern_019.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ IntegerNode (location: (1,5)-(1,6))
             │   └── flags: decimal
diff --git a/test/prism/snapshots/seattlerb/parse_pattern_044.txt b/test/prism/snapshots/seattlerb/parse_pattern_044.txt
index c354112e..f83bd5b0 100644
--- a/test/prism/snapshots/seattlerb/parse_pattern_044.txt
+++ b/test/prism/snapshots/seattlerb/parse_pattern_044.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ CallNode (location: (1,5)-(1,8))
             │   ├── receiver: ∅
diff --git a/test/prism/snapshots/seattlerb/parse_pattern_051.txt b/test/prism/snapshots/seattlerb/parse_pattern_051.txt
index 5d95d717..fb32cd03 100644
--- a/test/prism/snapshots/seattlerb/parse_pattern_051.txt
+++ b/test/prism/snapshots/seattlerb/parse_pattern_051.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ ArrayNode (location: (1,5)-(1,14))
             │   ├── elements: (length: 3)
diff --git a/test/prism/snapshots/seattlerb/parse_pattern_058.txt b/test/prism/snapshots/seattlerb/parse_pattern_058.txt
index 4812556b..6ec42013 100644
--- a/test/prism/snapshots/seattlerb/parse_pattern_058.txt
+++ b/test/prism/snapshots/seattlerb/parse_pattern_058.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ HashNode (location: (1,5)-(1,11))
             │   ├── opening_loc: (1,5)-(1,6) = "{"
diff --git a/test/prism/snapshots/seattlerb/parse_pattern_058_2.txt b/test/prism/snapshots/seattlerb/parse_pattern_058_2.txt
index 4e5bf2f2..0f578ee6 100644
--- a/test/prism/snapshots/seattlerb/parse_pattern_058_2.txt
+++ b/test/prism/snapshots/seattlerb/parse_pattern_058_2.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ HashNode (location: (1,5)-(1,11))
             │   ├── opening_loc: (1,5)-(1,6) = "{"
diff --git a/test/prism/snapshots/seattlerb/parse_pattern_069.txt b/test/prism/snapshots/seattlerb/parse_pattern_069.txt
index ae40e65d..98661ae2 100644
--- a/test/prism/snapshots/seattlerb/parse_pattern_069.txt
+++ b/test/prism/snapshots/seattlerb/parse_pattern_069.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ SymbolNode (location: (1,5)-(1,7))
             │   ├── opening_loc: (1,5)-(1,6) = ":"
diff --git a/test/prism/snapshots/seattlerb/parse_pattern_076.txt b/test/prism/snapshots/seattlerb/parse_pattern_076.txt
index b6643f86..7f3131cc 100644
--- a/test/prism/snapshots/seattlerb/parse_pattern_076.txt
+++ b/test/prism/snapshots/seattlerb/parse_pattern_076.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(4,3))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(4,3))
+        └── @ CaseMatchNode (location: (1,0)-(4,3))
             ├── predicate:
             │   @ HashNode (location: (1,5)-(1,11))
             │   ├── opening_loc: (1,5)-(1,6) = "{"
diff --git a/test/prism/snapshots/spanning_heredoc.txt b/test/prism/snapshots/spanning_heredoc.txt
index f28aeb81..ed9c03e6 100644
--- a/test/prism/snapshots/spanning_heredoc.txt
+++ b/test/prism/snapshots/spanning_heredoc.txt
@@ -352,4 +352,7 @@
             │   ├── block: ∅
             │   ├── flags: ∅
             │   └── name: :=~
-            └── locals: [:a]
+            └── targets: (length: 1)
+                └── @ LocalVariableTargetNode (location: (53,5)-(55,7))
+                    ├── name: :a
+                    └── depth: 0
diff --git a/test/prism/snapshots/super.txt b/test/prism/snapshots/super.txt
index 96b74ce0..55d7526b 100644
--- a/test/prism/snapshots/super.txt
+++ b/test/prism/snapshots/super.txt
@@ -1,7 +1,7 @@
-@ ProgramNode (location: (1,0)-(17,20))
+@ ProgramNode (location: (1,0)-(17,21))
 ├── locals: []
 └── statements:
-    @ StatementsNode (location: (1,0)-(17,20))
+    @ StatementsNode (location: (1,0)-(17,21))
     └── body: (length: 9)
         ├── @ ForwardingSuperNode (location: (1,0)-(1,5))
         │   └── block: ∅
@@ -51,7 +51,7 @@
         │       │   ├── closing_loc: ∅
         │       │   └── unescaped: "foo"
         │       └── operator_loc: (9,6)-(9,7) = "&"
-        ├── @ SuperNode (location: (11,0)-(11,11))
+        ├── @ SuperNode (location: (11,0)-(11,12))
         │   ├── keyword_loc: (11,0)-(11,5) = "super"
         │   ├── lparen_loc: (11,5)-(11,6) = "("
         │   ├── arguments: ∅
@@ -94,7 +94,7 @@
         │       ├── body: ∅
         │       ├── opening_loc: (15,15)-(15,16) = "{"
         │       └── closing_loc: (15,16)-(15,17) = "}"
-        └── @ SuperNode (location: (17,0)-(17,20))
+        └── @ SuperNode (location: (17,0)-(17,21))
             ├── keyword_loc: (17,0)-(17,5) = "super"
             ├── lparen_loc: (17,5)-(17,6) = "("
             ├── arguments:
diff --git a/test/prism/snapshots/unparser/corpus/literal/pattern.txt b/test/prism/snapshots/unparser/corpus/literal/pattern.txt
index 61e947a3..492528b5 100644
--- a/test/prism/snapshots/unparser/corpus/literal/pattern.txt
+++ b/test/prism/snapshots/unparser/corpus/literal/pattern.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(41,8))
     └── body: (length: 4)
-        ├── @ CaseNode (location: (1,0)-(33,3))
+        ├── @ CaseMatchNode (location: (1,0)-(33,3))
         │   ├── predicate:
         │   │   @ CallNode (location: (1,5)-(1,8))
         │   │   ├── receiver: ∅
@@ -332,7 +332,7 @@
         │   │   └── end_keyword_loc: (33,0)-(33,3) = "end"
         │   ├── case_keyword_loc: (1,0)-(1,4) = "case"
         │   └── end_keyword_loc: (33,0)-(33,3) = "end"
-        ├── @ CaseNode (location: (34,0)-(36,3))
+        ├── @ CaseMatchNode (location: (34,0)-(36,3))
         │   ├── predicate:
         │   │   @ CallNode (location: (34,5)-(34,8))
         │   │   ├── receiver: ∅
@@ -374,7 +374,7 @@
         │   ├── consequent: ∅
         │   ├── case_keyword_loc: (34,0)-(34,4) = "case"
         │   └── end_keyword_loc: (36,0)-(36,3) = "end"
-        ├── @ CaseNode (location: (37,0)-(40,3))
+        ├── @ CaseMatchNode (location: (37,0)-(40,3))
         │   ├── predicate:
         │   │   @ CallNode (location: (37,5)-(37,8))
         │   │   ├── receiver: ∅
diff --git a/test/prism/snapshots/unparser/corpus/literal/send.txt b/test/prism/snapshots/unparser/corpus/literal/send.txt
index 5a3e7ce6..68a83b21 100644
--- a/test/prism/snapshots/unparser/corpus/literal/send.txt
+++ b/test/prism/snapshots/unparser/corpus/literal/send.txt
@@ -1004,7 +1004,7 @@
         │   ├── block: ∅
         │   ├── flags: ∅
         │   └── name: :foo
-        ├── @ CallNode (location: (58,0)-(58,12))
+        ├── @ CallNode (location: (58,0)-(58,13))
         │   ├── receiver:
         │   │   @ CallNode (location: (58,0)-(58,3))
         │   │   ├── receiver: ∅
@@ -1178,7 +1178,7 @@
         │   ├── block: ∅
         │   ├── flags: ∅
         │   └── name: :bar
-        ├── @ CallNode (location: (62,0)-(62,18))
+        ├── @ CallNode (location: (62,0)-(62,19))
         │   ├── receiver:
         │   │   @ CallNode (location: (62,0)-(62,3))
         │   │   ├── receiver: ∅
@@ -1364,7 +1364,7 @@
         │   ├── block: ∅
         │   ├── flags: ∅
         │   └── name: :bar
-        ├── @ CallNode (location: (66,0)-(66,26))
+        ├── @ CallNode (location: (66,0)-(66,27))
         │   ├── receiver:
         │   │   @ CallNode (location: (66,0)-(66,3))
         │   │   ├── receiver: ∅
diff --git a/test/prism/snapshots/unparser/corpus/literal/super.txt b/test/prism/snapshots/unparser/corpus/literal/super.txt
index 924c016c..3beb4191 100644
--- a/test/prism/snapshots/unparser/corpus/literal/super.txt
+++ b/test/prism/snapshots/unparser/corpus/literal/super.txt
@@ -59,7 +59,7 @@
         │   │   └── flags: ∅
         │   ├── rparen_loc: (4,10)-(4,11) = ")"
         │   └── block: ∅
-        ├── @ SuperNode (location: (5,0)-(5,12))
+        ├── @ SuperNode (location: (5,0)-(5,13))
         │   ├── keyword_loc: (5,0)-(5,5) = "super"
         │   ├── lparen_loc: (5,5)-(5,6) = "("
         │   ├── arguments: ∅
@@ -78,7 +78,7 @@
         │       │   ├── flags: variable_call
         │       │   └── name: :block
         │       └── operator_loc: (5,6)-(5,7) = "&"
-        ├── @ SuperNode (location: (6,0)-(6,15))
+        ├── @ SuperNode (location: (6,0)-(6,16))
         │   ├── keyword_loc: (6,0)-(6,5) = "super"
         │   ├── lparen_loc: (6,5)-(6,6) = "("
         │   ├── arguments:
diff --git a/test/prism/snapshots/whitequark/lvar_injecting_match.txt b/test/prism/snapshots/whitequark/lvar_injecting_match.txt
index 0d615cc1..de6d5eb9 100644
--- a/test/prism/snapshots/whitequark/lvar_injecting_match.txt
+++ b/test/prism/snapshots/whitequark/lvar_injecting_match.txt
@@ -30,7 +30,10 @@
         │   │   ├── block: ∅
         │   │   ├── flags: ∅
         │   │   └── name: :=~
-        │   └── locals: [:match]
+        │   └── targets: (length: 1)
+        │       └── @ LocalVariableTargetNode (location: (1,4)-(1,9))
+        │           ├── name: :match
+        │           └── depth: 0
         └── @ LocalVariableReadNode (location: (1,26)-(1,31))
             ├── name: :match
             └── depth: 0
diff --git a/test/prism/snapshots/whitequark/newline_in_hash_argument.txt b/test/prism/snapshots/whitequark/newline_in_hash_argument.txt
index 9d29c284..43791501 100644
--- a/test/prism/snapshots/whitequark/newline_in_hash_argument.txt
+++ b/test/prism/snapshots/whitequark/newline_in_hash_argument.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(14,1))
     └── body: (length: 3)
-        ├── @ CaseNode (location: (1,0)-(8,3))
+        ├── @ CaseMatchNode (location: (1,0)-(8,3))
         │   ├── predicate:
         │   │   @ CallNode (location: (1,5)-(1,8))
         │   │   ├── receiver: ∅
diff --git a/test/prism/snapshots/whitequark/pattern_matching__FILE__LINE_literals.txt b/test/prism/snapshots/whitequark/pattern_matching__FILE__LINE_literals.txt
index 27e15115..c12a9a16 100644
--- a/test/prism/snapshots/whitequark/pattern_matching__FILE__LINE_literals.txt
+++ b/test/prism/snapshots/whitequark/pattern_matching__FILE__LINE_literals.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,8)-(3,11))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,8)-(3,11))
+        └── @ CaseMatchNode (location: (1,8)-(3,11))
             ├── predicate:
             │   @ ArrayNode (location: (1,13)-(1,51))
             │   ├── elements: (length: 3)
diff --git a/test/prism/snapshots/whitequark/pattern_matching_blank_else.txt b/test/prism/snapshots/whitequark/pattern_matching_blank_else.txt
index f6b212c8..308abd2f 100644
--- a/test/prism/snapshots/whitequark/pattern_matching_blank_else.txt
+++ b/test/prism/snapshots/whitequark/pattern_matching_blank_else.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(1,26))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(1,26))
+        └── @ CaseMatchNode (location: (1,0)-(1,26))
             ├── predicate:
             │   @ IntegerNode (location: (1,5)-(1,6))
             │   └── flags: decimal
diff --git a/test/prism/snapshots/whitequark/pattern_matching_else.txt b/test/prism/snapshots/whitequark/pattern_matching_else.txt
index 358a5569..944cc7b4 100644
--- a/test/prism/snapshots/whitequark/pattern_matching_else.txt
+++ b/test/prism/snapshots/whitequark/pattern_matching_else.txt
@@ -3,7 +3,7 @@
 └── statements:
     @ StatementsNode (location: (1,0)-(1,29))
     └── body: (length: 1)
-        └── @ CaseNode (location: (1,0)-(1,29))
+        └── @ CaseMatchNode (location: (1,0)-(1,29))
             ├── predicate:
             │   @ IntegerNode (location: (1,5)-(1,6))
             │   └── flags: decimal