Skip to content

Commit

Permalink
effects: minor fixes for the effects system correctness (#55536)
Browse files Browse the repository at this point in the history
This commit implements several fixes related to the correctness of the
effect system. The most significant change addresses an issue where
post-opt analysis was not correctly propagating the taints of `:noub`
and `:nortcall` of `:foreigncall` expressions, which could lead to
incorrect effect bits. Additionally, adjustments have been made to the
values of effects used in various worst-case scenarios.
  • Loading branch information
aviatesk authored Aug 21, 2024
1 parent 86cba99 commit 54142b7
Show file tree
Hide file tree
Showing 10 changed files with 38 additions and 14 deletions.
2 changes: 1 addition & 1 deletion base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2829,7 +2829,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp
elseif ehead === :globaldecl
return RTEffects(Nothing, Any, EFFECTS_UNKNOWN)
elseif ehead === :thunk
return RTEffects(Any, Any, EFFECTS_UNKNOWN)
return RTEffects(Any, Any, Effects())
end
# N.B.: abstract_eval_value_expr can modify the global effects, but
# we move out any arguments with effects during SSA construction later
Expand Down
10 changes: 5 additions & 5 deletions base/compiler/effects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,12 @@ const NOUB_IF_NOINBOUNDS = 0x01 << 1
# :nonoverlayed bits
const CONSISTENT_OVERLAY = 0x01 << 1

const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, true)
const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, true)
const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_TRUE, false) # unknown mostly, but it's not overlayed at least (e.g. it's not a call)
const _EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_FALSE, false) # unknown really
const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, true)
const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, true)
const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_TRUE, false) # unknown mostly, but it's not overlayed at least (e.g. it's not a call)

function Effects(effects::Effects = _EFFECTS_UNKNOWN;
function Effects(effects::Effects=Effects(
ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_FALSE, false);
consistent::UInt8 = effects.consistent,
effect_free::UInt8 = effects.effect_free,
nothrow::Bool = effects.nothrow,
Expand Down
10 changes: 8 additions & 2 deletions base/compiler/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ const IR_FLAGS_NEEDS_EA = IR_FLAG_EFIIMO | IR_FLAG_INACCESSIBLEMEM_OR_ARGMEM

has_flag(curr::UInt32, flag::UInt32) = (curr & flag) == flag

function iscallstmt(@nospecialize stmt)
stmt isa Expr || return false
head = stmt.head
return head === :call || head === :invoke || head === :foreigncall
end

function flags_for_effects(effects::Effects)
flags = zero(UInt32)
if is_consistent(effects)
Expand Down Expand Up @@ -380,7 +386,7 @@ function recompute_effects_flags(𝕃ₒ::AbstractLattice, @nospecialize(stmt),
elseif nothrow
flag |= IR_FLAG_NOTHROW
end
if !(isexpr(stmt, :call) || isexpr(stmt, :invoke))
if !iscallstmt(stmt)
# There is a bit of a subtle point here, which is that some non-call
# statements (e.g. PiNode) can be UB:, however, we consider it
# illegal to introduce such statements that actually cause UB (for any
Expand Down Expand Up @@ -784,7 +790,7 @@ function scan_non_dataflow_flags!(inst::Instruction, sv::PostOptAnalysisState)
if !has_flag(flag, IR_FLAG_NORTCALL)
# if a function call that might invoke `Core.Compiler.return_type` has been deleted,
# there's no need to taint with `:nortcall`, allowing concrete evaluation
if isexpr(stmt, :call) || isexpr(stmt, :invoke)
if iscallstmt(stmt)
sv.nortcall = false
end
end
Expand Down
3 changes: 2 additions & 1 deletion base/compiler/ssair/ir.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Core.PhiNode() = Core.PhiNode(Int32[], Any[])

isterminator(@nospecialize(stmt)) = isa(stmt, GotoNode) || isa(stmt, GotoIfNot) || isa(stmt, ReturnNode) || isa(stmt, EnterNode) || isexpr(stmt, :leave)
isterminator(@nospecialize(stmt)) = isa(stmt, GotoNode) || isa(stmt, GotoIfNot) ||
isa(stmt, ReturnNode) || isa(stmt, EnterNode) || isexpr(stmt, :leave)

struct CFG
blocks::Vector{BasicBlock}
Expand Down
3 changes: 2 additions & 1 deletion base/compiler/ssair/irinterp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ function reprocess_instruction!(interp::AbstractInterpreter, inst::Instruction,
rt = nothing
if isa(stmt, Expr)
head = stmt.head
if head === :call || head === :foreigncall || head === :new || head === :splatnew || head === :static_parameter || head === :isdefined || head === :boundscheck
if (head === :call || head === :foreigncall || head === :new || head === :splatnew ||
head === :static_parameter || head === :isdefined || head === :boundscheck)
(; rt, effects) = abstract_eval_statement_expr(interp, stmt, nothing, irsv)
add_flag!(inst, flags_for_effects(effects))
elseif head === :invoke
Expand Down
7 changes: 6 additions & 1 deletion base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2966,7 +2966,7 @@ end
function abstract_applicable(interp::AbstractInterpreter, argtypes::Vector{Any},
sv::AbsIntState, max_methods::Int)
length(argtypes) < 2 && return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo())
isvarargtype(argtypes[2]) && return CallMeta(Bool, Any, EFFECTS_UNKNOWN, NoCallInfo())
isvarargtype(argtypes[2]) && return CallMeta(Bool, Any, EFFECTS_THROWS, NoCallInfo())
argtypes = argtypes[2:end]
atype = argtypes_to_type(argtypes)
matches = find_method_matches(interp, argtypes, atype; max_methods)
Expand Down Expand Up @@ -3191,6 +3191,11 @@ function foreigncall_effects(@specialize(abstract_eval), e::Expr)
elseif name === :jl_genericmemory_copy_slice
return Effects(EFFECTS_TOTAL; consistent=CONSISTENT_IF_NOTRETURNED, nothrow=false)
end
# `:foreigncall` can potentially perform all sorts of operations, including calling
# overlay methods, but the `:foreigncall` itself is not dispatched, and there is no
# concern that the method calls that potentially occur within the `:foreigncall` will
# be executed using the wrong method table due to concrete evaluation, so using
# `EFFECTS_UNKNOWN` here and not tainting with `:nonoverlayed` is fine
return EFFECTS_UNKNOWN
end

Expand Down
4 changes: 3 additions & 1 deletion base/compiler/validation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ end

function is_valid_rvalue(@nospecialize(x))
is_valid_argument(x) && return true
if isa(x, Expr) && x.head in (:new, :splatnew, :the_exception, :isdefined, :call, :invoke, :invoke_modify, :foreigncall, :cfunction, :gc_preserve_begin, :copyast, :new_opaque_closure)
if isa(x, Expr) && x.head in (:new, :splatnew, :the_exception, :isdefined, :call,
:invoke, :invoke_modify, :foreigncall, :cfunction, :gc_preserve_begin, :copyast,
:new_opaque_closure)
return true
end
return false
Expand Down
6 changes: 4 additions & 2 deletions base/strings/string.jl
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ function unsafe_string(p::Union{Ptr{UInt8},Ptr{Int8}})
ccall(:jl_cstr_to_string, Ref{String}, (Ptr{UInt8},), p)
end

# This is @assume_effects :effect_free :nothrow :terminates_globally @ccall jl_alloc_string(n::Csize_t)::Ref{String},
# This is `@assume_effects :total !:consistent @ccall jl_alloc_string(n::Csize_t)::Ref{String}`,
# but the macro is not available at this time in bootstrap, so we write it manually.
@eval _string_n(n::Integer) = $(Expr(:foreigncall, QuoteNode(:jl_alloc_string), Ref{String}, Expr(:call, Expr(:core, :svec), :Csize_t), 1, QuoteNode((:ccall,0x000e)), :(convert(Csize_t, n))))
const _string_n_override = 0x04ee
@eval _string_n(n::Integer) = $(Expr(:foreigncall, QuoteNode(:jl_alloc_string), Ref{String},
:(Core.svec(Csize_t)), 1, QuoteNode((:ccall, _string_n_override)), :(convert(Csize_t, n))))

"""
String(s::AbstractString)
Expand Down
5 changes: 5 additions & 0 deletions test/compiler/effects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1361,3 +1361,8 @@ end |> Core.Compiler.is_nothrow
@test Base.infer_effects((Vector{Any},)) do xs
Core.svec(xs...)
end |> Core.Compiler.is_nothrow

# effects for unknown `:foreigncall`s
@test Base.infer_effects() do
@ccall unsafecall()::Cvoid
end == Core.Compiler.EFFECTS_UNKNOWN
2 changes: 2 additions & 0 deletions test/strings/basic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,8 @@ end
@test !Core.Compiler.is_removable_if_unused(e) || (f, Ts)
end
@test_throws ArgumentError Symbol("a\0a")

@test Base._string_n_override == Core.Compiler.encode_effects_override(Base.compute_assumed_settings((:total, :(!:consistent))))
end

@testset "Ensure UTF-8 DFA can never leave invalid state" begin
Expand Down

0 comments on commit 54142b7

Please sign in to comment.