Skip to content

Commit

Permalink
effects: taint :consistent-cy on allocation/access of uninitialized…
Browse files Browse the repository at this point in the history
… fields
  • Loading branch information
aviatesk committed Jul 24, 2022
1 parent 6e1ded4 commit 54881e2
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 7 deletions.
21 changes: 15 additions & 6 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1966,7 +1966,6 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e),
end
elseif ehead === :new
t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv))
is_nothrow = true
if isconcretedispatch(t)
ismutable = ismutabletype(t)
fcount = fieldcount(t)
Expand All @@ -1975,6 +1974,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e),
ats = Vector{Any}(undef, nargs)
local anyrefine = false
local allconst = true
local is_nothrow = true
for i = 1:nargs
at = widenconditional(abstract_eval_value(interp, e.args[i+1], vtypes, sv))
ft = fieldtype(t, i)
Expand All @@ -1992,12 +1992,22 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e),
end
ats[i] = at
end
if fcount > nargs && any(i::Int -> !is_undefref_fieldtype(fieldtype(t, i)), (nargs+1):fcount)
# allocation with undefined field leads to undefined behavior and should taint `:consistent`-cy
consistent = ALWAYS_FALSE
elseif ismutable
# mutable object isn't `:consistent`, but we can still give the return
# type information a chance to refine this `:consistent`-cy later
consistent = TRISTATE_UNKNOWN
else
consistent = ALWAYS_TRUE
end
# For now, don't allow:
# - Const/PartialStruct of mutables (but still allow PartialStruct of mutables
# with `const` fields if anything refined)
# - partially initialized Const/PartialStruct
if fcount == nargs
if !ismutable && allconst
if consistent === ALWAYS_TRUE && allconst
argvals = Vector{Any}(undef, nargs)
for j in 1:nargs
argvals[j] = (ats[j]::Const).val
Expand All @@ -2007,12 +2017,11 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e),
t = PartialStruct(t, ats)
end
end
nothrow = is_nothrow ? ALWAYS_TRUE : ALWAYS_FALSE
else
is_nothrow = false
consistent = nothrow = ALWAYS_FALSE
end
tristate_merge!(sv, Effects(EFFECTS_TOTAL;
consistent = !ismutabletype(t) ? ALWAYS_TRUE : TRISTATE_UNKNOWN,
nothrow = is_nothrow ? ALWAYS_TRUE : ALWAYS_FALSE))
tristate_merge!(sv, Effects(EFFECTS_TOTAL; consistent, nothrow))
elseif ehead === :splatnew
t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv))
is_nothrow = false # TODO: More precision
Expand Down
61 changes: 60 additions & 1 deletion base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ function _getfield_tfunc(@nospecialize(s00), @nospecialize(name), setfield::Bool
end
isa(s, DataType) || return Any
isabstracttype(s) && return Any
if s <: Tuple && !(Int <: widenconst(name))
if s <: Tuple && !hasintersect(widenconst(name), Int)
return Bottom
end
if s <: Module
Expand Down Expand Up @@ -971,6 +971,57 @@ function _getfield_tfunc(@nospecialize(s00), @nospecialize(name), setfield::Bool
return rewrap_unionall(R, s00)
end

function getfield_notundefined(@nospecialize(typ0), @nospecialize(name))
typ = unwrap_unionall(typ0)
if isa(typ, Union)
return getfield_notundefined(rewrap_unionall(typ.a, typ0), name) &&
getfield_notundefined(rewrap_unionall(typ.b, typ0), name)
end
isa(typ, DataType) || return false
if typ.name === Tuple.name || typ.name === _NAMEDTUPLE_NAME
# tuples and named tuples can't be instantiated with undefined fields,
# so we don't need to be conservative here
return true
end
if !isa(name, Const)
isvarargtype(name) && return false
if !hasintersect(widenconst(name), Union{Int,Symbol})
return true # no undefined behavior if thrown
end
# field isn't known precisely, but let's check if all the fields can't be
# initialized with undefined value so to avoid being too conservative
fcnt = fieldcount_noerror(typ)
fcnt === nothing && return false
all(i::Int->is_undefref_fieldtype(fieldtype(typ,i)), 1:fcnt) && return true
return false
end
name = name.val
if isa(name, Symbol)
fidx = fieldindex(typ, name, false)
fidx === nothing && return true # no undefined behavior if thrown
elseif isa(name, Int)
fidx = name
else
return true # no undefined behavior if thrown
end
fcnt = fieldcount_noerror(typ)
fcnt === nothing && return false
0 < fidx fcnt || return true # no undefined behavior if thrown
ftyp = fieldtype(typ, fidx)
is_undefref_fieldtype(ftyp) && return true
return fidx datatype_min_ninitialized(typ)
end
# checks if a field of this type will not be initialized with undefined value
# and the access to that uninitialized field will cause and `UndefRefError`, e.g.,
# - is_undefref_fieldtype(String) === true
# - is_undefref_fieldtype(Integer) === true
# - is_undefref_fieldtype(Any) === true
# - is_undefref_fieldtype(Int) === false
# - is_undefref_fieldtype(Union{Int32,Int64}) === false
function is_undefref_fieldtype(@nospecialize ftyp)
return !has_free_typevars(ftyp) && !allocatedinline(ftyp)
end

function setfield!_tfunc(o, f, v, order)
@nospecialize
if !isvarargtype(order)
Expand Down Expand Up @@ -1817,6 +1868,14 @@ function builtin_effects(f::Builtin, argtypes::Vector{Any}, @nospecialize(rt))
end
s = s::DataType
consistent = !ismutabletype(s) ? ALWAYS_TRUE : ALWAYS_FALSE
# access to `isbitstype`-field initialized with undefined value leads to undefined behavior
# so should taint `:consistent`-cy while access to uninitialized non-`isbitstype` field
# throws `UndefRefError` so doesn't need to taint it
# NOTE `getfield_notundefined` conservatively checks if this field is never initialized
# with undefined value so that we don't taint `:consistent`-cy too aggressively here
if f === Core.getfield && !getfield_notundefined(s, argtypes[2])
consistent = ALWAYS_FALSE
end
if f === Core.getfield && !isvarargtype(argtypes[end]) && getfield_boundscheck(argtypes) !== true
# If we cannot independently prove inboundsness, taint consistency.
# The inbounds-ness assertion requires dynamic reachability, while
Expand Down
71 changes: 71 additions & 0 deletions test/compiler/effects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,77 @@ function compare_inconsistent(x::T) where T
end
@test !compare_inconsistent(3)

# allocation/access of uninitialized fields should taint the :consistent-cy
struct Maybe{T}
x::T
Maybe{T}() where T = new{T}()
Maybe{T}(x) where T = new{T}(x)
Maybe(x::T) where T = new{T}(x)
end
Base.getindex(x::Maybe) = x.x

import Core.Compiler: Const, getfield_notundefined
for T = (Base.RefValue, Maybe) # both mutable and immutable
for name = (Const(1), Const(:x))
@test getfield_notundefined(T{String}, name)
@test getfield_notundefined(T{Integer}, name)
@test getfield_notundefined(T{Union{String,Integer}}, name)
@test getfield_notundefined(Union{T{String},T{Integer}}, name)
@test !getfield_notundefined(T{Int}, name)
@test !getfield_notundefined(T{<:Integer}, name)
@test !getfield_notundefined(T{Union{Int32,Int64}}, name)
@test !getfield_notundefined(T, name)
end
# throw doesn't account for undefined behavior
for name = (Const(0), Const(2), Const(1.0), Const(:y), Const("x"),
Float64, String, Nothing)
@test getfield_notundefined(T{String}, name)
@test getfield_notundefined(T{Int}, name)
@test getfield_notundefined(T{Integer}, name)
@test getfield_notundefined(T{<:Integer}, name)
@test getfield_notundefined(T{Union{Int32,Int64}}, name)
@test getfield_notundefined(T, name)
end
# should not be too conservative when field isn't known very well but object information is accurate
@test getfield_notundefined(T{String}, Int)
@test getfield_notundefined(T{String}, Symbol)
@test getfield_notundefined(T{Integer}, Int)
@test getfield_notundefined(T{Integer}, Symbol)
@test !getfield_notundefined(T{Int}, Int)
@test !getfield_notundefined(T{Int}, Symbol)
@test !getfield_notundefined(T{<:Integer}, Int)
@test !getfield_notundefined(T{<:Integer}, Symbol)
end
# should be conservative when object information isn't accurate
@test !getfield_notundefined(Any, Const(1))
@test !getfield_notundefined(Any, Const(:x))
# tuples and namedtuples should be okay if not given accurate information
for TupleType = Any[Tuple{Int,Int,Int}, Tuple{Int,Vararg{Int}}, Tuple{Any}, Tuple,
NamedTuple{(:a, :b), Tuple{Int,Int}}, NamedTuple{(:x,),Tuple{Any}}, NamedTuple],
FieldType = Any[Int, Symbol, Any]
@test getfield_notundefined(TupleType, FieldType)
end

# TODO add equivalent test cases for `Ref` once we handle mutability more nicely
@test Base.infer_effects() do
Maybe{Int}()
end |> !Core.Compiler.is_consistent
@test Base.infer_effects() do
Maybe{Int}()[]
end |> !Core.Compiler.is_consistent
@test !fully_eliminated() do
Maybe{Int}()[]
end
@test Base.infer_effects() do
Maybe{String}()
end |> Core.Compiler.is_consistent
@test Base.infer_effects() do
Maybe{String}()[]
end |> Core.Compiler.is_consistent
@test Base.return_types() do
Maybe{String}()[] # this expression should be concrete evaluated
end |> only === Union{}

# effects propagation for `Core.invoke` calls
# https://github.com/JuliaLang/julia/issues/44763
global x44763::Int = 0
Expand Down

0 comments on commit 54881e2

Please sign in to comment.