Skip to content

Commit 54142b7

Browse files
authored
effects: minor fixes for the effects system correctness (#55536)
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.
1 parent 86cba99 commit 54142b7

File tree

10 files changed

+38
-14
lines changed

10 files changed

+38
-14
lines changed

base/compiler/abstractinterpretation.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2829,7 +2829,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp
28292829
elseif ehead === :globaldecl
28302830
return RTEffects(Nothing, Any, EFFECTS_UNKNOWN)
28312831
elseif ehead === :thunk
2832-
return RTEffects(Any, Any, EFFECTS_UNKNOWN)
2832+
return RTEffects(Any, Any, Effects())
28332833
end
28342834
# N.B.: abstract_eval_value_expr can modify the global effects, but
28352835
# we move out any arguments with effects during SSA construction later

base/compiler/effects.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,12 @@ const NOUB_IF_NOINBOUNDS = 0x01 << 1
169169
# :nonoverlayed bits
170170
const CONSISTENT_OVERLAY = 0x01 << 1
171171

172-
const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, true)
173-
const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, true)
174-
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)
175-
const _EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_FALSE, false) # unknown really
172+
const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, true)
173+
const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, true)
174+
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)
176175

177-
function Effects(effects::Effects = _EFFECTS_UNKNOWN;
176+
function Effects(effects::Effects=Effects(
177+
ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, ALWAYS_FALSE, false);
178178
consistent::UInt8 = effects.consistent,
179179
effect_free::UInt8 = effects.effect_free,
180180
nothrow::Bool = effects.nothrow,

base/compiler/optimize.jl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ const IR_FLAGS_NEEDS_EA = IR_FLAG_EFIIMO | IR_FLAG_INACCESSIBLEMEM_OR_ARGMEM
5959

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

62+
function iscallstmt(@nospecialize stmt)
63+
stmt isa Expr || return false
64+
head = stmt.head
65+
return head === :call || head === :invoke || head === :foreigncall
66+
end
67+
6268
function flags_for_effects(effects::Effects)
6369
flags = zero(UInt32)
6470
if is_consistent(effects)
@@ -380,7 +386,7 @@ function recompute_effects_flags(𝕃ₒ::AbstractLattice, @nospecialize(stmt),
380386
elseif nothrow
381387
flag |= IR_FLAG_NOTHROW
382388
end
383-
if !(isexpr(stmt, :call) || isexpr(stmt, :invoke))
389+
if !iscallstmt(stmt)
384390
# There is a bit of a subtle point here, which is that some non-call
385391
# statements (e.g. PiNode) can be UB:, however, we consider it
386392
# illegal to introduce such statements that actually cause UB (for any
@@ -784,7 +790,7 @@ function scan_non_dataflow_flags!(inst::Instruction, sv::PostOptAnalysisState)
784790
if !has_flag(flag, IR_FLAG_NORTCALL)
785791
# if a function call that might invoke `Core.Compiler.return_type` has been deleted,
786792
# there's no need to taint with `:nortcall`, allowing concrete evaluation
787-
if isexpr(stmt, :call) || isexpr(stmt, :invoke)
793+
if iscallstmt(stmt)
788794
sv.nortcall = false
789795
end
790796
end

base/compiler/ssair/ir.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

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

5-
isterminator(@nospecialize(stmt)) = isa(stmt, GotoNode) || isa(stmt, GotoIfNot) || isa(stmt, ReturnNode) || isa(stmt, EnterNode) || isexpr(stmt, :leave)
5+
isterminator(@nospecialize(stmt)) = isa(stmt, GotoNode) || isa(stmt, GotoIfNot) ||
6+
isa(stmt, ReturnNode) || isa(stmt, EnterNode) || isexpr(stmt, :leave)
67

78
struct CFG
89
blocks::Vector{BasicBlock}

base/compiler/ssair/irinterp.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ function reprocess_instruction!(interp::AbstractInterpreter, inst::Instruction,
141141
rt = nothing
142142
if isa(stmt, Expr)
143143
head = stmt.head
144-
if head === :call || head === :foreigncall || head === :new || head === :splatnew || head === :static_parameter || head === :isdefined || head === :boundscheck
144+
if (head === :call || head === :foreigncall || head === :new || head === :splatnew ||
145+
head === :static_parameter || head === :isdefined || head === :boundscheck)
145146
(; rt, effects) = abstract_eval_statement_expr(interp, stmt, nothing, irsv)
146147
add_flag!(inst, flags_for_effects(effects))
147148
elseif head === :invoke

base/compiler/tfuncs.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2966,7 +2966,7 @@ end
29662966
function abstract_applicable(interp::AbstractInterpreter, argtypes::Vector{Any},
29672967
sv::AbsIntState, max_methods::Int)
29682968
length(argtypes) < 2 && return CallMeta(Bottom, Any, EFFECTS_THROWS, NoCallInfo())
2969-
isvarargtype(argtypes[2]) && return CallMeta(Bool, Any, EFFECTS_UNKNOWN, NoCallInfo())
2969+
isvarargtype(argtypes[2]) && return CallMeta(Bool, Any, EFFECTS_THROWS, NoCallInfo())
29702970
argtypes = argtypes[2:end]
29712971
atype = argtypes_to_type(argtypes)
29722972
matches = find_method_matches(interp, argtypes, atype; max_methods)
@@ -3191,6 +3191,11 @@ function foreigncall_effects(@specialize(abstract_eval), e::Expr)
31913191
elseif name === :jl_genericmemory_copy_slice
31923192
return Effects(EFFECTS_TOTAL; consistent=CONSISTENT_IF_NOTRETURNED, nothrow=false)
31933193
end
3194+
# `:foreigncall` can potentially perform all sorts of operations, including calling
3195+
# overlay methods, but the `:foreigncall` itself is not dispatched, and there is no
3196+
# concern that the method calls that potentially occur within the `:foreigncall` will
3197+
# be executed using the wrong method table due to concrete evaluation, so using
3198+
# `EFFECTS_UNKNOWN` here and not tainting with `:nonoverlayed` is fine
31943199
return EFFECTS_UNKNOWN
31953200
end
31963201

base/compiler/validation.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,9 @@ end
257257

258258
function is_valid_rvalue(@nospecialize(x))
259259
is_valid_argument(x) && return true
260-
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)
260+
if isa(x, Expr) && x.head in (:new, :splatnew, :the_exception, :isdefined, :call,
261+
:invoke, :invoke_modify, :foreigncall, :cfunction, :gc_preserve_begin, :copyast,
262+
:new_opaque_closure)
261263
return true
262264
end
263265
return false

base/strings/string.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,11 @@ function unsafe_string(p::Union{Ptr{UInt8},Ptr{Int8}})
102102
ccall(:jl_cstr_to_string, Ref{String}, (Ptr{UInt8},), p)
103103
end
104104

105-
# This is @assume_effects :effect_free :nothrow :terminates_globally @ccall jl_alloc_string(n::Csize_t)::Ref{String},
105+
# This is `@assume_effects :total !:consistent @ccall jl_alloc_string(n::Csize_t)::Ref{String}`,
106106
# but the macro is not available at this time in bootstrap, so we write it manually.
107-
@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))))
107+
const _string_n_override = 0x04ee
108+
@eval _string_n(n::Integer) = $(Expr(:foreigncall, QuoteNode(:jl_alloc_string), Ref{String},
109+
:(Core.svec(Csize_t)), 1, QuoteNode((:ccall, _string_n_override)), :(convert(Csize_t, n))))
108110

109111
"""
110112
String(s::AbstractString)

test/compiler/effects.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,3 +1361,8 @@ end |> Core.Compiler.is_nothrow
13611361
@test Base.infer_effects((Vector{Any},)) do xs
13621362
Core.svec(xs...)
13631363
end |> Core.Compiler.is_nothrow
1364+
1365+
# effects for unknown `:foreigncall`s
1366+
@test Base.infer_effects() do
1367+
@ccall unsafecall()::Cvoid
1368+
end == Core.Compiler.EFFECTS_UNKNOWN

test/strings/basic.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,6 +1235,8 @@ end
12351235
@test !Core.Compiler.is_removable_if_unused(e) || (f, Ts)
12361236
end
12371237
@test_throws ArgumentError Symbol("a\0a")
1238+
1239+
@test Base._string_n_override == Core.Compiler.encode_effects_override(Base.compute_assumed_settings((:total, :(!:consistent))))
12381240
end
12391241

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

0 commit comments

Comments
 (0)