Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

effects: taint :noub for memory operations #52186

Merged
merged 1 commit into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 12 additions & 16 deletions base/array.jl
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ const DenseVecOrMat{T} = Union{DenseVector{T}, DenseMatrix{T}}
@_safeindex

This internal macro converts:
- `getindex(xs::Tuple, )` -> `__inbounds_getindex(args...)`
- `setindex!(xs::Vector, args...)` -> `__inbounds_setindex!(xs, args...)`
- `getindex(xs::Tuple, i::Int)` -> `__safe_getindex(xs, i)`
- `setindex!(xs::Vector{T}, x, i::Int)` -> `__safe_setindex!(xs, x, i)`
to tell the compiler that indexing operations within the applied expression are always
inbounds and do not need to taint `:consistent` and `:nothrow`.
"""
Expand All @@ -143,10 +143,10 @@ function _safeindex(__module__, ex)
for i = 2:length(lhs.args)
args[i-1] = _safeindex(__module__, lhs.args[i])
end
return Expr(:call, GlobalRef(__module__, :__inbounds_setindex!), xs, _safeindex(__module__, rhs), args...)
return Expr(:call, GlobalRef(__module__, :__safe_setindex!), xs, _safeindex(__module__, rhs), args...)
end
elseif ex.head === :ref # xs[i]
return Expr(:call, GlobalRef(__module__, :__inbounds_getindex), ex.args...)
return Expr(:call, GlobalRef(__module__, :__safe_getindex), ex.args...)
end
args = Vector{Any}(undef, length(ex.args))
for i = 1:length(ex.args)
Expand Down Expand Up @@ -236,13 +236,15 @@ sizeof(a::Array) = length(a) * elsize(typeof(a)) # n.b. this ignores bitsunion b

function isassigned(a::Array, i::Int...)
@inline
@_noub_if_noinbounds_meta
@boundscheck checkbounds(Bool, a, i...) || return false
ii = _sub2ind(size(a), i...)
return @inbounds isassigned(memoryref(a.ref, ii, false))
end

function isassigned(a::Vector, i::Int) # slight compiler simplification for the most common case
@inline
@_noub_if_noinbounds_meta
@boundscheck checkbounds(Bool, a, i) || return false
return @inbounds isassigned(memoryref(a.ref, i, false))
end
Expand Down Expand Up @@ -962,29 +964,23 @@ Dict{String, Int64} with 2 entries:
function setindex! end

function setindex!(A::Array{T}, x, i::Int) where {T}
@_noub_if_noinbounds_meta
@boundscheck (i - 1)%UInt < length(A)%UInt || throw_boundserror(A, (i,))
memoryrefset!(memoryref(A.ref, i, false), x isa T ? x : convert(T,x)::T, :not_atomic, false)
return A
end
function setindex!(A::Array{T}, x, i1::Int, i2::Int, I::Int...) where {T}
@inline
@_noub_if_noinbounds_meta
@boundscheck checkbounds(A, i1, i2, I...) # generally _to_linear_index requires bounds checking
memoryrefset!(memoryref(A.ref, _to_linear_index(A, i1, i2, I...), false), x isa T ? x : convert(T,x)::T, :not_atomic, false)
return A
end

function __inbounds_setindex!(A::Array{T}, x, i::Int) where {T}
@inline
val = x isa T ? x : convert(T,x)::T
memoryrefset!(memoryref(A.ref, i, false), val, :not_atomic, false)
return A
end
function __inbounds_setindex!(A::Array{T}, x, i1::Int, i2::Int, I::Int...) where {T}
@inline
val = x isa T ? x : convert(T,x)::T
memoryrefset!(memoryref(A.ref, _to_linear_index(A, i1, i2, I...), false), val, :not_atomic, false)
return A
end
__safe_setindex!(A::Vector{T}, x::T, i::Int) where {T} = (@inline; @_nothrow_noub_meta;
memoryrefset!(memoryref(A.ref, i, false), x, :not_atomic, false); return A)
__safe_setindex!(A::Vector{T}, x, i::Int) where {T} = (@inline;
__safe_setindex!(A, convert(T, x)::T, i))

# This is redundant with the abstract fallbacks but needed and helpful for bootstrap
function setindex!(A::Array, X::AbstractArray, I::AbstractVector{Int})
Expand Down
3 changes: 2 additions & 1 deletion base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,8 @@ macro _foldable_meta()
#=:terminates_locally=#false,
#=:notaskstate=#true,
#=:inaccessiblememonly=#true,
#=:noub=#true))
#=:noub=#true,
#=:noub_if_noinbounds=#false))
end

macro inline() Expr(:meta, :inline) end
Expand Down
2 changes: 1 addition & 1 deletion base/c.jl
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,6 @@ macro ccall(expr)
return ccall_macro_lower(:ccall, ccall_macro_parse(expr)...)
end

macro ccall_effects(effects::UInt8, expr)
macro ccall_effects(effects::UInt16, expr)
return ccall_macro_lower((:ccall, effects), ccall_macro_parse(expr)...)
end
4 changes: 2 additions & 2 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2604,7 +2604,7 @@ function abstract_eval_foreigncall(interp::AbstractInterpreter, e::Expr, vtypes:
abstract_eval_value(interp, x, vtypes, sv)
end
cconv = e.args[5]
if isa(cconv, QuoteNode) && (v = cconv.value; isa(v, Tuple{Symbol, UInt8}))
if isa(cconv, QuoteNode) && (v = cconv.value; isa(v, Tuple{Symbol, UInt16}))
override = decode_effects_override(v[2])
effects = Effects(effects;
consistent = override.consistent ? ALWAYS_TRUE : effects.consistent,
Expand All @@ -2613,7 +2613,7 @@ function abstract_eval_foreigncall(interp::AbstractInterpreter, e::Expr, vtypes:
terminates = override.terminates_globally ? true : effects.terminates,
notaskstate = override.notaskstate ? true : effects.notaskstate,
inaccessiblememonly = override.inaccessiblememonly ? ALWAYS_TRUE : effects.inaccessiblememonly,
noub = override.noub ? ALWAYS_TRUE : effects.noub)
noub = override.noub ? ALWAYS_TRUE : override.noub_if_noinbounds ? NOUB_IF_NOINBOUNDS : effects.noub)
end
return RTEffects(t, effects)
end
Expand Down
46 changes: 24 additions & 22 deletions base/compiler/effects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -197,15 +197,14 @@ is_nothrow(effects::Effects) = effects.nothrow
is_terminates(effects::Effects) = effects.terminates
is_notaskstate(effects::Effects) = effects.notaskstate
is_inaccessiblememonly(effects::Effects) = effects.inaccessiblememonly === ALWAYS_TRUE
is_noub(effects::Effects) = effects.noub === ALWAYS_TRUE
is_noub_if_noinbounds(effects::Effects) = effects.noub === NOUB_IF_NOINBOUNDS
is_nonoverlayed(effects::Effects) = effects.nonoverlayed

is_noub(effects::Effects, noinbounds::Bool=true) =
effects.noub === ALWAYS_TRUE || (noinbounds && effects.noub === NOUB_IF_NOINBOUNDS)

# implies `is_notaskstate` & `is_inaccessiblememonly`, but not explicitly checked here
is_foldable(effects::Effects) =
is_consistent(effects) &&
is_noub(effects) &&
(is_noub(effects) || is_noub_if_noinbounds(effects)) &&
is_effect_free(effects) &&
is_terminates(effects)

Expand Down Expand Up @@ -262,29 +261,32 @@ struct EffectsOverride
notaskstate::Bool
inaccessiblememonly::Bool
noub::Bool
noub_if_noinbounds::Bool
end

function encode_effects_override(eo::EffectsOverride)
e = 0x00
eo.consistent && (e |= (0x01 << 0))
eo.effect_free && (e |= (0x01 << 1))
eo.nothrow && (e |= (0x01 << 2))
eo.terminates_globally && (e |= (0x01 << 3))
eo.terminates_locally && (e |= (0x01 << 4))
eo.notaskstate && (e |= (0x01 << 5))
eo.inaccessiblememonly && (e |= (0x01 << 6))
eo.noub && (e |= (0x01 << 7))
e = 0x0000
eo.consistent && (e |= (0x0001 << 0))
eo.effect_free && (e |= (0x0001 << 1))
eo.nothrow && (e |= (0x0001 << 2))
eo.terminates_globally && (e |= (0x0001 << 3))
eo.terminates_locally && (e |= (0x0001 << 4))
eo.notaskstate && (e |= (0x0001 << 5))
eo.inaccessiblememonly && (e |= (0x0001 << 6))
eo.noub && (e |= (0x0001 << 7))
eo.noub_if_noinbounds && (e |= (0x0001 << 8))
return e
end

function decode_effects_override(e::UInt8)
function decode_effects_override(e::UInt16)
return EffectsOverride(
(e & (0x01 << 0)) != 0x00,
(e & (0x01 << 1)) != 0x00,
(e & (0x01 << 2)) != 0x00,
(e & (0x01 << 3)) != 0x00,
(e & (0x01 << 4)) != 0x00,
(e & (0x01 << 5)) != 0x00,
(e & (0x01 << 6)) != 0x00,
(e & (0x01 << 7)) != 0x00)
!iszero(e & (0x0001 << 0)),
!iszero(e & (0x0001 << 1)),
!iszero(e & (0x0001 << 2)),
!iszero(e & (0x0001 << 3)),
!iszero(e & (0x0001 << 4)),
!iszero(e & (0x0001 << 5)),
!iszero(e & (0x0001 << 6)),
!iszero(e & (0x0001 << 7)),
!iszero(e & (0x0001 << 8)))
end
35 changes: 24 additions & 11 deletions base/compiler/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -563,25 +563,35 @@ function is_ipo_dataflow_analysis_profitable(effects::Effects)
is_nothrow(effects) && is_noub(effects))
end

function is_getfield_with_boundscheck_arg(@nospecialize(stmt), sv::PostOptAnalysisState)
is_known_call(stmt, getfield, sv.ir) || return false
length(stmt.args) < 4 && return false
function iscall_with_boundscheck(@nospecialize(stmt), sv::PostOptAnalysisState)
isexpr(stmt, :call) || return false
ft = argextype(stmt.args[1], sv.ir)
f = singleton_type(ft)
f === nothing && return false
if f === getfield
nargs = 4
elseif f === memoryref || f === memoryrefget || f === memoryref_isassigned
nargs = 4
elseif f === memoryrefset!
nargs = 5
else
return false
end
length(stmt.args) < nargs && return false
boundscheck = stmt.args[end]
argextype(boundscheck, sv.ir) === Bool || return false
isa(boundscheck, SSAValue) || return false
return true
end

function is_conditional_noub(inst::Instruction, sv::PostOptAnalysisState)
# Special case: `:boundscheck` into `getfield`
stmt = inst[:stmt]
is_getfield_with_boundscheck_arg(stmt, sv) || return false
barg = stmt.args[end]
iscall_with_boundscheck(stmt, sv) || return false
barg = stmt.args[end]::SSAValue
bstmt = sv.ir[barg][:stmt]
isexpr(bstmt, :boundscheck) || return false
# If IR_FLAG_INBOUNDS is already set, no more conditional ub
(!isempty(bstmt.args) && bstmt.args[1] === false) && return false
sv.any_conditional_ub = true
return true
end

Expand All @@ -596,7 +606,10 @@ function scan_non_dataflow_flags!(inst::Instruction, sv::PostOptAnalysisState)
end
sv.all_nothrow &= !iszero(flag & IR_FLAG_NOTHROW)
if iszero(flag & IR_FLAG_NOUB)
if !is_conditional_noub(inst, sv)
# Special case: `:boundscheck` into `getfield` or memory operations is `:noub_if_noinbounds`
if is_conditional_noub(inst, sv)
sv.any_conditional_ub = true
else
sv.all_noub = false
end
end
Expand All @@ -606,9 +619,9 @@ function scan_inconsistency!(inst::Instruction, idx::Int, sv::PostOptAnalysisSta
flag = inst[:flag]
stmt_inconsistent = iszero(flag & IR_FLAG_CONSISTENT)
stmt = inst[:stmt]
# Special case: For getfield, we allow inconsistency of the :boundscheck argument
# Special case: For `getfield` and memory operations, we allow inconsistency of the :boundscheck argument
(; inconsistent, tpdum) = sv
if is_getfield_with_boundscheck_arg(stmt, sv)
if iscall_with_boundscheck(stmt, sv)
for i = 1:(length(stmt.args)-1)
val = stmt.args[i]
if isa(val, SSAValue)
Expand Down Expand Up @@ -703,7 +716,7 @@ function check_inconsistentcy!(sv::PostOptAnalysisState, scanner::BBScanner)
idx = popfirst!(stmt_ip)
inst = ir[SSAValue(idx)]
stmt = inst[:stmt]
if is_getfield_with_boundscheck_arg(stmt, sv)
if iscall_with_boundscheck(stmt, sv)
any_non_boundscheck_inconsistent = false
for i = 1:(length(stmt.args)-1)
val = stmt.args[i]
Expand Down
2 changes: 1 addition & 1 deletion base/compiler/ssair/inlining.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,7 @@ function flags_for_effects(effects::Effects)
if is_nothrow(effects)
flags |= IR_FLAG_NOTHROW
end
if is_noub(effects, false)
if is_noub(effects)
flags |= IR_FLAG_NOUB
end
return flags
Expand Down
6 changes: 3 additions & 3 deletions base/compiler/ssair/irinterp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ function concrete_eval_invoke(interp::AbstractInterpreter,
try
Core._call_in_world_total(world, args...)
catch
return Pair{Any,Tuple{Bool,Bool}}(Bottom, (false, is_noub(effects, false)))
return Pair{Any,Tuple{Bool,Bool}}(Bottom, (false, is_noub(effects)))
end
end
return Pair{Any,Tuple{Bool,Bool}}(Const(value), (true, true))
else
if is_constprop_edge_recursed(mi, irsv)
return Pair{Any,Tuple{Bool,Bool}}(nothing, (is_nothrow(effects), is_noub(effects, false)))
return Pair{Any,Tuple{Bool,Bool}}(nothing, (is_nothrow(effects), is_noub(effects)))
end
newirsv = IRInterpretationState(interp, code, mi, argtypes, world)
if newirsv !== nothing
newirsv.parent = irsv
return ir_abstract_constant_propagation(interp, newirsv)
end
return Pair{Any,Tuple{Bool,Bool}}(nothing, (is_nothrow(effects), is_noub(effects, false)))
return Pair{Any,Tuple{Bool,Bool}}(nothing, (is_nothrow(effects), is_noub(effects)))
end
end

Expand Down
41 changes: 38 additions & 3 deletions base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,7 @@ end

function getfield_boundscheck(argtypes::Vector{Any})
if length(argtypes) == 2
isvarargtype(argtypes[2]) && return :unsafe
return :on
elseif length(argtypes) == 3
boundscheck = argtypes[3]
Expand All @@ -933,10 +934,10 @@ function getfield_boundscheck(argtypes::Vector{Any})
end
elseif length(argtypes) == 4
boundscheck = argtypes[4]
isvarargtype(boundscheck) && return :unsafe
else
return :unsafe
end
isvarargtype(boundscheck) && return :unsafe
boundscheck = widenconditional(boundscheck)
if widenconst(boundscheck) === Bool
if isa(boundscheck, Const)
Expand Down Expand Up @@ -2144,7 +2145,6 @@ end
return boundscheck ⊑ Bool && memtype ⊑ GenericMemoryRef && order ⊑ Symbol
end


# Query whether the given builtin is guaranteed not to throw given the argtypes
@nospecs function _builtin_nothrow(𝕃::AbstractLattice, f, argtypes::Vector{Any}, rt)
⊑ = Core.Compiler.:⊑(𝕃)
Expand Down Expand Up @@ -2509,8 +2509,43 @@ function builtin_effects(𝕃::AbstractLattice, @nospecialize(f::Builtin), argty
else
inaccessiblememonly = ALWAYS_FALSE
end
return Effects(EFFECTS_TOTAL; consistent, effect_free, nothrow, inaccessiblememonly)
if f === memoryref || f === memoryrefget || f === memoryrefset! || f === memoryref_isassigned
noub = memoryop_noub(f, argtypes) ? ALWAYS_TRUE : ALWAYS_FALSE
else
noub = ALWAYS_TRUE
end
return Effects(EFFECTS_TOTAL; consistent, effect_free, nothrow, inaccessiblememonly, noub)
end
end

function memoryop_noub(@nospecialize(f), argtypes::Vector{Any})
nargs = length(argtypes)
nargs == 0 && return true # must throw and noub
lastargtype = argtypes[end]
isva = isvarargtype(lastargtype)
if f === memoryref
if nargs == 1 && !isva
return true
elseif nargs == 2 && !isva
return true
end
expected_nargs = 3
elseif f === memoryrefget || f === memoryref_isassigned
expected_nargs = 3
else
@assert f === memoryrefset! "unexpected memoryop is given"
expected_nargs = 4
end
if nargs == expected_nargs && !isva
boundscheck = widenconditional(lastargtype)
hasintersect(widenconst(boundscheck), Bool) || return true # must throw and noub
boundscheck isa Const && boundscheck.val === true && return true
elseif nargs > expected_nargs + 1
return true # must throw and noub
elseif !isva
return true # must throw and noub
end
return false
end

"""
Expand Down
2 changes: 2 additions & 0 deletions base/compiler/typeinfer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,8 @@ function adjust_effects(ipo_effects::Effects, def::Method)
end
if is_effect_overridden(override, :noub)
ipo_effects = Effects(ipo_effects; noub=ALWAYS_TRUE)
elseif is_effect_overridden(override, :noub_if_noinbounds) && ipo_effects.noub !== ALWAYS_TRUE
ipo_effects = Effects(ipo_effects; noub=NOUB_IF_NOINBOUNDS)
end
return ipo_effects
end
Expand Down
2 changes: 0 additions & 2 deletions base/compiler/validation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -253,5 +253,3 @@ function is_valid_rvalue(@nospecialize(x))
end

is_valid_return(@nospecialize(x)) = is_valid_argument(x) || (isa(x, Expr) && x.head === :lambda)

is_flag_set(byte::UInt8, flag::UInt8) = (byte & flag) == flag
Loading