From 99d782356b01230bda07c80f72ceae6162b851d9 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Mon, 24 Aug 2026 16:58:27 +0900 Subject: [PATCH 1/5] Support the omission of `class Module`'s superclass RBS 4.1 changed `class Module < Object` to `class Module`, which makes Module's superclass known before Object's while loading the core RBS, so `each_superclass` cycled between the two while Object's was still nil. Co-Authored-By: Claude Opus 5 (1M context) --- lib/typeprof/core/env.rb | 3 +++ test/core/env_test.rb | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 test/core/env_test.rb diff --git a/lib/typeprof/core/env.rb b/lib/typeprof/core/env.rb index 314d3ee6..5aff729c 100644 --- a/lib/typeprof/core/env.rb +++ b/lib/typeprof/core/env.rb @@ -106,6 +106,9 @@ def get_superclass(singleton, mod) else return nil end + elsif mod == @mod_object + # Unresolved while loading the core RBS; the Module fallback below would cycle + return [singleton, @mod_basic_object] elsif mod == @mod_module && !singleton return nil else diff --git a/test/core/env_test.rb b/test/core/env_test.rb new file mode 100644 index 00000000..9d1919d6 --- /dev/null +++ b/test/core/env_test.rb @@ -0,0 +1,31 @@ +require_relative "../helper" +require "timeout" + +module TypeProf::Core + class EnvTest < Test::Unit::TestCase + def test_get_superclass_of_object_without_declaration + genv = GlobalEnv.new + + # `class Object < BasicObject` is not loaded yet, so superclass is still nil + assert_equal( + [false, genv.resolve_cpath([:BasicObject])], + genv.get_superclass(false, genv.mod_object), + ) + end + + def test_each_superclass_of_object_without_declaration + genv = GlobalEnv.new + + # Emulate the middle of loading the core RBS, where Module already knows + # its superclass but Object does not yet + genv.resolve_cpath([:Module]).instance_variable_set(:@superclass, genv.mod_object) + + chain = [] + Timeout.timeout(10) do + genv.each_superclass(genv.mod_object, false) {|mod, singleton| chain << [mod.cpath, singleton] } + end + + assert_equal([[[], false], [[:BasicObject], false]], chain) + end + end +end From d2e48ad3895d6e389bb46d2cd1781579ffb9f678 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Mon, 24 Aug 2026 16:58:27 +0900 Subject: [PATCH 2/5] Take the shim's type parameter names from the core RBS The shim hardcoded `class Array[Elem]`, but a module entity shares one set of parameter names across its declarations, so RBS 4.1 renaming `Array`'s to `E` made every use raise "unknown type variable: Elem". Look them up. Co-Authored-By: Claude Opus 5 (1M context) --- lib/typeprof/core/env.rb | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/typeprof/core/env.rb b/lib/typeprof/core/env.rb index 5aff729c..22a529ce 100644 --- a/lib/typeprof/core/env.rb +++ b/lib/typeprof/core/env.rb @@ -274,6 +274,17 @@ def resolve_type_alias(cpath, name) mod.get_type_alias(name) end + # Returns the type parameter names of the first generic declaration of cpath + def find_type_params(decls, cpath) + decls.each do |decl| + next unless decl.cpath == cpath + next unless decl.respond_to?(:params) + params = decl.params + return params if params && !params.empty? + end + nil + end + def load_core_rbs(raw_decls, position_encoding) file_context = FileContext.new(nil, position_encoding) lenv = LocalEnv.new(file_context, CRef::Toplevel, {}, []) @@ -281,6 +292,11 @@ def load_core_rbs(raw_decls, position_encoding) AST.create_rbs_decl(raw_decl, lenv) end.compact + # A module entity has one set of parameter names shared by all its declarations, + # so the shim must reuse the core's ones (Array's was `Elem`, and is `E` since RBS 4.1) + ary_elem, = find_type_params(decls, [:Array]) || [:Elem] + hash_key, hash_val = find_type_params(decls, [:Hash]) || [:K, :V] + decls += AST.parse_rbs("typeprof-rbs-shim.rbs", <<-RBS, position_encoding) class Exception include _Exception @@ -289,12 +305,12 @@ class String include _ToS include _ToStr end - class Array[Elem] - include _ToAry[Elem] - include _Each[Elem] + class Array[#{ ary_elem }] + include _ToAry[#{ ary_elem }] + include _Each[#{ ary_elem }] end - class Hash[K, V] - include _Each[[K, V]] + class Hash[#{ hash_key }, #{ hash_val }] + include _Each[[#{ hash_key }, #{ hash_val }]] end class Object include Hash::_Key From df6b1088b2a3851aa3f0f89832926c7b2ea5e155 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Mon, 24 Aug 2026 16:58:27 +0900 Subject: [PATCH 3/5] Infer type variables through a generic type alias `SigTyAliasNode#typecheck` bound the alias parameter to a fresh vertex, so what the alias body inferred never reached the method's type variable, and RBS 4.1's `Array#+: [U] (array[U]) -> Array[E | U]` gave `Array[untyped]`. Co-Authored-By: Claude Opus 5 (1M context) --- lib/typeprof/core/ast/sig_type.rb | 8 +++++++- scenario/rbs/type-alias-generic.rb | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 scenario/rbs/type-alias-generic.rb diff --git a/lib/typeprof/core/ast/sig_type.rb b/lib/typeprof/core/ast/sig_type.rb index 183b66c9..dae7a7a5 100644 --- a/lib/typeprof/core/ast/sig_type.rb +++ b/lib/typeprof/core/ast/sig_type.rb @@ -465,7 +465,13 @@ def typecheck(genv, changes, vtx, subst) decl = tae.decls.each {|decl| break decl } subst0 = subst.dup decl.params.zip(@args) do |param, arg| - subst0[param] = arg.covariant_vertex(genv, changes, subst0) + if arg.is_a?(SigTyVarNode) && subst[arg.var] + # Share the vertex so that the types inferred in the alias body flow + # back into the variable, which makes `U` of `(array[U])` inferrable + subst0[param] = subst[arg.var] + else + subst0[param] = arg.covariant_vertex(genv, changes, subst0) + end end tae.type.typecheck(genv, changes, vtx, subst0) end diff --git a/scenario/rbs/type-alias-generic.rb b/scenario/rbs/type-alias-generic.rb new file mode 100644 index 00000000..5e9ffd9b --- /dev/null +++ b/scenario/rbs/type-alias-generic.rb @@ -0,0 +1,16 @@ +## update: test.rbs +type list[T] = Array[T] | _ToAry[T] + +class Object + def take_list: [U] (list[U]) -> Array[U] +end + +## update: test.rb +def test + take_list([1]) +end + +## assert +class Object + def test: -> Array[Integer] +end From 0d3f3845244619199c000969d2d21d666c403ada Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Mon, 24 Aug 2026 16:58:27 +0900 Subject: [PATCH 4/5] Use a rest-positional overload only as a fallback `resolve_overloads` unions every matching overload, so RBS 4.1 adding a `(*_Each[U])` catch-all to `Array#zip` widened `[1].zip(["s"]) {|x, y| x }` to `Integer | String`. Prefer the fixed-arity overloads unless the call is splatted, which cannot match one anyway. Co-Authored-By: Claude Opus 5 (1M context) --- lib/typeprof/core/graph/box.rb | 20 +++++++++++++++++--- scenario/method/overload-rest-fallback.rb | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 scenario/method/overload-rest-fallback.rb diff --git a/lib/typeprof/core/graph/box.rb b/lib/typeprof/core/graph/box.rb index c0f6268d..f12847c4 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -146,6 +146,12 @@ def overloads_differ_at_top_level? } end + # lazy cache: overloads split into fixed-arity ones and rest-positional ones + def partition_by_rest_positionals + @partition_by_rest_positionals ||= + @method_types.partition {|method_type| !method_type.rest_positionals } + end + private # Check if two method types have structurally identical positional @@ -480,11 +486,19 @@ def resolve_overloads(changes, genv, node, param_map, a_args, ret, &blk) return end + # A splatted call can match only a rest-positional overload; otherwise prefer + # the fixed-arity ones, as a rest-positional one is usually a catch-all + overload_groups = + a_args.splat_flags.any? ? [@method_types] : @method_types.partition_by_rest_positionals + match_any_overload = false - @method_types.each do |method_type| - if resolve_overload(changes, genv, method_type, node, param_map, a_args, ret, false, &blk) - match_any_overload = true + overload_groups.each do |method_types| + method_types.each do |method_type| + if resolve_overload(changes, genv, method_type, node, param_map, a_args, ret, false, &blk) + match_any_overload = true + end end + break if match_any_overload end unless match_any_overload meth = node.mid_code_range ? :mid_code_range : :code_range diff --git a/scenario/method/overload-rest-fallback.rb b/scenario/method/overload-rest-fallback.rb new file mode 100644 index 00000000..acfbc5ca --- /dev/null +++ b/scenario/method/overload-rest-fallback.rb @@ -0,0 +1,21 @@ +## update: test.rbs +class Object + def pick: (Integer) -> Integer + | (*Integer) -> String +end + +## update: test.rb +def test_fixed + pick(1) +end + +def test_splat + ary = [1, 2] + pick(*ary) +end + +## assert +class Object + def test_fixed: -> Integer + def test_splat: -> String +end From a6c56e46d5bd1f7c6c4b1da4a5bf8bb5a4537c3c Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Mon, 24 Aug 2026 16:58:27 +0900 Subject: [PATCH 5/5] Depend on the released rbs gem and update to RBS 4.2 The `ENV["RBS_VERSION"]` conditional was commented out, so Gemfile.lock pinned a ruby/rbs git revision and CI never saw a released RBS. Restore it. `BUNDLED WITH` moves to 4.0.16, as Bundler 2.6.9 cannot install this Gemfile on ruby-head. Co-Authored-By: Claude Opus 5 (1M context) --- Gemfile | 9 ++++----- Gemfile.lock | 16 +++++----------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/Gemfile b/Gemfile index 4f4b0248..beb95f4a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,11 @@ source "https://rubygems.org" + +# Specify your gem's dependencies in typeprof.gemspec gemspec -#if ENV["RBS_VERSION"] +if ENV["RBS_VERSION"] gem "rbs", github: "ruby/rbs", ref: ENV["RBS_VERSION"] -#else -# # Specify your gem's dependencies in typeprof.gemspec -# gemspec -#end +end group :development do gem "rake" diff --git a/Gemfile.lock b/Gemfile.lock index c8f989ab..e1ccf10b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,3 @@ -GIT - remote: https://github.com/ruby/rbs.git - revision: 927ca592755d74bf6267b3c3183872e40f08be09 - specs: - rbs (4.0.0.dev.5) - logger - prism (>= 1.6.0) - tsort - PATH remote: . specs: @@ -23,6 +14,10 @@ GEM power_assert (3.0.1) prism (1.9.0) rake (13.3.1) + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -41,7 +36,6 @@ PLATFORMS DEPENDENCIES coverage-helpers rake - rbs! simplecov simplecov-html stackprof @@ -49,4 +43,4 @@ DEPENDENCIES typeprof! BUNDLED WITH - 2.6.9 + 4.0.16