# Local EA Test # ============= # EA works on post-inlining IR 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 GV = a nothing end @test has_all_escape(result.state[Argument(2)]) end let # global load result = code_escapes() do global GV return GV end i = only(findall(has_return_escape, map(i->result.state[SSAValue(i)], 1:length(result.ir.stmts)))) @test has_all_escape(result.state[SSAValue(i)]) end let # global store / load (https://github.com/aviatesk/EscapeAnalysis.jl/issues/56) result = code_escapes((Any,)) do s global GV GV = s return GV end r = only(findall(isreturn, result.ir.stmts.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) || 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 "builtins" begin let # throw r = code_escapes((Any,)) do a throw(a) end @test has_thrown_escape(r.state[Argument(2)]) end let # implicit throws r = code_escapes((Any,)) do a getfield(a, :may_not_field) end @test has_thrown_escape(r.state[Argument(2)]) r = code_escapes((Any,)) do a sizeof(a) end @test has_thrown_escape(r.state[Argument(2)]) end let # :=== result = code_escapes((Bool, SafeRef{String})) do cond, s m = cond ? s : nothing c = m === nothing return c end @test has_no_escape(ignore_argescape(result.state[Argument(2)])) end let # sizeof result = code_escapes((Vector{Any},)) do xs sizeof(xs) end @test has_no_escape(ignore_argescape(result.state[Argument(2)])) end let # ifelse result = code_escapes((Bool,)) do c r = ifelse(c, Ref("yes"), Ref("no")) return r end inds = findall(isnew, result.ir.stmts.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 escape_rethrow!() try rethrow() catch err GR[] = err end end @noinline function escape_current_exceptions!() excs = Base.current_exceptions() GR[] = excs end const GR = Ref{Any}() @__MODULE__ end let # simple: return escape result = @eval M $code_escapes() do r = Ref{String}() local ret try s = unsafeget(r) ret = sizeof(s) catch err ret = err end return ret end i = only(findall(isnew, result.ir.stmts.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 GV = 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 GR[] = 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 GR[] = 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 escape_rethrow!() 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) escape_rethrow!() 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 GR[] = 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 escape_current_exceptions!() 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 GV = 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 GV = 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 GV = 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 GV = (a,) nothing end i = only(findall(iscall((result.ir, tuple)), 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 o0 = SafeRef{Any}(a) global GV = 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 GV = (t0,) nothing end inds = findall(iscall((result.ir, tuple)), result.ir.stmts.inst) @assert length(inds) == 2 for i in inds; @test has_all_escape(result.state[SSAValue(i)]); end @test has_all_escape(result.state[Argument(2)]) end # global escape through `setfield!` let result = code_escapes((Any,)) do a r = SafeRef{Any}(:init) global GV = r r[] = a nothing end i = only(findall(isnew, result.ir.stmts.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 GV = 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 = SafeRef((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)]) result.state[SSAValue(i)].AliasInfo 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 GV (g::SafeRef{Any})[] = a nothing end @test has_all_escape(result.state[Argument(2)]) end # alias via ifelse let result = code_escapes((Bool,Any,Any)) do c, a, b r = ifelse(c, a, b) return r end r = only(findall(isreturn, result.ir.stmts.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 GV l = g if isa(l, SafeRef{String}) l[] = x end nothing end @test has_all_escape(result.state[Argument(2)]) # x end # circular reference let result = code_escapes() do x = Ref{Any}() x[] = x return x[] end i = only(findall(isnew, result.ir.stmts.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 alias analysis let result = code_escapes((SafeRef{String},)) do s s[] = "bar" global GV = s[] nothing end @test_broken !has_all_escape(result.state[Argument(2)]) end # aliasing between arguments let result = @eval EATModule() begin @noinline setxy!(x, y) = x[] = y $code_escapes((String,)) do y x = SafeRef("init") setxy!(x, y) return x end end i = only(findall(isnew, result.ir.stmts.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) c = add(a, b) # replaceable a = add(c, b) # replaceable end a.x, a.y end let result = @code_escapes compute(MPoint, 1+.5im, 2+.5im, 2+.25im, 4+.75im) for i in findall(1:length(result.ir.stmts)) do idx inst = EscapeAnalysis.getinst(result.ir, idx) stmt = inst[:inst] return (isnew(stmt) || isϕ(stmt)) && inst[:type] <: MPoint end @test is_load_forwardable(result.state[SSAValue(i)]) end end function compute(a, b) for i in 0:(100000000-1) c = add(a, b) # replaceable a = add(c, b) # unreplaceable (aliased to the call argument `a`) end a.x, a.y end let result = @code_escapes compute(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.75im)) idxs = findall(1:length(result.ir.stmts)) do idx inst = EscapeAnalysis.getinst(result.ir, idx) stmt = inst[:inst] return isnew(stmt) && inst[:type] <: MPoint end @assert length(idxs) == 2 @test count(i->is_load_forwardable(result.state[SSAValue(i)]), idxs) == 1 end function compute!(a, b) for i in 0:(100000000-1) c = add(a, b) # replaceable a′ = add(c, b) # replaceable a.x = a′.x a.y = a′.y end end let result = @code_escapes compute!(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.75im)) for i in findall(1:length(result.ir.stmts)) do idx inst = EscapeAnalysis.getinst(result.ir, idx) stmt = inst[:inst] return isnew(stmt) && inst[:type] <: MPoint end @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) @test !has_thrown_escape(result.state[SSAValue(i)]) @test has_return_escape(result.state[Argument(3)], r) # s @test !has_thrown_escape(result.state[Argument(3)]) # s end 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 GV = 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