[ruby/prism] Support forwarding flags on scopes

When parent scopes around an eval are forwarding parameters (like
*, **, &, or ...) we need to know that information when we are in
the parser. As such, we need to support passing that information
into the scopes option. In order to do this, unfortunately we need
a bunch of changes.

The scopes option was previously an array of array of strings.
These corresponded to the names of the locals in the parent scopes.
We still support this, but now additionally support passing in a
Prism::Scope instance at each index in the array. This Prism::Scope
class holds both the names of the locals as well as an array of
forwarding parameter names (symbols corresponding to the forwarding
parameters). There is convenience function on the Prism module that
creates a Prism::Scope object using Prism.scope.

In JavaScript, we now additionally support an object much the same
as the Ruby side. In Java, we now have a ParsingOptions.Scope class
that holds that information. In the dump APIs, these objects in all
3 languages will add an additional byte for the forwarding flags in
the middle of the scopes serialization.

All of this is in service of properly parsing the following code:

```ruby
def foo(*) = eval("bar(*)")
```

https://github.com/ruby/prism/commit/21abb6b7c4
This commit is contained in:
Kevin Newton 2025-01-14 13:10:46 -05:00 committed by git
parent f5fa1ee5f6
commit 51d3d6ac8c
7 changed files with 172 additions and 10 deletions

View File

@ -478,10 +478,35 @@ module Prism
values << scopes.length
scopes.each do |scope|
template << "L"
values << scope.length
locals = nil
forwarding = 0
scope.each do |local|
case scope
when Array
locals = scope
when Scope
locals = scope.locals
scope.forwarding.each do |forward|
case forward
when :* then forwarding |= 0x1
when :** then forwarding |= 0x2
when :& then forwarding |= 0x4
when :"..." then forwarding |= 0x8
else raise ArgumentError, "invalid forwarding value: #{forward}"
end
end
else
raise TypeError, "wrong argument type #{scope.class.inspect} (expected Array or Prism::Scope)"
end
template << "L"
values << locals.length
template << "C"
values << forwarding
locals.each do |local|
name = local.name
template << "L"
values << name.bytesize

View File

@ -879,4 +879,32 @@ module Prism
freeze
end
end
# This object is passed to the various Prism.* methods that accept the
# `scopes` option as an element of the list. It defines both the local
# variables visible at that scope as well as the forwarding parameters
# available at that scope.
class Scope
# The list of local variables that are defined in this scope. This should be
# defined as an array of symbols.
attr_reader :locals
# The list of local variables that are forwarded to the next scope. This
# should by defined as an array of symbols containing the specific values of
# :*, :**, :&, or :"...".
attr_reader :forwarding
# Create a new scope object with the given locals and forwarding.
def initialize(locals, forwarding)
@locals = locals
@forwarding = forwarding
end
end
# Create a new scope with the given locals and forwarding options that is
# suitable for passing into one of the Prism.* methods that accepts the
# `scopes` option.
def self.scope(locals: [], forwarding: [])
Scope.new(locals, forwarding)
end
end

View File

@ -24,6 +24,7 @@ VALUE rb_cPrismParseResult;
VALUE rb_cPrismLexResult;
VALUE rb_cPrismParseLexResult;
VALUE rb_cPrismStringQuery;
VALUE rb_cPrismScope;
VALUE rb_cPrismDebugEncoding;
@ -38,6 +39,10 @@ ID rb_id_option_partial_script;
ID rb_id_option_scopes;
ID rb_id_option_version;
ID rb_id_source_for;
ID rb_id_forwarding_positionals;
ID rb_id_forwarding_keywords;
ID rb_id_forwarding_block;
ID rb_id_forwarding_all;
/******************************************************************************/
/* IO of Ruby code */
@ -95,14 +100,53 @@ build_options_scopes(pm_options_t *options, VALUE scopes) {
for (size_t scope_index = 0; scope_index < scopes_count; scope_index++) {
VALUE scope = rb_ary_entry(scopes, scope_index);
// Check that the scope is an array. If it's not, then raise a type
// error.
if (!RB_TYPE_P(scope, T_ARRAY)) {
rb_raise(rb_eTypeError, "wrong argument type %"PRIsVALUE" (expected Array)", rb_obj_class(scope));
// The scope can be either an array or it can be a Prism::Scope object.
// Parse out the correct values here from either.
VALUE locals;
uint8_t forwarding = PM_OPTIONS_SCOPE_FORWARDING_NONE;
if (RB_TYPE_P(scope, T_ARRAY)) {
locals = scope;
} else if (rb_obj_is_kind_of(scope, rb_cPrismScope)) {
locals = rb_ivar_get(scope, rb_intern("@locals"));
if (!RB_TYPE_P(locals, T_ARRAY)) {
rb_raise(rb_eTypeError, "wrong argument type %"PRIsVALUE" (expected Array)", rb_obj_class(locals));
}
VALUE names = rb_ivar_get(scope, rb_intern("@forwarding"));
if (!RB_TYPE_P(names, T_ARRAY)) {
rb_raise(rb_eTypeError, "wrong argument type %"PRIsVALUE" (expected Array)", rb_obj_class(names));
}
size_t names_count = RARRAY_LEN(names);
for (size_t name_index = 0; name_index < names_count; name_index++) {
VALUE name = rb_ary_entry(names, name_index);
// Check that the name is a symbol. If it's not, then raise
// a type error.
if (!RB_TYPE_P(name, T_SYMBOL)) {
rb_raise(rb_eTypeError, "wrong argument type %"PRIsVALUE" (expected Symbol)", rb_obj_class(name));
}
ID id = SYM2ID(name);
if (id == rb_id_forwarding_positionals) {
forwarding |= PM_OPTIONS_SCOPE_FORWARDING_POSITIONALS;
} else if (id == rb_id_forwarding_keywords) {
forwarding |= PM_OPTIONS_SCOPE_FORWARDING_KEYWORDS;
} else if (id == rb_id_forwarding_block) {
forwarding |= PM_OPTIONS_SCOPE_FORWARDING_BLOCK;
} else if (id == rb_id_forwarding_all) {
forwarding |= PM_OPTIONS_SCOPE_FORWARDING_ALL;
} else {
rb_raise(rb_eArgError, "invalid forwarding value: %" PRIsVALUE, name);
}
}
} else {
rb_raise(rb_eTypeError, "wrong argument type %"PRIsVALUE" (expected Array or Prism::Scope)", rb_obj_class(scope));
}
// Initialize the scope array.
size_t locals_count = RARRAY_LEN(scope);
size_t locals_count = RARRAY_LEN(locals);
pm_options_scope_t *options_scope = &options->scopes[scope_index];
if (!pm_options_scope_init(options_scope, locals_count)) {
rb_raise(rb_eNoMemError, "failed to allocate memory");
@ -110,7 +154,7 @@ build_options_scopes(pm_options_t *options, VALUE scopes) {
// Iterate over the locals and add them to the scope.
for (size_t local_index = 0; local_index < locals_count; local_index++) {
VALUE local = rb_ary_entry(scope, local_index);
VALUE local = rb_ary_entry(locals, local_index);
// Check that the local is a symbol. If it's not, then raise a
// type error.
@ -123,6 +167,9 @@ build_options_scopes(pm_options_t *options, VALUE scopes) {
const char *name = rb_id2name(SYM2ID(local));
pm_string_constant_init(scope_local, name, strlen(name));
}
// Now set the forwarding options.
pm_options_scope_forwarding_set(options_scope, forwarding);
}
}
@ -1302,6 +1349,7 @@ Init_prism(void) {
rb_cPrismLexResult = rb_define_class_under(rb_cPrism, "LexResult", rb_cPrismResult);
rb_cPrismParseLexResult = rb_define_class_under(rb_cPrism, "ParseLexResult", rb_cPrismResult);
rb_cPrismStringQuery = rb_define_class_under(rb_cPrism, "StringQuery", rb_cObject);
rb_cPrismScope = rb_define_class_under(rb_cPrism, "Scope", rb_cObject);
// Intern all of the IDs eagerly that we support so that we don't have to do
// it every time we parse.
@ -1316,6 +1364,10 @@ Init_prism(void) {
rb_id_option_scopes = rb_intern_const("scopes");
rb_id_option_version = rb_intern_const("version");
rb_id_source_for = rb_intern("for");
rb_id_forwarding_positionals = rb_intern("*");
rb_id_forwarding_keywords = rb_intern("**");
rb_id_forwarding_block = rb_intern("&");
rb_id_forwarding_all = rb_intern("...");
/**
* The version of the prism library.

View File

@ -181,6 +181,7 @@ PRISM_EXPORTED_FUNCTION bool
pm_options_scope_init(pm_options_scope_t *scope, size_t locals_count) {
scope->locals_count = locals_count;
scope->locals = xcalloc(locals_count, sizeof(pm_string_t));
scope->forwarding = PM_OPTIONS_SCOPE_FORWARDING_NONE;
return scope->locals != NULL;
}
@ -192,6 +193,14 @@ pm_options_scope_local_get(const pm_options_scope_t *scope, size_t index) {
return &scope->locals[index];
}
/**
* Set the forwarding option on the given scope struct.
*/
PRISM_EXPORTED_FUNCTION void
pm_options_scope_forwarding_set(pm_options_scope_t *scope, uint8_t forwarding) {
scope->forwarding = forwarding;
}
/**
* Free the internal memory associated with the options.
*/
@ -300,6 +309,9 @@ pm_options_read(pm_options_t *options, const char *data) {
return;
}
uint8_t forwarding = (uint8_t) *data++;
pm_options_scope_forwarding_set(&options->scopes[scope_index], forwarding);
for (size_t local_index = 0; local_index < locals_count; local_index++) {
uint32_t local_length = pm_options_read_u32(data);
data += 4;

View File

@ -39,8 +39,26 @@ typedef struct pm_options_scope {
/** The names of the locals in the scope. */
pm_string_t *locals;
/** Flags for the set of forwarding parameters in this scope. */
uint8_t forwarding;
} pm_options_scope_t;
/** The default value for parameters. */
static const uint8_t PM_OPTIONS_SCOPE_FORWARDING_NONE = 0x0;
/** When the scope is fowarding with the * parameter. */
static const uint8_t PM_OPTIONS_SCOPE_FORWARDING_POSITIONALS = 0x1;
/** When the scope is fowarding with the ** parameter. */
static const uint8_t PM_OPTIONS_SCOPE_FORWARDING_KEYWORDS = 0x2;
/** When the scope is fowarding with the & parameter. */
static const uint8_t PM_OPTIONS_SCOPE_FORWARDING_BLOCK = 0x4;
/** When the scope is fowarding with the ... parameter. */
static const uint8_t PM_OPTIONS_SCOPE_FORWARDING_ALL = 0x8;
// Forward declaration needed by the callback typedef.
struct pm_options;
@ -337,6 +355,14 @@ PRISM_EXPORTED_FUNCTION bool pm_options_scope_init(pm_options_scope_t *scope, si
*/
PRISM_EXPORTED_FUNCTION const pm_string_t * pm_options_scope_local_get(const pm_options_scope_t *scope, size_t index);
/**
* Set the forwarding option on the given scope struct.
*
* @param scope The scope struct to set the forwarding on.
* @param forwarding The forwarding value to set.
*/
PRISM_EXPORTED_FUNCTION void pm_options_scope_forwarding_set(pm_options_scope_t *scope, uint8_t forwarding);
/**
* Free the internal memory associated with the options.
*
@ -386,6 +412,7 @@ PRISM_EXPORTED_FUNCTION void pm_options_free(pm_options_t *options);
* | # bytes | field |
* | ------- | -------------------------- |
* | `4` | the number of locals |
* | `1` | the forwarding flags |
* | ... | the locals |
*
* Each local is laid out as follows:

View File

@ -22492,7 +22492,7 @@ pm_parser_init(pm_parser_t *parser, const uint8_t *source, size_t size, const pm
// Scopes given from the outside are not allowed to have numbered
// parameters.
parser->current_scope->parameters |= PM_SCOPE_PARAMETERS_IMPLICIT_DISALLOWED;
parser->current_scope->parameters = ((pm_scope_parameters_t) scope->forwarding) | PM_SCOPE_PARAMETERS_IMPLICIT_DISALLOWED;
for (size_t local_index = 0; local_index < scope->locals_count; local_index++) {
const pm_string_t *local = pm_options_scope_local_get(scope, local_index);

View File

@ -140,6 +140,24 @@ module Prism
end
end
def test_scopes
assert_kind_of Prism::CallNode, Prism.parse_statement("foo")
assert_kind_of Prism::LocalVariableReadNode, Prism.parse_statement("foo", scopes: [[:foo]])
assert_kind_of Prism::LocalVariableReadNode, Prism.parse_statement("foo", scopes: [Prism.scope(locals: [:foo])])
assert Prism.parse_failure?("foo(*)")
assert Prism.parse_success?("foo(*)", scopes: [Prism.scope(forwarding: [:*])])
assert Prism.parse_failure?("foo(**)")
assert Prism.parse_success?("foo(**)", scopes: [Prism.scope(forwarding: [:**])])
assert Prism.parse_failure?("foo(&)")
assert Prism.parse_success?("foo(&)", scopes: [Prism.scope(forwarding: [:&])])
assert Prism.parse_failure?("foo(...)")
assert Prism.parse_success?("foo(...)", scopes: [Prism.scope(forwarding: [:"..."])])
end
private
def find_source_file_node(program)