Skip to content

Commit 6a32f7a

Browse files
aviateskvtjnash
andauthored
effects: fix effects of atomic pointer operations (#57806)
The `_unsetindex!(::GenericMemoryRef)` function was being concretely evaluated (meaning the code was actually being run during inference) by the `REPLInterpreter`. The root cause is that the Base compiler system forgot to mark `atomic_pointerset` as "effect-free". This is simply a bug in the effect system. It didn't cause problems during normal Base compilation because `atomic_pointerset` was correctly marked as "consistent" (concrete evaluation requires both "consistent" and "effect-free"). However, this was still a pretty risky situation. The reason this only caused problems with REPL completion is that the `REPLInterpreter` intentionally ignores the "consistent" requirement under certain conditions to achieve better completion accuracy. This is usually fine, but it relies on "effect-free" being correctly marked. So, when there's a critical bug like this in the effect system, these kinds of dangerous issues can occur. As discussed with Jameson earlier, the effects of atomic pointer operations are not precisely defined. This commit includes the minimal changes necessary to fix #57780, but a more extensive audit is planned for later. - closes #57780 --------- Co-authored-by: Jameson Nash <[email protected]>
1 parent 60a9f69 commit 6a32f7a

File tree

3 files changed

+72
-23
lines changed

3 files changed

+72
-23
lines changed

Compiler/src/tfuncs.jl

+32-23
Original file line numberDiff line numberDiff line change
@@ -2407,11 +2407,8 @@ const _ARGMEM_BUILTINS = Any[
24072407
]
24082408

24092409
const _INCONSISTENT_INTRINSICS = Any[
2410-
Intrinsics.pointerref, # this one is volatile
2411-
Intrinsics.sqrt_llvm_fast, # this one may differ at runtime (by a few ulps)
2412-
Intrinsics.have_fma, # this one depends on the runtime environment
2413-
Intrinsics.cglobal, # cglobal lookup answer changes at runtime
2414-
# ... and list fastmath intrinsics:
2410+
# all is_pure_intrinsic_infer plus
2411+
# ... all the unsound fastmath functions which should have been in is_pure_intrinsic_infer
24152412
# join(string.("Intrinsics.", sort(filter(endswith("_fast")∘string, names(Core.Intrinsics)))), ",\n")
24162413
Intrinsics.add_float_fast,
24172414
Intrinsics.div_float_fast,
@@ -2936,36 +2933,48 @@ function intrinsic_nothrow(f::IntrinsicFunction, argtypes::Vector{Any})
29362933
return intrinsic_exct(SimpleInferenceLattice.instance, f, argtypes) === Union{}
29372934
end
29382935

2939-
# whether `f` is pure for inference
2940-
function is_pure_intrinsic_infer(f::IntrinsicFunction)
2941-
return !(f === Intrinsics.pointerref || # this one is volatile
2942-
f === Intrinsics.pointerset || # this one is never effect-free
2943-
f === Intrinsics.llvmcall || # this one is never effect-free
2944-
f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps)
2945-
f === Intrinsics.have_fma || # this one depends on the runtime environment
2946-
f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime
2936+
function _is_effect_free_infer(f::IntrinsicFunction)
2937+
return !(f === Intrinsics.pointerset ||
2938+
f === Intrinsics.atomic_pointerref ||
2939+
f === Intrinsics.atomic_pointerset ||
2940+
f === Intrinsics.atomic_pointerswap ||
2941+
# f === Intrinsics.atomic_pointermodify ||
2942+
f === Intrinsics.atomic_pointerreplace ||
2943+
f === Intrinsics.atomic_fence)
29472944
end
29482945

2949-
# whether `f` is effect free if nothrow
2950-
function intrinsic_effect_free_if_nothrow(@nospecialize f)
2951-
return f === Intrinsics.pointerref ||
2952-
f === Intrinsics.have_fma ||
2953-
is_pure_intrinsic_infer(f)
2946+
# whether `f` is pure for inference
2947+
function is_pure_intrinsic_infer(f::IntrinsicFunction, is_effect_free::Union{Nothing,Bool}=nothing)
2948+
if is_effect_free === nothing
2949+
is_effect_free = _is_effect_free_infer(f)
2950+
end
2951+
return is_effect_free && !(
2952+
f === Intrinsics.llvmcall || # can do arbitrary things
2953+
f === Intrinsics.atomic_pointermodify || # can do arbitrary things
2954+
f === Intrinsics.pointerref || # this one is volatile
2955+
f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps)
2956+
f === Intrinsics.have_fma || # this one depends on the runtime environment
2957+
f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime
29542958
end
29552959

29562960
function intrinsic_effects(f::IntrinsicFunction, argtypes::Vector{Any})
29572961
if f === Intrinsics.llvmcall
29582962
# llvmcall can do arbitrary things
29592963
return Effects()
2964+
elseif f === atomic_pointermodify
2965+
# atomic_pointermodify has memory effects, plus any effects from the ModifyOpInfo
2966+
return Effects()
29602967
end
2961-
if contains_is(_INCONSISTENT_INTRINSICS, f)
2962-
consistent = ALWAYS_FALSE
2963-
else
2968+
is_effect_free = _is_effect_free_infer(f)
2969+
effect_free = is_effect_free ? ALWAYS_TRUE : ALWAYS_FALSE
2970+
if ((is_pure_intrinsic_infer(f, is_effect_free) && !contains_is(_INCONSISTENT_INTRINSICS, f)) ||
2971+
f === Intrinsics.pointerset || f === Intrinsics.atomic_pointerset || f === Intrinsics.atomic_fence)
29642972
consistent = ALWAYS_TRUE
2973+
else
2974+
consistent = ALWAYS_FALSE
29652975
end
2966-
effect_free = !(f === Intrinsics.pointerset) ? ALWAYS_TRUE : ALWAYS_FALSE
29672976
nothrow = intrinsic_nothrow(f, argtypes)
2968-
inaccessiblememonly = ALWAYS_TRUE
2977+
inaccessiblememonly = is_effect_free && !(f === Intrinsics.pointerref) ? ALWAYS_TRUE : ALWAYS_FALSE
29692978
return Effects(EFFECTS_TOTAL; consistent, effect_free, nothrow, inaccessiblememonly)
29702979
end
29712980

Compiler/test/effects.jl

+34
Original file line numberDiff line numberDiff line change
@@ -1427,3 +1427,37 @@ end |> !Compiler.is_nothrow
14271427
@test Base.infer_effects((UInt64,)) do x
14281428
return Core.Intrinsics.uitofp(Int64, x)
14291429
end |> !Compiler.is_nothrow
1430+
1431+
# effects modeling for pointer-related intrinsics
1432+
let effects = Base.infer_effects(Core.Intrinsics.pointerref, Tuple{Vararg{Any}})
1433+
@test !Compiler.is_consistent(effects)
1434+
@test Compiler.is_effect_free(effects)
1435+
@test !Compiler.is_inaccessiblememonly(effects)
1436+
end
1437+
let effects = Base.infer_effects(Core.Intrinsics.pointerset, Tuple{Vararg{Any}})
1438+
@test Compiler.is_consistent(effects)
1439+
@test !Compiler.is_effect_free(effects)
1440+
end
1441+
# effects modeling for atomic intrinsics
1442+
# these functions especially need to be marked !effect_free since they imply synchronization
1443+
for atomicfunc = Any[
1444+
Core.Intrinsics.atomic_pointerref,
1445+
Core.Intrinsics.atomic_pointerset,
1446+
Core.Intrinsics.atomic_pointerswap,
1447+
Core.Intrinsics.atomic_pointerreplace,
1448+
Core.Intrinsics.atomic_fence]
1449+
@test !Compiler.is_effect_free(Base.infer_effects(atomicfunc, Tuple{Vararg{Any}}))
1450+
end
1451+
1452+
# effects modeling for intrinsics that can do arbitrary things
1453+
let effects = Base.infer_effects(Core.Intrinsics.llvmcall, Tuple{Vararg{Any}})
1454+
@test effects == Compiler.Effects()
1455+
end
1456+
let effects = Base.infer_effects(Core.Intrinsics.atomic_pointermodify, Tuple{Vararg{Any}})
1457+
@test effects == Compiler.Effects()
1458+
end
1459+
1460+
# JuliaLang/julia#57780
1461+
let effects = Base.infer_effects(Base._unsetindex!, (MemoryRef{String},))
1462+
@test !Compiler.is_effect_free(effects)
1463+
end

stdlib/REPL/test/replcompletions.jl

+6
Original file line numberDiff line numberDiff line change
@@ -2484,3 +2484,9 @@ let (c, r, res) = test_complete_context("global xxx::Number = Base.", Main)
24842484
@test res
24852485
@test "pi" c
24862486
end
2487+
2488+
# JuliaLang/julia#57780
2489+
const issue57780 = ["a", "b", "c"]
2490+
const issue57780_orig = copy(issue57780)
2491+
test_complete_context("empty!(issue57780).", Main)
2492+
@test issue57780 == issue57780_orig

0 commit comments

Comments
 (0)