text:
include(normpath(@__DIR__, "setup.jl"))

@testset "basics" begin
    let # arg return
        result = code_escapes((Any,)) do a # return to caller
            return nothing
        end
        @test has_arg_escape(result.state[Argument(2)])
        # return
        result = code_escapes((Any,)) do a
            return a
        end
        i = only(findall(isreturn, result.ir.stmts.inst))
        @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 aa = a
            nothing
        end
        @test has_all_escape(result.state[Argument(2)])
    end
    let # global load
        result = code_escapes() do
            global gr
            return gr
        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 v
            v = s
            return v
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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
                return nothing
            end
        end
        i = findfirst(isT(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(isT(Base.RefValue{String}), result.ir.stmts.type) # find allocation statement
        @test !isnothing(i)
        @test 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.inst)
        i = only(findall(isreturn, result.ir.stmts.inst))
        @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.inst)
        @test any(findall(isreturn, result.ir.stmts.inst)) 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.inst)
        @assert any(@nospecialize(x)->isa(x, Core.UpsilonNode), result.ir.stmts.inst)
        i = only(findall(isreturn, result.ir.stmts.inst))
        @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.inst))
        @test has_return_escape(result.state[SSAValue(i)])
    end
    let # try/catch
        result = code_escapes((Any,)) do a
            try
                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
                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

let # simple allocation
    result = code_escapes((Bool,)) do c
        mm = SafeRef{Bool}(c) # just allocated, never escapes
        return mm[] ? nothing : 1
    end
    i = only(findall(isnew, result.ir.stmts.inst))
    @test has_no_escape(result.state[SSAValue(i)])
end

@testset "inter-procedural" begin
    # FIXME currently we can't prove the effect-freeness of `getfield(RefValue{String}, :x)`
    # because of this check https://github.com/JuliaLang/julia/blob/94b9d66b10e8e3ebdb268e4be5f7e1f43079ad4e/base/compiler/tfuncs.jl#L745
    # and thus it leads to the following two broken tests
    let result = @eval Module() begin
            @noinline broadcast_NoEscape(a) = (broadcast(identity, a); nothing)
            $code_escapes() do
                broadcast_NoEscape(Ref("Hi"))
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @test_broken has_no_escape(result.state[SSAValue(i)])
    end
    let result = @eval Module() begin
            @noinline broadcast_NoEscape2(b) = broadcast(identity, b)
            $code_escapes() do
                broadcast_NoEscape2(Ref("Hi"))
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @test_broken has_no_escape(result.state[SSAValue(i)])
    end
    let result = @eval Module() begin
            @noinline f_GlobalEscape_a(a) = (global globala = a) # obvious escape
            $code_escapes() do
                f_GlobalEscape_a(Ref("Hi"))
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @test has_return_escape(result.state[SSAValue(i)]) && has_thrown_escape(result.state[SSAValue(i)])
    end
    # if we can't determine the matching method statically, we should be conservative
    let result = @eval Module() $code_escapes((Ref{Any},)) do a
            may_exist(a)
        end
        @test has_all_escape(result.state[Argument(2)])
    end
    let result = @eval Module() begin
            @noinline broadcast_NoEscape(a) = (broadcast(identity, a); nothing)
            $code_escapes((Ref{Any},)) do a
                Base.@invokelatest broadcast_NoEscape(a)
            end
        end
        @test has_all_escape(result.state[Argument(2)])
    end

    # handling of simple union-split (just exploit the inliner's effort)
    let T = Union{Int,Nothing}
        result = @eval Module() begin
            @noinline unionsplit_NoEscape_a(a)      = string(nothing)
            @noinline unionsplit_NoEscape_a(a::Int) = a + 10
            $code_escapes(($T,)) do x
                s = $SafeRef{$T}(x)
                unionsplit_NoEscape_a(s[])
                return nothing
            end
        end
        inds = findall(isT(SafeRef{T}), result.ir.stmts.type) # find allocation statement
        @assert !isempty(inds)
        for i in inds
            @test has_no_escape(result.state[SSAValue(i)])
        end
    end

    # appropriate conversion of inter-procedural context
    # https://github.com/aviatesk/EscapeAnalysis.jl/issues/7
    let M = Module()
        @eval M @noinline f_NoEscape_a(a) = (println("prevent inlining"); Base.inferencebarrier(nothing))

        result = @eval M $code_escapes() do
            a = Ref("foo") # shouldn't be "return escape"
            b = f_NoEscape_a(a)
            nothing
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test !has_return_escape(result.state[SSAValue(i)], r)

        result = @eval M $code_escapes() do
            a = Ref("foo") # still should be "return escape"
            b = f_NoEscape_a(a)
            return a
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[SSAValue(i)], r)
    end

    # should propagate escape information imposed on return value to the aliased call argument
    let result = @eval Module() begin
            @noinline f_ReturnEscape_a(a) = (println("prevent inlining"); a)
            $code_escapes() do
                obj = Ref("foo")           # should be "return escape"
                ret = f_ReturnEscape_a(obj)
                return ret                 # alias of `obj`
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[SSAValue(i)], r)
    end
    let result = @eval Module() begin
            @noinline f_NoReturnEscape_a(a) = (println("prevent inlining"); identity("hi"))
            $code_escapes() do
                obj = Ref("foo")              # better to not be "return escape"
                ret = f_NoReturnEscape_a(obj)
                return ret                    # must not alias to `obj`
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test !has_return_escape(result.state[SSAValue(i)], r)
    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, String)) do cond, s
            m = cond ? SafeRef(s) : nothing
            c = m === nothing
            return c
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @test has_no_escape(result.state[SSAValue(i)])
    end

    let # sizeof
        ary = [0,1,2]
        result = @eval code_escapes() do
            ary = $(QuoteNode(ary))
            sizeof(ary)
        end
        i = only(findall(isT(Core.Const(ary)), result.ir.stmts.type))
        @test has_no_escape(result.state[SSAValue(i)])
    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.inst)
        @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.inst[i]) && isT(Base.RefValue{String})(result.ir.stmts.type[i])
                @test has_return_escape(result.state[SSAValue(i)])
            elseif isnew(result.ir.stmts.inst[i]) && isT(Base.RefValue{Nothing})(result.ir.stmts.type[i])
                @test has_no_escape(result.state[SSAValue(i)])
            end
        end
    end

    let # typeassert
        result = code_escapes((Any,)) do x
            y = x::String
            return y
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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.inst))
        @test has_return_escape(result.state[Argument(2)], r)
        @test !has_all_escape(result.state[Argument(2)])

        result = code_escapes((Module,)) do m
            isdefined(m, 10) # throws
        end
        @test has_thrown_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.inst))
        rts = findall(isreturn, result.ir.stmts.inst)
        @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.inst))
        rts = findall(isreturn, result.ir.stmts.inst) # 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 rethrow_escape!()
            try
                rethrow()
            catch err
                Gx[] = err
            end
        end
        @noinline function current_exceptions_escape!()
            excs = Base.current_exceptions()
            Gx[] = excs
        end
        const Gx = 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.inst))
        @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 g = err
            end
            nothing
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @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
                Gx[] = err2
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @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
                Gx[] = err2
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @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
                rethrow_escape!()
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @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)
                rethrow_escape!()
            end
            return t
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @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
                Gx[] = Base.current_exceptions()
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @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
                current_exceptions_escape!()
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @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 g = err
            end
            s2 = unsafeget(r2)
            return s2, r2
        end
        is = findall(isnew, result.ir.stmts.inst)
        @test length(is) == 2
        i1, i2 = is
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test_broken !has_return_escape(result.state[SSAValue(i)], r)
    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 g = err1
            end
            try
                s2 = unsafeget(r2)
                ret = sizeof(s2)
            catch err2
                ret = err2
            end
            return ret
        end
        is = findall(isnew, result.ir.stmts.inst)
        @test length(is) == 2
        i1, i2 = is
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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)])
    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.inst))
        @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.inst))
        rs = findall(isreturn, result.ir.stmts.inst)
        @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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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 g = SafeRef{Any}(a)
            nothing
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @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 g = (a,)
            nothing
        end
        i = only(findall(issubT(Tuple), result.ir.stmts.type))
        @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 g = SafeRef(o0)
            nothing
        end
        is = findall(isnew, result.ir.stmts.inst)
        @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 g = (t0,)
            nothing
        end
        inds = findall(issubT(Tuple), result.ir.stmts.type)
        @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 g = r
            r[] = a
            nothing
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @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 g = r
            r[] = b
            nothing
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @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{String}("Rx")
            $code_escapes((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((String,)) do a
            o = SafeRef(a)
            f = o[]
            return f
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[Argument(2)], r)
        @test is_load_forwardable(result.state[SSAValue(i)])
    end
    let result = code_escapes((String,)) do a
            t = (a,)
            f = t[1]
            return f
        end
        i = only(findall(iscall((result.ir, tuple)), result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[Argument(2)], r)
        @test is_load_forwardable(result.state[SSAValue(i)])
    end
    let result = code_escapes((String, String)) do a, b
            obj = SafeRefs(a, b)
            fld1 = obj[1]
            fld2 = obj[2]
            return (fld1, fld2)
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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((String,)) do a
            o = SafeRef("foo")
            o[] = a
            f = o[]
            return f
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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((String,)) do a
            obj = SafeRef("foo")
            return (obj[] = a)
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[Argument(2)], r)
        @test is_load_forwardable(result.state[SSAValue(i)])
    end

    # nested allocations
    let result = code_escapes((String,)) do a
            o1 = SafeRef(a)
            o2 = SafeRef(o1)
            return o2[]
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[Argument(2)], r)
        for i in 1:length(result.ir.stmts)
            if isnew(result.ir.stmts.inst[i]) && isT(SafeRef{String})(result.ir.stmts.type[i])
                @test has_return_escape(result.state[SSAValue(i)], r)
            elseif isnew(result.ir.stmts.inst[i]) && isT(SafeRef{SafeRef{String}})(result.ir.stmts.type[i])
                @test is_load_forwardable(result.state[SSAValue(i)])
            end
        end
    end
    let result = code_escapes((String,)) do a
            o1 = (a,)
            o2 = (o1,)
            return o2[1]
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[Argument(2)], r)
        for i in 1:length(result.ir.stmts)
            if isnew(result.ir.stmts.inst[i]) && isT(Tuple{String})(result.ir.stmts.type[i])
                @test has_return_escape(result.state[SSAValue(i)], r)
            elseif isnew(result.ir.stmts.inst[i]) && isT(Tuple{Tuple{String}})(result.ir.stmts.type[i])
                @test is_load_forwardable(result.state[SSAValue(i)])
            end
        end
    end
    let result = code_escapes((String,)) do a
            o1  = SafeRef(a)
            o2  = SafeRef(o1)
            o1′ = o2[]
            a′  = o1′[]
            return a′
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[Argument(2)], r)
        for i in findall(isnew, result.ir.stmts.inst)
            @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.inst))
        for i in findall(isnew, result.ir.stmts.inst)
            @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.inst))
        findall(1:length(result.ir.stmts)) do i
            if isnew(result.ir.stmts[i][:inst])
                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((String,)) do x
            broadcast(identity, Ref(x))
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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.inst))
        @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.inst))
        @test is_load_forwardable(result.state[SSAValue(i)])
        for i in findall(isnew, result.ir.stmts.inst)
            @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.inst))
        @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.inst)
            @test is_load_forwardable(result.state[SSAValue(i)])
        end
        for i in findall(isnew, result.ir.stmts.inst)
            @test is_load_forwardable(result.state[SSAValue(i)])
        end
    end
    # when ϕ-node merges values with different types
    let result = code_escapes((Bool,String,String,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.inst))
        t = only(findall(iscall((result.ir, throw)), result.ir.stmts.inst))
        ϕ = only(findall(isT(Union{SafeRef{String},SafeRefs{String,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)
            return r[]
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        val = (result.ir.stmts.inst[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)
            return r2[]
        end
        i1, i2 = findall(isnew, result.ir.stmts.inst)
        r = only(findall(isreturn, result.ir.stmts.inst))
        val = (result.ir.stmts.inst[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)
            return r2[][]
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        val = (result.ir.stmts.inst[r]::ReturnNode).val::SSAValue
        @test isaliased(Argument(2), val, result.state)
        for i in findall(isnew, result.ir.stmts.inst)
            @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)
                rx = r[] # rx aliased to Rx
                rx[] = s
                nothing
            end
        end
        i = findfirst(isnew, result.ir.stmts.inst)
        @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}()
            r[] = s
            return r[]
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        val = (result.ir.stmts.inst[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}}()
            r2[] = r1
            return r2[]
        end
        i1, i2 = findall(isnew, result.ir.stmts.inst)
        r = only(findall(isreturn, result.ir.stmts.inst))
        val = (result.ir.stmts.inst[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}}()
            r2[] = r1
            r1[] = s
            return r2[][]
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        val = (result.ir.stmts.inst[r]::ReturnNode).val::SSAValue
        @test isaliased(Argument(2), val, result.state)
        for i in findall(isnew, result.ir.stmts.inst)
            @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.inst))
        val = (result.ir.stmts.inst[r]::ReturnNode).val::SSAValue
        @test isaliased(Argument(2), val, result.state)
        for i in findall(isnew, result.ir.stmts.inst)
            @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)
                r[] = Rx
                rx = r[] # rx aliased to Rx
                rx[] = s
                nothing
            end
        end
        i = findfirst(isnew, result.ir.stmts.inst)
        @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::String
            return r
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        val = (result.ir.stmts.inst[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 g
            (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.inst))
        val = (result.ir.stmts.inst[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,String)) do cond, x
            if cond
                ϕ2 = ϕ1 = SafeRef("foo")
            else
                ϕ2 = ϕ1 = SafeRef("bar")
            end
            ϕ2[] = x
            return ϕ1[]
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        val = (result.ir.stmts.inst[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.inst)
            @test is_load_forwardable(result.state[SSAValue(i)])
        end
        for i in findall(isnew, result.ir.stmts.inst)
            @test is_load_forwardable(result.state[SSAValue(i)])
        end
    end
    let result = code_escapes((Bool,Bool,String)) do cond1, cond2, x
            if cond1
                ϕ2 = ϕ1 = SafeRef("foo")
            else
                ϕ2 = ϕ1 = SafeRef("bar")
            end
            cond2 && (ϕ2[] = x)
            return ϕ1[]
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        val = (result.ir.stmts.inst[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.inst)
            @test is_load_forwardable(result.state[SSAValue(i)])
        end
        for i in findall(isnew, result.ir.stmts.inst)
            @test is_load_forwardable(result.state[SSAValue(i)])
        end
    end
    # alias via π-node
    let result = code_escapes((Any,)) do x
            if isa(x, String)
                return x
            end
            throw("error!")
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        rval = (result.ir.stmts.inst[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 g
            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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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.inst))
        for i in findall(iscall((result.ir, getfield)), result.ir.stmts.inst)
            @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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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.inst))
        @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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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((String, Symbol)) do a, fld
            obj = SafeRef(a)
            return getfield(obj, fld)
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[Argument(2)], r) # a
        @test !is_load_forwardable(result.state[SSAValue(i)]) # obj
    end
    let result = code_escapes((String, 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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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((String, 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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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((String, String, Symbol)) do a, b, fld
            obj = SafeRefs("a", "b")
            setfield!(obj, fld, a)
            return obj[2] # should escape `a`
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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((String, Symbol)) do a, fld
            obj = SafeRefs("a", "b")
            setfield!(obj, fld, a)
            return obj[1] # this should escape `a`
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[Argument(2)], r) # a
        @test !is_load_forwardable(result.state[SSAValue(i)]) # obj
    end
    let result = code_escapes((String, String, Int)) do a, b, idx
            obj = SafeRefs("a", "b")
            obj[idx] = a
            return obj[2] # should escape `a`
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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((String,)) do a
                obj = SafeRef(a)
                fld = getx(obj)
                return fld
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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 field analysis
    let result = code_escapes((SafeRef{String},)) do s
            s[] = "bar"
            global g = 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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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)
                setxy!(x1, x2[])
                return x1
            end
        end
        i1, i2 = findall(isnew, result.ir.stmts.inst)
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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((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)
            r[] = b
            return r[]
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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)
            r[] = a
            r[] = b
            return r[]
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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)
            if cond
                r[] = a
                return r[]
            else
                r[] = b
                return nothing
            end
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        @test is_load_forwardable(result.state[SSAValue(i)])
        r = only(findall(result.ir.stmts.inst) 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,String,String,)) do cnd, baz, qux
            if cnd
                o = SafeRef("foo")
            else
                o = SafeRefs("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.inst))
        @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.inst)
            @test is_load_forwardable(result.state[SSAValue(new)])
        end
    end
    let result = code_escapes((Bool,String,String,)) do cnd, baz, qux
            if cnd
                o = SafeRefs("foo", "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.inst))
        @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.inst)
            @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)
        a = add(add(a, b), b)
    end
    a.x, a.y
end
function compute(a, b)
    for i in 0:(100000000-1)
        a = add(add(a, b), b) # unreplaceable, since it can be the call argument
    end
    a.x, a.y
end
function compute!(a, b)
    for i in 0:(100000000-1)
        a′ = add(add(a, b), b)
        a.x = a′.x
        a.y = a′.y
    end
end
let result = @code_escapes compute(MPoint, 1+.5im, 2+.5im, 2+.25im, 4+.75im)
    for i in findall(isnew, result.ir.stmts.inst)
        @test is_load_forwardable(result.state[SSAValue(i)])
    end
    for i in findall(isϕ, result.ir.stmts.inst)
        @test is_load_forwardable(result.state[SSAValue(i)])
    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 i
                 isϕ(result.ir.stmts[i][:inst]) && isT(MPoint{ComplexF64})(result.ir.stmts[i][:type])
             end
        @test !is_load_forwardable(result.state[SSAValue(i)])
    end
end
let result = @code_escapes compute!(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.75im))
    # FIXME with a proper interprocedural alias analysis
    # for i in findall(isnew, result.ir.stmts.inst)
    #     @test is_load_forwardable(result.state[SSAValue(i)])
    # end
    for i in findall(isϕ, result.ir.stmts.inst)
        @test is_load_forwardable(result.state[SSAValue(i)])
    end
end

@testset "array primitives" begin
    inbounds = Base.JLOptions().check_bounds == 0

    # arrayref
    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.inst))
        @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
    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.inst))
        @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
    inbounds && let result = code_escapes((Vector{String},Int)) do xs, i
            s = @inbounds xs[i]
            return s
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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
    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.inst))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
    end
    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.inst))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
    end
    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.inst))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
    end
    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.inst))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
    end

    # arrayset
    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.inst))
        @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
    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.inst))
        @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
    inbounds && let result = code_escapes((Vector{String},String,Int,)) do xs, x, i
            @inbounds xs[i] = x
            return xs
        end
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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
    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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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
    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.inst))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
        @test has_thrown_escape(result.state[Argument(3)], t) # x
    end
    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.inst))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs::String
        @test has_thrown_escape(result.state[Argument(3)], t) # x::String
    end
    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.inst))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
        @test has_thrown_escape(result.state[Argument(3)], t) # x
    end
    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.inst))
        @test has_thrown_escape(result.state[Argument(2)], t) # xs
        @test has_thrown_escape(result.state[Argument(3)], t) # x
    end

    # arrayref and arrayset
    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.inst))
        ai = only(findall(result.ir.stmts.inst) do @nospecialize x
            isarrayalloc(x) && x.args[2] === Vector{Vector{Any}}
        end)
        bi = only(findall(result.ir.stmts.inst) 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
    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.inst))
        ai = only(findall(result.ir.stmts.inst) do @nospecialize x
            isarrayalloc(x) && x.args[2] === Vector{Vector{Any}}
        end)
        bi = only(findall(result.ir.stmts.inst) 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
    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.inst))
        t = only(findall(iscall((result.ir, Base.arrayref)), result.ir.stmts.inst))
        @test has_thrown_escape(result.state[Argument(3)], t) # s
        @test has_thrown_escape(result.state[SSAValue(i)], t) # x
    end

    # arraysize
    let result = code_escapes((Vector{Any},)) do xs
            Core.arraysize(xs, 1)
        end
        t = only(findall(iscall((result.ir, Core.arraysize)), result.ir.stmts.inst))
        @test !has_thrown_escape(result.state[Argument(2)], t)
    end
    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.inst))
        @test !has_thrown_escape(result.state[Argument(2)], t)
    end
    let result = code_escapes((Any,)) do xs
            Core.arraysize(xs, 1)
        end
        t = only(findall(iscall((result.ir, Core.arraysize)), result.ir.stmts.inst))
        @test has_thrown_escape(result.state[Argument(2)], t)
    end

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

    # array resizing
    # without BoundsErrors
    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.inst))
        @test !has_thrown_escape(result.state[Argument(2)], t) # xs
        @test !has_thrown_escape(result.state[Argument(3)], t) # x
    end
    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.inst))
        @test !has_thrown_escape(result.state[Argument(2)], t) # xs
        @test !has_thrown_escape(result.state[Argument(3)], t) # x
    end
    # with possible BoundsErrors
    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.inst))
        t = only(findall(isarrayresize, result.ir.stmts.inst))
        @test has_thrown_escape(result.state[SSAValue(i)], t) # xs
        @test has_thrown_escape(result.state[Argument(2)], t) # x
    end
    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.inst))
        t = only(findall(isarrayresize, result.ir.stmts.inst))
        @test has_thrown_escape(result.state[SSAValue(i)], t) # xs
        @test has_thrown_escape(result.state[Argument(2)], t) # x
    end
    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.inst))
        t = only(findall(isarrayresize, result.ir.stmts.inst))
        @test has_thrown_escape(result.state[SSAValue(i)], t) # xs
        @test has_thrown_escape(result.state[Argument(2)], t) # x
    end
    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.inst))
        t = only(findall(isarrayresize, result.ir.stmts.inst))
        @test has_thrown_escape(result.state[SSAValue(i)], t) # xs
        @test has_thrown_escape(result.state[Argument(2)], t) # x
    end
    inbounds && let result = code_escapes((String,)) do x
            xs = @inbounds 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.inst))
        t = only(findall(isarrayresize, result.ir.stmts.inst))
        @test has_thrown_escape(result.state[SSAValue(i)], t) # xs
        @test has_thrown_escape(result.state[Argument(2)], t) # x
    end

    # array copy
    let result = code_escapes((Vector{Any},)) do xs
            return copy(xs)
        end
        i = only(findall(isarraycopy, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[SSAValue(i)], r)
        @test_broken !has_return_escape(result.state[Argument(2)], r)
    end
    let result = code_escapes((String,)) do s
            xs = String[s]
            xs′ = copy(xs)
            return xs′[1]
        end
        i1 = only(findall(isarrayalloc, result.ir.stmts.inst))
        i2 = only(findall(isarraycopy, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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
    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.inst))
        ref = only(findall(iscall((result.ir, Base.arrayref)), result.ir.stmts.inst))
        ret = only(findall(isreturn, result.ir.stmts.inst))
        @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
    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.inst))
        i2 = only(findall(isarraycopy, result.ir.stmts.inst))
        t = only(findall(iscall((result.ir, throw)), result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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.inst))
        @test !has_return_escape(result.state[Argument(2)], r)
        @test !has_thrown_escape(result.state[Argument(2)])
    end

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

    # safe case
    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.inst))
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @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
    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.inst))
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @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
    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.inst))
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @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
    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.inst))
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @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
    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.inst))
        is = findall(isarrayalloc, result.ir.stmts.inst)
        @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
    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.inst))
        for i in findall(isarrayalloc, result.ir.stmts.inst)
            @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
    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.inst))
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @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
    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.inst))
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @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
    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.inst))
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @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
    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.inst))
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @test !has_return_escape(result.state[SSAValue(i)], r)
        @test !is_load_forwardable(result.state[SSAValue(i)])
    end
    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.inst))
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @test_broken !has_return_escape(result.state[SSAValue(i)], r)
        @test !is_load_forwardable(result.state[SSAValue(i)])
    end

    # circular reference
    let result = code_escapes() do
            xs = Vector{Any}(undef, 1)
            xs[1] = xs
            return xs[1]
        end
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[SSAValue(i)], r)
    end
    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.inst))
        for i in findall(iscall((result.ir, Core.arrayref)), result.ir.stmts.inst)
            @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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @test has_return_escape(result.state[SSAValue(i)], r)
    end
end

# demonstrate array primitive support with a realistic end to end example
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.inst))
    r = only(findall(isreturn, result.ir.stmts.inst))
    @test has_return_escape(result.state[SSAValue(i)], r)
    Base.JLOptions().check_bounds ≠ 0 && @test has_thrown_escape(result.state[SSAValue(i)])
    @test has_return_escape(result.state[Argument(3)], r) # s
    Base.JLOptions().check_bounds ≠ 0 && @test has_thrown_escape(result.state[Argument(3)])    # s
end
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.inst))
    r = only(findall(isreturn, result.ir.stmts.inst))
    @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
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.inst))
    r = only(findall(isreturn, result.ir.stmts.inst))
    @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

@static if isdefined(Core, :ImmutableArray)

import Core: ImmutableArray, arrayfreeze, mutating_arrayfreeze, arraythaw

@testset "ImmutableArray" begin
    # arrayfreeze
    let result = code_escapes((Vector{Any},)) do xs
            arrayfreeze(xs)
        end
        @test !has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes((Vector,)) do xs
            arrayfreeze(xs)
        end
        @test !has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes((Any,)) do xs
            arrayfreeze(xs)
        end
        @test has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes((ImmutableArray{Any,1},)) do xs
            arrayfreeze(xs)
        end
        @test has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes() do
            xs = Any[]
            arrayfreeze(xs)
        end
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @test has_no_escape(result.state[SSAValue(1)])
    end

    # mutating_arrayfreeze
    let result = code_escapes((Vector{Any},)) do xs
            mutating_arrayfreeze(xs)
        end
        @test !has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes((Vector,)) do xs
            mutating_arrayfreeze(xs)
        end
        @test !has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes((Any,)) do xs
            mutating_arrayfreeze(xs)
        end
        @test has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes((ImmutableArray{Any,1},)) do xs
            mutating_arrayfreeze(xs)
        end
        @test has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes() do
            xs = Any[]
            mutating_arrayfreeze(xs)
        end
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @test has_no_escape(result.state[SSAValue(1)])
    end

    # arraythaw
    let result = code_escapes((ImmutableArray{Any,1},)) do xs
            arraythaw(xs)
        end
        @test !has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes((ImmutableArray,)) do xs
            arraythaw(xs)
        end
        @test !has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes((Any,)) do xs
            arraythaw(xs)
        end
        @test has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes((Vector{Any},)) do xs
            arraythaw(xs)
        end
        @test has_thrown_escape(result.state[Argument(2)])
    end
    let result = code_escapes() do
            xs = ImmutableArray(Any[])
            arraythaw(xs)
        end
        i = only(findall(isarrayalloc, result.ir.stmts.inst))
        @test has_no_escape(result.state[SSAValue(1)])
    end
end

# demonstrate some arrayfreeze optimizations
# !has_return_escape(ary) means ary is eligible for arrayfreeze to mutating_arrayfreeze optimization
let result = code_escapes((Int,)) do n
        xs = collect(1:n)
        ImmutableArray(xs)
    end
    i = only(findall(isarrayalloc, result.ir.stmts.inst))
    @test !has_return_escape(result.state[SSAValue(i)])
end
let result = code_escapes((Vector{Float64},)) do xs
        ys = sin.(xs)
        ImmutableArray(ys)
    end
    i = only(findall(isarrayalloc, result.ir.stmts.inst))
    @test !has_return_escape(result.state[SSAValue(i)])
end
let result = code_escapes((Vector{Pair{Int,String}},)) do xs
        n = maximum(first, xs)
        ys = Vector{String}(undef, n)
        for (i, s) in xs
            ys[i] = s
        end
        ImmutableArray(xs)
    end
    i = only(findall(isarrayalloc, result.ir.stmts.inst))
    @test !has_return_escape(result.state[SSAValue(i)])
end

end # @static if isdefined(Core, :ImmutableArray)

# 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 bb = a
        end
        @test !(has_all_escape(result.state[Argument(2)]))
    end

    let result = code_escapes((Int,)) do a
            o = SafeRef(a)
            f = o[]
            return f
        end
        i = only(findall(isnew, result.ir.stmts.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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.inst))
        r = only(findall(isreturn, result.ir.stmts.inst))
        @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

# IPO cache
# =========
# run EA before inlining

import EscapeAnalysis: ignore_argescape
const Gx = Ref{Any}()
noescape(a) = nothing
noescape(a, b) = nothing
function global_escape!(x)
    Gx[] = x
    return nothing
end
union_escape!(x) = global_escape!(x)
union_escape!(x::SafeRef) = nothing
union_escape!(x::SafeRefs) = nothing
Base.@constprop :aggressive function conditional_escape!(cnd, x)
    cnd && global_escape!(x)
    return cnd # XXX `cnd` needs to be returned (see https://github.com/JuliaLang/julia/pull/44001)
end

# MethodMatchInfo -- global cache
let result = code_escapes((String,); optimize=false) do x
        return noescape(x)
    end
    @test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end
let result = code_escapes((String,); optimize=false) do x
        identity(x)
        return nothing
    end
    @test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end
let result = code_escapes((String,); optimize=false) do x
        return identity(x)
    end
    r = only(findall(isreturn, result.ir.stmts.inst))
    @test has_return_escape(result.state[Argument(2)], r)
end
let result = code_escapes((String,); optimize=false) do x
        return Ref(x)
    end
    r = only(findall(isreturn, result.ir.stmts.inst))
    @test has_return_escape(result.state[Argument(2)], r)
end
let result = code_escapes((String,); optimize=false) do x
        r = Ref{String}()
        r[] = x
        return r
    end
    r = only(findall(isreturn, result.ir.stmts.inst))
    @test has_return_escape(result.state[Argument(2)], r)
end
let result = code_escapes((String,); optimize=false) do x
        global_escape!(x)
    end
    @test has_all_escape(result.state[Argument(2)])
end
# UnionSplitInfo
let result = code_escapes((Bool,String); optimize=false) do c, s
        x = c ? s : SafeRef(s)
        union_escape!(x)
    end
    @test has_all_escape(result.state[Argument(3)]) # s
end
let result = code_escapes((Bool,String); optimize=false) do c, s
        x = c ? SafeRef(s) : SafeRefs(s, s)
        union_escape!(x)
    end
    @test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end
# ConstCallInfo -- local cache
let result = code_escapes((String,); optimize=false) do x
        return conditional_escape!(false, x)
    end
    @test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end
# InvokeCallInfo
let result = code_escapes((String,); optimize=false) do x
        return Base.@invoke noescape(x::Any)
    end
    @test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end
let result = code_escapes((String,); optimize=false) do x
        return Base.@invoke conditional_escape!(false::Any, x::Any)
    end
    @test has_no_escape(ignore_argescape(result.state[Argument(2)]))
end

# ThrownEscape via potential MethodError
identity_if_string(x::String) = nothing
let result = code_escapes((Any,); optimize=false) do x
        identity_if_string(x)
    end
    i = only(findall(iscall((result.ir, identity_if_string)), result.ir.stmts.inst))
    r = only(findall(isreturn, result.ir.stmts.inst))
    @test has_thrown_escape(result.state[Argument(2)], i)
    @test !has_return_escape(result.state[Argument(2)], r)
end
let result = code_escapes((String,); optimize=false) do x
        identity_if_string(x)
    end
    i = only(findall(iscall((result.ir, identity_if_string)), result.ir.stmts.inst))
    r = only(findall(isreturn, result.ir.stmts.inst))
    @test !has_thrown_escape(result.state[Argument(2)], i)
    @test !has_return_escape(result.state[Argument(2)], r)
end
let result = code_escapes((String,); optimize=false) do x
        try
            identity_if_string(x)
        catch err
            global gx = err
        end
        return nothing
    end
    @test !has_all_escape(result.state[Argument(2)])
end
let result = code_escapes((Any,); optimize=false) do x
        try
            identity_if_string(x)
        catch err
            global gx = err
        end
        return nothing
    end
    @test has_all_escape(result.state[Argument(2)])
end

# code quality test
# =================

@testset "code quality" begin
    using JET

    # assert that our main routine are free from (unnecessary) runtime dispatches

    function function_filter(@nospecialize(ft))
        ft === typeof(Core.Compiler.widenconst) && return false # `widenconst` is very untyped, ignore
        ft === typeof(EscapeAnalysis.escape_builtin!) && return false # `escape_builtin!` is very untyped, ignore
        return true
    end
    target_modules = (EscapeAnalysis,)
    test_opt(only(methods(EscapeAnalysis.analyze_escapes)).sig;
        function_filter,
        target_modules,
        # skip_nonconcrete_calls=false,
        )
    for m in methods(EscapeAnalysis.escape_builtin!)
        Base._methods_by_ftype(m.sig, 1, Base.get_world_counter()) === false && continue
        test_opt(m.sig;
            function_filter,
            target_modules,
            # skip_nonconcrete_calls=false,
            )
    end
end


text:
import JSON
using IsURL: isurl
import YAML
import TOML

export load, save, loads, of_format

struct DataFormat{T} end

format(::Val{:json}) = DataFormat{:JSON}()
format(::Union{Val{:yaml},Val{:yml}}) = DataFormat{:YAML}()
format(::Val{:toml}) = DataFormat{:TOML}()

"""
    save(file, data)

Save `data` to `file`.

By now, `YAML`, `JSON`, and `TOML` formats are supported. The format is recognized by `file` extension.

If `data` is a `Dict`, its keys should be `String`s so that `load` can return the same `data`.

!!! warning
    Allowed `data` types can be referenced in [`JSON.jl` documentation](https://github.com/JuliaIO/JSON.jl/blob/master/README.md)
    and [`YAML.jl` documentation](https://github.com/JuliaData/YAML.jl/blob/master/README.md).
    For `TOML` format, only `AbstractDict` type is allowed.
"""
function save(file, data)
    path, ext = expanduser(file), extension(file)
    _save(path, format(Val(Symbol(ext))), data)
    return
end
function _save(f, ::DataFormat{:JSON}, data)
    open(f, "w") do io
        JSON.print(io, data)
    end
end
function _save(f, ::DataFormat{:TOML}, data::AbstractDict)
    open(f, "w") do io
        TOML.print(io, data)
    end
end
function _save(f, ::DataFormat{:YAML}, data)
    open(f, "w") do io
        YAML.write(io, data, "")
    end
end

"""
    load(file)

Load data from `file` to a `Dict`.

By now, `YAML`, `JSON`, and `TOML` formats are supported. The format is recognized by `file` extension.
"""
function load(url_or_file)
    path = filepath(url_or_file)
    ext = extension(path)
    return _load(path, format(Val(Symbol(ext))))
end
_load(path, ::DataFormat{:JSON}) = JSON.parsefile(path)
function _load(path, ::DataFormat{:TOML})
    open(path, "r") do io
        return TOML.parse(io)
    end
end
function _load(path, ::DataFormat{:YAML})
    open(path, "r") do io
        dict = YAML.load(io)
        return Dict(string(key) => value for (key, value) in dict)  # To keep up with JSON & TOML results
    end
end

function filepath(url_or_file)
    if isurl(url_or_file)
        return download(url_or_file, joinpath(mktempdir(), basename(url_or_file)))
    else
        if isfile(url_or_file)
            return expanduser(url_or_file)
        else
            error("file \"$url_or_file\" does not exists!")
        end
    end
end

"""
    loads(str, format)

Load data from `str` to a `Dict`. Allowed formats are `"yaml"`, `"yml"`, `"json"` and `"toml"`.
"""
loads(str, ::DataFormat{:JSON}) = JSON.parse(str)
loads(str, ::DataFormat{:TOML}) = TOML.parse(str)
loads(str, ::DataFormat{:YAML}) = YAML.load(str)

"""
    of_format(to, from)

Convert `from` to the format of `to`. Similar to `oftype`.
"""
function of_format(to, from)
    data = load(from)
    return save(to, data)
end

"""
    parentdir(file)

Get the directory of a `file`.

The problem of `dirname` is that it returns an empty string if users do not write `"./"` in the `file` path. This will cause an error in `tempname`.
"""
parentdir(file) = dirname(abspath(expanduser(file)))

"""
    extension(file)

Get the extension from `file`. Return an empty string if no extension is found.
"""
function extension(file)
    ext = splitext(file)[2]  # `splitext` works from `FilePathsBase.AbstractPath` since version 0.7.0.
    return isempty(ext) ? "" : lowercase(ext[2:end])
end

ext: jl


text:
# SECTION 4.2.2 MBTA

using NetworkDesignModels, Gurobi, JuMP
using DataFrames, JLD2, CSV
using ProgressMeter
NDM = NetworkDesignModels;

# parameters
nzs = 4

# load data
env = Gurobi.Env()
options = NDM.MasterOptions(
    nlegs = 2, distparam = 1.5,
    nfreqs = 2, costwts = [1.0, 0.3], freqwts = [1.0, 0.45], xfrwts = [1.0, 0.0],
    constrainedg = true, ninitial = 9)
JLD2.@load "data/transit-network-boston.jld2" stns np
@show sum(np.odmatrix .> 0)

for budget in [30, 40, 50]

# create master problem
options.modeltype = :lp
rmp = NDM.MasterProblem(
    np,
    linelist = copy(np.lines),
    options = options,
    solver = GurobiSolver(env, OutputFlag = 0)
);
# budget = 40
NDM.optimize(rmp, budget)
@show length(NDM.commutes(rmp))

directions = [round.([cos(f*pi),sin(f*pi)], digits=3) for f in (1/nzs):(1/nzs):1]
curtime = time()
for it in 1:20

    @show it
    NDM.optimize(rmp, budget)
    println(getobjectivevalue(rmp.model))
    pathstoadd = Vector{Vector{Int}}()

    for f in 1:options.nfreqs

        objs = Vector{Float64}()
        paths = Vector{Vector{Int}}()
        lastpath = nothing
        # solve
        @showprogress for d in 1:length(directions)
            sp_d = NDM.SubProblem(rmp,
                maxdist = 1.0,
                solver = GurobiSolver(env, OutputFlag = 0, TimeLimit = 1*60),
                maxlength = 19,
                direction = directions[d], delta = 1.0, # 1 - (delta in paper) -- 1.0 in the code corresponds to 0.0 in the paper
                traveltimes = true);
            if lastpath != nothing
                NDM.trywarmstart(sp_d, lastpath)
            end
            path = NDM.generatecolumn(sp_d, rmp, f=f)
            if length(path) > 0 && !NDM.linein(path, rmp.linelist)
                push!(paths, path)
                push!(objs, getobjectivevalue(sp_d.model))
                lastpath = path
            end
        end
        if length(objs) == 0
            continue
        else
            path_warm = paths[findmax(objs)[2]]
            push!(pathstoadd, path_warm)
        end

    end

    added = 0
    if length(pathstoadd) > 0
        for path_warm in pathstoadd
            if !NDM.linein(path_warm, rmp.linelist)
                NDM.addcolumn!(rmp, path_warm)
                added += 1
            end
        end
    end
    if added == 0
        break
    end

end
runtime = time() - curtime

# solve IP
options.modeltype = :ip
rmpip0 = NDM.MasterProblem(
    np,
    linelist = copy(np.lines),
    options = options,
    solver = GurobiSolver(env, OutputFlag = 0)
);
NDM.optimize(rmpip0, budget)
println(getobjectivevalue(rmpip0.model))
rmpip = NDM.MasterProblem(
    np,
    linelist = rmp.linelist,
    options = options,
    solver = GurobiSolver(env, OutputFlag = 0)
);
NDM.optimize(rmpip, budget)
println(getobjectivevalue(rmpip.model))

df = DataFrame(line = Int[], order = Int[], station = Int[], lat = Float64[], lon = Float64[], freq = Int[], old = Int[])
for l in 1:length(rmp.linelist)
    for f in 1:options.nfreqs
        if getvalue(rmpip.x[l,f]) > 0.1
            line = rmp.linelist[l]
            for i in 1:length(line)
                u = line[i]
                push!(df, [l, i, u, np.latlon[u, 1], np.latlon[u, 2], f, 1*(l <= length(np.lines))])
            end
        end
    end
end

# JLD2.@load "data/boston-all-results.jld2" allbudgets alloptions allcolumns allruntimes
# push!(allbudgets, budget)
# push!(alloptions, options)
# push!(allcolumns, rmp.linelist)
# push!(allruntimes, runtime)
# JLD2.@save "data/boston/boston-all-results.jld2" allbudgets alloptions allcolumns allruntimes

end


text:
using Documenter
using Traceur

makedocs(
    sitename = "Traceur",
    format = Documenter.HTML(),
    modules = [Traceur]
)

deploydocs(
    repo = "github.com/JunoLab/Traceur.jl.git"
)