Repository: ruby/prism
Pull request #1709 (2023-10-18 13:37:11)

Some constant pool fixes and enhancements
* The constant pool didn't have the concept of a constant string like `pm_string_t`, meaning it allocated memory even if the value was known at compile time. This has been fixed.
* When `...` is present in a method definition, you can use `*` and `&` to forward subsets of all of the parameters. This has been fixed.
* I changed the way the `Debug` module worked when comparing locals in the locals test. This makes it much more obvious where we need anonymous locals to handle specific values.


Patch:
diff --git a/include/prism/util/pm_constant_pool.h b/include/prism/util/pm_constant_pool.h
index 3ea335ac..defd6ad2 100644
--- a/include/prism/util/pm_constant_pool.h
+++ b/include/prism/util/pm_constant_pool.h
@@ -39,9 +39,21 @@ size_t pm_constant_id_list_memsize(pm_constant_id_list_t *list);
 // Free the memory associated with a list of constant ids.
 void pm_constant_id_list_free(pm_constant_id_list_t *list);
 
+// Constant pool buckets can have a couple of different types.
+typedef unsigned int pm_constant_pool_bucket_type_t;
+
+// By default, each constant is a slice of the source.
+static const pm_constant_pool_bucket_type_t PM_CONSTANT_POOL_BUCKET_DEFAULT = 0;
+
+// An owned constant is one for which memory has been allocated.
+static const pm_constant_pool_bucket_type_t PM_CONSTANT_POOL_BUCKET_OWNED = 1;
+
+// A constant constant is known at compile time.
+static const pm_constant_pool_bucket_type_t PM_CONSTANT_POOL_BUCKET_CONSTANT = 2;
+
 typedef struct {
-    unsigned int id: 31;
-    bool owned: 1;
+    unsigned int id: 30;
+    pm_constant_pool_bucket_type_t type: 2;
     uint32_t hash;
 } pm_constant_pool_bucket_t;
 
@@ -63,10 +75,8 @@ typedef struct {
 // Initialize a new constant pool with a given capacity.
 bool pm_constant_pool_init(pm_constant_pool_t *pool, uint32_t capacity);
 
-static inline pm_constant_t* pm_constant_pool_id_to_constant(pm_constant_pool_t *pool, pm_constant_id_t constant_id) {
-    assert(constant_id > 0 && constant_id <= pool->size);
-    return &pool->constants[constant_id - 1];
-}
+// Return a pointer to the constant indicated by the given constant id.
+pm_constant_t * pm_constant_pool_id_to_constant(pm_constant_pool_t *pool, pm_constant_id_t constant_id);
 
 // Insert a constant into a constant pool that is a slice of a source string.
 // Returns the id of the constant, or 0 if any potential calls to resize fail.
@@ -77,6 +87,10 @@ pm_constant_id_t pm_constant_pool_insert_shared(pm_constant_pool_t *pool, const
 // resize fail.
 pm_constant_id_t pm_constant_pool_insert_owned(pm_constant_pool_t *pool, const uint8_t *start, size_t length);
 
+// Insert a constant into a constant pool from memory that is constant. Returns
+// the id of the constant, or 0 if any potential calls to resize fail.
+pm_constant_id_t pm_constant_pool_insert_constant(pm_constant_pool_t *pool, const uint8_t *start, size_t length);
+
 // Free the memory associated with a constant pool.
 void pm_constant_pool_free(pm_constant_pool_t *pool);
 
diff --git a/lib/prism/debug.rb b/lib/prism/debug.rb
index 9c6756ff..6a68a3d3 100644
--- a/lib/prism/debug.rb
+++ b/lib/prism/debug.rb
@@ -45,29 +45,16 @@ module Prism
     # For the given source, compiles with CRuby and returns a list of all of the
     # sets of local variables that were encountered.
     def self.cruby_locals(source)
-      verbose = $VERBOSE
-      $VERBOSE = nil
+      verbose, $VERBOSE = $VERBOSE, nil
 
       begin
         locals = []
         stack = [ISeq.new(RubyVM::InstructionSequence.compile(source).to_a)]
 
         while (iseq = stack.pop)
-          if iseq.type != :once
-            names = iseq.local_table
-
-            # CRuby will push on a special local variable when there are keyword
-            # arguments. We get rid of that here.
-            names = names.grep_v(Integer)
-
-            # For some reason, CRuby occasionally pushes this special local
-            # variable when there are splat arguments. We get rid of that here.
-            names = names.grep_v(:"#arg_rest")
-
-            # Now push them onto the list of locals.
-            locals << names
-          end
-
+          # For some reason, CRuby occasionally pushes this special local
+          # variable when there are splat arguments. We get rid of that here.
+          locals << (iseq.local_table - [:"#arg_rest"])
           iseq.each_child { |child| stack << child }
         end
 
@@ -77,6 +64,8 @@ module Prism
       end
     end
 
+    AnonymousLocal = Object.new
+
     # For the given source, parses with prism and returns a list of all of the
     # sets of local variables that were encountered.
     def self.prism_locals(source)
@@ -97,20 +86,27 @@ module Prism
           # order here so that we can compare properly.
           if params
             sorted = [
-              *params.requireds.grep(RequiredParameterNode).map(&:name),
+              *params.requireds.map do |required|
+                if required.is_a?(RequiredParameterNode)
+                  required.name
+                else
+                  AnonymousLocal
+                end
+              end,
               *params.optionals.map(&:name),
               *((params.rest.name || :*) if params.rest && params.rest.operator != ","),
-              *params.posts.grep(RequiredParameterNode).map(&:name),
+              *params.posts.map do |post|
+                if post.is_a?(RequiredParameterNode)
+                  post.name
+                else
+                  AnonymousLocal
+                end
+              end,
               *params.keywords.reject(&:value).map(&:name),
               *params.keywords.select(&:value).map(&:name)
             ]
 
-            # TODO: When we get a ... parameter, we should be pushing * and &
-            # onto the local list. We don't do that yet, so we need to add them
-            # in here.
-            if params.keyword_rest.is_a?(ForwardingParameterNode)
-              sorted.push(:*, :&, :"...")
-            end
+            sorted << AnonymousLocal if params.keywords.any?
 
             # Recurse down the parameter tree to find any destructured
             # parameters and add them after the other parameters.
@@ -129,11 +125,19 @@ module Prism
             names = sorted.concat(names - sorted)
           end
 
+          names.map!.with_index do |name, index|
+            if name == AnonymousLocal
+              names.length - index + 1
+            else
+              name
+            end
+          end
+
           locals << names
         when ClassNode, ModuleNode, ProgramNode, SingletonClassNode
           locals << node.locals
         when ForNode
-          locals << []
+          locals << [2]
         when PostExecutionNode
           locals.push([], [])
         when InterpolatedRegularExpressionNode
diff --git a/src/prism.c b/src/prism.c
index 56b2252c..ae433b62 100644
--- a/src/prism.c
+++ b/src/prism.c
@@ -496,18 +496,8 @@ pm_parser_constant_id_owned(pm_parser_t *parser, const uint8_t *start, size_t le
 
 // Retrieve the constant pool id for the given static literal C string.
 static inline pm_constant_id_t
-pm_parser_constant_id_static(pm_parser_t *parser, const char *start, size_t length) {
-    uint8_t *owned_copy;
-    if (length > 0) {
-        owned_copy = malloc(length);
-        memcpy(owned_copy, start, length);
-    } else {
-        owned_copy = malloc(1);
-        owned_copy[0] = '\0';
-    }
-    return pm_constant_pool_insert_owned(&parser->constant_pool, owned_copy, length);
-    // Does not work because the static literal cannot be serialized as an offset of source
-    // return pm_constant_pool_insert_shared(&parser->constant_pool, start, length);
+pm_parser_constant_id_constant(pm_parser_t *parser, const char *start, size_t length) {
+    return pm_constant_pool_insert_constant(&parser->constant_pool, (const uint8_t *) start, length);
 }
 
 // Retrieve the constant pool id for the given token.
@@ -1458,7 +1448,7 @@ pm_call_node_aref_create(pm_parser_t *parser, pm_node_t *receiver, pm_arguments_
     node->closing_loc = arguments->closing_loc;
     node->block = arguments->block;
 
-    node->name = pm_parser_constant_id_static(parser, "[]", 2);
+    node->name = pm_parser_constant_id_constant(parser, "[]", 2);
     return node;
 }
 
@@ -1558,7 +1548,7 @@ pm_call_node_not_create(pm_parser_t *parser, pm_node_t *receiver, pm_token_t *me
     node->arguments = arguments->arguments;
     node->closing_loc = arguments->closing_loc;
 
-    node->name = pm_parser_constant_id_static(parser, "!", 1);
+    node->name = pm_parser_constant_id_constant(parser, "!", 1);
     return node;
 }
 
@@ -1585,7 +1575,7 @@ pm_call_node_shorthand_create(pm_parser_t *parser, pm_node_t *receiver, pm_token
         node->base.flags |= PM_CALL_NODE_FLAGS_SAFE_NAVIGATION;
     }
 
-    node->name = pm_parser_constant_id_static(parser, "call", 4);
+    node->name = pm_parser_constant_id_constant(parser, "call", 4);
     return node;
 }
 
@@ -1600,7 +1590,7 @@ pm_call_node_unary_create(pm_parser_t *parser, pm_token_t *operator, pm_node_t *
     node->receiver = receiver;
     node->message_loc = PM_OPTIONAL_LOCATION_TOKEN_VALUE(operator);
 
-    node->name = pm_parser_constant_id_static(parser, name, strlen(name));
+    node->name = pm_parser_constant_id_constant(parser, name, strlen(name));
     return node;
 }
 
@@ -1628,7 +1618,8 @@ pm_call_node_variable_call_p(pm_call_node_t *node) {
 static void
 pm_call_write_read_name_init(pm_parser_t *parser, pm_constant_id_t *read_name, pm_constant_id_t *write_name) {
     pm_constant_t *write_constant = pm_constant_pool_id_to_constant(&parser->constant_pool, *write_name);
-    if (write_constant->length >= 1) {
+
+    if (write_constant->length > 0) {
         size_t length = write_constant->length - 1;
 
         void *memory = malloc(length);
@@ -1637,7 +1628,7 @@ pm_call_write_read_name_init(pm_parser_t *parser, pm_constant_id_t *read_name, p
         *read_name = pm_constant_pool_insert_owned(&parser->constant_pool, (uint8_t *) memory, length);
     } else {
         // We can get here if the message was missing because of a syntax error.
-        *read_name = pm_parser_constant_id_static(parser, "", 0);
+        *read_name = pm_parser_constant_id_constant(parser, "", 0);
     }
 }
 
@@ -4994,6 +4985,13 @@ pm_parser_local_add(pm_parser_t *parser, pm_constant_id_t constant_id) {
     }
 }
 
+// Add a local variable from a constant string to the current scope.
+static inline void
+pm_parser_local_add_constant(pm_parser_t *parser, const char *start, size_t length) {
+    pm_constant_id_t constant_id = pm_parser_constant_id_constant(parser, start, length);
+    if (constant_id != 0) pm_parser_local_add(parser, constant_id);
+}
+
 // Add a local variable from a location to the current scope.
 static pm_constant_id_t
 pm_parser_local_add_location(pm_parser_t *parser, const uint8_t *start, const uint8_t *end) {
@@ -9522,7 +9520,7 @@ parse_target(pm_parser_t *parser, pm_node_t *target) {
                 (call->block == NULL)
             ) {
                 // Replace the name with "[]=".
-                call->name = pm_parser_constant_id_static(parser, "[]=", 3);
+                call->name = pm_parser_constant_id_constant(parser, "[]=", 3);
                 return target;
             }
         }
@@ -9689,7 +9687,7 @@ parse_write(pm_parser_t *parser, pm_node_t *target, pm_token_t *operator, pm_nod
                 target->location.end = value->location.end;
 
                 // Replace the name with "[]=".
-                call->name = pm_parser_constant_id_static(parser, "[]=", 3);
+                call->name = pm_parser_constant_id_constant(parser, "[]=", 3);
                 return target;
             }
 
@@ -10310,11 +10308,17 @@ parse_parameters(
                 if (!allows_forwarding_parameter) {
                     pm_parser_err_current(parser, PM_ERR_ARGUMENT_NO_FORWARDING_ELLIPSES);
                 }
+
                 if (order > PM_PARAMETERS_ORDER_NOTHING_AFTER) {
                     update_parameter_state(parser, &parser->current, &order);
                     parser_lex(parser);
 
-                    pm_parser_local_add_token(parser, &parser->previous);
+                    if (allows_forwarding_parameter) {
+                        pm_parser_local_add_constant(parser, "*", 1);
+                        pm_parser_local_add_constant(parser, "&", 1);
+                        pm_parser_local_add_token(parser, &parser->previous);
+                    }
+
                     pm_forwarding_parameter_node_t *param = pm_forwarding_parameter_node_create(parser, &parser->previous);
                     if (params->keyword_rest != NULL) {
                         // If we already have a keyword rest parameter, then we replace it with the
@@ -10329,6 +10333,7 @@ parse_parameters(
                     update_parameter_state(parser, &parser->current, &order);
                     parser_lex(parser);
                 }
+
                 break;
             }
             case PM_TOKEN_CLASS_VARIABLE:
diff --git a/src/util/pm_constant_pool.c b/src/util/pm_constant_pool.c
index d2c4f1e1..63dd86a8 100644
--- a/src/util/pm_constant_pool.c
+++ b/src/util/pm_constant_pool.c
@@ -154,9 +154,16 @@ pm_constant_pool_init(pm_constant_pool_t *pool, uint32_t capacity) {
     return true;
 }
 
+// Return a pointer to the constant indicated by the given constant id.
+pm_constant_t *
+pm_constant_pool_id_to_constant(pm_constant_pool_t *pool, pm_constant_id_t constant_id) {
+    assert(constant_id > 0 && constant_id <= pool->size);
+    return &pool->constants[constant_id - 1];
+}
+
 // Insert a constant into a constant pool and return its index in the pool.
 static inline pm_constant_id_t
-pm_constant_pool_insert(pm_constant_pool_t *pool, const uint8_t *start, size_t length, bool owned) {
+pm_constant_pool_insert(pm_constant_pool_t *pool, const uint8_t *start, size_t length, pm_constant_pool_bucket_type_t type) {
     if (pool->size >= (pool->capacity / 4 * 3)) {
         if (!pm_constant_pool_resize(pool)) return 0;
     }
@@ -178,19 +185,19 @@ pm_constant_pool_insert(pm_constant_pool_t *pool, const uint8_t *start, size_t l
             // Since we have found a match, we need to check if this is
             // attempting to insert a shared or an owned constant. We want to
             // prefer shared constants since they don't require allocations.
-            if (owned) {
+            if (type == PM_CONSTANT_POOL_BUCKET_OWNED) {
                 // If we're attempting to insert an owned constant and we have
                 // an existing constant, then either way we don't want the given
                 // memory. Either it's duplicated with the existing constant or
                 // it's not necessary because we have a shared version.
                 free((void *) start);
-            } else if (bucket->owned) {
+            } else if (bucket->type == PM_CONSTANT_POOL_BUCKET_OWNED) {
                 // If we're attempting to insert a shared constant and the
                 // existing constant is owned, then we can free the owned
                 // constant and replace it with the shared constant.
                 free((void *) constant->start);
                 constant->start = start;
-                bucket->owned = false;
+                bucket->type = PM_CONSTANT_POOL_BUCKET_DEFAULT;
             }
 
             return bucket->id;
@@ -202,15 +209,15 @@ pm_constant_pool_insert(pm_constant_pool_t *pool, const uint8_t *start, size_t l
     // IDs are allocated starting at 1, since the value 0 denotes a non-existant
     // constant.
     uint32_t id = ++pool->size;
-    assert(pool->size < ((uint32_t) (1 << 31)));
+    assert(pool->size < ((uint32_t) (1 << 30)));
 
     *bucket = (pm_constant_pool_bucket_t) {
-        .id = (unsigned int) (id & 0x7FFFFFFF),
-        .owned = owned,
+        .id = (unsigned int) (id & 0x3fffffff),
+        .type = (unsigned int) (type & 0x3),
         .hash = hash
     };
 
-    pool->constants[id - 1]  = (pm_constant_t) {
+    pool->constants[id - 1] = (pm_constant_t) {
         .start = start,
         .length = length,
     };
@@ -222,7 +229,7 @@ pm_constant_pool_insert(pm_constant_pool_t *pool, const uint8_t *start, size_t l
 // if any potential calls to resize fail.
 pm_constant_id_t
 pm_constant_pool_insert_shared(pm_constant_pool_t *pool, const uint8_t *start, size_t length) {
-    return pm_constant_pool_insert(pool, start, length, false);
+    return pm_constant_pool_insert(pool, start, length, PM_CONSTANT_POOL_BUCKET_DEFAULT);
 }
 
 // Insert a constant into a constant pool from memory that is now owned by the
@@ -230,7 +237,14 @@ pm_constant_pool_insert_shared(pm_constant_pool_t *pool, const uint8_t *start, s
 // resize fail.
 pm_constant_id_t
 pm_constant_pool_insert_owned(pm_constant_pool_t *pool, const uint8_t *start, size_t length) {
-    return pm_constant_pool_insert(pool, start, length, true);
+    return pm_constant_pool_insert(pool, start, length, PM_CONSTANT_POOL_BUCKET_OWNED);
+}
+
+// Insert a constant into a constant pool from memory that is constant. Returns
+// the id of the constant, or 0 if any potential calls to resize fail.
+pm_constant_id_t
+pm_constant_pool_insert_constant(pm_constant_pool_t *pool, const uint8_t *start, size_t length) {
+    return pm_constant_pool_insert(pool, start, length, PM_CONSTANT_POOL_BUCKET_CONSTANT);
 }
 
 // Free the memory associated with a constant pool.
@@ -242,7 +256,7 @@ pm_constant_pool_free(pm_constant_pool_t *pool) {
         pm_constant_pool_bucket_t *bucket = &pool->buckets[index];
 
         // If an id is set on this constant, then we know we have content here.
-        if (bucket->id != 0 && bucket->owned) {
+        if (bucket->id != 0 && bucket->type == PM_CONSTANT_POOL_BUCKET_OWNED) {
             pm_constant_t *constant = &pool->constants[bucket->id - 1];
             free((void *) constant->start);
         }
diff --git a/templates/src/serialize.c.erb b/templates/src/serialize.c.erb
index 9ee179af..006fae6f 100644
--- a/templates/src/serialize.c.erb
+++ b/templates/src/serialize.c.erb
@@ -234,12 +234,12 @@ pm_serialize_content(pm_parser_t *parser, pm_node_t *node, pm_buffer_t *buffer)
             pm_constant_t *constant = &parser->constant_pool.constants[bucket->id - 1];
             size_t buffer_offset = offset + ((((size_t)bucket->id) - 1) * 8);
 
-            if (bucket->owned) {
-                // Since this is an owned constant, we are going to write its
-                // contents into the buffer after the constant pool. So
-                // effectively in place of the source offset, we have a buffer
-                // offset. We will add a leading 1 to indicate that this is a
-                // buffer offset.
+            if (bucket->type == PM_CONSTANT_POOL_BUCKET_OWNED || bucket->type == PM_CONSTANT_POOL_BUCKET_CONSTANT) {
+                // Since this is an owned or constant constant, we are going to
+                // write its contents into the buffer after the constant pool.
+                // So effectively in place of the source offset, we have a
+                // buffer offset. We will add a leading 1 to indicate that this
+                // is a buffer offset.
                 uint32_t content_offset = pm_sizet_to_u32(buffer->length);
                 uint32_t owned_mask = (uint32_t) (1 << 31);
 


Test patch:
diff --git a/test/prism/errors_test.rb b/test/prism/errors_test.rb
index e043c591..70fd030a 100644
--- a/test/prism/errors_test.rb
+++ b/test/prism/errors_test.rb
@@ -723,7 +723,7 @@ module Prism
           nil
         ),
         nil,
-        [:"...", :a],
+        [:*, :&, :"...", :a],
         Location(),
         nil,
         Location(),
@@ -800,7 +800,7 @@ module Prism
         nil,
         ParametersNode([], [], nil, [], [], ForwardingParameterNode(), nil),
         nil,
-        [:"..."],
+        [:*, :&, :"..."],
         Location(),
         nil,
         Location(),
@@ -987,7 +987,7 @@ module Prism
 
     def test_do_not_allow_forward_arguments_in_lambda_literals
       expected = LambdaNode(
-        [:"..."],
+        [],
         Location(),
         Location(),
         Location(),
@@ -1009,7 +1009,7 @@ module Prism
         nil,
         nil,
         BlockNode(
-          [:"..."],
+          [],
           BlockParametersNode(ParametersNode([], [], nil, [], [], ForwardingParameterNode(), nil), [], Location(), Location()),
           nil,
           Location(),
diff --git a/test/prism/snapshots/methods.txt b/test/prism/snapshots/methods.txt
index 73db4ae5..bf681386 100644
--- a/test/prism/snapshots/methods.txt
+++ b/test/prism/snapshots/methods.txt
@@ -183,7 +183,7 @@
         │   │   │   @ ForwardingParameterNode (location: (19,6)-(19,9))
         │   │   └── block: ∅
         │   ├── body: ∅
-        │   ├── locals: [:"..."]
+        │   ├── locals: [:*, :&, :"..."]
         │   ├── def_keyword_loc: (19,0)-(19,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: (19,5)-(19,6) = "("
@@ -949,7 +949,7 @@
         │   │           ├── block: ∅
         │   │           ├── flags: ∅
         │   │           └── name: :b
-        │   ├── locals: [:"..."]
+        │   ├── locals: [:*, :&, :"..."]
         │   ├── def_keyword_loc: (112,0)-(112,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: (112,5)-(112,6) = "("
@@ -990,7 +990,7 @@
         │   │           ├── block: ∅
         │   │           ├── flags: ∅
         │   │           └── name: :b
-        │   ├── locals: [:"..."]
+        │   ├── locals: [:*, :&, :"..."]
         │   ├── def_keyword_loc: (114,0)-(114,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: (114,5)-(114,6) = "("
@@ -1210,7 +1210,7 @@
         │   │           │       │           └── name: :b
         │   │           │       └── closing_loc: (136,24)-(136,25) = "}"
         │   │           └── closing_loc: (136,25)-(136,26) = "\""
-        │   ├── locals: [:"..."]
+        │   ├── locals: [:*, :&, :"..."]
         │   ├── def_keyword_loc: (136,0)-(136,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: (136,5)-(136,6) = "("
diff --git a/test/prism/snapshots/seattlerb/defn_arg_forward_args.txt b/test/prism/snapshots/seattlerb/defn_arg_forward_args.txt
index 7fb3e123..75990338 100644
--- a/test/prism/snapshots/seattlerb/defn_arg_forward_args.txt
+++ b/test/prism/snapshots/seattlerb/defn_arg_forward_args.txt
@@ -38,7 +38,7 @@
             │           ├── block: ∅
             │           ├── flags: ∅
             │           └── name: :b
-            ├── locals: [:x, :"..."]
+            ├── locals: [:x, :*, :&, :"..."]
             ├── def_keyword_loc: (1,0)-(1,3) = "def"
             ├── operator_loc: ∅
             ├── lparen_loc: (1,5)-(1,6) = "("
diff --git a/test/prism/snapshots/seattlerb/defn_args_forward_args.txt b/test/prism/snapshots/seattlerb/defn_args_forward_args.txt
index dfd0b67c..c1f190d5 100644
--- a/test/prism/snapshots/seattlerb/defn_args_forward_args.txt
+++ b/test/prism/snapshots/seattlerb/defn_args_forward_args.txt
@@ -47,7 +47,7 @@
             │           ├── block: ∅
             │           ├── flags: ∅
             │           └── name: :b
-            ├── locals: [:x, :y, :z, :"..."]
+            ├── locals: [:x, :y, :z, :*, :&, :"..."]
             ├── def_keyword_loc: (1,0)-(1,3) = "def"
             ├── operator_loc: ∅
             ├── lparen_loc: (1,5)-(1,6) = "("
diff --git a/test/prism/snapshots/seattlerb/defn_forward_args.txt b/test/prism/snapshots/seattlerb/defn_forward_args.txt
index 4f4bc7cc..a711515c 100644
--- a/test/prism/snapshots/seattlerb/defn_forward_args.txt
+++ b/test/prism/snapshots/seattlerb/defn_forward_args.txt
@@ -33,7 +33,7 @@
             │           ├── block: ∅
             │           ├── flags: ∅
             │           └── name: :b
-            ├── locals: [:"..."]
+            ├── locals: [:*, :&, :"..."]
             ├── def_keyword_loc: (1,0)-(1,3) = "def"
             ├── operator_loc: ∅
             ├── lparen_loc: (1,5)-(1,6) = "("
diff --git a/test/prism/snapshots/seattlerb/defn_forward_args__no_parens.txt b/test/prism/snapshots/seattlerb/defn_forward_args__no_parens.txt
index f3eaef61..db999623 100644
--- a/test/prism/snapshots/seattlerb/defn_forward_args__no_parens.txt
+++ b/test/prism/snapshots/seattlerb/defn_forward_args__no_parens.txt
@@ -33,7 +33,7 @@
             │           ├── block: ∅
             │           ├── flags: ∅
             │           └── name: :m
-            ├── locals: [:"..."]
+            ├── locals: [:*, :&, :"..."]
             ├── def_keyword_loc: (1,0)-(1,3) = "def"
             ├── operator_loc: ∅
             ├── lparen_loc: ∅
diff --git a/test/prism/snapshots/whitequark/endless_method_forwarded_args_legacy.txt b/test/prism/snapshots/whitequark/endless_method_forwarded_args_legacy.txt
index c5b7ca8b..3117027f 100644
--- a/test/prism/snapshots/whitequark/endless_method_forwarded_args_legacy.txt
+++ b/test/prism/snapshots/whitequark/endless_method_forwarded_args_legacy.txt
@@ -33,7 +33,7 @@
             │           ├── block: ∅
             │           ├── flags: ∅
             │           └── name: :bar
-            ├── locals: [:"..."]
+            ├── locals: [:*, :&, :"..."]
             ├── def_keyword_loc: (1,0)-(1,3) = "def"
             ├── operator_loc: ∅
             ├── lparen_loc: (1,7)-(1,8) = "("
diff --git a/test/prism/snapshots/whitequark/forward_arg.txt b/test/prism/snapshots/whitequark/forward_arg.txt
index 6db649d9..58e17007 100644
--- a/test/prism/snapshots/whitequark/forward_arg.txt
+++ b/test/prism/snapshots/whitequark/forward_arg.txt
@@ -33,7 +33,7 @@
             │           ├── block: ∅
             │           ├── flags: ∅
             │           └── name: :bar
-            ├── locals: [:"..."]
+            ├── locals: [:*, :&, :"..."]
             ├── def_keyword_loc: (1,0)-(1,3) = "def"
             ├── operator_loc: ∅
             ├── lparen_loc: (1,7)-(1,8) = "("
diff --git a/test/prism/snapshots/whitequark/forward_arg_with_open_args.txt b/test/prism/snapshots/whitequark/forward_arg_with_open_args.txt
index 28f8e2c8..f2cf7c5e 100644
--- a/test/prism/snapshots/whitequark/forward_arg_with_open_args.txt
+++ b/test/prism/snapshots/whitequark/forward_arg_with_open_args.txt
@@ -37,7 +37,7 @@
         │   │           │           ├── block: ∅
         │   │           │           ├── flags: ∅
         │   │           │           └── name: :bar
-        │   │           ├── locals: [:"..."]
+        │   │           ├── locals: [:*, :&, :"..."]
         │   │           ├── def_keyword_loc: (1,1)-(1,4) = "def"
         │   │           ├── operator_loc: ∅
         │   │           ├── lparen_loc: ∅
@@ -80,7 +80,7 @@
         │   │           │           ├── block: ∅
         │   │           │           ├── flags: ∅
         │   │           │           └── name: :bar
-        │   │           ├── locals: [:"..."]
+        │   │           ├── locals: [:*, :&, :"..."]
         │   │           ├── def_keyword_loc: (5,1)-(5,4) = "def"
         │   │           ├── operator_loc: ∅
         │   │           ├── lparen_loc: ∅
@@ -104,7 +104,7 @@
         │   │   │   @ ForwardingParameterNode (location: (7,8)-(7,11))
         │   │   └── block: ∅
         │   ├── body: ∅
-        │   ├── locals: [:"..."]
+        │   ├── locals: [:*, :&, :"..."]
         │   ├── def_keyword_loc: (7,0)-(7,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: ∅
@@ -141,7 +141,7 @@
         │   │           ├── block: ∅
         │   │           ├── flags: ∅
         │   │           └── name: :bar
-        │   ├── locals: [:"..."]
+        │   ├── locals: [:*, :&, :"..."]
         │   ├── def_keyword_loc: (10,0)-(10,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: ∅
@@ -180,7 +180,7 @@
         │   │           ├── block: ∅
         │   │           ├── flags: ∅
         │   │           └── name: :bar
-        │   ├── locals: [:a, :"..."]
+        │   ├── locals: [:a, :*, :&, :"..."]
         │   ├── def_keyword_loc: (12,0)-(12,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: ∅
@@ -219,7 +219,7 @@
         │   │           ├── block: ∅
         │   │           ├── flags: ∅
         │   │           └── name: :bar
-        │   ├── locals: [:a, :"..."]
+        │   ├── locals: [:a, :*, :&, :"..."]
         │   ├── def_keyword_loc: (16,0)-(16,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: ∅
@@ -250,7 +250,7 @@
         │   │   │   @ ForwardingParameterNode (location: (18,18)-(18,21))
         │   │   └── block: ∅
         │   ├── body: ∅
-        │   ├── locals: [:a, :b, :"..."]
+        │   ├── locals: [:a, :b, :*, :&, :"..."]
         │   ├── def_keyword_loc: (18,0)-(18,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: ∅
@@ -294,7 +294,7 @@
         │   │           ├── block: ∅
         │   │           ├── flags: ∅
         │   │           └── name: :bar
-        │   ├── locals: [:b, :"..."]
+        │   ├── locals: [:b, :*, :&, :"..."]
         │   ├── def_keyword_loc: (21,0)-(21,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: ∅
@@ -338,7 +338,7 @@
         │   │           ├── block: ∅
         │   │           ├── flags: ∅
         │   │           └── name: :bar
-        │   ├── locals: [:b, :"..."]
+        │   ├── locals: [:b, :*, :&, :"..."]
         │   ├── def_keyword_loc: (25,0)-(25,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: ∅
@@ -377,7 +377,7 @@
             │           ├── block: ∅
             │           ├── flags: ∅
             │           └── name: :bar
-            ├── locals: [:a, :"..."]
+            ├── locals: [:a, :*, :&, :"..."]
             ├── def_keyword_loc: (27,0)-(27,3) = "def"
             ├── operator_loc: ∅
             ├── lparen_loc: (27,7)-(27,8) = "("
diff --git a/test/prism/snapshots/whitequark/forward_args_legacy.txt b/test/prism/snapshots/whitequark/forward_args_legacy.txt
index 281e5e97..8ab9fd40 100644
--- a/test/prism/snapshots/whitequark/forward_args_legacy.txt
+++ b/test/prism/snapshots/whitequark/forward_args_legacy.txt
@@ -33,7 +33,7 @@
         │   │           ├── block: ∅
         │   │           ├── flags: ∅
         │   │           └── name: :bar
-        │   ├── locals: [:"..."]
+        │   ├── locals: [:*, :&, :"..."]
         │   ├── def_keyword_loc: (1,0)-(1,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: (1,7)-(1,8) = "("
@@ -55,7 +55,7 @@
         │   │   │   @ ForwardingParameterNode (location: (3,8)-(3,11))
         │   │   └── block: ∅
         │   ├── body: ∅
-        │   ├── locals: [:"..."]
+        │   ├── locals: [:*, :&, :"..."]
         │   ├── def_keyword_loc: (3,0)-(3,3) = "def"
         │   ├── operator_loc: ∅
         │   ├── lparen_loc: (3,7)-(3,8) = "("
@@ -88,7 +88,7 @@
             │           │       └── @ ForwardingArgumentsNode (location: (5,20)-(5,23))
             │           ├── rparen_loc: (5,23)-(5,24) = ")"
             │           └── block: ∅
-            ├── locals: [:"..."]
+            ├── locals: [:*, :&, :"..."]
             ├── def_keyword_loc: (5,0)-(5,3) = "def"
             ├── operator_loc: ∅
             ├── lparen_loc: (5,7)-(5,8) = "("
diff --git a/test/prism/snapshots/whitequark/trailing_forward_arg.txt b/test/prism/snapshots/whitequark/trailing_forward_arg.txt
index 15c8b4ac..f30c0b1e 100644
--- a/test/prism/snapshots/whitequark/trailing_forward_arg.txt
+++ b/test/prism/snapshots/whitequark/trailing_forward_arg.txt
@@ -42,7 +42,7 @@
             │           ├── block: ∅
             │           ├── flags: ∅
             │           └── name: :bar
-            ├── locals: [:a, :b, :"..."]
+            ├── locals: [:a, :b, :*, :&, :"..."]
             ├── def_keyword_loc: (1,0)-(1,3) = "def"
             ├── operator_loc: ∅
             ├── lparen_loc: (1,7)-(1,8) = "("