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`.