Skip to content

Commit 43654b3

Browse files
committed
Gate forwarding parameter syntax behind a parser option
`(...)` forwarding parameters (#3042) are syntax-only for now: nothing defines their type checking semantics yet, and distributing signatures that use them would break older parsers and tools that silently drop the node. Introduce `rbs_parser_options_t` and `rbs_parser_new_with_options()` so the syntax must be opted into at the C API level. `rbs_parser_new()` uses the zero-initialized options, which disable every optional syntax, and the Ruby API doesn't expose the option, so `(...)` is a syntax error everywhere in the gem. The AST, types, and serialization support stays in place for when the semantics are settled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bgoC4byczqmYRzNrDkVrL
1 parent 91601f1 commit 43654b3

5 files changed

Lines changed: 65 additions & 25 deletions

File tree

include/rbs/parser.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ typedef struct rbs_error_t {
4040
bool syntax_error;
4141
} rbs_error_t;
4242

43+
/**
44+
* Options that control which syntax the parser accepts.
45+
*
46+
* Zero-initializing the struct gives the default configuration, where
47+
* every optional syntax is disabled.
48+
* */
49+
typedef struct {
50+
/**
51+
* Accept `(...)` forwarding parameters in method types.
52+
*
53+
* The syntax is experimental and disabled by default.
54+
* */
55+
bool enable_forwarding_params;
56+
} rbs_parser_options_t;
57+
4358
/**
4459
* An RBS parser is a LL(3) parser.
4560
* */
@@ -57,6 +72,8 @@ typedef struct {
5772
rbs_constant_pool_t constant_pool;
5873
rbs_allocator_t *allocator;
5974
rbs_error_t *error;
75+
76+
rbs_parser_options_t options;
6077
} rbs_parser_t;
6178

6279
/**
@@ -107,6 +124,16 @@ RBS_NODISCARD rbs_lexer_t *rbs_lexer_new(rbs_allocator_t *, rbs_string_t string,
107124
* Returns `NULL` for a `start_pos` that `rbs_lexer_new` rejects.
108125
* */
109126
RBS_NODISCARD rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos);
127+
128+
/**
129+
* Allocate new rbs_parser_t object with the given options.
130+
*
131+
* `rbs_parser_new` is equivalent to passing a zero-initialized
132+
* `rbs_parser_options_t`, which disables every optional syntax.
133+
*
134+
* Returns `NULL` for a `start_pos` that `rbs_lexer_new` rejects.
135+
* */
136+
RBS_NODISCARD rbs_parser_t *rbs_parser_new_with_options(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos, rbs_parser_options_t options);
110137
void rbs_parser_free(rbs_parser_t *parser);
111138

112139
/**

src/parser.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,11 @@ static bool parse_params(rbs_parser_t *parser, method_params *params, bool forwa
556556
return false;
557557
}
558558

559+
if (!parser->options.enable_forwarding_params) {
560+
rbs_parser_set_error(parser, parser->next_token, true, "forwarding parameter syntax is not enabled");
561+
return false;
562+
}
563+
559564
rbs_parser_advance(parser);
560565
params->forwarding = (rbs_node_t *) rbs_types_function_forwarding_param_new(
561566
ALLOCATOR(),
@@ -3569,6 +3574,10 @@ rbs_lexer_t *rbs_lexer_new(rbs_allocator_t *allocator, rbs_string_t string, cons
35693574
}
35703575

35713576
rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos) {
3577+
return rbs_parser_new_with_options(string, encoding, start_pos, end_pos, (rbs_parser_options_t) { 0 });
3578+
}
3579+
3580+
rbs_parser_t *rbs_parser_new_with_options(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos, rbs_parser_options_t options) {
35723581
rbs_allocator_t *allocator = rbs_allocator_init();
35733582

35743583
rbs_lexer_t *lexer = rbs_lexer_new(allocator, string, encoding, start_pos, end_pos);
@@ -3593,6 +3602,8 @@ rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding
35933602
.constant_pool = { 0 },
35943603
.allocator = allocator,
35953604
.error = NULL,
3605+
3606+
.options = options,
35963607
};
35973608

35983609
// The parser's constant pool is mainly used for storing the names of type variables, which usually aren't many.

test/rbs/method_type_parsing_test.rb

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -48,34 +48,29 @@ def test_method_param
4848
end
4949
end
5050

51-
def test_forwarding_parameter
52-
parse_method_type("(...) -> void").tap do |type|
53-
assert_equal "(...) -> void", type.to_s
54-
assert_instance_of Types::Function::ForwardingParam, type.type.forwarding
55-
assert_equal "...", type.type.forwarding.location.source
56-
assert_empty type.type.required_positionals
57-
assert_nil type.block
51+
def test_forwarding_parameter_syntax_is_not_enabled
52+
# `(...)` forwarding parameters are gated behind a C-level parser option
53+
# (`rbs_parser_options_t`) that the Ruby API doesn't expose, so parsing
54+
# them from Ruby is always an error.
55+
error = assert_raise(RBS::ParsingError) do
56+
parse_method_type("(...) -> void")
5857
end
58+
assert_include error.message, "forwarding parameter syntax is not enabled"
5959

60-
parse_method_type("(String message, ...) -> void").tap do |type|
61-
assert_equal "(String message, ...) -> void", type.to_s
62-
assert_equal 1, type.type.required_positionals.size
63-
assert_predicate type.type, :forwarding?
64-
assert_instance_of Types::Function::ForwardingParam, type.type.forwarding
60+
assert_raise(RBS::ParsingError) do
61+
parse_method_type("(String message, ...) -> void")
6562
end
6663
end
6764

68-
def test_forwarding_parameter_with_overload_continuation
69-
_, _, declarations = parse_signature(<<~RBS)
70-
class Foo
71-
def foo: (...) -> void
72-
| ...
73-
end
74-
RBS
75-
76-
method = declarations.fetch(0).members.fetch(0)
77-
assert_predicate method, :overloading?
78-
assert_predicate method.overloads.fetch(0).method_type.type, :forwarding?
65+
def test_forwarding_parameter_syntax_is_not_enabled_in_signature
66+
assert_raise(RBS::ParsingError) do
67+
parse_signature(<<~RBS)
68+
class Foo
69+
def foo: (...) -> void
70+
| ...
71+
end
72+
RBS
73+
end
7974
end
8075

8176
def test_forwarding_parameter_rejects_nonleading_parameters

test/rbs/schema_test.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,16 @@ def test_method_type_schema
121121
parse_method_type("[G] (A a, ?B, *C, d: D, ?e: E e, **f) ?{ (G) -> void } -> String").to_json
122122
)
123123

124+
# Forwarding parameters can't be parsed from Ruby, so build the node directly
124125
JSONValidator.method_type.validate!(
125-
parse_method_type("(String message, ...) -> void").to_json
126+
RBS::MethodType.new(
127+
type_params: [],
128+
type: RBS::Types::Function.empty(RBS::Types::Bases::Void.new(location: nil)).update(
129+
forwarding: RBS::Types::Function::ForwardingParam.new(location: nil)
130+
),
131+
block: nil,
132+
location: nil
133+
).to_json
126134
)
127135
end
128136

test/rbs/wasm/serialization_test.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ def test_method_type_round_trip
109109
"(Integer) -> String",
110110
"[T] (T) -> T",
111111
"(Integer, ?String, *Symbol, foo: bool, ?bar: Integer, **untyped) -> void",
112-
"(String message, ...) -> void",
113112
"() { (Integer) -> void } -> bool",
114113
"() ?{ () -> void } -> void",
115114
"[A, B < Comparable[A]] (A) -> B",

0 commit comments

Comments
 (0)