From d92e1e22f2de22ec5cfdad7987c13a04286fbdc8 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Tue, 28 Apr 2026 22:25:48 +0900 Subject: [PATCH] Fix anonymous rest/keyword sentinel collision with empty parens `CallBaseNode` and `HashNode` used `DummyNilNode` as a sentinel for anonymous rest forwarding (bare `*`) and anonymous keyword splat forwarding (bare `**`), checking via `is_a?(DummyNilNode)` at install time. Once empty parentheses `()` started producing `DummyNilNode` (via the parentheses_node unwrap in AST.create_node), passing `()` as a method argument or hash splat value was misinterpreted as anonymous forwarding, raising `RuntimeError: *anonymous_rest` or `**anonymous_keyword` from `LocalEnv#get_var`. Use plain `nil` as the placeholder for those forwarding cases instead, so `DummyNilNode` is reserved for actual nil-valued expressions. Also extend `scenario/misc/parens.rb` with cases where empty parens appear as an array element, an if condition, a method argument, a splatted argument, and a hash double-splat value. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/typeprof/core/ast/call.rb | 4 ++-- lib/typeprof/core/ast/value.rb | 4 ++-- scenario/misc/parens.rb | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/typeprof/core/ast/call.rb b/lib/typeprof/core/ast/call.rb index de6a7e5f5..a2ddb1e31 100644 --- a/lib/typeprof/core/ast/call.rb +++ b/lib/typeprof/core/ast/call.rb @@ -37,7 +37,7 @@ def initialize(raw_node, recv, mid, mid_code_range, raw_args, last_arg, raw_bloc @splat_flags << false end end - @positional_args = args.map {|arg| arg ? AST.create_node(arg, lenv) : DummyNilNode.new(code_range, lenv) } + @positional_args = args.map {|arg| arg ? AST.create_node(arg, lenv) : nil } kw = @positional_args.last if kw.is_a?(TypeProf::Core::AST::HashNode) && kw.keywords @@ -119,7 +119,7 @@ def install0(genv) keyword_args = forward_a_args.keywords else positional_args = @positional_args.map do |arg| - if arg.is_a?(DummyNilNode) + if arg.nil? @lenv.get_var(:"*anonymous_rest") else arg.install(genv) diff --git a/lib/typeprof/core/ast/value.rb b/lib/typeprof/core/ast/value.rb index a5cc282e2..a90e26f64 100644 --- a/lib/typeprof/core/ast/value.rb +++ b/lib/typeprof/core/ast/value.rb @@ -275,7 +275,7 @@ def initialize(raw_node, lenv, keywords) if raw_elem.value @vals << AST.create_node(raw_elem.value, lenv) else - @vals << DummyNilNode.new(code_range, lenv) + @vals << nil end @splat = true else @@ -306,7 +306,7 @@ def install0(genv) all_symbol_keys = false end else - if val.is_a?(DummyNilNode) + if val.nil? h = @lenv.get_var(:"**anonymous_keyword") else h = val.install(genv) diff --git a/scenario/misc/parens.rb b/scenario/misc/parens.rb index 2c104f348..e40a56936 100644 --- a/scenario/misc/parens.rb +++ b/scenario/misc/parens.rb @@ -15,11 +15,38 @@ def with_default(x = ()) x end +def in_array + [()] +end + +def in_condition + if (); 1; end +end + +def take_arg(x); x; end + +def empty_as_arg + take_arg(()) +end + +def empty_as_splat_arg + take_arg(*()) +end + +def empty_as_kw_splat + { **() } +end + grouping nested empty with_default with_default(1) +in_array +in_condition +empty_as_arg +empty_as_splat_arg +empty_as_kw_splat ## assert class Object @@ -27,4 +54,10 @@ def grouping: -> Integer def nested: -> Integer def empty: -> nil def with_default: (?Integer?) -> Integer? + def in_array: -> [nil] + def in_condition: -> Integer? + def take_arg: (nil) -> nil + def empty_as_arg: -> nil + def empty_as_splat_arg: -> nil + def empty_as_kw_splat: -> Hash[untyped, untyped] end