From 113f1dd0177d29b1a92159ddd5ccdc54940428eb Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Wed, 4 Oct 2023 12:07:06 -0400 Subject: [PATCH 1/5] inference: Model type propagation through exceptions Currently the type of a caught exception is always modeled as `Any`. This isn't a huge problem, because control flow in Julia is generally assumed to be somewhat slow, so the extra type imprecision of not knowing the return type does not matter all that much. However, there are a few situations where it matters. For example: ``` maybe_getindex(A, i) = try; A[i]; catch e; isa(e, BoundsError) && return nothing; rethrow(); end ``` At present, we cannot infer :nothrow for this method, even if that is the only error type that `A[i]` can throw. This is particularly noticable, since we can now optimize away `:nothrow` exception frames entirely (#51674). Note that this PR still does not make the above example particularly efficient (at least interprocedurally), though specialized codegen could be added on top of this to make that happen. It does however improve the inference result. A second major motivation of this change is that reasoning about exception types is likely to be a major aspect of any future work on interface checking (since interfaces imply the absence of MethodErrors), so this PR lays the groundwork for appropriate modeling of these error paths. Note that this PR adds all the required plumbing, but does not yet have a particularly precise model of error types for our builtins, bailing to `Any` for any builtin not known to be `:nothrow`. This can be improved in follow up PRs as required. --- base/boot.jl | 6 +- base/compiler/abstractinterpretation.jl | 288 +++++++++++------- base/compiler/effects.jl | 54 ++++ base/compiler/inferencestate.jl | 58 ++-- base/compiler/ssair/irinterp.jl | 4 +- base/compiler/ssair/slot2ssa.jl | 15 +- base/compiler/stmtinfo.jl | 1 + base/compiler/tfuncs.jl | 44 +-- base/compiler/typeinfer.jl | 26 +- base/compiler/types.jl | 3 +- base/compiler/utilities.jl | 6 +- src/codegen.cpp | 11 +- src/gf.c | 20 +- src/jltypes.c | 12 +- src/julia.h | 1 + src/julia_internal.h | 7 + src/opaque_closure.c | 9 +- stdlib/REPL/src/REPLCompletions.jl | 6 +- test/compiler/AbstractInterpreter.jl | 2 +- .../compiler/EscapeAnalysis/EscapeAnalysis.jl | 2 +- test/compiler/inference.jl | 29 +- test/core.jl | 2 +- test/precompile.jl | 2 +- 23 files changed, 396 insertions(+), 212 deletions(-) diff --git a/base/boot.jl b/base/boot.jl index ff3502c2f418ea..056b14fa5d0ace 100644 --- a/base/boot.jl +++ b/base/boot.jl @@ -479,13 +479,13 @@ eval(Core, quote end) function CodeInstance( - mi::MethodInstance, @nospecialize(rettype), @nospecialize(inferred_const), + mi::MethodInstance, @nospecialize(rettype), @nospecialize(exctype), @nospecialize(inferred_const), @nospecialize(inferred), const_flags::Int32, min_world::UInt, max_world::UInt, ipo_effects::UInt32, effects::UInt32, @nospecialize(analysis_results), relocatability::UInt8) return ccall(:jl_new_codeinst, Ref{CodeInstance}, - (Any, Any, Any, Any, Int32, UInt, UInt, UInt32, UInt32, Any, UInt8), - mi, rettype, inferred_const, inferred, const_flags, min_world, max_world, + (Any, Any, Any, Any, Any, Int32, UInt, UInt, UInt32, UInt32, Any, UInt8), + mi, rettype, exctype, inferred_const, inferred, const_flags, min_world, max_world, ipo_effects, effects, analysis_results, relocatability) end diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 77024670b7002b..03174aeed3e5a9 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -18,7 +18,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), # which is all that's required for :consistent-cy. Of course, we don't # know anything else about this statement. effects = Effects(; consistent=ALWAYS_TRUE) - return CallMeta(Any, effects, NoCallInfo()) + return CallMeta(Any, Any, effects, NoCallInfo()) end argtypes = arginfo.argtypes @@ -26,13 +26,13 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), InferenceParams(interp).max_union_splitting, max_methods) if isa(matches, FailedMethodMatch) add_remark!(interp, sv, matches.reason) - return CallMeta(Any, Effects(), NoCallInfo()) + return CallMeta(Any, Any, Effects(), NoCallInfo()) end (; valid_worlds, applicable, info) = matches update_valid_age!(sv, valid_worlds) napplicable = length(applicable) - rettype = Bottom + rettype = excttype = Bottom edges = MethodInstance[] conditionals = nothing # keeps refinement information of call argument types when the return type is boolean seen = 0 # number of signatures actually inferred @@ -51,6 +51,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), break end this_rt = Bottom + this_exct = Bottom splitunions = false # TODO: this used to trigger a bug in inference recursion detection, and is unmaintained now # sigtuple = unwrap_unionall(sig)::DataType @@ -59,15 +60,22 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), splitsigs = switchtupleunion(sig) for sig_n in splitsigs result = abstract_call_method(interp, method, sig_n, svec(), multiple_matches, si, sv) - (; rt, edge, effects, volatile_inf_result) = result + (; rt, exct, edge, effects, volatile_inf_result) = result this_argtypes = isa(matches, MethodMatches) ? argtypes : matches.applicable_argtypes[i] this_arginfo = ArgInfo(fargs, this_argtypes) const_call_result = abstract_call_method_with_const_args(interp, result, f, this_arginfo, si, match, sv) const_result = volatile_inf_result if const_call_result !== nothing - if const_call_result.rt ⊑ₚ rt + if !(rt ⊑ₚ const_call_result.rt) rt = const_call_result.rt + exct = const_call_result.exct + (; effects, const_result, edge) = const_call_result + elseif better_effects(const_call_result.effects, effects) + exct = const_call_result.exct + (; effects, const_result, edge) = const_call_result + elseif !(exct ⊑ₚ const_call_result.exct) + exct = const_call_result.exct (; effects, const_result, edge) = const_call_result else add_remark!(interp, sv, "[constprop] Discarded because the result was wider than inference") @@ -82,6 +90,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), end edge === nothing || push!(edges, edge) this_rt = tmerge(this_rt, rt) + this_exct = tmerge(this_exct, exct) if bail_out_call(interp, this_rt, sv) break end @@ -90,9 +99,10 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), this_rt = widenwrappedconditional(this_rt) else result = abstract_call_method(interp, method, sig, match.sparams, multiple_matches, si, sv) - (; rt, edge, effects, volatile_inf_result) = result + (; rt, exct, edge, effects, volatile_inf_result) = result this_conditional = ignorelimited(rt) this_rt = widenwrappedconditional(rt) + this_exct = exct # try constant propagation with argtypes for this match # this is in preparation for inlining, or improving the return result this_argtypes = isa(matches, MethodMatches) ? argtypes : matches.applicable_argtypes[i] @@ -105,9 +115,16 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), this_const_rt = widenwrappedconditional(const_call_result.rt) # return type of const-prop' inference can be wider than that of non const-prop' inference # e.g. in cases when there are cycles but cached result is still accurate - if this_const_rt ⊑ₚ this_rt + if !(this_rt ⊑ₚ this_const_rt) this_conditional = this_const_conditional this_rt = this_const_rt + this_exct = const_call_result.exct + (; effects, const_result, edge) = const_call_result + elseif better_effects(const_call_result.effects, effects) + this_exct = const_call_result.exct + (; effects, const_result, edge) = const_call_result + elseif !(this_exct ⊑ₚ const_call_result.exct) + this_exct = const_call_result.exct (; effects, const_result, edge) = const_call_result else add_remark!(interp, sv, "[constprop] Discarded because the result was wider than inference") @@ -125,6 +142,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), @assert !(this_conditional isa Conditional || this_rt isa MustAlias) "invalid lattice element returned from inter-procedural context" seen += 1 rettype = tmerge(𝕃ₚ, rettype, this_rt) + excttype = tmerge(𝕃ₚ, excttype, this_exct) if has_conditional(𝕃ₚ, sv) && this_conditional !== Bottom && is_lattice_bool(𝕃ₚ, rettype) && fargs !== nothing if conditionals === nothing conditionals = Any[Bottom for _ in 1:length(argtypes)], @@ -149,12 +167,13 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), if seen ≠ napplicable # there is unanalyzed candidate, widen type and effects to the top - rettype = Any + rettype = excttype = Any all_effects = Effects() elseif isa(matches, MethodMatches) ? (!matches.fullmatch || any_ambig(matches)) : (!all(matches.fullmatches) || any_ambig(matches)) # Account for the fact that we may encounter a MethodError with a non-covered or ambiguous signature. all_effects = Effects(all_effects; nothrow=false) + excttype = tmerge(𝕃ₚ, excttype, MethodError) end rettype = from_interprocedural!(interp, rettype, sv, arginfo, conditionals) @@ -199,7 +218,8 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), end end end - return CallMeta(rettype, all_effects, info) + + return CallMeta(rettype, excttype, all_effects, info) end struct FailedMethodMatch @@ -490,13 +510,13 @@ function abstract_call_method(interp::AbstractInterpreter, hardlimit::Bool, si::StmtInfo, sv::AbsIntState) if method.name === :depwarn && isdefined(Main, :Base) && method.module === Main.Base add_remark!(interp, sv, "Refusing to infer into `depwarn`") - return MethodCallResult(Any, false, false, nothing, Effects()) + return MethodCallResult(Any, Any, false, false, nothing, Effects()) end sigtuple = unwrap_unionall(sig) sigtuple isa DataType || - return MethodCallResult(Any, false, false, nothing, Effects()) + return MethodCallResult(Any, Any, false, false, nothing, Effects()) all(@nospecialize(x) -> valid_as_lattice(unwrapva(x), true), sigtuple.parameters) || - return MethodCallResult(Union{}, false, false, nothing, EFFECTS_THROWS) # catch bad type intersections early + return MethodCallResult(Union{}, Any, false, false, nothing, EFFECTS_THROWS) # catch bad type intersections early if is_nospecializeinfer(method) sig = get_nospecializeinfer_sig(method, sig, sparams) @@ -521,7 +541,7 @@ function abstract_call_method(interp::AbstractInterpreter, # we have a self-cycle in the call-graph, but not in the inference graph (typically): # break this edge now (before we record it) by returning early # (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases) - return MethodCallResult(Any, true, true, nothing, Effects()) + return MethodCallResult(Any, Any, true, true, nothing, Effects()) end topmost = nothing edgecycle = true @@ -576,7 +596,7 @@ function abstract_call_method(interp::AbstractInterpreter, # since it's very unlikely that we'll try to inline this, # or want make an invoke edge to its calling convention return type. # (non-typically, this means that we lose the ability to detect a guaranteed StackOverflow in some cases) - return MethodCallResult(Any, true, true, nothing, Effects()) + return MethodCallResult(Any, Any, true, true, nothing, Effects()) end add_remark!(interp, sv, washardlimit ? RECURSION_MSG_HARDLIMIT : RECURSION_MSG) # TODO (#48913) implement a proper recursion handling for irinterp: @@ -622,7 +642,7 @@ function abstract_call_method(interp::AbstractInterpreter, sparams = recomputed[2]::SimpleVector end - (; rt, edge, effects, volatile_inf_result) = typeinf_edge(interp, method, sig, sparams, sv) + (; rt, exct, edge, effects, volatile_inf_result) = typeinf_edge(interp, method, sig, sparams, sv) if edge === nothing edgecycle = edgelimited = true @@ -646,7 +666,7 @@ function abstract_call_method(interp::AbstractInterpreter, end end - return MethodCallResult(rt, edgecycle, edgelimited, edge, effects, volatile_inf_result) + return MethodCallResult(rt, exct, edgecycle, edgelimited, edge, effects, volatile_inf_result) end function edge_matches_sv(interp::AbstractInterpreter, frame::AbsIntState, @@ -745,18 +765,19 @@ end # backedge computation, and concrete evaluation or constant-propagation struct MethodCallResult rt + exct edgecycle::Bool edgelimited::Bool edge::Union{Nothing,MethodInstance} effects::Effects volatile_inf_result::Union{Nothing,VolatileInferenceResult} - function MethodCallResult(@nospecialize(rt), + function MethodCallResult(@nospecialize(rt), @nospecialize(exct), edgecycle::Bool, edgelimited::Bool, edge::Union{Nothing,MethodInstance}, effects::Effects, volatile_inf_result::Union{Nothing,VolatileInferenceResult}=nothing) - return new(rt, edgecycle, edgelimited, edge, effects, volatile_inf_result) + return new(rt, exct, edgecycle, edgelimited, edge, effects, volatile_inf_result) end end @@ -768,15 +789,16 @@ end struct ConstCallResults rt::Any + exct::Any const_result::ConstResult effects::Effects edge::MethodInstance function ConstCallResults( - @nospecialize(rt), + @nospecialize(rt), @nospecialize(exct), const_result::ConstResult, effects::Effects, edge::MethodInstance) - return new(rt, const_result, effects, edge) + return new(rt, exct, const_result, effects, edge) end end @@ -903,11 +925,12 @@ function concrete_eval_call(interp::AbstractInterpreter, edge = result.edge::MethodInstance value = try Core._call_in_world_total(world, f, args...) - catch - # The evaluation threw. By :consistent-cy, we're guaranteed this would have happened at runtime - return ConstCallResults(Bottom, ConcreteResult(edge, result.effects), result.effects, edge) + catch e + # The evaluation threw. By :consistent-cy, we're guaranteed this would have happened at runtime. + # Howevever, at present, :consistency does not mandate the type of the exception + return ConstCallResults(Bottom, Any, ConcreteResult(edge, result.effects), result.effects, edge) end - return ConstCallResults(Const(value), ConcreteResult(edge, EFFECTS_TOTAL, value), EFFECTS_TOTAL, edge) + return ConstCallResults(Const(value), Union{}, ConcreteResult(edge, EFFECTS_TOTAL, value), EFFECTS_TOTAL, edge) end # check if there is a cycle and duplicated inference of `mi` @@ -1170,7 +1193,7 @@ function semi_concrete_eval_call(interp::AbstractInterpreter, if noub effects = Effects(effects; noub = ALWAYS_TRUE) end - return ConstCallResults(rt, SemiConcreteResult(mi, ir, effects), effects, mi) + return ConstCallResults(rt, result.exct, SemiConcreteResult(mi, ir, effects), effects, mi) end end end @@ -1179,7 +1202,7 @@ end function const_prop_call(interp::AbstractInterpreter, mi::MethodInstance, result::MethodCallResult, arginfo::ArgInfo, sv::AbsIntState, - concrete_eval_result::Union{Nothing,ConstCallResults}=nothing) + concrete_eval_result::Union{Nothing, ConstCallResults}=nothing) inf_cache = get_inference_cache(interp) 𝕃ᵢ = typeinf_lattice(interp) inf_result = cache_lookup(𝕃ᵢ, mi, arginfo.argtypes, inf_cache) @@ -1214,7 +1237,8 @@ function const_prop_call(interp::AbstractInterpreter, return nothing end end - return ConstCallResults(inf_result.result, ConstPropResult(inf_result), inf_result.ipo_effects, mi) + return ConstCallResults(inf_result.result, inf_result.exc_result, + ConstPropResult(inf_result), inf_result.ipo_effects, mi) end # TODO implement MustAlias forwarding @@ -1457,7 +1481,7 @@ function abstract_iteration(interp::AbstractInterpreter, @nospecialize(itft), @n # WARNING: Changes to the iteration protocol must be reflected here, # this is not just an optimization. # TODO: this doesn't realize that Array, SimpleVector, Tuple, and NamedTuple do not use the iterate protocol - stateordonet === Bottom && return AbstractIterationResult(Any[Bottom], AbstractIterationInfo(CallMeta[CallMeta(Bottom, call.effects, info)], true)) + stateordonet === Bottom && return AbstractIterationResult(Any[Bottom], AbstractIterationInfo(CallMeta[CallMeta(Bottom, Any, call.effects, info)], true)) valtype = statetype = Bottom ret = Any[] calls = CallMeta[call] @@ -1535,7 +1559,7 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, si:: sv::AbsIntState, max_methods::Int=get_max_methods(interp, sv)) itft = argtype_by_index(argtypes, 2) aft = argtype_by_index(argtypes, 3) - (itft === Bottom || aft === Bottom) && return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo()) + (itft === Bottom || aft === Bottom) && return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()) aargtypes = argtype_tail(argtypes, 4) aftw = widenconst(aft) if !isa(aft, Const) && !isa(aft, PartialOpaque) && (!isType(aftw) || has_free_typevars(aftw)) @@ -1543,7 +1567,7 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, si:: add_remark!(interp, sv, "Core._apply_iterate called on a function of a non-concrete type") # bail now, since it seems unlikely that abstract_call will be able to do any better after splitting # this also ensures we don't call abstract_call_gf_by_type below on an IntrinsicFunction or Builtin - return CallMeta(Any, Effects(), NoCallInfo()) + return CallMeta(Any, Any, Effects(), NoCallInfo()) end end res = Union{} @@ -1602,6 +1626,7 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, si:: retinfo = UnionSplitApplyCallInfo(retinfos) napplicable = length(ctypes) seen = 0 + exct = effects.nothrow ? Union{} : Any for i = 1:napplicable ct = ctypes[i] arginfo = infos[i] @@ -1619,6 +1644,7 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, si:: seen += 1 push!(retinfos, ApplyCallInfo(call.info, arginfo)) res = tmerge(typeinf_lattice(interp), res, call.rt) + exct = tmerge(typeinf_lattice(interp), exct, call.exct) effects = merge_effects(effects, call.effects) if bail_out_apply(interp, InferenceLoopState(ct, res, effects), sv) add_remark!(interp, sv, "_apply_iterate inference reached maximally imprecise information. Bailing on.") @@ -1628,12 +1654,13 @@ function abstract_apply(interp::AbstractInterpreter, argtypes::Vector{Any}, si:: if seen ≠ napplicable # there is unanalyzed candidate, widen type and effects to the top res = Any + exct = Any effects = Effects() retinfo = NoCallInfo() # NOTE this is necessary to prevent the inlining processing end # TODO: Add a special info type to capture all the iteration info. # For now, only propagate info if we don't also union-split the iteration - return CallMeta(res, effects, retinfo) + return CallMeta(res, exct, effects, retinfo) end function argtype_by_index(argtypes::Vector{Any}, i::Int) @@ -1882,9 +1909,9 @@ function abstract_call_unionall(interp::AbstractInterpreter, argtypes::Vector{An na = length(argtypes) if isvarargtype(argtypes[end]) if na ≤ 2 - return CallMeta(Any, EFFECTS_THROWS, call.info) + return CallMeta(Any, Any, EFFECTS_THROWS, call.info) elseif na > 4 - return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo()) + return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()) end a2 = argtypes[2] a3 = unwrapva(argtypes[3]) @@ -1895,7 +1922,7 @@ function abstract_call_unionall(interp::AbstractInterpreter, argtypes::Vector{An ⊑ᵢ = ⊑(typeinf_lattice(interp)) nothrow = a2 ⊑ᵢ TypeVar && (a3 ⊑ᵢ Type || a3 ⊑ᵢ TypeVar) else - return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo()) + return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()) end canconst = true if isa(a3, Const) @@ -1904,10 +1931,10 @@ function abstract_call_unionall(interp::AbstractInterpreter, argtypes::Vector{An body = a3.parameters[1] canconst = false else - return CallMeta(Any, Effects(EFFECTS_TOTAL; nothrow), call.info) + return CallMeta(Any, Any, Effects(EFFECTS_TOTAL; nothrow), call.info) end if !(isa(body, Type) || isa(body, TypeVar)) - return CallMeta(Any, EFFECTS_THROWS, call.info) + return CallMeta(Any, Any, EFFECTS_THROWS, call.info) end if has_free_typevars(body) if isa(a2, Const) @@ -1916,36 +1943,36 @@ function abstract_call_unionall(interp::AbstractInterpreter, argtypes::Vector{An tv = a2.tv canconst = false else - return CallMeta(Any, EFFECTS_THROWS, call.info) + return CallMeta(Any, Any, EFFECTS_THROWS, call.info) end - isa(tv, TypeVar) || return CallMeta(Any, EFFECTS_THROWS, call.info) + isa(tv, TypeVar) || return CallMeta(Any, Any, EFFECTS_THROWS, call.info) body = UnionAll(tv, body) end ret = canconst ? Const(body) : Type{body} - return CallMeta(ret, Effects(EFFECTS_TOTAL; nothrow), call.info) + return CallMeta(ret, Any, Effects(EFFECTS_TOTAL; nothrow), call.info) end function abstract_invoke(interp::AbstractInterpreter, (; fargs, argtypes)::ArgInfo, si::StmtInfo, sv::AbsIntState) ft′ = argtype_by_index(argtypes, 2) ft = widenconst(ft′) - ft === Bottom && return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo()) + ft === Bottom && return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()) (types, isexact, isconcrete, istype) = instanceof_tfunc(argtype_by_index(argtypes, 3), false) - isexact || return CallMeta(Any, Effects(), NoCallInfo()) + isexact || return CallMeta(Any, Any, Effects(), NoCallInfo()) unwrapped = unwrap_unionall(types) if types === Bottom || !(unwrapped isa DataType) || unwrapped.name !== Tuple.name - return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo()) + return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()) end argtype = argtypes_to_type(argtype_tail(argtypes, 4)) nargtype = typeintersect(types, argtype) - nargtype === Bottom && return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo()) - nargtype isa DataType || return CallMeta(Any, Effects(), NoCallInfo()) # other cases are not implemented below - isdispatchelem(ft) || return CallMeta(Any, Effects(), NoCallInfo()) # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below + nargtype === Bottom && return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()) + nargtype isa DataType || return CallMeta(Any, Any, Effects(), NoCallInfo()) # other cases are not implemented below + isdispatchelem(ft) || return CallMeta(Any, Any, Effects(), NoCallInfo()) # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below ft = ft::DataType lookupsig = rewrap_unionall(Tuple{ft, unwrapped.parameters...}, types)::Type nargtype = Tuple{ft, nargtype.parameters...} argtype = Tuple{ft, argtype.parameters...} match, valid_worlds = findsup(lookupsig, method_table(interp)) - match === nothing && return CallMeta(Any, Effects(), NoCallInfo()) + match === nothing && return CallMeta(Any, Any, Effects(), NoCallInfo()) update_valid_age!(sv, valid_worlds) method = match.method tienv = ccall(:jl_type_intersection_with_env, Any, (Any, Any), nargtype, method.sig)::SimpleVector @@ -1977,7 +2004,7 @@ function abstract_invoke(interp::AbstractInterpreter, (; fargs, argtypes)::ArgIn rt = from_interprocedural!(interp, rt, sv, arginfo, sig) info = InvokeCallInfo(match, const_result) edge !== nothing && add_invoke_backedge!(sv, lookupsig, edge) - return CallMeta(rt, effects, info) + return CallMeta(rt, Any, effects, info) end function invoke_rewrite(xs::Vector{Any}) @@ -1991,9 +2018,9 @@ function abstract_finalizer(interp::AbstractInterpreter, argtypes::Vector{Any}, if length(argtypes) == 3 finalizer_argvec = Any[argtypes[2], argtypes[3]] call = abstract_call(interp, ArgInfo(nothing, finalizer_argvec), StmtInfo(false), sv, #=max_methods=#1) - return CallMeta(Nothing, Effects(), FinalizerInfo(call.info, call.effects)) + return CallMeta(Nothing, Any, Effects(), FinalizerInfo(call.info, call.effects)) end - return CallMeta(Nothing, Effects(), NoCallInfo()) + return CallMeta(Nothing, Any, Effects(), NoCallInfo()) end # call where the function is known exactly @@ -2015,19 +2042,21 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), return abstract_finalizer(interp, argtypes, sv) elseif f === applicable return abstract_applicable(interp, argtypes, sv, max_methods) + elseif f === throw + return CallMeta(Union{}, la == 2 ? argtypes[2] : ArgumentError, EFFECTS_THROWS, NoCallInfo()) end rt = abstract_call_builtin(interp, f, arginfo, sv) ft = popfirst!(argtypes) effects = builtin_effects(𝕃ᵢ, f, argtypes, rt) pushfirst!(argtypes, ft) - return CallMeta(rt, effects, NoCallInfo()) + return CallMeta(rt, effects.nothrow ? Union{} : Any, effects, NoCallInfo()) elseif isa(f, Core.OpaqueClosure) # calling an OpaqueClosure about which we have no information returns no information - return CallMeta(typeof(f).parameters[2], Effects(), NoCallInfo()) + return CallMeta(typeof(f).parameters[2], Any, Effects(), NoCallInfo()) elseif f === TypeVar # Manually look through the definition of TypeVar to # make sure to be able to get `PartialTypeVar`s out. - (la < 2 || la > 4) && return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo()) + (la < 2 || la > 4) && return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()) n = argtypes[2] ub_var = Const(Any) lb_var = Const(Union{}) @@ -2046,7 +2075,7 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), end pT = typevar_tfunc(𝕃ᵢ, n, lb_var, ub_var) effects = builtin_effects(𝕃ᵢ, Core._typevar, Any[n, lb_var, ub_var], pT) - return CallMeta(pT, effects, call.info) + return CallMeta(pT, Any, effects, call.info) elseif f === UnionAll call = abstract_call_gf_by_type(interp, f, ArgInfo(nothing, Any[Const(UnionAll), Any, Any]), si, Tuple{Type{UnionAll}, Any, Any}, sv, max_methods) return abstract_call_unionall(interp, argtypes, call) @@ -2054,7 +2083,7 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), aty = argtypes[2] ty = isvarargtype(aty) ? unwrapva(aty) : widenconst(aty) if !isconcretetype(ty) - return CallMeta(Tuple, EFFECTS_UNKNOWN, NoCallInfo()) + return CallMeta(Tuple, Any, EFFECTS_UNKNOWN, NoCallInfo()) end elseif is_return_type(f) return return_type_tfunc(interp, argtypes, si, sv) @@ -2063,16 +2092,16 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), aty = argtypes[2] if isa(aty, Conditional) call = abstract_call_gf_by_type(interp, f, ArgInfo(fargs, Any[Const(f), Bool]), si, Tuple{typeof(f), Bool}, sv, max_methods) # make sure we've inferred `!(::Bool)` - return CallMeta(Conditional(aty.slot, aty.elsetype, aty.thentype), call.effects, call.info) + return CallMeta(Conditional(aty.slot, aty.elsetype, aty.thentype), Any, call.effects, call.info) end elseif la == 3 && istopfunction(f, :!==) # mark !== as exactly a negated call to === call = abstract_call_gf_by_type(interp, f, ArgInfo(fargs, Any[Const(f), Any, Any]), si, Tuple{typeof(f), Any, Any}, sv, max_methods) rty = abstract_call_known(interp, (===), arginfo, si, sv, max_methods).rt if isa(rty, Conditional) - return CallMeta(Conditional(rty.slot, rty.elsetype, rty.thentype), EFFECTS_TOTAL, NoCallInfo()) # swap if-else + return CallMeta(Conditional(rty.slot, rty.elsetype, rty.thentype), Bottom, EFFECTS_TOTAL, NoCallInfo()) # swap if-else elseif isa(rty, Const) - return CallMeta(Const(rty.val === false), EFFECTS_TOTAL, MethodResultPure()) + return CallMeta(Const(rty.val === false), Bottom, EFFECTS_TOTAL, MethodResultPure()) end return call elseif la == 3 && istopfunction(f, :(>:)) @@ -2086,7 +2115,7 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), argtypes = Any[typeof(<:), argtypes[3], argtypes[2]] return abstract_call_known(interp, <:, ArgInfo(fargs, argtypes), si, sv, max_methods) elseif la == 2 && istopfunction(f, :typename) - return CallMeta(typename_static(argtypes[2]), EFFECTS_TOTAL, MethodResultPure()) + return CallMeta(typename_static(argtypes[2]), Any, EFFECTS_TOTAL, MethodResultPure()) elseif f === Core._hasmethod return _hasmethod_tfunc(interp, argtypes, sv) end @@ -2125,7 +2154,7 @@ function abstract_call_opaque_closure(interp::AbstractInterpreter, rt = from_interprocedural!(interp, rt, sv, arginfo, match.spec_types) info = OpaqueClosureCallInfo(match, const_result) edge !== nothing && add_backedge!(sv, edge) - return CallMeta(rt, effects, info) + return CallMeta(rt, Any, effects, info) end function most_general_argtypes(closure::PartialOpaque) @@ -2150,13 +2179,13 @@ function abstract_call_unknown(interp::AbstractInterpreter, @nospecialize(ft), wft = widenconst(ft) if hasintersect(wft, Builtin) add_remark!(interp, sv, "Could not identify method table for call") - return CallMeta(Any, Effects(), NoCallInfo()) + return CallMeta(Any, Any, Effects(), NoCallInfo()) elseif hasintersect(wft, Core.OpaqueClosure) uft = unwrap_unionall(wft) if isa(uft, DataType) - return CallMeta(rewrap_unionall(uft.parameters[2], wft), Effects(), NoCallInfo()) + return CallMeta(rewrap_unionall(uft.parameters[2], wft), Any, Effects(), NoCallInfo()) end - return CallMeta(Any, Effects(), NoCallInfo()) + return CallMeta(Any, Any, Effects(), NoCallInfo()) end # non-constant function, but the number of arguments is known and the `f` is not a builtin or intrinsic atype = argtypes_to_type(arginfo.argtypes) @@ -2237,31 +2266,30 @@ end function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize(e), vtypes::Union{VarTable,Nothing}, sv::AbsIntState) if isa(e, QuoteNode) - return RTEffects(Const(e.value), EFFECTS_TOTAL) + return RTEffects(Const(e.value), Union{}, EFFECTS_TOTAL) elseif isa(e, SSAValue) - return RTEffects(abstract_eval_ssavalue(e, sv), EFFECTS_TOTAL) + return RTEffects(abstract_eval_ssavalue(e, sv), Union{}, EFFECTS_TOTAL) elseif isa(e, SlotNumber) - effects = EFFECTS_THROWS if vtypes !== nothing vtyp = vtypes[slot_id(e)] if !vtyp.undef - effects = EFFECTS_TOTAL + return RTEffects(vtyp.typ, Union{}, EFFECTS_TOTAL) end - return RTEffects(vtyp.typ, effects) + return RTEffects(vtyp.typ, UndefVarError, EFFECTS_THROWS) end - return RTEffects(Any, effects) + return RTEffects(Any, UndefVarError, EFFECTS_THROWS) elseif isa(e, Argument) if vtypes !== nothing - return RTEffects(vtypes[slot_id(e)].typ, EFFECTS_TOTAL) + return RTEffects(vtypes[slot_id(e)].typ, Union{}, EFFECTS_TOTAL) else @assert isa(sv, IRInterpretationState) - return RTEffects(sv.ir.argtypes[e.n], EFFECTS_TOTAL) # TODO frame_argtypes(sv)[e.n] and remove the assertion + return RTEffects(sv.ir.argtypes[e.n], Union{}, EFFECTS_TOTAL) # TODO frame_argtypes(sv)[e.n] and remove the assertion end elseif isa(e, GlobalRef) return abstract_eval_globalref(interp, e, sv) end - return RTEffects(Const(e), EFFECTS_TOTAL) + return RTEffects(Const(e), Union{}, EFFECTS_TOTAL) end function abstract_eval_value_expr(interp::AbstractInterpreter, e::Expr, vtypes::Union{VarTable,Nothing}, sv::AbsIntState) @@ -2316,17 +2344,18 @@ end struct RTEffects rt + exct effects::Effects - RTEffects(@nospecialize(rt), effects::Effects) = new(rt, effects) + RTEffects(@nospecialize(rt), @nospecialize(exct), effects::Effects) = new(rt, exct, effects) end function abstract_call(interp::AbstractInterpreter, arginfo::ArgInfo, sv::InferenceState) si = StmtInfo(!call_result_unused(sv, sv.currpc)) - (; rt, effects, info) = abstract_call(interp, arginfo, si, sv) + (; rt, exct, effects, info) = abstract_call(interp, arginfo, si, sv) sv.stmt_info[sv.currpc] = info # mark this call statement as DCE-eligible # TODO better to do this in a single pass based on the `info` object at the end of abstractinterpret? - return RTEffects(rt, effects) + return RTEffects(rt, exct, effects) end function abstract_eval_call(interp::AbstractInterpreter, e::Expr, vtypes::Union{VarTable,Nothing}, @@ -2334,26 +2363,33 @@ function abstract_eval_call(interp::AbstractInterpreter, e::Expr, vtypes::Union{ ea = e.args argtypes = collect_argtypes(interp, ea, vtypes, sv) if argtypes === nothing - return RTEffects(Bottom, Effects()) + return RTEffects(Bottom, Any, Effects()) end arginfo = ArgInfo(ea, argtypes) return abstract_call(interp, arginfo, sv) end +function abstract_eval_the_exception(interp::AbstractInterpreter, sv::InferenceState) + return sv.handlers[sv.handler_at[sv.currpc][2]].exct +end +abstract_eval_the_exception(interp, sv) = Any + function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtypes::Union{VarTable,Nothing}, sv::AbsIntState) effects = Effects() ehead = e.head 𝕃ᵢ = typeinf_lattice(interp) ⊑ᵢ = ⊑(𝕃ᵢ) + exct = Any if ehead === :call - (; rt, effects) = abstract_eval_call(interp, e, vtypes, sv) + (; rt, exct, effects) = abstract_eval_call(interp, e, vtypes, sv) t = rt elseif ehead === :new t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv), true) ut = unwrap_unionall(t) consistent = noub = ALWAYS_FALSE nothrow = false + exct = Union{ErrorException, TypeError} if isa(ut, DataType) && !isabstracttype(ut) ismutable = ismutabletype(ut) fcount = datatype_fieldcount(ut) @@ -2413,6 +2449,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp t = refine_partial_type(t) end end + nothrow && (exct = Union{}) effects = Effects(EFFECTS_TOTAL; consistent, nothrow, noub) elseif ehead === :splatnew t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv), true) @@ -2463,7 +2500,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp end end elseif ehead === :foreigncall - (; rt, effects) = abstract_eval_foreigncall(interp, e, vtypes, sv) + (; rt, exct, effects) = abstract_eval_foreigncall(interp, e, vtypes, sv) t = rt elseif ehead === :cfunction effects = EFFECTS_UNKNOWN @@ -2486,6 +2523,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp sym = e.args[1] t = Bool effects = EFFECTS_TOTAL + exct = Union{} if isa(sym, SlotNumber) && vtypes !== nothing vtyp = vtypes[slot_id(sym)] if vtyp.typ === Bottom @@ -2526,10 +2564,12 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp condt = argextype(stmt.args[2], ir) condval = maybe_extract_const_bool(condt) t = Nothing + exct = UndefVarError effects = EFFECTS_THROWS if condval isa Bool if condval effects = EFFECTS_TOTAL + exct = Union{} else t = Union{} end @@ -2538,13 +2578,16 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp end elseif ehead === :boundscheck t = Bool + exct = Union{} effects = Effects(EFFECTS_TOTAL; consistent=ALWAYS_FALSE) elseif ehead === :the_exception - t = Any + t = abstract_eval_the_exception(interp, sv) + exct = Union{} effects = Effects(EFFECTS_TOTAL; consistent=ALWAYS_FALSE) elseif ehead === :static_parameter n = e.args[1]::Int nothrow = false + exct = UndefVarError if 1 <= n <= length(sv.sptypes) sp = sv.sptypes[n] t = sp.typ @@ -2552,15 +2595,21 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp else t = Any end + if nothrow + exct = Union{} + end effects = Effects(EFFECTS_TOTAL; nothrow) elseif ehead === :gc_preserve_begin || ehead === :aliasscope t = Any + exct = Union{} effects = Effects(EFFECTS_TOTAL; consistent=ALWAYS_FALSE, effect_free=EFFECT_FREE_GLOBALLY) elseif ehead === :gc_preserve_end || ehead === :leave || ehead === :pop_exception || ehead === :global || ehead === :popaliasscope t = Nothing + exct = Union{} effects = Effects(EFFECTS_TOTAL; effect_free=EFFECT_FREE_GLOBALLY) elseif ehead === :method t = Method + exct = Union{} effects = Effects(EFFECTS_TOTAL; effect_free=EFFECT_FREE_GLOBALLY) elseif ehead === :thunk t = Any @@ -2576,7 +2625,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp # and recompute the effects. effects = EFFECTS_TOTAL end - return RTEffects(t, effects) + return RTEffects(t, exct, effects) end # refine the result of instantiation of partially-known type `t` if some invariant can be assumed @@ -2597,7 +2646,7 @@ function abstract_eval_foreigncall(interp::AbstractInterpreter, e::Expr, vtypes: t = sp_type_rewrap(e.args[2], mi, true) for i = 3:length(e.args) if abstract_eval_value(interp, e.args[i], vtypes, sv) === Bottom - return RTEffects(Bottom, EFFECTS_THROWS) + return RTEffects(Bottom, Any, EFFECTS_THROWS) end end effects = foreigncall_effects(e) do @nospecialize x @@ -2615,7 +2664,7 @@ function abstract_eval_foreigncall(interp::AbstractInterpreter, e::Expr, vtypes: inaccessiblememonly = override.inaccessiblememonly ? ALWAYS_TRUE : effects.inaccessiblememonly, noub = override.noub ? ALWAYS_TRUE : override.noub_if_noinbounds ? NOUB_IF_NOINBOUNDS : effects.noub) end - return RTEffects(t, effects) + return RTEffects(t, Any, effects) end function abstract_eval_phi(interp::AbstractInterpreter, phi::PhiNode, vtypes::Union{VarTable,Nothing}, sv::AbsIntState) @@ -2639,11 +2688,11 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), if !isa(e, Expr) if isa(e, PhiNode) add_curr_ssaflag!(sv, IR_FLAG_EFFECT_FREE | IR_FLAG_NOTHROW) - return abstract_eval_phi(interp, e, vtypes, sv) + return RTEffects(abstract_eval_phi(interp, e, vtypes, sv), Union{}, EFFECTS_TOTAL) end - (; rt, effects) = abstract_eval_special_value(interp, e, vtypes, sv) + (; rt, exct, effects) = abstract_eval_special_value(interp, e, vtypes, sv) else - (; rt, effects) = abstract_eval_statement_expr(interp, e, vtypes, sv) + (; rt, exct, effects) = abstract_eval_statement_expr(interp, e, vtypes, sv) if effects.noub === NOUB_IF_NOINBOUNDS if !iszero(get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) effects = Effects(effects; noub=ALWAYS_FALSE) @@ -2671,7 +2720,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), set_curr_ssaflag!(sv, flags_for_effects(effects), IR_FLAGS_EFFECTS) merge_effects!(interp, sv, effects) - return rt + return RTEffects(rt, exct, effects) end function isdefined_globalref(g::GlobalRef) @@ -2704,7 +2753,7 @@ function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv:: consistent = inaccessiblememonly = ALWAYS_TRUE rt = Union{} end - return RTEffects(rt, Effects(EFFECTS_TOTAL; consistent, nothrow, inaccessiblememonly)) + return RTEffects(rt, nothrow ? Union{} : UndefVarError, Effects(EFFECTS_TOTAL; consistent, nothrow, inaccessiblememonly)) end function handle_global_assignment!(interp::AbstractInterpreter, frame::InferenceState, lhs::GlobalRef, @nospecialize(newty)) @@ -2898,50 +2947,51 @@ end struct BasicStmtChange changes::Union{Nothing,StateUpdate} - type::Any # ::Union{Type, Nothing} - `nothing` if this statement may not be used as an SSA Value + rt::Any # extended lattice element or `nothing` - `nothing` if this statement may not be used as an SSA Value + exct::Any # TODO effects::Effects - BasicStmtChange(changes::Union{Nothing,StateUpdate}, @nospecialize type) = new(changes, type) + BasicStmtChange(changes::Union{Nothing,StateUpdate}, @nospecialize(rt), @nospecialize(exct)) = new(changes, rt, exct) end @inline function abstract_eval_basic_statement(interp::AbstractInterpreter, @nospecialize(stmt), pc_vartable::VarTable, frame::InferenceState) if isa(stmt, NewvarNode) changes = StateUpdate(stmt.slot, VarState(Bottom, true), pc_vartable, false) - return BasicStmtChange(changes, nothing) + return BasicStmtChange(changes, nothing, Union{}) elseif !isa(stmt, Expr) - t = abstract_eval_statement(interp, stmt, pc_vartable, frame) - return BasicStmtChange(nothing, t) + (; rt, exct) = abstract_eval_statement(interp, stmt, pc_vartable, frame) + return BasicStmtChange(nothing, rt, exct) end changes = nothing stmt = stmt::Expr hd = stmt.head if hd === :(=) - t = abstract_eval_statement(interp, stmt.args[2], pc_vartable, frame) - if t === Bottom - return BasicStmtChange(nothing, Bottom) + (; rt, exct) = abstract_eval_statement(interp, stmt.args[2], pc_vartable, frame) + if rt === Bottom + return BasicStmtChange(nothing, Bottom, exct) end lhs = stmt.args[1] if isa(lhs, SlotNumber) - changes = StateUpdate(lhs, VarState(t, false), pc_vartable, false) + changes = StateUpdate(lhs, VarState(rt, false), pc_vartable, false) elseif isa(lhs, GlobalRef) - handle_global_assignment!(interp, frame, lhs, t) + handle_global_assignment!(interp, frame, lhs, rt) elseif !isa(lhs, SSAValue) merge_effects!(interp, frame, EFFECTS_UNKNOWN) end - return BasicStmtChange(changes, t) + return BasicStmtChange(changes, rt, exct) elseif hd === :method fname = stmt.args[1] if isa(fname, SlotNumber) changes = StateUpdate(fname, VarState(Any, false), pc_vartable, false) end - return BasicStmtChange(changes, nothing) + return BasicStmtChange(changes, nothing, Union{}) elseif (hd === :code_coverage_effect || ( hd !== :boundscheck && # :boundscheck can be narrowed to Bool is_meta_expr(stmt))) - return BasicStmtChange(nothing, Nothing) + return BasicStmtChange(nothing, Nothing, Bottom) else - t = abstract_eval_statement(interp, stmt, pc_vartable, frame) - return BasicStmtChange(nothing, t) + (; rt, exct) = abstract_eval_statement(interp, stmt, pc_vartable, frame) + return BasicStmtChange(nothing, rt, exct) end end @@ -3002,9 +3052,9 @@ end function propagate_to_error_handler!(frame::InferenceState, currpc::Int, W::BitSet, 𝕃ᵢ::AbstractLattice, currstate::VarTable) # If this statement potentially threw, propagate the currstate to the # exception handler, BEFORE applying any state changes. - cur_hand = frame.handler_at[currpc] + cur_hand = frame.handler_at[currpc][1] if cur_hand != 0 - enter = frame.src.code[cur_hand]::Expr + enter = frame.src.code[frame.handlers[cur_hand].enter_idx]::Expr l = enter.args[1]::Int exceptbb = block_for_inst(frame.cfg, l) if update_bbstate!(𝕃ᵢ, frame, exceptbb, currstate) @@ -3150,23 +3200,47 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) # Fall through terminator - treat as regular stmt end # Process non control-flow statements - (; changes, type) = abstract_eval_basic_statement(interp, + (; changes, rt, exct) = abstract_eval_basic_statement(interp, stmt, currstate, frame) + if exct !== Union{} + 𝕃ₚ = ipo_lattice(interp) + cur_hand = frame.handler_at[currpc][1] + if cur_hand == 0 + if !⊑(𝕃ₚ, exct, frame.exc_bestguess) + frame.exc_bestguess = tmerge(𝕃ₚ, frame.exc_bestguess, exct) + for (caller, caller_pc) in frame.cycle_backedges + handler = caller.handler_at[caller_pc][1] + if (handler == 0 ? caller.exc_bestguess : caller.handlers[handler].exct) !== Any + push!(caller.ip, block_for_inst(caller.cfg, caller_pc)) + end + end + end + else + handler_frame = frame.handlers[cur_hand] + if !⊑(𝕃ₚ, exct, handler_frame.exct) + handler_frame.exct = tmerge(𝕃ₚ, handler_frame.exct, exct) + enter = frame.src.code[handler_frame.enter_idx]::Expr + l = enter.args[1]::Int + exceptbb = block_for_inst(frame.cfg, l) + push!(W, exceptbb) + end + end + end if (get_curr_ssaflag(frame) & IR_FLAG_NOTHROW) != IR_FLAG_NOTHROW propagate_to_error_handler!(frame, currpc, W, 𝕃ᵢ, currstate) end - if type === Bottom + if rt === Bottom ssavaluetypes[currpc] = Bottom @goto find_next_bb end if changes !== nothing stoverwrite1!(currstate, changes) end - if type === nothing + if rt === nothing ssavaluetypes[currpc] = Any continue end - record_ssa_assign!(𝕃ᵢ, currpc, type, frame) + record_ssa_assign!(𝕃ᵢ, currpc, rt, frame) end # for currpc in bbstart:bbend # Case 1: Fallthrough termination diff --git a/base/compiler/effects.jl b/base/compiler/effects.jl index fc774efbbee85d..4b5f3b392107a4 100644 --- a/base/compiler/effects.jl +++ b/base/compiler/effects.jl @@ -171,6 +171,60 @@ function Effects(effects::Effects = _EFFECTS_UNKNOWN; nonoverlayed) end +function better_effects(new::Effects, old::Effects) + any_improved = false + if new.consistent == ALWAYS_TRUE + any_improved |= old.consistent != ALWAYS_TRUE + elseif new.consistent != old.consistent + return false + end + if new.effect_free == ALWAYS_TRUE + any_improved |= old.consistent != ALWAYS_TRUE + elseif new.effect_free == EFFECT_FREE_IF_INACCESSIBLEMEMONLY + old.effect_free == ALWAYS_TRUE && return false + any_improved |= old.effect_free != EFFECT_FREE_IF_INACCESSIBLEMEMONLY + elseif new.effect_free != old.effect_free + return false + end + if new.nothrow + any_improved |= !old.nothrow + elseif new.nothrow != old.nothrow + return false + end + if new.terminates + any_improved |= !old.terminates + elseif new.terminates != old.terminates + return false + end + if new.notaskstate + any_improved |= !old.notaskstate + elseif new.notaskstate != old.notaskstate + return false + end + if new.inaccessiblememonly == ALWAYS_TRUE + any_improved |= old.inaccessiblememonly != ALWAYS_TRUE + elseif new.inaccessiblememonly == INACCESSIBLEMEM_OR_ARGMEMONLY + old.inaccessiblememonly == ALWAYS_TRUE && return false + any_improved |= old.inaccessiblememonly != INACCESSIBLEMEM_OR_ARGMEMONLY + elseif new.inaccessiblememonly != old.inaccessiblememonly + return false + end + if new.noub == ALWAYS_TRUE + any_improved |= old.noub != ALWAYS_TRUE + elseif new.noub == NOUB_IF_NOINBOUNDS + old.noub == ALWAYS_TRUE && return false + any_improved |= old.noub != NOUB_IF_NOINBOUNDS + elseif new.noub != old.noub + return false + end + if new.nonoverlayed + any_improved |= !old.nonoverlayed + elseif new.nonoverlayed != old.nonoverlayed + return false + end + return any_improved +end + function merge_effects(old::Effects, new::Effects) return Effects( merge_effectbits(old.consistent, new.consistent), diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index 4024273944200c..5f9855372ff08b 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -203,6 +203,11 @@ const CACHE_MODE_GLOBAL = 0x01 << 0 # cached globally, optimization allowed const CACHE_MODE_LOCAL = 0x01 << 1 # cached locally, optimization allowed const CACHE_MODE_VOLATILE = 0x01 << 2 # not cached, optimization allowed +mutable struct TryCatchFrame + exct + const enter_idx +end + mutable struct InferenceState #= information about this method instance =# linfo::MethodInstance @@ -218,7 +223,8 @@ mutable struct InferenceState currbb::Int currpc::Int ip::BitSet#=TODO BoundedMinPrioritySet=# # current active instruction pointers - handler_at::Vector{Int} # current exception handler info + handlers::Vector{TryCatchFrame} + handler_at::Vector{Tuple{Int, Int}} # tuple of current (handler, exception stack) value at the pc ssavalue_uses::Vector{BitSet} # ssavalue sparsity and restart info # TODO: Could keep this sparsely by doing structural liveness analysis ahead of time. bb_vartables::Vector{Union{Nothing,VarTable}} # nothing if not analyzed yet @@ -239,6 +245,7 @@ mutable struct InferenceState unreachable::BitSet # statements that were found to be statically unreachable valid_worlds::WorldRange bestguess #::Type + exc_bestguess ipo_effects::Effects #= flags =# @@ -266,7 +273,7 @@ mutable struct InferenceState currbb = currpc = 1 ip = BitSet(1) # TODO BitSetBoundedMinPrioritySet(1) - handler_at = compute_trycatch(code, BitSet()) + handler_at, handlers = compute_trycatch(code, BitSet()) nssavalues = src.ssavaluetypes::Int ssavalue_uses = find_ssavalue_uses(code, nssavalues) nstmts = length(code) @@ -296,6 +303,7 @@ mutable struct InferenceState valid_worlds = WorldRange(src.min_world, src.max_world == typemax(UInt) ? get_world_counter() : src.max_world) bestguess = Bottom + exc_bestguess = Bottom ipo_effects = EFFECTS_TOTAL insert_coverage = should_insert_coverage(mod, src) @@ -315,9 +323,9 @@ mutable struct InferenceState return new( linfo, world, mod, sptypes, slottypes, src, cfg, method_info, - currbb, currpc, ip, handler_at, ssavalue_uses, bb_vartables, ssavaluetypes, stmt_edges, stmt_info, + currbb, currpc, ip, handlers, handler_at, ssavalue_uses, bb_vartables, ssavaluetypes, stmt_edges, stmt_info, pclimitations, limitations, cycle_backedges, callers_in_cycle, dont_work_on_me, parent, - result, unreachable, valid_worlds, bestguess, ipo_effects, + result, unreachable, valid_worlds, bestguess, exc_bestguess, ipo_effects, restrict_abstract_call_sites, cache_mode, insert_coverage, interp) end @@ -347,16 +355,19 @@ function compute_trycatch(code::Vector{Any}, ip::BitSet) empty!(ip) ip.offset = 0 # for _bits_findnext push!(ip, n + 1) - handler_at = fill(0, n) + handler_at = fill((0, 0), n) + handlers = TryCatchFrame[] # start from all :enter statements and record the location of the try for pc = 1:n stmt = code[pc] if isexpr(stmt, :enter) l = stmt.args[1]::Int - handler_at[pc + 1] = pc + push!(handlers, TryCatchFrame(Bottom, pc)) + handler_id = length(handlers) + handler_at[pc + 1] = (handler_id, 0) push!(ip, pc + 1) - handler_at[l] = pc + handler_at[l] = (handler_id, handler_id) push!(ip, l) end end @@ -369,25 +380,26 @@ function compute_trycatch(code::Vector{Any}, ip::BitSet) while true # inner loop optimizes the common case where it can run straight from pc to pc + 1 pc´ = pc + 1 # next program-counter (after executing instruction) delete!(ip, pc) - cur_hand = handler_at[pc] - @assert cur_hand != 0 "unbalanced try/catch" + cur_stacks = handler_at[pc] + @assert cur_stacks != (0, 0) "unbalanced try/catch" stmt = code[pc] if isa(stmt, GotoNode) pc´ = stmt.label elseif isa(stmt, GotoIfNot) l = stmt.dest::Int - if handler_at[l] != cur_hand - @assert handler_at[l] == 0 "unbalanced try/catch" - handler_at[l] = cur_hand + if handler_at[l] != cur_stacks + @assert handler_at[l][1] == 0 || handler_at[l][1] == cur_stacks[1] "unbalanced try/catch" + handler_at[l] = cur_stacks push!(ip, l) end elseif isa(stmt, ReturnNode) - @assert !isdefined(stmt, :val) "unbalanced try/catch" + @assert !isdefined(stmt, :val) || cur_stacks[1] == 0 "unbalanced try/catch" break elseif isa(stmt, Expr) head = stmt.head if head === :enter - cur_hand = pc + # Already set above + cur_stacks = (handler_at[pc´][1], cur_stacks[2]) elseif head === :leave l = 0 for j = 1:length(stmt.args) @@ -403,19 +415,21 @@ function compute_trycatch(code::Vector{Any}, ip::BitSet) end l += 1 end + cur_hand = cur_stacks[1] for i = 1:l - cur_hand = handler_at[cur_hand] + cur_hand = handler_at[handlers[cur_hand].enter_idx][1] end - cur_hand == 0 && break + cur_stacks = (cur_hand, cur_stacks[2]) + cur_stacks == (0, 0) && break + elseif head === :pop_exception + cur_stacks = (cur_stacks[1], handler_at[(stmt.args[1]::SSAValue).id][2]) + cur_stacks == (0, 0) && break end end pc´ > n && break # can't proceed with the fast-path fall-through - if handler_at[pc´] != cur_hand - if handler_at[pc´] != 0 - @assert false "unbalanced try/catch" - end - handler_at[pc´] = cur_hand + if handler_at[pc´] != cur_stacks + handler_at[pc´] = cur_stacks elseif !in(pc´, ip) break # already visited end @@ -424,7 +438,7 @@ function compute_trycatch(code::Vector{Any}, ip::BitSet) end @assert first(ip) == n + 1 - return handler_at + return handler_at, handlers end # check if coverage mode is enabled diff --git a/base/compiler/ssair/irinterp.jl b/base/compiler/ssair/irinterp.jl index fbd2112a1c0fef..c7982869da0049 100644 --- a/base/compiler/ssair/irinterp.jl +++ b/base/compiler/ssair/irinterp.jl @@ -46,9 +46,9 @@ end function abstract_call(interp::AbstractInterpreter, arginfo::ArgInfo, irsv::IRInterpretationState) si = StmtInfo(true) # TODO better job here? - (; rt, effects, info) = abstract_call(interp, arginfo, si, irsv) + (; rt, exct, effects, info) = abstract_call(interp, arginfo, si, irsv) irsv.ir.stmts[irsv.curridx][:info] = info - return RTEffects(rt, effects) + return RTEffects(rt, exct, effects) end function update_phi!(irsv::IRInterpretationState, from::Int, to::Int) diff --git a/base/compiler/ssair/slot2ssa.jl b/base/compiler/ssair/slot2ssa.jl index f3253526d25e98..cbe167926014cd 100644 --- a/base/compiler/ssair/slot2ssa.jl +++ b/base/compiler/ssair/slot2ssa.jl @@ -584,7 +584,7 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, sv::OptimizationState, end # Record the correct exception handler for all critical sections - handler_at = compute_trycatch(code, BitSet()) + handler_at, handlers = compute_trycatch(code, BitSet()) phi_slots = Vector{Int}[Int[] for _ = 1:length(ir.cfg.blocks)] live_slots = Vector{Int}[Int[] for _ = 1:length(ir.cfg.blocks)] @@ -627,10 +627,12 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, sv::OptimizationState, # The slot is live-in into this block. We need to # Create a PhiC node in the catch entry block and # an upsilon node in the corresponding enter block + varstate = sv.bb_vartables[li] + if varstate === nothing + continue + end node = PhiCNode(Any[]) insertpoint = first_insert_for_bb(code, cfg, li) - varstate = sv.bb_vartables[li] - @assert varstate !== nothing vt = varstate[idx] phic_ssa = NewSSAValue( insert_node!(ir, insertpoint, @@ -690,6 +692,9 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, sv::OptimizationState, new_nodes = ir.new_nodes @timeit "SSA Rename" while !isempty(worklist) (item::Int, pred, incoming_vals) = pop!(worklist) + if sv.bb_vartables[item] === nothing + continue + end # Rename existing phi nodes first, because their uses occur on the edge # TODO: This isn't necessary if inlining stops replacing arguments by slots. for idx in cfg.blocks[item].stmts @@ -810,8 +815,8 @@ function construct_ssa!(ci::CodeInfo, ir::IRCode, sv::OptimizationState, incoming_vals[id] = Pair{Any, Any}(thisval, thisdef) has_pinode[id] = false enter_idx = idx - while handler_at[enter_idx] != 0 - enter_idx = handler_at[enter_idx] + while handler_at[enter_idx][1] != 0 + (; enter_idx) = handlers[handler_at[enter_idx][1]] leave_block = block_for_inst(cfg, code[enter_idx].args[1]::Int) cidx = findfirst((; slot)::NewPhiCNode2->slot_id(slot)==id, new_phic_nodes[leave_block]) if cidx !== nothing diff --git a/base/compiler/stmtinfo.jl b/base/compiler/stmtinfo.jl index d634c88cdf8e89..e28858eea60aa2 100644 --- a/base/compiler/stmtinfo.jl +++ b/base/compiler/stmtinfo.jl @@ -10,6 +10,7 @@ and any additional information (`call.info`) for a given generic call. """ struct CallMeta rt::Any + exct::Any effects::Effects info::CallInfo end diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index eb43e77885d64a..c549f3f05959e8 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -1374,10 +1374,10 @@ end function abstract_modifyfield!(interp::AbstractInterpreter, argtypes::Vector{Any}, si::StmtInfo, sv::AbsIntState) nargs = length(argtypes) if !isempty(argtypes) && isvarargtype(argtypes[nargs]) - nargs - 1 <= 6 || return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo()) - nargs > 3 || return CallMeta(Any, Effects(), NoCallInfo()) + nargs - 1 <= 6 || return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()) + nargs > 3 || return CallMeta(Any, Any, Effects(), NoCallInfo()) else - 5 <= nargs <= 6 || return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo()) + 5 <= nargs <= 6 || return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()) end 𝕃ᵢ = typeinf_lattice(interp) o = unwrapva(argtypes[2]) @@ -1399,7 +1399,7 @@ function abstract_modifyfield!(interp::AbstractInterpreter, argtypes::Vector{Any end info = ModifyFieldInfo(callinfo.info) end - return CallMeta(RT, Effects(), info) + return CallMeta(RT, Any, Effects(), info) end @nospecs function replacefield!_tfunc(𝕃::AbstractLattice, o, f, x, v, success_order, failure_order) return replacefield!_tfunc(𝕃, o, f, x, v) @@ -2742,7 +2742,7 @@ end # since abstract_call_gf_by_type is a very inaccurate model of _method and of typeinf_type, # while this assumes that it is an absolutely precise and accurate and exact model of both function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, si::StmtInfo, sv::AbsIntState) - UNKNOWN = CallMeta(Type, EFFECTS_THROWS, NoCallInfo()) + UNKNOWN = CallMeta(Type, Any, EFFECTS_THROWS, NoCallInfo()) if !(2 <= length(argtypes) <= 3) return UNKNOWN end @@ -2771,7 +2771,7 @@ function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, s end if contains_is(argtypes_vec, Union{}) - return CallMeta(Const(Union{}), EFFECTS_TOTAL, NoCallInfo()) + return CallMeta(Const(Union{}), Any, EFFECTS_TOTAL, NoCallInfo()) end # Run the abstract_call without restricting abstract call @@ -2789,33 +2789,33 @@ function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, s rt = widenslotwrapper(call.rt) if isa(rt, Const) # output was computed to be constant - return CallMeta(Const(typeof(rt.val)), EFFECTS_TOTAL, info) + return CallMeta(Const(typeof(rt.val)), Any, EFFECTS_TOTAL, info) end rt = widenconst(rt) if rt === Bottom || (isconcretetype(rt) && !iskindtype(rt)) # output cannot be improved so it is known for certain - return CallMeta(Const(rt), EFFECTS_TOTAL, info) + return CallMeta(Const(rt), Union{}, EFFECTS_TOTAL, info) elseif isa(sv, InferenceState) && !isempty(sv.pclimitations) # conservatively express uncertainty of this result # in two ways: both as being a subtype of this, and # because of LimitedAccuracy causes - return CallMeta(Type{<:rt}, EFFECTS_TOTAL, info) + return CallMeta(Type{<:rt}, Union{}, EFFECTS_TOTAL, info) elseif isa(tt, Const) || isconstType(tt) # input arguments were known for certain # XXX: this doesn't imply we know anything about rt - return CallMeta(Const(rt), EFFECTS_TOTAL, info) + return CallMeta(Const(rt), Union{}, EFFECTS_TOTAL, info) elseif isType(rt) - return CallMeta(Type{rt}, EFFECTS_TOTAL, info) + return CallMeta(Type{rt}, Union{}, EFFECTS_TOTAL, info) else - return CallMeta(Type{<:rt}, EFFECTS_TOTAL, info) + return CallMeta(Type{<:rt}, Union{}, EFFECTS_TOTAL, info) end end # a simplified model of abstract_call_gf_by_type for applicable function abstract_applicable(interp::AbstractInterpreter, argtypes::Vector{Any}, sv::AbsIntState, max_methods::Int) - length(argtypes) < 2 && return CallMeta(Bottom, EFFECTS_THROWS, NoCallInfo()) - isvarargtype(argtypes[2]) && return CallMeta(Bool, EFFECTS_UNKNOWN, NoCallInfo()) + length(argtypes) < 2 && return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo()) + isvarargtype(argtypes[2]) && return CallMeta(Bool, Any, EFFECTS_UNKNOWN, NoCallInfo()) argtypes = argtypes[2:end] atype = argtypes_to_type(argtypes) matches = find_matching_methods(typeinf_lattice(interp), argtypes, atype, method_table(interp), @@ -2854,7 +2854,7 @@ function abstract_applicable(interp::AbstractInterpreter, argtypes::Vector{Any}, end end end - return CallMeta(rt, EFFECTS_TOTAL, NoCallInfo()) + return CallMeta(rt, Union{}, EFFECTS_TOTAL, NoCallInfo()) end add_tfunc(applicable, 1, INT_INF, @nospecs((𝕃::AbstractLattice, f, args...)->Bool), 40) @@ -2863,26 +2863,26 @@ function _hasmethod_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, sv if length(argtypes) == 3 && !isvarargtype(argtypes[3]) ft′ = argtype_by_index(argtypes, 2) ft = widenconst(ft′) - ft === Bottom && return CallMeta(Bool, EFFECTS_THROWS, NoCallInfo()) + ft === Bottom && return CallMeta(Bool, Any, EFFECTS_THROWS, NoCallInfo()) typeidx = 3 elseif length(argtypes) == 2 && !isvarargtype(argtypes[2]) typeidx = 2 else - return CallMeta(Any, Effects(), NoCallInfo()) + return CallMeta(Any, Any, Effects(), NoCallInfo()) end (types, isexact, isconcrete, istype) = instanceof_tfunc(argtype_by_index(argtypes, typeidx), false) - isexact || return CallMeta(Bool, Effects(), NoCallInfo()) + isexact || return CallMeta(Bool, Any, Effects(), NoCallInfo()) unwrapped = unwrap_unionall(types) if types === Bottom || !(unwrapped isa DataType) || unwrapped.name !== Tuple.name - return CallMeta(Bool, EFFECTS_THROWS, NoCallInfo()) + return CallMeta(Bool, Any, EFFECTS_THROWS, NoCallInfo()) end if typeidx == 3 - isdispatchelem(ft) || return CallMeta(Bool, Effects(), NoCallInfo()) # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below + isdispatchelem(ft) || return CallMeta(Bool, Any, Effects(), NoCallInfo()) # check that we might not have a subtype of `ft` at runtime, before doing supertype lookup below types = rewrap_unionall(Tuple{ft, unwrapped.parameters...}, types)::Type end mt = ccall(:jl_method_table_for, Any, (Any,), types) if !isa(mt, MethodTable) - return CallMeta(Bool, EFFECTS_THROWS, NoCallInfo()) + return CallMeta(Bool, Any, EFFECTS_THROWS, NoCallInfo()) end match, valid_worlds = findsup(types, method_table(interp)) update_valid_age!(sv, valid_worlds) @@ -2894,7 +2894,7 @@ function _hasmethod_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, sv edge = specialize_method(match)::MethodInstance add_invoke_backedge!(sv, types, edge) end - return CallMeta(rt, EFFECTS_TOTAL, NoCallInfo()) + return CallMeta(rt, Any, EFFECTS_TOTAL, NoCallInfo()) end # N.B.: typename maps type equivalence classes to a single value diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index c1230980c42a65..55026e54f777d6 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -327,7 +327,7 @@ function CodeInstance(interp::AbstractInterpreter, result::InferenceResult, end # relocatability = isa(inferred_result, String) ? inferred_result[end] : UInt8(0) return CodeInstance(result.linfo, - widenconst(result_type), rettype_const, inferred_result, + widenconst(result_type), widenconst(result.exc_result), rettype_const, inferred_result, const_flags, first(valid_worlds), last(valid_worlds), # TODO: Actually do something with non-IPO effects encode_effects(result.ipo_effects), encode_effects(result.ipo_effects), result.analysis_results, @@ -524,7 +524,8 @@ function finish(me::InferenceState, interp::AbstractInterpreter) # inspect whether our inference had a limited result accuracy, # else it may be suitable to cache bestguess = me.bestguess = cycle_fix_limited(me.bestguess, me) - limited_ret = bestguess isa LimitedAccuracy + exc_bestguess = me.exc_bestguess = cycle_fix_limited(me.exc_bestguess, me) + limited_ret = bestguess isa LimitedAccuracy || exc_bestguess isa LimitedAccuracy limited_src = false if !limited_ret gt = me.ssavaluetypes @@ -539,6 +540,7 @@ function finish(me::InferenceState, interp::AbstractInterpreter) me.result.valid_worlds = me.valid_worlds me.result.result = bestguess me.result.ipo_effects = me.ipo_effects = adjust_effects(me) + me.result.exc_result = exc_bestguess if limited_ret # a parent may be cached still, but not this intermediate work: @@ -568,6 +570,7 @@ function finish(me::InferenceState, interp::AbstractInterpreter) me.result.src = me.src # for reflection etc. end end + validate_code_in_debug_mode(me.linfo, me.src, "inferred") nothing end @@ -795,15 +798,16 @@ end ipo_effects(code::CodeInstance) = decode_effects(code.ipo_purity_bits) struct EdgeCallResult - rt #::Type + rt + exct edge::Union{Nothing,MethodInstance} effects::Effects volatile_inf_result::Union{Nothing,VolatileInferenceResult} - function EdgeCallResult(@nospecialize(rt), + function EdgeCallResult(@nospecialize(rt), @nospecialize(exct), edge::Union{Nothing,MethodInstance}, effects::Effects, volatile_inf_result::Union{Nothing,VolatileInferenceResult} = nothing) - return new(rt, edge, effects, volatile_inf_result) + return new(rt, exct, edge, effects, volatile_inf_result) end end @@ -823,14 +827,14 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize rt = cached_return_type(code) effects = ipo_effects(code) update_valid_age!(caller, WorldRange(min_world(code), max_world(code))) - return EdgeCallResult(rt, mi, effects) + return EdgeCallResult(rt, code.exctype, mi, effects) end else cache_mode = CACHE_MODE_GLOBAL # cache edge targets globally by default end if ccall(:jl_get_module_infer, Cint, (Any,), method.module) == 0 && !generating_output(#=incremental=#false) add_remark!(interp, caller, "Inference is disabled for the target module") - return EdgeCallResult(Any, nothing, Effects()) + return EdgeCallResult(Any, Any, nothing, Effects()) end if !is_cached(caller) && frame_parent(caller) === nothing # this caller exists to return to the user @@ -848,7 +852,7 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize add_remark!(interp, caller, "Failed to retrieve source") # can't get the source for this, so we know nothing unlock_mi_inference(interp, mi) - return EdgeCallResult(Any, nothing, Effects()) + return EdgeCallResult(Any, Any, nothing, Effects()) end if is_cached(caller) || frame_parent(caller) !== nothing # don't involve uncached functions in cycle resolution frame.parent = caller @@ -863,15 +867,15 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize volatile_inf_result = isinferred && let inferred_src = result.src isa(inferred_src, CodeInfo) && (is_inlineable(inferred_src) || force_inline) end ? VolatileInferenceResult(result) : nothing - return EdgeCallResult(frame.bestguess, edge, effects, volatile_inf_result) + return EdgeCallResult(frame.bestguess, frame.exc_bestguess, edge, effects, volatile_inf_result) elseif frame === true # unresolvable cycle - return EdgeCallResult(Any, nothing, Effects()) + return EdgeCallResult(Any, Any, nothing, Effects()) end # return the current knowledge about this cycle frame = frame::InferenceState update_valid_age!(caller, frame.valid_worlds) - return EdgeCallResult(frame.bestguess, nothing, adjust_effects(Effects(), method)) + return EdgeCallResult(frame.bestguess, frame.exc_bestguess, nothing, adjust_effects(Effects(), method)) end function cached_return_type(code::CodeInstance) diff --git a/base/compiler/types.jl b/base/compiler/types.jl index 41fde10dfedf45..b98cf09ff7cf1b 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -81,6 +81,7 @@ mutable struct InferenceResult const argtypes::Vector{Any} const overridden_by_const::BitVector result # extended lattice element if inferred, nothing otherwise + exc_result # like `result`, but for the thrown value src # ::Union{CodeInfo, IRCode, OptimizationState} if inferred copy is available, nothing otherwise valid_worlds::WorldRange # if inference and optimization is finished ipo_effects::Effects # if inference is finished @@ -91,7 +92,7 @@ mutable struct InferenceResult # def = linfo.def # nargs = def isa Method ? Int(def.nargs) : 0 # @assert length(cache_argtypes) == nargs - return new(linfo, cache_argtypes, overridden_by_const, nothing, nothing, + return new(linfo, cache_argtypes, overridden_by_const, nothing, nothing, nothing, WorldRange(), Effects(), Effects(), NULL_ANALYSIS_RESULTS, false) end end diff --git a/base/compiler/utilities.jl b/base/compiler/utilities.jl index 3d1fe354530605..d8ca4d9551656a 100644 --- a/base/compiler/utilities.jl +++ b/base/compiler/utilities.jl @@ -462,14 +462,14 @@ function is_throw_call(e::Expr, code::Vector{Any}) return false end -function mark_throw_blocks!(src::CodeInfo, handler_at::Vector{Int}) +function mark_throw_blocks!(src::CodeInfo, handler_at::Vector{Tuple{Int, Int}}) for stmt in find_throw_blocks(src.code, handler_at) src.ssaflags[stmt] |= IR_FLAG_THROW_BLOCK end return nothing end -function find_throw_blocks(code::Vector{Any}, handler_at::Vector{Int}) +function find_throw_blocks(code::Vector{Any}, handler_at::Vector{Tuple{Int, Int}}) stmts = BitSet() n = length(code) for i in n:-1:1 @@ -482,7 +482,7 @@ function find_throw_blocks(code::Vector{Any}, handler_at::Vector{Int}) elseif s.head === :return # see `ReturnNode` handling elseif is_throw_call(s, code) - if handler_at[i] == 0 + if handler_at[i][1] == 0 push!(stmts, i) end elseif i+1 in stmts diff --git a/src/codegen.cpp b/src/codegen.cpp index 632c76caa1c08c..16f0b6903aa28c 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -8006,7 +8006,16 @@ static jl_llvm_functions_t auto scan_ssavalue = [&](jl_value_t *val) { if (jl_is_ssavalue(val)) { - ctx.ssavalue_usecount[((jl_ssavalue_t*)val)->id-1] += 1; + size_t ssa_idx = ((jl_ssavalue_t*)val)->id-1; + /* + * We technically allow out of bounds SSAValues in dead IR, so make + * sure to bounds check this here. It's still not *good* to leave + * dead code in the IR, because this will conservatively overcount + * it, but let's at least make it not crash. + */ + if (ssa_idx < ctx.ssavalue_usecount.size()) { + ctx.ssavalue_usecount[ssa_idx] += 1; + } return true; } return false; diff --git a/src/gf.c b/src/gf.c index f964927aa33681..fffeb928a18896 100644 --- a/src/gf.c +++ b/src/gf.c @@ -284,13 +284,6 @@ JL_DLLEXPORT jl_value_t *jl_methtable_lookup(jl_methtable_t *mt, jl_value_t *typ // ----- MethodInstance specialization instantiation ----- // -JL_DLLEXPORT jl_code_instance_t* jl_new_codeinst( - jl_method_instance_t *mi, jl_value_t *rettype, - jl_value_t *inferred_const, jl_value_t *inferred, - int32_t const_flags, size_t min_world, size_t max_world, - uint32_t ipo_effects, uint32_t effects, jl_value_t *analysis_results, - uint8_t relocatability); - jl_datatype_t *jl_mk_builtin_func(jl_datatype_t *dt, const char *name, jl_fptr_args_t fptr) JL_GC_DISABLED { jl_sym_t *sname = jl_symbol(name); @@ -323,7 +316,7 @@ jl_datatype_t *jl_mk_builtin_func(jl_datatype_t *dt, const char *name, jl_fptr_a jl_gc_wb(m, mi); jl_code_instance_t *codeinst = jl_new_codeinst(mi, - (jl_value_t*)jl_any_type, jl_nothing, jl_nothing, + (jl_value_t*)jl_any_type, (jl_value_t*)jl_any_type, jl_nothing, jl_nothing, 0, 1, ~(size_t)0, 0, 0, jl_nothing, 0); jl_mi_cache_insert(mi, codeinst); jl_atomic_store_relaxed(&codeinst->specptr.fptr1, fptr); @@ -466,7 +459,7 @@ JL_DLLEXPORT jl_code_instance_t *jl_get_method_inferred( codeinst = jl_atomic_load_relaxed(&codeinst->next); } codeinst = jl_new_codeinst( - mi, rettype, NULL, NULL, + mi, rettype, (jl_value_t*)jl_any_type, NULL, NULL, 0, min_world, max_world, 0, 0, jl_nothing, 0); jl_mi_cache_insert(mi, codeinst); return codeinst; @@ -483,7 +476,7 @@ JL_DLLEXPORT jl_code_instance_t *jl_get_codeinst_for_src( } JL_DLLEXPORT jl_code_instance_t *jl_new_codeinst( - jl_method_instance_t *mi, jl_value_t *rettype, + jl_method_instance_t *mi, jl_value_t *rettype, jl_value_t *exctype, jl_value_t *inferred_const, jl_value_t *inferred, int32_t const_flags, size_t min_world, size_t max_world, uint32_t ipo_effects, uint32_t effects, jl_value_t *analysis_results, @@ -498,6 +491,7 @@ JL_DLLEXPORT jl_code_instance_t *jl_new_codeinst( codeinst->min_world = min_world; codeinst->max_world = max_world; codeinst->rettype = rettype; + codeinst->exctype = exctype; jl_atomic_store_release(&codeinst->inferred, inferred); //codeinst->edges = NULL; if ((const_flags & 2) == 0) @@ -2455,7 +2449,7 @@ jl_code_instance_t *jl_compile_method_internal(jl_method_instance_t *mi, size_t jl_callptr_t unspec_invoke = NULL; if (unspec && (unspec_invoke = jl_atomic_load_acquire(&unspec->invoke))) { jl_code_instance_t *codeinst = jl_new_codeinst(mi, - (jl_value_t*)jl_any_type, NULL, NULL, + (jl_value_t*)jl_any_type, (jl_value_t*)jl_any_type, NULL, NULL, 0, 1, ~(size_t)0, 0, 0, jl_nothing, 0); void *unspec_fptr = jl_atomic_load_relaxed(&unspec->specptr.fptr); if (unspec_fptr) { @@ -2482,7 +2476,7 @@ jl_code_instance_t *jl_compile_method_internal(jl_method_instance_t *mi, size_t jl_code_info_t *src = jl_code_for_interpreter(mi, world); if (!jl_code_requires_compiler(src, 0)) { jl_code_instance_t *codeinst = jl_new_codeinst(mi, - (jl_value_t*)jl_any_type, NULL, NULL, + (jl_value_t*)jl_any_type, (jl_value_t*)jl_any_type, NULL, NULL, 0, 1, ~(size_t)0, 0, 0, jl_nothing, 0); jl_atomic_store_release(&codeinst->invoke, jl_fptr_interpret_call); jl_mi_cache_insert(mi, codeinst); @@ -2516,7 +2510,7 @@ jl_code_instance_t *jl_compile_method_internal(jl_method_instance_t *mi, size_t // only these care about the exact specTypes, otherwise we can use it directly return ucache; } - codeinst = jl_new_codeinst(mi, (jl_value_t*)jl_any_type, NULL, NULL, + codeinst = jl_new_codeinst(mi, (jl_value_t*)jl_any_type, (jl_value_t*)jl_any_type, NULL, NULL, 0, 1, ~(size_t)0, 0, 0, jl_nothing, 0); void *unspec_fptr = jl_atomic_load_relaxed(&ucache->specptr.fptr); if (unspec_fptr) { diff --git a/src/jltypes.c b/src/jltypes.c index b1830ec4e765e6..c7c7d213c5317a 100644 --- a/src/jltypes.c +++ b/src/jltypes.c @@ -3244,12 +3244,13 @@ void jl_init_types(void) JL_GC_DISABLED jl_code_instance_type = jl_new_datatype(jl_symbol("CodeInstance"), core, jl_any_type, jl_emptysvec, - jl_perm_symsvec(15, + jl_perm_symsvec(16, "def", "next", "min_world", "max_world", "rettype", + "exctype", "rettype_const", "inferred", //"edges", @@ -3258,7 +3259,7 @@ void jl_init_types(void) JL_GC_DISABLED "analysis_results", "isspecsig", "precompile", "relocatability", "invoke", "specptr"), // function object decls - jl_svec(15, + jl_svec(16, jl_method_instance_type, jl_any_type, jl_ulong_type, @@ -3266,6 +3267,7 @@ void jl_init_types(void) JL_GC_DISABLED jl_any_type, jl_any_type, jl_any_type, + jl_any_type, //jl_any_type, //jl_bool_type, jl_uint32_type, jl_uint32_type, @@ -3277,8 +3279,8 @@ void jl_init_types(void) JL_GC_DISABLED jl_emptysvec, 0, 1, 1); jl_svecset(jl_code_instance_type->types, 1, jl_code_instance_type); - const static uint32_t code_instance_constfields[1] = { 0b000001010110001 }; // Set fields 1, 5-6, 8, 10 as const - const static uint32_t code_instance_atomicfields[1] = { 0b110100101000010 }; // Set fields 2, 7, 9, 12, 14-15 as atomic + const static uint32_t code_instance_constfields[1] = { 0b0000010101110001 }; // Set fields 1, 5-7, 9, 11 as const + const static uint32_t code_instance_atomicfields[1] = { 0b1101001010000010 }; // Set fields 2, 8, 10, 13, 15-16 as atomic //Fields 3-4 are only operated on by construction and deserialization, so are const at runtime //Fields 11 and 15 must be protected by locks, and thus all operations on jl_code_instance_t are threadsafe jl_code_instance_type->name->constfields = code_instance_constfields; @@ -3420,8 +3422,8 @@ void jl_init_types(void) JL_GC_DISABLED jl_svecset(jl_methtable_type->types, 10, jl_uint8_type); jl_svecset(jl_method_type->types, 12, jl_method_instance_type); jl_svecset(jl_method_instance_type->types, 6, jl_code_instance_type); - jl_svecset(jl_code_instance_type->types, 13, jl_voidpointer_type); jl_svecset(jl_code_instance_type->types, 14, jl_voidpointer_type); + jl_svecset(jl_code_instance_type->types, 15, jl_voidpointer_type); jl_svecset(jl_binding_type->types, 1, jl_globalref_type); jl_svecset(jl_binding_type->types, 2, jl_binding_type); diff --git a/src/julia.h b/src/julia.h index acb700c0bd936e..cdc3477a30419a 100644 --- a/src/julia.h +++ b/src/julia.h @@ -411,6 +411,7 @@ typedef struct _jl_code_instance_t { // inference state cache jl_value_t *rettype; // return type for fptr + jl_value_t *exctype; // thrown type for fptr jl_value_t *rettype_const; // inferred constant return value, or null _Atomic(jl_value_t *) inferred; // inferred jl_code_info_t (may be compressed), or jl_nothing, or null //TODO: jl_array_t *edges; // stored information about edges from this object diff --git a/src/julia_internal.h b/src/julia_internal.h index c09b73f8a1052b..eeee548a9a4eee 100644 --- a/src/julia_internal.h +++ b/src/julia_internal.h @@ -622,6 +622,13 @@ JL_DLLEXPORT jl_code_instance_t *jl_get_codeinst_for_src( jl_method_instance_t *jl_get_unspecialized_from_mi(jl_method_instance_t *method JL_PROPAGATES_ROOT); jl_method_instance_t *jl_get_unspecialized(jl_method_t *def JL_PROPAGATES_ROOT); +JL_DLLEXPORT jl_code_instance_t* jl_new_codeinst( + jl_method_instance_t *mi, jl_value_t *rettype, jl_value_t *exctype, + jl_value_t *inferred_const, jl_value_t *inferred, + int32_t const_flags, size_t min_world, size_t max_world, + uint32_t ipo_effects, uint32_t effects, jl_value_t *analysis_results, + uint8_t relocatability); + JL_DLLEXPORT void jl_compile_method_instance(jl_method_instance_t *mi, jl_tupletype_t *types, size_t world); JL_DLLEXPORT int jl_compile_hint(jl_tupletype_t *types); jl_code_info_t *jl_code_for_interpreter(jl_method_instance_t *lam JL_PROPAGATES_ROOT, size_t world); diff --git a/src/opaque_closure.c b/src/opaque_closure.c index 8b7cc7292be229..8d8698c67ed9a1 100644 --- a/src/opaque_closure.c +++ b/src/opaque_closure.c @@ -134,13 +134,6 @@ jl_opaque_closure_t *jl_new_opaque_closure(jl_tupletype_t *argt, jl_value_t *rt_ jl_method_t *jl_make_opaque_closure_method(jl_module_t *module, jl_value_t *name, int nargs, jl_value_t *functionloc, jl_code_info_t *ci, int isva); -JL_DLLEXPORT jl_code_instance_t* jl_new_codeinst( - jl_method_instance_t *mi, jl_value_t *rettype, - jl_value_t *inferred_const, jl_value_t *inferred, - int32_t const_flags, size_t min_world, size_t max_world, - uint32_t ipo_effects, uint32_t effects, jl_value_t *analysis_results, - uint8_t relocatability); - JL_DLLEXPORT jl_opaque_closure_t *jl_new_opaque_closure_from_code_info(jl_tupletype_t *argt, jl_value_t *rt_lb, jl_value_t *rt_ub, jl_module_t *mod, jl_code_info_t *ci, int lineno, jl_value_t *file, int nargs, int isva, jl_value_t *env, int do_compile) { @@ -157,7 +150,7 @@ JL_DLLEXPORT jl_opaque_closure_t *jl_new_opaque_closure_from_code_info(jl_tuplet sigtype = jl_argtype_with_function(env, (jl_value_t*)argt); jl_method_instance_t *mi = jl_specializations_get_linfo((jl_method_t*)root, sigtype, jl_emptysvec); - inst = jl_new_codeinst(mi, rt_ub, NULL, (jl_value_t*)ci, + inst = jl_new_codeinst(mi, rt_ub, NULL, NULL, (jl_value_t*)ci, 0, meth->primary_world, -1, 0, 0, jl_nothing, 0); jl_mi_cache_insert(mi, inst); diff --git a/stdlib/REPL/src/REPLCompletions.jl b/stdlib/REPL/src/REPLCompletions.jl index c89cff0db38b30..2c9d8560aed4d0 100644 --- a/stdlib/REPL/src/REPLCompletions.jl +++ b/stdlib/REPL/src/REPLCompletions.jl @@ -565,9 +565,9 @@ function CC.abstract_eval_globalref(interp::REPLInterpreter, g::GlobalRef, sv::CC.InferenceState) if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv)) if CC.isdefined_globalref(g) - return CC.RTEffects(Const(ccall(:jl_get_globalref_value, Any, (Any,), g)), CC.EFFECTS_TOTAL) + return CC.RTEffects(Const(ccall(:jl_get_globalref_value, Any, (Any,), g)), Union{}, CC.EFFECTS_TOTAL) end - return CC.RTEffects(Union{}, CC.EFFECTS_THROWS) + return CC.RTEffects(Union{}, UndefVarError, CC.EFFECTS_THROWS) end return @invoke CC.abstract_eval_globalref(interp::CC.AbstractInterpreter, g::GlobalRef, sv::CC.InferenceState) @@ -609,7 +609,7 @@ function CC.concrete_eval_eligible(interp::REPLInterpreter, @nospecialize(f), sv::CC.InferenceState) if (interp.limit_aggressive_inference ? is_repl_frame(sv) : is_call_graph_uncached(sv)) neweffects = CC.Effects(result.effects; consistent=CC.ALWAYS_TRUE) - result = CC.MethodCallResult(result.rt, result.edgecycle, result.edgelimited, + result = CC.MethodCallResult(result.rt, result.exct, result.edgecycle, result.edgelimited, result.edge, neweffects) end ret = @invoke CC.concrete_eval_eligible(interp::CC.AbstractInterpreter, f::Any, diff --git a/test/compiler/AbstractInterpreter.jl b/test/compiler/AbstractInterpreter.jl index 9580fc55508ac5..121e7fad55c90b 100644 --- a/test/compiler/AbstractInterpreter.jl +++ b/test/compiler/AbstractInterpreter.jl @@ -330,7 +330,7 @@ function CC.abstract_call(interp::NoinlineInterpreter, ret = @invoke CC.abstract_call(interp::CC.AbstractInterpreter, arginfo::CC.ArgInfo, si::CC.StmtInfo, sv::CC.InferenceState, max_methods::Int) if sv.mod in noinline_modules(interp) - return CC.CallMeta(ret.rt, ret.effects, NoinlineCallInfo(ret.info)) + return CC.CallMeta(ret.rt, ret.exct, ret.effects, NoinlineCallInfo(ret.info)) end return ret end diff --git a/test/compiler/EscapeAnalysis/EscapeAnalysis.jl b/test/compiler/EscapeAnalysis/EscapeAnalysis.jl index d8ea8be21fe07b..7366024329ee6d 100644 --- a/test/compiler/EscapeAnalysis/EscapeAnalysis.jl +++ b/test/compiler/EscapeAnalysis/EscapeAnalysis.jl @@ -70,7 +70,7 @@ function is_load_forwardable(x::EscapeInfo) end @testset "EAUtils" begin - @test_throws "everything has been constant folded" code_escapes() do; sin(42); end + @test code_escapes() do; println("prevent ConstABI"); sin(42); end isa EAUtils.EscapeResult @test code_escapes(sin, (Int,)) isa EAUtils.EscapeResult @test code_escapes(sin, (Int,)) isa EAUtils.EscapeResult end diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index ace17baeb5859b..b75e5e5fe87f08 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -4439,8 +4439,8 @@ let x = Tuple{Int,Any}[ #=21=# (0, Expr(:pop_exception, Core.SSAValue(2))) #=22=# (0, Core.ReturnNode(Core.SlotNumber(3))) ] - handler_at = Core.Compiler.compute_trycatch(last.(x), Core.Compiler.BitSet()) - @test handler_at == first.(x) + handler_at, handlers = Core.Compiler.compute_trycatch(last.(x), Core.Compiler.BitSet()) + @test map(x->x[1] == 0 ? 0 : handlers[x[1]].enter_idx, handler_at) == first.(x) end @test only(Base.return_types((Bool,)) do y @@ -5533,3 +5533,28 @@ function test_exit_bottom(s) n end @test only(Base.return_types(test_exit_bottom, Tuple{String})) == Int + +function foo_typed_throw_error() + try + error() + catch e + if isa(e, ErrorException) + return 1.0 + end + end + return 1 +end +@test Base.return_types(foo_typed_throw_error) |> only === Float64 + +will_throw_no_method(x::Int) = 1 +function foo_typed_throw_metherr() + try + will_throw_no_method(1.0) + catch e + if isa(e, MethodError) + return 1.0 + end + end + return 1 +end +@test Base.return_types(foo_typed_throw_metherr) |> only === Float64 diff --git a/test/core.jl b/test/core.jl index 00ab41e4ecd487..c85868c496d919 100644 --- a/test/core.jl +++ b/test/core.jl @@ -14,7 +14,7 @@ include("testenv.jl") # sanity tests that our built-in types are marked correctly for const fields for (T, c) in ( (Core.CodeInfo, []), - (Core.CodeInstance, [:def, :rettype, :rettype_const, :ipo_purity_bits, :analysis_results]), + (Core.CodeInstance, [:def, :rettype, :exctype, :rettype_const, :ipo_purity_bits, :analysis_results]), (Core.Method, [#=:name, :module, :file, :line, :primary_world, :sig, :slot_syms, :external_mt, :nargs, :called, :nospecialize, :nkw, :isva, :is_for_opaque_closure, :constprop=#]), (Core.MethodInstance, [#=:def, :specTypes, :sparam_vals=#]), (Core.MethodTable, [:module]), diff --git a/test/precompile.jl b/test/precompile.jl index bb87e1f6b1dc79..051b750fa7fdfb 100644 --- a/test/precompile.jl +++ b/test/precompile.jl @@ -1684,7 +1684,7 @@ precompile_test_harness("issue #46296") do load_path module CodeInstancePrecompile mi = first(Base.specializations(first(methods(identity)))) - ci = Core.CodeInstance(mi, Any, nothing, nothing, zero(Int32), typemin(UInt), + ci = Core.CodeInstance(mi, Any, Any, nothing, nothing, zero(Int32), typemin(UInt), typemax(UInt), zero(UInt32), zero(UInt32), nothing, 0x00) __init__() = @assert ci isa Core.CodeInstance From 46deabfe414a5cb5be6513e37672810a77098c2b Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sat, 18 Nov 2023 05:24:58 +0000 Subject: [PATCH 2/5] Handle CFG transforms in ADCE --- base/compiler/ssair/ir.jl | 28 +++++++++++----- base/compiler/ssair/irinterp.jl | 58 +++++++++++++++++++++++++-------- base/compiler/ssair/passes.jl | 8 +++-- base/compiler/ssair/verify.jl | 5 ++- 4 files changed, 72 insertions(+), 27 deletions(-) diff --git a/base/compiler/ssair/ir.jl b/base/compiler/ssair/ir.jl index 3f8160a088e8d3..4389b41f65d8e4 100644 --- a/base/compiler/ssair/ir.jl +++ b/base/compiler/ssair/ir.jl @@ -587,6 +587,7 @@ struct CFGTransformState result_bbs::Vector{BasicBlock} bb_rename_pred::Vector{Int} bb_rename_succ::Vector{Int} + domtree::Union{Nothing, DomTree} end # N.B.: Takes ownership of the CFG array @@ -622,11 +623,14 @@ function CFGTransformState!(blocks::Vector{BasicBlock}, allow_cfg_transforms::Bo let blocks = blocks, bb_rename = bb_rename result_bbs = BasicBlock[blocks[i] for i = 1:length(blocks) if bb_rename[i] != -1] end + # TODO: This could be done by just renaming the domtree + domtree = construct_domtree(result_bbs) else bb_rename = Vector{Int}() result_bbs = blocks + domtree = nothing end - return CFGTransformState(allow_cfg_transforms, allow_cfg_transforms, result_bbs, bb_rename, bb_rename) + return CFGTransformState(allow_cfg_transforms, allow_cfg_transforms, result_bbs, bb_rename, bb_rename, domtree) end mutable struct IncrementalCompact @@ -681,7 +685,7 @@ mutable struct IncrementalCompact bb_rename = Vector{Int}() pending_nodes = NewNodeStream() pending_perm = Int[] - return new(code, parent.result, CFGTransformState(false, false, parent.cfg_transform.result_bbs, bb_rename, bb_rename), + return new(code, parent.result, CFGTransformState(false, false, parent.cfg_transform.result_bbs, bb_rename, bb_rename, nothing), ssa_rename, parent.used_ssas, parent.late_fixup, perm, 1, parent.new_new_nodes, parent.new_new_used_ssas, pending_nodes, pending_perm, @@ -1222,19 +1226,25 @@ end # N.B.: from and to are non-renamed indices function kill_edge!(compact::IncrementalCompact, active_bb::Int, from::Int, to::Int) - # Note: We recursively kill as many edges as are obviously dead. However, this - # may leave dead loops in the IR. We kill these later in a CFG cleanup pass (or - # worstcase during codegen). - (; bb_rename_pred, bb_rename_succ, result_bbs) = compact.cfg_transform + # Note: We recursively kill as many edges as are obviously dead. + (; bb_rename_pred, bb_rename_succ, result_bbs, domtree) = compact.cfg_transform preds = result_bbs[bb_rename_succ[to]].preds succs = result_bbs[bb_rename_pred[from]].succs deleteat!(preds, findfirst(x::Int->x==bb_rename_pred[from], preds)::Int) deleteat!(succs, findfirst(x::Int->x==bb_rename_succ[to], succs)::Int) + if domtree !== nothing + domtree_delete_edge!(domtree, result_bbs, bb_rename_pred[from], bb_rename_succ[to]) + end # Check if the block is now dead - if length(preds) == 0 - for succ in copy(result_bbs[bb_rename_succ[to]].succs) - kill_edge!(compact, active_bb, to, findfirst(x::Int->x==succ, bb_rename_pred)::Int) + if length(preds) == 0 || (domtree !== nothing && bb_unreachable(domtree, bb_rename_succ[to])) + to_succs = result_bbs[bb_rename_succ[to]].succs + for succ in copy(to_succs) + new_succ = findfirst(x::Int->x==succ, bb_rename_pred) + new_succ === nothing && continue + kill_edge!(compact, active_bb, to, new_succ) end + empty!(preds) + empty!(to_succs) if to < active_bb # Kill all statements in the block stmts = result_bbs[bb_rename_succ[to]].stmts diff --git a/base/compiler/ssair/irinterp.jl b/base/compiler/ssair/irinterp.jl index c7982869da0049..5b414f8786e98d 100644 --- a/base/compiler/ssair/irinterp.jl +++ b/base/compiler/ssair/irinterp.jl @@ -51,17 +51,24 @@ function abstract_call(interp::AbstractInterpreter, arginfo::ArgInfo, irsv::IRIn return RTEffects(rt, exct, effects) end +function kill_block!(ir, bb) + # Kill the entire block + stmts = ir.cfg.blocks[bb].stmts + for bidx = stmts + inst = ir[SSAValue(bidx)] + inst[:stmt] = nothing + inst[:type] = Bottom + inst[:flag] = IR_FLAG_EFFECT_FREE | IR_FLAG_NOTHROW + end + ir[SSAValue(last(stmts))][:stmt] = ReturnNode() + return +end + + function update_phi!(irsv::IRInterpretationState, from::Int, to::Int) ir = irsv.ir if length(ir.cfg.blocks[to].preds) == 0 - # Kill the entire block - for bidx = ir.cfg.blocks[to].stmts - inst = ir[SSAValue(bidx)] - inst[:stmt] = nothing - inst[:type] = Bottom - inst[:flag] = IR_FLAG_EFFECT_FREE | IR_FLAG_NOTHROW - end - return + kill_block!(ir, to) end for sidx = ir.cfg.blocks[to].stmts stmt = ir[SSAValue(sidx)][:stmt] @@ -83,15 +90,38 @@ function kill_terminator_edges!(irsv::IRInterpretationState, term_idx::Int, bb:: ir = irsv.ir stmt = ir[SSAValue(term_idx)][:stmt] if isa(stmt, GotoIfNot) - kill_edge!(ir, bb, stmt.dest, update_phi!(irsv)) - kill_edge!(ir, bb, bb+1, update_phi!(irsv)) + kill_edge!(irsv, bb, stmt.dest) + kill_edge!(irsv, bb, bb+1) elseif isa(stmt, GotoNode) - kill_edge!(ir, bb, stmt.label, update_phi!(irsv)) + kill_edge!(irsv, bb, stmt.label) elseif isa(stmt, ReturnNode) # Nothing to do else @assert !isexpr(stmt, :enter) - kill_edge!(ir, bb, bb+1, update_phi!(irsv)) + kill_edge!(irsv, bb, bb+1) + end +end + +function kill_edge!(irsv::IRInterpretationState, from::Int, to::Int) + ir = irsv.ir + kill_edge!(ir, from, to, update_phi!(irsv)) + + lazydomtree = irsv.lazydomtree + domtree = nothing + if isdefined(lazydomtree, :domtree) + domtree = get!(lazydomtree) + domtree_delete_edge!(domtree, ir.cfg.blocks, from, to) + elseif length(ir.cfg.blocks[to].preds) != 0 + # TODO: If we're not maintaining the domtree, computing it just for this + # is slightly overkill - just the dfs tree would be enough. + domtree = get!(lazydomtree) + end + + if domtree !== nothing && bb_unreachable(domtree, to) + kill_block!(ir, to) + for edge in ir.cfg.blocks[to].succs + kill_edge!(irsv, to, edge) + end end end @@ -113,10 +143,10 @@ function reprocess_instruction!(interp::AbstractInterpreter, inst::Instruction, if condval inst[:stmt] = nothing inst[:type] = Any - kill_edge!(ir, bb, stmt.dest, update_phi!(irsv)) + kill_edge!(irsv, bb, stmt.dest) else inst[:stmt] = GotoNode(stmt.dest) - kill_edge!(ir, bb, bb+1, update_phi!(irsv)) + kill_edge!(irsv, bb, bb+1) end return true end diff --git a/base/compiler/ssair/passes.jl b/base/compiler/ssair/passes.jl index 85f704e9a076d7..945879ab99e0bb 100644 --- a/base/compiler/ssair/passes.jl +++ b/base/compiler/ssair/passes.jl @@ -382,7 +382,9 @@ function lift_leaves(compact::IncrementalCompact, field::Int, elseif isexpr(def, :new) typ = unwrap_unionall(widenconst(types(compact)[leaf])) (isa(typ, DataType) && !isabstracttype(typ)) || return nothing - @assert !ismutabletype(typ) + if ismutabletype(typ) + isconst(typ, field) || return nothing + end if length(def.args) < 1+field if field > fieldcount(typ) return nothing @@ -1735,7 +1737,7 @@ function adce_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing) phi_uses = fill(0, length(ir.stmts) + length(ir.new_nodes)) all_phis = Int[] unionphis = Pair{Int,Any}[] # sorted - compact = IncrementalCompact(ir) + compact = IncrementalCompact(ir, true) for ((_, idx), stmt) in compact if isa(stmt, PhiNode) push!(all_phis, idx) @@ -2203,7 +2205,7 @@ function cfg_simplify!(ir::IRCode) # Run instruction compaction to produce the result, # but we're messing with the CFG # so we don't want compaction to do so independently - compact = IncrementalCompact(ir, CFGTransformState(true, false, cresult_bbs, bb_rename_pred, bb_rename_succ)) + compact = IncrementalCompact(ir, CFGTransformState(true, false, cresult_bbs, bb_rename_pred, bb_rename_succ, nothing)) result_idx = 1 for (idx, orig_bb) in enumerate(result_bbs) ms = orig_bb diff --git a/base/compiler/ssair/verify.jl b/base/compiler/ssair/verify.jl index f770c72d0fae51..801836c485e31e 100644 --- a/base/compiler/ssair/verify.jl +++ b/base/compiler/ssair/verify.jl @@ -47,7 +47,10 @@ function check_op(ir::IRCode, domtree::DomTree, @nospecialize(op), use_bb::Int, end use_inst = ir[op] - if isa(use_inst[:stmt], Union{GotoIfNot, GotoNode, ReturnNode}) + if isa(use_inst[:stmt], Union{GotoIfNot, GotoNode, ReturnNode}) && !(isa(use_inst[:stmt], ReturnNode) && !isdefined(use_inst[:stmt], :val)) + # Allow uses of `unreachable`, which may have been inserted when + # an earlier block got deleted, but for some reason we didn't figure + # out yet that this entire block is dead also. @verify_error "At statement %$use_idx: Invalid use of value statement or terminator %$(op.id)" error("") end From 1704053ae6d6ba1ba76c86fe64a06f556695310a Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sat, 18 Nov 2023 07:30:50 +0000 Subject: [PATCH 3/5] Address Shuhei's review --- base/compiler/abstractinterpretation.jl | 20 +++++++++++++++---- base/compiler/effects.jl | 14 ++++++++++--- base/compiler/inferencestate.jl | 1 + base/compiler/tfuncs.jl | 4 ++-- src/opaque_closure.c | 2 +- .../compiler/EscapeAnalysis/EscapeAnalysis.jl | 2 +- 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 03174aeed3e5a9..753eb4c6a13b77 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -71,7 +71,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), rt = const_call_result.rt exct = const_call_result.exct (; effects, const_result, edge) = const_call_result - elseif better_effects(const_call_result.effects, effects) + elseif is_better_effects(const_call_result.effects, effects) exct = const_call_result.exct (; effects, const_result, edge) = const_call_result elseif !(exct ⊑ₚ const_call_result.exct) @@ -120,7 +120,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), this_rt = this_const_rt this_exct = const_call_result.exct (; effects, const_result, edge) = const_call_result - elseif better_effects(const_call_result.effects, effects) + elseif is_better_effects(const_call_result.effects, effects) this_exct = const_call_result.exct (; effects, const_result, edge) = const_call_result elseif !(this_exct ⊑ₚ const_call_result.exct) @@ -2043,7 +2043,19 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), elseif f === applicable return abstract_applicable(interp, argtypes, sv, max_methods) elseif f === throw - return CallMeta(Union{}, la == 2 ? argtypes[2] : ArgumentError, EFFECTS_THROWS, NoCallInfo()) + if la == 2 + arg2 = argtypes[2] + if isvarargtype(arg2) + exct = tmerge(𝕃ᵢ, unwrapva(argtypes[2]), ArgumentError) + else + exct = arg2 + end + elseif la == 3 && isvarargtype(argtypes[3]) + exct = tmerge(𝕃ᵢ, argtypes[2], ArgumentError) + else + exct = ArgumentError + end + return CallMeta(Union{}, exct, EFFECTS_THROWS, NoCallInfo()) end rt = abstract_call_builtin(interp, f, arginfo, sv) ft = popfirst!(argtypes) @@ -2372,7 +2384,7 @@ end function abstract_eval_the_exception(interp::AbstractInterpreter, sv::InferenceState) return sv.handlers[sv.handler_at[sv.currpc][2]].exct end -abstract_eval_the_exception(interp, sv) = Any +abstract_eval_the_exception(::AbstractInterpreter, ::IRInterpretationState) = Any function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtypes::Union{VarTable,Nothing}, sv::AbsIntState) diff --git a/base/compiler/effects.jl b/base/compiler/effects.jl index 4b5f3b392107a4..d4c972ab89e951 100644 --- a/base/compiler/effects.jl +++ b/base/compiler/effects.jl @@ -171,12 +171,20 @@ function Effects(effects::Effects = _EFFECTS_UNKNOWN; nonoverlayed) end -function better_effects(new::Effects, old::Effects) +function is_better_effects(new::Effects, old::Effects) any_improved = false if new.consistent == ALWAYS_TRUE any_improved |= old.consistent != ALWAYS_TRUE - elseif new.consistent != old.consistent - return false + else + if !iszero(new.consistent & CONSISTENT_IF_NOTRETURNED) + old.consistent == ALWAYS_TRUE && return false + any_improved |= iszero(old.consistent & CONSISTENT_IF_NOTRETURNED) + elseif !iszero(new.consistent & CONSISTENT_IF_INACCESSIBLEMEMONLY) + old.consistent == ALWAYS_TRUE && return false + any_improved |= iszero(old.consistent & CONSISTENT_IF_INACCESSIBLEMEMONLY) + else + return false + end end if new.effect_free == ALWAYS_TRUE any_improved |= old.consistent != ALWAYS_TRUE diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index 5f9855372ff08b..07c20521ab67c9 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -206,6 +206,7 @@ const CACHE_MODE_VOLATILE = 0x01 << 2 # not cached, optimization allowed mutable struct TryCatchFrame exct const enter_idx + TryCatchFrame(@nospecialize(exct), enter_idx::Int) = new(exct, enter_idx) end mutable struct InferenceState diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index c549f3f05959e8..97d8bf90c060da 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -2771,7 +2771,7 @@ function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, s end if contains_is(argtypes_vec, Union{}) - return CallMeta(Const(Union{}), Any, EFFECTS_TOTAL, NoCallInfo()) + return CallMeta(Const(Union{}), Union{}, EFFECTS_TOTAL, NoCallInfo()) end # Run the abstract_call without restricting abstract call @@ -2789,7 +2789,7 @@ function return_type_tfunc(interp::AbstractInterpreter, argtypes::Vector{Any}, s rt = widenslotwrapper(call.rt) if isa(rt, Const) # output was computed to be constant - return CallMeta(Const(typeof(rt.val)), Any, EFFECTS_TOTAL, info) + return CallMeta(Const(typeof(rt.val)), Union{}, EFFECTS_TOTAL, info) end rt = widenconst(rt) if rt === Bottom || (isconcretetype(rt) && !iskindtype(rt)) diff --git a/src/opaque_closure.c b/src/opaque_closure.c index 8d8698c67ed9a1..7fd6d5a0f86662 100644 --- a/src/opaque_closure.c +++ b/src/opaque_closure.c @@ -150,7 +150,7 @@ JL_DLLEXPORT jl_opaque_closure_t *jl_new_opaque_closure_from_code_info(jl_tuplet sigtype = jl_argtype_with_function(env, (jl_value_t*)argt); jl_method_instance_t *mi = jl_specializations_get_linfo((jl_method_t*)root, sigtype, jl_emptysvec); - inst = jl_new_codeinst(mi, rt_ub, NULL, NULL, (jl_value_t*)ci, + inst = jl_new_codeinst(mi, rt_ub, (jl_value_t*)jl_any_type, NULL, (jl_value_t*)ci, 0, meth->primary_world, -1, 0, 0, jl_nothing, 0); jl_mi_cache_insert(mi, inst); diff --git a/test/compiler/EscapeAnalysis/EscapeAnalysis.jl b/test/compiler/EscapeAnalysis/EscapeAnalysis.jl index 7366024329ee6d..d8ea8be21fe07b 100644 --- a/test/compiler/EscapeAnalysis/EscapeAnalysis.jl +++ b/test/compiler/EscapeAnalysis/EscapeAnalysis.jl @@ -70,7 +70,7 @@ function is_load_forwardable(x::EscapeInfo) end @testset "EAUtils" begin - @test code_escapes() do; println("prevent ConstABI"); sin(42); end isa EAUtils.EscapeResult + @test_throws "everything has been constant folded" code_escapes() do; sin(42); end @test code_escapes(sin, (Int,)) isa EAUtils.EscapeResult @test code_escapes(sin, (Int,)) isa EAUtils.EscapeResult end From adff8e74c402d062fe37d1aaea08dd51769b5668 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 19 Nov 2023 11:33:18 +0000 Subject: [PATCH 4/5] perf: Only run final commit if adce_pass! made any intersting changes adce_pass! runs its own compaction, so we only need to run another run of compaction if it made any post-compaction changes. --- base/compiler/inferencestate.jl | 2 +- base/compiler/optimize.jl | 6 ++++-- base/compiler/ssair/ir.jl | 8 ++++++++ base/compiler/ssair/passes.jl | 12 ++++++++---- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index 07c20521ab67c9..772dbe72d4dc1d 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -205,7 +205,7 @@ const CACHE_MODE_VOLATILE = 0x01 << 2 # not cached, optimization allowed mutable struct TryCatchFrame exct - const enter_idx + const enter_idx::Int TryCatchFrame(@nospecialize(exct), enter_idx::Int) = new(exct, enter_idx) end diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 514f2e03c2cae9..a0cf9758ac255a 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -925,8 +925,10 @@ function run_passes_ipo_safe( # @timeit "verify 2" verify_ir(ir) @pass "compact 2" ir = compact!(ir) @pass "SROA" ir = sroa_pass!(ir, sv.inlining) - @pass "ADCE" ir = adce_pass!(ir, sv.inlining) - @pass "compact 3" ir = compact!(ir, true) + @pass "ADCE" (ir, made_changes) = adce_pass!(ir, sv.inlining) + if made_changes + @pass "compact 3" ir = compact!(ir, true) + end if JLOptions().debug_level == 2 @timeit "verify 3" (verify_ir(ir, true, false, optimizer_lattice(sv.inlining.interp)); verify_linetable(ir.linetable)) end diff --git a/base/compiler/ssair/ir.jl b/base/compiler/ssair/ir.jl index 4389b41f65d8e4..ef49f638e1c57a 100644 --- a/base/compiler/ssair/ir.jl +++ b/base/compiler/ssair/ir.jl @@ -946,6 +946,14 @@ function insert_node_here!(compact::IncrementalCompact, newinst::NewInstruction, return inst end +function delete_inst_here!(compact) + # Delete the statement, update refcounts etc + compact[compact.idx-1] = nothing + # Pretend that we never compacted this statement in the first place + compact.idx -= 1 + return nothing +end + function getindex(view::TypesView, v::OldSSAValue) id = v.id ir = view.ir.ir diff --git a/base/compiler/ssair/passes.jl b/base/compiler/ssair/passes.jl index 945879ab99e0bb..e22213d4a0c3bd 100644 --- a/base/compiler/ssair/passes.jl +++ b/base/compiler/ssair/passes.jl @@ -1738,6 +1738,7 @@ function adce_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing) all_phis = Int[] unionphis = Pair{Int,Any}[] # sorted compact = IncrementalCompact(ir, true) + made_changes = false for ((_, idx), stmt) in compact if isa(stmt, PhiNode) push!(all_phis, idx) @@ -1759,7 +1760,7 @@ function adce_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing) # nullify safe `typeassert` calls ty, isexact = instanceof_tfunc(argextype(stmt.args[3], compact), true) if isexact && ⊑(𝕃ₒ, argextype(stmt.args[2], compact), ty) - compact[idx] = nothing + delete_inst_here!(compact) continue end end @@ -1801,6 +1802,7 @@ function adce_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing) if t === Union{} stmt = compact[SSAValue(phi)][:stmt]::PhiNode kill_phi!(compact, phi_uses, 1:length(stmt.values), SSAValue(phi), stmt, true) + made_changes = true continue elseif t === Any continue @@ -1822,16 +1824,17 @@ function adce_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing) end compact.result[phi][:type] = t kill_phi!(compact, phi_uses, to_drop, SSAValue(phi), stmt, false) + made_changes = true end # Perform simple DCE for unused values extra_worklist = Int[] for (idx, nused) in Iterators.enumerate(compact.used_ssas) idx >= compact.result_idx && break nused == 0 || continue - adce_erase!(phi_uses, extra_worklist, compact, idx, false) + made_changes |= adce_erase!(phi_uses, extra_worklist, compact, idx, false) end while !isempty(extra_worklist) - adce_erase!(phi_uses, extra_worklist, compact, pop!(extra_worklist), true) + made_changes |= adce_erase!(phi_uses, extra_worklist, compact, pop!(extra_worklist), true) end # Go back and erase any phi cycles changed = true @@ -1852,10 +1855,11 @@ function adce_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing) while !isempty(extra_worklist) if adce_erase!(phi_uses, extra_worklist, compact, pop!(extra_worklist), true) changed = true + made_changes = true end end end - return complete(compact) + return Pair{IRCode, Bool}(complete(compact), made_changes) end function is_bb_empty(ir::IRCode, bb::BasicBlock) From fdfc6e448616ab6101caf3f59a42dc1601aa85fe Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 19 Nov 2023 12:48:27 +0000 Subject: [PATCH 5/5] Undo change that dropped const-propped information This PR included a change that only used constant results if the result was *better*. However, this is not what we want, because even though the result may have been the same, the IR we computed will likely still have more precise (and fewer) statements, because we optimized a bunch of things away based on constant information. --- base/compiler/abstractinterpretation.jl | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 753eb4c6a13b77..91e4d6b52bbb86 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -67,7 +67,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), result, f, this_arginfo, si, match, sv) const_result = volatile_inf_result if const_call_result !== nothing - if !(rt ⊑ₚ const_call_result.rt) + if const_call_result.rt ⊑ₚ rt rt = const_call_result.rt exct = const_call_result.exct (; effects, const_result, edge) = const_call_result @@ -76,7 +76,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), (; effects, const_result, edge) = const_call_result elseif !(exct ⊑ₚ const_call_result.exct) exct = const_call_result.exct - (; effects, const_result, edge) = const_call_result + (; const_result, edge) = const_call_result else add_remark!(interp, sv, "[constprop] Discarded because the result was wider than inference") end @@ -113,9 +113,14 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), if const_call_result !== nothing this_const_conditional = ignorelimited(const_call_result.rt) this_const_rt = widenwrappedconditional(const_call_result.rt) - # return type of const-prop' inference can be wider than that of non const-prop' inference - # e.g. in cases when there are cycles but cached result is still accurate - if !(this_rt ⊑ₚ this_const_rt) + if this_const_rt ⊑ₚ this_rt + # As long as the const-prop result we have is not *worse* than + # what we found out on types, we'd like to use it. Even if the + # end result is exactly equivalent, it is likely that the IR + # we produced while constproping is better than that with + # generic types. + # Return type of const-prop' inference can be wider than that of non const-prop' inference + # e.g. in cases when there are cycles but cached result is still accurate this_conditional = this_const_conditional this_rt = this_const_rt this_exct = const_call_result.exct @@ -125,7 +130,7 @@ function abstract_call_gf_by_type(interp::AbstractInterpreter, @nospecialize(f), (; effects, const_result, edge) = const_call_result elseif !(this_exct ⊑ₚ const_call_result.exct) this_exct = const_call_result.exct - (; effects, const_result, edge) = const_call_result + (; const_result, edge) = const_call_result else add_remark!(interp, sv, "[constprop] Discarded because the result was wider than inference") end