diff --git a/Compiler/src/optimize.jl b/Compiler/src/optimize.jl index 1c02bd67b5bd4a..751432229f60dc 100644 --- a/Compiler/src/optimize.jl +++ b/Compiler/src/optimize.jl @@ -669,12 +669,13 @@ end function refine_effects!(interp::AbstractInterpreter, opt::OptimizationState, sv::PostOptAnalysisState) if !is_effect_free(sv.result.ipo_effects) && sv.all_effect_free && !isempty(sv.ea_analysis_pending) - ir = sv.ir - nargs = Int(opt.src.nargs) - estate = EscapeAnalysis.analyze_escapes(ir, nargs, optimizer_lattice(interp), get_escape_cache(interp)) - argescapes = EscapeAnalysis.ArgEscapeCache(estate) - stack_analysis_result!(sv.result, argescapes) - validate_mutable_arg_escapes!(estate, sv) + # ir = sv.ir + # nargs = Int(opt.src.nargs) + # eresult = EscapeAnalysis.analyze_escapes(ir, nargs, optimizer_lattice(interp), get_escape_cache(interp)) + # argescapes = EscapeAnalysis.ArgEscapeCache(eresult) + # stack_analysis_result!(sv.result, argescapes) + # validate_mutable_arg_escapes!(eresult, sv) + sv.all_effect_free = false end any_refinable(sv) || return false @@ -716,7 +717,7 @@ function iscall_with_boundscheck(@nospecialize(stmt), sv::PostOptAnalysisState) end function check_all_args_noescape!(sv::PostOptAnalysisState, ir::IRCode, @nospecialize(stmt), - estate::EscapeAnalysis.EscapeState) + eresult::EscapeAnalysis.EscapeResult) stmt isa Expr || return false if isexpr(stmt, :invoke) startidx = 2 @@ -735,7 +736,7 @@ function check_all_args_noescape!(sv::PostOptAnalysisState, ir::IRCode, @nospeci end # See if we can find the allocation if isa(arg, Argument) - if has_no_escape(estate[arg]) + if has_no_escape(eresult[arg]) # Even if we prove everything else effect_free, the best we can # say is :effect_free_if_argmem_only if sv.effect_free_if_argmem_only === nothing @@ -746,8 +747,8 @@ function check_all_args_noescape!(sv::PostOptAnalysisState, ir::IRCode, @nospeci end return false elseif isa(arg, SSAValue) - has_no_escape(estate[arg]) || return false - check_all_args_noescape!(sv, ir, ir[arg][:stmt], estate) || return false + has_no_escape(eresult[arg]) || return false + check_all_args_noescape!(sv, ir, ir[arg][:stmt], eresult) || return false else return false end @@ -755,14 +756,14 @@ function check_all_args_noescape!(sv::PostOptAnalysisState, ir::IRCode, @nospeci return true end -function validate_mutable_arg_escapes!(estate::EscapeAnalysis.EscapeState, sv::PostOptAnalysisState) +function validate_mutable_arg_escapes!(eresult::EscapeAnalysis.EscapeResult, sv::PostOptAnalysisState) ir = sv.ir for idx in sv.ea_analysis_pending # See if any mutable memory was allocated in this function and determined # not to escape. inst = ir[SSAValue(idx)] stmt = inst[:stmt] - if !check_all_args_noescape!(sv, ir, stmt, estate) + if !check_all_args_noescape!(sv, ir, stmt, eresult) return sv.all_effect_free = false end end diff --git a/Compiler/src/ssair/EscapeAnalysis.jl b/Compiler/src/ssair/EscapeAnalysis.jl index 68a7aef1cbba47..3442ffccc25564 100644 --- a/Compiler/src/ssair/EscapeAnalysis.jl +++ b/Compiler/src/ssair/EscapeAnalysis.jl @@ -4,6 +4,7 @@ export analyze_escapes, getaliases, isaliased, + is_load_forwardable, has_no_escape, has_arg_escape, has_return_escape, @@ -13,21 +14,21 @@ export using Base: Base # imports -import Base: ==, copy, getindex, setindex! +import Base: ==, ∈, copy, delete!, getindex, isempty, setindex! # usings using Core: Builtin, IntrinsicFunction, SimpleVector, ifelse, sizeof using Core.IR using Base: # Base definitions @__MODULE__, @assert, @eval, @goto, @inbounds, @inline, @label, @noinline, @nospecialize, @specialize, BitSet, IdDict, IdSet, UnitRange, Vector, - delete!, empty!, enumerate, first, get, get!, hasintersect, haskey, isassigned, - isempty, length, max, min, missing, println, push!, pushfirst!, - !, !==, &, *, +, -, :, <, <<, >, |, ∈, ∉, ∩, ∪, ≠, ≤, ≥, ⊆ + _bits_findnext, copy!, empty!, enumerate, fill!, first, get, hasintersect, haskey, + isassigned, isexpr, last, length, max, min, missing, only, println, push!, pushfirst!, + resize!, :, !, !==, <, <<, >, =>, ≠, ≤, ≥, ∉, ⊆, ⊇, &, *, +, -, | using ..Compiler: # Compiler specific definitions - AbstractLattice, Compiler, IRCode, IR_FLAG_NOTHROW, - argextype, fieldcount_noerror, has_flag, intrinsic_nothrow, is_meta_expr_head, - is_identity_free_argtype, isexpr, setfield!_nothrow, singleton_type, try_compute_field, - try_compute_fieldidx, widenconst + @show, AbstractLattice, Compiler, HandlerInfo, IRCode, IR_FLAG_NOTHROW, SimpleHandler, + argextype, block_for_inst, compute_trycatch, fieldcount_noerror, gethandler, has_flag, + intrinsic_nothrow, is_meta_expr_head, is_identity_free_argtype, isterminator, + singleton_type, try_compute_field, try_compute_fieldidx, widenconst function include(x::String) if !isdefined(Base, :end_base_include) @@ -39,13 +40,116 @@ end include("disjoint_set.jl") -const AInfo = IdSet{Any} +@nospecialize + +abstract type MemoryInfo end +struct UninitializedMemory <: MemoryInfo end +const AliasedValues = IdSet{Any} +struct AliasedMemory <: MemoryInfo + alias::Any # anything that is valid as IR elements (e.g. `SSAValue`, `Argument`, `GlobalRef`, literals), or `AliasedValues` of them + maybeundef::Bool # required when `AliasedMemory` is merged with `UninitializedMemory` +end +x::MemoryInfo == y::MemoryInfo = begin + if x === UninitializedMemory() + return y === UninitializedMemory() + else + x = x::AliasedMemory + y isa AliasedMemory || return false + return x.alias == y.alias && x.maybeundef == y.maybeundef + end +end +function copy(MemoryInfo::MemoryInfo) + if MemoryInfo isa AliasedMemory + (; alias, maybeundef) = MemoryInfo + return AliasedMemory(alias isa AliasedValues ? copy(alias) : alias, maybeundef) + end + return MemoryInfo +end + +abstract type ObjectInfo end +struct HasUnanalyzedMemory <: ObjectInfo end +struct HasIndexableMemory <: ObjectInfo + MemoryInfos::Vector{MemoryInfo} +end +struct HasUnknownMemory <: ObjectInfo end +const ⊥ₒ, ⊤ₒ = HasUnanalyzedMemory(), HasUnknownMemory() +x::ObjectInfo == y::ObjectInfo = begin + x === y && return true + if x === ⊥ₒ + return y === ⊥ₒ + elseif x === ⊤ₒ + return y === ⊤ₒ + else + x = x::HasIndexableMemory + y isa HasIndexableMemory || return false + return x.MemoryInfos == y.MemoryInfos + end +end +function copy(ObjectInfo::ObjectInfo) + if ObjectInfo isa HasIndexableMemory + return HasIndexableMemory( + MemoryInfo[copy(MemoryInfo) for MemoryInfo in ObjectInfo.MemoryInfos]) + end + return ObjectInfo +end + +abstract type Liveness end +struct BotLiveness <: Liveness end +struct PCLiveness <: Liveness + pcs::BitSet +end +PCLiveness(pc::Int) = PCLiveness(BitSet(pc)) +struct TopLiveness <: Liveness end +const ⊥ₗ, ⊤ₗ = BotLiveness(), TopLiveness() +x::Liveness == y::Liveness = begin + if x === ⊥ₗ + return y === ⊥ₗ + elseif x === ⊤ₗ + return y === ⊤ₗ + else + x = x::PCLiveness + y isa PCLiveness || return false + return x.pcs == y.pcs + end +end +pc::Int ∈ Liveness::Liveness = begin + if Liveness === ⊤ₗ + return true + elseif Liveness === ⊥ₗ + return false + else + Liveness = Liveness::PCLiveness + return pc ∈ Liveness.pcs + end +end +function isempty(Liveness::Liveness) + if Liveness === ⊥ₗ + return true + elseif Liveness === ⊤ₗ + return false + else + return isempty(Liveness.pcs) + end +end +function copy(Liveness::Liveness) + if Liveness isa PCLiveness + return PCLiveness(copy(Liveness.pcs)) + end + return Liveness +end +function delete!(Liveness::Liveness, pc::Int) + if Liveness isa PCLiveness + delete!(Liveness.pcs, pc) + end + return Liveness +end + +@specialize """ x::EscapeInfo A lattice for escape information, which holds the following properties: -- `x.Analyzed::Bool`: not formally part of the lattice, only indicates whether `x` has been analyzed - `x.ReturnEscape::Bool`: indicates `x` can escape to the caller via return - `x.ThrownEscape::BitSet`: records SSA statement numbers where `x` can be thrown as exception: * `isempty(x.ThrownEscape)`: `x` will never be thrown in this call frame (the bottom) @@ -77,145 +181,79 @@ in the same direction as Julia's native type inference routine. An abstract state will be initialized with the bottom(-like) elements: - the call arguments are initialized as `ArgEscape()`, whose `Liveness` property includes `0` to indicate that it is passed as a call argument and visible from a caller immediately -- the other states are initialized as `NotAnalyzed()`, which is a special lattice element that - is slightly lower than `NoEscape`, but at the same time doesn't represent any meaning - other than it's not analyzed yet (thus it's not formally part of the lattice) +- the other states are initialized as `NoEscape()` """ struct EscapeInfo - Analyzed::Bool ReturnEscape::Bool - ThrownEscape::BitSet - AliasInfo #::Union{IndexableFields,Unindexable,Bool} - Liveness::BitSet + ThrownEscape::Bool + ObjectInfo::ObjectInfo + Liveness::Liveness function EscapeInfo( - Analyzed::Bool, ReturnEscape::Bool, - ThrownEscape::BitSet, - AliasInfo#=::Union{IndexableFields,Unindexable,Bool}=#, - Liveness::BitSet) - @nospecialize AliasInfo + ThrownEscape::Bool, + ObjectInfo::ObjectInfo, + Liveness::Liveness) return new( - Analyzed, ReturnEscape, ThrownEscape, - AliasInfo, + ObjectInfo, Liveness) end function EscapeInfo( - x::EscapeInfo, - # non-concrete fields should be passed as default arguments - # in order to avoid allocating non-concrete `NamedTuple`s - AliasInfo#=::Union{IndexableFields,Unindexable,Bool}=# = x.AliasInfo; - Analyzed::Bool = x.Analyzed, + x::EscapeInfo; ReturnEscape::Bool = x.ReturnEscape, - ThrownEscape::BitSet = x.ThrownEscape, - Liveness::BitSet = x.Liveness) - @nospecialize AliasInfo + ThrownEscape::Bool = x.ThrownEscape, + ObjectInfo::ObjectInfo = x.ObjectInfo, + Liveness::Liveness = x.Liveness) return new( - Analyzed, ReturnEscape, ThrownEscape, - AliasInfo, + ObjectInfo, Liveness) end end -# precomputed default values in order to eliminate computations at each callsite - -const BOT_THROWN_ESCAPE = BitSet() -# NOTE the lattice operations should try to avoid actual set computations on this top value, -# and e.g. BitSet(0:1000000) should also work without incurring excessive computations -const TOP_THROWN_ESCAPE = BitSet(-1) +function copy(x::EscapeInfo) + return EscapeInfo( + x.ReturnEscape, + x.ThrownEscape, + copy(x.ObjectInfo), + copy(x.Liveness)) +end -const BOT_LIVENESS = BitSet() -# NOTE the lattice operations should try to avoid actual set computations on this top value, -# and e.g. BitSet(0:1000000) should also work without incurring excessive computations -const TOP_LIVENESS = BitSet(-1:0) -const ARG_LIVENESS = BitSet(0) +const ARG_LIVENESS = PCLiveness(BitSet(0)) -# the constructors -NotAnalyzed() = EscapeInfo(false, false, BOT_THROWN_ESCAPE, false, BOT_LIVENESS) # not formally part of the lattice -NoEscape() = EscapeInfo(true, false, BOT_THROWN_ESCAPE, false, BOT_LIVENESS) -ArgEscape() = EscapeInfo(true, false, BOT_THROWN_ESCAPE, true, ARG_LIVENESS) -ReturnEscape(pc::Int) = EscapeInfo(true, true, BOT_THROWN_ESCAPE, false, BitSet(pc)) -AllReturnEscape() = EscapeInfo(true, true, BOT_THROWN_ESCAPE, false, TOP_LIVENESS) -ThrownEscape(pc::Int) = EscapeInfo(true, false, BitSet(pc), false, BOT_LIVENESS) -AllEscape() = EscapeInfo(true, true, TOP_THROWN_ESCAPE, true, TOP_LIVENESS) +NoEscape() = EscapeInfo(false, false, ⊥ₒ, ⊥ₗ) +ArgEscape() = EscapeInfo(false, false, ⊤ₒ, ARG_LIVENESS) +AllEscape() = EscapeInfo(true, true, ⊤ₒ, ⊤ₗ) -const ⊥, ⊤ = NotAnalyzed(), AllEscape() +const ⊥, ⊤ = NoEscape(), AllEscape() # Convenience names for some ⊑ₑ queries -has_no_escape(x::EscapeInfo) = !x.ReturnEscape && isempty(x.ThrownEscape) && 0 ∉ x.Liveness +has_no_escape(x::EscapeInfo) = !x.ReturnEscape && !x.ThrownEscape && 0 ∉ x.Liveness has_arg_escape(x::EscapeInfo) = 0 ∈ x.Liveness has_return_escape(x::EscapeInfo) = x.ReturnEscape -has_return_escape(x::EscapeInfo, pc::Int) = x.ReturnEscape && (-1 ∈ x.Liveness || pc ∈ x.Liveness) -has_thrown_escape(x::EscapeInfo) = !isempty(x.ThrownEscape) -has_thrown_escape(x::EscapeInfo, pc::Int) = -1 ∈ x.ThrownEscape || pc ∈ x.ThrownEscape -has_all_escape(x::EscapeInfo) = ⊤ ⊑ₑ x +has_return_escape(x::EscapeInfo, pc::Int) = x.ReturnEscape && pc ∈ x.Liveness +has_thrown_escape(x::EscapeInfo) = x.ThrownEscape +has_all_escape(x::EscapeInfo, strict::Bool=false) = + strict ? ⊤ ⊑ₑ x : ignore_thrownescapes(⊤) ⊑ₑ ignore_thrownescapes(x) # utility lattice constructors ignore_argescape(x::EscapeInfo) = EscapeInfo(x; Liveness=delete!(copy(x.Liveness), 0)) -ignore_thrownescapes(x::EscapeInfo) = EscapeInfo(x; ThrownEscape=BOT_THROWN_ESCAPE) -ignore_aliasinfo(x::EscapeInfo) = EscapeInfo(x, false) -ignore_liveness(x::EscapeInfo) = EscapeInfo(x; Liveness=BOT_LIVENESS) - -# AliasInfo -struct IndexableFields - infos::Vector{AInfo} -end -struct Unindexable - info::AInfo -end -IndexableFields(nflds::Int) = IndexableFields(AInfo[AInfo() for _ in 1:nflds]) -Unindexable() = Unindexable(AInfo()) -copy(AliasInfo::IndexableFields) = IndexableFields(AInfo[copy(info) for info in AliasInfo.infos]) -copy(AliasInfo::Unindexable) = Unindexable(copy(AliasInfo.info)) - -merge_to_unindexable(AliasInfo::IndexableFields) = Unindexable(merge_to_unindexable(AliasInfo.infos)) -merge_to_unindexable(AliasInfo::Unindexable, AliasInfos::IndexableFields) = Unindexable(merge_to_unindexable(AliasInfo.info, AliasInfos.infos)) -merge_to_unindexable(infos::Vector{AInfo}) = merge_to_unindexable(AInfo(), infos) -function merge_to_unindexable(info::AInfo, infos::Vector{AInfo}) - for i = 1:length(infos) - info = info ∪ infos[i] - end - return info -end +ignore_thrownescapes(x::EscapeInfo) = EscapeInfo(x; ThrownEscape=false) +ignore_objectinfo(x::EscapeInfo) = EscapeInfo(x; ObjectInfo=⊥ₒ) +ignore_liveness(x::EscapeInfo) = EscapeInfo(x; Liveness=⊥ₗ) # we need to make sure this `==` operator corresponds to lattice equality rather than object equality, # otherwise `propagate_changes` can't detect the convergence x::EscapeInfo == y::EscapeInfo = begin # fast pass: better to avoid top comparison x === y && return true - x.Analyzed === y.Analyzed || return false x.ReturnEscape === y.ReturnEscape || return false - xt, yt = x.ThrownEscape, y.ThrownEscape - if xt === TOP_THROWN_ESCAPE - yt === TOP_THROWN_ESCAPE || return false - elseif yt === TOP_THROWN_ESCAPE - return false # x.ThrownEscape === TOP_THROWN_ESCAPE - else - xt == yt || return false - end - xa, ya = x.AliasInfo, y.AliasInfo - if isa(xa, Bool) - xa === ya || return false - elseif isa(xa, IndexableFields) - isa(ya, IndexableFields) || return false - xa.infos == ya.infos || return false - else - xa = xa::Unindexable - isa(ya, Unindexable) || return false - xa.info == ya.info || return false - end - xl, yl = x.Liveness, y.Liveness - if xl === TOP_LIVENESS - yl === TOP_LIVENESS || return false - elseif yl === TOP_LIVENESS - return false # x.Liveness === TOP_LIVENESS - else - xl == yl || return false - end + x.ThrownEscape === y.ThrownEscape || return false + x.ObjectInfo == y.ObjectInfo || return false + x.Liveness == y.Liveness || return false return true end @@ -226,58 +264,82 @@ The non-strict partial order over [`EscapeInfo`](@ref). """ x::EscapeInfo ⊑ₑ y::EscapeInfo = begin # fast pass: better to avoid top comparison - if y === ⊤ + if x === ⊥ return true elseif x === ⊤ - return false # return y === ⊤ - elseif x === ⊥ - return true + return y === ⊤ elseif y === ⊥ - return false # return x === ⊥ + return false + elseif y === ⊤ + return true end - x.Analyzed ≤ y.Analyzed || return false x.ReturnEscape ≤ y.ReturnEscape || return false - xt, yt = x.ThrownEscape, y.ThrownEscape - if xt === TOP_THROWN_ESCAPE - yt !== TOP_THROWN_ESCAPE && return false - elseif yt !== TOP_THROWN_ESCAPE - xt ⊆ yt || return false - end - xa, ya = x.AliasInfo, y.AliasInfo - if isa(xa, Bool) - xa && ya !== true && return false - elseif isa(xa, IndexableFields) - if isa(ya, IndexableFields) - xinfos, yinfos = xa.infos, ya.infos - xn, yn = length(xinfos), length(yinfos) - xn > yn && return false - for i in 1:xn - xinfos[i] ⊆ yinfos[i] || return false - end - elseif isa(ya, Unindexable) - xinfos, yinfo = xa.infos, ya.info - for i = length(xinfos) - xinfos[i] ⊆ yinfo || return false - end - else - ya === true || return false + x.ThrownEscape ≤ y.ThrownEscape || return false + x.ObjectInfo ⊑ₒ y.ObjectInfo || return false + x.Liveness ⊑ₗ y.Liveness || return false + return true +end + +x::Liveness ⊑ₗ y::Liveness = begin + if x === ⊥ₗ + return true + elseif x === ⊤ₗ + return y === ⊤ₗ + elseif y === ⊥ₗ + return false + elseif y === ⊤ₗ + return true + else + x, y = x::PCLiveness, y::PCLiveness + return x.pcs ⊆ y.pcs + end +end + +x::ObjectInfo ⊑ₒ y::ObjectInfo = begin + if x === ⊥ₒ + return true + elseif x === ⊤ₒ + return y === ⊤ₒ + elseif y === ⊥ₒ + return false + elseif y === ⊤ₒ + return true + else + x, y = x::HasIndexableMemory, y::HasIndexableMemory + xinfos, yinfos = x.MemoryInfos, y.MemoryInfos + xn, yn = length(xinfos), length(yinfos) + xn ≤ yn || return false + for i in 1:xn + xinfos[i] ⊑ₘ yinfos[i] || return false end + return true + end +end + +x::MemoryInfo ⊑ₘ y::MemoryInfo = begin + if x === UninitializedMemory() + return y === UninitializedMemory() + elseif y === UninitializedMemory() + return false else - xa = xa::Unindexable - if isa(ya, Unindexable) - xinfo, yinfo = xa.info, ya.info - xinfo ⊆ yinfo || return false + x, y = x::AliasedMemory, y::AliasedMemory + xa, ya = x.alias, y.alias + if !(xa isa AliasedValues) + if ya isa AliasedValues + xa ∈ ya || return false + else + xa == ya || return false + end + elseif ya isa AliasedValues + xa ⊆ ya || return false else - ya === true || return false + return false end + if x.maybeundef + y.maybeundef || return false + end + return true end - xl, yl = x.Liveness, y.Liveness - if xl === TOP_LIVENESS - yl !== TOP_LIVENESS && return false - elseif yl !== TOP_LIVENESS - xl ⊆ yl || return false - end - return true end """ @@ -297,136 +359,207 @@ where we can safely assume `x ⊑ₑ y` holds. x::EscapeInfo ⋤ₑ y::EscapeInfo = !(y ⊑ₑ x) """ - x::EscapeInfo ⊔ₑ y::EscapeInfo -> EscapeInfo + x::EscapeInfo ⊔ₑꜝ y::EscapeInfo -> (xy::EscapeInfo, changed::Bool) Computes the join of `x` and `y` in the partial order defined by [`EscapeInfo`](@ref). +This operation is destructive and modifies `x` in place. +`changed` indicates whether the join result is different from `x`. """ -x::EscapeInfo ⊔ₑ y::EscapeInfo = begin +x::EscapeInfo ⊔ₑꜝ y::EscapeInfo = begin # fast pass: better to avoid top join - if x === ⊤ || y === ⊤ - return ⊤ - elseif x === ⊥ - return y + if x === ⊥ + if y === ⊥ + return ⊥, false + else + return copy(y), true + end + elseif x === ⊤ + return ⊤, false elseif y === ⊥ - return x - end - xt, yt = x.ThrownEscape, y.ThrownEscape - if xt === TOP_THROWN_ESCAPE || yt === TOP_THROWN_ESCAPE - ThrownEscape = TOP_THROWN_ESCAPE - elseif xt === BOT_THROWN_ESCAPE - ThrownEscape = yt - elseif yt === BOT_THROWN_ESCAPE - ThrownEscape = xt - else - ThrownEscape = xt ∪ yt - end - AliasInfo = merge_alias_info(x.AliasInfo, y.AliasInfo) - xl, yl = x.Liveness, y.Liveness - if xl === TOP_LIVENESS || yl === TOP_LIVENESS - Liveness = TOP_LIVENESS - elseif xl === BOT_LIVENESS - Liveness = yl - elseif yl === BOT_LIVENESS - Liveness = xl - else - Liveness = xl ∪ yl - end - return EscapeInfo( - x.Analyzed | y.Analyzed, - x.ReturnEscape | y.ReturnEscape, + return x, false + elseif y === ⊤ + return ⊤, true # x !== ⊤ + end + changed = false + ReturnEscape = x.ReturnEscape | y.ReturnEscape + changed |= x.ReturnEscape < ReturnEscape + ThrownEscape = x.ThrownEscape | y.ThrownEscape + changed |= x.ThrownEscape < y.ThrownEscape + ObjectInfo, changed′ = x.ObjectInfo ⊔ₒꜝ y.ObjectInfo + changed |= changed′ + Liveness, changed′ = x.Liveness ⊔ₗꜝ y.Liveness + changed |= changed′ + xy = EscapeInfo( + ReturnEscape, ThrownEscape, - AliasInfo, - Liveness, - ) + ObjectInfo, + Liveness) + return xy, changed end +x::EscapeInfo ⊔ₑ y::EscapeInfo = copy(x) ⊔ₑꜝ y -function merge_alias_info(@nospecialize(xa), @nospecialize(ya)) - if xa === true || ya === true - return true - elseif xa === false - return ya - elseif ya === false - return xa - elseif isa(xa, IndexableFields) - if isa(ya, IndexableFields) - xinfos, yinfos = xa.infos, ya.infos - xn, yn = length(xinfos), length(yinfos) - nmax, nmin = max(xn, yn), min(xn, yn) - infos = Vector{AInfo}(undef, nmax) - for i in 1:nmax - if i > nmin - infos[i] = (xn > yn ? xinfos : yinfos)[i] - else - infos[i] = xinfos[i] ∪ yinfos[i] - end +x::Liveness ⊔ₗꜝ y::Liveness = begin + if x === ⊥ₗ + if y === ⊥ₗ + return ⊥ₗ, false + else + return copy(y), true + end + elseif x === ⊤ₗ + return ⊤ₗ, false + elseif y === ⊥ₗ + return x, false + elseif y === ⊤ₗ + return ⊤ₗ, true # x !== ⊤ₗ + end + x, y = x::PCLiveness, y::PCLiveness + if x.pcs ⊇ y.pcs + return x, false + end + union!(x.pcs, y.pcs) + return x, true +end +x::Liveness ⊔ₗ y::Liveness = copy(x) ⊔ₗꜝ y + +x::ObjectInfo ⊔ₒꜝ y::ObjectInfo = begin + if x === ⊥ₒ + if y === ⊥ₒ + return ⊥ₒ, false + else + return copy(y), true + end + elseif x === ⊤ₒ + return ⊤ₒ, false + elseif y === ⊥ₒ + return x, false + elseif y === ⊤ₒ + return ⊤ₒ, true # x !== ⊤ₒ + end + x, y = x::HasIndexableMemory, y::HasIndexableMemory + xinfos, yinfos = x.MemoryInfos, y.MemoryInfos + xn, yn = length(xinfos), length(yinfos) + nmax, nmin = max(xn, yn), min(xn, yn) + changed = false + if xn < nmax + resize!(xinfos, nmax) + changed = true + end + for i in 1:nmax + if nmin < i + if xn < nmax + xinfos[i] = copy(yinfos[i]) end - return IndexableFields(infos) - elseif isa(ya, Unindexable) - xinfos, yinfo = xa.infos, ya.info - return merge_to_unindexable(ya, xa) else - return true # handle conflicting case conservatively + xinfos[i], changed′ = xinfos[i] ⊔ₘꜝ yinfos[i] + changed |= changed′ end - else - xa = xa::Unindexable - if isa(ya, IndexableFields) - return merge_to_unindexable(xa, ya) + end + return x, changed +end +x::ObjectInfo ⊔ₒ y::ObjectInfo = copy(x) ⊔ₒꜝ y + +x::MemoryInfo ⊔ₘꜝ y::MemoryInfo = begin + if x === UninitializedMemory() + if y === UninitializedMemory() + return UninitializedMemory(), false + else + return AliasedMemory(copy(y::AliasedMemory).alias, true), true + end + end + x = x::AliasedMemory + if y === UninitializedMemory() + maybeundef = true + return AliasedMemory(x.alias, maybeundef), x.maybeundef < maybeundef + end + y = y::AliasedMemory + xa, ya = x.alias, y.alias + changed = false + if xa isa AliasedValues + alias = xa + if ya isa AliasedValues + changed = alias ≠ ya + changed && union!(alias, ya) else - ya = ya::Unindexable - xinfo, yinfo = xa.info, ya.info - info = xinfo ∪ yinfo - return Unindexable(info) + changed = ya ∉ alias + changed && push!(alias, ya) end + elseif ya isa AliasedValues + alias = copy(ya) + changed = xa ∉ alias + changed && push!(alias, xa) + else + changed = xa ≠ ya + alias = changed ? AliasedValues((xa, ya)) : xa end + maybeundef = x.maybeundef | y.maybeundef + changed |= x.maybeundef < maybeundef + return AliasedMemory(alias, maybeundef), changed end +x::MemoryInfo ⊔ₘ y::MemoryInfo = copy(x) ⊔ₘꜝ y +const EscapeTable = IdDict{Int,EscapeInfo} # TODO `Dict` would be more efficient? const AliasSet = IntDisjointSet{Int} -""" - estate::EscapeState - -Extended lattice that maps arguments and SSA values to escape information represented as [`EscapeInfo`](@ref). -Escape information imposed on SSA IR element `x` can be retrieved by `estate[x]`. -""" -struct EscapeState - escapes::Vector{EscapeInfo} +struct BlockEscapeState{Sealed#=::Bool=#} + escapes::EscapeTable aliasset::AliasSet nargs::Int end -function EscapeState(nargs::Int, nstmts::Int) - escapes = EscapeInfo[ - 1 ≤ i ≤ nargs ? ArgEscape() : ⊥ for i in 1:(nargs+nstmts)] - aliasset = AliasSet(nargs+nstmts) - return EscapeState(escapes, aliasset, nargs) +function BlockEscapeState(ir::IRCode, nargs::Int) + nstmts = length(ir.stmts) + nelms = nargs + nstmts + escapes = EscapeTable() + aliasset = AliasSet(nelms) + return BlockEscapeState{false}(escapes, aliasset, nargs) +end +function BlockEscapeState{Sealed}(bbstate::BlockEscapeState) where Sealed + return BlockEscapeState{Sealed}(bbstate.escapes, bbstate.aliasset, bbstate.nargs) end -function getindex(estate::EscapeState, @nospecialize(x)) - xidx = iridx(x, estate) - return xidx === nothing ? nothing : estate.escapes[xidx] + +function getindex(bbstate::BlockEscapeState{Sealed}, @nospecialize(x)) where Sealed + xidx = iridx(x, bbstate) + return xidx === nothing ? nothing : bbstate[xidx] end -function setindex!(estate::EscapeState, v::EscapeInfo, @nospecialize(x)) - xidx = iridx(x, estate) +getindex(bbstate::BlockEscapeState{Sealed}, xidx::Int) where Sealed = + get(bbstate.escapes, xidx, xidx ≤ bbstate.nargs ? ArgEscape() : Sealed ? nothing : ⊥) +function setindex!(bbstate::BlockEscapeState, xinfo::EscapeInfo, @nospecialize(x)) + xidx = iridx(x, bbstate) if xidx !== nothing - estate.escapes[xidx] = v + bbstate[xidx] = xinfo end - return estate + return bbstate +end +function setindex!(bbstate::BlockEscapeState{Sealed}, xinfo::EscapeInfo, xidx::Int) where Sealed + Sealed && error("This BlockEscapeState is sealed") + return bbstate.escapes[xidx] = xinfo +end +function copy(bbstate::BlockEscapeState{Sealed}) where Sealed + escapes = EscapeTable(i => copy(x) for (i, x) in bbstate.escapes) + return BlockEscapeState{Sealed}(escapes, copy(bbstate.aliasset), bbstate.nargs) +end +function (bbstate1::BlockEscapeState{Sealed1} == bbstate2::BlockEscapeState{Sealed2}) where {Sealed1,Sealed2} + return Sealed1 == Sealed2 && + bbstate1.escapes == bbstate2.escapes && + bbstate1.aliasset == bbstate2.aliasset && + bbstate1.nargs == bbstate2.nargs end """ - iridx(x, estate::EscapeState) -> xidx::Union{Int,Nothing} + iridx(x, bbstate::BlockEscapeState) -> xidx::Union{Int,Nothing} Tries to convert analyzable IR element `x::Union{Argument,SSAValue}` to -its unique identifier number `xidx` that is valid in the analysis context of `estate`. -Returns `nothing` if `x` isn't maintained by `estate` and thus unanalyzable (e.g. `x::GlobalRef`). +its unique identifier number `xidx` that is valid in the analysis context of `bbstate`. +Returns `nothing` if `x` isn't maintained by `bbstate` and thus unanalyzable (e.g. `x::GlobalRef`). `irval` is the inverse function of `iridx` (not formally), i.e. `irval(iridx(x::Union{Argument,SSAValue}, state), state) === x`. """ -function iridx(@nospecialize(x), estate::EscapeState) +function iridx(@nospecialize(x), bbstate::BlockEscapeState) if isa(x, Argument) xidx = x.n - @assert 1 ≤ xidx ≤ estate.nargs "invalid Argument" + @assert 1 ≤ xidx ≤ bbstate.nargs "invalid Argument" elseif isa(x, SSAValue) - xidx = x.id + estate.nargs + xidx = x.id + bbstate.nargs else return nothing end @@ -434,33 +567,33 @@ function iridx(@nospecialize(x), estate::EscapeState) end """ - irval(xidx::Int, estate::EscapeState) -> x::Union{Argument,SSAValue} + irval(xidx::Int, bbstate::BlockEscapeState) -> x::Union{Argument,SSAValue} Converts its unique identifier number `xidx` to the original IR element `x::Union{Argument,SSAValue}` -that is analyzable in the context of `estate`. +that is analyzable in the context of `bbstate`. `iridx` is the inverse function of `irval` (not formally), i.e. `iridx(irval(xidx, state), state) === xidx`. """ -function irval(xidx::Int, estate::EscapeState) - return xidx > estate.nargs ? SSAValue(xidx-estate.nargs) : Argument(xidx) +function irval(xidx::Int, bbstate::BlockEscapeState) + return xidx > bbstate.nargs ? SSAValue(xidx-bbstate.nargs) : Argument(xidx) end -function getaliases(x::Union{Argument,SSAValue}, estate::EscapeState) - xidx = iridx(x, estate) - aliases = getaliases(xidx, estate) +function getaliases(bbstate::BlockEscapeState, x::Union{Argument,SSAValue}) + xidx = iridx(x, bbstate) + aliases = getaliases(bbstate, xidx) aliases === nothing && return nothing - return Union{Argument,SSAValue}[irval(aidx, estate) for aidx in aliases] + return Union{Argument,SSAValue}[irval(aidx, bbstate) for aidx in aliases] end -function getaliases(xidx::Int, estate::EscapeState) - aliasset = estate.aliasset - root = find_root!(aliasset, xidx) - if xidx ≠ root || aliasset.ranks[xidx] > 0 +function getaliases(bbstate::BlockEscapeState, xidx::Int) + aliasset = bbstate.aliasset + xroot, hasalias = getaliasroot!(aliasset, xidx) + if hasalias # the size of this alias set containing `key` is larger than 1, # collect the entire alias set aliases = Int[] for aidx in 1:length(aliasset.parents) - if aliasset.parents[aidx] == root + if _find_root_impl!(aliasset.parents, aidx) == xroot push!(aliases, aidx) end end @@ -469,11 +602,19 @@ function getaliases(xidx::Int, estate::EscapeState) return nothing end end +@inline function getaliasroot!(aliasset::AliasSet, xidx::Int) + root = find_root!(aliasset, xidx) + if xidx ≠ root || aliasset.ranks[xidx] > 0 + return root, true + else + return root, false + end +end -isaliased(x::Union{Argument,SSAValue}, y::Union{Argument,SSAValue}, estate::EscapeState) = - isaliased(iridx(x, estate), iridx(y, estate), estate) -isaliased(xidx::Int, yidx::Int, estate::EscapeState) = - in_same_set(estate.aliasset, xidx, yidx) +isaliased(bbstate::BlockEscapeState, x::Union{Argument,SSAValue}, y::Union{Argument,SSAValue}) = + isaliased(bbstate, iridx(x, bbstate), iridx(y, bbstate)) +isaliased(bbstate::BlockEscapeState, xidx::Int, yidx::Int) = + in_same_set(bbstate.aliasset, xidx, yidx) struct ArgEscapeInfo escape_bits::UInt8 @@ -495,24 +636,94 @@ has_all_escape(x::ArgEscapeInfo) = x.escape_bits & ARG_ALL_ESCAPE ≠ 0 has_return_escape(x::ArgEscapeInfo) = x.escape_bits & ARG_RETURN_ESCAPE ≠ 0 has_thrown_escape(x::ArgEscapeInfo) = x.escape_bits & ARG_THROWN_ESCAPE ≠ 0 +abstract type Change end +const Changes = Vector{Change} +const SSAMemoryInfo = IdDict{Int,Any} +const SSA_MEMORY_INFO = SSAMemoryInfo() +# TODO CFG-aware `MemoryInfo`: +# By incorporating some form of CFG information into `MemoryInfo`, it becomes possible +# to enable load-forwarding even in cases where conflicts occur by inserting φ-nodes. +struct ConflictedMemory end # a special instance to indicate the aliased memory is conflicted +struct UnknownMemory end # a special instance to indicate the aliased memory is unknown + +struct AnalysisState{GetEscapeCache, Lattice<:AbstractLattice} + ir::IRCode + bbescapes::Vector{Union{Nothing,BlockEscapeState{false}}} + retescape::BlockEscapeState{false} + ssamemoryinfo::SSAMemoryInfo + 𝕃ₒ::Lattice + get_escape_cache::GetEscapeCache + #= temporary states =# + currstate::BlockEscapeState{false} # the state currently being analyzed + changes::Changes # changes made at the current statement + visited::BitSet + equalized_roots::BitSet + handler_info::Union{Nothing,HandlerInfo{SimpleHandler}} +end +function AnalysisState(ir::IRCode, nargs::Int, 𝕃ₒ::AbstractLattice, get_escape_cache) + nbbs = length(ir.cfg.blocks) + bbescapes = fill!(Vector{Union{Nothing,BlockEscapeState{false}}}(undef, nbbs), nothing) + retescape = BlockEscapeState(ir, nargs) + @assert isempty(SSA_MEMORY_INFO) "SSA_MEMORY_INFO is not empty" + ssamemoryinfo = SSA_MEMORY_INFO + currstate = BlockEscapeState(ir, nargs) + changes = Changes() + visited = BitSet() + equalized_roots = BitSet() + handler_info = compute_trycatch(ir) + return AnalysisState(ir, bbescapes, retescape, ssamemoryinfo, 𝕃ₒ, get_escape_cache, + currstate, changes, visited, equalized_roots, handler_info) +end + +""" + eresult::EscapeResult + +Extended lattice that maps arguments and SSA values to escape information represented as [`EscapeInfo`](@ref). +Escape information imposed on SSA IR element `x` can be retrieved by `eresult[x]`. +""" +struct EscapeResult + bbescapes::Vector{Union{Nothing,BlockEscapeState{true}}} + retescape::BlockEscapeState{true} + ssamemoryinfo::Union{Nothing,SSAMemoryInfo} + function EscapeResult(astate::AnalysisState) + bbescapes = Union{Nothing,BlockEscapeState{true}}[bbstate === nothing ? + nothing : BlockEscapeState{true}(bbstate) for bbstate in astate.bbescapes] + retescape = BlockEscapeState{true}(astate.retescape) + if isempty(astate.ssamemoryinfo) + ssamemoryinfo = nothing + else + ssamemoryinfo = copy(astate.ssamemoryinfo) + empty!(astate.ssamemoryinfo) + end + return new(bbescapes, retescape, ssamemoryinfo) + end +end +getindex(eresult::EscapeResult, @nospecialize(x)) = getindex(eresult.retescape, x) +isaliased(eresult::EscapeResult, @nospecialize(x), @nospecialize(y)) = isaliased(eresult.retescape, x, y) +getaliases(eresult::EscapeResult, @nospecialize(x)) = getaliases(eresult.retescape, x) +function is_load_forwardable((; ssamemoryinfo)::EscapeResult, pc::Int) + ssamemoryinfo !== nothing || return false + memoryinfo = ssamemoryinfo[pc] + return memoryinfo !== ConflictedMemory() && memoryinfo !== UnknownMemory() +end + struct ArgAliasing aidx::Int bidx::Int end - struct ArgEscapeCache argescapes::Vector{ArgEscapeInfo} argaliases::Vector{ArgAliasing} - function ArgEscapeCache(estate::EscapeState) - nargs = estate.nargs + function ArgEscapeCache(eresult::EscapeResult) + nargs = eresult.nargs argescapes = Vector{ArgEscapeInfo}(undef, nargs) argaliases = ArgAliasing[] for i = 1:nargs - info = estate.escapes[i] + info = eresult.escapes[i] @assert info.AliasInfo === true argescapes[i] = ArgEscapeInfo(info) for j = (i+1):nargs - if isaliased(i, j, estate) + if isaliased(eresult, i, j) push!(argaliases, ArgAliasing(i, j)) end end @@ -521,35 +732,8 @@ struct ArgEscapeCache end end -abstract type Change end -struct EscapeChange <: Change - xidx::Int - xinfo::EscapeInfo -end -struct AliasChange <: Change - xidx::Int - yidx::Int -end -struct ArgAliasChange <: Change - xidx::Int - yidx::Int -end -struct LivenessChange <: Change - xidx::Int - livepc::Int -end -const Changes = Vector{Change} - -struct AnalysisState{GetEscapeCache, Lattice<:AbstractLattice} - ir::IRCode - estate::EscapeState - changes::Changes - 𝕃ₒ::Lattice - get_escape_cache::GetEscapeCache -end - """ - analyze_escapes(ir::IRCode, nargs::Int, get_escape_cache) -> estate::EscapeState + analyze_escapes(ir::IRCode, nargs::Int, get_escape_cache) -> eresult::EscapeResult Analyzes escape information in `ir`: - `nargs`: the number of actual arguments of the analyzed call @@ -557,297 +741,606 @@ Analyzes escape information in `ir`: retrieves cached argument escape information """ function analyze_escapes(ir::IRCode, nargs::Int, 𝕃ₒ::AbstractLattice, get_escape_cache) - stmts = ir.stmts - nstmts = length(stmts) + length(ir.new_nodes.stmts) - - tryregions = compute_frameinfo(ir) - estate = EscapeState(nargs, nstmts) - changes = Changes() # keeps changes that happen at current statement - astate = AnalysisState(ir, estate, changes, 𝕃ₒ, get_escape_cache) - - local debug_itr_counter = 0 + @assert isempty(ir.new_nodes.stmts) "compacted IRCode is assumed currently" + currbb = 1 + bbs = ir.cfg.blocks + W = BitSet() + astate = AnalysisState(ir, nargs, 𝕃ₒ, get_escape_cache) + (; currstate) = astate while true - local anyupdate = false - - for pc in nstmts:-1:1 + local nextbb::Int + bbstart, bbend = first(bbs[currbb].stmts), last(bbs[currbb].stmts) + for pc = bbstart:bbend stmt = ir[SSAValue(pc)][:stmt] - - # collect escape information - if isa(stmt, Expr) - head = stmt.head - if head === :call - escape_call!(astate, pc, stmt.args) - elseif head === :invoke - escape_invoke!(astate, pc, stmt.args) - elseif head === :new || head === :splatnew - escape_new!(astate, pc, stmt.args) - elseif head === :foreigncall - escape_foreigncall!(astate, pc, stmt.args) - elseif head === :throw_undef_if_not # XXX when is this expression inserted ? - add_escape_change!(astate, stmt.args[1], ThrownEscape(pc)) - elseif is_meta_expr_head(head) - # meta expressions doesn't account for any usages - continue - elseif head === :leave || head === :the_exception || head === :pop_exception - # ignore these expressions since escapes via exceptions are handled by `escape_exception!` - # `escape_exception!` conservatively propagates `AllEscape` anyway, - # and so escape information imposed on `:the_exception` isn't computed - continue - elseif head === :gc_preserve_begin - # GC preserve is handled by `escape_gc_preserve!` - elseif head === :gc_preserve_end - escape_gc_preserve!(astate, pc, stmt.args) - elseif head === :static_parameter || # this exists statically, not interested in its escape - head === :copyast || # XXX escape something? - head === :isdefined # just returns `Bool`, nothing accounts for any escapes - continue - else - add_conservative_changes!(astate, pc, stmt.args) + if pc == bbend + # if this is the last statement of the current block, handle the control-flow + if stmt isa GotoNode + succs = bbs[currbb].succs + @assert length(succs) == 1 "GotoNode with multiple successors" + nextbb = only(succs) + @goto propagate_state + elseif stmt isa GotoIfNot + condval = stmt.cond + if condval === true + @goto fall_through + elseif condval === false + nextbb = falsebb + @goto propagate_state + else + succs = bbs[currbb].succs + if length(succs) == 1 + nextbb = only(succs) + @assert stmt.dest == nextbb + 1 "Invalid GotoIfNot" + @goto propagate_state + end + @assert length(succs) == 2 "GotoIfNot with ≥2 successors" + truebb = currbb + 1 + falsebb = succs[1] == truebb ? succs[2] : succs[1] + if propagate_bbstate!(astate, currstate, falsebb) + push!(W, falsebb) + end + @goto fall_through + end + elseif stmt isa ReturnNode + if isdefined(stmt, :val) + add_return_escape_change!(astate, stmt.val) + if !isempty(astate.changes) + apply_changes!(astate, pc) + empty!(astate.changes) + end + end + propagate_ret_state!(astate, currstate) + @goto next_bb + elseif stmt isa EnterNode + @goto fall_through + elseif isexpr(stmt, :leave) + @goto fall_through end - elseif isa(stmt, EnterNode) - # Handled via escape_exception! - continue - elseif isa(stmt, ReturnNode) - if isdefined(stmt, :val) - add_escape_change!(astate, stmt.val, ReturnEscape(pc)) + # fall through terminator – treat as a regular statement + end + + # process non control-flow statements + escape_stmt!(astate, pc, stmt) + if !isempty(astate.changes) + excstate_excbb = apply_changes!(astate, pc) + empty!(astate.changes) + if excstate_excbb !== nothing + # propagate the escape information of this block to the exception handler block + excstate, excbb = excstate_excbb + if propagate_bbstate!(astate, excstate, excbb) + push!(W, excbb) + end end - elseif isa(stmt, PhiNode) - escape_edges!(astate, pc, stmt.values) - elseif isa(stmt, PiNode) - escape_val_ifdefined!(astate, pc, stmt) - elseif isa(stmt, PhiCNode) - escape_edges!(astate, pc, stmt.values) - elseif isa(stmt, UpsilonNode) - escape_val_ifdefined!(astate, pc, stmt) - elseif isa(stmt, GlobalRef) # global load - add_escape_change!(astate, SSAValue(pc), ⊤) - elseif isa(stmt, SSAValue) - escape_val!(astate, pc, stmt) - elseif isa(stmt, Argument) - escape_val!(astate, pc, stmt) - else # otherwise `stmt` can be GotoNode, GotoIfNot, and inlined values etc. - continue end + end - isempty(changes) && continue + begin @label fall_through + nextbb = currbb + 1 + end - anyupdate |= propagate_changes!(estate, changes) + begin @label propagate_state + if propagate_bbstate!(astate, currstate, nextbb) + push!(W, nextbb) + end + end - empty!(changes) + begin @label next_bb + currbb = _bits_findnext(W.bits, 1) + currbb == -1 && break # the working set is empty + delete!(W, currbb) + nextcurrstate = astate.bbescapes[currbb] + if nextcurrstate === nothing + initialize_state!(currstate, ir, nargs) + else + overwrite_state!(currstate, nextcurrstate) + end end + end - tryregions !== nothing && escape_exception!(astate, tryregions) + return EscapeResult(astate) +end - debug_itr_counter += 1 +function initialize_state!(currstate::BlockEscapeState, ir::IRCode, nargs::Int) + nstmts = length(ir.stmts) + nelms = nargs + nstmts + empty!(currstate.escapes) + resize!(currstate.aliasset.parents, nelms) + for i = 1:nelms + s.parents[i] = i + end + resize!(s.ranks, nelms) + fill!(s.ranks, 0) + s.ngroups = nelms + nothing +end - anyupdate || break +function overwrite_state!(currstate::BlockEscapeState, nextcurrstate::BlockEscapeState) + copy!(currstate.escapes, nextcurrstate.escapes) + currstate.aliasset.parents = copy(nextcurrstate.aliasset.parents) + currstate.aliasset.ranks = copy(nextcurrstate.aliasset.ranks) + currstate.aliasset.ngroups = nextcurrstate.aliasset.ngroups + nothing +end + +function propagate_bbstate!(astate::AnalysisState, currstate::BlockEscapeState, nextbb::Int) + bbescapes = astate.bbescapes + nextstate = bbescapes[nextbb] + if nextstate === nothing + bbescapes[nextbb] = copy(currstate) + return true + else + return propagate_bbstate!(nextstate, currstate) end +end - # if debug_itr_counter > 2 - # println("[EA] excessive iteration count found ", debug_itr_counter, " (", singleton_type(ir.argtypes[1]), ")") - # end +function propagate_bbstate!(nextstate::BlockEscapeState, currstate::BlockEscapeState) + anychanged = false + for (idx, newinfo) in currstate.escapes + if haskey(nextstate.escapes, idx) # this is hot place, so optimize + oldinfo = nextstate.escapes[idx] + newnewinfo, changed = oldinfo ⊔ₑꜝ newinfo + if changed + nextstate.escapes[idx] = newnewinfo + anychanged |= true + end + else + nextstate.escapes[idx] = copy(newinfo) + anychanged |= true + end + end + anychanged |= merge_aliassets!(nextstate.aliasset, currstate.aliasset) + return anychanged +end - return estate +function merge_aliassets!(nextaliasset::AliasSet, curraliasset::AliasSet) + anychanged = false + for xidx = 1:length(curraliasset.parents) + xroot = _find_root_impl!(curraliasset.parents, xidx) + if xroot ≠ xidx + if !in_same_set(nextaliasset, xidx, xroot) + union!(nextaliasset, xidx, xroot) + anychanged |= true + end + end + end + return anychanged end -""" - compute_frameinfo(ir::IRCode) -> tryregions +propagate_ret_state!(astate::AnalysisState, currstate::BlockEscapeState) = + propagate_bbstate!(astate.retescape, currstate) -A preparatory linear scan before the escape analysis on `ir` to find -`tryregions::Union{Nothing,Vector{UnitRange{Int}}}`, that represent regions in which -potential `throw`s can be caught (used by `escape_exception!`) -""" -function compute_frameinfo(ir::IRCode) - nstmts, nnewnodes = length(ir.stmts), length(ir.new_nodes.stmts) - tryregions = nothing - for idx in 1:nstmts+nnewnodes - inst = ir[SSAValue(idx)] - stmt = inst[:stmt] - if isa(stmt, EnterNode) - leave_block = stmt.catch_dest - if leave_block ≠ 0 - @assert idx ≤ nstmts "try/catch inside new_nodes unsupported" - tryregions === nothing && (tryregions = UnitRange{Int}[]) - leave_pc = first(ir.cfg.blocks[leave_block].stmts) - push!(tryregions, idx:leave_pc) +# COMBAK Is the separation of "apply" and "add" changes necessary? + +# Apply +# ===== +# Apply `Change`s and update the current escape information `currstate` + +struct AllEscapeChange <: Change + xidx::Int +end +struct ReturnEscapeChange <: Change + xidx::Int +end +struct ThrownEscapeChange <: Change + xidx::Int +end +struct ObjectInfoChange <: Change + xidx::Int + ObjectInfo::ObjectInfo +end +struct LivenessChange <: Change + xidx::Int +end +struct AliasChange <: Change + xidx::Int + yidx::Int +end + +# apply changes, equalize updated escape information across the aliases sets, +# and also return a new escape state for the exception handler block +# if the current statement is inside a `try` block +function apply_changes!(astate::AnalysisState, pc::Int) + @assert isempty(astate.equalized_roots) "`astate.equalized_roots` should be empty" + nothrow = is_nothrow(astate.ir, pc) + curr_hand = gethandler(astate.handler_info, pc) + for change in astate.changes + if change isa AllEscapeChange + apply_all_escape_change!(astate, change, nothrow) + elseif change isa ReturnEscapeChange + apply_return_escape_change!(astate, change) + elseif change isa ThrownEscapeChange + @assert !nothrow "ThrownEscapeChange in a nothrow statement" + if curr_hand !== nothing + # If there is a handler for this statement, escape information needs to be + # propagated to the exception handler block via `propagate_exct_state!` + # after all alias information has been updated. + # Note that it is not necessary to update the `ThrownEscape` information for + # this current block for this case. This is because if an object is raised, + # the control flow will transition to the exception handler block. Otherwise, + # no exception is raised, thus the `ThrownEscape` information for this + # statement does not need to be propagated to subsequent statements. + continue + else + apply_thrown_escape_change!(astate, change) end + elseif change isa ObjectInfoChange + apply_object_info_change!(astate, change) + elseif change isa LivenessChange + apply_liveness_change!(astate, change, pc) + else + apply_alias_change!(astate, change::AliasChange) end end - return tryregions + equalize_aliased_escapes!(astate) + empty!(astate.equalized_roots) + if !nothrow && curr_hand !== nothing + return make_exc_state(astate, curr_hand.enter_idx) + else + return nothing + end end -# propagate changes, and check convergence -function propagate_changes!(estate::EscapeState, changes::Changes) - local anychanged = false - for change in changes - if isa(change, EscapeChange) - anychanged |= propagate_escape_change!(estate, change) - elseif isa(change, LivenessChange) - anychanged |= propagate_liveness_change!(estate, change) +# When this statement is inside a `try` block with an associated exception handler, +# escape information needs to be propagated to the `catch` block of the exception handler. +# Additionally, the escape information needs to be updated to reflect the possibility that +# objects raised as the exception might be `catch`ed. +# In the current Julia implementation, mechanisms like `rethrow()` or `Expr(:the_exception)` +# allow access to the exception object from anywhere. Consequently, any objects that might +# be raised as exceptions must be assigned the most conservative escape information. +# As a potential future improvement, if effect analysis or similar techniques can guarantee +# that no function calls capable of returning exception objects +# (e.g., `Base.current_exceptions()`) exist within the `catch` block, we could avoid +# propagating the most conservative escape information. Instead, alias information could be +# added between all objects that might be raised as exceptions and the `:the_exception` of +# the `catch` block. +function make_exc_state(astate::AnalysisState, enter_idx::Int) + enter_node = astate.ir[SSAValue(enter_idx)][:stmt]::EnterNode + excstate = copy(astate.currstate) + # update escape information of any objects that might be raised as exception: + # currently their escape information is simply widened to `⊤` (the most conservative information) + for change in astate.changes + local xidx::Int + if change isa ThrownEscapeChange + xidx = change.xidx + elseif change isa AllEscapeChange + xidx = change.xidx + else + continue + end + aliases = getaliases(astate.currstate, xidx) + if aliases !== nothing + for aidx in aliases + excstate[aidx] = ⊤ + end else - change = change::AliasChange - anychanged |= propagate_alias_change!(estate, change) + excstate[xidx] = ⊤ end end - return anychanged + return excstate, enter_node.catch_dest end -@inline propagate_escape_change!(estate::EscapeState, change::EscapeChange) = - propagate_escape_change!(⊔ₑ, estate, change) +function apply_all_escape_change!(astate::AnalysisState, change::AllEscapeChange, nothrow::Bool) + (; xidx) = change + oldinfo = astate.currstate[xidx] + newnewinfo, changed = oldinfo ⊔ₑꜝ EscapeInfo(⊤; ThrownEscape=!nothrow) + if changed + astate.currstate[xidx] = newnewinfo + record_equalized_root!(astate, xidx) + end + nothing +end -# allows this to work as lattice join as well as lattice meet -@inline function propagate_escape_change!(@specialize(op), - estate::EscapeState, change::EscapeChange) - (; xidx, xinfo) = change - anychanged = _propagate_escape_change!(op, estate, xidx, xinfo) - # COMBAK is there a more efficient method of escape information equalization on aliasset? - aliases = getaliases(xidx, estate) - if aliases !== nothing - for aidx in aliases - anychanged |= _propagate_escape_change!(op, estate, aidx, xinfo) - end +function apply_return_escape_change!(astate::AnalysisState, change::ReturnEscapeChange) + (; xidx) = change + xinfo = astate.currstate[xidx] + (; ReturnEscape) = xinfo + if !ReturnEscape + astate.currstate[xidx] = EscapeInfo(xinfo; ReturnEscape=true) + record_equalized_root!(astate, xidx) end - return anychanged end -@inline function _propagate_escape_change!(@specialize(op), - estate::EscapeState, xidx::Int, info::EscapeInfo) - old = estate.escapes[xidx] - new = op(old, info) - if old ≠ new - estate.escapes[xidx] = new - return true +function apply_thrown_escape_change!(astate::AnalysisState, change::ThrownEscapeChange) + (; xidx) = change + xinfo = astate.currstate[xidx] + (; ThrownEscape) = xinfo + if !ThrownEscape + astate.currstate[xidx] = EscapeInfo(xinfo; ThrownEscape=true) + record_equalized_root!(astate, xidx) end - return false end -# propagate Liveness changes separately in order to avoid constructing too many BitSet -@inline function propagate_liveness_change!(estate::EscapeState, change::LivenessChange) - (; xidx, livepc) = change - info = estate.escapes[xidx] - Liveness = info.Liveness - Liveness === TOP_LIVENESS && return false - livepc ∈ Liveness && return false - if Liveness === BOT_LIVENESS || Liveness === ARG_LIVENESS - # if this Liveness is a constant, we shouldn't modify it and propagate this change as a new EscapeInfo - Liveness = copy(Liveness) - push!(Liveness, livepc) - estate.escapes[xidx] = EscapeInfo(info; Liveness) - return true +# COMBAK support weak update +function apply_object_info_change!(astate::AnalysisState, change::ObjectInfoChange) + (; xidx, ObjectInfo) = change + astate.currstate[xidx] = EscapeInfo(astate.currstate[xidx]; ObjectInfo) + record_equalized_root!(astate, xidx) +end + +function apply_liveness_change!(astate::AnalysisState, change::LivenessChange, pc::Int) + (; xidx) = change + xinfo = astate.currstate[xidx] + (; Liveness) = xinfo + if Liveness === ⊤ₗ + return nothing + elseif Liveness === ⊥ₗ + astate.currstate[xidx] = EscapeInfo(xinfo; Liveness=PCLiveness(pc)) else - # directly modify Liveness property in order to avoid excessive copies - push!(Liveness, livepc) - return true + Liveness = Liveness::PCLiveness + if pc ∈ Liveness.pcs + return nothing + end + push!(Liveness.pcs, pc) end + record_equalized_root!(astate, xidx) end -@inline function propagate_alias_change!(estate::EscapeState, change::AliasChange) +function apply_alias_change!(astate::AnalysisState, change::AliasChange) anychange = false (; xidx, yidx) = change - aliasset = estate.aliasset + aliasset = astate.currstate.aliasset xroot = find_root!(aliasset, xidx) yroot = find_root!(aliasset, yidx) if xroot ≠ yroot - union!(aliasset, xroot, yroot) - return true + xroot = union!(aliasset, xroot, yroot) + record_equalized_root!(astate, xroot) + xroot ≠ yroot && record_equalized_root!(astate, yroot) end - return false + nothing end -function add_escape_change!(astate::AnalysisState, @nospecialize(x), xinfo::EscapeInfo, - force::Bool = false) - xinfo === ⊥ && return nothing # performance optimization - xidx = iridx(x, astate.estate) - if xidx !== nothing - if force || !is_identity_free_argtype(argextype(x, astate.ir)) - push!(astate.changes, EscapeChange(xidx, xinfo)) +function record_equalized_root!(astate::AnalysisState, xidx::Int) + xroot, hasalias = getaliasroot!(astate.currstate.aliasset, xidx) + if hasalias + push!(astate.equalized_roots, xroot) + end +end + +function equalize_aliased_escapes!(astate::AnalysisState) + for xroot in astate.equalized_roots + equalize_aliased_escapes!(astate, xroot) + end +end +function equalize_aliased_escapes!(astate::AnalysisState, xroot::Int) + aliases = getaliases(astate.currstate, xroot) + @assert aliases !== nothing "no aliases found" + ainfo = ⊥ + for aidx in aliases + ainfo, _ = ainfo ⊔ₑꜝ astate.currstate[aidx] + end + for aidx in aliases + astate.currstate[aidx] = ainfo + end +end + +# Add +# === +# Store `Change`s for the current escape state that will be applied later + +function add_all_escape_change!(astate::AnalysisState, @nospecialize(x)) + @assert isempty(astate.visited) + _add_all_escape_change!(astate, x) + empty!(astate.visited) +end +function _add_all_escape_change!(astate::AnalysisState, @nospecialize(x)) + with_profitable_irval(astate, x) do xidx::Int + push!(astate.changes, AllEscapeChange(xidx)) + # Propagate the updated information to the field values of `x` + traverse_object_memory(astate, xidx) do @nospecialize aval + _add_all_escape_change!(astate, aval) end end - return nothing end -function add_liveness_change!(astate::AnalysisState, @nospecialize(x), livepc::Int) - xidx = iridx(x, astate.estate) +function add_return_escape_change!(astate::AnalysisState, @nospecialize(x)) + @assert isempty(astate.visited) + _add_return_escape_change!(astate, x) + empty!(astate.visited) +end +function _add_return_escape_change!(astate::AnalysisState, @nospecialize(x)) + with_profitable_irval(astate, x) do xidx::Int + push!(astate.changes, ReturnEscapeChange(xidx)) + push!(astate.changes, LivenessChange(xidx)) + # Propagate the updated information to the field values of `x` + traverse_object_memory(astate, xidx) do @nospecialize aval + _add_return_escape_change!(astate, aval) + end + end +end + +function add_thrown_escape_change!(astate::AnalysisState, @nospecialize(x)) + @assert isempty(astate.visited) + _add_thrown_escape_change!(astate, x) + empty!(astate.visited) +end +function _add_thrown_escape_change!(astate::AnalysisState, @nospecialize(x)) + with_profitable_irval(astate, x) do xidx::Int + push!(astate.changes, ThrownEscapeChange(xidx)) + # Propagate the updated information to the field values of `x` + traverse_object_memory(astate, xidx) do @nospecialize aval + _add_thrown_escape_change!(astate, aval) + end + end +end + +function add_object_info_change!(astate::AnalysisState, @nospecialize(x), ObjectInfo::ObjectInfo) + with_profitable_irval(astate, x) do xidx::Int + push!(astate.changes, ObjectInfoChange(xidx, ObjectInfo)) + if ObjectInfo === ⊤ₒ + # The field values might have been analyzed precisely, but now that is no longer possible: + # Alias `obj` with its field values so that escape information for `obj` is directly + # propagated to the field values. + traverse_object_memory(astate, xidx, #=track_visited=#false) do @nospecialize aval + _add_alias_change!(astate, xidx, aval) + end + end + end +end + +function add_liveness_change!(astate::AnalysisState, @nospecialize(x)) + @assert isempty(astate.visited) + _add_liveness_change!(astate, x) + empty!(astate.visited) +end +function _add_liveness_change!(astate::AnalysisState, @nospecialize(x)) + with_profitable_irval(astate, x) do xidx::Int + push!(astate.changes, LivenessChange(xidx)) + # Propagate the updated information to the field values of `x` + traverse_object_memory(astate, xidx) do @nospecialize aval + _add_liveness_change!(astate, aval) + end + end +end + +function with_profitable_irval(callback, astate::AnalysisState, @nospecialize(x)) + xidx = iridx(x, astate.currstate) if xidx !== nothing if !is_identity_free_argtype(argextype(x, astate.ir)) - push!(astate.changes, LivenessChange(xidx, livepc)) + callback(xidx) end end - return nothing + nothing +end + +function traverse_object_memory(callback, astate::AnalysisState, xidx::Int, + track_visited::Bool=true) + (; ObjectInfo) = astate.currstate[xidx] + if ObjectInfo isa HasIndexableMemory && (!track_visited || xidx ∉ astate.visited) + track_visited && push!(astate.visited, xidx) # avoid infinite traversal for cyclic references + for MemoryInfo in ObjectInfo.MemoryInfos + if MemoryInfo isa AliasedMemory + traverse_aliased_memory(callback, MemoryInfo) + end + end + end +end + +function traverse_aliased_memory(callback, MemoryInfo::AliasedMemory) + alias = MemoryInfo.alias + if alias isa AliasedValues + for aval in alias + callback(aval) + end + else + callback(alias) + end end function add_alias_change!(astate::AnalysisState, @nospecialize(x), @nospecialize(y)) if isa(x, GlobalRef) - return add_escape_change!(astate, y, ⊤) + return add_all_escape_change!(astate, y) elseif isa(y, GlobalRef) - return add_escape_change!(astate, x, ⊤) + return add_all_escape_change!(astate, x) end - estate = astate.estate - xidx = iridx(x, estate) - yidx = iridx(y, estate) + bbstate = astate.currstate + xidx = iridx(x, bbstate) + yidx = iridx(y, bbstate) if xidx !== nothing && yidx !== nothing - if !isaliased(xidx, yidx, astate.estate) - pushfirst!(astate.changes, AliasChange(xidx, yidx)) + if !isaliased(bbstate, xidx, yidx) + push!(astate.changes, AliasChange(xidx, yidx)) end - # add new escape change here so that it's shared among the expanded `aliasset` in `propagate_escape_change!` - xinfo = estate.escapes[xidx] - yinfo = estate.escapes[yidx] - add_escape_change!(astate, x, xinfo ⊔ₑ yinfo, #=force=#true) end return nothing end -struct LocalDef - idx::Int -end -struct LocalUse - idx::Int -end - -function add_alias_escapes!(astate::AnalysisState, @nospecialize(v), ainfo::AInfo) - estate = astate.estate - for x in ainfo - isa(x, LocalUse) || continue # ignore def - x = SSAValue(x.idx) # obviously this won't be true once we implement interprocedural AliasInfo - add_alias_change!(astate, v, x) +function _add_alias_change!(astate::AnalysisState, xidx::Int, @nospecialize(y)) + if isa(y, GlobalRef) + return add_all_escape_change!(astate, x) end -end - -function add_thrown_escapes!(astate::AnalysisState, pc::Int, args::Vector{Any}, - first_idx::Int = 1, last_idx::Int = length(args)) - info = ThrownEscape(pc) - for i in first_idx:last_idx - add_escape_change!(astate, args[i], info) + bbstate = astate.currstate + yidx = iridx(y, bbstate) + if yidx !== nothing + if !isaliased(bbstate, xidx, yidx) + push!(astate.changes, AliasChange(xidx, yidx)) + end end + return nothing end -function add_liveness_changes!(astate::AnalysisState, pc::Int, args::Vector{Any}, - first_idx::Int = 1, last_idx::Int = length(args)) +function add_liveness_changes!(astate::AnalysisState, args::Vector{Any}, + first_idx::Int = 1, last_idx::Int = length(args)) for i in first_idx:last_idx arg = args[i] - add_liveness_change!(astate, arg, pc) + add_liveness_change!(astate, arg) end end -function add_fallback_changes!(astate::AnalysisState, pc::Int, args::Vector{Any}, - first_idx::Int = 1, last_idx::Int = length(args)) - info = ThrownEscape(pc) +function add_fallback_changes!(astate::AnalysisState, args::Vector{Any}, + first_idx::Int = 1, last_idx::Int = length(args)) for i in first_idx:last_idx arg = args[i] - add_escape_change!(astate, arg, info) - add_liveness_change!(astate, arg, pc) + add_thrown_escape_change!(astate, arg) + add_liveness_change!(astate, arg) end end function add_conservative_changes!(astate::AnalysisState, pc::Int, args::Vector{Any}, - first_idx::Int = 1, last_idx::Int = length(args)) + first_idx::Int = 1, last_idx::Int = length(args)) for i in first_idx:last_idx - add_escape_change!(astate, args[i], ⊤) + add_all_escape_change!(astate, args[i]) end - add_escape_change!(astate, SSAValue(pc), ⊤) # it may return GlobalRef etc. + add_all_escape_change!(astate, SSAValue(pc)) # it may return GlobalRef etc. return nothing end +# Escape +# ====== +# Subroutines to analyze the current statement and add `Change`s from it + +function escape_stmt!(astate::AnalysisState, pc::Int, @nospecialize(stmt)) + @assert isempty(astate.changes) "`astate.changes` should have been applied" + if isa(stmt, Expr) + head = stmt.head + if head === :call + escape_call!(astate, pc, stmt.args) + elseif head === :invoke + escape_invoke!(astate, pc, stmt.args) + elseif head === :new || head === :splatnew + escape_new!(astate, pc, stmt.args) + elseif head === :foreigncall + escape_foreigncall!(astate, pc, stmt.args) + elseif is_meta_expr_head(head) + # meta expressions doesn't account for any usages + elseif head === :the_exception || head === :pop_exception + # ignore these expressions since escapes via exceptions are handled by `escape_exception!`: + # `escape_exception!` conservatively propagates `AllEscape` anyway, + # and so escape information imposed on `:the_exception` isn't computed + elseif head === :gc_preserve_begin + # GC preserve is handled by `escape_gc_preserve!` + elseif head === :gc_preserve_end + escape_gc_preserve!(astate, pc, stmt.args) + elseif head === :static_parameter || # this exists statically, not interested in its escape + head === :copyast || # XXX escape something? + head === :isdefined || # returns `Bool`, nothing accounts for any escapes + head === :throw_undef_if_not # may throwx `UndefVarError`, nothing accounts for any escapes + else + @assert head !== :leave "Found unexpected IR element" + add_conservative_changes!(astate, pc, stmt.args) + end + elseif isa(stmt, PhiNode) + escape_edges!(astate, pc, stmt.values) + elseif isa(stmt, PiNode) + if isdefined(stmt, :val) + add_alias_change!(astate, SSAValue(pc), stmt.val) + end + elseif isa(stmt, PhiCNode) + escape_edges!(astate, pc, stmt.values) + elseif isa(stmt, UpsilonNode) + if isdefined(stmt, :val) + add_alias_change!(astate, SSAValue(pc), stmt.val) + end + elseif isa(stmt, GlobalRef) # global load + add_all_escape_change!(astate, SSAValue(pc)) + elseif isa(stmt, SSAValue) + add_alias_change!(astate, SSAValue(pc), stmt) + elseif isa(stmt, Argument) + add_alias_change!(astate, SSAValue(pc), stmt) + else + # otherwise `stmt` can be inlined literal values etc. + @assert !isterminator(stmt) "Found unexpected IR element" + end +end + function escape_edges!(astate::AnalysisState, pc::Int, edges::Vector{Any}) ret = SSAValue(pc) for i in 1:length(edges) @@ -858,90 +1351,8 @@ function escape_edges!(astate::AnalysisState, pc::Int, edges::Vector{Any}) end end -function escape_val_ifdefined!(astate::AnalysisState, pc::Int, x) - if isdefined(x, :val) - escape_val!(astate, pc, x.val) - end -end - -function escape_val!(astate::AnalysisState, pc::Int, @nospecialize(val)) - ret = SSAValue(pc) - add_alias_change!(astate, ret, val) -end - -function escape_unanalyzable_obj!(astate::AnalysisState, @nospecialize(obj), objinfo::EscapeInfo) - objinfo = EscapeInfo(objinfo, true) - add_escape_change!(astate, obj, objinfo) - return objinfo -end - is_nothrow(ir::IRCode, pc::Int) = has_flag(ir[SSAValue(pc)], IR_FLAG_NOTHROW) -""" - escape_exception!(astate::AnalysisState, tryregions::Vector{UnitRange{Int}}) - -Propagates escapes via exceptions that can happen in `tryregions`. - -Naively it seems enough to propagate escape information imposed on `:the_exception` object, -but actually there are several other ways to access to the exception object such as -`Base.current_exceptions` and manual catch of `rethrow`n object. -For example, escape analysis needs to account for potential escape of the allocated object -via `rethrow_escape!()` call in the example below: -```julia -const Gx = Ref{Any}() -@noinline function rethrow_escape!() - try - rethrow() - catch err - Gx[] = err - end -end -unsafeget(x) = isassigned(x) ? x[] : throw(x) - -code_escapes() do - r = Ref{String}() - try - t = unsafeget(r) - catch err - t = typeof(err) # `err` (which `r` may alias to) doesn't escape here - rethrow_escape!() # `r` can escape here - end - return t -end -``` - -As indicated by the above example, it requires a global analysis in addition to a base escape -analysis to reason about all possible escapes via existing exception interfaces correctly. -For now we conservatively always propagate `AllEscape` to all potentially thrown objects, -since such an additional analysis might not be worthwhile to do given that exception handlings -and error paths usually don't need to be very performance sensitive, and optimizations of -error paths might be very ineffective anyway since they are sometimes "unoptimized" -intentionally for latency reasons. -""" -function escape_exception!(astate::AnalysisState, tryregions::Vector{UnitRange{Int}}) - estate = astate.estate - # NOTE if `:the_exception` is the only way to access the exception, we can do: - # exc = SSAValue(pc) - # excinfo = estate[exc] - # TODO? set up a special effect bit that checks the existence of `rethrow` and `current_exceptions` and use it here - excinfo = ⊤ - escapes = estate.escapes - for i in 1:length(escapes) - x = escapes[i] - xt = x.ThrownEscape - xt === TOP_THROWN_ESCAPE && @goto propagate_exception_escape # fast pass - for pc in xt - for region in tryregions - pc ∈ region && @goto propagate_exception_escape # early break because of AllEscape - end - end - continue - @label propagate_exception_escape - xval = irval(i, estate) - add_escape_change!(astate, xval, excinfo) - end -end - # escape statically-resolved call, i.e. `Expr(:invoke, ::MethodInstance, ...)` function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}) mi = first(args) @@ -949,7 +1360,7 @@ function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}) mi = (mi::CodeInstance).def # COMBAK get escape info directly from CI instead? end first_idx, last_idx = 2, length(args) - add_liveness_changes!(astate, pc, args, first_idx, last_idx) + add_liveness_changes!(astate, args, first_idx, last_idx) # TODO inspect `astate.ir.stmts[pc][:info]` and use const-prop'ed `InferenceResult` if available cache = astate.get_escape_cache(mi) ret = SSAValue(pc) @@ -973,7 +1384,7 @@ function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}) end end cache = cache::ArgEscapeCache - retinfo = astate.estate[ret] # escape information imposed on the call statement + retinfo = astate.currstate[ret] # escape information imposed on the call statement method = mi.def::Method nargs = Int(method.nargs) for (i, argidx) in enumerate(first_idx:last_idx) @@ -986,7 +1397,7 @@ function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}) argescape = cache.argescapes[i] info = from_interprocedural(argescape, pc) # propagate the escape information imposed on this call argument by the callee - add_escape_change!(astate, arg, info) + error("TODO") # add_escape_change!(astate, arg, info) if has_return_escape(argescape) # if this argument can be "returned", we should also account for possible # aliasing between this argument and the returned value @@ -997,7 +1408,7 @@ function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}) add_alias_change!(astate, args[aidx+(first_idx-1)], args[bidx+(first_idx-1)]) end # we should disable the alias analysis on this newly introduced object - add_escape_change!(astate, ret, EscapeInfo(retinfo, true)) + error("TODO") # add_escape_change!(astate, ret, EscapeInfo(retinfo, true)) end """ @@ -1008,7 +1419,7 @@ in the context of the caller frame, where `pc` is the SSA statement number of th """ function from_interprocedural(argescape::ArgEscapeInfo, pc::Int) has_all_escape(argescape) && return ⊤ - ThrownEscape = has_thrown_escape(argescape) ? BitSet(pc) : BOT_THROWN_ESCAPE + ThrownEscape = has_thrown_escape(argescape) ? BitSet(pc) : false # TODO implement interprocedural memory effect-analysis: # currently, this essentially disables the entire field analysis–it might be okay from # the SROA point of view, since we can't remove the allocation as far as it's passed to @@ -1016,7 +1427,7 @@ function from_interprocedural(argescape::ArgEscapeInfo, pc::Int) # or some other IPO optimizations AliasInfo = true Liveness = BitSet(pc) - return EscapeInfo(#=Analyzed=#true, #=ReturnEscape=#false, ThrownEscape, AliasInfo, Liveness) + return EscapeInfo(#=ReturnEscape=#false, ThrownEscape, AliasInfo, Liveness) end # escape every argument `(args[6:length(args[3])])` and the name `args[1]` @@ -1033,24 +1444,24 @@ function escape_foreigncall!(astate::AnalysisState, pc::Int, args::Vector{Any}) name = args[1] # NOTE array allocations might have been proven as nothrow (https://github.com/JuliaLang/julia/pull/43565) nothrow = is_nothrow(astate.ir, pc) - name_info = nothrow ? ⊥ : ThrownEscape(pc) - add_escape_change!(astate, name, name_info) - add_liveness_change!(astate, name, pc) + nothrow || add_thrown_escape_change!(astate, name) + add_liveness_change!(astate, name) for i = 1:nargs # we should escape this argument if it is directly called, # otherwise just impose ThrownEscape if not nothrow + arg = args[5+i] if argtypes[i] === Any - arg_info = ⊤ + add_all_escape_change!(astate, arg) + elseif nothrow + add_liveness_change!(astate, arg) else - arg_info = nothrow ? ⊥ : ThrownEscape(pc) + add_thrown_escape_change!(astate, arg) + add_liveness_change!(astate, arg) end - add_escape_change!(astate, args[5+i], arg_info) - add_liveness_change!(astate, args[5+i], pc) end for i = (5+nargs):length(args) arg = args[i] - add_escape_change!(astate, arg, ⊥) - add_liveness_change!(astate, arg, pc) + add_liveness_change!(astate, arg) end end @@ -1062,7 +1473,7 @@ function escape_gc_preserve!(astate::AnalysisState, pc::Int, args::Vector{Any}) @assert isexpr(beginstmt, :gc_preserve_begin) "invalid :gc_preserve_end" beginargs = beginstmt.args # COMBAK we might need to add liveness for all statements from `:gc_preserve_begin` to `:gc_preserve_end` - add_liveness_changes!(astate, pc, beginargs) + add_liveness_changes!(astate, beginargs) end function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any}) @@ -1070,9 +1481,9 @@ function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any}) f = singleton_type(ft) if f isa IntrinsicFunction if is_nothrow(astate.ir, pc) - add_liveness_changes!(astate, pc, args, 2) + add_liveness_changes!(astate, args, 2) else - add_fallback_changes!(astate, pc, args, 2) + add_fallback_changes!(astate, args, 2) end # TODO needs to account for pointer operations? elseif f isa Builtin @@ -1080,12 +1491,10 @@ function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any}) if result === missing # if this call hasn't been handled by any of pre-defined handlers, escape it conservatively add_conservative_changes!(astate, pc, args) - elseif result === true - add_liveness_changes!(astate, pc, args, 2) - elseif is_nothrow(astate.ir, pc) - add_liveness_changes!(astate, pc, args, 2) + elseif result === true || is_nothrow(astate.ir, pc) + add_liveness_changes!(astate, args, 2) else - add_fallback_changes!(astate, pc, args, 2) + add_fallback_changes!(astate, args, 2) end else # escape this generic function or unknown function call conservatively @@ -1127,246 +1536,150 @@ end function escape_builtin!(::typeof(typeassert), astate::AnalysisState, pc::Int, args::Vector{Any}) length(args) == 3 || return false f, obj, typ = args - ret = SSAValue(pc) - add_alias_change!(astate, ret, obj) + add_alias_change!(astate, SSAValue(pc), obj) return false end -function escape_new!(astate::AnalysisState, pc::Int, args::Vector{Any}) +function escape_new!(astate::AnalysisState, pc::Int, args::Vector{Any}, add_liveness::Bool=true) obj = SSAValue(pc) - objinfo = astate.estate[obj] - AliasInfo = objinfo.AliasInfo nargs = length(args) - if isa(AliasInfo, Bool) - AliasInfo && @goto conservative_propagation - # AliasInfo of this object hasn't been analyzed yet: set AliasInfo now - typ = widenconst(argextype(obj, astate.ir)) - nflds = fieldcount_noerror(typ) - if nflds === nothing - AliasInfo = Unindexable() - @goto escape_unindexable_def - else - AliasInfo = IndexableFields(nflds) - @goto escape_indexable_def - end - elseif isa(AliasInfo, IndexableFields) - AliasInfo = copy(AliasInfo) - @label escape_indexable_def - # fields are known precisely: propagate escape information imposed on recorded possibilities to the exact field values - infos = AliasInfo.infos - nf = length(infos) - objinfo′ = ignore_aliasinfo(objinfo) - for i in 2:nargs - i-1 > nf && break # may happen when e.g. ϕ-node merges values with different types - arg = args[i] - add_alias_escapes!(astate, arg, infos[i-1]) - push!(infos[i-1], LocalDef(pc)) - # propagate the escape information of this object ignoring field information - add_escape_change!(astate, arg, objinfo′) - add_liveness_change!(astate, arg, pc) - end - add_escape_change!(astate, obj, EscapeInfo(objinfo, AliasInfo)) # update with new AliasInfo - elseif isa(AliasInfo, Unindexable) - AliasInfo = copy(AliasInfo) - @label escape_unindexable_def - # fields are known partially: propagate escape information imposed on recorded possibilities to all fields values - info = AliasInfo.info - objinfo′ = ignore_aliasinfo(objinfo) + typ = widenconst(argextype(obj, astate.ir)) + nflds = fieldcount_noerror(typ) + if nflds === nothing + # The values stored into the fields can't be analyzed precisely: + # Alias `obj` with its field values so that escape information for `obj` is directly + # propagated to the field values. for i in 2:nargs arg = args[i] - add_alias_escapes!(astate, arg, info) - push!(info, LocalDef(pc)) - # propagate the escape information of this object ignoring field information - add_escape_change!(astate, arg, objinfo′) - add_liveness_change!(astate, arg, pc) + add_alias_change!(astate, obj, arg) + add_liveness && add_liveness_change!(astate, arg) end - add_escape_change!(astate, obj, EscapeInfo(objinfo, AliasInfo)) # update with new AliasInfo + add_object_info_change!(astate, obj, ⊤ₒ) else - # this object has been used as array, but it is allocated as struct here (i.e. should throw) - # update obj's field information and just handle this case conservatively - objinfo = escape_unanalyzable_obj!(astate, obj, objinfo) - @label conservative_propagation - # the fields couldn't be analyzed precisely: propagate the entire escape information - # of this object to all its fields as the most conservative propagation - for i in 2:nargs - arg = args[i] - add_escape_change!(astate, arg, objinfo) - add_liveness_change!(astate, arg, pc) + MemoryInfos = Vector{MemoryInfo}(undef, nflds) + for i = 1:nflds + if i+1 > nargs + MemoryInfo = UninitializedMemory() + else + arg = args[i+1] + MemoryInfo = AliasedMemory(arg, false) + add_liveness && add_liveness_change!(astate, arg) + end + MemoryInfos[i] = MemoryInfo end + add_object_info_change!(astate, obj, HasIndexableMemory(MemoryInfos)) end if !is_nothrow(astate.ir, pc) - add_thrown_escapes!(astate, pc, args) + add_thrown_escape_change!(astate, obj) end end function escape_builtin!(::typeof(tuple), astate::AnalysisState, pc::Int, args::Vector{Any}) - escape_new!(astate, pc, args) - return false -end - -function analyze_fields(ir::IRCode, @nospecialize(typ), @nospecialize(fld)) - nflds = fieldcount_noerror(typ) - if nflds === nothing - return Unindexable(), 0 - end - if isa(typ, DataType) - fldval = try_compute_field(ir, fld) - fidx = try_compute_fieldidx(typ, fldval) - else - fidx = nothing - end - if fidx === nothing - return Unindexable(), 0 - end - return IndexableFields(nflds), fidx -end - -function reanalyze_fields(AliasInfo::IndexableFields, ir::IRCode, @nospecialize(typ), @nospecialize(fld)) - nflds = fieldcount_noerror(typ) - if nflds === nothing - return merge_to_unindexable(AliasInfo), 0 - end - if isa(typ, DataType) - fldval = try_compute_field(ir, fld) - fidx = try_compute_fieldidx(typ, fldval) - else - fidx = nothing - end - if fidx === nothing - return merge_to_unindexable(AliasInfo), 0 - end - AliasInfo = copy(AliasInfo) - infos = AliasInfo.infos - ninfos = length(infos) - if nflds > ninfos - for _ in 1:(nflds-ninfos) - push!(infos, AInfo()) - end - end - return AliasInfo, fidx + # `add_liveness = false` since it will be added in `escape_call!` instead + escape_new!(astate, pc, args, #=add_liveness=#false) + return true end function escape_builtin!(::typeof(getfield), astate::AnalysisState, pc::Int, args::Vector{Any}) length(args) ≥ 3 || return false - ir, estate = astate.ir, astate.estate + ir, bbstate = astate.ir, astate.currstate obj = args[2] typ = widenconst(argextype(obj, ir)) + retval = SSAValue(pc) if hasintersect(typ, Module) # global load - add_escape_change!(astate, SSAValue(pc), ⊤) - end - if isa(obj, SSAValue) || isa(obj, Argument) - objinfo = estate[obj] + @goto unanalyzable_object + elseif isa(obj, SSAValue) || isa(obj, Argument) + objinfo = bbstate[obj] else - # unanalyzable object, so the return value is also unanalyzable - add_escape_change!(astate, SSAValue(pc), ⊤) + @label unanalyzable_object + add_all_escape_change!(astate, obj) + add_all_escape_change!(astate, retval) return false end - AliasInfo = objinfo.AliasInfo - if isa(AliasInfo, Bool) - AliasInfo && @goto conservative_propagation - # AliasInfo of this object hasn't been analyzed yet: set AliasInfo now - AliasInfo, fidx = analyze_fields(ir, typ, args[3]) - if isa(AliasInfo, IndexableFields) - @goto record_indexable_use + nothrow = is_nothrow(astate.ir, pc) + ObjectInfo = objinfo.ObjectInfo + if ObjectInfo isa HasIndexableMemory + fval = try_compute_field(ir, args[3]) + fval === nothing && @goto conservative_propagation + fidx = try_compute_fieldidx(typ, fval) + fidx === nothing && @goto conservative_propagation + @assert length(ObjectInfo.MemoryInfos) ≥ fidx "invalid field index" + MemoryInfo = ObjectInfo.MemoryInfos[fidx] + if MemoryInfo isa UninitializedMemory + # `UndefRefError` should be raised here + astate.ssamemoryinfo[pc] = UninitializedMemory() # TODO CFG-aware `MemoryInfo` else - @goto record_unindexable_use + MemoryInfo = MemoryInfo::AliasedMemory + nothrow |= !MemoryInfo.maybeundef + traverse_aliased_memory(MemoryInfo) do @nospecialize aval + add_alias_change!(astate, retval, aval) + if !haskey(astate.ssamemoryinfo, pc) + if MemoryInfo.maybeundef + astate.ssamemoryinfo[pc] = ConflictedMemory() # TODO CFG-aware `MemoryInfo` + else + astate.ssamemoryinfo[pc] = aval + end + elseif astate.ssamemoryinfo[pc] !== aval + astate.ssamemoryinfo[pc] = ConflictedMemory() # TODO CFG-aware `MemoryInfo` + end + end end - elseif isa(AliasInfo, IndexableFields) - AliasInfo, fidx = reanalyze_fields(AliasInfo, ir, typ, args[3]) - isa(AliasInfo, Unindexable) && @goto record_unindexable_use - @label record_indexable_use - push!(AliasInfo.infos[fidx], LocalUse(pc)) - add_escape_change!(astate, obj, EscapeInfo(objinfo, AliasInfo)) # update with new AliasInfo - elseif isa(AliasInfo, Unindexable) - AliasInfo = copy(AliasInfo) - @label record_unindexable_use - push!(AliasInfo.info, LocalUse(pc)) - add_escape_change!(astate, obj, EscapeInfo(objinfo, AliasInfo)) # update with new AliasInfo else - # this object has been used as array, but it is used as struct here (i.e. should throw) - # update obj's field information and just handle this case conservatively - objinfo = escape_unanalyzable_obj!(astate, obj, objinfo) @label conservative_propagation - # at the extreme case, a field of `obj` may point to `obj` itself - # so add the alias change here as the most conservative propagation - add_alias_change!(astate, obj, SSAValue(pc)) + # the field being read couldn't be analyzed precisely, now we need to: + # 1. mark its `ObjectInfo` as `HasUnknownMemory`, and also + # 2. alias the object to the returned value (since the field may opoint to `obj` itself) + add_object_info_change!(astate, obj, ⊤ₒ) # 1 + add_alias_change!(astate, obj, retval) # 2 + astate.ssamemoryinfo[pc] = UnknownMemory() end - return false + return nothrow end function escape_builtin!(::typeof(setfield!), astate::AnalysisState, pc::Int, args::Vector{Any}) length(args) ≥ 4 || return false - ir, estate = astate.ir, astate.estate + ir, bbstate = astate.ir, astate.currstate obj = args[2] + typ = widenconst(argextype(obj, ir)) val = args[4] - if isa(obj, SSAValue) || isa(obj, Argument) - objinfo = estate[obj] + if hasintersect(typ, Module) # global store + add_all_escape_change!(astate, val) + return false + elseif isa(obj, SSAValue) || isa(obj, Argument) + objinfo = bbstate[obj] else # unanalyzable object (e.g. obj::GlobalRef): escape field value conservatively - add_escape_change!(astate, val, ⊤) - @goto add_thrown_escapes - end - AliasInfo = objinfo.AliasInfo - if isa(AliasInfo, Bool) - AliasInfo && @goto conservative_propagation - # AliasInfo of this object hasn't been analyzed yet: set AliasInfo now - typ = widenconst(argextype(obj, ir)) - AliasInfo, fidx = analyze_fields(ir, typ, args[3]) - if isa(AliasInfo, IndexableFields) - @goto escape_indexable_def - else - @goto escape_unindexable_def - end - elseif isa(AliasInfo, IndexableFields) - typ = widenconst(argextype(obj, ir)) - AliasInfo, fidx = reanalyze_fields(AliasInfo, ir, typ, args[3]) - isa(AliasInfo, Unindexable) && @goto escape_unindexable_def - @label escape_indexable_def - add_alias_escapes!(astate, val, AliasInfo.infos[fidx]) - push!(AliasInfo.infos[fidx], LocalDef(pc)) - objinfo = EscapeInfo(objinfo, AliasInfo) - add_escape_change!(astate, obj, objinfo) # update with new AliasInfo - # propagate the escape information of this object ignoring field information - add_escape_change!(astate, val, ignore_aliasinfo(objinfo)) - elseif isa(AliasInfo, Unindexable) - AliasInfo = copy(AliasInfo) - @label escape_unindexable_def - add_alias_escapes!(astate, val, AliasInfo.info) - push!(AliasInfo.info, LocalDef(pc)) - objinfo = EscapeInfo(objinfo, AliasInfo) - add_escape_change!(astate, obj, objinfo) # update with new AliasInfo - # propagate the escape information of this object ignoring field information - add_escape_change!(astate, val, ignore_aliasinfo(objinfo)) + add_all_escape_change!(astate, val) + return false + end + nothrow = is_nothrow(astate.ir, pc) + ObjectInfo = objinfo.ObjectInfo + if ObjectInfo isa HasIndexableMemory + fval = try_compute_field(ir, args[3]) + fval === nothing && @goto conservative_propagation + fidx = try_compute_fieldidx(typ, fval) + fidx === nothing && @goto conservative_propagation + @assert length(ObjectInfo.MemoryInfos) ≥ fidx "invalid field index" + # COMBAK use `add_object_info_change!` here + # TODO fix for the "may-alias" case + ObjectInfo.MemoryInfos[fidx] = AliasedMemory(val, false) else - # this object has been used as array, but it is used as struct here (i.e. should throw) - # update obj's field information and just handle this case conservatively - objinfo = escape_unanalyzable_obj!(astate, obj, objinfo) @label conservative_propagation - # the field couldn't be analyzed: alias this object to the value being assigned - # as the most conservative propagation (as required for ArgAliasing) - add_alias_change!(astate, val, obj) + # the field being stored couldn't be analyzed precisely, now we need to: + # 1. mark its `ObjectInfo` as `HasUnknownMemory`, and also + # 2. alias the object to the stored value (since the field may opoint to `obj` itself) + add_object_info_change!(astate, obj, ⊤ₒ) # 1 + add_alias_change!(astate, obj, val) # 2 end # also propagate escape information imposed on the return value of this `setfield!` - ssainfo = estate[SSAValue(pc)] - add_escape_change!(astate, val, ssainfo) - # compute the throwness of this setfield! call here since builtin_nothrow doesn't account for that - @label add_thrown_escapes - if length(args) == 4 && setfield!_nothrow(astate.𝕃ₒ, - argextype(args[2], ir), argextype(args[3], ir), argextype(args[4], ir)) - return true - elseif length(args) == 3 && setfield!_nothrow(astate.𝕃ₒ, - argextype(args[2], ir), argextype(args[3], ir)) - return true - else - add_thrown_escapes!(astate, pc, args, 2) - return true - end + add_alias_change!(astate, val, SSAValue(pc)) + return false end function escape_builtin!(::typeof(Core.finalizer), astate::AnalysisState, pc::Int, args::Vector{Any}) if length(args) ≥ 3 obj = args[3] - add_liveness_change!(astate, obj, pc) # TODO setup a proper FinalizerEscape? + add_liveness_change!(astate, obj) # TODO setup a proper FinalizerEscape? end return false end diff --git a/Compiler/src/ssair/disjoint_set.jl b/Compiler/src/ssair/disjoint_set.jl index e000d7e8a582fb..b45ce7c78f6e1d 100644 --- a/Compiler/src/ssair/disjoint_set.jl +++ b/Compiler/src/ssair/disjoint_set.jl @@ -3,7 +3,7 @@ # under the MIT license: https://github.com/JuliaCollections/DataStructures.jl/blob/master/License.md # imports -import Base: length, eltype, union!, push! +import Base: ==, copy, eltype, length, push!, union! # usings using Base: OneTo, collect, zero, zeros, one, typemax @@ -43,6 +43,11 @@ IntDisjointSet(n::T) where {T<:Integer} = IntDisjointSet{T}(collect(OneTo(n)), z IntDisjointSet{T}(n::Integer) where {T<:Integer} = IntDisjointSet{T}(collect(OneTo(T(n))), zeros(T, T(n)), T(n)) length(s::IntDisjointSet) = length(s.parents) +copy(s::IntDisjointSet) = IntDisjointSet(copy(s.parents), copy(s.ranks), s.ngroups) + +s1::IntDisjointSet == s2::IntDisjointSet = + s1.parents == s2.parents && s1.ranks == s2.ranks && s1.ngroups == s2.ngroups + """ num_groups(s::IntDisjointSet) diff --git a/Compiler/src/ssair/ir.jl b/Compiler/src/ssair/ir.jl index 9103dba04fa540..e343d131bcc9a2 100644 --- a/Compiler/src/ssair/ir.jl +++ b/Compiler/src/ssair/ir.jl @@ -763,22 +763,22 @@ mutable struct IncrementalCompact active_result_bb::Int renamed_new_nodes::Bool - function IncrementalCompact(code::IRCode, cfg_transform::CFGTransformState) + function IncrementalCompact(ir::IRCode, cfg_transform::CFGTransformState) # Sort by position with attach after nodes after regular ones - info = code.new_nodes.info + info = ir.new_nodes.info perm = sort!(collect(eachindex(info)); by=i::Int->(2info[i].pos+info[i].attach_after, i)) - new_len = length(code.stmts) + length(info) + new_len = length(ir.stmts) + length(info) result = InstructionStream(new_len) - code.debuginfo.codelocs = result.line + ir.debuginfo.codelocs = result.line used_ssas = fill(0, new_len) new_new_used_ssas = Vector{Int}() - blocks = code.cfg.blocks + blocks = ir.cfg.blocks ssa_rename = Any[SSAValue(i) for i = 1:new_len] late_fixup = Vector{Int}() new_new_nodes = NewNodeStream() pending_nodes = NewNodeStream() pending_perm = Int[] - return new(code, result, cfg_transform, ssa_rename, used_ssas, late_fixup, perm, 1, + return new(ir, result, cfg_transform, ssa_rename, used_ssas, late_fixup, perm, 1, new_new_nodes, new_new_used_ssas, pending_nodes, pending_perm, 1, 1, 1, 1, false) end @@ -800,8 +800,8 @@ mutable struct IncrementalCompact end end -function IncrementalCompact(code::IRCode, allow_cfg_transforms::Bool=false) - return IncrementalCompact(code, CFGTransformState!(code.cfg.blocks, allow_cfg_transforms)) +function IncrementalCompact(ir::IRCode, allow_cfg_transforms::Bool=false) + return IncrementalCompact(ir, CFGTransformState!(ir.cfg.blocks, allow_cfg_transforms)) end struct TypesView{T} @@ -2130,8 +2130,8 @@ function complete(compact::IncrementalCompact) return IRCode(compact.ir, compact.result, cfg, compact.new_new_nodes) end -function compact!(code::IRCode, allow_cfg_transforms::Bool=false) - compact = IncrementalCompact(code, allow_cfg_transforms) +function compact!(ir::IRCode, allow_cfg_transforms::Bool=false) + compact = IncrementalCompact(ir, allow_cfg_transforms) # Just run through the iterator without any processing for _ in compact; end # _ isa Pair{Int, Any} return finish(compact) diff --git a/Compiler/src/ssair/passes.jl b/Compiler/src/ssair/passes.jl index ff333b9b0a1293..0927db8ed22ed7 100644 --- a/Compiler/src/ssair/passes.jl +++ b/Compiler/src/ssair/passes.jl @@ -1730,16 +1730,19 @@ function sroa_mutables!(ir::IRCode, defuses::IdDict{Int,Tuple{SPCSet,SSADefUse}} finalizer_useidx = find_finalizer_useidx(defuse) if finalizer_useidx isa Int nargs = length(ir.argtypes) # COMBAK this might need to be `Int(opt.src.nargs)` - estate = EscapeAnalysis.analyze_escapes(ir, nargs, 𝕃ₒ, get_escape_cache(inlining.interp)) + eresult = EscapeAnalysis.analyze_escapes(ir, nargs, 𝕃ₒ, get_escape_cache(inlining.interp)) # disable finalizer inlining when this allocation is aliased to somewhere, # mostly likely to edges of `PhiNode` - hasaliases = EscapeAnalysis.getaliases(SSAValue(defidx), estate) !== nothing - einfo = estate[SSAValue(defidx)] + hasaliases = EscapeAnalysis.getaliases(eresult, SSAValue(defidx)) !== nothing + einfo = eresult[SSAValue(defidx)] if !hasaliases && EscapeAnalysis.has_no_escape(einfo) already = BitSet(use.idx for use in defuse.uses) - for idx = einfo.Liveness - if idx ∉ already - push!(defuse.uses, SSAUse(:EALiveness, idx)) + Liveness = einfo.Liveness + if Liveness isa EscapeAnalysis.PCLiveness + for idx = Liveness.pcs + if idx ∉ already + push!(defuse.uses, SSAUse(:EALiveness, idx)) + end end end finalizer_idx = defuse.uses[finalizer_useidx].idx diff --git a/Compiler/test/EAUtils.jl b/Compiler/test/EAUtils.jl index f124aea2544fd1..43d77ff788655e 100644 --- a/Compiler/test/EAUtils.jl +++ b/Compiler/test/EAUtils.jl @@ -14,11 +14,10 @@ import .Compiler: AbstractInterpreter, NativeInterpreter, WorldView, WorldRange, InferenceParams, OptimizationParams, get_world_counter, get_inference_cache, ipo_dataflow_analysis! # usings -using Core: - CodeInstance, MethodInstance, CodeInfo +using Core.IR using .Compiler: InferenceResult, InferenceState, OptimizationState, IRCode -using .EA: analyze_escapes, ArgEscapeCache, ArgEscapeInfo, EscapeInfo, EscapeState +using .EA: analyze_escapes, ArgEscapeCache, ArgEscapeInfo, EscapeInfo, EscapeResult struct EAToken end @@ -26,8 +25,8 @@ struct EAToken end # cache entire escape state for later inspection and debugging struct EscapeCacheInfo argescapes::ArgEscapeCache - state::EscapeState # preserved just for debugging purpose - ir::IRCode # preserved just for debugging purpose + eresult::EscapeResult # preserved just for debugging purpose + ir::IRCode # preserved just for debugging purpose end struct EscapeCache @@ -36,9 +35,9 @@ end EscapeCache() = EscapeCache(IdDict{MethodInstance,EscapeCacheInfo}()) const GLOBAL_ESCAPE_CACHE = EscapeCache() -struct EscapeResultForEntry +struct EscapeAnalysisResultForEntry ir::IRCode - estate::EscapeState + eresult::EscapeResult mi::MethodInstance end @@ -49,7 +48,7 @@ mutable struct EscapeAnalyzer <: AbstractInterpreter const inf_cache::Vector{InferenceResult} const escape_cache::EscapeCache const entry_mi::Union{Nothing,MethodInstance} - result::EscapeResultForEntry + result::EscapeAnalysisResultForEntry function EscapeAnalyzer(world::UInt, escape_cache::EscapeCache; entry_mi::Union{Nothing,MethodInstance}=nothing) inf_params = InferenceParams() @@ -64,15 +63,15 @@ Compiler.OptimizationParams(interp::EscapeAnalyzer) = interp.opt_params Compiler.get_inference_world(interp::EscapeAnalyzer) = interp.world Compiler.get_inference_cache(interp::EscapeAnalyzer) = interp.inf_cache Compiler.cache_owner(::EscapeAnalyzer) = EAToken() -Compiler.get_escape_cache(interp::EscapeAnalyzer) = GetEscapeCache(interp) +Compiler.get_escape_cache(interp::EscapeAnalyzer) = GetEAEscapeCache(interp) function Compiler.ipo_dataflow_analysis!(interp::EscapeAnalyzer, opt::OptimizationState, - ir::IRCode, caller::InferenceResult) + ir::IRCode, caller::InferenceResult) # run EA on all frames that have been optimized nargs = Int(opt.src.nargs) 𝕃ₒ = Compiler.optimizer_lattice(interp) - get_escape_cache = GetEscapeCache(interp) - estate = try + get_escape_cache = GetEAEscapeCache(interp) + eresult = try analyze_escapes(ir, nargs, 𝕃ₒ, get_escape_cache) catch err @error "error happened within EA, inspect `Main.failedanalysis`" @@ -82,35 +81,37 @@ function Compiler.ipo_dataflow_analysis!(interp::EscapeAnalyzer, opt::Optimizati end if caller.linfo === interp.entry_mi # return back the result - interp.result = EscapeResultForEntry(Compiler.copy(ir), estate, caller.linfo) + interp.result = EscapeAnalysisResultForEntry(Compiler.copy(ir), eresult, caller.linfo) end - record_escapes!(interp, caller, estate, ir) + # record_escapes!(interp, caller, eresult, ir) @invoke Compiler.ipo_dataflow_analysis!(interp::AbstractInterpreter, opt::OptimizationState, ir::IRCode, caller::InferenceResult) end function record_escapes!(interp::EscapeAnalyzer, - caller::InferenceResult, estate::EscapeState, ir::IRCode) - argescapes = ArgEscapeCache(estate) - ecacheinfo = EscapeCacheInfo(argescapes, estate, ir) + caller::InferenceResult, eresult::EscapeResult, ir::IRCode) + return nothing + argescapes = ArgEscapeCache(eresult) + ecacheinfo = EscapeCacheInfo(argescapes, eresult, ir) return Compiler.stack_analysis_result!(caller, ecacheinfo) end -struct GetEscapeCache +struct GetEAEscapeCache escape_cache::EscapeCache - GetEscapeCache(interp::EscapeAnalyzer) = new(interp.escape_cache) -end -function ((; escape_cache)::GetEscapeCache)(mi::MethodInstance) - ecacheinfo = get(escape_cache.cache, mi, nothing) - return ecacheinfo === nothing ? false : ecacheinfo.argescapes + GetEAEscapeCache(interp::EscapeAnalyzer) = new(interp.escape_cache) end +# function ((; escape_cache)::GetEAEscapeCache)(mi::MethodInstance) +# ecacheinfo = get(escape_cache.cache, mi, nothing) +# return ecacheinfo === nothing ? false : ecacheinfo.argescapes +# end +(::GetEAEscapeCache)(::MethodInstance) = false struct FailedAnalysis caller::InferenceResult ir::IRCode nargs::Int - get_escape_cache::GetEscapeCache + get_escape_cache::GetEAEscapeCache end function Compiler.finish!(interp::EscapeAnalyzer, state::InferenceState; can_discard_trees::Bool=Compiler.may_discard_trees(interp)) @@ -124,30 +125,29 @@ end # printing # -------- -using Core: Argument, SSAValue using .Compiler: widenconst, singleton_type -function get_name_color(x::EscapeInfo, symbol::Bool = false) +function get_name_color(x::Union{Nothing,EscapeInfo}, symbol::Bool = false) getname(x) = string(nameof(x)) - if x === EA.⊥ - name, color = (getname(EA.NotAnalyzed), "◌"), :plain + if x === nothing + name, color = ("NotAnalyzed", "◌"), :plain elseif EA.has_no_escape(EA.ignore_argescape(x)) if EA.has_arg_escape(x) - name, color = (getname(EA.ArgEscape), "✓"), :cyan + name, color = ("ArgEscape", "✓"), :cyan else - name, color = (getname(EA.NoEscape), "✓"), :green + name, color = ("NoEscape", "✓"), :green end elseif EA.has_all_escape(x) - name, color = (getname(EA.AllEscape), "X"), :red + name, color = ("AllEscape", "X"), :red elseif EA.has_return_escape(x) - name = (getname(EA.ReturnEscape), "↑") + name = ("ReturnEscape", "↑") color = EA.has_thrown_escape(x) ? :yellow : :blue else name = (nothing, "*") color = EA.has_thrown_escape(x) ? :yellow : :bold end name = symbol ? last(name) : first(name) - if name !== nothing && !isa(x.AliasInfo, Bool) + if name !== nothing && x !== nothing && isa(x.ObjectInfo, EA.HasIndexableMemory) name = string(name, "′") end return name, color @@ -199,45 +199,58 @@ function Base.show(io::IO, x::ArgEscapeInfo) printstyled(io, "ArgEscapeInfo(", sym, ")"; color) end -struct EscapeResult +struct EscapeAnalysisResult ir::IRCode - state::EscapeState + eresult::EscapeResult mi::Union{Nothing,MethodInstance} slotnames::Union{Nothing,Vector{Symbol}} source::Bool interp::Union{Nothing,EscapeAnalyzer} - function EscapeResult(ir::IRCode, state::EscapeState, - mi::Union{Nothing,MethodInstance}=nothing, - slotnames::Union{Nothing,Vector{Symbol}}=nothing, - source::Bool=false, - interp::Union{Nothing,EscapeAnalyzer}=nothing) - return new(ir, state, mi, slotnames, source, interp) + function EscapeAnalysisResult(ir::IRCode, eresult::EscapeResult, + mi::Union{Nothing,MethodInstance}=nothing, + slotnames::Union{Nothing,Vector{Symbol}}=nothing, + source::Bool=false, + interp::Union{Nothing,EscapeAnalyzer}=nothing) + return new(ir, eresult, mi, slotnames, source, interp) end end -Base.show(io::IO, result::EscapeResult) = print_with_info(io, result) -@eval Base.iterate(res::EscapeResult, state=1) = - return state > $(fieldcount(EscapeResult)) ? nothing : (getfield(res, state), state+1) +Base.getindex(res::EscapeAnalysisResult, @nospecialize(x)) = res.eresult[x] +EA.isaliased(res::EscapeAnalysisResult, @nospecialize(x), @nospecialize(y)) = EA.isaliased(res.eresult, x, y) +EA.getaliases(res::EscapeAnalysisResult, @nospecialize(x)) = EA.getaliases(res.eresult, x) +EA.is_load_forwardable(res::EscapeAnalysisResult, pc::Int) = EA.is_load_forwardable(res.eresult, pc) +@eval Base.iterate(res::EscapeAnalysisResult, s=1) = + return s > $(fieldcount(EscapeAnalysisResult)) ? nothing : (getfield(res, s), s+1) + +Base.show(io::IO, ecacheinfo::EscapeCacheInfo) = show(io, EscapeAnalysisResult(ecacheinfo.ir, ecacheinfo.eresult)) + +using Compiler: IRShow +function Base.show(io::IO, result::EscapeAnalysisResult, bb::Int=0) + (; ir, eresult, mi, slotnames, source) = result + if bb ≠ 0 + bbstate = eresult.bbescapes[bb] + ssamemoryinfo = nothing + else + bbstate = eresult.retescape + ssamemoryinfo = eresult.ssamemoryinfo + end -Base.show(io::IO, ecacheinfo::EscapeCacheInfo) = show(io, EscapeResult(ecacheinfo.ir, ecacheinfo.state)) + io = IOContext(io, :displaysize=>displaysize(io)) -# adapted from https://github.com/JuliaDebug/LoweredCodeUtils.jl/blob/4612349432447e868cf9285f647108f43bd0a11c/src/codeedges.jl#L881-L897 -function print_with_info(io::IO, result::EscapeResult) - (; ir, state, mi, slotnames, source) = result # print escape information on SSA values - function preprint(io::IO) + function print_header(io::IO) ft = ir.argtypes[1] f = singleton_type(ft) if f === nothing f = widenconst(ft) end print(io, f, '(') - for i in 1:state.nargs - arg = state[Argument(i)] + for i in 1:bbstate.nargs + arginfo = bbstate[Argument(i)] i == 1 && continue - c, color = get_name_color(arg, true) + c, color = get_name_color(arginfo, true) slot = isnothing(slotnames) ? "_$i" : slotnames[i] printstyled(io, c, ' ', slot, "::", ir.argtypes[i]; color) - i ≠ state.nargs && print(io, ", ") + i ≠ bbstate.nargs && print(io, ", ") end print(io, ')') if !isnothing(mi) @@ -249,40 +262,55 @@ function print_with_info(io::IO, result::EscapeResult) # print escape information on SSA values # nd = ndigits(length(ssavalues)) - function preprint(io::IO, idx::Int) - c, color = get_name_color(state[SSAValue(idx)], true) + function print_header(io::IO, idx::Int) + c, color = get_name_color(bbstate[SSAValue(idx)], true) # printstyled(io, lpad(idx, nd), ' ', c, ' '; color) printstyled(io, rpad(c, 2), ' '; color) end - print_with_info(preprint, (args...)->nothing, io, ir, source) -end + lineprinter = IRShow.inline_linfo_printer(ir) + preprinter = function (@nospecialize(io::IO), linestart::String, idx::Int) + str1 = lineprinter(io, linestart, idx) + c, color = get_name_color(bbstate[SSAValue(idx)], true) + str2 = sprint(;context=IOContext(io)) do @nospecialize io::IO + print(io, " ") + printstyled(io, rpad(c, 2); color) + end + str1 * str2 + end -function print_with_info(preprint, postprint, io::IO, ir::IRCode, source::Bool) - io = IOContext(io, :displaysize=>displaysize(io)) - used = Compiler.IRShow.stmts_used(io, ir) - if source - line_info_preprinter = function (io::IO, indent::String, idx::Int) - r = Compiler.IRShow.inline_linfo_printer(ir)(io, indent, idx) - idx ≠ 0 && preprint(io, idx) - return r + _postprinter = IRShow.default_expr_type_printer + postprinter = if ssamemoryinfo !== nothing + function (io::IO; idx::Int, @nospecialize(kws...)) + _postprinter(io; idx, kws...) + if haskey(ssamemoryinfo, idx) + memoryinfo = ssamemoryinfo[idx] + if memoryinfo === EA.ConflictedMemory() + c, color = "*", :yellow + elseif memoryinfo === EA.UnknownMemory() + c, color = "X", :red + elseif memoryinfo === EA.UninitializedMemory() + c, color = "◌", :yellow + else + c = sprint(context=IOContext(io)) do @nospecialize io::IO + Base.show_unquoted(io, memoryinfo) + end + color = :cyan + end + printstyled(io, " (↦ "; color=:light_black) + printstyled(io, c; color) + printstyled(io, ")"; color=:light_black) + end end else - line_info_preprinter = Compiler.IRShow.lineinfo_disabled - end - line_info_postprinter = Compiler.IRShow.default_expr_type_printer - preprint(io) - bb_idx_prev = bb_idx = 1 - for idx = 1:length(ir.stmts) - preprint(io, idx) - bb_idx = Compiler.IRShow.show_ir_stmt(io, ir, idx, line_info_preprinter, line_info_postprinter, ir.sptypes, used, ir.cfg, bb_idx) - postprint(io, idx, bb_idx != bb_idx_prev) - bb_idx_prev = bb_idx + _postprinter end - max_bb_idx_size = ndigits(length(ir.cfg.blocks)) - line_info_preprinter(io, " "^(max_bb_idx_size + 2), 0) - postprint(io) - return nothing + + bb_color = :normal + irshow_config = IRShow.IRShowConfig(preprinter, postprinter; bb_color) + + print_header(io) + IRShow.show_ir(io, ir, irshow_config) end # entries @@ -303,8 +331,8 @@ macro code_escapes(ex0...) end """ - code_escapes(f, argtypes=Tuple{}; [world::UInt], [debuginfo::Symbol]) -> result::EscapeResult - code_escapes(mi::MethodInstance; [world::UInt], [interp::EscapeAnalyzer], [debuginfo::Symbol]) -> result::EscapeResult + code_escapes(f, argtypes=Tuple{}; [world::UInt], [debuginfo::Symbol]) -> result::EscapeAnalysisResult + code_escapes(mi::MethodInstance; [world::UInt], [interp::EscapeAnalyzer], [debuginfo::Symbol]) -> result::EscapeAnalysisResult Runs the escape analysis on optimized IR of a generic function call with the given type signature, while caching the analysis results. @@ -336,12 +364,12 @@ function code_escapes(mi::MethodInstance; slotnames = let src = frame.src src isa CodeInfo ? src.slotnames : nothing end - return EscapeResult(interp.result.ir, interp.result.estate, interp.result.mi, - slotnames, debuginfo === :source, interp) + return EscapeAnalysisResult(interp.result.ir, interp.result.eresult, interp.result.mi, + slotnames, debuginfo === :source, interp) end """ - code_escapes(ir::IRCode, nargs::Int; [world::UInt], [interp::AbstractInterpreter]) -> result::EscapeResult + code_escapes(ir::IRCode, nargs::Int; [world::UInt], [interp::AbstractInterpreter]) -> result::EscapeAnalysisResult Runs the escape analysis on `ir::IRCode`. `ir` is supposed to be optimized already, specifically after inlining has been applied. @@ -357,8 +385,8 @@ Note that this version does not cache the analysis results. function code_escapes(ir::IRCode, nargs::Int; world::UInt = get_world_counter(), interp::AbstractInterpreter=EscapeAnalyzer(world, EscapeCache())) - estate = analyze_escapes(ir, nargs, Compiler.optimizer_lattice(interp), Compiler.get_escape_cache(interp)) - return EscapeResult(ir, estate) # return back the result + eresult = analyze_escapes(ir, nargs, Compiler.optimizer_lattice(interp), Compiler.get_escape_cache(interp)) + return EscapeAnalysisResult(ir, eresult) # return back the result end # in order to run a whole analysis from ground zero (e.g. for benchmarking, etc.) diff --git a/Compiler/test/EscapeAnalysis.jl b/Compiler/test/EscapeAnalysis.jl index e1638e74e61fe8..fad136630b1ace 100644 --- a/Compiler/test/EscapeAnalysis.jl +++ b/Compiler/test/EscapeAnalysis.jl @@ -34,68 +34,62 @@ let utils_ex = quote Core.eval(@__MODULE__, utils_ex) end -using .EscapeAnalysis: EscapeInfo, IndexableFields - isϕ(@nospecialize x) = isa(x, Core.PhiNode) -""" - is_load_forwardable(x::EscapeInfo) -> Bool - -Queries if `x` is elibigle for store-to-load forwarding optimization. -""" -function is_load_forwardable(x::EscapeInfo) - AliasInfo = x.AliasInfo - # NOTE technically we also need to check `!has_thrown_escape(x)` here as well, - # but we can also do equivalent check during forwarding - return isa(AliasInfo, IndexableFields) +is_load_forwardable_old(x::EscapeAnalysis.EscapeInfo) = # TODO use new `is_load_forwardable` instead + isa(x.ObjectInfo, EscapeAnalysis.HasIndexableMemory) + +mutable struct Object + s::String end @testset "EAUtils" begin @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 + @test code_escapes(sin, (Int,)) isa EAUtils.EscapeAnalysisResult + @test code_escapes(sin, (Int,)) isa EAUtils.EscapeAnalysisResult end @testset "basics" begin let # arg return - result = code_escapes((Any,)) do a # return to caller + result = code_escapes((Object,)) do a # return to caller println("prevent ConstABI") return nothing end - @test has_arg_escape(result.state[Argument(2)]) + @test has_arg_escape(result[Argument(2)]) # return - result = code_escapes((Any,)) do a + result = code_escapes((Object,)) do a println("prevent ConstABI") return a end i = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_arg_escape(result.state[Argument(1)]) # self - @test !has_return_escape(result.state[Argument(1)], i) # self - @test has_arg_escape(result.state[Argument(2)]) # a - @test has_return_escape(result.state[Argument(2)], i) # a + @test has_arg_escape(result[Argument(1)]) # self + @test !has_return_escape(result[Argument(1)], i) # self + @test has_arg_escape(result[Argument(2)]) # a + @test has_return_escape(result[Argument(2)], i) # a end let # global store - result = code_escapes((Any,)) do a + result = code_escapes((Object,)) do a global GV = a nothing end - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[Argument(2)]) end let # global load result = code_escapes() do global GV return GV end - i = only(findall(has_return_escape, map(i->result.state[SSAValue(i)], 1:length(result.ir.stmts)))) - @test has_all_escape(result.state[SSAValue(i)]) + r = only(findall(isreturn, result.ir.stmts.stmt)) + rval = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue + @test has_all_escape(result[rval]) end let # global store / load (https://github.com/aviatesk/EscapeAnalysis.jl/issues/56) - result = code_escapes((Any,)) do s + result = code_escapes((Object,)) do s global GV GV = s return GV end r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) + @test has_return_escape(result[Argument(2)], r) end let # :gc_preserve_begin / :gc_preserve_end result = code_escapes((String,)) do s @@ -107,43 +101,43 @@ end end i = findfirst(==(SafeRef{String}), result.ir.stmts.type) # find allocation statement @test !isnothing(i) - @test has_no_escape(result.state[SSAValue(i)]) + @test has_no_escape(result[SSAValue(i)]) end let # :isdefined result = code_escapes((String, Bool,)) do a, b if b - s = Ref(a) + s = Object(a) end return @isdefined(s) end - i = findfirst(==(Base.RefValue{String}), result.ir.stmts.type) # find allocation statement - @test isnothing(i) || has_no_escape(result.state[SSAValue(i)]) + i = findfirst(==(Object), result.ir.stmts.type) # find allocation statement + @test isnothing(i) || has_no_escape(result[SSAValue(i)]) end let # ϕ-node - result = code_escapes((Bool,Any,Any)) do cond, a, b + result = code_escapes((Bool,Object,Object)) do cond, a, b c = cond ? a : b # ϕ(a, b) return c end @assert any(@nospecialize(x)->isa(x, Core.PhiNode), result.ir.stmts.stmt) i = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(3)], i) # a - @test has_return_escape(result.state[Argument(4)], i) # b + @test has_return_escape(result[Argument(3)], i) # a + @test has_return_escape(result[Argument(4)], i) # b end let # π-node result = code_escapes((Any,)) do a - if isa(a, Regex) # a::π(Regex) + if isa(a, Object) # a::π(Object) return a end return nothing end @assert any(@nospecialize(x)->isa(x, Core.PiNode), result.ir.stmts.stmt) @test any(findall(isreturn, result.ir.stmts.stmt)) do i - has_return_escape(result.state[Argument(2)], i) + has_return_escape(result[Argument(2)], i) end end let # φᶜ-node / ϒ-node - result = code_escapes((Any,String)) do a, b - local x::String + result = code_escapes((Any,Object)) do a, b + local x::Object try x = a catch err @@ -154,8 +148,8 @@ end @assert any(@nospecialize(x)->isa(x, Core.PhiCNode), result.ir.stmts.stmt) @assert any(@nospecialize(x)->isa(x, Core.UpsilonNode), result.ir.stmts.stmt) i = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], i) - @test has_return_escape(result.state[Argument(3)], i) + @test has_return_escape(result[Argument(2)], i) + @test has_return_escape(result[Argument(3)], i) end let # branching result = code_escapes((Any,Bool,)) do a, c @@ -165,7 +159,7 @@ end return a # a escapes to a caller end end - @test has_return_escape(result.state[Argument(2)]) + @test has_return_escape(result[Argument(2)]) end let # loop result = code_escapes((Int,)) do n @@ -176,7 +170,7 @@ end nothing end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_return_escape(result.state[SSAValue(i)]) + @test has_return_escape(result[SSAValue(i)]) end let # try/catch result = code_escapes((Any,)) do a @@ -187,7 +181,7 @@ end return a # return escape end end - @test has_return_escape(result.state[Argument(2)]) + @test has_return_escape(result[Argument(2)]) end let result = code_escapes((Any,)) do a try @@ -197,13 +191,13 @@ end return a # return escape end end - @test has_return_escape(result.state[Argument(2)]) + @test has_return_escape(result[Argument(2)]) end let # :foreigncall result = code_escapes((Any,)) do x ccall(:some_ccall, Any, (Any,), x) end - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[Argument(2)]) end end @@ -212,19 +206,19 @@ end r = code_escapes((Any,)) do a throw(a) end - @test has_thrown_escape(r.state[Argument(2)]) + @test has_thrown_escape(r[Argument(2)]) end let # implicit throws r = code_escapes((Any,)) do a getfield(a, :may_not_field) end - @test has_thrown_escape(r.state[Argument(2)]) + @test has_thrown_escape(r[Argument(2)]) r = code_escapes((Any,)) do a sizeof(a) end - @test has_thrown_escape(r.state[Argument(2)]) + @test has_thrown_escape(r[Argument(2)]) end let # :=== @@ -233,14 +227,14 @@ end c = m === nothing return c end - @test has_no_escape(ignore_argescape(result.state[Argument(2)])) + @test has_no_escape(ignore_argescape(result[Argument(2)])) end let # sizeof result = code_escapes((Vector{Any},)) do xs sizeof(xs) end - @test has_no_escape(ignore_argescape(result.state[Argument(2)])) + @test has_no_escape(ignore_argescape(result[Argument(2)])) end let # ifelse @@ -251,7 +245,7 @@ end inds = findall(isnew, result.ir.stmts.stmt) @assert !isempty(inds) for i in inds - @test has_return_escape(result.state[SSAValue(i)]) + @test has_return_escape(result[SSAValue(i)]) end end let # ifelse (with constant condition) @@ -261,9 +255,9 @@ end end for i in 1:length(result.ir.stmts) if isnew(result.ir.stmts.stmt[i]) && result.ir.stmts.type[i] == Base.RefValue{String} - @test has_return_escape(result.state[SSAValue(i)]) + @test has_return_escape(result[SSAValue(i)]) elseif isnew(result.ir.stmts.stmt[i]) && result.ir.stmts.type[i] == Base.RefValue{Nothing} - @test has_no_escape(result.state[SSAValue(i)]) + @test has_no_escape(result[SSAValue(i)]) end end end @@ -274,8 +268,8 @@ end return y end r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) - @test !has_all_escape(result.state[Argument(2)]) + @test has_return_escape(result[Argument(2)], r) + @test !has_all_escape(result[Argument(2)]) end let # isdefined @@ -283,8 +277,8 @@ end isdefined(x, :foo) ? x : throw("undefined") end r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) - @test !has_all_escape(result.state[Argument(2)]) + @test has_return_escape(result[Argument(2)], r) + @test !has_all_escape(result[Argument(2)]) end end @@ -300,22 +294,22 @@ end i = only(findall(isnew, result.ir.stmts.stmt)) rts = findall(isreturn, result.ir.stmts.stmt) @assert length(rts) == 2 - @test count(rt->has_return_escape(result.state[SSAValue(i)], rt), rts) == 1 + @test count(rt->has_return_escape(result[SSAValue(i)], rt), rts) == 1 end let result = code_escapes((Bool,)) do cond r = Ref("foo") cnt = 0 - while rand(Bool) + while @noinline(rand(Bool)) cnt += 1 - rand(Bool) && return r + @noinline(rand(Bool)) && return r end - rand(Bool) && return r + @noinline(rand(Bool)) && return r return cnt end i = only(findall(isnew, result.ir.stmts.stmt)) rts = findall(isreturn, result.ir.stmts.stmt) # return statement @assert length(rts) == 3 - @test count(rt->has_return_escape(result.state[SSAValue(i)], rt), rts) == 2 + @test count(rt->has_return_escape(result[SSAValue(i)], rt), rts) == 2 end end @@ -350,7 +344,7 @@ end return ret end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_return_escape(result.state[SSAValue(i)]) + @test has_return_escape(result[SSAValue(i)]) end let # simple: global escape @@ -366,7 +360,7 @@ end nothing end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) + @test has_all_escape(result[SSAValue(i)]) end let # account for possible escapes via nested throws @@ -383,7 +377,7 @@ end end end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) + @test has_all_escape(result[SSAValue(i)]) end let # account for possible escapes via `rethrow` result = @eval M $code_escapes() do @@ -399,7 +393,7 @@ end end end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) + @test has_all_escape(result[SSAValue(i)]) end let # account for possible escapes via `rethrow` result = @eval M $code_escapes() do @@ -411,7 +405,7 @@ end end end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) + @test has_all_escape(result[SSAValue(i)]) end let # account for possible escapes via `rethrow` result = @eval M $code_escapes() do @@ -426,7 +420,7 @@ end return t end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) + @test has_all_escape(result[SSAValue(i)]) end let # account for possible escapes via `Base.current_exceptions` result = @eval M $code_escapes() do @@ -438,7 +432,7 @@ end end end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) + @test has_all_escape(result[SSAValue(i)]) end let # account for possible escapes via `Base.current_exceptions` result = @eval M $code_escapes() do @@ -450,7 +444,7 @@ end end end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) + @test has_all_escape(result[SSAValue(i)]) end let # contextual: escape information imposed on `err` shouldn't propagate to `r2`, but only to `r1` @@ -471,12 +465,12 @@ end @test length(is) == 2 i1, i2 = is r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i1)]) - @test !has_all_escape(result.state[SSAValue(i2)]) - @test has_return_escape(result.state[SSAValue(i2)], r) + @test has_all_escape(result[SSAValue(i1)]) + @test !has_all_escape(result[SSAValue(i2)]) + @test has_return_escape(result[SSAValue(i2)], r) end - # XXX test cases below are currently broken because of the technical reason described in `escape_exception!` + # XXX test cases below are currently broken because of the technical reason described in `propagate_exct_state` let # limited propagation: exception is caught within a frame => doesn't escape to a caller result = @eval M $code_escapes() do @@ -492,7 +486,7 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test_broken !has_return_escape(result.state[SSAValue(i)], r) # TODO? see `escape_exception!` + @test_broken !has_return_escape(result[SSAValue(i)], r) # TODO? see `escape_exception!` end let # sequential: escape information imposed on `err1` and `err2 should propagate separately result = @eval M $code_escapes() do @@ -517,9 +511,9 @@ end @test length(is) == 2 i1, i2 = is r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i1)]) - @test has_return_escape(result.state[SSAValue(i2)], r) - @test_broken !has_all_escape(result.state[SSAValue(i2)]) # TODO? see `escape_exception!` + @test has_all_escape(result[SSAValue(i1)]) + @test has_return_escape(result[SSAValue(i2)], r) + @test_broken !has_all_escape(result[SSAValue(i2)]) # TODO? see `escape_exception!` end let # nested: escape information imposed on `inner` shouldn't propagate to `s` result = @eval M $code_escapes() do @@ -538,7 +532,7 @@ end return ret end i = only(findall(isnew, result.ir.stmts.stmt)) - @test_broken !has_return_escape(result.state[SSAValue(i)]) + @test_broken !has_return_escape(result[SSAValue(i)]) end let # merge: escape information imposed on `err1` and `err2 should be merged result = @eval M $code_escapes() do @@ -560,9 +554,9 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) rs = findall(isreturn, result.ir.stmts.stmt) - @test_broken !has_all_escape(result.state[SSAValue(i)]) + @test_broken !has_all_escape(result[SSAValue(i)]) for r in rs - @test has_return_escape(result.state[SSAValue(i)], r) + @test has_return_escape(result[SSAValue(i)], r) end end let # no exception handling: should keep propagating the escape @@ -581,74 +575,98 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test_broken !has_return_escape(result.state[SSAValue(i)], r) + @test_broken !has_return_escape(result[SSAValue(i)], r) end end +const g_exception_escape = Ref{Any}() +@noinline function get_exception() + try + rethrow() + catch err + err + end +end +let result = code_escapes((Bool,String,)) do b, s + x = Ref{String}() + b && (x[] = s) + try + return x[] # may throw + catch + err = get_exception() + g_exception_escape[] = err + return nothing + end + end + i = only(findall(iscall((result.ir, getfield)), result.ir.stmts.stmt)) + @test_broken is_load_forwardable(result[SSAValue(i)]) # TODO CFG-aware `MemoryInfo` + i = only(findall(isnew, result.ir.stmts.stmt)) + @test has_all_escape(result[SSAValue(i)]) +end + @testset "field analysis / alias analysis" begin # escaped allocations # ------------------- # escaped object should escape its fields as well - let result = code_escapes((Any,)) do a - global GV = SafeRef{Any}(a) + let result = code_escapes((Object,)) do a + global GV = SafeRef(a) nothing end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[SSAValue(i)]) + @test has_all_escape(result[Argument(2)]) end - let result = code_escapes((Any,)) do a + let result = code_escapes((Object,)) do a global GV = (a,) nothing end i = only(findall(iscall((result.ir, tuple)), result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[SSAValue(i)]) + @test has_all_escape(result[Argument(2)]) end - let result = code_escapes((Any,)) do a - o0 = SafeRef{Any}(a) + let result = code_escapes((Object,)) do a + o0 = SafeRef(a) global GV = SafeRef(o0) nothing end is = findall(isnew, result.ir.stmts.stmt) @test length(is) == 2 i0, i1 = is - @test has_all_escape(result.state[SSAValue(i0)]) - @test has_all_escape(result.state[SSAValue(i1)]) - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[SSAValue(i0)]) + @test has_all_escape(result[SSAValue(i1)]) + @test has_all_escape(result[Argument(2)]) end - let result = code_escapes((Any,)) do a + let result = code_escapes((Object,)) do a t0 = (a,) global GV = (t0,) nothing end inds = findall(iscall((result.ir, tuple)), result.ir.stmts.stmt) @assert length(inds) == 2 - for i in inds; @test has_all_escape(result.state[SSAValue(i)]); end - @test has_all_escape(result.state[Argument(2)]) + for i in inds; @test has_all_escape(result[SSAValue(i)]); end + @test has_all_escape(result[Argument(2)]) end # global escape through `setfield!` - let result = code_escapes((Any,)) do a - r = SafeRef{Any}(:init) + let result = code_escapes((Object,)) do a + r = SafeRef{Any}(nothing) global GV = r r[] = a nothing end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[SSAValue(i)]) + @test has_all_escape(result[Argument(2)]) end - let result = code_escapes((Any,Any)) do a, b - r = SafeRef{Any}(a) + let result = code_escapes((Object,Object)) do a, b + r = SafeRef{Object}(a) global GV = r - r[] = b nothing end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) - @test has_all_escape(result.state[Argument(2)]) # a - @test has_all_escape(result.state[Argument(3)]) # b + @test has_all_escape(result[SSAValue(i)]) + @test has_all_escape(result[Argument(2)]) # a + @test !has_all_escape(result[Argument(3)]) # b end let result = @eval EATModule() begin const Rx = SafeRef(Ref("")) @@ -657,57 +675,58 @@ end Core.sizeof(Rx[]) end end - @test has_all_escape(result.state[Argument(2)]) - end - let result = @eval EATModule() begin - const Rx = SafeRef{Any}(nothing) - $code_escapes((Base.RefValue{String},)) do s - setfield!(Rx, :x, s) - Core.sizeof(Rx[]) - end - end - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[Argument(2)]) end let M = EATModule() @eval M module ___xxx___ import ..SafeRef - const Rx = SafeRef("Rx") + const Rx = SafeRef(SafeRef("Rx")) end - result = @eval M begin - $code_escapes((String,)) do s - rx = getfield(___xxx___, :Rx) - rx[] = s - nothing + let result = @eval M begin + $code_escapes((SafeRef{String},)) do s + rx = getfield(___xxx___, :Rx) + rx[] = s + nothing + end + end + @test has_all_escape(result[Argument(2)]) + end + let result = @eval M begin + $code_escapes((SafeRef{String},)) do s + rx = getglobal(___xxx___, :Rx) + rx[] = s + nothing + end end + @test has_all_escape(result[Argument(2)]) end - @test has_all_escape(result.state[Argument(2)]) end # field escape # ------------ # field escape should propagate to :new arguments - let result = code_escapes((Base.RefValue{String},)) do a + let result = code_escapes((Object,)) do a o = SafeRef(a) Core.donotdelete(o) return o[] end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test has_return_escape(result[Argument(2)], r) + @test is_load_forwardable_old(result[SSAValue(i)]) end - let result = code_escapes((Base.RefValue{String},)) do a + let result = code_escapes((Object,)) do a t = SafeRef((a,)) f = t[][1] return f end i = only(findall(iscall((result.ir, tuple)), result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test has_return_escape(result[Argument(2)], r) + @test is_load_forwardable_old(result[SSAValue(i)]) end - let result = code_escapes((Base.RefValue{String}, Base.RefValue{String})) do a, b + let result = code_escapes((Object,Object)) do a, b obj = SafeRefs(a, b) Core.donotdelete(obj) fld1 = obj[1] @@ -716,9 +735,9 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) # a - @test has_return_escape(result.state[Argument(3)], r) # b - @test is_load_forwardable(result.state[SSAValue(i)]) + @test has_return_escape(result[Argument(2)], r) # a + @test has_return_escape(result[Argument(3)], r) # b + @test is_load_forwardable_old(result[SSAValue(i)]) end # field escape should propagate to `setfield!` argument @@ -730,8 +749,8 @@ end end i = last(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test has_return_escape(result[Argument(2)], r) + @test is_load_forwardable_old(result[SSAValue(i)]) end # propagate escape information imposed on return value of `setfield!` call let result = code_escapes((Base.RefValue{String},)) do a @@ -741,42 +760,42 @@ end end i = last(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test has_return_escape(result[Argument(2)], r) + @test is_load_forwardable_old(result[SSAValue(i)]) end # nested allocations - let result = code_escapes((Base.RefValue{String},)) do a + let result = code_escapes((Object,)) do a o1 = SafeRef(a) o2 = SafeRef(o1) return o2[] end r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) + @test has_return_escape(result[Argument(2)], r) for i in 1:length(result.ir.stmts) if isnew(result.ir.stmts.stmt[i]) && result.ir.stmts.type[i] == SafeRef{String} - @test has_return_escape(result.state[SSAValue(i)], r) + @test has_return_escape(result[SSAValue(i)], r) elseif isnew(result.ir.stmts.stmt[i]) && result.ir.stmts.type[i] == SafeRef{SafeRef{String}} - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end end end - let result = code_escapes((Base.RefValue{String},)) do a + let result = code_escapes((Object,)) do a o1 = (a,) o2 = (o1,) return o2[1] end r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) + @test has_return_escape(result[Argument(2)], r) for i in 1:length(result.ir.stmts) if isnew(result.ir.stmts.stmt[i]) && result.ir.stmts.type[i] == Tuple{String} - @test has_return_escape(result.state[SSAValue(i)], r) + @test has_return_escape(result[SSAValue(i)], r) elseif isnew(result.ir.stmts.stmt[i]) && result.ir.stmts.type[i] == Tuple{Tuple{String}} - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end end end - let result = code_escapes((Base.RefValue{String},)) do a + let result = code_escapes((Object,)) do a o1 = SafeRef(a) o2 = SafeRef(o1) o1′ = o2[] @@ -784,9 +803,9 @@ end return a′ end r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) + @test has_return_escape(result[Argument(2)], r) for i in findall(isnew, result.ir.stmts.stmt) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end end let result = code_escapes() do @@ -796,7 +815,7 @@ end end r = only(findall(isreturn, result.ir.stmts.stmt)) for i in findall(isnew, result.ir.stmts.stmt) - @test has_return_escape(result.state[SSAValue(i)], r) + @test has_return_escape(result[SSAValue(i)], r) end end let result = code_escapes() do @@ -815,7 +834,7 @@ end end return false end |> x->foreach(x) do i - @test has_return_escape(result.state[SSAValue(i)], r) + @test has_return_escape(result[SSAValue(i)], r) end end let result = code_escapes((Base.RefValue{String},)) do x @@ -825,8 +844,8 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test has_return_escape(result[Argument(2)], r) + @test is_load_forwardable_old(result[SSAValue(i)]) end # ϕ-node allocations @@ -839,50 +858,49 @@ end return ϕ[] end r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(3)], r) # x - @test has_return_escape(result.state[Argument(4)], r) # y + @test has_return_escape(result[Argument(3)], r) # x + @test has_return_escape(result[Argument(4)], r) # y i = only(findall(isϕ, result.ir.stmts.stmt)) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) for i in findall(isnew, result.ir.stmts.stmt) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end end - let result = code_escapes((Bool,Any,Any)) do cond, x, y + let result = code_escapes((Bool,Object,Object)) do cond, x, y if cond - ϕ2 = ϕ1 = SafeRef{Any}(x) + ϕ2 = ϕ1 = SafeRef(x) else - ϕ2 = ϕ1 = SafeRef{Any}(y) + ϕ2 = ϕ1 = SafeRef(y) end return ϕ1[], ϕ2[] end r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(3)], r) # x - @test has_return_escape(result.state[Argument(4)], r) # y + @test has_return_escape(result[Argument(3)], r) # x + @test has_return_escape(result[Argument(4)], r) # y for i in findall(isϕ, result.ir.stmts.stmt) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end for i in findall(isnew, result.ir.stmts.stmt) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end end # when ϕ-node merges values with different types - let result = code_escapes((Bool,Base.RefValue{String},Base.RefValue{String},Base.RefValue{String})) do cond, x, y, z + let result = code_escapes((Bool,Object,Object,Object)) do cond, x, y, z local out if cond ϕ = SafeRef(x) out = ϕ[] else - ϕ = SafeRefs(z, y) + ϕ = SafeRefs(y, z) end return @isdefined(out) ? out : throw(ϕ) end r = only(findall(isreturn, result.ir.stmts.stmt)) - t = only(findall(iscall((result.ir, throw)), result.ir.stmts.stmt)) - ϕ = only(findall(==(Union{SafeRef{Base.RefValue{String}},SafeRefs{Base.RefValue{String},Base.RefValue{String}}}), result.ir.stmts.type)) - @test has_return_escape(result.state[Argument(3)], r) # x - @test !has_return_escape(result.state[Argument(4)], r) # y - @test has_return_escape(result.state[Argument(5)], r) # z - @test has_thrown_escape(result.state[SSAValue(ϕ)], t) + ϕ = only(findall(==(Union{SafeRef{Object},SafeRefs{Object,Object}}), result.ir.stmts.type)) + @test has_return_escape(result[Argument(3)], r) # x + @test !has_return_escape(result[Argument(4)], r) # y + @test !has_return_escape(result[Argument(5)], r) # z + @test has_thrown_escape(result[SSAValue(ϕ)]) end # alias analysis @@ -897,8 +915,8 @@ end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test isaliased(Argument(2), val, result.state) - @test !isaliased(Argument(2), SSAValue(i), result.state) + @test isaliased(result.eresult, Argument(2), val) + @test !isaliased(result.eresult, Argument(2), SSAValue(i)) end let result = code_escapes((String,)) do s r1 = SafeRef(s) @@ -909,11 +927,11 @@ end i1, i2 = findall(isnew, result.ir.stmts.stmt) r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test !isaliased(SSAValue(i1), SSAValue(i2), result.state) - @test isaliased(SSAValue(i1), val, result.state) - @test !isaliased(SSAValue(i2), val, result.state) + @test !isaliased(result.eresult, SSAValue(i1), SSAValue(i2)) + @test isaliased(result.eresult, SSAValue(i1), val) + @test !isaliased(result.eresult, SSAValue(i2), val) end - let result = code_escapes((String,)) do s + let result = code_escapes((Object,)) do s r1 = SafeRef(s) r2 = SafeRef(r1) Core.donotdelete(r1, r2) @@ -921,14 +939,14 @@ end end r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test isaliased(Argument(2), val, result.state) + @test isaliased(result.eresult, Argument(2), val) for i in findall(isnew, result.ir.stmts.stmt) - @test !isaliased(SSAValue(i), val, result.state) + @test !isaliased(result.eresult, SSAValue(i), val) end end let result = @eval EATModule() begin - const Rx = SafeRef("Rx") - $code_escapes((String,)) do s + const Rx = SafeRef(SafeRef("Rx")) + $code_escapes((SafeRef{String},)) do s r = SafeRef(Rx) Core.donotdelete(r) rx = r[] # rx aliased to Rx @@ -937,12 +955,12 @@ end end end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[Argument(2)]) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test has_all_escape(result[Argument(2)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end # alias via getfield & setfield! - let result = code_escapes((String,)) do s - r = Ref{String}() + let result = code_escapes((Object,)) do s + r = Ref{Object}() Core.donotdelete(r) r[] = s return r[] @@ -950,12 +968,12 @@ end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test isaliased(Argument(2), val, result.state) - @test !isaliased(Argument(2), SSAValue(i), result.state) + @test isaliased(result.eresult, Argument(2), val) + @test !isaliased(result.eresult, Argument(2), SSAValue(i)) end - let result = code_escapes((String,)) do s + let result = code_escapes((Object,)) do s r1 = Ref(s) - r2 = Ref{Base.RefValue{String}}() + r2 = Ref{Base.RefValue{Object}}() Core.donotdelete(r1, r2) r2[] = r1 return r2[] @@ -963,13 +981,13 @@ end i1, i2 = findall(isnew, result.ir.stmts.stmt) r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test !isaliased(SSAValue(i1), SSAValue(i2), result.state) - @test isaliased(SSAValue(i1), val, result.state) - @test !isaliased(SSAValue(i2), val, result.state) + @test !isaliased(result.eresult, SSAValue(i1), SSAValue(i2)) + @test isaliased(result.eresult, SSAValue(i1), val) + @test !isaliased(result.eresult, SSAValue(i2), val) end - let result = code_escapes((String,)) do s - r1 = Ref{String}() - r2 = Ref{Base.RefValue{String}}() + let result = code_escapes((Object,)) do s + r1 = Ref{Object}() + r2 = Ref{Base.RefValue{Object}}() Core.donotdelete(r1, r2) r2[] = r1 r1[] = s @@ -977,9 +995,9 @@ end end r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test isaliased(Argument(2), val, result.state) + @test isaliased(result.eresult, Argument(2), val) for i in findall(isnew, result.ir.stmts.stmt) - @test !isaliased(SSAValue(i), val, result.state) + @test !isaliased(result.eresult, SSAValue(i), val) end result = code_escapes((String,)) do s r1 = Ref{String}() @@ -990,9 +1008,9 @@ end end r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test isaliased(Argument(2), val, result.state) + @test isaliased(result.eresult, Argument(2), val) for i in findall(isnew, result.ir.stmts.stmt) - @test !isaliased(SSAValue(i), val, result.state) + @test !isaliased(result.eresult, SSAValue(i), val) end end let result = @eval EATModule() begin @@ -1007,8 +1025,8 @@ end end end i = findfirst(isnew, result.ir.stmts.stmt) - @test has_all_escape(result.state[Argument(3)]) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test has_all_escape(result[Argument(3)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end # alias via typeassert let result = code_escapes((Any,)) do a @@ -1017,15 +1035,15 @@ end end r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test has_return_escape(result.state[Argument(2)], r) # a - @test isaliased(Argument(2), val, result.state) # a <-> r + @test has_return_escape(result[Argument(2)], r) # a + @test isaliased(result.eresult, Argument(2), val) # a <-> r end let result = code_escapes((Any,)) do a global GV (g::SafeRef{Any})[] = a nothing end - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[Argument(2)]) end # alias via ifelse let result = code_escapes((Bool,Any,Any)) do c, a, b @@ -1034,11 +1052,11 @@ end end r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test has_return_escape(result.state[Argument(3)], r) # a - @test has_return_escape(result.state[Argument(4)], r) # b - @test !isaliased(Argument(2), val, result.state) # c r - @test isaliased(Argument(3), val, result.state) # a <-> r - @test isaliased(Argument(4), val, result.state) # b <-> r + @test has_return_escape(result[Argument(3)], r) # a + @test has_return_escape(result[Argument(4)], r) # b + @test !isaliased(result.eresult, Argument(2), val) # c r + @test isaliased(result.eresult, Argument(3), val) # a <-> r + @test isaliased(result.eresult, Argument(4), val) # b <-> r end let result = @eval EATModule() begin const Lx, Rx = SafeRef("Lx"), SafeRef("Rx") @@ -1048,7 +1066,7 @@ end nothing end end - @test has_all_escape(result.state[Argument(3)]) # a + @test has_all_escape(result[Argument(3)]) # a end # alias via ϕ-node let result = code_escapes((Bool,Base.RefValue{String})) do cond, x @@ -1062,14 +1080,14 @@ end end r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test has_return_escape(result.state[Argument(3)], r) # x - @test isaliased(Argument(3), val, result.state) # x + @test has_return_escape(result[Argument(3)], r) # x + @test isaliased(result.eresult, Argument(3), val) # x for i in findall(isϕ, result.ir.stmts.stmt) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end for i in findall(isnew, result.ir.stmts.stmt) if result.ir[SSAValue(i)][:type] <: SafeRef - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end end end @@ -1084,14 +1102,14 @@ end end r = only(findall(isreturn, result.ir.stmts.stmt)) val = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test has_return_escape(result.state[Argument(4)], r) # x - @test isaliased(Argument(4), val, result.state) # x + @test has_return_escape(result[Argument(4)], r) # x + @test isaliased(result.eresult, Argument(4), val) # x for i in findall(isϕ, result.ir.stmts.stmt) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end for i in findall(isnew, result.ir.stmts.stmt) if result.ir[SSAValue(i)][:type] <: SafeRef - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end end end @@ -1104,8 +1122,8 @@ end end r = only(findall(isreturn, result.ir.stmts.stmt)) rval = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue - @test has_return_escape(result.state[Argument(2)], r) # x - @test isaliased(Argument(2), rval, result.state) + @test has_return_escape(result[Argument(2)], r) # x + @test isaliased(result.eresult, Argument(2), rval) end let result = code_escapes((String,)) do x global GV @@ -1115,7 +1133,7 @@ end end nothing end - @test has_all_escape(result.state[Argument(2)]) # x + @test has_all_escape(result[Argument(2)]) # x end # circular reference let result = code_escapes() do @@ -1125,7 +1143,7 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[SSAValue(i)], r) + @test has_return_escape(result[SSAValue(i)], r) end let result = @eval Module() begin const Rx = Ref{Any}() @@ -1137,7 +1155,7 @@ end end r = only(findall(isreturn, result.ir.stmts.stmt)) for i in findall(iscall((result.ir, getfield)), result.ir.stmts.stmt) - @test has_return_escape(result.state[SSAValue(i)], r) + @test has_return_escape(result[SSAValue(i)], r) end end let result = @eval Module() begin @@ -1153,7 +1171,7 @@ end end i = only(findall(isinvoke(:genr), result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[SSAValue(i)], r) + @test has_return_escape(result[SSAValue(i)], r) end # dynamic semantics @@ -1163,9 +1181,7 @@ end let result = @eval code_escapes((Any,Any,)) do T, x obj = $(Expr(:new, :T, :x)) end - t = only(findall(isnew, result.ir.stmts.stmt)) - @test #=T=# has_thrown_escape(result.state[Argument(2)], t) # T - @test #=x=# has_thrown_escape(result.state[Argument(3)], t) # x + @test #=x=# has_thrown_escape(result[Argument(3)]) # x end let result = @eval code_escapes((Any,Any,Any,Any)) do T, x, y, z obj = $(Expr(:new, :T, :x, :y)) @@ -1173,9 +1189,9 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test #=x=# has_return_escape(result.state[Argument(3)], r) - @test #=y=# has_return_escape(result.state[Argument(4)], r) - @test #=z=# !has_return_escape(result.state[Argument(5)], r) + @test #=x=# has_return_escape(result[Argument(3)], r) + @test #=y=# has_return_escape(result[Argument(4)], r) + @test #=z=# !has_return_escape(result[Argument(5)], r) end let result = @eval code_escapes((Any,Any,Any,Any)) do T, x, y, z obj = $(Expr(:new, :T, :x)) @@ -1184,9 +1200,9 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test #=x=# has_return_escape(result.state[Argument(3)], r) - @test #=y=# has_return_escape(result.state[Argument(4)], r) - @test #=z=# !has_return_escape(result.state[Argument(5)], r) + @test #=x=# has_return_escape(result[Argument(3)], r) + @test #=y=# has_return_escape(result[Argument(4)], r) + @test #=z=# !has_return_escape(result[Argument(5)], r) end # conservatively handle unknown field: @@ -1197,8 +1213,8 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) # a - @test !is_load_forwardable(result.state[SSAValue(i)]) # obj + @test has_return_escape(result[Argument(2)], r) # a + @test !is_load_forwardable_old(result[SSAValue(i)]) # obj end let result = code_escapes((Base.RefValue{String}, Base.RefValue{String}, Symbol)) do a, b, fld obj = SafeRefs(a, b) @@ -1206,9 +1222,9 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) # a - @test has_return_escape(result.state[Argument(3)], r) # b - @test !is_load_forwardable(result.state[SSAValue(i)]) # obj + @test has_return_escape(result[Argument(2)], r) # a + @test has_return_escape(result[Argument(3)], r) # b + @test !is_load_forwardable_old(result[SSAValue(i)]) # obj end let result = code_escapes((Base.RefValue{String}, Base.RefValue{String}, Int)) do a, b, idx obj = SafeRefs(a, b) @@ -1216,9 +1232,9 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) # a - @test has_return_escape(result.state[Argument(3)], r) # b - @test !is_load_forwardable(result.state[SSAValue(i)]) # obj + @test has_return_escape(result[Argument(2)], r) # a + @test has_return_escape(result[Argument(3)], r) # b + @test !is_load_forwardable_old(result[SSAValue(i)]) # obj end let result = code_escapes((Base.RefValue{String}, Base.RefValue{String}, Symbol)) do a, b, fld obj = SafeRefs(Ref("a"), Ref("b")) @@ -1227,9 +1243,9 @@ end end i = last(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) # a - @test !has_return_escape(result.state[Argument(3)], r) # b - @test !is_load_forwardable(result.state[SSAValue(i)]) # obj + @test has_return_escape(result[Argument(2)], r) # a + @test !has_return_escape(result[Argument(3)], r) # b + @test !is_load_forwardable_old(result[SSAValue(i)]) # obj end let result = code_escapes((Base.RefValue{String}, Symbol)) do a, fld obj = SafeRefs(Ref("a"), Ref("b")) @@ -1238,8 +1254,8 @@ end end i = last(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) # a - @test !is_load_forwardable(result.state[SSAValue(i)]) # obj + @test has_return_escape(result[Argument(2)], r) # a + @test !is_load_forwardable_old(result[SSAValue(i)]) # obj end let result = code_escapes((Base.RefValue{String}, Base.RefValue{String}, Int)) do a, b, idx obj = SafeRefs(Ref("a"), Ref("b")) @@ -1248,9 +1264,9 @@ end end i = last(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) # a - @test !has_return_escape(result.state[Argument(3)], r) # b - @test !is_load_forwardable(result.state[SSAValue(i)]) # obj + @test has_return_escape(result[Argument(2)], r) # a + @test !has_return_escape(result[Argument(3)], r) # b + @test !is_load_forwardable_old(result[SSAValue(i)]) # obj end # interprocedural @@ -1266,9 +1282,9 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(2)], r) + @test has_return_escape(result[Argument(2)], r) # NOTE we can't scalar replace `obj`, but still we may want to stack allocate it - @test_broken is_load_forwardable(result.state[SSAValue(i)]) + @test_broken is_load_forwardable_old(result[SSAValue(i)]) end # TODO interprocedural alias analysis @@ -1277,27 +1293,27 @@ end global GV = s[] nothing end - @test_broken !has_all_escape(result.state[Argument(2)]) + @test_broken !has_all_escape(result[Argument(2)]) end # aliasing between arguments let result = @eval EATModule() begin @noinline setxy!(x, y) = x[] = y - $code_escapes((String,)) do y - x = SafeRef("init") + $code_escapes((SafeRef{Any},)) do y + x = SafeRef{Any}(nothing) setxy!(x, y) return x end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[SSAValue(i)], r) - @test has_return_escape(result.state[Argument(2)], r) # y + @test has_return_escape(result[SSAValue(i)], r) + @test has_return_escape(result[Argument(2)], r) # y end let result = @eval EATModule() begin @noinline setxy!(x, y) = x[] = y - $code_escapes((String,)) do y - x1 = SafeRef("init") + $code_escapes((SafeRef{Any},)) do y + x1 = SafeRef{Any}(nothing) x2 = SafeRef(y) Core.donotdelete(x1, x2) setxy!(x1, x2[]) @@ -1306,9 +1322,9 @@ end end i1, i2 = findall(isnew, result.ir.stmts.stmt) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[SSAValue(i1)], r) - @test !has_return_escape(result.state[SSAValue(i2)], r) - @test has_return_escape(result.state[Argument(2)], r) # y + @test has_return_escape(result[SSAValue(i1)], r) + @test !has_return_escape(result[SSAValue(i2)], r) + @test has_return_escape(result[Argument(2)], r) # y end let result = @eval EATModule() begin @noinline mysetindex!(x, a) = x[1] = a @@ -1317,7 +1333,7 @@ end mysetindex!(Ax, s) end end - @test has_all_escape(result.state[Argument(2)]) # s + @test has_all_escape(result[Argument(2)]) # s end # TODO flow-sensitivity? @@ -1331,9 +1347,9 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test_broken !has_return_escape(result.state[Argument(2)], r) # a - @test has_return_escape(result.state[Argument(3)], r) # b - @test is_load_forwardable(result.state[SSAValue(i)]) + @test !has_return_escape(result[Argument(2)], r) # a + @test has_return_escape(result[Argument(3)], r) # b + @test is_load_forwardable_old(result[SSAValue(i)]) end let result = code_escapes((Any,Any)) do a, b r = SafeRef{Any}(:init) @@ -1344,9 +1360,9 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test_broken !has_return_escape(result.state[Argument(2)], r) # a - @test has_return_escape(result.state[Argument(3)], r) # b - @test is_load_forwardable(result.state[SSAValue(i)]) + @test !has_return_escape(result[Argument(2)], r) # a + @test has_return_escape(result[Argument(3)], r) # b + @test is_load_forwardable_old(result[SSAValue(i)]) end let result = code_escapes((Any,Any,Bool)) do a, b, cond r = SafeRef{Any}(:init) @@ -1360,12 +1376,12 @@ end end end i = only(findall(isnew, result.ir.stmts.stmt)) - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) r = only(findall(result.ir.stmts.stmt) do @nospecialize x isreturn(x) && isa(x.val, Core.SSAValue) end) - @test has_return_escape(result.state[Argument(2)], r) # a - @test_broken !has_return_escape(result.state[Argument(3)], r) # b + @test has_return_escape(result[Argument(2)], r) # a + @test !has_return_escape(result[Argument(3)], r) # b end # handle conflicting field information correctly @@ -1384,11 +1400,11 @@ end r end r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(3)], r) # baz - @test has_return_escape(result.state[Argument(4)], r) # qux + @test has_return_escape(result[Argument(3)], r) # baz + @test has_return_escape(result[Argument(4)], r) # qux for new in findall(isnew, result.ir.stmts.stmt) if !(result.ir[SSAValue(new)][:type] <: Base.RefValue) - @test is_load_forwardable(result.state[SSAValue(new)]) + @test is_load_forwardable_old(result[SSAValue(new)]) end end end @@ -1406,8 +1422,8 @@ end r end r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[Argument(3)], r) # baz - @test has_return_escape(result.state[Argument(4)], r) # qux + @test has_return_escape(result[Argument(3)], r) # baz + @test has_return_escape(result[Argument(4)], r) # qux end # foreigncall should disable field analysis @@ -1422,7 +1438,7 @@ end return mt, has_ambig[] end for i in findall(isnew, result.ir.stmts.stmt) - @test !is_load_forwardable(result.state[SSAValue(i)]) + @test !is_load_forwardable_old(result[SSAValue(i)]) end end end @@ -1449,7 +1465,7 @@ let result = @code_escapes compute(MPoint, 1+.5im, 2+.5im, 2+.25im, 4+.75im) stmt = inst[:stmt] return (isnew(stmt) || isϕ(stmt)) && inst[:type] <: MPoint end - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end end function compute(a, b) @@ -1459,15 +1475,15 @@ function compute(a, b) end a.x, a.y end -# let result = @code_escapes compute(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.75im)) -# idxs = findall(1:length(result.ir.stmts)) do idx -# inst = result.ir[SSAValue(idx)] -# stmt = inst[:stmt] -# return isnew(stmt) && inst[:type] <: MPoint -# end -# @assert length(idxs) == 2 -# @test count(i->is_load_forwardable(result.state[SSAValue(i)]), idxs) == 1 -# end +let result = @code_escapes compute(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.75im)) + idxs = findall(1:length(result.ir.stmts)) do idx + inst = result.ir[SSAValue(idx)] + stmt = inst[:stmt] + return isnew(stmt) && inst[:type] <: MPoint + end + @assert length(idxs) == 1 + @test_broken is_load_forwardable_old(result[SSAValue(only(idxs))]) +end function compute!(a, b) for i in 0:(100000000-1) c = add(a, b) # replaceable @@ -1477,12 +1493,12 @@ function compute!(a, b) end end let result = @code_escapes compute!(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.75im)) - for i in findall(1:length(result.ir.stmts)) do idx + for i in findall(1:length(result.ir.stmts)) do idx::Int inst = result.ir[SSAValue(idx)] stmt = inst[:stmt] return isnew(stmt) && inst[:type] <: MPoint end - @test is_load_forwardable(result.state[SSAValue(i)]) + @test is_load_forwardable_old(result[SSAValue(i)]) end end @@ -1492,7 +1508,7 @@ end let result = code_escapes((Nothing,)) do a global GV = a end - @test !(has_all_escape(result.state[Argument(2)])) + @test !(has_all_escape(result[Argument(2)])) end let result = code_escapes((Int,)) do a @@ -1502,7 +1518,7 @@ end end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test !has_return_escape(result.state[SSAValue(i)], r) + @test !has_return_escape(result[SSAValue(i)], r) end # an escaped tuple stmt will not propagate to its Int argument (since `Int` is of bitstype) @@ -1512,8 +1528,8 @@ end end i = only(findall(iscall((result.ir, tuple)), result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test !has_return_escape(result.state[Argument(2)], r) - @test has_return_escape(result.state[Argument(3)], r) + @test !has_return_escape(result[Argument(2)], r) + @test has_return_escape(result[Argument(3)], r) end end @@ -1526,26 +1542,26 @@ let result = code_escapes() do broadcast_noescape2(Ref(Ref("Hi"))) end i = last(findall(isnew, result.ir.stmts.stmt)) - @test_broken !has_return_escape(result.state[SSAValue(i)]) # TODO interprocedural alias analysis - @test !has_thrown_escape(result.state[SSAValue(i)]) + @test_broken !has_return_escape(result[SSAValue(i)]) # TODO interprocedural alias analysis + @test !has_thrown_escape(result[SSAValue(i)]) end @noinline allescape_argument(a) = (global GV = a) # obvious escape let result = code_escapes() do allescape_argument(Ref("Hi")) end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_all_escape(result.state[SSAValue(i)]) + @test has_all_escape(result[SSAValue(i)]) end # if we can't determine the matching method statically, we should be conservative let result = code_escapes((Ref{Any},)) do a may_exist(a) end - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[Argument(2)]) end let result = code_escapes((Ref{Any},)) do a Base.@invokelatest broadcast_noescape1(a) end - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[Argument(2)]) end # handling of simple union-split (just exploit the inliner's effort) @@ -1559,7 +1575,7 @@ let result = code_escapes((Union{Int,Nothing},)) do x inds = findall(isnew, result.ir.stmts.stmt) # find allocation statement @assert !isempty(inds) for i in inds - @test has_no_escape(result.state[SSAValue(i)]) + @test has_no_escape(result[SSAValue(i)]) end end @@ -1570,7 +1586,7 @@ let result = code_escapes() do nothing end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_no_escape(result.state[SSAValue(i)]) + @test_broken has_no_escape(result[SSAValue(i)]) result = code_escapes() do a = Ref("foo") # still should be "return escape" @@ -1579,7 +1595,7 @@ let result = code_escapes() do end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[SSAValue(i)], r) + @test has_return_escape(result[SSAValue(i)], r) end # should propagate escape information imposed on return value to the aliased call argument @@ -1591,7 +1607,7 @@ let result = code_escapes() do end i = only(findall(isnew, result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_return_escape(result.state[SSAValue(i)], r) + @test has_return_escape(result[SSAValue(i)], r) end @noinline noreturnescape_argument(a) = (println("prevent inlining"); identity("hi")) let result = code_escapes() do @@ -1600,7 +1616,7 @@ let result = code_escapes() do return ret # must not alias to `obj` end i = only(findall(isnew, result.ir.stmts.stmt)) - @test has_no_escape(result.state[SSAValue(i)]) + @test_broken has_no_escape(result[SSAValue(i)]) end function with_self_aliased(from_bb::Int, succs::Vector{Int}) @@ -1617,7 +1633,7 @@ function with_self_aliased(from_bb::Int, succs::Vector{Int}) end return visited end -@test code_escapes(with_self_aliased) isa EAUtils.EscapeResult +@test code_escapes(with_self_aliased) isa EAUtils.EscapeAnalysisResult # accounts for ThrownEscape via potential MethodError @@ -1626,15 +1642,14 @@ end let result = code_escapes((SafeRef{String},)) do x identity_if_string(x) end - @test has_no_escape(ignore_argescape(result.state[Argument(2)])) + @test_broken has_no_escape(ignore_argescape(result[Argument(2)])) end let result = code_escapes((SafeRef,)) do x identity_if_string(x) end - i = only(findall(iscall((result.ir, identity_if_string)), result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_thrown_escape(result.state[Argument(2)], i) - @test_broken !has_return_escape(result.state[Argument(2)], r) + @test has_thrown_escape(result[Argument(2)]) + @test_broken !has_return_escape(result[Argument(2)], r) end let result = code_escapes((SafeRef{String},)) do x try @@ -1644,7 +1659,7 @@ let result = code_escapes((SafeRef{String},)) do x end return nothing end - @test !has_all_escape(result.state[Argument(2)]) + @test_broken !has_all_escape(result[Argument(2)]) end let result = code_escapes((Union{SafeRef{String},Vector{String}},)) do x try @@ -1654,7 +1669,7 @@ let result = code_escapes((Union{SafeRef{String},Vector{String}},)) do x end return nothing end - @test has_all_escape(result.state[Argument(2)]) + @test has_all_escape(result[Argument(2)]) end # method ambiguity error @noinline ambig_error_test(a::SafeRef, b) = (println("preventing inlining"); nothing) @@ -1663,12 +1678,11 @@ end let result = code_escapes((SafeRef{String},Any)) do x, y ambig_error_test(x, y) end - i = only(findall(iscall((result.ir, ambig_error_test)), result.ir.stmts.stmt)) r = only(findall(isreturn, result.ir.stmts.stmt)) - @test has_thrown_escape(result.state[Argument(2)], i) # x - @test has_thrown_escape(result.state[Argument(3)], i) # y - @test_broken !has_return_escape(result.state[Argument(2)], r) # x - @test_broken !has_return_escape(result.state[Argument(3)], r) # y + @test has_thrown_escape(result[Argument(2)]) # x + @test has_thrown_escape(result[Argument(3)]) # y + @test_broken !has_return_escape(result[Argument(2)], r) # x + @test_broken !has_return_escape(result[Argument(3)], r) # y end let result = code_escapes((SafeRef{String},Any)) do x, y try @@ -1677,8 +1691,8 @@ let result = code_escapes((SafeRef{String},Any)) do x, y global GV = err end end - @test has_all_escape(result.state[Argument(2)]) # x - @test has_all_escape(result.state[Argument(3)]) # y + @test has_all_escape(result[Argument(2)]) # x + @test has_all_escape(result[Argument(3)]) # y end @eval function scope_folding() @@ -1695,7 +1709,177 @@ end :(return Core.current_scope())), :(), :(Base.inferencebarrier(1)))) end -@test (@code_escapes scope_folding()) isa EAUtils.EscapeResult -@test (@code_escapes scope_folding_opt()) isa EAUtils.EscapeResult +@test (@code_escapes scope_folding()) isa EAUtils.EscapeAnalysisResult +@test (@code_escapes scope_folding_opt()) isa EAUtils.EscapeAnalysisResult + +# flow-sensitivity +# ================ + +let result = code_escapes() do + x = Ref{String}() + x[] = "foo" + out1 = x[] + x[] = "bar" + out2 = x[] + return x, out1, out2 + end + idxs = findall(iscall((result.ir, getfield)), result.ir.stmts.stmt) + @test length(idxs) == 2 + for idx = idxs + @test is_load_forwardable(result, idx) + end +end + +let result = code_escapes((Bool,String,String)) do c, s1, s2 + if c + x = Ref(s1) + else + x = Ref(s2) + end + return x[] + end + idx = only(findall(iscall((result.ir, getfield)), result.ir.stmts.stmt)) + @test_broken is_load_forwardable(result, idx) # TODO CFG-aware `MemoryInfo` +end + +const g_flowsensitive_1 = Ref{Any}() +let result = code_escapes((Bool,String)) do c, s + x = Ref(s) + if c + g_flowsensitive_1[] = x + return nothing + end + return x[] + end + idx = only(findall(iscall((result.ir, getfield)), result.ir.stmts.stmt)) + @test is_load_forwardable(result, idx) + idx = only(findall(isnew, result.ir.stmts.stmt)) + @test has_all_escape(result[SSAValue(idx)]) +end + +function func_g_flowsensitive_1() + g_flowsensitive_1[][] = join(rand(Char, 5)) +end +let result = code_escapes((Int,Bool,String,)) do n, x, s + x = Ref(s) + local out = nothing + for i = 1:n + out = x[] + if n ≥ length(s) + g_flowsensitive_1[] = x + end + @noinline rand(Bool) && func_g_flowsensitive_1() + end + return x, out + end + idx = only(findall(iscall((result.ir, getfield)), result.ir.stmts.stmt)) + @test !is_load_forwardable(result, idx) + idx = only(findall(isnew, result.ir.stmts.stmt)) + @test has_all_escape(result[SSAValue(idx)]) +end + +let result = code_escapes((Int,Bool,String,)) do n, x, s + x = Ref(s) + local out = nothing + for i = 1:n + out = x[] + if n ≥ length(s) + x[] = "" + end + end + return x, out + end + idx = only(findall(iscall((result.ir, getfield)), result.ir.stmts.stmt)) + @test_broken is_load_forwardable(result, idx) # TODO CFG-aware `MemoryInfo` + idx = only(findall(isnew, result.ir.stmts.stmt)) + @test !has_all_escape(result[SSAValue(idx)]) +end + +let result = code_escapes((String,)) do s + x = Ref(s) + xx = Ref(x) + g_flowsensitive_1[] = xx + some_unsafe_call() + x[] + end + idx = only(findall(iscall((result.ir, getfield)), result.ir.stmts.stmt)) + @test !is_load_forwardable(result, idx) + idxs = findall(isnew, result.ir.stmts.stmt) + @test length(idxs) == 2 + for idx = idxs + @test has_all_escape(result[SSAValue(idx)]) + end +end + +let result = code_escapes((Bool,Bool,String,String,)) do c1, c2, x, y + local w + if c1 + z = w = Ref(x) + else + z = Ref(y) + end + if c2 + z[] = "foo" + end + if @isdefined w + return w[] + end + nothing + end + idx = only(findall(iscall((result.ir, getfield)), result.ir.stmts.stmt)) + @test !is_load_forwardable(result, idx) # COMBAK maybe CFG-aware `MemoryInfo` would allow this? + idxs = findall(isnew, result.ir.stmts.stmt) + @test length(idxs) == 2 + for idx = idxs + @test has_no_escape(result[SSAValue(idx)]) + end +end + +# Flow-sensitive escape analysis example from the paper: +# Lukas Stadler, Thomas Würthinger, and Hanspeter Mössenböck. 2018. +# Partial Escape Analysis and Scalar Replacement for Java. +# In Proceedings of Annual IEEE/ACM International Symposium on Code Generation and Optimization (CGO '14). +# Association for Computing Machinery, New York, NY, USA, 165–174. https://doi.org/10.1145/2544137.2544157 +mutable struct Key + idx::Int + ref + Key(idx::Int, @nospecialize(ref)) = new(idx, ref) +end +import Base: == +key1::Key == key2::Key = + key1.idx == key2.idx && key1.ref === key2.ref + +global cache_key::Key +global cache_value + +function get_value(idx::Int, ref) + global cache_key, cache_value + key = Key(idx, ref) + if key == cache_key + return cache_value + else + cache_key = key + cache_value = create_value(key) + return cache_value + end +end + +let result = code_escapes(get_value, (Int,Any)) + idx = only(findall(isnew, result.ir.stmts.stmt)) + idxs = findall(result.ir.stmts.stmt) do @nospecialize x + return iscall((result.ir, getfield))(x) && + x.args[1] == SSAValue(idx) + end + for idx = idxs + @test is_load_forwardable(result, idx) + end + @test has_all_escape(result[SSAValue(idx)]) + # TODO Have a better way to represent "flow-sensitive" information for allocating sinking + rets = findall(isreturn, result.ir.stmts.stmt) + @test count(rets) do retidx::Int + bb = Compiler.block_for_inst(result.ir, retidx) + has_no_escape(result.eresult.bbescapes[bb][SSAValue(idx)]) + end == 1 +end end # module test_EA