Repository: ruby/prism
Pull request #1898 (2023-11-20 19:51:48)

Rename the big5-hkscs stuff to something more generic and add Big5-UAO sharing common code.
This adds support for Big5-UAO. See: https://github.com/ruby/prism/issues/1843

Rename some of the Big5-HKSCS files and definitions to reuse them for both cases since the logic appears to be the same.

kddnewton: @rgarver this looks good but do you mind just putting everything into `big5.c`? That'll just simplify this whole thing.
rgarver: No problem. That makes sense.

Patch:
diff --git a/config.yml b/config.yml
index 6f12a513..05290173 100644
--- a/config.yml
+++ b/config.yml
@@ -1880,8 +1880,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/include/prism/enc/pm_encoding.h b/include/prism/enc/pm_encoding.h
index 1785a842..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;
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/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/prism.c b/src/prism.c
index f041fb51..7330063b 100644
--- a/src/prism.c
+++ b/src/prism.c
@@ -796,6 +796,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 +1632,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 +1681,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 +1712,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 +1760,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);
@@ -4028,25 +4029,40 @@ pm_refute_numbered_parameter(pm_parser_t *parser, const uint8_t *start, const ui
 }
 
 /**
- * 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 +4125,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 +5137,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 +5703,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 +5722,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 +6091,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);
@@ -15766,37 +15785,40 @@ 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;
@@ -15804,15 +15826,27 @@ parse_regular_expression_named_captures(pm_parser_t *parser, const pm_string_t *
                 }
             }
 
-            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) {
@@ -15820,6 +15854,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 8704945d..75a6b027 100644
--- a/test/prism/encoding_test.rb
+++ b/test/prism/encoding_test.rb
@@ -65,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/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/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/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/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