Skip to content

Commit 31fbbb5

Browse files
aviateskKeno
andcommitted
inference: Relax constprop recursion detection
At the moment, we restrict const prop whenever we see a cycle in methods being called. However, I think this condition can be relaxed slightly: In particular, if the type complexity limiting did not decide to limit the growth of the type in question, I think it should be fine to constant prop as long as there is no cycle in *method instances* (rather than just methods). Fixes JuliaLang#39915, replaces JuliaLang#39918 Co-Authored-By: Keno Fisher <[email protected]>
1 parent cb22c8e commit 31fbbb5

File tree

2 files changed

+41
-13
lines changed

2 files changed

+41
-13
lines changed

base/compiler/abstractinterpretation.jl

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,12 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
137137
if splitunions
138138
splitsigs = switchtupleunion(sig)
139139
for sig_n in splitsigs
140-
rt, edgecycle, edge = abstract_call_method(interp, method, sig_n, svec(), multiple_matches, sv)
140+
rt, edgecycle, edgelimited, edge = abstract_call_method(interp, method, sig_n, svec(), multiple_matches, sv)
141141
if edge !== nothing
142142
push!(edges, edge)
143143
end
144144
this_argtypes = applicable_argtypes === nothing ? argtypes : applicable_argtypes[i]
145-
const_rt, const_result = abstract_call_method_with_const_args(interp, rt, f, this_argtypes, match, sv, edgecycle, false)
145+
const_rt, const_result = abstract_call_method_with_const_args(interp, rt, f, this_argtypes, match, sv, edgecycle, edgelimited, false)
146146
if const_rt !== rt && const_rt rt
147147
rt = const_rt
148148
end
@@ -156,14 +156,14 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f),
156156
end
157157
end
158158
else
159-
this_rt, edgecycle, edge = abstract_call_method(interp, method, sig, match.sparams, multiple_matches, sv)
159+
this_rt, edgecycle, edgelimited, edge = abstract_call_method(interp, method, sig, match.sparams, multiple_matches, sv)
160160
if edge !== nothing
161161
push!(edges, edge)
162162
end
163163
# try constant propagation with argtypes for this match
164164
# this is in preparation for inlining, or improving the return result
165165
this_argtypes = applicable_argtypes === nothing ? argtypes : applicable_argtypes[i]
166-
const_this_rt, const_result = abstract_call_method_with_const_args(interp, this_rt, f, this_argtypes, match, sv, edgecycle, false)
166+
const_this_rt, const_result = abstract_call_method_with_const_args(interp, this_rt, f, this_argtypes, match, sv, edgecycle, edgelimited, false)
167167
if const_this_rt !== this_rt && const_this_rt this_rt
168168
this_rt = const_this_rt
169169
end
@@ -315,14 +315,15 @@ const RECURSION_MSG = "Bounded recursion detected. Call was widened to force con
315315
function abstract_call_method(interp::AbstractInterpreter, method::Method, @nospecialize(sig), sparams::SimpleVector, hardlimit::Bool, sv::InferenceState)
316316
if method.name === :depwarn && isdefined(Main, :Base) && method.module === Main.Base
317317
add_remark!(interp, sv, "Refusing to infer into `depwarn`")
318-
return Any, false, nothing
318+
return Any, false, false, nothing
319319
end
320320
topmost = nothing
321321
# Limit argument type tuple growth of functions:
322322
# look through the parents list to see if there's a call to the same method
323323
# and from the same method.
324324
# Returns the topmost occurrence of that repeated edge.
325325
edgecycle = false
326+
edgelimited = false
326327
# The `method_for_inference_heuristics` will expand the given method's generator if
327328
# necessary in order to retrieve this field from the generated `CodeInfo`, if it exists.
328329
# The other `CodeInfo`s we inspect will already have this field inflated, so we just
@@ -383,7 +384,7 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp
383384
# we have a self-cycle in the call-graph, but not in the inference graph (typically):
384385
# break this edge now (before we record it) by returning early
385386
# (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases)
386-
return Any, true, nothing
387+
return Any, true, true, nothing
387388
end
388389
topmost = nothing
389390
edgecycle = true
@@ -432,14 +433,15 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp
432433
# since it's very unlikely that we'll try to inline this,
433434
# or want make an invoke edge to its calling convention return type.
434435
# (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases)
435-
return Any, true, nothing
436+
return Any, true, true, nothing
436437
end
437438
add_remark!(interp, sv, RECURSION_MSG)
438439
topmost = topmost::InferenceState
439440
parentframe = topmost.parent
440441
poison_callstack(sv, parentframe === nothing ? topmost : parentframe)
441442
sig = newsig
442443
sparams = svec()
444+
edgelimited = true
443445
end
444446
end
445447

@@ -471,14 +473,14 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp
471473

472474
rt, edge = typeinf_edge(interp, method, sig, sparams, sv)
473475
if edge === nothing
474-
edgecycle = true
476+
edgecycle = edgelimited = true
475477
end
476-
return rt, edgecycle, edge
478+
return rt, edgecycle, edgelimited, edge
477479
end
478480

479481
function abstract_call_method_with_const_args(interp::AbstractInterpreter, @nospecialize(rettype),
480482
@nospecialize(f), argtypes::Vector{Any}, match::MethodMatch,
481-
sv::InferenceState, edgecycle::Bool,
483+
sv::InferenceState, edgecycle::Bool, edgelimited::Bool,
482484
va_override::Bool)
483485
mi = maybe_get_const_prop_profitable(interp, rettype, f, argtypes, match, sv, edgecycle)
484486
mi === nothing && return Any, nothing
@@ -489,7 +491,11 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, @nosp
489491
# if there might be a cycle, check to make sure we don't end up
490492
# calling ourselves here.
491493
if edgecycle && _any(InfStackUnwind(sv)) do infstate
492-
return match.method === infstate.linfo.def && any(infstate.result.overridden_by_const)
494+
# if the type complexity limiting didn't decide to limit the call signature (`edgelimited = false`)
495+
# we can relax the cycle detection by comparing `MethodInstance`s and allow inference to
496+
# propagate different constant elements if the recursion is finite over the lattice
497+
return (edgelimited ? match.method === infstate.linfo.def : mi === infstate.linfo) &&
498+
any(infstate.result.overridden_by_const)
493499
end
494500
add_remark!(interp, sv, "[constprop] Edge cycle encountered")
495501
return Any, nothing
@@ -1231,15 +1237,15 @@ end
12311237
function abstract_call_opaque_closure(interp::AbstractInterpreter, closure::PartialOpaque, argtypes::Vector{Any}, sv::InferenceState)
12321238
pushfirst!(argtypes, closure.env)
12331239
sig = argtypes_to_type(argtypes)
1234-
rt, edgecycle, edge = abstract_call_method(interp, closure.source::Method, sig, Core.svec(), false, sv)
1240+
rt, edgecycle, edgelimited, edge = abstract_call_method(interp, closure.source::Method, sig, Core.svec(), false, sv)
12351241
edge !== nothing && add_backedge!(edge, sv)
12361242
tt = closure.typ
12371243
sigT = unwrap_unionall(tt).parameters[1]
12381244
match = MethodMatch(sig, Core.svec(), closure.source::Method, sig <: rewrap_unionall(sigT, tt))
12391245
info = OpaqueClosureCallInfo(match)
12401246
if !edgecycle
12411247
const_rettype, result = abstract_call_method_with_const_args(interp, rt, closure, argtypes,
1242-
match, sv, edgecycle, closure.isva)
1248+
match, sv, edgecycle, edgelimited, closure.isva)
12431249
if const_rettype rt
12441250
rt = const_rettype
12451251
end

test/compiler/inference.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3270,3 +3270,25 @@ end == [Union{Some{Float64}, Some{Int}, Some{UInt8}}]
32703270
true
32713271
end
32723272
end
3273+
3274+
# Make sure that const prop doesn't fall into cycles that aren't problematic
3275+
# in the type domain
3276+
f_recurse(x) = x > 1000000 ? x : f_recurse(x+1)
3277+
@test Base.return_types() do
3278+
f_recurse(1)
3279+
end |> first === Int
3280+
3281+
# issue #39915
3282+
function f33915(a_tuple, which_ones)
3283+
rest = f33915(Base.tail(a_tuple), Base.tail(which_ones))
3284+
if first(which_ones)
3285+
(first(a_tuple), rest...)
3286+
else
3287+
rest
3288+
end
3289+
end
3290+
f33915(a_tuple::Tuple{}, which_ones::Tuple{}) = ()
3291+
g39915(a_tuple) = f33915(a_tuple, (true, false, true, false))
3292+
@test Base.return_types() do
3293+
g39915((1, 1.0, "a", :a))
3294+
end |> first === Tuple{Int, String}

0 commit comments

Comments
 (0)