Repository navigation
codegen: Never let optimistic effects declassify a julia.safepoint call - #62698
Closed
IanButterworth wants to merge 1 commit into
Closed
IanButterworth wants to merge 1 commit into
IanButterworth wants to merge 1 commit into
Conversation
Since JuliaLang#61394, calls to `:consistent`+`:effect_free` functions carry optimistic memory effects (e.g. memory(argmem: read)) so pre-GC passes can optimize around them, relying on LateLowerGCFrame stripping the effects before its safepoint analysis. That made rooting correctness depend on pass-ordering alone: had LocalScan ever seen the un-stripped attributes, it would have classified those calls as non-safepoints and dropped the roots of values live across them (JuliaLang#62613). Make the invariant unconditional: LocalScan now refuses to treat any call carrying "julia.safepoint" (on the call site or the callee) as a non-safepoint, whatever effects it advertises. Also widen the strip to match call-site-only attributes, as emitted when the callee is a definition rather than a declaration. The new tests cover each attribute placement (callee declaration, call site only, indirect call), both effect forms (argmem-only and readonly), the removal of the optimistic ReadNone on the gcstack argument, and a negative control confirming genuinely-readonly helpers still avoid frames. An end-to-end test verifies both that codegen still emits the optimistic attributes (so the test cannot silently go stale) and that a tracked value live across such a call keeps its GC frame root through the full -O2 pipeline. Mutation-tested: disabling the LocalScan gate fails every positive case; disabling only the strip fails exactly the ReadNone-removal case. Fixes JuliaLang#62613 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RY7drZ1oxF3yPMvVJAL7v2
This was referenced Aug 13, 2026
IanButterworth
marked this pull request as ready for review
August 13, 2026 16:51
Member
|
This is not how this should be fixed. The issues are not specific to lower GC |
Member
Author
|
Is it worth merging the tests as broken? or are we committed to the revert? |
Member
|
We are committed to revert because there's an issue identified, though we may re-land as soon as those are resolved. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I saw the revert #62697 and hadn't seen #62613 until now. This seems to be a fix based on the tests?
Note: everything below this line was written with generative AI (Claude).
Since #61394, calls to
:consistent+:effect_freefunctions carry optimisticmemory effects (e.g.
memory(argmem: read)) so pre-GC LLVM passes can optimizearound them, relying on
LateLowerGCFramestripping those effects before itssafepoint analysis. That made rooting correctness depend on pass ordering
alone: had
LocalScanever seen the un-stripped attributes, it would haveclassified those calls as non-safepoints and dropped the roots of values live
across them — the unsoundness identified in #62613.
Changes
LocalScannow refuses to treat any call carrying"julia.safepoint"(onthe call site or the callee) as a non-safepoint, whatever memory effects it
advertises. Rooting no longer depends on the strip having run first.
callee is a definition rather than a declaration (single-module AOT), where
the previous callee-only condition missed it.
Tests
test/llvmpasses/late-lower-gc.llgains six cases:memory(argmem: read)on the callee declarationLocalScanskip quoted in #62613 (JIT path)memory(read)onlyReadsMemory()arm of both checksreadnoneon the gcstack argument is strippedpgcstack(the frame link store must stay visible)"julia.safepoint"gets no frametest/llvmpasses/safepoint-effects-rooting.jlis an end-to-end test throughreal codegen with two FileCheck prefixes: the unoptimized run asserts the
optimistic attributes are actually emitted (so the test cannot silently go
stale if effects inference or attribute emission changes), and the optimized
run asserts a tracked value live across such a call keeps its GC-frame root
through the full
-O2pipeline.Mutation-tested: disabling the
LocalScangate fails every positive case(the end-to-end test fails on rooting, not on attribute emission); disabling
only the strip fails exactly the
readnone-removal case while all rootingcases still pass on the gate alone.
On the other concerns raised in #62613
add_fn_attrs_for_effectsnever emitsnosync, and the JITpipeline has no
PostOrderFunctionAttrs(onlyInferFunctionAttrsPass,which is TLI-libfunc-only), so nothing infers it; per LangRef,
memory(...)does not imply
nosync. Atomics and fences still cannot be reorderedacross these calls.
.valueestablishes a strong root —LateLowerGCFrameroots the loadedSSA value like any other tracked value, and since
.valueis a mutablefield, the
isLoadFromImmutrefinement (rooted-by-parent) can never applyto it, so the loaded value always gets its own root.
already deletes and reorders
:effect_freecalls; programs thatsynchronize with finalizers do so through locks, and lock operations are
never
:effect_free, so those remain full barriers.(the
has_user_ptrgate), so everything LLVM may derive from them is asubset of what
:consistent+:effect_freealready permit at the Julialevel — except GC rooting, which
LateLowerGCFramenow guardsunconditionally.
A follow-up worth doing regardless (relevant for a future concurrent GC):
emit
WeakRef.valueloads as atomic monotonic.Alternative to #62697.
Fixes #62613