From 55c5b02df0239b10e0f3f75088a7ddbfe74de236 Mon Sep 17 00:00:00 2001 From: pvcresin Date: Mon, 17 Aug 2026 17:33:35 +0900 Subject: [PATCH] Fix splat fallback for union types --- lib/typeprof/core/ast/misc.rb | 16 +++++++++++++--- lib/typeprof/core/graph/box.rb | 16 ++++++++++------ lib/typeprof/core/graph/change_set.rb | 12 ++++++------ scenario/array/splat.rb | 13 +++++++++++++ scenario/known-issues/splat-union-fallback.rb | 12 ------------ 5 files changed, 42 insertions(+), 27 deletions(-) delete mode 100644 scenario/known-issues/splat-union-fallback.rb diff --git a/lib/typeprof/core/ast/misc.rb b/lib/typeprof/core/ast/misc.rb index 52db028d6..04a957d99 100644 --- a/lib/typeprof/core/ast/misc.rb +++ b/lib/typeprof/core/ast/misc.rb @@ -256,9 +256,19 @@ def install0(genv) vtx = @expr.install(genv) a_args = ActualArguments.new([], [], nil, nil) - to_a_vtx = @changes.add_method_call_box(genv, vtx, :to_a, a_args, false, suppress_errors: true).ret - - @changes.add_splat_box(genv, to_a_vtx, nil, vtx).ret + # Keep missing `to_a` receivers separate so Union members can fall back independently. + to_a_fallback = Vertex.new(self) + to_a_box = @changes.add_method_call_box( + genv, + vtx, + :to_a, + a_args, + false, + suppress_errors: true, + fallback: to_a_fallback, + ) + + @changes.add_splat_box(genv, to_a_box.ret, nil, to_a_fallback).ret end end diff --git a/lib/typeprof/core/graph/box.rb b/lib/typeprof/core/graph/box.rb index c0f6268db..d62f31e29 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -629,13 +629,13 @@ def wrong_return_type(f_ret_show, changes) end class SplatBox < Box - def initialize(node, genv, ary, idx, orig = nil) + def initialize(node, genv, ary, idx, fallback = nil) super(node) @ary = ary @idx = idx - @orig = orig + @fallback = fallback @ary.add_edge(genv, self) - @orig.add_edge(genv, self) if @orig + @fallback.add_edge(genv, self) if @fallback @ret = Vertex.new(node) end @@ -661,8 +661,8 @@ def run0(genv, changes) end end # For types where to_a is not defined, [*x] wraps x as [x] - if @orig && @ary.types.empty? - @orig.each_type do |ty| + if @fallback + @fallback.each_type do |ty| changes.add_edge(genv, Source.new(ty), @ret) end end @@ -1033,7 +1033,7 @@ def run0(genv, changes) end class MethodCallBox < Box - def initialize(node, genv, recv, mid, a_args, subclasses, suppress_errors: false) + def initialize(node, genv, recv, mid, a_args, subclasses, suppress_errors: false, fallback: nil) raise mid.to_s unless mid super(node) @recv = recv.new_vertex(genv, node) @@ -1045,6 +1045,7 @@ def initialize(node, genv, recv, mid, a_args, subclasses, suppress_errors: false @ret = Vertex.new(node) @subclasses = subclasses @suppress_errors = suppress_errors + @fallback = fallback @generics = {} end @@ -1059,6 +1060,9 @@ def run0(genv, changes) box = add_symbol_proc_call_box(changes, genv, orig_ty.sym, @a_args.positionals, @a_args.keywords) changes.add_edge(genv, box.ret, @ret) if box elsif !me + if @fallback + changes.add_edge(genv, Source.new(orig_ty), @fallback) + end unless @suppress_errors if error_count < 3 meth = @node.mid_code_range ? :mid_code_range : :code_range diff --git a/lib/typeprof/core/graph/change_set.rb b/lib/typeprof/core/graph/change_set.rb index 44f2b54a6..d80a81220 100644 --- a/lib/typeprof/core/graph/change_set.rb +++ b/lib/typeprof/core/graph/change_set.rb @@ -71,9 +71,9 @@ def add_edge(genv, src, dst) # TODO: if an edge is removed during one analysis, we may need to remove sub-boxes? - def add_method_call_box(genv, recv, mid, a_args, subclasses, suppress_errors: false) - key = [:mcall, recv, mid, a_args, subclasses, suppress_errors] - new_boxes[key] ||= MethodCallBox.new(@node, genv, recv, mid, a_args, subclasses, suppress_errors: suppress_errors) + def add_method_call_box(genv, recv, mid, a_args, subclasses, suppress_errors: false, fallback: nil) + key = [:mcall, recv, mid, a_args, subclasses, suppress_errors, fallback] + new_boxes[key] ||= MethodCallBox.new(@node, genv, recv, mid, a_args, subclasses, suppress_errors: suppress_errors, fallback: fallback) end def add_escape_box(genv, a_ret) @@ -81,9 +81,9 @@ def add_escape_box(genv, a_ret) new_boxes[key] ||= EscapeBox.new(@node, genv, a_ret) end - def add_splat_box(genv, arg, idx = nil, orig = nil) - key = [:splat, arg, idx, orig] - new_boxes[key] ||= SplatBox.new(@node, genv, arg, idx, orig) + def add_splat_box(genv, arg, idx = nil, fallback = nil) + key = [:splat, arg, idx, fallback] + new_boxes[key] ||= SplatBox.new(@node, genv, arg, idx, fallback) end def add_hash_splat_box(genv, arg, unified_key, unified_val) diff --git a/scenario/array/splat.rb b/scenario/array/splat.rb index 8297b4b9a..5578825f4 100644 --- a/scenario/array/splat.rb +++ b/scenario/array/splat.rb @@ -11,3 +11,16 @@ class Object def foo: -> Array[Integer] def bar: -> Array[String] end + +## update +def foo(x) + [*x] +end + +foo([:int]) +foo(:sym) + +## assert +class Object + def foo: (:sym | [:int]) -> Array[:int | :sym] +end diff --git a/scenario/known-issues/splat-union-fallback.rb b/scenario/known-issues/splat-union-fallback.rb deleted file mode 100644 index a6b6b6252..000000000 --- a/scenario/known-issues/splat-union-fallback.rb +++ /dev/null @@ -1,12 +0,0 @@ -## update -def foo(x) - [*x] -end - -foo([:int]) -foo(:sym) - -## assert -class Object - def foo: (Array[:int] | :sym) -> Array[:int | :sym] -end