diff --git a/Compiler/src/optimize.jl b/Compiler/src/optimize.jl index 12b2f3c9a269f..0030d8f72fa39 100644 --- a/Compiler/src/optimize.jl +++ b/Compiler/src/optimize.jl @@ -653,7 +653,7 @@ function ((; code_cache)::GetNativeEscapeCache)(codeinst::Union{CodeInstance,Met codeinst isa CodeInstance || return false end argescapes = traverse_analysis_results(codeinst) do @nospecialize result - return result isa EscapeAnalysis.ArgEscapeCache ? result : nothing + return result isa EscapeAnalysis.EscapeCache ? result : nothing end if argescapes !== nothing return argescapes @@ -673,10 +673,10 @@ function refine_effects!(interp::AbstractInterpreter, opt::OptimizationState, sv 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) + eresult = EscapeAnalysis.analyze_escapes(ir, nargs, get_escape_cache(interp)) + argescapes = EscapeAnalysis.EscapeCache(eresult) stack_analysis_result!(sv.result, argescapes) - validate_mutable_arg_escapes!(estate, sv) + validate_mutable_arg_escapes!(eresult, sv) end any_refinable(sv) || return false @@ -718,7 +718,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 @@ -737,7 +737,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 @@ -748,8 +748,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 @@ -757,14 +757,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 af8e9b1a4959e..556bec5a7be53 100644 --- a/Compiler/src/ssair/EscapeAnalysis.jl +++ b/Compiler/src/ssair/EscapeAnalysis.jl @@ -3,6 +3,8 @@ baremodule EscapeAnalysis export analyze_escapes, getaliases, + is_load_forwardable, + is_not_analyzed, isaliased, has_no_escape, has_arg_escape, @@ -13,21 +15,23 @@ 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!, - !, !==, &, *, +, -, :, <, <<, >, |, ∈, ∉, ∩, ∪, ≠, ≤, ≥, ⊆ + @__MODULE__, @assert, @eval, @goto, @inbounds, @inline, @isdefined, @label, @noinline, + @nospecialize, @specialize, BitSet, IdDict, IdSet, Pair, RefValue, Vector, + _bits_findnext, copy!, empty!, enumerate, error, fill!, first, get, hasintersect, + haskey, isassigned, isexpr, keys, 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, Compiler, HandlerInfo, IRCode, IR_FLAG_NOTHROW, NewNodeInfo, SimpleHandler, + argextype, argument_datatype, compute_trycatch, datatype_min_ninitialized, + fieldcount_noerror, gethandler, has_flag, is_identity_free_argtype, + is_inaccessiblememonly, is_inaccessiblemem_or_argmemonly, is_meta_expr_head, + is_nothrow, isterminator, singleton_type, try_compute_field, try_compute_fieldidx, + widenconst function include(x::String) if !isdefined(Base, :end_base_include) @@ -39,27 +43,177 @@ end include("disjoint_set.jl") -const AInfo = IdSet{Any} +abstract type MemoryKind end +struct UninitializedMemory <: MemoryKind end +struct CallerMemory <: MemoryKind + id::Int + arg::Argument # TODO arg::Union{Argument,CallerMemory} + fidx::Int +end +# struct SSAValueMemory <: MemoryKind end +# struct GlobalRefMemory <: MemoryKind end +# struct LiteralMemory <: MemoryKind end + +# 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. + +abstract type MemoryInfo end +struct MustAliasMemoryInfo <: MemoryInfo + alias::Any # anything that is valid as IR elements (e.g. `SSAValue`, `Argument`, `GlobalRef`, literals, and `MemoryKind`) + MustAliasMemoryInfo(@nospecialize alias) = new(alias) +end +struct MayAliasMemoryInfo <: MemoryInfo + aliases::IdSet{Any} + function MayAliasMemoryInfo(aliases::IdSet{Any}) + @assert length(aliases) ≥ 2 + return new(aliases) + end +end +struct UnknownMemoryInfo <: MemoryInfo end # not part of the `⊑ₘ` lattice, just a marker for `ssamemoryinfo` +const ⊤ₘ = UnknownMemoryInfo() + +x::MemoryInfo == y::MemoryInfo = begin + @nospecialize + if x isa MayAliasMemoryInfo + return y isa MayAliasMemoryInfo && x.aliases == y.aliases + else + x = x::MustAliasMemoryInfo + y isa MustAliasMemoryInfo || return false + return x === y + end +end +function copy(x::MemoryInfo) + @nospecialize x + if x isa MayAliasMemoryInfo + return MayAliasMemoryInfo(copy(x.aliases)) + end + return x +end + +abstract type ObjectInfo end +struct HasUnanalyzedMemory <: ObjectInfo end +const FieldInfos = Vector{MemoryInfo} +struct HasIndexableFields <: ObjectInfo + fields::FieldInfos +end +const CallerMemoryList = Vector{Union{CallerMemory,IdSet{CallerMemory}}} +struct HasIndexableCallerFields <: ObjectInfo + fields::FieldInfos + caller_memory_list::CallerMemoryList + function HasIndexableCallerFields( + fields::FieldInfos, + caller_memory_list::CallerMemoryList) + @assert length(fields) == length(caller_memory_list) + return new(fields, caller_memory_list) + end +end +struct HasUnknownMemory <: ObjectInfo end +const ⊥ₒ, ⊤ₒ = HasUnanalyzedMemory(), HasUnknownMemory() +x::ObjectInfo == y::ObjectInfo = begin + @nospecialize + x === y && return true + if x === ⊥ₒ + return y === ⊥ₒ + elseif x === ⊤ₒ + return y === ⊤ₒ + elseif x isa HasIndexableFields + y isa HasIndexableFields || return false + return x.fields == y.fields + else + x = x::HasIndexableCallerFields + y isa HasIndexableCallerFields || return false + return x.fields == y.fields && x.caller_memory_list == y.caller_memory_list + end +end +function copy(x::ObjectInfo) + @nospecialize x + if x isa HasIndexableFields + return HasIndexableFields( + MemoryInfo[copy(xfinfo) for xfinfo in x.fields]) + elseif x isa HasIndexableCallerFields + return HasIndexableCallerFields( + MemoryInfo[copy(xfinfo) for xfinfo in x.fields], + Union{CallerMemory,IdSet{CallerMemory}}[ + caller_memory isa IdSet{CallerMemory} ? copy(caller_memory) : caller_memory + for caller_memory in x.caller_memory_list]) + end + return x +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 + @nospecialize + 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 ∈ x::Liveness = begin + @nospecialize x + if x === ⊤ₗ + return true + elseif x === ⊥ₗ + return false + else + x = x::PCLiveness + return pc ∈ x.pcs + end +end +function isempty(x::Liveness) + @nospecialize x + if x === ⊥ₗ + return true + elseif x === ⊤ₗ + return false + else + return isempty(x.pcs) + end +end +function copy(x::Liveness) + @nospecialize x + if x isa PCLiveness + return PCLiveness(copy(x.pcs)) + end + return x +end +function delete!(x::Liveness, pc::Int) + @nospecialize x + if x isa PCLiveness + delete!(x.pcs, pc) + end + return x +end """ 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) * `pc ∈ x.ThrownEscape`: `x` may be thrown at the SSA statement at `pc` * `-1 ∈ x.ThrownEscape`: `x` may be thrown at arbitrary points of this call frame (the top) This information will be used by `escape_exception!` to propagate potential escapes via exception. -- `x.AliasInfo::Union{Bool,IndexableFields,Unindexable}`: maintains all possible values +- `x.ObjectInfo::ObjectInfo`: maintains all possible values that can be aliased to fields or array elements of `x`: - * `x.AliasInfo === false` indicates the fields/elements of `x` aren't analyzed yet - * `x.AliasInfo === true` indicates the fields/elements of `x` can't be analyzed, + * `x.ObjectInfo::HasUnanalyzedMemory` indicates the fields/elements of `x` aren't analyzed yet + * `x.ObjectInfo::HasUnknownMemory` indicates the fields/elements of `x` can't be analyzed, e.g. the type of `x` is not known or is not concrete and thus its fields/elements can't be known precisely - * `x.AliasInfo::IndexableFields` records all the possible values that can be aliased to fields of object `x` with precise index information - * `x.AliasInfo::Unindexable` records all the possible values that can be aliased to fields/elements of `x` without precise index information + * `x.ObjectInfo::HasIndexableFields` records all the possible values that can be aliased to fields of object `x` with precise index information - `x.Liveness::BitSet`: records SSA statement numbers where `x` should be live, e.g. to be used as a call argument, to be returned to a caller, or preserved for `:foreigncall`: * `isempty(x.Liveness)`: `x` is never be used in this call frame (the bottom) @@ -77,145 +231,94 @@ 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) +ArgLiveness() = 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(x::ObjectInfo=⊤ₒ) = EscapeInfo(false, false, x, ArgLiveness()) +CallerEscape() = EscapeInfo(false, false, ⊤ₒ, ArgLiveness()) # COMBAK allow only one-level inter-procedural alias analysis (for now) +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 +function is_not_analyzed(x::EscapeInfo) + if x.Liveness === BotLiveness() + @assert !x.ThrownEscape && !x.ReturnEscape && x.ObjectInfo === HasUnanalyzedMemory() + return true + else + return false + end +end +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 +function has_all_escape(x::EscapeInfo) + if x.Liveness === TopLiveness() # top-liveness == this object may exist anywhere + @assert x.ThrownEscape && x.ReturnEscape && x.ObjectInfo === HasUnknownMemory() + return true + else + return false + end +end # 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 @@ -225,59 +328,114 @@ end The non-strict partial order over [`EscapeInfo`](@ref). """ x::EscapeInfo ⊑ₑ y::EscapeInfo = begin + @nospecialize # 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 + 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 + @nospecialize + 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 + @nospecialize + if x === ⊥ₒ + return true + elseif x === ⊤ₒ + return y === ⊤ₒ + elseif y === ⊥ₒ + return false + elseif y === ⊤ₒ + return true + elseif x isa HasIndexableCallerFields + if y isa HasIndexableCallerFields + xcallers, ycallers = x.caller_memory_list, y.caller_memory_list + xn, yn = length(xcallers), length(ycallers) + 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 + xcallerᵢ = xcallers[i] + ycallerᵢ = ycallers[i] + if !(xcallerᵢ isa IdSet{CallerMemory}) + if !(ycallerᵢ isa IdSet{CallerMemory}) + xcallerᵢ == ycallerᵢ || return false + else + xcallerᵢ ∈ ycallerᵢ || return false + end + else + if !(ycallerᵢ isa IdSet{CallerMemory}) + return false + else + xcallerᵢ ⊆ ycallerᵢ || return false + end + end end + xfields, yfields = x.fields, y.fields + @goto compare_xfields_yfields else - ya === true || return false + return false end else - xa = xa::Unindexable - if isa(ya, Unindexable) - xinfo, yinfo = xa.info, ya.info - xinfo ⊆ yinfo || return false + x = x::HasIndexableFields + if y isa HasIndexableCallerFields + xfields, yfields = x.fields, y.fields + @goto compare_xfields_yfields else - ya === true || return false + y = y::HasIndexableFields + xfields, yfields = x.fields, y.fields + @label compare_xfields_yfields + xn, yn = length(xfields), length(yfields) + xn ≤ yn || return false + for i in 1:xn + xfields[i] ⊑ₘ yfields[i] || return false + end + return true end 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 + +x::MemoryInfo ⊑ₘ y::MemoryInfo = begin + @nospecialize + if x isa MustAliasMemoryInfo + if y isa MustAliasMemoryInfo + return x.alias === y.alias + else + y = y::MayAliasMemoryInfo + return x.alias ∈ y.aliases + end + else + x = x::MayAliasMemoryInfo + if y isa MustAliasMemoryInfo + return false + else + y = y::MayAliasMemoryInfo + return x.aliases ⊆ y.aliases + end end - return true end """ @@ -297,136 +455,308 @@ 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 = first(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) +x::Liveness ⊔ₗꜝ y::Liveness = begin + @nospecialize + 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 = (@nospecialize; first(copy(x) ⊔ₗꜝ y)) + +x::ObjectInfo ⊔ₒꜝ y::ObjectInfo = begin + @nospecialize + 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 !== ⊤ₒ + elseif x isa HasIndexableCallerFields + if y isa HasIndexableCallerFields + xfields, yfields = x.fields, y.fields + xcallers, ycallers = x.caller_memory_list, y.caller_memory_list + xn, yn = length(xfields), length(yfields) nmax, nmin = max(xn, yn), min(xn, yn) - infos = Vector{AInfo}(undef, nmax) + changed = false + if xn < nmax + resize!(xfields, nmax) + resize!(xcallers, nmax) + changed |= true + end for i in 1:nmax - if i > nmin - infos[i] = (xn > yn ? xinfos : yinfos)[i] + if nmin < i + if xn < nmax + xcallers[i] = copy(ycallers[i]) + end else - infos[i] = xinfos[i] ∪ yinfos[i] + xcallerᵢ, ycallerᵢ = xcallers[i], ycallers[i] + if !(xcallerᵢ isa IdSet{CallerMemory}) + if !(ycallerᵢ isa IdSet{CallerMemory}) + if xcallerᵢ ≠ ycallerᵢ + xcallers[i] = IdSet{CallerMemory}() + push!(xcallers[i], xcallerᵢ) + push!(xcallers[i], ycallerᵢ) + changed |= true + end + else + if xcallerᵢ ∉ ycallerᵢ + xcallers[i] = push!(copy(ycallerᵢ), xcallerᵢ) + changed |= true + end + end + else + if !(ycallerᵢ isa IdSet{CallerMemory}) + if xcallerᵢ ∌ ycallerᵢ + push!(xcallerᵢ, ycallerᵢ) + changed |= true + end + else + if xcallerᵢ ⊋ ycallerᵢ + union!(xcallerᵢ, ycallerᵢ) + changed |= true + end + end + end end end - return IndexableFields(infos) - elseif isa(ya, Unindexable) - xinfos, yinfo = xa.infos, ya.info - return merge_to_unindexable(ya, xa) + @goto merge_xfields_yfields else - return true # handle conflicting case conservatively + y = y::HasIndexableFields + xfields, yfields = x.fields, y.fields + xcallers = x.caller_memory_list + xn, yn = length(xfields), length(yfields) + nmax, nmin = max(xn, yn), min(xn, yn) + if xn < nmax + resize!(xfields, nmax) + resize!(xcallers, nmax) + for i = xn+1:nmax + xcallers[i] = IdSet{CallerMemory}() + end + end + changed = true + @goto merge_xfields_yfields end else - xa = xa::Unindexable - if isa(ya, IndexableFields) - return merge_to_unindexable(xa, ya) + x = x::HasIndexableFields + if y isa HasIndexableCallerFields + xfields, yfields = x.fields, y.fields + ycallers = copy(y.caller_memory_list) + xn, yn = length(xfields), length(yfields) + nmax, nmin = max(xn, yn), min(xn, yn) + if xn < nmax + resize!(xfields, nmax) + elseif yn < nmax + resize!(ycallers, nmax) + for i = yn+1:nmax + ycallers[i] = IdSet{CallerMemory}() + end + end + x = HasIndexableCallerFields(xfields, ycallers) + changed = true + @goto merge_xfields_yfields else - ya = ya::Unindexable - xinfo, yinfo = xa.info, ya.info - info = xinfo ∪ yinfo - return Unindexable(info) + y = y::HasIndexableFields + xfields, yfields = x.fields, y.fields + xn, yn = length(xfields), length(yfields) + nmax, nmin = max(xn, yn), min(xn, yn) + changed = false + if xn < nmax + resize!(xfields, nmax) + changed |= true + end + @label merge_xfields_yfields + for i in 1:nmax + if nmin < i + if xn < nmax + xfields[i] = copy(yfields[i]) + end + else + xfields[i], changed′ = xfields[i] ⊔ₘꜝ yfields[i] + changed |= changed′ + end + end + return x, changed end end end +x::ObjectInfo ⊔ₒ y::ObjectInfo = (@nospecialize; first(copy(x) ⊔ₒꜝ y)) + +x::MemoryInfo ⊔ₘꜝ y::MemoryInfo = begin + @nospecialize + if x isa MustAliasMemoryInfo + if y isa MustAliasMemoryInfo + if x.alias !== y.alias + # TODO [^CFG-aware `MemoryInfo`] + aliases = IdSet{Any}() + push!(aliases, x.alias) + push!(aliases, y.alias) + return MayAliasMemoryInfo(aliases), true + else + return x, false + end + else + y = y::MayAliasMemoryInfo + if x.alias ∈ y.aliases + return copy(y), true + else + x′ = copy(y) + push!(x′.aliases, x.alias) + return x′, true + end + end + else + x = x::MayAliasMemoryInfo + if y isa MustAliasMemoryInfo + if x.aliases ∋ y.alias + return x, false + else + push!(x.aliases, y.alias) + return x, true + end + else + y = y::MayAliasMemoryInfo + if x.aliases ⊇ y.aliases + return x, false + else + union!(x.aliases, y.aliases) + return x, true + end + end + end +end +x::MemoryInfo ⊔ₘ y::MemoryInfo = (@nospecialize; first(copy(x) ⊔ₘꜝ y)) -const AliasSet = IntDisjointSet{Int} - -""" - estate::EscapeState +const EscapeTable = IdDict{Int,EscapeInfo} # TODO `Dict` would be more efficient? -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} - aliasset::AliasSet +struct AnalysisFrameInfo nargs::Int + nstmts::Int + caller_memory_map::Vector{CallerMemory} 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) + +struct BlockEscapeState + escapes::EscapeTable + afinfo::AnalysisFrameInfo end -function getindex(estate::EscapeState, @nospecialize(x)) - xidx = iridx(x, estate) - return xidx === nothing ? nothing : estate.escapes[xidx] + +function getindex(bbstate::BlockEscapeState, @nospecialize(x)) + xidx = iridx(x, bbstate) + return xidx === nothing ? nothing : bbstate[xidx] end -function setindex!(estate::EscapeState, v::EscapeInfo, @nospecialize(x)) - xidx = iridx(x, estate) +function getindex(bbstate::BlockEscapeState, xidx::Int) + return get(bbstate.escapes, xidx) do + (; nargs, nstmts) = bbstate.afinfo + if xidx ≤ nargs + return ArgEscape() + elseif nargs < xidx ≤ nargs + nstmts + return ⊥ + else + error("The escape information for `CallerMemory` should be initialized in the `AnalysisState` constructor") + end + end +end +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, xinfo::EscapeInfo, xidx::Int) + return bbstate.escapes[xidx] = xinfo +end +function copy(bbstate::BlockEscapeState) + escapes = EscapeTable(i => copy(x) for (i, x) in bbstate.escapes) + return BlockEscapeState(escapes, bbstate.afinfo) +end +bbstate1::BlockEscapeState == bbstate2::BlockEscapeState = + bbstate1.escapes == bbstate2.escapes && bbstate1.afinfo === bbstate2.afinfo + +const AnalyzableIRElement = Union{Argument,SSAValue,CallerMemory} """ - 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`). +Tries to convert analyzable IR element `x::AnalyzableIRElement` to +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`. +`irval(iridx(x::AnalyzableIRElement, state), state) === x`. """ -function iridx(@nospecialize(x), estate::EscapeState) +iridx(@nospecialize(x), bbstate::BlockEscapeState) = iridx(x, bbstate.afinfo) +function iridx(@nospecialize(x), afinfo::AnalysisFrameInfo) + (; nargs, nstmts) = afinfo if isa(x, Argument) xidx = x.n - @assert 1 ≤ xidx ≤ estate.nargs "invalid Argument" + @assert 1 ≤ xidx ≤ nargs "invalid Argument" elseif isa(x, SSAValue) - xidx = x.id + estate.nargs + xidx = x.id + nargs + @assert nargs < xidx ≤ nargs + nstmts "invalid SSAValue" + elseif isa(x, CallerMemory) + xidx = nargs + nstmts + x.id + @assert nargs + nstmts < xidx ≤ nargs + nstmts + length(afinfo.caller_memory_map) "invalid CallerMemory" else return nothing end @@ -434,529 +764,1045 @@ function iridx(@nospecialize(x), estate::EscapeState) end """ - irval(xidx::Int, estate::EscapeState) -> x::Union{Argument,SSAValue} + irval(xidx::Int, bbstate::BlockEscapeState) -> x::AnalyzableIRElement -Converts its unique identifier number `xidx` to the original IR element `x::Union{Argument,SSAValue}` -that is analyzable in the context of `estate`. +Converts its unique identifier number `xidx` to the original IR element `x::AnalyzableIRElement` +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) -end - -function getaliases(x::Union{Argument,SSAValue}, estate::EscapeState) - xidx = iridx(x, estate) - aliases = getaliases(xidx, estate) - aliases === nothing && return nothing - return Union{Argument,SSAValue}[irval(aidx, estate) 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 - # 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 - push!(aliases, aidx) - end - end - return aliases +irval(xidx::Int, bbstate::BlockEscapeState) = irval(xidx, bbstate.afinfo) +function irval(xidx::Int, afinfo::AnalysisFrameInfo) + (; nargs, nstmts) = afinfo + if xidx ≤ nargs + return Argument(xidx) + elseif xidx ≤ nargs + nstmts + return SSAValue(xidx - nargs) + elseif nargs + nstmts < xidx ≤ nargs + nstmts + length(afinfo.caller_memory_map) + return afinfo.caller_memory_map[xidx - nargs - nstmts] else - return nothing + error("invalid xidx::Int") 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) +abstract type Change end +const Changes = Vector{Change} -struct ArgEscapeInfo - escape_bits::UInt8 -end -function ArgEscapeInfo(x::EscapeInfo) - x === ⊤ && return ArgEscapeInfo(ARG_ALL_ESCAPE) - escape_bits = 0x00 - has_return_escape(x) && (escape_bits |= ARG_RETURN_ESCAPE) - has_thrown_escape(x) && (escape_bits |= ARG_THROWN_ESCAPE) - return ArgEscapeInfo(escape_bits) -end +const SSAMemoryInfo = IdDict{Int,MemoryInfo} -const ARG_ALL_ESCAPE = 0x01 << 0 -const ARG_RETURN_ESCAPE = 0x01 << 1 -const ARG_THROWN_ESCAPE = 0x01 << 2 +const LINEAR_BBESCAPES = Union{Bool,BlockEscapeState}[false] -has_no_escape(x::ArgEscapeInfo) = !has_all_escape(x) && !has_return_escape(x) && !has_thrown_escape(x) -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 - -struct ArgAliasing - aidx::Int - bidx::Int -end +const AliasSet = IntDisjointSet{Int} -struct ArgEscapeCache - argescapes::Vector{ArgEscapeInfo} - argaliases::Vector{ArgAliasing} - function ArgEscapeCache(estate::EscapeState) - nargs = estate.nargs - argescapes = Vector{ArgEscapeInfo}(undef, nargs) - argaliases = ArgAliasing[] - for i = 1:nargs - info = estate.escapes[i] - @assert info.AliasInfo === true - argescapes[i] = ArgEscapeInfo(info) - for j = (i+1):nargs - if isaliased(i, j, estate) - push!(argaliases, ArgAliasing(i, j)) - end +struct AnalysisState{GetEscapeCache} + ir::IRCode + afinfo::AnalysisFrameInfo + new_nodes_map::Union{Nothing,IdDict{Int,Vector{Pair{Int,NewNodeInfo}}}} + # escape states for each basic block: + # - `bbescape === false` indicates the state for the block has not been initialized + # - `bbescape === true` indicates the state for the block is known to be identical to + # the state of its single predecessor + bbescapes::Vector{Union{Bool,BlockEscapeState}} + aliasset::AliasSet + get_escape_cache::GetEscapeCache + #= results =# + retescape::BlockEscapeState + ssamemoryinfo::SSAMemoryInfo + caller_memory_map::Vector{CallerMemory} + #= temporary states =# + currstate::BlockEscapeState # 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, get_escape_cache) + if isempty(ir.new_nodes) + new_nodes_map = nothing + else + new_nodes_map = IdDict{Int,Vector{Pair{Int,NewNodeInfo}}}() + for (i, nni) in enumerate(ir.new_nodes.info) + if haskey(new_nodes_map, nni.pos) + push!(new_nodes_map[nni.pos], i => nni) + else + new_nodes_map[nni.pos] = Pair{Int,NewNodeInfo}[i => nni] end end - return new(argescapes, argaliases) end + nbbs = length(ir.cfg.blocks) + nstmts = length(ir.stmts) + length(ir.new_nodes) + caller_memory_map = CallerMemory[] + currtable = EscapeTable() + aliasset = AliasSet(nargs + nstmts) + for argn = 1:nargs + object_info = initialize_argument_object_info!( + caller_memory_map, currtable, aliasset, argn, ir, nargs, nstmts) + currtable[argn] = ArgEscape(object_info) + end + afinfo = AnalysisFrameInfo(nargs, nstmts, caller_memory_map) + if nbbs == 1 # optimization for linear control flow + bbescapes = LINEAR_BBESCAPES # avoid unnecessary allocation + retescape = currstate = BlockEscapeState(currtable, afinfo) # no need to maintain a separate state + else + bbescapes = fill!(Vector{Union{Bool,BlockEscapeState}}(undef, nbbs), false) + retescape = BlockEscapeState(EscapeTable(), afinfo) + currstate = BlockEscapeState(currtable, afinfo) + end + retescape[0] = ⊥ + ssamemoryinfo = SSAMemoryInfo() + changes = Changes() + visited = BitSet() + equalized_roots = BitSet() + handler_info = compute_trycatch(ir) + return AnalysisState( + ir, afinfo, new_nodes_map, + bbescapes, aliasset, get_escape_cache, + retescape, ssamemoryinfo, caller_memory_map, + currstate, changes, visited, equalized_roots, handler_info) +end + +# COMBAK allow only one-level inter-procedural alias analysis (for now) + +function initialize_argument_object_info!( + caller_memory_map::Vector{CallerMemory}, currtable::EscapeTable, aliasset::AliasSet, + argn::Int, ir::IRCode, nargs::Int, nstmts::Int) + arg = Argument(argn) + argtyp = argextype(arg, ir) + is_identity_free_argtype(argtyp) && return ⊥ₒ + typ = argument_datatype(argtyp) + typ isa DataType || return ⊤ₒ + nflds = fieldcount_noerror(typ) + if nflds === nothing + return ⊤ₒ + end + fields = FieldInfos(undef, nflds) + caller_memory_list = CallerMemoryList(undef, nflds) + nmin = datatype_min_ninitialized(typ) + for fidx = 1:nflds + caller_memory = add_caller_memory!( + caller_memory_map, currtable, aliasset, + arg, fidx, nargs, nstmts) + if fidx > nmin # maybe undef + aliases = IdSet{Any}() + push!(aliases, caller_memory) + push!(aliases, UninitializedMemory()) + memory_info = MayAliasMemoryInfo(aliases) + else + memory_info = MustAliasMemoryInfo(caller_memory) + end + fields[fidx] = memory_info + caller_memory_list[fidx] = caller_memory + end + # return HasIndexableCallerFields(fields, caller_memory_list) + return HasIndexableFields(fields) end -abstract type Change end -struct EscapeChange <: Change - xidx::Int - xinfo::EscapeInfo +function add_caller_memory!( + caller_memory_map::Vector{CallerMemory}, currtable::EscapeTable, aliasset::AliasSet, + arg::Argument, fidx::Int, nargs::Int, nstmts::Int) + id = length(caller_memory_map) + 1 + cidx = nargs + nstmts + id + currtable[cidx] = CallerEscape() + aidx = push!(aliasset) + @assert cidx == aidx + caller_memory = CallerMemory(id, arg, fidx) + push!(caller_memory_map, caller_memory) + return caller_memory end -struct AliasChange <: Change - xidx::Int - yidx::Int + +function getaliases(aliasset::AliasSet, afinfo::AnalysisFrameInfo, x::AnalyzableIRElement) + aliases = getaliases(aliasset, iridx(x, afinfo)) + aliases === nothing && return nothing + return (irval(aidx, afinfo) for aidx in aliases) end -struct ArgAliasChange <: Change - xidx::Int - yidx::Int +function getaliases(aliasset::AliasSet, xidx::Int) + xroot, hasalias = getaliasroot!(aliasset, xidx) + if hasalias + # the size of this alias set containing `key` is larger than 1, + # collect the entire alias set + return (aidx for aidx in 1:length(aliasset.parents) + if _find_root_impl!(aliasset.parents, aidx) == xroot) + else + return nothing + end end -struct LivenessChange <: Change - xidx::Int - livepc::Int +@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 -const Changes = Vector{Change} +getaliases(astate::AnalysisState, x::AnalyzableIRElement) = getaliases(astate.aliasset, astate.afinfo, x) +getaliases(astate::AnalysisState, xidx::Int) = getaliases(astate.aliasset, xidx) -struct AnalysisState{GetEscapeCache, Lattice<:AbstractLattice} - ir::IRCode - estate::EscapeState - changes::Changes - 𝕃ₒ::Lattice - get_escape_cache::GetEscapeCache +isaliased(aliasset::AliasSet, afinfo::AnalysisFrameInfo, x::AnalyzableIRElement, y::AnalyzableIRElement) = isaliased(aliasset, iridx(x, afinfo), iridx(y, afinfo)) +isaliased(aliasset::AliasSet, xidx::Int, yidx::Int) = in_same_set(aliasset, xidx, yidx) +isaliased(astate::AnalysisState, x::AnalyzableIRElement, y::AnalyzableIRElement) = isaliased(astate.aliasset, astate.afinfo, x, y) +isaliased(astate::AnalysisState, xidx::Int, yidx::Int) = isaliased(astate.aliasset, xidx, yidx) + +""" + 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 + afinfo::AnalysisFrameInfo + bbescapes::Vector{Union{Bool,BlockEscapeState}} + aliasset::AliasSet + retescape::BlockEscapeState + ssamemoryinfo::SSAMemoryInfo + caller_memory_map::Vector{CallerMemory} + function EscapeResult(astate::AnalysisState) + return new(astate.afinfo, astate.bbescapes, astate.aliasset, astate.retescape, + astate.ssamemoryinfo, astate.caller_memory_map) + end +end +iridx(x::AnalyzableIRElement, eresult::EscapeResult) = iridx(x, eresult.afinfo) +irval(xidx::Int, eresult::EscapeResult) = irval(xidx, eresult.afinfo) +getindex(eresult::EscapeResult, @nospecialize(x)) = getindex(eresult.retescape, x) +getaliases(eresult::EscapeResult, x::AnalyzableIRElement) = getaliases(eresult.aliasset, eresult.afinfo, x) +getaliases(eresult::EscapeResult, xidx::Int) = getaliases(eresult.aliasset, xidx) +isaliased(eresult::EscapeResult, x::AnalyzableIRElement, y::AnalyzableIRElement) = isaliased(eresult.aliasset, eresult.afinfo, x, y) +isaliased(eresult::EscapeResult, xidx::Int, yidx::Int) = isaliased(eresult.aliasset, xidx, yidx) + +function is_load_forwardable((; ssamemoryinfo)::EscapeResult, pc::Int) + haskey(ssamemoryinfo, pc) || return false + return ssamemoryinfo[pc] isa MustAliasMemoryInfo 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 -- `get_escape_cache(::MethodInstance) -> Union{Bool,ArgEscapeCache}`: +- `get_escape_cache(::MethodInstance) -> Union{Bool,EscapeCache}`: 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) +function analyze_escapes(ir::IRCode, nargs::Int, get_escape_cache) + currbb = 1 + bbs = ir.cfg.blocks + W = BitSet() + W.offset = 0 # for _bits_findnext + astate = AnalysisState(ir, nargs, get_escape_cache) + (; new_nodes_map, currstate) = astate - 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 while true - local anyupdate = false - - for pc in nstmts:-1:1 - 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 + local nextbb::Int, nextcurrbb::Int + bbstart, bbend = first(bbs[currbb].stmts), last(bbs[currbb].stmts) + for pc = bbstart:bbend + local new_nodes_counter::Int = 0 + if new_nodes_map === nothing || pc ∉ keys(new_nodes_map) + stmt = ir[SSAValue(pc)][:stmt] + isterminator = pc == bbend + else + new_nodes = new_nodes_map[pc] + attach_before_idxs = Int[i for (i, nni) in new_nodes if !nni.attach_after] + attach_after_idxs = Int[i for (i, nni) in new_nodes if nni.attach_after] + na, nb = length(attach_after_idxs), length(attach_before_idxs) + n_nodes = new_nodes_counter = na + nb + 1 # +1 for this statement + @label analyze_new_node + curridx = n_nodes - new_nodes_counter + 1 + if curridx ≤ nb + stmt = ir.new_nodes.stmts[attach_before_idxs[curridx]][:stmt] + elseif curridx == nb + 1 + stmt = ir[SSAValue(pc)][:stmt] else - add_conservative_changes!(astate, pc, stmt.args) + @assert curridx ≤ n_nodes + stmt = ir.new_nodes.stmts[attach_after_idxs[curridx - nb - 1]][:stmt] 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)) + isterminator = curridx == n_nodes + new_nodes_counter -= 1 + end + + if isterminator + # 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] + falseret = propagate_bbstate!(astate, currstate, falsebb, #=allow_direct_propagation=#!(@isdefined nextcurrbb)) + if falseret === nothing + @assert currbb == only(bbs[falsebb].preds) + nextcurrbb = falsebb + elseif falseret + 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 + if length(bbs) == 1 # see the constructor of `AnalysisState` + @assert astate.retescape === currstate # `astate.retescape` has been updated in-place + else + propagate_ret_state!(astate, currstate) + end + if isdefined(stmt, :val) + # Accumulate the escape information of the return value + # so that it can be expanded in the caller context. + retval = stmt.val + if retval isa GlobalRef + with_profitable_irval(astate, retval) do _::Int + astate.retescape[0] = ⊤ + end + elseif retval isa SSAValue || retval isa Argument + retinfo = currstate[retval] + newrinfo, changed = astate.retescape[0] ⊔ₑꜝ retinfo + if changed + astate.retescape[0] = newrinfo + end + end + end + @goto next_bb + elseif stmt isa EnterNode + @goto fall_through + elseif isexpr(stmt, :leave) + @goto fall_through 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 + # fall through terminator – treat as a regular statement end - isempty(changes) && continue + # process non control-flow statements + analyze_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)::Bool + push!(W, excbb) + end + end + else + # propagate the escape information of this block to the exception handler block + # even if there are no changes made on this statement + if !is_nothrow(ir, pc) + curr_hand = gethandler(astate.handler_info, pc) + if curr_hand !== nothing + enter_node = ir[SSAValue(curr_hand.enter_idx)][:stmt]::EnterNode + if propagate_bbstate!(astate, currstate, enter_node.catch_dest)::Bool + push!(W, enter_node.catch_dest) + end + end + end + end - anyupdate |= propagate_changes!(estate, changes) + if new_nodes_counter > 0 + @goto analyze_new_node + end + end - empty!(changes) + begin @label fall_through + nextbb = currbb + 1 end - tryregions !== nothing && escape_exception!(astate, tryregions) + begin @label propagate_state + ret = propagate_bbstate!(astate, currstate, nextbb, #=allow_direct_propagation=#!(@isdefined nextcurrbb)) + if ret === nothing + @assert currbb == only(bbs[nextbb].preds) + nextcurrbb = nextbb + elseif ret + push!(W, nextbb) + end + end - debug_itr_counter += 1 + begin @label next_bb + if !(@isdefined nextcurrbb) + nextcurrbb = _bits_findnext(W.bits, 1) + nextcurrbb == -1 && break # the working set is empty + delete!(W, nextcurrbb) + nextcurrstate = astate.bbescapes[nextcurrbb] + if nextcurrstate isa Bool + @assert nextcurrstate === false + empty!(currstate.escapes) # initialize the state + else + copy!(currstate.escapes, nextcurrstate.escapes) # overwrite the state + end + else + # propagate the current state to the next block directly + end + currbb = nextcurrbb + end + end - anyupdate || break + return EscapeResult(astate) +end + +function propagate_bbstate!(astate::AnalysisState, currstate::BlockEscapeState, nextbbidx::Int, + allow_direct_propagation::Bool=false) + bbescapes = astate.bbescapes + nextstate = bbescapes[nextbbidx] + if nextstate isa Bool + nextbb = astate.ir.cfg.blocks[nextbbidx] + if allow_direct_propagation && length(nextbb.stmts) == 1 && length(nextbb.preds) == 1 + # Performance optimization: + # If the next block has a single predecessor (the current block) and it has a + # single statament which is a non-returning terminator, then it can simply + # propagate the current state to the subsequent block(s). This is valid because + # it does not update the escape information at all, and even if the state is + # updated during the further iterations of abstract interpretation, the updated + # state is always propagated from the predecessor. + # In such cases, set `bbescapes[nextbbidx] = true` to explicitly indicate that + # the state for this next block is identical to the state of the predecessor, + # avoiding unnecessary copying of the state. + nextpc = only(nextbb.stmts) + new_nodes_map = astate.new_nodes_map + if new_nodes_map === nothing || nextpc ∉ keys(new_nodes_map) + nextstmt = astate.ir[SSAValue(nextpc)][:stmt] + if is_stmt_escape_free(nextstmt) + bbescapes[nextbbidx] = true + return nothing + end + end + end + bbescapes[nextbbidx] = 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 +# NOTE we can't include `ReturnNode` here since it may add `ReturnEscape` change +is_stmt_escape_free(@nospecialize(stmt)) = + stmt === nothing || stmt isa GotoNode || stmt isa Core.GotoIfNot || + stmt isa EnterNode || isexpr(stmt, :leave) - return estate +function propagate_bbstate!(nextstate::BlockEscapeState, currstate::BlockEscapeState) + anychanged = false + for (idx, newinfo) in currstate.escapes + if haskey(nextstate.escapes, idx) + 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 + 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) +# 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) + 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, 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) + (; xidx) = change + oldinfo = astate.currstate[xidx] + newnewinfo, changed = oldinfo ⊔ₑꜝ ⊤ + 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 +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.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.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, 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 + __add_all_escape_change!(astate, xidx) + end +end +function __add_all_escape_change!(astate::AnalysisState, 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 + +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 - return nothing end -function add_liveness_change!(astate::AnalysisState, @nospecialize(x), livepc::Int) - xidx = iridx(x, astate.estate) +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 + nothing +end + +function with_profitable_irvals(callback, astate::AnalysisState, @nospecialize(x), @nospecialize(y)) + xidx = iridx(x, astate.currstate) + yidx = iridx(y, astate.currstate) + if xidx !== nothing && yidx !== nothing + if (!is_identity_free_argtype(argextype(x, astate.ir)) && + !is_identity_free_argtype(argextype(y, astate.ir))) + callback(xidx, yidx) + end + end + nothing +end + +function traverse_object_memory(callback, astate::AnalysisState, xidx::Int, + track_visited::Bool=true) + (; ObjectInfo) = astate.currstate[xidx] + if ObjectInfo isa HasIndexableFields && (!track_visited || xidx ∉ astate.visited) + track_visited && push!(astate.visited, xidx) # avoid infinite traversal for cyclic references + for xfinfo in ObjectInfo.fields + if xfinfo isa MustAliasMemoryInfo + callback(xfinfo.alias) + else + xfinfo = xfinfo::MayAliasMemoryInfo + for aval in xfinfo.aliases + callback(aval) + end + end end end - return nothing 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) - if xidx !== nothing && yidx !== nothing - if !isaliased(xidx, yidx, astate.estate) - pushfirst!(astate.changes, AliasChange(xidx, yidx)) + with_profitable_irvals(astate, x, y) do xidx::Int, yidx::Int + if !isaliased(astate, 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, xidx) 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) + with_profitable_irval(astate, y) do yidx::Int + if !isaliased(astate, 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 -function escape_edges!(astate::AnalysisState, pc::Int, edges::Vector{Any}) +# Analyze +# ======= +# Subroutines to analyze the current statement and add `Change`s from it + +function analyze_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 + analyze_call!(astate, pc, stmt.args) + elseif head === :invoke + analyze_invoke!(astate, pc, stmt.args) + elseif head === :new || head === :splatnew + analyze_new!(astate, pc, stmt.args) + elseif head === :foreigncall + analyze_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 + analyze_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) + analyze_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) + analyze_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 analyze_edges!(astate::AnalysisState, pc::Int, edges::Vector{Any}) ret = SSAValue(pc) for i in 1:length(edges) if isassigned(edges, i) - v = edges[i] - add_alias_change!(astate, ret, v) + add_alias_change!(astate, ret, edges[i]) end end end -function escape_val_ifdefined!(astate::AnalysisState, pc::Int, x) - if isdefined(x, :val) - escape_val!(astate, pc, x.val) - end +struct Parameter <: MemoryKind + n::Int end - -function escape_val!(astate::AnalysisState, pc::Int, @nospecialize(val)) - ret = SSAValue(pc) - add_alias_change!(astate, ret, val) +Parameter(arg::Argument) = Parameter(arg.n) +struct ParameterMemory <: MemoryKind + id::Int + param::Parameter + fidx::Int end +ParameterMemory((; id, arg, fidx)::CallerMemory) = ParameterMemory(id, Parameter(arg), fidx) -function escape_unanalyzable_obj!(astate::AnalysisState, @nospecialize(obj), objinfo::EscapeInfo) - objinfo = EscapeInfo(objinfo, true) - add_escape_change!(astate, obj, objinfo) - return objinfo +abstract type InterMemoryInfo end +struct InterMustAliasMemoryInfo <: InterMemoryInfo + alias::Any end +struct InterMayAliasMemoryInfo <: InterMemoryInfo + aliases::Vector{Any} +end +abstract type InterObjectInfo end +struct HasInterUnanalyzedMemory <: InterObjectInfo end +struct HasInterIndexableFields <: InterObjectInfo + fields::Vector{InterMemoryInfo} +end +struct HasInterUnknownMemory <: InterObjectInfo end +const ⊥ₒ̅, ⊤ₒ̅ = HasInterUnanalyzedMemory(), HasInterUnknownMemory() -is_nothrow(ir::IRCode, pc::Int) = has_flag(ir[SSAValue(pc)], IR_FLAG_NOTHROW) +struct InterEscapeInfo + escape_bits::UInt8 + InterObjectInfo::InterObjectInfo +end +function InterEscapeInfo(x::EscapeInfo) + has_all_escape(x) && return InterEscapeInfo(ARG_ALL_ESCAPE, ⊤ₒ̅) + escape_bits = 0x00 + has_return_escape(x) && (escape_bits |= ARG_RETURN_ESCAPE) + has_thrown_escape(x) && (escape_bits |= ARG_THROWN_ESCAPE) + InterObjectInfo = convert_to_inter_object_info(x.ObjectInfo) + return InterEscapeInfo(escape_bits, InterObjectInfo) +end -""" - 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 +function convert_to_inter_object_info(@nospecialize(x::ObjectInfo)) + if x === ⊥ₒ + return ⊥ₒ̅ + elseif x === ⊤ₒ + return ⊤ₒ̅ + else + x = x::Union{HasIndexableFields,HasIndexableCallerFields} + nf = length(x.fields) + inter_fields = Vector{InterMemoryInfo}(undef, nf) + for i = 1:nf + xfinfo = x.fields[i] + if xfinfo isa MayAliasMemoryInfo + inter_aliases = Any[] + for aval in xfinfo.aliases + push!(inter_aliases, convert_to_inter_irval(aval)) + end + inter_xfinfo = InterMayAliasMemoryInfo(inter_aliases) + else + xfinfo = xfinfo::MustAliasMemoryInfo + inter_xfinfo = InterMustAliasMemoryInfo(convert_to_inter_irval(xfinfo.alias)) end + inter_fields[i] = inter_xfinfo end - continue - @label propagate_exception_escape - xval = irval(i, estate) - add_escape_change!(astate, xval, excinfo) + return HasInterIndexableFields(inter_fields) end end -# escape statically-resolved call, i.e. `Expr(:invoke, ::MethodInstance, ...)` -function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}) - codeinst = first(args) - if codeinst isa MethodInstance - mi = codeinst +function convert_to_inter_irval(@nospecialize(val)) + if val isa Argument + return Parameter(val) + elseif val isa SSAValue + println("TODO") + elseif val isa CallerMemory + return ParameterMemory(val) else - mi = (codeinst::CodeInstance).def + return val + end +end + +const ARG_ALL_ESCAPE = 0x01 << 0 +const ARG_RETURN_ESCAPE = 0x01 << 1 +const ARG_THROWN_ESCAPE = 0x01 << 2 + +has_no_escape(x::InterEscapeInfo) = !has_all_escape(x) && !has_return_escape(x) && !has_thrown_escape(x) +has_all_escape(x::InterEscapeInfo) = x.escape_bits & ARG_ALL_ESCAPE ≠ 0 +has_return_escape(x::InterEscapeInfo) = x.escape_bits & ARG_RETURN_ESCAPE ≠ 0 +has_thrown_escape(x::InterEscapeInfo) = x.escape_bits & ARG_THROWN_ESCAPE ≠ 0 + +struct EscapeCache + nparams::Int + param_memory_map::Vector{ParameterMemory} + escapes::Vector{InterEscapeInfo} + aliasset::AliasSet + retescape::InterEscapeInfo # TODO maintain distingushied memory for return values? + function EscapeCache(eresult::EscapeResult) + (; nargs, nstmts, caller_memory_map) = eresult.afinfo + param_memory_map = ParameterMemory[ + ParameterMemory(caller_memory) for caller_memory in caller_memory_map] + n_caller_memory = length(caller_memory_map) + nelms = nargs + n_caller_memory + cached_escapes = Vector{InterEscapeInfo}(undef, nelms) + cached_aliasset = AliasSet(nelms) + for i = 1:nargs + xidx = i + cached_xidx = xidx + cached_escapes[cached_xidx] = InterEscapeInfo(eresult[xidx]) + add_alias_for_cache!(cached_aliasset, eresult, xidx, cached_xidx) + end + for i = 1:n_caller_memory + xidx = nargs + nstmts + i + cached_xidx = xidx - nstmts + cached_escapes[cached_xidx] = InterEscapeInfo(eresult[xidx]) + add_alias_for_cache!(cached_aliasset, eresult, xidx, cached_xidx) + end + retescape = InterEscapeInfo(eresult[0]) + return new(nargs, param_memory_map, cached_escapes, cached_aliasset, retescape) + end +end + +function add_alias_for_cache!(cached_aliasset::AliasSet, eresult::EscapeResult, xidx::Int, cached_xidx::Int) + aliases = getaliases(eresult, xidx) + if aliases !== nothing + for aidx in aliases + aval = irval(aidx, eresult) + if aval isa Argument + paramidx = aval.n + union!(cached_aliasset, cached_xidx, paramidx) + elseif aval isa SSAValue + println("TODO") + elseif aval isa CallerMemory + param_memory_id = aval.id + eresult.afinfo.nargs + union!(cached_aliasset, cached_xidx, param_memory_id) + end + end end +end + +# analyze statically-resolved call, i.e. `Expr(:invoke, ::MethodInstance, ...)` +function analyze_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}) + codeinst = first(args) first_idx, last_idx = 2, length(args) - add_liveness_changes!(astate, pc, 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(codeinst) - ret = SSAValue(pc) - if cache isa Bool - if cache + add_liveness_changes!(astate, args, first_idx, last_idx) + escape_cache = astate.get_escape_cache(codeinst) + retval = SSAValue(pc) + if escape_cache isa Bool + if escape_cache # This method call is very simple and has good effects, so there's no need to # escape its arguments. However, since the arguments might be returned, we need # to consider the possibility of aliasing between them and the return value. @@ -966,64 +1812,130 @@ function escape_invoke!(astate::AnalysisState, pc::Int, args::Vector{Any}) continue # :effect_free guarantees that nothings escapes to the global scope end if !is_identity_free_argtype(argextype(arg, astate.ir)) - add_alias_change!(astate, ret, arg) + add_alias_change!(astate, retval, arg) end end return nothing else - return add_conservative_changes!(astate, pc, args, 2) + return add_conservative_changes!(astate, pc, args, #=first_idx=#2) end end - cache = cache::ArgEscapeCache - retinfo = astate.estate[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) - arg = args[argidx] - if i > nargs + escape_cache = escape_cache::EscapeCache + for (paramidx, argidx) in enumerate(first_idx:last_idx) + if paramidx > escape_cache.nparams # handle isva signature # COMBAK will this be invalid once we take alias information into account? - i = nargs + paramidx = escape_cache.nparams + end + add_inter_procedural_change!(astate, retval, args, paramidx, argidx, escape_cache) + end + # # propagate the escape information of the return value to the `retval::SSAValue` + # add_inter_procedural_change(astate, retval, retval, retescape) +end + +# TODO account for aliasing between parameter memory + +function add_inter_procedural_change!(astate::AnalysisState, + retval::SSAValue, args::Vector{Any}, paramidx::Int, argidx::Int, escape_cache::EscapeCache) + argval = args[argidx] + argescape = escape_cache.escapes[paramidx] + if has_all_escape(argescape) + add_all_escape_change!(astate, argval) + else + if !is_nothrow(astate.ir, retval) && has_thrown_escape(argescape) + add_thrown_escape_change!(astate, argval) end - 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) 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 - add_alias_change!(astate, ret, arg) + add_alias_change!(astate, argval, retval) end + object_info = convert_to_object_info(argescape.InterObjectInfo, astate, args) + add_object_info_change!(astate, argval, object_info) + add_liveness_change!(astate, argval) end - for (; aidx, bidx) in cache.argaliases - 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)) end -""" - from_interprocedural(argescape::ArgEscapeInfo, pc::Int) -> x::EscapeInfo +function convert_to_object_info(@nospecialize(x::InterObjectInfo), astate::AnalysisState, args::Vector{Any}) + if x === ⊥ₒ̅ + return ⊥ₒ + elseif x === ⊤ₒ̅ + return ⊤ₒ + else + x = x::HasInterIndexableFields + nf = length(x.fields) + fields = FieldInfos(undef, nf) + for i = 1:nf + inter_xfinfo = x.fields[i] + if inter_xfinfo isa InterMayAliasMemoryInfo + aliases = IdSet{Any}() + for j = 1:length(inter_xfinfo.aliases) + irvalsingle = convert_to_irval(inter_xfinfo.aliases[j], astate, args) + if irvalsingle === nothing + return ⊤ₒ + end + irval, single = irvalsingle + if single + push!(aliases, irval) + else + for ival in irval + push!(aliases, aval) + end + end + end + xfinfo = MayAliasMemoryInfo(aliases) + else + inter_xfinfo = inter_xfinfo::InterMustAliasMemoryInfo + irvalsingle = convert_to_irval(inter_xfinfo.alias, astate, args) + if irvalsingle === nothing + return ⊤ₒ + end + irval, single = irvalsingle + if single + xfinfo = MustAliasMemoryInfo(irval) + else + aliases = IdSet{Any}() + for ival in irval + push!(aliases, ival) + end + xfinfo = MayAliasMemoryInfo(aliases) + end + end + fields[i] = xfinfo + end + return HasIndexableFields(fields) + end +end + +function convert_to_irval(@nospecialize(interval), astate::AnalysisState, args::Vector{Any}) + if interval isa Parameter + return args[interval.n+1], true + elseif interval isa ParameterMemory + arg = args[interval.param.n+1] + if arg isa SSAValue || arg isa Argument + ainfo = astate.currstate[arg] + object_info = ainfo.ObjectInfo + if object_info isa HasIndexableFields + @assert 1 ≤ interval.fidx ≤ length(object_info.fields) + afinfo = object_info.fields[interval.fidx] + if afinfo isa MayAliasMemoryInfo + return afinfo.aliases, false + else + afinfo = afinfo::MustAliasMemoryInfo + return afinfo.alias, true + end + else + return nothing + end + else + return nothing + end + else + return interval, true + end +end -Reinterprets the escape information imposed on the call argument which is cached as `argescape` -in the context of the caller frame, where `pc` is the SSA statement number of the return value. -""" -function from_interprocedural(argescape::ArgEscapeInfo, pc::Int) - has_all_escape(argescape) && return ⊤ - ThrownEscape = has_thrown_escape(argescape) ? BitSet(pc) : BOT_THROWN_ESCAPE - # 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 - # a callee anyway, but still we may want some field analysis for e.g. stack allocation - # or some other IPO optimizations - AliasInfo = true - Liveness = BitSet(pc) - return EscapeInfo(#=Analyzed=#true, #=ReturnEscape=#false, ThrownEscape, AliasInfo, Liveness) -end - -# escape every argument `(args[6:length(args[3])])` and the name `args[1]` +# analyze every argument `(args[6:length(args[3])])` and the name `args[1]` # TODO: we can apply a similar strategy like builtin calls to specialize some foreigncalls -function escape_foreigncall!(astate::AnalysisState, pc::Int, args::Vector{Any}) +function analyze_foreigncall!(astate::AnalysisState, pc::Int, args::Vector{Any}) nargs = length(args) if nargs < 6 # invalid foreigncall, just escape everything @@ -1035,28 +1947,28 @@ 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 -function escape_gc_preserve!(astate::AnalysisState, pc::Int, args::Vector{Any}) +function analyze_gc_preserve!(astate::AnalysisState, pc::Int, args::Vector{Any}) @assert length(args) == 1 "invalid :gc_preserve_end" val = args[1] @assert val isa SSAValue "invalid :gc_preserve_end" @@ -1064,30 +1976,28 @@ 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}) +function analyze_call!(astate::AnalysisState, pc::Int, args::Vector{Any}) ft = argextype(first(args), astate.ir) 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 - result = escape_builtin!(f, astate, pc, args) + result = analyze_builtin!(f, astate, pc, args) 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 @@ -1095,20 +2005,19 @@ function escape_call!(astate::AnalysisState, pc::Int, args::Vector{Any}) end end -escape_builtin!(@nospecialize(f), _...) = missing +analyze_builtin!(@nospecialize(f), _...) = missing # safe builtins -escape_builtin!(::typeof(isa), _...) = false -escape_builtin!(::typeof(typeof), _...) = false -escape_builtin!(::typeof(sizeof), _...) = false -escape_builtin!(::typeof(===), _...) = false -escape_builtin!(::typeof(Core.donotdelete), _...) = false +analyze_builtin!(::typeof(isa), _...) = false +analyze_builtin!(::typeof(typeof), _...) = false +analyze_builtin!(::typeof(sizeof), _...) = false +analyze_builtin!(::typeof(===), _...) = false +analyze_builtin!(::typeof(Core.donotdelete), _...) = false # not really safe, but `ThrownEscape` will be imposed later -escape_builtin!(::typeof(isdefined), _...) = false -escape_builtin!(::typeof(throw), _...) = false -escape_builtin!(::typeof(Core.throw_methoderror), _...) = false +analyze_builtin!(::typeof(throw), _...) = false +analyze_builtin!(::typeof(Core.throw_methoderror), _...) = false -function escape_builtin!(::typeof(ifelse), astate::AnalysisState, pc::Int, args::Vector{Any}) +function analyze_builtin!(::typeof(ifelse), astate::AnalysisState, pc::Int, args::Vector{Any}) length(args) == 4 || return false f, cond, th, el = args ret = SSAValue(pc) @@ -1126,249 +2035,206 @@ function escape_builtin!(::typeof(ifelse), astate::AnalysisState, pc::Int, args: return false end -function escape_builtin!(::typeof(typeassert), astate::AnalysisState, pc::Int, args::Vector{Any}) +function analyze_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 analyze_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) + fields = FieldInfos(undef, nflds) + for i = 1:nflds + if i+1 > nargs + xfinfo = MustAliasMemoryInfo(UninitializedMemory()) + else + arg = args[i+1] + xfinfo = MustAliasMemoryInfo(arg) + add_liveness && add_liveness_change!(astate, arg) + end + fields[i] = xfinfo end + add_object_info_change!(astate, obj, HasIndexableFields(fields)) 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 +function analyze_builtin!(::typeof(tuple), astate::AnalysisState, pc::Int, args::Vector{Any}) + # `add_liveness = false` since it will be added in `escape_call!` instead + analyze_new!(astate, pc, args, #=add_liveness=#false) + return true # `tuple` call is always no throw 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) +function analyze_builtin!(::typeof(isdefined), astate::AnalysisState, pc::Int, args::Vector{Any}) + length(args) ≥ 3 || return false + ir = astate.ir + obj = args[2] + if isa(obj, SSAValue) || isa(obj, Argument) + objinfo = astate.currstate[obj] else - fidx = nothing - end - if fidx === nothing - return merge_to_unindexable(AliasInfo), 0 + return false end - AliasInfo = copy(AliasInfo) - infos = AliasInfo.infos - ninfos = length(infos) - if nflds > ninfos - for _ in 1:(nflds-ninfos) - push!(infos, AInfo()) + xoinfo = objinfo.ObjectInfo + if xoinfo isa HasIndexableFields || xoinfo isa HasIndexableCallerFields + fval = try_compute_field(ir, args[3]) + fval === nothing && return false + objtyp = widenconst(argextype(obj, ir)) + fidx = try_compute_fieldidx(objtyp, fval) + fidx === nothing && return false + @assert length(xoinfo.fields) ≥ fidx "invalid field index" + xfinfo = xoinfo.fields[fidx] + local initialized::Union{Nothing,Bool} = nothing + if xfinfo isa MustAliasMemoryInfo + aval = xfinfo.alias + initialized = aval !== UninitializedMemory() + else + xfinfo = xfinfo::MayAliasMemoryInfo + for aval in xfinfo.aliases + if initialized === nothing + initialized = aval !== UninitializedMemory() + elseif initialized + if aval === UninitializedMemory() + return false + end + else + if aval !== UninitializedMemory() + return false + end + end + end + end + if initialized !== nothing + astate.ssamemoryinfo[pc] = MustAliasMemoryInfo(initialized) end end - return AliasInfo, fidx + return false end -function escape_builtin!(::typeof(getfield), astate::AnalysisState, pc::Int, args::Vector{Any}) +function analyze_builtin!(::typeof(getfield), astate::AnalysisState, pc::Int, args::Vector{Any}) length(args) ≥ 3 || return false - ir, estate = astate.ir, astate.estate + ir = astate.ir obj = args[2] - typ = widenconst(argextype(obj, ir)) - if hasintersect(typ, Module) # global load - add_escape_change!(astate, SSAValue(pc), ⊤) - end - if isa(obj, SSAValue) || isa(obj, Argument) - objinfo = estate[obj] + objtyp = widenconst(argextype(obj, ir)) + retval = SSAValue(pc) + if hasintersect(objtyp, Module) # global load + @goto unanalyzable_object + elseif isa(obj, SSAValue) || isa(obj, Argument) + objinfo = astate.currstate[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) + xoinfo = objinfo.ObjectInfo + if xoinfo isa HasIndexableFields || xoinfo isa HasIndexableCallerFields + fval = try_compute_field(ir, args[3]) + fval === nothing && @goto conservative_propagation + fidx = try_compute_fieldidx(objtyp, fval) + fidx === nothing && @goto conservative_propagation + @assert length(xoinfo.fields) ≥ fidx "invalid field index" + xfinfo = xoinfo.fields[fidx] + @label precise_propagation + local all_initialized::Bool = true + if xfinfo isa MustAliasMemoryInfo + aval = xfinfo.alias + add_alias_change!(astate, retval, aval) + all_initialized &= !(aval === UninitializedMemory()) else - @goto record_unindexable_use + xfinfo = xfinfo::MayAliasMemoryInfo + for aval in xfinfo.aliases + add_alias_change!(astate, retval, aval) + all_initialized &= !(aval === UninitializedMemory()) + end + end + nothrow = all_initialized # refine `nothrow` information if possible + if xoinfo isa HasIndexableCallerFields + @assert length(xoinfo.caller_memory_list) ≥ fidx "invalid field index" + caller_memory = xoinfo.caller_memory_list[fidx] + if caller_memory isa IdSet{CallerMemory} + for argmem in caller_memory + add_alias_change!(astate, retval, argmem) + end + else + add_alias_change!(astate, retval, caller_memory) + end + astate.ssamemoryinfo[pc] = ⊤ₘ # load forwarding is impossible for fields with caller memories + else + astate.ssamemoryinfo[pc] = xfinfo 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 + add_all_escape_change!(astate, retval) + astate.ssamemoryinfo[pc] = ⊤ₘ end - return false + return nothrow end -function escape_builtin!(::typeof(setfield!), astate::AnalysisState, pc::Int, args::Vector{Any}) +function analyze_builtin!(::typeof(setfield!), astate::AnalysisState, pc::Int, args::Vector{Any}) length(args) ≥ 4 || return false - ir, estate = astate.ir, astate.estate - obj = args[2] - val = args[4] - if isa(obj, SSAValue) || isa(obj, Argument) - objinfo = estate[obj] + ir = astate.ir + obj, val = args[2], args[4] + objtyp = widenconst(argextype(obj, ir)) + if hasintersect(objtyp, Module) # global store + add_all_escape_change!(astate, val) + return false + elseif isa(obj, SSAValue) || isa(obj, Argument) + objinfo = astate.currstate[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) + xoinfo = objinfo.ObjectInfo + if xoinfo isa HasIndexableFields || xoinfo isa HasIndexableCallerFields + fval = try_compute_field(ir, args[3]) + fval === nothing && @goto conservative_propagation + fidx = try_compute_fieldidx(objtyp, fval) + fidx === nothing && @goto conservative_propagation + @assert length(xoinfo.fields) ≥ fidx "invalid field index" + xoinfo.fields[fidx] = MustAliasMemoryInfo(val) + add_object_info_change!(astate, obj, xoinfo) 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}) +function analyze_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 e000d7e8a582f..b45ce7c78f6e1 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 9103dba04fa54..e343d131bcc9a 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 ff333b9b0a129..c112327b84f7c 100644 --- a/Compiler/src/ssair/passes.jl +++ b/Compiler/src/ssair/passes.jl @@ -1569,6 +1569,7 @@ function try_inline_finalizer!(ir::IRCode, argexprs::Vector{Any}, idx::Int, end is_nothrow(ir::IRCode, ssa::SSAValue) = has_flag(ir[ssa], IR_FLAG_NOTHROW) +is_nothrow(ir::IRCode, id::Int) = is_nothrow(ir, SSAValue(id)) function reachable_blocks(cfg::CFG, from_bb::Int, to_bb::Int) worklist = Int[from_bb] @@ -1730,16 +1731,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/src/types.jl b/Compiler/src/types.jl index 5669ec3175c9e..1a0dcc2e0ae33 100644 --- a/Compiler/src/types.jl +++ b/Compiler/src/types.jl @@ -104,7 +104,7 @@ mutable struct InferenceResult valid_worlds::WorldRange # if inference and optimization is finished ipo_effects::Effects # if inference is finished effects::Effects # if optimization is finished - analysis_results::AnalysisResults # AnalysisResults with e.g. result::ArgEscapeCache if optimized, otherwise NULL_ANALYSIS_RESULTS + analysis_results::AnalysisResults # AnalysisResults with e.g. result::EscapeCache if optimized, otherwise NULL_ANALYSIS_RESULTS is_src_volatile::Bool # `src` has been cached globally as the compressed format already, allowing `src` to be used destructively #=== uninitialized fields ===# diff --git a/Compiler/test/EAUtils.jl b/Compiler/test/EAUtils.jl index 990a7de3b8141..3a66905b45b20 100644 --- a/Compiler/test/EAUtils.jl +++ b/Compiler/test/EAUtils.jl @@ -16,14 +16,15 @@ import .Compiler: # usings using Core.IR using .Compiler: InferenceResult, InferenceState, OptimizationState, IRCode -using .EA: analyze_escapes, ArgEscapeCache, ArgEscapeInfo, EscapeInfo, EscapeState +using .EA: EscapeCache, InterEscapeInfo, EscapeInfo, EscapeResult, MemoryInfo, + analyze_escapes mutable struct EscapeAnalyzerCacheToken end global GLOBAL_EA_CACHE_TOKEN::EscapeAnalyzerCacheToken = EscapeAnalyzerCacheToken() -struct EscapeResultForEntry +struct EscapeAnalysisResultForEntry ir::IRCode - estate::EscapeState + eresult::EscapeResult mi::MethodInstance end @@ -34,7 +35,7 @@ mutable struct EscapeAnalyzer <: AbstractInterpreter const inf_cache::Vector{InferenceResult} const token::EscapeAnalyzerCacheToken const entry_mi::Union{Nothing,MethodInstance} - result::EscapeResultForEntry + result::EscapeAnalysisResultForEntry function EscapeAnalyzer(world::UInt, cache_token::EscapeAnalyzerCacheToken; entry_mi::Union{Nothing,MethodInstance}=nothing) inf_params = InferenceParams() @@ -55,9 +56,8 @@ function Compiler.ipo_dataflow_analysis!(interp::EscapeAnalyzer, opt::Optimizati ir::IRCode, caller::InferenceResult) # run EA on all frames that have been optimized nargs = Int(opt.src.nargs) - 𝕃ₒ = Compiler.optimizer_lattice(interp) - estate = try - analyze_escapes(ir, nargs, 𝕃ₒ, GetEscapeCache()) + eresult = try + analyze_escapes(ir, nargs, GetEscapeCache()) catch err @error "error happened within EA, inspect `Main.failedanalysis`" failedanalysis = FailedAnalysis(caller, ir, nargs) @@ -66,9 +66,9 @@ 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!(caller, estate, ir) + record_escapes!(caller, eresult, ir) @invoke Compiler.ipo_dataflow_analysis!(interp::AbstractInterpreter, opt::OptimizationState, ir::IRCode, caller::InferenceResult) @@ -76,14 +76,14 @@ end # cache entire escape state for inspection and debugging struct EscapeCacheInfo - argescapes::ArgEscapeCache - state::EscapeState # preserved just for debugging purpose - ir::IRCode # preserved just for debugging purpose + argescapes::EscapeCache + eresult::EscapeResult # preserved just for debugging purpose + ir::IRCode # preserved just for debugging purpose end -function record_escapes!(caller::InferenceResult, estate::EscapeState, ir::IRCode) - argescapes = ArgEscapeCache(estate) - ecacheinfo = EscapeCacheInfo(argescapes, estate, ir) +function record_escapes!(caller::InferenceResult, eresult::EscapeResult, ir::IRCode) + argescapes = EscapeCache(eresult) + ecacheinfo = EscapeCacheInfo(argescapes, eresult, ir) return Compiler.stack_analysis_result!(caller, ecacheinfo) end @@ -105,120 +105,141 @@ end # printing # -------- -using Core: Argument, SSAValue using .Compiler: widenconst, singleton_type -function get_name_color(x::EscapeInfo, symbol::Bool = false) - getname(x) = string(nameof(x)) - if x === EA.⊥ - name, color = (getname(EA.NotAnalyzed), "◌"), :plain +function print_escape_info(io::IO, x::EscapeInfo, symbol::Union{Nothing,Bool} = nothing) + # print non-ObjectInfo escape information first + if EA.is_not_analyzed(x) + 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", "✓"), :blue 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) - name = string(name, "′") - end - return name, color -end -# pcs = sprint(show, collect(x.EscapeSites); context=:limit=>true) -function Base.show(io::IO, x::EscapeInfo) - name, color = get_name_color(x) - if isnothing(name) - @invoke show(io::IO, x::Any) + if x.ObjectInfo isa EA.HasUnanalyzedMemory + oname = nothing + elseif x.ObjectInfo isa EA.HasIndexableFields + oname = "ₒ" + elseif x.ObjectInfo isa EA.HasIndexableCallerFields + oname = "ₒ̅" else - printstyled(io, name; color) + x.ObjectInfo::EA.HasUnknownMemory + oname = "ₓ" end -end - -function get_sym_color(x::ArgEscapeInfo) - escape_bits = x.escape_bits - if escape_bits == EA.ARG_ALL_ESCAPE - color, sym = :red, "X" - elseif escape_bits == 0x00 - color, sym = :green, "✓" - else - color, sym = :bold, "*" - if !iszero(escape_bits & EA.ARG_RETURN_ESCAPE) - color, sym = :blue, "↑" + if symbol !== nothing + name = last(name) + if oname !== nothing + printstyled(io, name, oname; color) + else + symbol && print(io, " ") + printstyled(io, name; color) end - if !iszero(escape_bits & EA.ARG_THROWN_ESCAPE) - color = :yellow + else + name = first(name) + if name === nothing + @invoke Base.show(io::IO, x::Any) + else + if oname !== nothing + printstyled(io, name, oname; color) + else + printstyled(io, name; color) + end end end - return sym, color + + return color end -function Base.show(io::IO, x::ArgEscapeInfo) +Base.show(io::IO, x::EscapeInfo) = print_escape_info(io, x) + +function get_sym_color(x::InterEscapeInfo) escape_bits = x.escape_bits if escape_bits == EA.ARG_ALL_ESCAPE - color, sym = :red, "X" + sym, color = "X", :red elseif escape_bits == 0x00 - color, sym = :green, "✓" + sym, color = "✓", :green else - color, sym = :bold, "*" + sym, color = "*", :bold if !iszero(escape_bits & EA.ARG_RETURN_ESCAPE) - color, sym = :blue, "↑" + sym, color = "↑", :blue end if !iszero(escape_bits & EA.ARG_THROWN_ESCAPE) color = :yellow end end - printstyled(io, "ArgEscapeInfo(", sym, ")"; color) + return sym, color +end + +function Base.show(io::IO, x::InterEscapeInfo) + sym, color = get_sym_color(x) + printstyled(io, "InterEscapeInfo(", sym, ")"; color) end -struct EscapeResult +struct EscapeAnalysisResult ir::IRCode - state::EscapeState + eresult::EscapeResult + cacheresult::EscapeCache 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, EscapeCache(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.getaliases(res::EscapeAnalysisResult, args...) = EA.getaliases(res.eresult, args...) +EA.isaliased(res::EscapeAnalysisResult, args...) = EA.isaliased(res.eresult, args...) +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)] + (; nargs) = bbstate.afinfo + for i in 1:nargs + arginfo = bbstate[Argument(i)] i == 1 && continue - c, color = get_name_color(arg, true) + color = print_escape_info(io, arginfo, false) slot = isnothing(slotnames) ? "_$i" : slotnames[i] - printstyled(io, c, ' ', slot, "::", ir.argtypes[i]; color) - i ≠ state.nargs && print(io, ", ") + printstyled(io, ' ', slot, "::", ir.argtypes[i]; color) + i ≠ nargs && print(io, ", ") end print(io, ')') if !isnothing(mi) @@ -229,41 +250,58 @@ function print_with_info(io::IO, result::EscapeResult) end # 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) - # printstyled(io, lpad(idx, nd), ' ', c, ' '; color) - printstyled(io, rpad(c, 2), ' '; color) + lineprinter = IRShow.inline_linfo_printer(ir) + preprinter = function (@nospecialize(io::IO), linestart::String, idx::Int) + str = lineprinter(io, linestart, idx) + if idx ≠ 0 + return str * sprint(;context=IOContext(io)) do @nospecialize io::IO + print(io, " ") + print_escape_info(io, bbstate[SSAValue(idx)], true) + end + end + return str end - print_with_info(preprint, (args...)->nothing, io, ir, source) -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 isa EA.MustAliasMemoryInfo + color = :green + c = sprint(context=IOContext(io)) do @nospecialize io::IO + Base.show_unquoted(io, MemoryInfo.alias) + end + elseif MemoryInfo isa EA.MayAliasMemoryInfo + color = :yellow + c = "[" * sprint(context=IOContext(io)) do @nospecialize io::IO + local isfirst::Bool = true + for aval in MemoryInfo.aliases + if isfirst + isfirst = false + else + print(io, ", ") + end + Base.show_unquoted(io, aval) + end + end * "]" + else + @assert MemoryInfo isa EA.UnknownMemoryInfo + c, color = "X", :red + end + printstyled(io, " (↦ ", c, ")"; color) + end end else - line_info_preprinter = Compiler.IRShow.lineinfo_disabled + _postprinter 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 - 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 @@ -284,8 +322,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. @@ -323,12 +361,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. @@ -349,8 +387,8 @@ function code_escapes(ir::IRCode, nargs::Int; world::UInt = get_world_counter(), cache_token::EscapeAnalyzerCacheToken = GLOBAL_EA_CACHE_TOKEN, interp::AbstractInterpreter=EscapeAnalyzer(world, cache_token)) - 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.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 60364769c95a8..46412e6498506 100644 --- a/Compiler/test/EscapeAnalysis.jl +++ b/Compiler/test/EscapeAnalysis.jl @@ -25,6 +25,10 @@ let utils_ex = quote global GV::Any const global GR = Ref{Any}() + + mutable struct Object + s::String + end end global function EATModule(utils_ex = utils_ex) M = Module() @@ -34,68 +38,61 @@ 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) -end +is_load_forwardable_old(x::EscapeAnalysis.EscapeInfo) = # TODO use new `is_load_forwardable` instead + isa(x.ObjectInfo, EscapeAnalysis.HasIndexableFields) @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 + let ir = first(only(Base.code_ircode(sin, (Int,)))) + @test code_escapes(ir, 2) isa EAUtils.EscapeAnalysisResult + end 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 +104,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 +151,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 +162,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,34 +173,34 @@ 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 try - println("prevent ConstABI") + @noinline(rand(Bool)) && error("") nothing - catch err + catch 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 - println("prevent ConstABI") + @noinline(rand(Bool)) && error("") nothing finally 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 +209,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 +230,18 @@ end c = m === nothing return c end - @test has_no_escape(ignore_argescape(result.state[Argument(2)])) + @test !has_thrown_escape(result[Argument(2)]) + @test !has_return_escape(result[Argument(2)]) + @test_broken has_no_escape(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_thrown_escape(result[Argument(2)]) + @test !has_return_escape(result[Argument(2)]) + @test_broken has_no_escape(result[Argument(2)]) end let # ifelse @@ -251,7 +252,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 +262,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 +275,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 +284,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 +301,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 +351,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 +367,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 +384,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 +400,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 +412,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 +427,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 +439,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 +451,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 +472,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 +493,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 +518,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 +539,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 +561,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,8 +582,33 @@ 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 @@ -590,65 +616,64 @@ end # ------------------- # 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 +682,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 +742,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 +756,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 +767,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 +810,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 +822,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 +841,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 +851,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,66 +865,65 @@ 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 # -------------- # alias via getfield & Expr(:new) - let result = code_escapes((String,)) do s + let result = code_escapes((Object,)) do s r = SafeRef(s) Core.donotdelete(r) return r[] 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) + retval = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue + @test isaliased(result.eresult, Argument(2), retval) + @test !isaliased(result.eresult, Argument(2), SSAValue(i)) end let result = code_escapes((String,)) do s r1 = SafeRef(s) @@ -909,11 +934,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 +946,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 +962,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 +975,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,41 +988,41 @@ 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 return r2[][] 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) + retval = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue + @test isaliased(result.eresult, Argument(2), retval) for i in findall(isnew, result.ir.stmts.stmt) - @test !isaliased(SSAValue(i), val, result.state) + @test !isaliased(result.eresult, SSAValue(i), retval) end - result = code_escapes((String,)) do s - r1 = Ref{String}() - r2 = Ref{Base.RefValue{String}}() + result = code_escapes((Object,)) do s + r1 = Ref{Object}() + r2 = Ref{Base.RefValue{Object}}() r1[] = s r2[] = r1 return r2[][] 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) + retval = (result.ir.stmts.stmt[r]::ReturnNode).val::SSAValue + @test isaliased(result.eresult, Argument(2), retval) for i in findall(isnew, result.ir.stmts.stmt) - @test !isaliased(SSAValue(i), val, result.state) + @test !isaliased(result.eresult, SSAValue(i), retval) end end let result = @eval EATModule() begin - const Rx = SafeRef("Rx") - $code_escapes((SafeRef{String}, String,)) do _rx, s + const Rx = SafeRef(Object("Rx")) + $code_escapes((SafeRef{Object}, Object,)) do _rx, s r = SafeRef(_rx) Core.donotdelete(r) r[] = Rx @@ -1007,8 +1032,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 +1042,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,21 +1059,21 @@ 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") - $code_escapes((Bool,String,)) do c, a + const Lx, Rx = SafeRef(Object("Lx")), SafeRef(Object("Rx")) + $code_escapes((Bool,Object,)) do c, a r = ifelse(c, Lx, Rx) r[] = a 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 +1087,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 +1109,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,18 +1129,18 @@ 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 - l = g - if isa(l, SafeRef{String}) + let result = code_escapes((Object,)) do x + global typed_global_object + tgo = typed_global_object + if isa(tgo, SafeRef{Object}) l[] = x 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 +1150,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 +1162,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 +1178,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 +1188,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 +1196,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 +1207,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 +1220,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 +1229,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 +1239,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 +1250,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 +1261,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,13 +1271,13 @@ 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 - # --------------- + # inter-procedural + # ---------------- let result = @eval EATModule() begin @noinline getx(obj) = obj[] @@ -1266,38 +1289,38 @@ 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 + # TODO inter-procedural alias analysis let result = code_escapes((SafeRef{Base.RefValue{String}},)) do s s[] = Ref("bar") 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 +1329,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 +1340,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 +1354,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 +1367,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 +1383,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 +1407,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 +1429,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 +1445,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 +1472,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 +1482,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 +1500,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 +1515,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 +1525,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,13 +1535,13 @@ 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 -# interprocedural analysis -# ======================== +# inter-procedural analysis +# ========================= # propagate escapes imposed on call arguments @noinline broadcast_noescape2(b) = broadcast(identity, b) @@ -1526,8 +1549,8 @@ 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_broken !has_thrown_escape(result.state[SSAValue(i)]) # IDEA embed const-prop'ed `CodeInstance` for `:invoke`? + @test_broken !has_return_escape(result[SSAValue(i)]) # TODO inter-procedural alias analysis + @test_broken !has_thrown_escape(result[SSAValue(i)]) # TODO inter-procedural alias analysis end let result = code_escapes((Base.RefValue{Base.RefValue{String}},)) do x out1 = broadcast_noescape2(Ref(Ref("Hi"))) @@ -1535,27 +1558,27 @@ let result = code_escapes((Base.RefValue{Base.RefValue{String}},)) do x return out1, out2 end i = last(findall(isnew, result.ir.stmts.stmt)) - @test_broken !has_return_escape(result.state[SSAValue(i)]) # TODO interprocedural alias analysis - @test_broken !has_thrown_escape(result.state[SSAValue(i)]) # IDEA embed const-prop'ed `CodeInstance` for `:invoke`? - @test has_thrown_escape(result.state[Argument(2)]) + @test_broken !has_return_escape(result[SSAValue(i)]) # TODO inter-procedural alias analysis + @test_broken !has_thrown_escape(result[SSAValue(i)]) # TODO inter-procedural alias analysis + @test has_thrown_escape(result[Argument(2)]) 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) @@ -1569,7 +1592,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 @@ -1580,7 +1603,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 has_no_escape(result[SSAValue(i)]) result = code_escapes() do a = Ref("foo") # still should be "return escape" @@ -1589,7 +1612,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 @@ -1601,7 +1624,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 @@ -1610,7 +1633,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 has_no_escape(result[SSAValue(i)]) end function with_self_aliased(from_bb::Int, succs::Vector{Int}) @@ -1627,7 +1650,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 @@ -1636,15 +1659,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 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 @@ -1654,7 +1676,7 @@ let result = code_escapes((SafeRef{String},)) do x end return nothing end - @test !has_all_escape(result.state[Argument(2)]) + @test !has_all_escape(result[Argument(2)]) end let result = code_escapes((Union{SafeRef{String},Vector{String}},)) do x try @@ -1664,7 +1686,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) @@ -1673,12 +1695,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 @@ -1687,8 +1708,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() @@ -1705,7 +1726,360 @@ 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 + +let result = code_escapes((String,Bool)) do s, c + xs = Ref(s) + if c + xs[] = "foo" + else + @goto block4 + end + @label block4 + return xs[], xs + end + idx = only(findall(iscall((result.ir, getfield)), result.ir.stmts.stmt)) + @test !is_load_forwardable(result, idx) +end + +# `analyze_escapes` should be able to handle `IRCode` with new nodes +let code = Any[ + # block 1 + #=1=# Expr(:new, Base.RefValue{Bool}, Argument(2)) + #=2=# Expr(:call, GlobalRef(Core, :getfield), SSAValue(1), 1) + #=3=# GotoIfNot(SSAValue(2), 5) + # block 2 + #=4=# nothing + # block 3 + #=5=# Expr(:call, GlobalRef(Core, :setfield!), SSAValue(1), 1, false) + #=6=# ReturnNode(nothing) + ] + ir = make_ircode(code; slottypes=Any[Any,Bool]) + ir.stmts[1][:type] = Base.RefValue{Bool} + Compiler.insert_node!(ir, SSAValue(4), Compiler.NewInstruction(Expr(:call, GlobalRef(Core, :setfield!), SSAValue(1), 1, false), Any), #=attach_after=#false) + Compiler.insert_node!(ir, SSAValue(4), Compiler.NewInstruction(GotoNode(3), Any), #=attach_after=#true) + ir[SSAValue(6)] = nothing # eliminate the ReturnNode + s = Compiler.insert_node!(ir, SSAValue(6), Compiler.NewInstruction(Expr(:call, GlobalRef(Core, :getfield), SSAValue(1), 1), Any), #=attach_after=#false) + Compiler.insert_node!(ir, SSAValue(6), Compiler.NewInstruction(ReturnNode(s), Any), #=attach_after=#true) + # now this `ir` would look like: + # 1 ─ %1 = %new(Base.RefValue{Bool}, _2)::Base.RefValue{Bool} │ + # │ %2 = builtin Core.getfield(%1, 1)::Any │ + # └── goto #3 if not %2 │ + # 2 ─ builtin Core.setfield!(%1, 1, false)::Any │ + # │ nothing::Any + # └── goto #3 + # 3 ┄ builtin Core.setfield!(%1, 1, false)::Any │ + # │ %9 = builtin Core.getfield(%1, 1)::Any │ + # │ nothing::Any + # └── return %9 + result = code_escapes(ir, 2) + idxs = findall(iscall((result.ir, getfield)), result.ir.stmts.stmt) + for idx = idxs + @test is_load_forwardable(result, idx) + end +end + +mutable struct ObjectC + c::Char +end +const g_xoc = Ref{ObjectC}() +const g_xxoc = Ref{Ref{ObjectC}}() + +@noinline func_inter_return() = g_xoc +let result = code_escapes(func_inter_return) + @test has_all_escape(result[0]) +end +let result = code_escapes((Char,)) do c + oc = ObjectC(c) + xoc = func_inter_return() + xoc[] = oc + nothing + end + idx = only(findall(isnew_with_type((result.ir, ObjectC)), result.ir.stmts.stmt)) + @test has_all_escape(result[SSAValue(idx)]) +end + +@noinline func_raiser() = throw(g_xoc) +@noinline function func_inter_throw(xoc) + try + xoc[] = ObjectC('a') + func_raiser() + catch e + e = e::Base.RefValue{ObjectC} + g_xxoc[] = e + e[] = ObjectC('b') + end + xoc[] +end +code_escapes(func_inter_throw, (typeof(g_xoc),)) + +# local alias analysis +# ==================== + +# refine `:nothrow` information using `MemoryInfo` analysis +function func_refine_nothrow(s::String) + x = Ref{String}() + x[] = s + return x, x[] +end +let result = code_escapes(func_refine_nothrow, (String,)) + idx = only(findall(isnew, result.ir.stmts.stmt)) + @test !has_thrown_escape(result[SSAValue(idx)]) + effects = Base.infer_effects(func_refine_nothrow, (String,)) + @test_broken Compiler.is_nothrow(effects) +end + +# inter-procedural alias analysis +# =============================== + +mutable struct ObjectS + s::String +end + +@noinline update_os(os::ObjectS) = (os.s = ""; nothing) +@noinline update_os(os::ObjectS, s::String) = (os.s = s; nothing) +@noinline update_os(os::ObjectS, s::String, c::Bool) = (c && (os.s = s); nothing) +code_escapes((String,)) do s + os = ObjectS(s) + update_os(os) + os.s +end + +@noinline function swap_os(os1::ObjectS, os2::ObjectS) + s1 = os1.s + s2 = os2.s + os1.s = s1 + os2.s = s2 +end + +function set_xos_yos(xos::ObjectS, yos::ObjectS) + xos.s = "a" + yos.s = "b" + return xos.s # might be "b" +end + +# needs to account for possibility of aliasing between arguments (and their memories) +# > alias analysis is inherently inter-procedural +let result = code_escapes(set_xos_yos, (ObjectS,ObjectS)) + idx = only(findall(iscall((result.ir, getfield)), result.ir.stmts.stmt)) + @test !is_load_forwardable(result, idx) +end + +let result = code_escapes((String,)) do s + os = ObjectS(s) + set_xos_yos(os, os) + os + end +end + +@noinline make_os(s::String) = ObjectS(s) +code_escapes((String,)) do s + os = make_os(s) + os.s +end + +const g_xos = Ref(ObjectS("")) +@noinline function some_unsafe_func() + println("some_unsafe_func is called") + g_xos[] = ObjectS("julia2") +end + +function callerfunc(@specialize(calleefunc), s::String) + xxos = Ref(Ref{ObjectS}()) + calleefunc(xxos) + xxos[][] = ObjectS(s) # may escape here (depending on `calleefunc`) + some_unsafe_func() + return xxos[][].s # returns `s` +end +@noinline function calleefunc1(xxos) + yy = Ref(ObjectS("julia")) + xxos[] = yy + nothing +end +@noinline function calleefunc2(xxos) + xxos[] = g_xos + nothing +end + +let result = code_escapes(callerfunc, (typeof(calleefunc1), String,)) + idx = only(findall(isnew_with_type((result.ir, ObjectS)), result.ir.stmts.stmt)) + @test_broken !has_all_escape(result[SSAValue(idx)]) +end + +let result = code_escapes(callerfunc, (typeof(calleefunc2), String,)) + idx = only(findall(isnew_with_type((result.ir, ObjectS)), result.ir.stmts.stmt)) + @test has_all_escape(result[SSAValue(idx)]) +end end # module test_EA diff --git a/Compiler/test/irutils.jl b/Compiler/test/irutils.jl index c1616ad4a8fd0..aeddbe6694987 100644 --- a/Compiler/test/irutils.jl +++ b/Compiler/test/irutils.jl @@ -3,7 +3,7 @@ include("setup_Compiler.jl") using Core.IR -using .Compiler: IRCode, IncrementalCompact, singleton_type, VarState +using .Compiler: IRCode, IncrementalCompact, VarState, singleton_type using Base.Meta: isexpr using InteractiveUtils: gen_call_with_extracted_types_and_kwargs @@ -19,6 +19,13 @@ end # check if `x` is a statement with a given `head` isnew(@nospecialize x) = isexpr(x, :new) +isnew_with_type(@nospecialize(srcT)) = @nospecialize(x) -> isnew_with_type(srcT, x) +function isnew_with_type(@nospecialize(srcT), @nospecialize(x)) + isexpr(x, :new) || return false + (src, T) = srcT + return singleton_type(argextype(x.args[1], src)) == T +end +isnew_with_type(@nospecialize(T::Type)) = (@nospecialize(x) -> isnew_with_type(x, T)) issplatnew(@nospecialize x) = isexpr(x, :splatnew) isreturn(@nospecialize x) = isa(x, ReturnNode) && isdefined(x, :val) isisdefined(@nospecialize x) = isexpr(x, :isdefined) diff --git a/Compiler/test/runtests.jl b/Compiler/test/runtests.jl index 6a38fce678ba0..a709ec363b0d3 100644 --- a/Compiler/test/runtests.jl +++ b/Compiler/test/runtests.jl @@ -3,10 +3,11 @@ using Test, Compiler using InteractiveUtils: @activate @activate Compiler -@testset "Compiler.jl" begin - for file in readlines(joinpath(@__DIR__, "testgroups")) - file == "special_loading" && continue # Only applicable to Base.Compiler - testfile = file * ".jl" - @eval @testset $testfile include($testfile) - end -end +@testset "EscapeAnalysis.jl" include("EscapeAnalysis.jl") +# @testset "Compiler.jl" begin +# for file in readlines(joinpath(@__DIR__, "testgroups")) +# file == "special_loading" && continue # Only applicable to Base.Compiler +# testfile = file * ".jl" +# @eval @testset $testfile include($testfile) +# end +# end diff --git a/doc/src/devdocs/EscapeAnalysis.md b/doc/src/devdocs/EscapeAnalysis.md index d8efd759fa131..ea45bea35df40 100644 --- a/doc/src/devdocs/EscapeAnalysis.md +++ b/doc/src/devdocs/EscapeAnalysis.md @@ -354,7 +354,7 @@ and especially, it is supposed to be used at the following two stages: - `IPO EA`: analyze pre-inlining IR to generate IPO-valid escape information cache - `Local EA`: analyze post-inlining IR to collect locally-valid escape information -Escape information derived by `IPO EA` is transformed to the `ArgEscapeCache` data structure and cached globally. +Escape information derived by `IPO EA` is transformed to the `EscapeCache` data structure and cached globally. By passing an appropriate `get_escape_cache` callback to `analyze_escapes`, the escape analysis can improve analysis accuracy by utilizing cached inter-procedural information of non-inlined callees that has been derived by previous `IPO EA`.