diff --git a/lib/typeprof/core/ast/sig_type.rb b/lib/typeprof/core/ast/sig_type.rb index 263abf1aa..ad3e3a415 100644 --- a/lib/typeprof/core/ast/sig_type.rb +++ b/lib/typeprof/core/ast/sig_type.rb @@ -258,9 +258,24 @@ def covariant_vertex0(genv, changes, vtx, subst) changes.add_depended_static_read(@static_ret.last) tae = @static_ret.last.type_alias_entity if tae && tae.exist? + # Check for recursive expansion + expansion_key = [@cpath, @name] + subst[:__expansion_stack__] ||= [] + + if subst[:__expansion_stack__].include?(expansion_key) + # Recursive expansion detected: this type alias references itself + # Stop expansion here to prevent SystemStackError. The type system + # will handle the incomplete expansion gracefully, typically by + # treating unresolved recursive references as 'untyped', which + # maintains type safety while allowing the program to continue. + return + end + # need to check tae decls are all consistent? decl = tae.decls.each {|decl| break decl } subst0 = subst.dup + subst0[:__expansion_stack__] = subst[:__expansion_stack__].dup + [expansion_key] + # raise if decl.params.size != @args.size # ? decl.params.zip(@args) do |param, arg| subst0[param] = arg.covariant_vertex(genv, changes, subst0) # passing subst0 is ok? @@ -273,9 +288,24 @@ def contravariant_vertex0(genv, changes, vtx, subst) changes.add_depended_static_read(@static_ret.last) tae = @static_ret.last.type_alias_entity if tae && tae.exist? + # Check for recursive expansion + expansion_key = [@cpath, @name] + subst[:__expansion_stack__] ||= [] + + if subst[:__expansion_stack__].include?(expansion_key) + # Recursive expansion detected: this type alias references itself + # Stop expansion here to prevent SystemStackError. The type system + # will handle the incomplete expansion gracefully, typically by + # treating unresolved recursive references as 'untyped', which + # maintains type safety while allowing the program to continue. + return + end + # need to check tae decls are all consistent? decl = tae.decls.each {|decl| break decl } subst0 = subst.dup + subst0[:__expansion_stack__] = subst[:__expansion_stack__].dup + [expansion_key] + # raise if decl.params.size != @args.size # ? decl.params.zip(@args) do |param, arg| subst0[param] = arg.contravariant_vertex(genv, changes, subst0) diff --git a/scenario/rbs/recursive-type-alias.rb b/scenario/rbs/recursive-type-alias.rb new file mode 100644 index 000000000..4f64e407a --- /dev/null +++ b/scenario/rbs/recursive-type-alias.rb @@ -0,0 +1,79 @@ +## update: test.rbs +# Basic recursive type alias +type context = nil | [context, bool] + +class C + def foo: -> context +end + +## update: test.rb +def test1 + C.new.foo +end + +## assert: test.rb +class Object + def test1: -> [untyped, bool]? +end + +## diagnostics: test.rb + +## update: test.rbs +# Recursive type alias in class scope +class D + type tree = Integer | [tree, tree] + def get_tree: -> tree +end + +## update: test.rb +def test2 + D.new.get_tree +end + +## assert: test.rb +class Object + def test2: -> (Integer | [untyped, untyped]) +end + +## diagnostics: test.rb + +## update: test.rbs +# Mutually recursive type aliases +type node = [Integer, nodes] +type nodes = Array[node] + +class E + def build_tree: -> node +end + +## update: test.rb +def test3 + E.new.build_tree +end + +## assert: test.rb +class Object + def test3: -> [Integer, Array[untyped]] +end + +## diagnostics: test.rb + +## update: test.rbs +# Recursive type alias with generic parameters +type list[T] = nil | [T, list[T]] + +class F + def create_list: -> list[String] +end + +## update: test.rb +def test4 + F.new.create_list +end + +## assert: test.rb +class Object + def test4: -> [String, untyped]? +end + +## diagnostics: test.rb