Skip to content

Commit 5945e4d

Browse files
author
Patrick Häcker
committed
fix: bound type-parameter expansion to prevent gigabyte caches
FakeTypeName expanded a type's parameters recursively with no size bound. A few types explode into enormous trees: type-domain packages whose types recurse into themselves (e.g. TypeDomainNaturalNumbers — naturals as nested types, rationals as continued fractions), and even some Base/LinearAlgebra signatures whose `where`-bounded `Union`s reach tens of thousands of nodes. This produced multi-gigabyte cache files (2.2 GB for TypeDomainNaturalNumbers) that exhausted memory — OOMing the machine — when read back into the store. Cap expansion with a per-type budget of expanded DataTypes (MAX_EXPANDED_TYPES = 128): once it is spent, a type's remaining parameters are dropped and only its name is kept. The budget bounds any shape of explosion (depth, width, or indirect recursion through variable bounds), while ordinary types stay far below it and are unaffected. Add a regression test. Ported from julia-vscode/SymbolServer.jl#320 (SymbolServer.jl is deprecated; the code now lives here under shared/symbolserver/).
1 parent 6424c73 commit 5945e4d

2 files changed

Lines changed: 54 additions & 14 deletions

File tree

shared/symbolserver/faketypes.jl

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,44 @@ struct FakeTypeName
1414
parameters::Vector{Any}
1515
end
1616

17-
function FakeTypeName(@nospecialize(x))
17+
# Type parameters are expanded recursively so cached signatures keep their
18+
# structure, but a few types expand into huge trees — type-domain packages
19+
# whose types recurse into themselves (naturals as nested `NonnegativeInteger`,
20+
# rationals as continued fractions), and even some Base/LinearAlgebra
21+
# signatures whose `where`-bounded `Union`s reach tens of thousands of nodes —
22+
# which unbounded would produce multi-gigabyte caches that exhaust memory on
23+
# read. To prevent that, `budget` caps how many `DataType`s are expanded while
24+
# building one type: once it is spent the remaining parameters are dropped,
25+
# keeping only their names. The limit sits far above any ordinary type, so only
26+
# these outliers are truncated; the serializer's own MAX_DEPTH guard is a
27+
# separate cycle check, not a size bound.
28+
const MAX_EXPANDED_TYPES = 128
29+
30+
mutable struct ExpandBudget
31+
remaining::Int
32+
end
33+
ExpandBudget() = ExpandBudget(MAX_EXPANDED_TYPES)
34+
35+
function FakeTypeName(@nospecialize(x), budget::ExpandBudget=ExpandBudget())
1836
@static if !(Vararg isa Type)
1937
x isa typeof(Vararg) && return FakeTypeofVararg(x)
2038
end
2139
if x isa DataType
2240
xname = x.name
23-
xnamename = xname.name
24-
ft = FakeTypeName(VarRef(VarRef(x.name.module), xnamename), [])
25-
for p in x.parameters
26-
push!(ft.parameters, _parameter(p))
41+
ft = FakeTypeName(VarRef(VarRef(xname.module), xname.name), [])
42+
if budget.remaining > 0
43+
budget.remaining -= 1
44+
for p in x.parameters
45+
push!(ft.parameters, _parameter(p, budget))
46+
end
2747
end
2848
ft
2949
elseif x isa Union
30-
FakeUnion(x)
50+
FakeUnion(x, budget)
3151
elseif x isa UnionAll
32-
FakeUnionAll(x)
52+
FakeUnionAll(x, budget)
3353
elseif x isa TypeVar
34-
FakeTypeVar(x)
54+
FakeTypeVar(x, budget)
3555
elseif x isa Core.TypeofBottom
3656
FakeTypeofBottom()
3757
elseif x isa Module
@@ -50,28 +70,28 @@ struct FakeUnion
5070
a
5171
b
5272
end
53-
FakeUnion(u::Union) = FakeUnion(FakeTypeName(u.a), FakeTypeName(u.b))
73+
FakeUnion(u::Union, budget::ExpandBudget=ExpandBudget()) = FakeUnion(FakeTypeName(u.a, budget), FakeTypeName(u.b, budget))
5474
struct FakeTypeVar
5575
name::Symbol
5676
lb
5777
ub
5878
end
59-
FakeTypeVar(tv::TypeVar) = FakeTypeVar(tv.name, FakeTypeName(tv.lb), FakeTypeName(tv.ub))
79+
FakeTypeVar(tv::TypeVar, budget::ExpandBudget=ExpandBudget()) = FakeTypeVar(tv.name, FakeTypeName(tv.lb, budget), FakeTypeName(tv.ub, budget))
6080
struct FakeUnionAll
6181
var::FakeTypeVar
6282
body::Any
6383
end
64-
FakeUnionAll(ua::UnionAll) = FakeUnionAll(FakeTypeVar(ua.var), FakeTypeName(ua.body))
84+
FakeUnionAll(ua::UnionAll, budget::ExpandBudget=ExpandBudget()) = FakeUnionAll(FakeTypeVar(ua.var, budget), FakeTypeName(ua.body, budget))
6585

66-
function _parameter(@nospecialize(p))
86+
function _parameter(@nospecialize(p), budget::ExpandBudget=ExpandBudget())
6787
if p isa Union{Int,Symbol,Bool,Char}
6888
p
6989
elseif !(p isa Type) && isbitstype(typeof(p))
7090
0
7191
elseif p isa Tuple
72-
_parameter.(p)
92+
map(pp -> _parameter(pp, budget), p)
7393
else
74-
FakeTypeName(p)
94+
FakeTypeName(p, budget)
7595
end
7696
end
7797

test/test_symbolserver.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,3 +779,23 @@ end
779779
@test VarRef(nothing, :B) in exts[b_foo.extends]
780780
end
781781
end
782+
783+
@testitem "SymbolServer: recursive type parameters are bounded" begin
784+
using JuliaWorkspaces.SymbolServer: FakeTypeName, ExpandBudget
785+
786+
# Number of DataTypes that were expanded (i.e. kept their parameters).
787+
expanded(x) = x isa FakeTypeName && !isempty(x.parameters) ? 1 + sum(expanded, x.parameters) : 0
788+
789+
limit = 8
790+
deep = foldl((T, _) -> Tuple{T}, 1:100; init = Int) # Tuple{Tuple{…{Int}}}
791+
wide = Tuple{ntuple(i -> Val{i}, 100)...} # Tuple{Val{1},…,Val{100}}
792+
793+
# However a type explodes — by depth or by width — expansion stops at the budget.
794+
for T in (deep, wide)
795+
@test expanded(FakeTypeName(T, ExpandBudget(limit))) <= limit
796+
end
797+
798+
# An ordinary type has far fewer nodes than the budget, so nothing is dropped.
799+
ordinary = Dict{String,Vector{Tuple{Int,Float64}}}
800+
@test FakeTypeName(ordinary, ExpandBudget(limit)) == FakeTypeName(ordinary)
801+
end

0 commit comments

Comments
 (0)