From 3bd90b77832813248ff3c67b833c084bb05e585f Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Mon, 28 Sep 2026 19:13:35 +0900 Subject: [PATCH 1/2] Share one vertex for omitted type arguments When a type argument was not given (e.g., `class Sub < Gen` in Ruby for `Gen[T]` in RBS), get_instance_type and similar places created a fresh `Source.new` for it on every call. A box that typechecks against such a type adds an edge from that vertex to itself, so each run produced a different edge; ChangeSet#reinstall then added the new edge and removed the old one, both of which re-queue the box, and the analysis never terminated: class Sub < Gen; end accept(Sub.create) # accept: (Gen[Integer]) -> String Use a single empty Source, GlobalEnv#untyped_arg, instead. An empty Source neither holds nor propagates types, so sharing it among type variables does not leak types between unrelated places, and the edges and Instance types built from it stay the same across runs. Co-Authored-By: Claude Opus 5.5 --- lib/typeprof/core/ast/sig_type.rb | 8 +++---- lib/typeprof/core/env.rb | 17 +++++++++----- lib/typeprof/core/graph/box.rb | 10 ++++---- lib/typeprof/core/type.rb | 2 +- .../rbs/generic-superclass-without-args.rb | 23 +++++++++++++++++++ 5 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 scenario/rbs/generic-superclass-without-args.rb diff --git a/lib/typeprof/core/ast/sig_type.rb b/lib/typeprof/core/ast/sig_type.rb index c3b90632..36936b7f 100644 --- a/lib/typeprof/core/ast/sig_type.rb +++ b/lib/typeprof/core/ast/sig_type.rb @@ -39,7 +39,7 @@ def self.typecheck_for_prepended_modules(genv, changes, a_ty, f_mod, f_args, sub if prep_decl.is_a?(AST::SigPrependNode) && prep_mod.type_params prep_ty = genv.get_instance_type(prep_mod, prep_decl.args, changes, {}, a_ty) else - type_params = prep_mod.type_params.map {|(_name, _default_ty)| Source.new() } # TODO: better support + type_params = prep_mod.type_params.map {|(_name, _default_ty)| genv.untyped_arg } # TODO: better support prep_ty = Type::Instance.new(genv, prep_mod, type_params) end if prep_ty.mod == f_mod @@ -64,7 +64,7 @@ def self.typecheck_for_included_modules(genv, changes, a_ty, f_mod, f_args, subs if inc_decl.is_a?(AST::SigIncludeNode) && inc_mod.type_params inc_ty = genv.get_instance_type(inc_mod, inc_decl.args, changes, {}, a_ty) else - type_params = inc_mod.type_params.map {|(_name, _default_ty)| Source.new() } # TODO: better support + type_params = inc_mod.type_params.map {|(_name, _default_ty)| genv.untyped_arg } # TODO: better support inc_ty = Type::Instance.new(genv, inc_mod, type_params) end if inc_ty.mod == f_mod @@ -89,7 +89,7 @@ def self.typecheck_for_extended_modules(genv, changes, a_ty, f_mod, f_args, subs if ext_decl.is_a?(AST::SigExtendNode) && ext_mod.type_params ext_ty = genv.get_instance_type(ext_mod, ext_decl.args, changes, {}, a_ty) else - type_params = ext_mod.type_params.map {|(_name, _default_ty)| Source.new() } # TODO: better support + type_params = ext_mod.type_params.map {|(_name, _default_ty)| genv.untyped_arg } # TODO: better support ext_ty = Type::Instance.new(genv, ext_mod, type_params) end if ext_ty.mod == f_mod @@ -716,7 +716,7 @@ def contravariant_vertex0(genv, changes, vtx, subst) # TODO: report error for wrong type arguments # TODO: support default type args args = mod.type_params.zip(@args).map do |_, arg| - arg ? arg.contravariant_vertex(genv, changes, subst) : Source.new + arg ? arg.contravariant_vertex(genv, changes, subst) : genv.untyped_arg end changes.add_edge(genv, Source.new(Type::Instance.new(genv, mod, args)), vtx) end diff --git a/lib/typeprof/core/env.rb b/lib/typeprof/core/env.rb index d1b8ce83..68c31038 100644 --- a/lib/typeprof/core/env.rb +++ b/lib/typeprof/core/env.rb @@ -42,6 +42,11 @@ def initialize @bot_type = Type::Bot.new(self) + # The vertex for a type argument that is not given. This must be shared: + # a fresh vertex makes a different Instance type and a different edge on + # every run, so a box that uses it is re-run forever. + @untyped_arg = Source.new + @run_count = 0 end @@ -52,7 +57,7 @@ def initialize attr_reader :obj_type, :nil_type, :true_type, :false_type, :str_type attr_reader :int_type, :float_type, :rational_type, :complex_type attr_reader :proc_type, :symbol_type, :method_type, :set_type, :regexp_type - attr_reader :bot_type + attr_reader :bot_type, :untyped_arg def gen_ary_type(elem_vtx) Type::Instance.new(self, @mod_ary, [elem_vtx]) @@ -130,18 +135,18 @@ def get_instance_type(mod, type_args, changes, base_ty_env, base_ty) ty_env = base_ty_env.dup if base_ty.is_a?(Type::Instance) base_ty.mod.type_params.zip(base_ty.args) do |(param, default_ty), arg| - ty_env[param] = arg || (default_ty ? default_ty.covariant_vertex(self, changes, ty_env) : Source.new) + ty_env[param] = arg || (default_ty ? default_ty.covariant_vertex(self, changes, ty_env) : @untyped_arg) end elsif base_ty.is_a?(Type::Singleton) base_ty.mod.type_params&.each do |(param, default_ty)| - ty_env[param] = default_ty ? default_ty.covariant_vertex(self, changes, ty_env) : Source.new + ty_env[param] = default_ty ? default_ty.covariant_vertex(self, changes, ty_env) : @untyped_arg end end args = mod.type_params.zip(type_args).map do |(param, default_ty), arg| if changes - (arg || default_ty)&.covariant_vertex(self, changes, ty_env) || Source.new + (arg || default_ty)&.covariant_vertex(self, changes, ty_env) || @untyped_arg else - Source.new + @untyped_arg end end Type::Instance.new(self, mod, args) @@ -463,7 +468,7 @@ def get_self(genv) case @scope_level when :instance mod = genv.resolve_cpath(@cpath || []) - type_params = mod.type_params.map {|(_name, _default_ty)| Source.new() } # TODO: better support + type_params = mod.type_params.map {|(_name, _default_ty)| genv.untyped_arg } # TODO: better support ty = Type::Instance.new(genv, mod, type_params) Source.new(ty) when :class diff --git a/lib/typeprof/core/graph/box.rb b/lib/typeprof/core/graph/box.rb index 9883abc7..51ab2749 100644 --- a/lib/typeprof/core/graph/box.rb +++ b/lib/typeprof/core/graph/box.rb @@ -779,7 +779,7 @@ def run0(genv, changes) ty = Type::Singleton.new(genv, mod) param_map0 = Type.default_param_map(genv, ty) else - type_params = mod.type_params.map {|(_name, _default_ty)| Source.new() } # TODO: better support + type_params = mod.type_params.map {|(_name, _default_ty)| genv.untyped_arg } # TODO: better support ty = Type::Instance.new(genv, mod, type_params) param_map0 = Type.default_param_map(genv, ty) if ty.is_a?(Type::Instance) @@ -984,7 +984,7 @@ def run0(genv, changes) ty_env = Type.default_param_map(genv, orig_ty) if ty.is_a?(Type::Instance) ty.mod.type_params.zip(ty.args) do |(param, default_ty), arg| - ty_env[param] = arg || (default_ty ? default_ty.covariant_vertex(genv, changes, ty_env) : Source.new) + ty_env[param] = arg || (default_ty ? default_ty.covariant_vertex(genv, changes, ty_env) : genv.untyped_arg) end end mdecl.resolve_overloads(changes, genv, @node, ty_env, a_args, @ret) do |method_type| @@ -1095,7 +1095,7 @@ def resolve_prepended_modules(genv, changes, base_ty_env, ty, mid, &blk) if prep_decl.is_a?(AST::SigPrependNode) && prep_mod.type_params prep_ty = genv.get_instance_type(prep_mod, prep_decl.args, changes, base_ty_env, ty) else - type_params = prep_mod.type_params.map { Source.new() } # TODO: better support + type_params = prep_mod.type_params.map { genv.untyped_arg } # TODO: better support prep_ty = Type::Instance.new(genv, prep_mod, type_params) end @@ -1150,7 +1150,7 @@ def resolve_included_modules(genv, changes, base_ty_env, ty, mid, &blk) if inc_decl.is_a?(AST::SigIncludeNode) && inc_mod.type_params inc_ty = genv.get_instance_type(inc_mod, inc_decl.args, changes, base_ty_env, ty) else - type_params = inc_mod.type_params.map { Source.new() } # TODO: better support + type_params = inc_mod.type_params.map { genv.untyped_arg } # TODO: better support inc_ty = Type::Instance.new(genv, inc_mod, type_params) end @@ -1181,7 +1181,7 @@ def resolve_extended_modules(genv, changes, base_ty_env, ty, mid, &blk) if ext_decl.is_a?(AST::SigExtendNode) && ext_mod.type_params ext_ty = genv.get_instance_type(ext_mod, ext_decl.args, changes, base_ty_env, ty) else - type_params = ext_mod.type_params.map { Source.new() } # TODO: better support + type_params = ext_mod.type_params.map { genv.untyped_arg } # TODO: better support ext_ty = Type::Instance.new(genv, ext_mod, type_params) end diff --git a/lib/typeprof/core/type.rb b/lib/typeprof/core/type.rb index 7ddf8634..512fe38e 100644 --- a/lib/typeprof/core/type.rb +++ b/lib/typeprof/core/type.rb @@ -77,7 +77,7 @@ def show def get_instance_type(genv) params = @mod.type_params - Instance.new(genv, @mod, params ? params.map { Source.new } : []) # TODO: respect param_default_types + Instance.new(genv, @mod, params ? params.map { genv.untyped_arg } : []) # TODO: respect param_default_types end end diff --git a/scenario/rbs/generic-superclass-without-args.rb b/scenario/rbs/generic-superclass-without-args.rb new file mode 100644 index 00000000..19314f9c --- /dev/null +++ b/scenario/rbs/generic-superclass-without-args.rb @@ -0,0 +1,23 @@ +## update: test.rbs +class Gen[T] + def self.create: () -> instance +end + +class Object + def accept: (Gen[Integer]) -> String +end + +## update: test.rb +class Sub < Gen +end + +def test + accept(Sub.create) +end + +## assert +class Sub < Gen +end +class Object + def test: -> String +end From db9215fa37861f0c4d6d043e45b15ef5fcc6f459 Mon Sep 17 00:00:00 2001 From: Yusuke Endoh Date: Mon, 28 Sep 2026 19:13:35 +0900 Subject: [PATCH 2/2] Fill in omitted type arguments of RBS types A generic class or interface written without type arguments in RBS (e.g., `def gen: () -> Gen` for `Gen[T]`) made an Instance type with no arguments, and matching it against `Gen[Integer]` passed nil as the actual argument, which crashed with "undefined method 'each_type' for nil". Fill in the missing arguments with their default types or untyped. Co-Authored-By: Claude Opus 5.5 --- lib/typeprof/core/ast/sig_type.rb | 9 +++++++ scenario/rbs/omitted-type-args.rb | 40 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 scenario/rbs/omitted-type-args.rb diff --git a/lib/typeprof/core/ast/sig_type.rb b/lib/typeprof/core/ast/sig_type.rb index 36936b7f..57bbf76a 100644 --- a/lib/typeprof/core/ast/sig_type.rb +++ b/lib/typeprof/core/ast/sig_type.rb @@ -705,6 +705,10 @@ def covariant_vertex0(genv, changes, vtx, subst) return unless cpath mod = genv.resolve_cpath(cpath) args = @args.map {|arg| arg.covariant_vertex(genv, changes, subst) } + # Omitted type arguments (e.g., `Foo` for `Foo[T]`) default to untyped + mod.type_params.drop(args.size).each do |_, default_ty| + args << (default_ty ? default_ty.covariant_vertex(genv, changes, subst) : genv.untyped_arg) + end changes.add_edge(genv, Source.new(Type::Instance.new(genv, mod, args)), vtx) end @@ -1095,6 +1099,10 @@ def covariant_vertex0(genv, changes, vtx, subst) return unless cpath mod = genv.resolve_cpath(cpath) args = @args.map {|arg| arg.covariant_vertex(genv, changes, subst) } + # Omitted type arguments (e.g., `Foo` for `Foo[T]`) default to untyped + mod.type_params.drop(args.size).each do |_, default_ty| + args << (default_ty ? default_ty.covariant_vertex(genv, changes, subst) : genv.untyped_arg) + end changes.add_edge(genv, Source.new(Type::Instance.new(genv, mod, args)), vtx) end @@ -1104,6 +1112,7 @@ def contravariant_vertex0(genv, changes, vtx, subst) return unless cpath mod = genv.resolve_cpath(cpath) args = @args.map {|arg| arg.contravariant_vertex(genv, changes, subst) } + mod.type_params.drop(args.size).each { args << genv.untyped_arg } changes.add_edge(genv, Source.new(Type::Instance.new(genv, mod, args)), vtx) end diff --git a/scenario/rbs/omitted-type-args.rb b/scenario/rbs/omitted-type-args.rb new file mode 100644 index 00000000..bcd278e9 --- /dev/null +++ b/scenario/rbs/omitted-type-args.rb @@ -0,0 +1,40 @@ +## update: test.rbs +class Gen[T] +end + +class D[T, U = String] + def u: () -> U +end + +interface _I[T] + def get: () -> T +end + +class Object + def gen: () -> Gen + def d: () -> D + def i: () -> _I + def accept_gen: (Gen[Integer]) -> String + def accept_d: (D[Integer, String]) -> String + def accept_i: (_I[Integer]) -> String +end + +## update: test.rb +def test_gen = accept_gen(gen) +def test_d = accept_d(d) +def test_i = accept_i(i) +def raw_gen = gen +def raw_d = d +def raw_d_u = d.u +def raw_i = i + +## assert +class Object + def test_gen: -> String + def test_d: -> String + def test_i: -> String + def raw_gen: -> Gen[untyped] + def raw_d: -> D[untyped, String] + def raw_d_u: -> String + def raw_i: -> _I[untyped] +end