Skip to content

Commit

Permalink
compiler: move effects-related test cases into dedicated file (JuliaL…
Browse files Browse the repository at this point in the history
  • Loading branch information
aviatesk authored and Francesco Fucci committed Aug 11, 2022
1 parent a6908d7 commit daaca22
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 157 deletions.
4 changes: 2 additions & 2 deletions test/choosetests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ function choosetests(choices = [])
"strings/io", "strings/types"])
# do subarray before sparse but after linalg
filtertests!(tests, "subarray")
filtertests!(tests, "compiler", ["compiler/inference", "compiler/validation",
"compiler/ssair", "compiler/irpasses", "compiler/codegen",
filtertests!(tests, "compiler", ["compiler/inference", "compiler/effects",
"compiler/validation", "compiler/ssair", "compiler/irpasses", "compiler/codegen",
"compiler/inline", "compiler/contextual", "compiler/AbstractInterpreter",
"compiler/EscapeAnalysis/local", "compiler/EscapeAnalysis/interprocedural"])
filtertests!(tests, "compiler/EscapeAnalysis", [
Expand Down
158 changes: 158 additions & 0 deletions test/compiler/effects.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using Test
include("irutils.jl")

# control flow backedge should taint `terminates`
@test Base.infer_effects((Int,)) do n
for i = 1:n; end
end |> !Core.Compiler.is_terminates

# refine :consistent-cy effect inference using the return type information
@test Base.infer_effects((Any,)) do x
taint = Ref{Any}(x) # taints :consistent-cy, but will be adjusted
throw(taint)
end |> Core.Compiler.is_consistent
@test Base.infer_effects((Int,)) do x
if x < 0
taint = Ref(x) # taints :consistent-cy, but will be adjusted
throw(DomainError(x, taint))
end
return nothing
end |> Core.Compiler.is_consistent
@test Base.infer_effects((Int,)) do x
if x < 0
taint = Ref(x) # taints :consistent-cy, but will be adjusted
throw(DomainError(x, taint))
end
return x == 0 ? nothing : x # should `Union` of isbitstype objects nicely
end |> Core.Compiler.is_consistent
@test Base.infer_effects((Symbol,Any)) do s, x
if s === :throw
taint = Ref{Any}(":throw option given") # taints :consistent-cy, but will be adjusted
throw(taint)
end
return s # should handle `Symbol` nicely
end |> Core.Compiler.is_consistent
@test Base.infer_effects((Int,)) do x
return Ref(x)
end |> !Core.Compiler.is_consistent
@test Base.infer_effects((Int,)) do x
return x < 0 ? Ref(x) : nothing
end |> !Core.Compiler.is_consistent
@test Base.infer_effects((Int,)) do x
if x < 0
throw(DomainError(x, lazy"$x is negative"))
end
return nothing
end |> Core.Compiler.is_foldable

# effects propagation for `Core.invoke` calls
# https://github.com/JuliaLang/julia/issues/44763
global x44763::Int = 0
increase_x44763!(n) = (global x44763; x44763 += n)
invoke44763(x) = @invoke increase_x44763!(x)
@test Base.return_types() do
invoke44763(42)
end |> only === Int
@test x44763 == 0

# Test that purity doesn't try to accidentally run unreachable code due to
# boundscheck elimination
function f_boundscheck_elim(n)
# Inbounds here assumes that this is only ever called with n==0, but of
# course the compiler has no way of knowing that, so it must not attempt
# to run the @inbounds `getfield(sin, 1)`` that ntuple generates.
ntuple(x->(@inbounds getfield(sin, x)), n)
end
@test Tuple{} <: code_typed(f_boundscheck_elim, Tuple{Int})[1][2]

# Test that purity modeling doesn't accidentally introduce new world age issues
f_redefine_me(x) = x+1
f_call_redefine() = f_redefine_me(0)
f_mk_opaque() = Base.Experimental.@opaque ()->Base.inferencebarrier(f_call_redefine)()
const op_capture_world = f_mk_opaque()
f_redefine_me(x) = x+2
@test op_capture_world() == 1
@test f_mk_opaque()() == 2

# backedge insertion for Any-typed, effect-free frame
const CONST_DICT = let d = Dict()
for c in 'A':'z'
push!(d, c => Int(c))
end
d
end
Base.@assume_effects :foldable getcharid(c) = CONST_DICT[c]
@noinline callf(f, args...) = f(args...)
function entry_to_be_invalidated(c)
return callf(getcharid, c)
end
@test Base.infer_effects((Char,)) do x
entry_to_be_invalidated(x)
end |> Core.Compiler.is_foldable
@test fully_eliminated(; retval=97) do
entry_to_be_invalidated('a')
end
getcharid(c) = CONST_DICT[c] # now this is not eligible for concrete evaluation
@test Base.infer_effects((Char,)) do x
entry_to_be_invalidated(x)
end |> !Core.Compiler.is_foldable
@test !fully_eliminated() do
entry_to_be_invalidated('a')
end

@test !Core.Compiler.builtin_nothrow(Core.get_binding_type, Any[Rational{Int}, Core.Const(:foo)], Any)

# Nothrow for assignment to globals
global glob_assign_int::Int = 0
f_glob_assign_int() = global glob_assign_int += 1
let effects = Base.infer_effects(f_glob_assign_int, ())
@test !Core.Compiler.is_effect_free(effects)
@test Core.Compiler.is_nothrow(effects)
end
# Nothrow for setglobal!
global SETGLOBAL!_NOTHROW::Int = 0
let effects = Base.infer_effects() do
setglobal!(@__MODULE__, :SETGLOBAL!_NOTHROW, 42)
end
@test Core.Compiler.is_nothrow(effects)
end

# we should taint `nothrow` if the binding doesn't exist and isn't fixed yet,
# as the cached effects can be easily wrong otherwise
# since the inference curently doesn't track "world-age" of global variables
@eval global_assignment_undefinedyet() = $(GlobalRef(@__MODULE__, :UNDEFINEDYET)) = 42
setglobal!_nothrow_undefinedyet() = setglobal!(@__MODULE__, :UNDEFINEDYET, 42)
let effects = Base.infer_effects() do
global_assignment_undefinedyet()
end
@test !Core.Compiler.is_nothrow(effects)
end
let effects = Base.infer_effects() do
setglobal!_nothrow_undefinedyet()
end
@test !Core.Compiler.is_nothrow(effects)
end
global UNDEFINEDYET::String = "0"
let effects = Base.infer_effects() do
global_assignment_undefinedyet()
end
@test !Core.Compiler.is_nothrow(effects)
end
let effects = Base.infer_effects() do
setglobal!_nothrow_undefinedyet()
end
@test !Core.Compiler.is_nothrow(effects)
end
@test_throws ErrorException setglobal!_nothrow_undefinedyet()

# Nothrow for setfield!
mutable struct SetfieldNothrow
x::Int
end
f_setfield_nothrow() = SetfieldNothrow(0).x = 1
let effects = Base.infer_effects(f_setfield_nothrow, ())
# Technically effect free even though we use the heap, since the
# object doesn't escape, but the compiler doesn't know that.
#@test Core.Compiler.is_effect_free(effects)
@test Core.Compiler.is_nothrow(effects)
end
156 changes: 1 addition & 155 deletions test/compiler/inference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4056,27 +4056,6 @@ end
end |> only === Union{}
end

# Test that purity modeling doesn't accidentally introduce new world age issues
f_redefine_me(x) = x+1
f_call_redefine() = f_redefine_me(0)
f_mk_opaque() = @Base.Experimental.opaque ()->Base.inferencebarrier(f_call_redefine)()
const op_capture_world = f_mk_opaque()
f_redefine_me(x) = x+2
@test op_capture_world() == 1
@test f_mk_opaque()() == 2

# Test that purity doesn't try to accidentally run unreachable code due to
# boundscheck elimination
function f_boundscheck_elim(n)
# Inbounds here assumes that this is only ever called with n==0, but of
# course the compiler has no way of knowing that, so it must not attempt
# to run the @inbounds `getfield(sin, 1)`` that ntuple generates.
ntuple(x->(@inbounds getfield(sin, x)), n)
end
@test Tuple{} <: code_typed(f_boundscheck_elim, Tuple{Int})[1][2]

@test !Core.Compiler.builtin_nothrow(Core.get_binding_type, Any[Rational{Int}, Core.Const(:foo)], Any)

# Test that max_methods works as expected
@Base.Experimental.max_methods 1 function f_max_methods end
f_max_methods(x::Int) = 1
Expand All @@ -4102,145 +4081,12 @@ end
Core.Compiler.return_type(+, NTuple{2, Rational})
end == Rational

# vararg-tuple comparison within `PartialStruct`
# https://github.com/JuliaLang/julia/issues/44965
let t = Core.Compiler.tuple_tfunc(Any[Core.Const(42), Vararg{Any}])
@test Core.Compiler.issimplertype(t, t)
end

# https://github.com/JuliaLang/julia/issues/44763
global x44763::Int = 0
increase_x44763!(n) = (global x44763; x44763 += n)
invoke44763(x) = @invoke increase_x44763!(x)
@test Base.return_types() do
invoke44763(42)
end |> only === Int
@test x44763 == 0

# backedge insertion for Any-typed, effect-free frame
const CONST_DICT = let d = Dict()
for c in 'A':'z'
push!(d, c => Int(c))
end
d
end
Base.@assume_effects :foldable getcharid(c) = CONST_DICT[c]
@noinline callf(f, args...) = f(args...)
function entry_to_be_invalidated(c)
return callf(getcharid, c)
end
@test Base.infer_effects((Char,)) do x
entry_to_be_invalidated(x)
end |> Core.Compiler.is_foldable
@test fully_eliminated(; retval=97) do
entry_to_be_invalidated('a')
end
getcharid(c) = CONST_DICT[c] # now this is not eligible for concrete evaluation
@test Base.infer_effects((Char,)) do x
entry_to_be_invalidated(x)
end |> !Core.Compiler.is_foldable
@test !fully_eliminated() do
entry_to_be_invalidated('a')
end

# control flow backedge should taint `terminates`
@test Base.infer_effects((Int,)) do n
for i = 1:n; end
end |> !Core.Compiler.is_terminates

# Nothrow for assignment to globals
global glob_assign_int::Int = 0
f_glob_assign_int() = global glob_assign_int += 1
let effects = Base.infer_effects(f_glob_assign_int, ())
@test !Core.Compiler.is_effect_free(effects)
@test Core.Compiler.is_nothrow(effects)
end
# Nothrow for setglobal!
global SETGLOBAL!_NOTHROW::Int = 0
let effects = Base.infer_effects() do
setglobal!(@__MODULE__, :SETGLOBAL!_NOTHROW, 42)
end
@test Core.Compiler.is_nothrow(effects)
end

# we should taint `nothrow` if the binding doesn't exist and isn't fixed yet,
# as the cached effects can be easily wrong otherwise
# since the inference curently doesn't track "world-age" of global variables
@eval global_assignment_undefinedyet() = $(GlobalRef(@__MODULE__, :UNDEFINEDYET)) = 42
setglobal!_nothrow_undefinedyet() = setglobal!(@__MODULE__, :UNDEFINEDYET, 42)
let effects = Base.infer_effects() do
global_assignment_undefinedyet()
end
@test !Core.Compiler.is_nothrow(effects)
end
let effects = Base.infer_effects() do
setglobal!_nothrow_undefinedyet()
end
@test !Core.Compiler.is_nothrow(effects)
end
global UNDEFINEDYET::String = "0"
let effects = Base.infer_effects() do
global_assignment_undefinedyet()
end
@test !Core.Compiler.is_nothrow(effects)
end
let effects = Base.infer_effects() do
setglobal!_nothrow_undefinedyet()
end
@test !Core.Compiler.is_nothrow(effects)
end
@test_throws ErrorException setglobal!_nothrow_undefinedyet()

# Nothrow for setfield!
mutable struct SetfieldNothrow
x::Int
end
f_setfield_nothrow() = SetfieldNothrow(0).x = 1
let effects = Base.infer_effects(f_setfield_nothrow, ())
# Technically effect free even though we use the heap, since the
# object doesn't escape, but the compiler doesn't know that.
#@test Core.Compiler.is_effect_free(effects)
@test Core.Compiler.is_nothrow(effects)
end

# refine :consistent-cy effect inference using the return type information
@test Base.infer_effects((Any,)) do x
taint = Ref{Any}(x) # taints :consistent-cy, but will be adjusted
throw(taint)
end |> Core.Compiler.is_consistent
@test Base.infer_effects((Int,)) do x
if x < 0
taint = Ref(x) # taints :consistent-cy, but will be adjusted
throw(DomainError(x, taint))
end
return nothing
end |> Core.Compiler.is_consistent
@test Base.infer_effects((Int,)) do x
if x < 0
taint = Ref(x) # taints :consistent-cy, but will be adjusted
throw(DomainError(x, taint))
end
return x == 0 ? nothing : x # should `Union` of isbitstype objects nicely
end |> Core.Compiler.is_consistent
@test Base.infer_effects((Symbol,Any)) do s, x
if s === :throw
taint = Ref{Any}(":throw option given") # taints :consistent-cy, but will be adjusted
throw(taint)
end
return s # should handle `Symbol` nicely
end |> Core.Compiler.is_consistent
@test Base.infer_effects((Int,)) do x
return Ref(x)
end |> !Core.Compiler.is_consistent
@test Base.infer_effects((Int,)) do x
return x < 0 ? Ref(x) : nothing
end |> !Core.Compiler.is_consistent
@test Base.infer_effects((Int,)) do x
if x < 0
throw(DomainError(x, lazy"$x is negative"))
end
return nothing
end |> Core.Compiler.is_foldable

# check the inference convergence with an empty vartable:
# the inference state for the toplevel chunk below will have an empty vartable,
# and so we may fail to terminate (or optimize) it if we don't update vartables correctly
Expand Down

0 comments on commit daaca22

Please sign in to comment.