module test_EA

const use_core_compiler = true

if use_core_compiler
    const EscapeAnalysis = Core.Compiler.EscapeAnalysis
else
    include(normpath(Sys.BINDIR, "..", "..", "base", "compiler", "ssair", "EscapeAnalysis", "EscapeAnalysis.jl"))
end

include("EAUtils.jl")
include("../irutils.jl")

using Test, .EscapeAnalysis, .EAUtils
using .EscapeAnalysis: ignore_argescape

let utils_ex = quote
        mutable struct SafeRef{T}
            x::T
        end
        Base.getindex(s::SafeRef) = getfield(s, 1)
        Base.setindex!(s::SafeRef, x) = setfield!(s, 1, x)

        mutable struct SafeRefs{S,T}
            x1::S
            x2::T
        end
        Base.getindex(s::SafeRefs, idx::Int) = getfield(s, idx)
        Base.setindex!(s::SafeRefs, x, idx::Int) = setfield!(s, idx, x)

        global GV::Any
        const global GR = Ref{Any}()
    end
    global function EATModule(utils_ex = utils_ex)
        M = Module()
        Core.eval(M, utils_ex)
        return M
    end
    Core.eval(@__MODULE__, utils_ex)
end

using .EscapeAnalysis:
    EscapeInfo, IndexableElements, IndexableFields, normalize

isϕ(@nospecialize x) = isa(x, Core.PhiNode)
function with_normalized_name(@nospecialize(f), @nospecialize(x))
    if Meta.isexpr(x, :foreigncall)
        name = x.args[1]
        nn = normalize(name)
        return isa(nn, Symbol) && f(nn)
    end
    return false
end
isarrayalloc(@nospecialize x) =
    with_normalized_name(nn::Symbol->false, x)
isarrayresize(@nospecialize x) =
    with_normalized_name(nn::Symbol->false, x)
isarraycopy(@nospecialize x) =
    with_normalized_name(nn::Symbol->false, x)
"""
    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) || isa(AliasInfo, IndexableElements)
end

@testset "EAUtils" begin
    @test_throws "everything has been constant folded" code_escapes() do; sin(42); end
    @test code_escapes(sin, (Int,)) isa EAUtils.EscapeResult
    @test code_escapes(sin, (Int,)) isa EAUtils.EscapeResult
end

@testset "basics" begin
    let # arg return
        result = code_escapes((Any,)) do a # return to caller
            println("prevent ConstABI")
            return nothing
        end
        @test has_arg_escape(result.state[Argument(2)])
        # return
        result = code_escapes((Any,)) 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
    end
    let # global store
        result = code_escapes((Any,)) do a
            global GV = a
            nothing
        end
        @test has_all_escape(result.state[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)])
    end
    let # global store / load (https://github.com/aviatesk/EscapeAnalysis.jl/issues/56)
        result = code_escapes((Any,)) 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)
    end
    let # :gc_preserve_begin / :gc_preserve_end
        result = code_escapes((String,)) do s
            m = SafeRef(s)
            GC.@preserve m begin
                println(s)
                return nothing
            end
        end
        i = findfirst(==(SafeRef{String}), result.ir.stmts.type) # find allocation statement
        @test !isnothing(i)
        @test has_no_escape(result.state[SSAValue(i)])
    end
    let # :isdefined
        result = code_escapes((String, Bool,)) do a, b
            if b
                s = Ref(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)])
    end
    let # ϕ-node
        result = code_escapes((Bool,Any,Any)) 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
    end
    let # π-node
        result = code_escapes((Any,)) do a
            if isa(a, Regex) # a::π(Regex)
                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)
        end
    end
    let # φᶜ-node / ϒ-node
        result = code_escapes((Any,String)) do a, b
            local x::String
            try
                x = a
            catch err
                x = b
            end
            return x
        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)
    end
    let # branching
        result = code_escapes((Any,Bool,)) do a, c
            if c
                return nothing # a doesn't escape in this branch
            else
                return a # a escapes to a caller
            end
        end
        @test has_return_escape(result.state[Argument(2)])
    end
    let # loop
        result = code_escapes((Int,)) do n
            c = SafeRef{Bool}(false)
            while n > 0
                rand(Bool) && return c
            end
            nothing
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test has_return_escape(result.state[SSAValue(i)])
    end
    let # try/catch
        result = code_escapes((Any,)) do a
            try
                println("prevent ConstABI")
                nothing
            catch err
                return a # return escape
            end
        end
        @test has_return_escape(result.state[Argument(2)])
    end
    let result = code_escapes((Any,)) do a
            try
                println("prevent ConstABI")
                nothing
            finally
                return a # return escape
            end
        end
        @test has_return_escape(result.state[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)])
    end
end

@testset "builtins" begin
    let # throw
        r = code_escapes((Any,)) do a
            throw(a)
        end
        @test has_thrown_escape(r.state[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)])

        r = code_escapes((Any,)) do a
            sizeof(a)
        end
        @test has_thrown_escape(r.state[Argument(2)])
    end

    let # :===
        result = code_escapes((Bool, SafeRef{String})) do cond, s
            m = cond ? s : nothing
            c = m === nothing
            return c
        end
        @test has_no_escape(ignore_argescape(result.state[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)]))
    end

    let # ifelse
        result = code_escapes((Bool,)) do c
            r = ifelse(c, Ref("yes"), Ref("no"))
            return r
        end
        inds = findall(isnew, result.ir.stmts.stmt)
        @assert !isempty(inds)
        for i in inds
            @test has_return_escape(result.state[SSAValue(i)])
        end
    end
    let # ifelse (with constant condition)
        result = code_escapes() do
            r = ifelse(true, Ref("yes"), Ref(nothing))
            return r
        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)])
            elseif isnew(result.ir.stmts.stmt[i]) && result.ir.stmts.type[i] == Base.RefValue{Nothing}
                @test has_no_escape(result.state[SSAValue(i)])
            end
        end
    end

    let # typeassert
        result = code_escapes((Any,)) do x
            y = x::Base.RefValue{Any}
            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)])
    end

    let # isdefined
        result = code_escapes((Any,)) do x
            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)])
    end
end

@testset "flow-sensitivity" begin
    # ReturnEscape
    let result = code_escapes((Bool,)) do cond
            r = Ref("foo")
            if cond
                return cond
            end
            return r
        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
    end
    let result = code_escapes((Bool,)) do cond
            r = Ref("foo")
            cnt = 0
            while rand(Bool)
                cnt += 1
                rand(Bool) && return r
            end
            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
    end
end

@testset "escape through exceptions" begin
    M = @eval Module() begin
        unsafeget(x) = isassigned(x) ? x[] : throw(x)
        @noinline function escape_rethrow!()
            try
                rethrow()
            catch err
                GR[] = err
            end
        end
        @noinline function escape_current_exceptions!()
            excs = Base.current_exceptions()
            GR[] = excs
        end
        const GR = Ref{Any}()
        @__MODULE__
    end

    let # simple: return escape
        result = @eval M $code_escapes() do
            r = Ref{String}()
            local ret
            try
                s = unsafeget(r)
                ret = sizeof(s)
            catch err
                ret = err
            end
            return ret
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test has_return_escape(result.state[SSAValue(i)])
    end

    let # simple: global escape
        result = @eval M $code_escapes() do
            r = Ref{String}()
            local ret # prevent DCE
            try
                s = unsafeget(r)
                ret = sizeof(s)
            catch err
                global GV = err
            end
            nothing
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test has_all_escape(result.state[SSAValue(i)])
    end

    let # account for possible escapes via nested throws
        result = @eval M $code_escapes() do
            r = Ref{String}()
            try
                try
                    unsafeget(r)
                catch err1
                    throw(err1)
                end
            catch err2
                GR[] = err2
            end
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test has_all_escape(result.state[SSAValue(i)])
    end
    let # account for possible escapes via `rethrow`
        result = @eval M $code_escapes() do
            r = Ref{String}()
            try
                try
                    unsafeget(r)
                catch err1
                    rethrow(err1)
                end
            catch err2
                GR[] = err2
            end
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test has_all_escape(result.state[SSAValue(i)])
    end
    let # account for possible escapes via `rethrow`
        result = @eval M $code_escapes() do
            try
                r = Ref{String}()
                unsafeget(r)
            catch
                escape_rethrow!()
            end
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test has_all_escape(result.state[SSAValue(i)])
    end
    let # account for possible escapes via `rethrow`
        result = @eval M $code_escapes() do
            local t
            try
                r = Ref{String}()
                t = unsafeget(r)
            catch err
                t = typeof(err)
                escape_rethrow!()
            end
            return t
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test has_all_escape(result.state[SSAValue(i)])
    end
    let # account for possible escapes via `Base.current_exceptions`
        result = @eval M $code_escapes() do
            try
                r = Ref{String}()
                unsafeget(r)
            catch
                GR[] = Base.current_exceptions()
            end
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test has_all_escape(result.state[SSAValue(i)])
    end
    let # account for possible escapes via `Base.current_exceptions`
        result = @eval M $code_escapes() do
            try
                r = Ref{String}()
                unsafeget(r)
            catch
                escape_current_exceptions!()
            end
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test has_all_escape(result.state[SSAValue(i)])
    end

    let # contextual: escape information imposed on `err` shouldn't propagate to `r2`, but only to `r1`
        result = @eval M $code_escapes() do
            r1 = Ref{String}()
            r2 = Ref{String}()
            local ret
            try
                s1 = unsafeget(r1)
                ret = sizeof(s1)
            catch err
                global GV = err
            end
            s2 = unsafeget(r2)
            return s2, r2
        end
        is = findall(isnew, result.ir.stmts.stmt)
        @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)
    end

    # XXX test cases below are currently broken because of the technical reason described in `escape_exception!`

    let # limited propagation: exception is caught within a frame => doesn't escape to a caller
        result = @eval M $code_escapes() do
            r = Ref{String}()
            local ret
            try
                s = unsafeget(r)
                ret = sizeof(s)
            catch
                ret = nothing
            end
            return ret
        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!`
    end
    let # sequential: escape information imposed on `err1` and `err2 should propagate separately
        result = @eval M $code_escapes() do
            r1 = Ref{String}()
            r2 = Ref{String}()
            local ret
            try
                s1 = unsafeget(r1)
                ret = sizeof(s1)
            catch err1
                global GV = err1
            end
            try
                s2 = unsafeget(r2)
                ret = sizeof(s2)
            catch err2
                ret = err2
            end
            return ret
        end
        is = findall(isnew, result.ir.stmts.stmt)
        @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!`
    end
    let # nested: escape information imposed on `inner` shouldn't propagate to `s`
        result = @eval M $code_escapes() do
            r = Ref{String}()
            local ret
            try
                s = unsafeget(r)
                try
                    ret = sizeof(s)
                catch inner
                    return inner
                end
            catch outer
                ret = nothing
            end
            return ret
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test_broken !has_return_escape(result.state[SSAValue(i)])
    end
    let # merge: escape information imposed on `err1` and `err2 should be merged
        result = @eval M $code_escapes() do
            r = Ref{String}()
            local ret
            try
                s = unsafeget(r)
                ret = sizeof(s)
            catch err1
                return err1
            end
            try
                s = unsafeget(r)
                ret = sizeof(s)
            catch err2
                return err2
            end
            nothing
        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)])
        for r in rs
            @test has_return_escape(result.state[SSAValue(i)], r)
        end
    end
    let # no exception handling: should keep propagating the escape
        result = @eval M $code_escapes() do
            r = Ref{String}()
            local ret
            try
                s = unsafeget(r)
                ret = sizeof(s)
            finally
                if !@isdefined(ret)
                    ret = 42
                end
            end
            return ret
        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)
    end
end

@testset "field analysis / alias analysis" begin
    # escaped allocations
    # -------------------

    # escaped object should escape its fields as well
    let result = code_escapes((Any,)) do a
            global GV = SafeRef{Any}(a)
            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)])
    end
    let result = code_escapes((Any,)) 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)])
    end
    let result = code_escapes((Any,)) do a
            o0 = SafeRef{Any}(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)])
    end
    let result = code_escapes((Any,)) 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)])
    end
    # global escape through `setfield!`
    let result = code_escapes((Any,)) do a
            r = SafeRef{Any}(:init)
            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)])
    end
    let result = code_escapes((Any,Any)) do a, b
            r = SafeRef{Any}(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
    end
    let result = @eval EATModule() begin
            const Rx = SafeRef{String}("Rx")
            $code_escapes((String,)) do s
                Rx[] = s
                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)])
    end
    let M = EATModule()
        @eval M module ___xxx___
            import ..SafeRef
            const Rx = SafeRef("Rx")
        end
        result = @eval M begin
            $code_escapes((String,)) do s
                rx = getfield(___xxx___, :Rx)
                rx[] = s
                nothing
            end
        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
            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)])
    end
    let result = code_escapes((Base.RefValue{String},)) 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)])
    end
    let result = code_escapes((Base.RefValue{String}, Base.RefValue{String})) do a, b
            obj = SafeRefs(a, b)
            Core.donotdelete(obj)
            fld1 = obj[1]
            fld2 = obj[2]
            return (fld1, fld2)
        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)])
    end

    # field escape should propagate to `setfield!` argument
    let result = code_escapes((Base.RefValue{String},)) do a
            o = SafeRef(Ref("foo"))
            Core.donotdelete(o)
            o[] = a
            return o[]
        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)])
    end
    # propagate escape information imposed on return value of `setfield!` call
    let result = code_escapes((Base.RefValue{String},)) do a
            obj = SafeRef(Ref("foo"))
            Core.donotdelete(obj)
            return (obj[] = a)
        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)])
    end

    # nested allocations
    let result = code_escapes((Base.RefValue{String},)) 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)
        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)
            elseif isnew(result.ir.stmts.stmt[i]) && result.ir.stmts.type[i] == SafeRef{SafeRef{String}}
                @test is_load_forwardable(result.state[SSAValue(i)])
            end
        end
    end
    let result = code_escapes((Base.RefValue{String},)) 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)
        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)
            elseif isnew(result.ir.stmts.stmt[i]) && result.ir.stmts.type[i] == Tuple{Tuple{String}}
                @test is_load_forwardable(result.state[SSAValue(i)])
            end
        end
    end
    let result = code_escapes((Base.RefValue{String},)) do a
            o1  = SafeRef(a)
            o2  = SafeRef(o1)
            o1′ = o2[]
            a′  = o1′[]
            return a′
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test has_return_escape(result.state[Argument(2)], r)
        for i in findall(isnew, result.ir.stmts.stmt)
            @test is_load_forwardable(result.state[SSAValue(i)])
        end
    end
    let result = code_escapes() do
            o1 = SafeRef("foo")
            o2 = SafeRef(o1)
            return o2
        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)
        end
    end
    let result = code_escapes() do
            o1   = SafeRef("foo")
            o2′  = SafeRef(nothing)
            o2   = SafeRef{SafeRef}(o2′)
            o2[] = o1
            return o2
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        findall(1:length(result.ir.stmts)) do i
            if isnew(result.ir.stmts[i][:stmt])
                t = result.ir.stmts[i][:type]
                return t === SafeRef{String}  || # o1
                       t === SafeRef{SafeRef}    # o2
            end
            return false
        end |> x->foreach(x) do i
            @test has_return_escape(result.state[SSAValue(i)], r)
        end
    end
    let result = code_escapes((Base.RefValue{String},)) do x
            o = Ref(x)
            Core.donotdelete(o)
            broadcast(identity, 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)])
    end

    # ϕ-node allocations
    let result = code_escapes((Bool,Any,Any)) do cond, x, y
            if cond
                ϕ = SafeRef{Any}(x)
            else
                ϕ = SafeRef{Any}(y)
            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
        i = only(findall(isϕ, result.ir.stmts.stmt))
        @test is_load_forwardable(result.state[SSAValue(i)])
        for i in findall(isnew, result.ir.stmts.stmt)
            @test is_load_forwardable(result.state[SSAValue(i)])
        end
    end
    let result = code_escapes((Bool,Any,Any)) do cond, x, y
            if cond
                ϕ2 = ϕ1 = SafeRef{Any}(x)
            else
                ϕ2 = ϕ1 = SafeRef{Any}(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
        for i in findall(isϕ, result.ir.stmts.stmt)
            @test is_load_forwardable(result.state[SSAValue(i)])
        end
        for i in findall(isnew, result.ir.stmts.stmt)
            @test is_load_forwardable(result.state[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
            local out
            if cond
                ϕ = SafeRef(x)
                out = ϕ[]
            else
                ϕ = SafeRefs(z, y)
            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)
    end

    # alias analysis
    # --------------

    # alias via getfield & Expr(:new)
    let result = code_escapes((String,)) 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)
    end
    let result = code_escapes((String,)) do s
            r1 = SafeRef(s)
            r2 = SafeRef(r1)
            Core.donotdelete(r1, r2)
            return r2[]
        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)
    end
    let result = code_escapes((String,)) do s
            r1 = SafeRef(s)
            r2 = SafeRef(r1)
            Core.donotdelete(r1, r2)
            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)
        for i in findall(isnew, result.ir.stmts.stmt)
            @test !isaliased(SSAValue(i), val, result.state)
        end
    end
    let result = @eval EATModule() begin
            const Rx = SafeRef("Rx")
            $code_escapes((String,)) do s
                r = SafeRef(Rx)
                Core.donotdelete(r)
                rx = r[] # rx aliased to Rx
                rx[] = s
                nothing
            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)])
    end
    # alias via getfield & setfield!
    let result = code_escapes((String,)) do s
            r = Ref{String}()
            Core.donotdelete(r)
            r[] = s
            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)
    end
    let result = code_escapes((String,)) do s
            r1 = Ref(s)
            r2 = Ref{Base.RefValue{String}}()
            Core.donotdelete(r1, r2)
            r2[] = r1
            return r2[]
        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)
    end
    let result = code_escapes((String,)) do s
            r1 = Ref{String}()
            r2 = Ref{Base.RefValue{String}}()
            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)
        for i in findall(isnew, result.ir.stmts.stmt)
            @test !isaliased(SSAValue(i), val, result.state)
        end
        result = code_escapes((String,)) do s
            r1 = Ref{String}()
            r2 = Ref{Base.RefValue{String}}()
            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)
        for i in findall(isnew, result.ir.stmts.stmt)
            @test !isaliased(SSAValue(i), val, result.state)
        end
    end
    let result = @eval EATModule() begin
            const Rx = SafeRef("Rx")
            $code_escapes((SafeRef{String}, String,)) do _rx, s
                r = SafeRef(_rx)
                Core.donotdelete(r)
                r[] = Rx
                rx = r[] # rx aliased to Rx
                rx[] = s
                nothing
            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)])
    end
    # alias via typeassert
    let result = code_escapes((Any,)) do a
            r = a::Base.RefValue{String}
            return r
        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
    end
    let result = code_escapes((Any,)) do a
            global GV
            (g::SafeRef{Any})[] = a
            nothing
        end
        @test has_all_escape(result.state[Argument(2)])
    end
    # alias via ifelse
    let result = code_escapes((Bool,Any,Any)) do c, a, b
            r = ifelse(c, a, b)
            return r
        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
    end
    let result = @eval EATModule() begin
            const Lx, Rx = SafeRef("Lx"), SafeRef("Rx")
            $code_escapes((Bool,String,)) do c, a
                r = ifelse(c, Lx, Rx)
                r[] = a
                nothing
            end
        end
        @test has_all_escape(result.state[Argument(3)]) # a
    end
    # alias via ϕ-node
    let result = code_escapes((Bool,Base.RefValue{String})) do cond, x
            if cond
                ϕ2 = ϕ1 = SafeRef(Ref("foo"))
            else
                ϕ2 = ϕ1 = SafeRef(Ref("bar"))
            end
            ϕ2[] = x
            return ϕ1[]
        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
        for i in findall(isϕ, result.ir.stmts.stmt)
            @test is_load_forwardable(result.state[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)])
            end
        end
    end
    let result = code_escapes((Bool,Bool,Base.RefValue{String})) do cond1, cond2, x
            if cond1
                ϕ2 = ϕ1 = SafeRef(Ref("foo"))
            else
                ϕ2 = ϕ1 = SafeRef(Ref("bar"))
            end
            cond2 && (ϕ2[] = x)
            return ϕ1[]
        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
        for i in findall(isϕ, result.ir.stmts.stmt)
            @test is_load_forwardable(result.state[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)])
            end
        end
    end
    # alias via π-node
    let result = code_escapes((Any,)) do x
            if isa(x, Base.RefValue{String})
                return x
            end
            throw("error!")
        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)
    end
    let result = code_escapes((String,)) do x
            global GV
            l = g
            if isa(l, SafeRef{String})
                l[] = x
            end
            nothing
        end
        @test has_all_escape(result.state[Argument(2)]) # x
    end
    # circular reference
    let result = code_escapes() do
            x = Ref{Any}()
            x[] = x
            return x[]
        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)
    end
    let result = @eval Module() begin
            const Rx = Ref{Any}()
            Rx[] = Rx
            $code_escapes() do
                r = Rx[]::Base.RefValue{Any}
                return r[]
            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)
        end
    end
    let result = @eval Module() begin
            @noinline function genr()
                r = Ref{Any}()
                r[] = r
                return r
            end
            $code_escapes() do
                x = genr()
                return x[]
            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)
    end

    # dynamic semantics
    # -----------------

    # conservatively handle untyped objects
    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
    end
    let result = @eval code_escapes((Any,Any,Any,Any)) do T, x, y, z
            obj = $(Expr(:new, :T, :x, :y))
            return getfield(obj, :x)
        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)
    end
    let result = @eval code_escapes((Any,Any,Any,Any)) do T, x, y, z
            obj = $(Expr(:new, :T, :x))
            setfield!(obj, :x, y)
            return getfield(obj, :x)
        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)
    end

    # conservatively handle unknown field:
    # all fields should be escaped, but the allocation itself doesn't need to be escaped
    let result = code_escapes((Base.RefValue{String}, Symbol)) do a, fld
            obj = SafeRef(a)
            return getfield(obj, fld)
        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
    end
    let result = code_escapes((Base.RefValue{String}, Base.RefValue{String}, Symbol)) do a, b, fld
            obj = SafeRefs(a, b)
            return getfield(obj, fld) # should escape both `a` and `b`
        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
    end
    let result = code_escapes((Base.RefValue{String}, Base.RefValue{String}, Int)) do a, b, idx
            obj = SafeRefs(a, b)
            return obj[idx] # should escape both `a` and `b`
        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
    end
    let result = code_escapes((Base.RefValue{String}, Base.RefValue{String}, Symbol)) do a, b, fld
            obj = SafeRefs(Ref("a"), Ref("b"))
            setfield!(obj, fld, a)
            return obj[2] # should escape `a`
        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
    end
    let result = code_escapes((Base.RefValue{String}, Symbol)) do a, fld
            obj = SafeRefs(Ref("a"), Ref("b"))
            setfield!(obj, fld, a)
            return obj[1] # this should escape `a`
        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
    end
    let result = code_escapes((Base.RefValue{String}, Base.RefValue{String}, Int)) do a, b, idx
        obj = SafeRefs(Ref("a"), Ref("b"))
            obj[idx] = a
            return obj[2] # should escape `a`
        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
    end

    # interprocedural
    # ---------------

    let result = @eval EATModule() begin
            @noinline getx(obj) = obj[]
            $code_escapes((Base.RefValue{String},)) do a
                obj = SafeRef(a)
                fld = getx(obj)
                return fld
            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)
        # 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)])
    end

    # TODO interprocedural 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)])
    end

    # aliasing between arguments
    let result = @eval EATModule() begin
            @noinline setxy!(x, y) = x[] = y
            $code_escapes((String,)) do y
                x = SafeRef("init")
                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
    end
    let result = @eval EATModule() begin
            @noinline setxy!(x, y) = x[] = y
            $code_escapes((String,)) do y
                x1 = SafeRef("init")
                x2 = SafeRef(y)
                Core.donotdelete(x1, x2)
                setxy!(x1, x2[])
                return x1
            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
    end
    let result = @eval EATModule() begin
            @noinline mysetindex!(x, a) = x[1] = a
            const Ax = Vector{Any}(undef, 1)
            $code_escapes((Base.RefValue{String},)) do s
                mysetindex!(Ax, s)
            end
        end
        @test has_all_escape(result.state[Argument(2)]) # s
    end

    # TODO flow-sensitivity?
    # ----------------------

    let result = code_escapes((Any,Any)) do a, b
            r = SafeRef{Any}(a)
            Core.donotdelete(r)
            r[] = b
            return r[]
        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)])
    end
    let result = code_escapes((Any,Any)) do a, b
            r = SafeRef{Any}(:init)
            Core.donotdelete(r)
            r[] = a
            r[] = b
            return r[]
        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)])
    end
    let result = code_escapes((Any,Any,Bool)) do a, b, cond
            r = SafeRef{Any}(:init)
            Core.donotdelete(r)
            if cond
                r[] = a
                return r[]
            else
                r[] = b
                return nothing
            end
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        @test is_load_forwardable(result.state[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
    end

    # handle conflicting field information correctly
    let result = code_escapes((Bool,Base.RefValue{String},Base.RefValue{String},)) do cnd, baz, qux
            if cnd
                o = SafeRef(Ref("foo"))
            else
                o = SafeRefs(Ref("bar"), baz)
                r = getfield(o, 2)
            end
            if cnd
                o = o::SafeRef
                setfield!(o, 1, qux)
                r = getfield(o, 1)
            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
        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)])
            end
        end
    end
    let result = code_escapes((Bool,Base.RefValue{String},Base.RefValue{String},)) do cnd, baz, qux
            if cnd
                o = SafeRefs(Ref("foo"), Ref("bar"))
                r = setfield!(o, 2, baz)
            else
                o = SafeRef(qux)
            end
            if !cnd
                o = o::SafeRef
                r = getfield(o, 1)
            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
    end

    # foreigncall should disable field analysis
    let result = code_escapes((Any,Nothing,Int,UInt)) do t, mt, lim, world
            ambig = false
            min = Ref{UInt}(typemin(UInt))
            max = Ref{UInt}(typemax(UInt))
            has_ambig = Ref{Int32}(0)
            mt = ccall(:jl_matching_methods, Any,
                (Any, Any, Cint, Cint, UInt, Ptr{UInt}, Ptr{UInt}, Ref{Int32}),
                t, mt, lim, ambig, world, min, max, has_ambig)::Union{Array{Any,1}, Bool}
            return mt, has_ambig[]
        end
        for i in findall(isnew, result.ir.stmts.stmt)
            @test !is_load_forwardable(result.state[SSAValue(i)])
        end
    end
end

# demonstrate the power of our field / alias analysis with a realistic end to end example
abstract type AbstractPoint{T} end
mutable struct MPoint{T} <: AbstractPoint{T}
    x::T
    y::T
end
add(a::P, b::P) where P<:AbstractPoint = P(a.x + b.x, a.y + b.y)
function compute(T, ax, ay, bx, by)
    a = T(ax, ay)
    b = T(bx, by)
    for i in 0:(100000000-1)
        c = add(a, b) # replaceable
        a = add(c, b) # replaceable
    end
    a.x, a.y
end
let result = @code_escapes compute(MPoint, 1+.5im, 2+.5im, 2+.25im, 4+.75im)
    for i in findall(1:length(result.ir.stmts)) do idx
                 inst = result.ir[SSAValue(idx)]
                 stmt = inst[:stmt]
                 return (isnew(stmt) || isϕ(stmt)) && inst[:type] <: MPoint
             end
        @test is_load_forwardable(result.state[SSAValue(i)])
    end
end
function compute(a, b)
    for i in 0:(100000000-1)
        c = add(a, b) # replaceable
        a = add(c, b) # unreplaceable (aliased to the call argument `a`)
    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
function compute!(a, b)
    for i in 0:(100000000-1)
        c = add(a, b)  # replaceable
        a′ = add(c, b) # replaceable
        a.x = a′.x
        a.y = a′.y
    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
                 inst = result.ir[SSAValue(idx)]
                 stmt = inst[:stmt]
                 return isnew(stmt) && inst[:type] <: MPoint
             end
        @test is_load_forwardable(result.state[SSAValue(i)])
    end
end

@testset "array primitives" begin
    # arrayref
    @test_skip let result = code_escapes((Vector{String},Int)) do xs, i
            s = Base.arrayref(true, xs, i)
            return s
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test has_return_escape(result.state[Argument(2)], r)   # xs
        @test has_thrown_escape(result.state[Argument(2)])      # xs
        @test !has_return_escape(result.state[Argument(3)], r)  # i
    end
    @test_skip let result = code_escapes((Vector{String},Int)) do xs, i
            s = Base.arrayref(false, xs, i)
            return s
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test has_return_escape(result.state[Argument(2)], r)   # xs
        @test !has_thrown_escape(result.state[Argument(2)])     # xs
        @test !has_return_escape(result.state[Argument(3)], r)  # i
    end
    @test_skip let result = code_escapes((Vector{String},Bool)) do xs, i
            c = Base.arrayref(true, xs, i) # TypeError will happen here
            return c
        end
        t = only(findall(iscall((result.ir, Base.arrayref)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
    end
    @test_skip let result = code_escapes((String,Int)) do xs, i
            c = Base.arrayref(true, xs, i) # TypeError will happen here
            return c
        end
        t = only(findall(iscall((result.ir, Base.arrayref)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
    end
    @test_skip let result = code_escapes((AbstractVector{String},Int)) do xs, i
            c = Base.arrayref(true, xs, i) # TypeError may happen here
            return c
        end
        t = only(findall(iscall((result.ir, Base.arrayref)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
    end
    @test_skip let result = code_escapes((Vector{String},Any)) do xs, i
            c = Base.arrayref(true, xs, i) # TypeError may happen here
            return c
        end
        t = only(findall(iscall((result.ir, Base.arrayref)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
    end

    # arrayset
    @test_skip let result = code_escapes((Vector{String},String,Int,)) do xs, x, i
            Base.arrayset(true, xs, x, i)
            return xs
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test has_return_escape(result.state[Argument(2)], r) # xs
        @test has_thrown_escape(result.state[Argument(2)])    # xs
        @test has_return_escape(result.state[Argument(3)], r) # x
    end
    @test_skip let result = code_escapes((Vector{String},String,Int,)) do xs, x, i
            Base.arrayset(false, xs, x, i)
            return xs
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test has_return_escape(result.state[Argument(2)], r) # xs
        @test !has_thrown_escape(result.state[Argument(2)])    # xs
        @test has_return_escape(result.state[Argument(3)], r) # x
    end
    @test_skip let result = code_escapes((String,String,String,)) do s, t, u
            xs = Vector{String}(undef, 3)
            Base.arrayset(true, xs, s, 1)
            Base.arrayset(true, xs, t, 2)
            Base.arrayset(true, xs, u, 3)
            return xs
        end
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test has_return_escape(result.state[SSAValue(i)], r)
        for i in 2:result.state.nargs
            @test has_return_escape(result.state[Argument(i)], r)
        end
    end
    @test_skip let result = code_escapes((Vector{String},String,Bool,)) do xs, x, i
            Base.arrayset(true, xs, x, i) # TypeError will happen here
            return xs
        end
        t = only(findall(iscall((result.ir, Base.arrayset)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
        @test has_thrown_escape(result.state[Argument(3)], t) # x
    end
    @test_skip let result = code_escapes((String,String,Int,)) do xs, x, i
            Base.arrayset(true, xs, x, i) # TypeError will happen here
            return xs
        end
        t = only(findall(iscall((result.ir, Base.arrayset)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs::String
        @test has_thrown_escape(result.state[Argument(3)], t) # x::String
    end
    @test_skip let result = code_escapes((AbstractVector{String},String,Int,)) do xs, x, i
            Base.arrayset(true, xs, x, i) # TypeError may happen here
            return xs
        end
        t = only(findall(iscall((result.ir, Base.arrayset)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
        @test has_thrown_escape(result.state[Argument(3)], t) # x
    end
    @test_skip let result = code_escapes((Vector{String},AbstractString,Int,)) do xs, x, i
            Base.arrayset(true, xs, x, i) # TypeError may happen here
            return xs
        end
        t = only(findall(iscall((result.ir, Base.arrayset)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
        @test has_thrown_escape(result.state[Argument(3)], t) # x
    end

    # arrayref and arrayset
    @test_skip let result = code_escapes() do
            a = Vector{Vector{Any}}(undef, 1)
            b = Any[]
            a[1] = b
            return a[1]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        ai = only(findall(result.ir.stmts.stmt) do @nospecialize x
            isarrayalloc(x) && x.args[2] === Vector{Vector{Any}}
        end)
        bi = only(findall(result.ir.stmts.stmt) do @nospecialize x
            isarrayalloc(x) && x.args[2] === Vector{Any}
        end)
        @test !has_return_escape(result.state[SSAValue(ai)], r)
        @test has_return_escape(result.state[SSAValue(bi)], r)
    end
    @test_skip let result = code_escapes() do
            a = Vector{Vector{Any}}(undef, 1)
            b = Any[]
            a[1] = b
            return a
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        ai = only(findall(result.ir.stmts.stmt) do @nospecialize x
            isarrayalloc(x) && x.args[2] === Vector{Vector{Any}}
        end)
        bi = only(findall(result.ir.stmts.stmt) do @nospecialize x
            isarrayalloc(x) && x.args[2] === Vector{Any}
        end)
        @test has_return_escape(result.state[SSAValue(ai)], r)
        @test has_return_escape(result.state[SSAValue(bi)], r)
    end
    @test_skip let result = code_escapes((Vector{Any},String,Int,Int)) do xs, s, i, j
            x = SafeRef(s)
            xs[i] = x
            xs[j] # potential error
        end
        i = only(findall(isnew, result.ir.stmts.stmt))
        t = only(findall(iscall((result.ir, Base.arrayref)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(3)], t) # s
        @test has_thrown_escape(result.state[SSAValue(i)], t) # x
    end

    # arraysize
    @test_skip let result = code_escapes((Vector{Any},)) do xs
            Core.arraysize(xs, 1)
        end
        t = only(findall(iscall((result.ir, Core.arraysize)), result.ir.stmts.stmt))
        @test !has_thrown_escape(result.state[Argument(2)], t)
    end
    @test_skip let result = code_escapes((Vector{Any},Int,)) do xs, dim
            Core.arraysize(xs, dim)
        end
        t = only(findall(iscall((result.ir, Core.arraysize)), result.ir.stmts.stmt))
        @test !has_thrown_escape(result.state[Argument(2)], t)
    end
    @test_skip let result = code_escapes((Any,)) do xs
            Core.arraysize(xs, 1)
        end
        t = only(findall(iscall((result.ir, Core.arraysize)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t)
    end

    # arraylen
    @test_skip let result = code_escapes((Vector{Any},)) do xs
            Base.arraylen(xs)
        end
        t = only(findall(iscall((result.ir, Base.arraylen)), result.ir.stmts.stmt))
        @test !has_thrown_escape(result.state[Argument(2)], t) # xs
    end
    @test_skip let result = code_escapes((String,)) do xs
            Base.arraylen(xs)
        end
        t = only(findall(iscall((result.ir, Base.arraylen)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
    end
    @test_skip let result = code_escapes((Vector{Any},)) do xs
            Base.arraylen(xs, 1)
        end
        t = only(findall(iscall((result.ir, Base.arraylen)), result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
    end

    # array resizing
    # without BoundsErrors
    @test_skip let result = code_escapes((Vector{Any},String)) do xs, x
            @ccall jl_array_grow_beg(xs::Any, 2::UInt)::Cvoid
            xs[1] = x
            xs
        end
        t = only(findall(isarrayresize, result.ir.stmts.stmt))
        @test !has_thrown_escape(result.state[Argument(2)], t) # xs
        @test !has_thrown_escape(result.state[Argument(3)], t) # x
    end
    @test_skip let result = code_escapes((Vector{Any},String)) do xs, x
            @ccall jl_array_grow_end(xs::Any, 2::UInt)::Cvoid
            xs[1] = x
            xs
        end
        t = only(findall(isarrayresize, result.ir.stmts.stmt))
        @test !has_thrown_escape(result.state[Argument(2)], t) # xs
        @test !has_thrown_escape(result.state[Argument(3)], t) # x
    end
    # with possible BoundsErrors
    @test_skip let result = code_escapes((String,)) do x
            xs = Any[1,2,3]
            xs[3] = x
            @ccall jl_array_del_beg(xs::Any, 2::UInt)::Cvoid # can potentially throw
            xs
        end
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        t = only(findall(isarrayresize, result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[SSAValue(i)], t) # xs
        @test has_thrown_escape(result.state[Argument(2)], t) # x
    end
    @test_skip let result = code_escapes((String,)) do x
            xs = Any[1,2,3]
            xs[1] = x
            @ccall jl_array_del_end(xs::Any, 2::UInt)::Cvoid # can potentially throw
            xs
        end
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        t = only(findall(isarrayresize, result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[SSAValue(i)], t) # xs
        @test has_thrown_escape(result.state[Argument(2)], t) # x
    end
    @test_skip let result = code_escapes((String,)) do x
            xs = Any[x]
            @ccall jl_array_grow_at(xs::Any, 1::UInt, 2::UInt)::Cvoid # can potentially throw
        end
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        t = only(findall(isarrayresize, result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[SSAValue(i)], t) # xs
        @test has_thrown_escape(result.state[Argument(2)], t) # x
    end
    @test_skip let result = code_escapes((String,)) do x
            xs = Any[x]
            @ccall jl_array_del_at(xs::Any, 1::UInt, 2::UInt)::Cvoid # can potentially throw
        end
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        t = only(findall(isarrayresize, result.ir.stmts.stmt))
        @test has_thrown_escape(result.state[SSAValue(i)], t) # xs
        @test has_thrown_escape(result.state[Argument(2)], t) # x
    end

    # array copy
    @test_skip let result = code_escapes((Vector{Any},)) do xs
            return copy(xs)
        end
        i = only(findall(isarraycopy, 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)
    end
    @test_skip let result = code_escapes((String,)) do s
            xs = String[s]
            xs′ = copy(xs)
            return xs′[1]
        end
        i1 = only(findall(isarrayalloc, result.ir.stmts.stmt))
        i2 = only(findall(isarraycopy, result.ir.stmts.stmt))
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test !has_return_escape(result.state[SSAValue(i1)])
        @test !has_return_escape(result.state[SSAValue(i2)])
        @test has_return_escape(result.state[Argument(2)], r) # s
    end
    @test_skip let result = code_escapes((Vector{Any},)) do xs
            xs′ = copy(xs)
            return xs′[1] # may potentially throw BoundsError, should escape `xs` conservatively (i.e. escape its elements)
        end
        i = only(findall(isarraycopy, result.ir.stmts.stmt))
        ref = only(findall(iscall((result.ir, Base.arrayref)), result.ir.stmts.stmt))
        ret = only(findall(isreturn, result.ir.stmts.stmt))
        @test_broken !has_thrown_escape(result.state[SSAValue(i)], ref)
        @test_broken !has_return_escape(result.state[SSAValue(i)], ret)
        @test has_thrown_escape(result.state[Argument(2)], ref)
        @test has_return_escape(result.state[Argument(2)], ret)
    end
    @test_skip let result = code_escapes((String,)) do s
            xs = Vector{String}(undef, 1)
            xs[1] = s
            xs′ = copy(xs)
            length(xs′) > 2 && throw(xs′)
            return xs′
        end
        i1 = only(findall(isarrayalloc, result.ir.stmts.stmt))
        i2 = only(findall(isarraycopy, result.ir.stmts.stmt))
        t = only(findall(iscall((result.ir, throw)), result.ir.stmts.stmt))
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test_broken !has_thrown_escape(result.state[SSAValue(i1)], t)
        @test_broken !has_return_escape(result.state[SSAValue(i1)], r)
        @test has_thrown_escape(result.state[SSAValue(i2)], t)
        @test has_return_escape(result.state[SSAValue(i2)], r)
        @test has_thrown_escape(result.state[Argument(2)], t)
        @test has_return_escape(result.state[Argument(2)], r)
    end

    # isassigned
    let result = code_escapes((Vector{Any},Int)) do xs, i
            return isassigned(xs, i)
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test_broken !has_return_escape(result.state[Argument(2)], r)
        @test_broken !has_thrown_escape(result.state[Argument(2)])
    end

    # indexing analysis
    # -----------------

    # safe case
    @test_skip let result = code_escapes((String,String)) do s, t
            a = Vector{Any}(undef, 2)
            a[1] = s
            a[2] = t
            return a[1]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        @test !has_return_escape(result.state[SSAValue(i)], r)
        @test is_load_forwardable(result.state[SSAValue(i)])
        @test has_return_escape(result.state[Argument(2)], r) # s
        @test !has_return_escape(result.state[Argument(3)], r) # t
    end
    @test_skip let result = code_escapes((String,String)) do s, t
            a = Matrix{Any}(undef, 1, 2)
            a[1, 1] = s
            a[1, 2] = t
            return a[1, 1]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        @test !has_return_escape(result.state[SSAValue(i)], r)
        @test is_load_forwardable(result.state[SSAValue(i)])
        @test has_return_escape(result.state[Argument(2)], r) # s
        @test !has_return_escape(result.state[Argument(3)], r) # t
    end
    @test_skip let result = code_escapes((Bool,String,String,String)) do c, s, t, u
            a = Vector{Any}(undef, 2)
            if c
                a[1] = s
                a[2] = u
            else
                a[1] = t
                a[2] = u
            end
            return a[1]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        @test is_load_forwardable(result.state[SSAValue(i)])
        @test !has_return_escape(result.state[SSAValue(i)], r)
        @test has_return_escape(result.state[Argument(3)], r) # s
        @test has_return_escape(result.state[Argument(4)], r) # t
        @test !has_return_escape(result.state[Argument(5)], r) # u
    end
    @test_skip let result = code_escapes((Bool,String,String,String)) do c, s, t, u
            a = Any[nothing, nothing] # TODO how to deal with loop indexing?
            if c
                a[1] = s
                a[2] = u
            else
                a[1] = t
                a[2] = u
            end
            return a[1]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        @test !has_return_escape(result.state[SSAValue(i)], r)
        @test_broken is_load_forwardable(result.state[SSAValue(i)])
        @test has_return_escape(result.state[Argument(3)], r) # s
        @test has_return_escape(result.state[Argument(4)], r) # t
        @test_broken !has_return_escape(result.state[Argument(5)], r) # u
    end
    @test_skip let result = code_escapes((String,)) do s
            a = Vector{Vector{Any}}(undef, 1)
            b = Any[s]
            a[1] = b
            return a[1][1]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        is = findall(isarrayalloc, result.ir.stmts.stmt)
        @assert length(is) == 2
        ia, ib = is
        @test !has_return_escape(result.state[SSAValue(ia)], r)
        @test is_load_forwardable(result.state[SSAValue(ia)])
        @test !has_return_escape(result.state[SSAValue(ib)], r)
        @test_broken is_load_forwardable(result.state[SSAValue(ib)])
        @test has_return_escape(result.state[Argument(2)], r) # s
    end
    @test_skip let result = code_escapes((Bool,String,String,Regex,Regex,)) do c, s1, s2, t1, t2
            if c
                a = Vector{String}(undef, 2)
                a[1] = s1
                a[2] = s2
            else
                a = Vector{Regex}(undef, 2)
                a[1] = t1
                a[2] = t2
            end
            return a[1]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        for i in findall(isarrayalloc, result.ir.stmts.stmt)
            @test !has_return_escape(result.state[SSAValue(i)], r)
            @test is_load_forwardable(result.state[SSAValue(i)])
        end
        @test has_return_escape(result.state[Argument(3)], r) # s1
        @test !has_return_escape(result.state[Argument(4)], r) # s2
        @test has_return_escape(result.state[Argument(5)], r) # t1
        @test !has_return_escape(result.state[Argument(6)], r) # t2
    end
    @test_skip let result = code_escapes((String,String,Int)) do s, t, i
            a = Any[s]
            push!(a, t)
            return a[2]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        @test !has_return_escape(result.state[SSAValue(i)], r)
        @test_broken is_load_forwardable(result.state[SSAValue(i)])
        @test_broken !has_return_escape(result.state[Argument(2)], r) # s
        @test has_return_escape(result.state[Argument(3)], r) # t
    end
    # unsafe cases
    @test_skip let result = code_escapes((String,String,Int)) do s, t, i
            a = Vector{Any}(undef, 2)
            a[1] = s
            a[2] = t
            return a[i]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        @test !has_return_escape(result.state[SSAValue(i)], r)
        @test !is_load_forwardable(result.state[SSAValue(i)])
        @test has_return_escape(result.state[Argument(2)], r) # s
        @test has_return_escape(result.state[Argument(3)], r) # t
    end
    @test_skip let result = code_escapes((String,String,Int)) do s, t, i
            a = Vector{Any}(undef, 2)
            a[1] = s
            a[i] = t
            return a[1]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        @test !has_return_escape(result.state[SSAValue(i)], r)
        @test !is_load_forwardable(result.state[SSAValue(i)])
        @test has_return_escape(result.state[Argument(2)], r) # s
        @test has_return_escape(result.state[Argument(3)], r) # t
    end
    @test_skip let result = code_escapes((String,String,Int,Int,Int)) do s, t, i, j, k
            a = Vector{Any}(undef, 2)
            a[3] = s # BoundsError
            a[1] = t
            return a[1]
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        @test !has_return_escape(result.state[SSAValue(i)], r)
        @test !is_load_forwardable(result.state[SSAValue(i)])
    end
    @test_skip let result = @eval Module() begin
            @noinline some_resize!(a) = pushfirst!(a, nothing)
            $code_escapes((String,String,Int)) do s, t, i
                a = Vector{Any}(undef, 2)
                a[1] = s
                some_resize!(a)
                return a[2]
            end
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        @test_broken !has_return_escape(result.state[SSAValue(i)], r)
        @test !is_load_forwardable(result.state[SSAValue(i)])
    end

    # circular reference
    @test_skip let result = code_escapes() do
            xs = Vector{Any}(undef, 1)
            xs[1] = xs
            return xs[1]
        end
        i = only(findall(isarrayalloc, result.ir.stmts.stmt))
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test has_return_escape(result.state[SSAValue(i)], r)
    end
    @test_skip let result = @eval Module() begin
            const Ax = Vector{Any}(undef, 1)
            Ax[1] = Ax
            $code_escapes() do
                xs = Ax[1]::Vector{Any}
                return xs[1]
            end
        end
        r = only(findall(isreturn, result.ir.stmts.stmt))
        for i in findall(iscall((result.ir, Core.arrayref)), result.ir.stmts.stmt)
            @test has_return_escape(result.state[SSAValue(i)], r)
        end
    end
    let result = @eval Module() begin
            @noinline function genxs()
                xs = Vector{Any}(undef, 1)
                xs[1] = xs
                return xs
            end
            $code_escapes() do
                xs = genxs()
                return xs[1]
            end
        end
        i = only(findall(isinvoke(:genxs), result.ir.stmts.stmt))
        r = only(findall(isreturn, result.ir.stmts.stmt))
        @test has_return_escape(result.state[SSAValue(i)], r)
    end
end

# demonstrate array primitive support with a realistic end to end example
@test_skip let result = code_escapes((Int,String,)) do n,s
        xs = String[]
        for i in 1:n
            push!(xs, s)
        end
        xs
    end
    i = only(findall(isarrayalloc, result.ir.stmts.stmt))
    r = only(findall(isreturn, result.ir.stmts.stmt))
    @test has_return_escape(result.state[SSAValue(i)], r)
    @test !has_thrown_escape(result.state[SSAValue(i)])
    @test has_return_escape(result.state[Argument(3)], r) # s
    @test !has_thrown_escape(result.state[Argument(3)])    # s
end
@test_skip let result = code_escapes((Int,String,)) do n,s
        xs = String[]
        for i in 1:n
            pushfirst!(xs, s)
        end
        xs
    end
    i = only(findall(isarrayalloc, result.ir.stmts.stmt))
    r = only(findall(isreturn, result.ir.stmts.stmt))
    @test has_return_escape(result.state[SSAValue(i)], r) # xs
    @test !has_thrown_escape(result.state[SSAValue(i)])    # xs
    @test has_return_escape(result.state[Argument(3)], r) # s
    @test !has_thrown_escape(result.state[Argument(3)])    # s
end
@test_skip let result = code_escapes((String,String,String)) do s, t, u
        xs = String[]
        resize!(xs, 3)
        xs[1] = s
        xs[1] = t
        xs[1] = u
        xs
    end
    i = only(findall(isarrayalloc, result.ir.stmts.stmt))
    r = only(findall(isreturn, result.ir.stmts.stmt))
    @test has_return_escape(result.state[SSAValue(i)], r)
    @test has_thrown_escape(result.state[SSAValue(i)])    # xs
    @test has_return_escape(result.state[Argument(2)], r) # s
    @test has_return_escape(result.state[Argument(3)], r) # t
    @test has_return_escape(result.state[Argument(4)], r) # u
end

# demonstrate a simple type level analysis can sometimes improve the analysis accuracy
# by compensating the lack of yet unimplemented analyses
@testset "special-casing bitstype" begin
    let result = code_escapes((Nothing,)) do a
            global GV = a
        end
        @test !(has_all_escape(result.state[Argument(2)]))
    end

    let result = code_escapes((Int,)) 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[SSAValue(i)], r)
    end

    # an escaped tuple stmt will not propagate to its Int argument (since `Int` is of bitstype)
    let result = code_escapes((Int,Any,)) do a, b
            t = tuple(a, b)
            return t
        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)
    end
end

# # TODO implement a finalizer elision pass
# mutable struct WithFinalizer
#     v
#     function WithFinalizer(v)
#         x = new(v)
#         f(t) = @async println("Finalizing $t.")
#         return finalizer(x, x)
#     end
# end
# make_m(v = 10) = MyMutable(v)
# function simple(cond)
#     m = make_m()
#     if cond
#         # println(m.v)
#         return nothing # <= insert `finalize` call here
#     end
#     return m
# end

# interprocedural analysis
# ========================

# propagate escapes imposed on call arguments
@noinline broadcast_noescape2(b) = broadcast(identity, b)
let result = code_escapes() do
        broadcast_noescape2(Ref(Ref("Hi")))
    end
    i = last(findall(isnew, result.ir.stmts.stmt))
    @test_broken !has_return_escape(result.state[SSAValue(i)]) # TODO interprocedural alias analysis
    @test !has_thrown_escape(result.state[SSAValue(i)])
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)])
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)])
end
let result = code_escapes((Ref{Any},)) do a
        Base.@invokelatest broadcast_noescape1(a)
    end
    @test has_all_escape(result.state[Argument(2)])
end

# handling of simple union-split (just exploit the inliner's effort)
@noinline unionsplit_noescape(a)      = string(nothing)
@noinline unionsplit_noescape(a::Int) = a + 10
let result = code_escapes((Union{Int,Nothing},)) do x
        s = SafeRef{Union{Int,Nothing}}(x)
        unionsplit_noescape(s[])
        return nothing
    end
    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)])
    end
end

@noinline unused_argument(a) = (println("prevent inlining"); nothing)
let result = code_escapes() do
        a = Ref("foo") # shouldn't be "return escape"
        b = unused_argument(a)
        nothing
    end
    i = only(findall(isnew, result.ir.stmts.stmt))
    @test has_no_escape(result.state[SSAValue(i)])

    result = code_escapes() do
        a = Ref("foo") # still should be "return escape"
        b = unused_argument(a)
        return a
    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)
end

# should propagate escape information imposed on return value to the aliased call argument
@noinline returnescape_argument(a) = (println("prevent inlining"); a)
let result = code_escapes() do
        obj = Ref("foo")           # should be "return escape"
        ret = returnescape_argument(obj)
        return ret                 # alias of `obj`
    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)
end
@noinline noreturnescape_argument(a) = (println("prevent inlining"); identity("hi"))
let result = code_escapes() do
        obj = Ref("foo")              # better to not be "return escape"
        ret = noreturnescape_argument(obj)
        return ret                    # must not alias to `obj`
    end
    i = only(findall(isnew, result.ir.stmts.stmt))
    @test has_no_escape(result.state[SSAValue(i)])
end

function with_self_aliased(from_bb::Int, succs::Vector{Int})
    worklist = Int[from_bb]
    visited = BitSet(from_bb)
    function visit!(bb::Int)
        if bb ∉ visited
            push!(visited, bb)
            push!(worklist, bb)
        end
    end
    while !isempty(worklist)
        foreach(visit!, succs)
    end
    return visited
end
@test code_escapes(with_self_aliased) isa EAUtils.EscapeResult

# accounts for ThrownEscape via potential MethodError

# no method error
@noinline identity_if_string(x::SafeRef{<:AbstractString}) = (println("preventing inlining"); nothing)
let result = code_escapes((SafeRef{String},)) do x
        identity_if_string(x)
    end
    @test has_no_escape(ignore_argescape(result.state[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)
end
let result = code_escapes((SafeRef{String},)) do x
        try
            identity_if_string(x)
        catch err
            global GV = err
        end
        return nothing
    end
    @test !has_all_escape(result.state[Argument(2)])
end
let result = code_escapes((Union{SafeRef{String},Vector{String}},)) do x
        try
            identity_if_string(x)
        catch err
            global GV = err
        end
        return nothing
    end
    @test has_all_escape(result.state[Argument(2)])
end
# method ambiguity error
@noinline ambig_error_test(a::SafeRef, b) = (println("preventing inlining"); nothing)
@noinline ambig_error_test(a, b::SafeRef) = (println("preventing inlining"); nothing)
@noinline ambig_error_test(a, b) = (println("preventing inlining"); nothing)
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
end
let result = code_escapes((SafeRef{String},Any)) do x, y
        try
            ambig_error_test(x, y)
        catch err
            global GV = err
        end
    end
    @test has_all_escape(result.state[Argument(2)])  # x
    @test has_all_escape(result.state[Argument(3)])  # y
end

@eval function scope_folding()
    $(Expr(:tryfinally,
        Expr(:block,
            Expr(:tryfinally, :(), :(), 2),
            :(return Core.current_scope())),
    :(), 1))
end
@eval function scope_folding_opt()
    $(Expr(:tryfinally,
        Expr(:block,
            Expr(:tryfinally, :(), :(), :(Base.inferencebarrier(2))),
            :(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

end # module test_EA
