Skip to content

Commit 7acacfc

Browse files
authored
inference: add missing limit-cycle poisoning (#30098)
Seems to have been missing since the original implementation of IPO constant-prop. It's impressive how long we got away with just poisoning the cache with `Any` if there was a self-recursive call, but it requires a fairly convoluted setup to trigger this case. Fix #29923
1 parent 58c0ed5 commit 7acacfc

File tree

4 files changed

+43
-22
lines changed

4 files changed

+43
-22
lines changed

base/compiler/abstractinterpretation.jl

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -310,25 +310,7 @@ function abstract_call_method(method::Method, @nospecialize(sig), sparams::Simpl
310310
# (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases)
311311
return Any, false, nothing
312312
end
313-
infstate = sv
314-
topmost = topmost::InferenceState
315-
while !(infstate === topmost.parent)
316-
if call_result_unused(infstate)
317-
# If we won't propagate the result any further (since it's typically unused),
318-
# it's OK that we keep and cache the "limited" result in the parents
319-
# (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases)
320-
# TODO: we might be able to halt progress much more strongly here,
321-
# since now we know we won't be able to keep anything much that we learned.
322-
# We were mainly only here to compute the calling convention return type,
323-
# but in most situations now, we are unlikely to be able to use that information.
324-
break
325-
end
326-
infstate.limited = true
327-
for infstate_cycle in infstate.callers_in_cycle
328-
infstate_cycle.limited = true
329-
end
330-
infstate = infstate.parent
331-
end
313+
poison_callstack(sv, topmost::InferenceState, true)
332314
sig = newsig
333315
sparams = svec()
334316
end

base/compiler/inferencestate.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,28 @@ function add_mt_backedge!(mt::Core.MethodTable, @nospecialize(typ), caller::Infe
226226
nothing
227227
end
228228

229+
function poison_callstack(infstate::InferenceState, topmost::InferenceState, poison_topmost::Bool)
230+
poison_topmost && (topmost = topmost.parent)
231+
while !(infstate === topmost)
232+
if call_result_unused(infstate)
233+
# If we won't propagate the result any further (since it's typically unused),
234+
# it's OK that we keep and cache the "limited" result in the parents
235+
# (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases)
236+
# TODO: we might be able to halt progress much more strongly here,
237+
# since now we know we won't be able to keep anything much that we learned.
238+
# We were mainly only here to compute the calling convention return type,
239+
# but in most situations now, we are unlikely to be able to use that information.
240+
break
241+
end
242+
infstate.limited = true
243+
for infstate_cycle in infstate.callers_in_cycle
244+
infstate_cycle.limited = true
245+
end
246+
infstate = infstate.parent
247+
infstate === nothing && return
248+
end
249+
end
250+
229251
function is_specializable_vararg_slot(@nospecialize(arg), nargs::Int, vargs::Vector{Any})
230252
return (isa(arg, Slot) && slot_id(arg) == nargs && !isempty(vargs))
231253
end

base/compiler/typeinfer.jl

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ function typeinf(result::InferenceResult, cached::Bool, params::Params)
1111
end
1212

1313
function typeinf(frame::InferenceState)
14-
cached = frame.cached
1514
typeinf_nocycle(frame) || return false # frame is now part of a higher cycle
1615
# with no active ip's, frame is done
1716
frames = frame.callers_in_cycle
@@ -28,6 +27,7 @@ function typeinf(frame::InferenceState)
2827
# empty!(frames)
2928
min_valid = frame.min_valid
3029
max_valid = frame.max_valid
30+
cached = frame.cached
3131
if cached || frame.parent !== nothing
3232
for caller in results
3333
opt = caller.src
@@ -442,13 +442,22 @@ function resolve_call_cycle!(linfo::MethodInstance, parent::InferenceState)
442442
uncached |= !frame.cached # ensure we never add an uncached frame to a cycle
443443
limited |= frame.limited
444444
if frame.linfo === linfo
445-
uncached && return true
445+
if uncached
446+
# our attempt to speculate into a constant call lead to an undesired self-cycle
447+
# that cannot be converged: poison our call-stack (up to the discovered duplicate frame)
448+
# with the limited flag and abort (set return type to Any) now
449+
poison_callstack(parent, frame, false)
450+
return true
451+
end
446452
merge_call_chain!(parent, frame, frame, limited)
447453
return frame
448454
end
449455
for caller in frame.callers_in_cycle
450456
if caller.linfo === linfo
451-
uncached && return true
457+
if uncached
458+
poison_callstack(parent, frame, false)
459+
return true
460+
end
452461
merge_call_chain!(parent, frame, caller, limited)
453462
return caller
454463
end

test/compiler/compiler.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2134,3 +2134,11 @@ end
21342134
# Test that inference can infer .instance of types
21352135
f_instance(::Type{T}) where {T} = T.instance
21362136
@test @inferred(f_instance(Nothing)) === nothing
2137+
2138+
# test for some limit-cycle caching poisoning
2139+
_false30098 = false
2140+
f30098() = _false30098 ? g30098() : 3
2141+
g30098() = (h30098(:f30098); 4)
2142+
h30098(f) = getfield(@__MODULE__, f)()
2143+
@test @inferred(g30098()) == 4 # make sure that this
2144+
@test @inferred(f30098()) == 3 # doesn't pollute the inference cache of this

0 commit comments

Comments
 (0)