Skip to content

Commit e66354c

Browse files
sinsokuclaude
andcommitted
Fix stack overflow error in recursive type alias expansion
This commit fixes issue #324 where recursive type aliases caused SystemStackError due to infinite recursion during type expansion. The fix adds recursion detection in SigTyAliasNode#covariant_vertex0 and #contravariant_vertex0 methods by tracking the expansion stack using subst[:__expansion_stack__]. When a type alias is already being expanded, the expansion stops to prevent infinite recursion. Also adds comprehensive test cases for various recursive type alias patterns including direct recursion, mutual recursion, and generic recursive types. Fixes #324 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 82cf54b commit e66354c

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

lib/typeprof/core/ast/sig_type.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,24 @@ def covariant_vertex0(genv, changes, vtx, subst)
258258
changes.add_depended_static_read(@static_ret.last)
259259
tae = @static_ret.last.type_alias_entity
260260
if tae && tae.exist?
261+
# Check for recursive expansion
262+
expansion_key = [@cpath, @name]
263+
subst[:__expansion_stack__] ||= []
264+
265+
if subst[:__expansion_stack__].include?(expansion_key)
266+
# Recursive expansion detected: this type alias references itself
267+
# Stop expansion here to prevent SystemStackError. The type system
268+
# will handle the incomplete expansion gracefully, typically by
269+
# treating unresolved recursive references as 'untyped', which
270+
# maintains type safety while allowing the program to continue.
271+
return
272+
end
273+
261274
# need to check tae decls are all consistent?
262275
decl = tae.decls.each {|decl| break decl }
263276
subst0 = subst.dup
277+
subst0[:__expansion_stack__] = subst[:__expansion_stack__].dup + [expansion_key]
278+
264279
# raise if decl.params.size != @args.size # ?
265280
decl.params.zip(@args) do |param, arg|
266281
subst0[param] = arg.covariant_vertex(genv, changes, subst0) # passing subst0 is ok?
@@ -273,9 +288,24 @@ def contravariant_vertex0(genv, changes, vtx, subst)
273288
changes.add_depended_static_read(@static_ret.last)
274289
tae = @static_ret.last.type_alias_entity
275290
if tae && tae.exist?
291+
# Check for recursive expansion
292+
expansion_key = [@cpath, @name]
293+
subst[:__expansion_stack__] ||= []
294+
295+
if subst[:__expansion_stack__].include?(expansion_key)
296+
# Recursive expansion detected: this type alias references itself
297+
# Stop expansion here to prevent SystemStackError. The type system
298+
# will handle the incomplete expansion gracefully, typically by
299+
# treating unresolved recursive references as 'untyped', which
300+
# maintains type safety while allowing the program to continue.
301+
return
302+
end
303+
276304
# need to check tae decls are all consistent?
277305
decl = tae.decls.each {|decl| break decl }
278306
subst0 = subst.dup
307+
subst0[:__expansion_stack__] = subst[:__expansion_stack__].dup + [expansion_key]
308+
279309
# raise if decl.params.size != @args.size # ?
280310
decl.params.zip(@args) do |param, arg|
281311
subst0[param] = arg.contravariant_vertex(genv, changes, subst0)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
## update: test.rbs
2+
# Basic recursive type alias
3+
type context = nil | [context, bool]
4+
5+
class C
6+
def foo: -> context
7+
end
8+
9+
## update: test.rb
10+
def test1
11+
C.new.foo
12+
end
13+
14+
## assert: test.rb
15+
class Object
16+
def test1: -> [untyped, bool]?
17+
end
18+
19+
## diagnostics: test.rb
20+
21+
## update: test.rbs
22+
# Recursive type alias in class scope
23+
class D
24+
type tree = Integer | [tree, tree]
25+
def get_tree: -> tree
26+
end
27+
28+
## update: test.rb
29+
def test2
30+
D.new.get_tree
31+
end
32+
33+
## assert: test.rb
34+
class Object
35+
def test2: -> (Integer | [untyped, untyped])
36+
end
37+
38+
## diagnostics: test.rb
39+
40+
## update: test.rbs
41+
# Mutually recursive type aliases
42+
type node = [Integer, nodes]
43+
type nodes = Array[node]
44+
45+
class E
46+
def build_tree: -> node
47+
end
48+
49+
## update: test.rb
50+
def test3
51+
E.new.build_tree
52+
end
53+
54+
## assert: test.rb
55+
class Object
56+
def test3: -> [Integer, Array[untyped]]
57+
end
58+
59+
## diagnostics: test.rb
60+
61+
## update: test.rbs
62+
# Recursive type alias with generic parameters
63+
type list[T] = nil | [T, list[T]]
64+
65+
class F
66+
def create_list: -> list[String]
67+
end
68+
69+
## update: test.rb
70+
def test4
71+
F.new.create_list
72+
end
73+
74+
## assert: test.rb
75+
class Object
76+
def test4: -> [String, untyped]?
77+
end
78+
79+
## diagnostics: test.rb

0 commit comments

Comments
 (0)