From ef6ac50d8b7bd6080c8dfb0ee6d1601749c86d36 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Thu, 26 Feb 2026 05:38:40 +0000 Subject: [PATCH] Skip overload resolution when splat arguments have uninformative element types The previous fix (ae9dccc) skips overload resolution when positional arguments have no type information. However, splat arguments bypass this check because the positional vertex holds Array types (non-empty) while the array *element* vertex may be empty or conflicting. This causes oscillation: get_rest_args flattens element vertices from all Array types into a single list, and the universal typecheck in match_arguments? fails when elements from different array sources conflict (e.g. Integer from [42] vs String from [@x]). The cycle is: empty elements -> all overloads match -> types flow -> conflicting elements -> no overload matches -> types removed -> empty again. Extend the uninformative-args check to also inspect element vertices of splatted Array types. When any element vertex is empty, skip overload resolution and set up dependency edges, consistent with the existing positional-args fix. --- lib/typeprof/core/graph/box.rb | 22 ++++++++++++++++++- .../splat-overload-oscillation-unseeded.rb | 19 ++++++++++++++++ .../regressions/splat-overload-oscillation.rb | 21 ++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 scenario/regressions/splat-overload-oscillation-unseeded.rb create mode 100644 scenario/regressions/splat-overload-oscillation.rb diff --git a/lib/typeprof/core/graph/box.rb b/lib/typeprof/core/graph/box.rb index dc359c09..7eb4d49f 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -259,7 +259,27 @@ def resolve_overloads(changes, genv, node, param_map, a_args, ret, &blk) # "failed to resolve overloads" diagnostics for untyped arguments. # We still set up dependency edges so the box re-runs when the # empty arguments later receive types. - if a_args.positionals.any? {|vtx| vtx.types.empty? } + # + # For splat arguments, the positional vertex itself holds Array + # types (non-empty), but the array *element* vertex may be empty. + # The same oscillation occurs when match_arguments? extracts + # elements via get_rest_args and the universal typecheck on the + # flattened element list fails due to conflicting array sources. + # We detect this by checking element vertices of splatted arrays. + has_uninformative_args = a_args.positionals.any? {|vtx| vtx.types.empty? } + unless has_uninformative_args + a_args.positionals.each_with_index do |vtx, i| + next unless a_args.splat_flags[i] + vtx.each_type do |ty| + base = ty.base_type(genv) + if base.is_a?(Type::Instance) && base.mod == genv.mod_ary && base.args[0] + has_uninformative_args = true if base.args[0].types.empty? + end + end + break if has_uninformative_args + end + end + if has_uninformative_args a_args.positionals.each do |vtx| changes.add_edge(genv, vtx, changes.target) end diff --git a/scenario/regressions/splat-overload-oscillation-unseeded.rb b/scenario/regressions/splat-overload-oscillation-unseeded.rb new file mode 100644 index 00000000..5ea4c619 --- /dev/null +++ b/scenario/regressions/splat-overload-oscillation-unseeded.rb @@ -0,0 +1,19 @@ +## update: test.rbs +class Foo + def self.f: (*Integer) -> String | (*String) -> Symbol +end + +## update: test.rb +# Minimal reproduction: unseeded splat overload oscillation. +# The splat array's element vertex is empty, triggering the +# skip in overload resolution. +def check + @x = Foo.f(*[@x]) +end + +## assert +class Object + def check: -> untyped +end + +## diagnostics diff --git a/scenario/regressions/splat-overload-oscillation.rb b/scenario/regressions/splat-overload-oscillation.rb new file mode 100644 index 00000000..f936cb0f --- /dev/null +++ b/scenario/regressions/splat-overload-oscillation.rb @@ -0,0 +1,21 @@ +## update: test.rbs +class Foo + def self.f: (*Integer) -> String | (*String) -> Symbol +end + +## update: test.rb +# Splat arguments with rest-positional overloads used to cause +# oscillation. The overload fix skips resolution when any splat +# element vertex has no type information, preventing the cycle. +def check + @args = [42] + @x = Foo.f(*@args) + @args = [@x] +end + +## assert +class Object + def check: -> [untyped] +end + +## diagnostics