Skip to content

Commit ce0f17c

Browse files
committed
Merge pull request #45993 from JuliaLang/avi/recursion-effects
effects: relax recursion detection for effects analysis
1 parent c882602 commit ce0f17c

File tree

2 files changed

+285
-18
lines changed

2 files changed

+285
-18
lines changed

base/compiler/abstractinterpretation.jl

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -653,14 +653,109 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp
653653
# this edge is known to terminate
654654
edge_effects = Effects(edge_effects; terminates=ALWAYS_TRUE)
655655
elseif edgecycle
656-
# Some sort of recursion was detected. Even if we did not limit types,
657-
# we cannot guarantee that the call will terminate
658-
edge_effects = Effects(edge_effects; terminates=TRISTATE_UNKNOWN)
656+
# Some sort of recursion was detected.
657+
if edge !== nothing && !edgelimited && !is_edge_recursed(edge, sv)
658+
# no `MethodInstance` cycles -- don't taint :terminate
659+
else
660+
# we cannot guarantee that the call will terminate
661+
effects = Effects(effects; terminates=TRISTATE_UNKNOWN)
662+
end
659663
end
660664
return MethodCallResult(rt, edgecycle, edgelimited, edge, edge_effects)
661665
end
662666

663-
# keeps result and context information of abstract method call, will be used by succeeding constant-propagation
667+
function edge_matches_sv(frame::InferenceState, method::Method, @nospecialize(sig), sparams::SimpleVector, hardlimit::Bool, sv::InferenceState)
668+
# The `method_for_inference_heuristics` will expand the given method's generator if
669+
# necessary in order to retrieve this field from the generated `CodeInfo`, if it exists.
670+
# The other `CodeInfo`s we inspect will already have this field inflated, so we just
671+
# access it directly instead (to avoid regeneration).
672+
callee_method2 = method_for_inference_heuristics(method, sig, sparams) # Union{Method, Nothing}
673+
674+
inf_method2 = frame.src.method_for_inference_limit_heuristics # limit only if user token match
675+
inf_method2 isa Method || (inf_method2 = nothing)
676+
if callee_method2 !== inf_method2
677+
return false
678+
end
679+
if !hardlimit
680+
# if this is a soft limit,
681+
# also inspect the parent of this edge,
682+
# to see if they are the same Method as sv
683+
# in which case we'll need to ensure it is convergent
684+
# otherwise, we don't
685+
686+
# check in the cycle list first
687+
# all items in here are mutual parents of all others
688+
if !_any(p::InferenceState->matches_sv(p, sv), frame.callers_in_cycle)
689+
let parent = frame.parent
690+
parent !== nothing || return false
691+
parent = parent::InferenceState
692+
(parent.cached || parent.parent !== nothing) || return false
693+
matches_sv(parent, sv) || return false
694+
end
695+
end
696+
697+
# If the method defines a recursion relation, give it a chance
698+
# to tell us that this recursion is actually ok.
699+
if isdefined(method, :recursion_relation)
700+
if Core._apply_pure(method.recursion_relation, Any[method, callee_method2, sig, frame.linfo.specTypes])
701+
return false
702+
end
703+
end
704+
end
705+
return true
706+
end
707+
708+
# This function is used for computing alternate limit heuristics
709+
function method_for_inference_heuristics(method::Method, @nospecialize(sig), sparams::SimpleVector)
710+
if isdefined(method, :generator) && method.generator.expand_early && may_invoke_generator(method, sig, sparams)
711+
method_instance = specialize_method(method, sig, sparams)
712+
if isa(method_instance, MethodInstance)
713+
cinfo = get_staged(method_instance)
714+
if isa(cinfo, CodeInfo)
715+
method2 = cinfo.method_for_inference_limit_heuristics
716+
if method2 isa Method
717+
return method2
718+
end
719+
end
720+
end
721+
end
722+
return nothing
723+
end
724+
725+
function matches_sv(parent::InferenceState, sv::InferenceState)
726+
sv_method2 = sv.src.method_for_inference_limit_heuristics # limit only if user token match
727+
sv_method2 isa Method || (sv_method2 = nothing)
728+
parent_method2 = parent.src.method_for_inference_limit_heuristics # limit only if user token match
729+
parent_method2 isa Method || (parent_method2 = nothing)
730+
return parent.linfo.def === sv.linfo.def && sv_method2 === parent_method2
731+
end
732+
733+
function is_edge_recursed(edge::MethodInstance, sv::InferenceState)
734+
return any(InfStackUnwind(sv)) do infstate
735+
return edge === infstate.linfo
736+
end
737+
end
738+
739+
function is_method_recursed(method::Method, sv::InferenceState)
740+
return any(InfStackUnwind(sv)) do infstate
741+
return method === infstate.linfo.def
742+
end
743+
end
744+
745+
function is_constprop_edge_recursed(edge::MethodInstance, sv::InferenceState)
746+
return any(InfStackUnwind(sv)) do infstate
747+
return edge === infstate.linfo && any(infstate.result.overridden_by_const)
748+
end
749+
end
750+
751+
function is_constprop_method_recursed(method::Method, sv::InferenceState)
752+
return any(InfStackUnwind(sv)) do infstate
753+
return method === infstate.linfo.def && any(infstate.result.overridden_by_const)
754+
end
755+
end
756+
757+
# keeps result and context information of abstract_method_call, which will later be used for
758+
# backedge computation, and concrete evaluation or constant-propagation
664759
struct MethodCallResult
665760
rt
666761
edgecycle::Bool
@@ -802,17 +897,14 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, resul
802897
if inf_result === nothing
803898
# if there might be a cycle, check to make sure we don't end up
804899
# calling ourselves here.
805-
let result = result # prevent capturing
806-
if result.edgecycle && _any(InfStackUnwind(sv)) do infstate
807-
# if the type complexity limiting didn't decide to limit the call signature (`result.edgelimited = false`)
808-
# we can relax the cycle detection by comparing `MethodInstance`s and allow inference to
809-
# propagate different constant elements if the recursion is finite over the lattice
810-
return (result.edgelimited ? match.method === infstate.linfo.def : mi === infstate.linfo) &&
811-
any(infstate.result.overridden_by_const)
812-
end
813-
add_remark!(interp, sv, "[constprop] Edge cycle encountered")
814-
return nothing
815-
end
900+
if result.edgecycle && (result.edgelimited ?
901+
is_constprop_method_recursed(match.method, sv) :
902+
# if the type complexity limiting didn't decide to limit the call signature (`result.edgelimited = false`)
903+
# we can relax the cycle detection by comparing `MethodInstance`s and allow inference to
904+
# propagate different constant elements if the recursion is finite over the lattice
905+
is_constprop_edge_recursed(mi, sv))
906+
add_remark!(interp, sv, "[constprop] Edge cycle encountered")
907+
return nothing
816908
end
817909
inf_result = InferenceResult(mi, (arginfo, sv))
818910
if !any(inf_result.overridden_by_const)
@@ -923,8 +1015,8 @@ function is_const_prop_profitable_arg(@nospecialize(arg))
9231015
isa(arg, PartialOpaque) && return true
9241016
isa(arg, Const) || return true
9251017
val = arg.val
926-
# don't consider mutable values or Strings useful constants
927-
return isa(val, Symbol) || isa(val, Type) || (!isa(val, String) && !ismutable(val))
1018+
# don't consider mutable values useful constants
1019+
return isa(val, Symbol) || isa(val, Type) || !ismutable(val)
9281020
end
9291021

9301022
function is_const_prop_profitable_conditional(cnd::Conditional, fargs::Vector{Any}, sv::InferenceState)
@@ -1276,7 +1368,7 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, sv::
12761368
end
12771369
cti = Any[Vararg{argt}]
12781370
end
1279-
if _any(t -> t === Bottom, cti)
1371+
if any(@nospecialize(t) -> t === Bottom, cti)
12801372
continue
12811373
end
12821374
for j = 1:length(ctypes)
@@ -1951,6 +2043,8 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e),
19512043
for i = 3:length(e.args)
19522044
if abstract_eval_value(interp, e.args[i], vtypes, sv) === Bottom
19532045
t = Bottom
2046+
tristate_merge!(sv, EFFECTS_THROWS)
2047+
@goto t_computed
19542048
end
19552049
end
19562050
cconv = e.args[5]

test/compiler/effects.jl

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
using Test
2+
include("irutils.jl")
3+
4+
# control flow backedge should taint `terminates`
5+
@test Base.infer_effects((Int,)) do n
6+
for i = 1:n; end
7+
end |> !Core.Compiler.is_terminates
8+
9+
# interprocedural-recursion should taint `terminates` **appropriately**
10+
function sumrecur(a, x)
11+
isempty(a) && return x
12+
return sumrecur(Base.tail(a), x + first(a))
13+
end
14+
@test Base.infer_effects(sumrecur, (Tuple{Int,Int,Int},Int)) |> Core.Compiler.is_terminates
15+
@test Base.infer_effects(sumrecur, (Tuple{Int,Int,Int,Vararg{Int}},Int)) |> !Core.Compiler.is_terminates
16+
17+
# https://github.com/JuliaLang/julia/issues/45781
18+
@test Base.infer_effects((Float32,)) do a
19+
out1 = promote_type(Irrational{}, Bool)
20+
out2 = sin(a)
21+
out1, out2
22+
end |> Core.Compiler.is_terminates
23+
24+
# refine :consistent-cy effect inference using the return type information
25+
@test Base.infer_effects((Any,)) do x
26+
taint = Ref{Any}(x) # taints :consistent-cy, but will be adjusted
27+
throw(taint)
28+
end |> Core.Compiler.is_consistent
29+
@test Base.infer_effects((Int,)) do x
30+
if x < 0
31+
taint = Ref(x) # taints :consistent-cy, but will be adjusted
32+
throw(DomainError(x, taint))
33+
end
34+
return nothing
35+
end |> Core.Compiler.is_consistent
36+
@test Base.infer_effects((Int,)) do x
37+
if x < 0
38+
taint = Ref(x) # taints :consistent-cy, but will be adjusted
39+
throw(DomainError(x, taint))
40+
end
41+
return x == 0 ? nothing : x # should `Union` of isbitstype objects nicely
42+
end |> Core.Compiler.is_consistent
43+
@test Base.infer_effects((Symbol,Any)) do s, x
44+
if s === :throw
45+
taint = Ref{Any}(":throw option given") # taints :consistent-cy, but will be adjusted
46+
throw(taint)
47+
end
48+
return s # should handle `Symbol` nicely
49+
end |> Core.Compiler.is_consistent
50+
@test Base.infer_effects((Int,)) do x
51+
return Ref(x)
52+
end |> !Core.Compiler.is_consistent
53+
@test Base.infer_effects((Int,)) do x
54+
return x < 0 ? Ref(x) : nothing
55+
end |> !Core.Compiler.is_consistent
56+
@test Base.infer_effects((Int,)) do x
57+
if x < 0
58+
throw(DomainError(x, lazy"$x is negative"))
59+
end
60+
return nothing
61+
end |> Core.Compiler.is_foldable
62+
63+
# effects propagation for `Core.invoke` calls
64+
# https://github.com/JuliaLang/julia/issues/44763
65+
global x44763::Int = 0
66+
increase_x44763!(n) = (global x44763; x44763 += n)
67+
invoke44763(x) = @invoke increase_x44763!(x)
68+
@test Base.return_types() do
69+
invoke44763(42)
70+
end |> only === Int
71+
@test x44763 == 0
72+
73+
# Test that purity doesn't try to accidentally run unreachable code due to
74+
# boundscheck elimination
75+
function f_boundscheck_elim(n)
76+
# Inbounds here assumes that this is only ever called with n==0, but of
77+
# course the compiler has no way of knowing that, so it must not attempt
78+
# to run the @inbounds `getfield(sin, 1)`` that ntuple generates.
79+
ntuple(x->(@inbounds getfield(sin, x)), n)
80+
end
81+
@test Tuple{} <: code_typed(f_boundscheck_elim, Tuple{Int})[1][2]
82+
83+
# Test that purity modeling doesn't accidentally introduce new world age issues
84+
f_redefine_me(x) = x+1
85+
f_call_redefine() = f_redefine_me(0)
86+
f_mk_opaque() = Base.Experimental.@opaque ()->Base.inferencebarrier(f_call_redefine)()
87+
const op_capture_world = f_mk_opaque()
88+
f_redefine_me(x) = x+2
89+
@test op_capture_world() == 1
90+
@test f_mk_opaque()() == 2
91+
92+
# backedge insertion for Any-typed, effect-free frame
93+
const CONST_DICT = let d = Dict()
94+
for c in 'A':'z'
95+
push!(d, c => Int(c))
96+
end
97+
d
98+
end
99+
Base.@assume_effects :foldable getcharid(c) = CONST_DICT[c]
100+
@noinline callf(f, args...) = f(args...)
101+
function entry_to_be_invalidated(c)
102+
return callf(getcharid, c)
103+
end
104+
@test Base.infer_effects((Char,)) do x
105+
entry_to_be_invalidated(x)
106+
end |> Core.Compiler.is_foldable
107+
@test fully_eliminated(; retval=97) do
108+
entry_to_be_invalidated('a')
109+
end
110+
getcharid(c) = CONST_DICT[c] # now this is not eligible for concrete evaluation
111+
@test Base.infer_effects((Char,)) do x
112+
entry_to_be_invalidated(x)
113+
end |> !Core.Compiler.is_foldable
114+
@test !fully_eliminated() do
115+
entry_to_be_invalidated('a')
116+
end
117+
118+
@test !Core.Compiler.builtin_nothrow(Core.get_binding_type, Any[Rational{Int}, Core.Const(:foo)], Any)
119+
120+
# Nothrow for assignment to globals
121+
global glob_assign_int::Int = 0
122+
f_glob_assign_int() = global glob_assign_int += 1
123+
let effects = Base.infer_effects(f_glob_assign_int, ())
124+
@test !Core.Compiler.is_effect_free(effects)
125+
@test Core.Compiler.is_nothrow(effects)
126+
end
127+
# Nothrow for setglobal!
128+
global SETGLOBAL!_NOTHROW::Int = 0
129+
let effects = Base.infer_effects() do
130+
setglobal!(@__MODULE__, :SETGLOBAL!_NOTHROW, 42)
131+
end
132+
@test Core.Compiler.is_nothrow(effects)
133+
end
134+
135+
# we should taint `nothrow` if the binding doesn't exist and isn't fixed yet,
136+
# as the cached effects can be easily wrong otherwise
137+
# since the inference curently doesn't track "world-age" of global variables
138+
@eval global_assignment_undefinedyet() = $(GlobalRef(@__MODULE__, :UNDEFINEDYET)) = 42
139+
setglobal!_nothrow_undefinedyet() = setglobal!(@__MODULE__, :UNDEFINEDYET, 42)
140+
let effects = Base.infer_effects() do
141+
global_assignment_undefinedyet()
142+
end
143+
@test !Core.Compiler.is_nothrow(effects)
144+
end
145+
let effects = Base.infer_effects() do
146+
setglobal!_nothrow_undefinedyet()
147+
end
148+
@test !Core.Compiler.is_nothrow(effects)
149+
end
150+
global UNDEFINEDYET::String = "0"
151+
let effects = Base.infer_effects() do
152+
global_assignment_undefinedyet()
153+
end
154+
@test !Core.Compiler.is_nothrow(effects)
155+
end
156+
let effects = Base.infer_effects() do
157+
setglobal!_nothrow_undefinedyet()
158+
end
159+
@test !Core.Compiler.is_nothrow(effects)
160+
end
161+
@test_throws ErrorException setglobal!_nothrow_undefinedyet()
162+
163+
# Nothrow for setfield!
164+
mutable struct SetfieldNothrow
165+
x::Int
166+
end
167+
f_setfield_nothrow() = SetfieldNothrow(0).x = 1
168+
let effects = Base.infer_effects(f_setfield_nothrow, ())
169+
# Technically effect free even though we use the heap, since the
170+
# object doesn't escape, but the compiler doesn't know that.
171+
#@test Core.Compiler.is_effect_free(effects)
172+
@test Core.Compiler.is_nothrow(effects)
173+
end

0 commit comments

Comments
 (0)