From b6d0f624a0ab779d2ca7898285346604b958a609 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Tue, 18 Aug 2026 13:10:57 +0200 Subject: [PATCH] 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 JuliaLang/julia#57780, but a more extensive audit is planned for later. - closes JuliaLang/julia#57780 (cherry picked from commit 6a32f7a427789306a0008646900a636a7d91f36e) Adapted for release-1.11: the compiler lives in `base/compiler/` rather than `Compiler/`, and `intrinsic_effects` keeps this branch's varargs guard on `nothrow`. Otherwise identical to the change that shipped in 1.12 (e4e489a740). Co-authored-by: Jameson Nash Assisted-by: Claude Code (Opus 5) --- base/compiler/tfuncs.jl | 56 ++++++++++++++++------------- stdlib/REPL/test/replcompletions.jl | 6 ++++ test/compiler/effects.jl | 34 ++++++++++++++++++ 3 files changed, 72 insertions(+), 24 deletions(-) diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index baa364a69609b..3d159d901dc92 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -2403,11 +2403,8 @@ const _ARGMEM_BUILTINS = Any[ ] const _INCONSISTENT_INTRINSICS = Any[ - Intrinsics.pointerref, # this one is volatile - Intrinsics.sqrt_llvm_fast, # this one may differ at runtime (by a few ulps) - Intrinsics.have_fma, # this one depends on the runtime environment - Intrinsics.cglobal, # cglobal lookup answer changes at runtime - # ... and list fastmath intrinsics: + # all is_pure_intrinsic_infer plus + # ... all the unsound fastmath functions which should have been in is_pure_intrinsic_infer # join(string.("Intrinsics.", sort(filter(endswith("_fast")∘string, names(Core.Intrinsics)))), ",\n") Intrinsics.add_float_fast, Intrinsics.div_float_fast, @@ -2892,37 +2889,48 @@ function intrinsic_nothrow(f::IntrinsicFunction, argtypes::Vector{Any}) return intrinsic_exct(SimpleInferenceLattice.instance, f, argtypes) === Union{} end -# whether `f` is pure for inference -function is_pure_intrinsic_infer(f::IntrinsicFunction) - return !(f === Intrinsics.pointerref || # this one is volatile - f === Intrinsics.pointerset || # this one is never effect-free - f === Intrinsics.llvmcall || # this one is never effect-free - f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps) - f === Intrinsics.have_fma || # this one depends on the runtime environment - f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime +function _is_effect_free_infer(f::IntrinsicFunction) + return !(f === Intrinsics.pointerset || + f === Intrinsics.atomic_pointerref || + f === Intrinsics.atomic_pointerset || + f === Intrinsics.atomic_pointerswap || + # f === Intrinsics.atomic_pointermodify || + f === Intrinsics.atomic_pointerreplace || + f === Intrinsics.atomic_fence) end -# whether `f` is effect free if nothrow -function intrinsic_effect_free_if_nothrow(@nospecialize f) - return f === Intrinsics.pointerref || - f === Intrinsics.have_fma || - is_pure_intrinsic_infer(f) +# whether `f` is pure for inference +function is_pure_intrinsic_infer(f::IntrinsicFunction, is_effect_free::Union{Nothing,Bool}=nothing) + if is_effect_free === nothing + is_effect_free = _is_effect_free_infer(f) + end + return is_effect_free && !( + f === Intrinsics.llvmcall || # can do arbitrary things + f === Intrinsics.atomic_pointermodify || # can do arbitrary things + f === Intrinsics.pointerref || # this one is volatile + f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps) + f === Intrinsics.have_fma || # this one depends on the runtime environment + f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime end function intrinsic_effects(f::IntrinsicFunction, argtypes::Vector{Any}) if f === Intrinsics.llvmcall # llvmcall can do arbitrary things return Effects() + elseif f === atomic_pointermodify + # atomic_pointermodify has memory effects, plus any effects from the ModifyOpInfo + return Effects() end - - if contains_is(_INCONSISTENT_INTRINSICS, f) - consistent = ALWAYS_FALSE - else + is_effect_free = _is_effect_free_infer(f) + effect_free = is_effect_free ? ALWAYS_TRUE : ALWAYS_FALSE + if ((is_pure_intrinsic_infer(f, is_effect_free) && !contains_is(_INCONSISTENT_INTRINSICS, f)) || + f === Intrinsics.pointerset || f === Intrinsics.atomic_pointerset || f === Intrinsics.atomic_fence) consistent = ALWAYS_TRUE + else + consistent = ALWAYS_FALSE end - effect_free = !(f === Intrinsics.pointerset) ? ALWAYS_TRUE : ALWAYS_FALSE nothrow = (isempty(argtypes) || !isvarargtype(argtypes[end])) && intrinsic_nothrow(f, argtypes) - inaccessiblememonly = ALWAYS_TRUE + inaccessiblememonly = is_effect_free && !(f === Intrinsics.pointerref) ? ALWAYS_TRUE : ALWAYS_FALSE return Effects(EFFECTS_TOTAL; consistent, effect_free, nothrow, inaccessiblememonly) end diff --git a/stdlib/REPL/test/replcompletions.jl b/stdlib/REPL/test/replcompletions.jl index 5e6dddc455056..5f8ad44d72a96 100644 --- a/stdlib/REPL/test/replcompletions.jl +++ b/stdlib/REPL/test/replcompletions.jl @@ -2330,3 +2330,9 @@ f54131 = F54131() @test_broken REPLCompletions.KeywordArgumentCompletion("kwarg") in a @test (@elapsed completions(s, lastindex(s), @__MODULE__, false)) < 1 end + +# JuliaLang/julia#57780 +const issue57780 = ["a", "b", "c"] +const issue57780_orig = copy(issue57780) +test_complete_context("empty!(issue57780).", Main) +@test issue57780 == issue57780_orig diff --git a/test/compiler/effects.jl b/test/compiler/effects.jl index 98681ca6ce997..c09eefdddf3ff 100644 --- a/test/compiler/effects.jl +++ b/test/compiler/effects.jl @@ -1405,3 +1405,37 @@ global v53613 = nothing @test f53613() === nothing @test g53613() === nothing @test h53613() === nothing + +# effects modeling for pointer-related intrinsics +let effects = Base.infer_effects(Core.Intrinsics.pointerref, Tuple{Vararg{Any}}) + @test !Core.Compiler.is_consistent(effects) + @test Core.Compiler.is_effect_free(effects) + @test !Core.Compiler.is_inaccessiblememonly(effects) +end +let effects = Base.infer_effects(Core.Intrinsics.pointerset, Tuple{Vararg{Any}}) + @test Core.Compiler.is_consistent(effects) + @test !Core.Compiler.is_effect_free(effects) +end +# effects modeling for atomic intrinsics +# these functions especially need to be marked !effect_free since they imply synchronization +for atomicfunc = Any[ + Core.Intrinsics.atomic_pointerref, + Core.Intrinsics.atomic_pointerset, + Core.Intrinsics.atomic_pointerswap, + Core.Intrinsics.atomic_pointerreplace, + Core.Intrinsics.atomic_fence] + @test !Core.Compiler.is_effect_free(Base.infer_effects(atomicfunc, Tuple{Vararg{Any}})) +end + +# effects modeling for intrinsics that can do arbitrary things +let effects = Base.infer_effects(Core.Intrinsics.llvmcall, Tuple{Vararg{Any}}) + @test effects == Core.Compiler.Effects() +end +let effects = Base.infer_effects(Core.Intrinsics.atomic_pointermodify, Tuple{Vararg{Any}}) + @test effects == Core.Compiler.Effects() +end + +# JuliaLang/julia#57780 +let effects = Base.infer_effects(Base._unsetindex!, (MemoryRef{String},)) + @test !Core.Compiler.is_effect_free(effects) +end