Skip to content

Revert "codegen: Propagate ipo_purity_bits to LLVM function attributes (#61394)" - #62697

Merged
vtjnash merged 1 commit into
masterfrom
jn/revert-61394
Aug 20, 2026
Merged

vtjnash merged 1 commit into
masterfrom
jn/revert-61394

Conversation

@vtjnash

@vtjnash vtjnash commented Aug 12, 2026

Copy link
Copy Markdown
Member

This reverts commit d50d2a5 to close #62613 correctness regression.

@IanButterworth

Copy link
Copy Markdown
Member

Proposed fix instead #62698

@vtjnash

vtjnash commented Aug 17, 2026 •

Copy link
Copy Markdown
Member Author

I've been trying to get Claude to show some additional specific examples of #62613. It makes a lot of claims, but I don't think most of them are necessarily valid (its unclear exactly what we promise around WeakRef and finalizers and llvm IPO / Attributor behaviors and raw pointer_from_objref). The original claim also isn't clear if it really is a bug, or is rescued normally by late-gc-rooting semantics anyways. But it did find one other claimed risk with #62613 that does sound definite and real to me:

Store↔write-barrier separation (generational invariant)

The runtime invariant is "no safepoint between a heap store and its write barrier." Nothing in the IR states it: julia.write_barrier is inaccessibleMemOnly, so it doesn't pin the adjacent store, and until now the invariant was maintained accidentally by every call being a may-read-write barrier to store motion. memory(argmem: read) on a pure call removes the pin. LICM store promotion is the realistic trigger: a loop body of obj.f = x; write_barrier(obj); f_pure() promotes the store out of the loop while barriers execute inside it. Now the sequence is barrier → GC at f_pure's safepoint (remset processed, old obj re-marked without the new edge, removed from remset) → store installs a young pointer into an old object with no barrier after it. Next GC frees the young object while it's referenced. This is heap corruption in single-threaded program order — no race to blame it on — and rooting doesn't help because the child's SSA liveness ends at the store; the damage surfaces at a later collection.

The suggested alternative (memory(read)) may be pretty reasonable, given my instruction for it to ignore the effect of finalizers (it was already UB for finalizers themselves to not to use atomics) and WeakRef lifetimes (generally fine for these to get hoisted into local roots, which will survive the call anyways). It recommends we invert the polarity of has_safepoint to no_safepoint to be more conservatively correct as well.

Classes 1 and 2 are both write/read interactions with non-argument heap memory, which suggests the honest attribute is memory(read) rather than memory(argmem: read): the safepoint genuinely reads everything (mark phase) and writes nothing you've chosen to care about. That single change blocks DSE and store sinking/promotion across the call (killing the two corruption classes) while still permitting load hoisting/CSE across it (the conceded staleness, i.e. #62613's example) and dead-call deletion — which is most of the optimization value anyway. Combined with making NoteSafepoint trust only a positive Julia marker instead of LLVM memory effects (closing class 3 structurally instead of by pipeline discipline), the residue is just the weakref and finalizer gap

.

@vtjnash
vtjnash requested review from gbaraldi and xal-0 August 17, 2026 18:22
@vtjnash
vtjnash merged commit a48a49a into master Aug 20, 2026
7 of 9 checks passed
@vtjnash
vtjnash deleted the jn/revert-61394 branch August 20, 2026 15:52
topolarity pushed a commit to topolarity/julia that referenced this pull request Oct 5, 2026
Calls tagged "julia.safepoint" are Julia calls whose memory effects
codegen narrowed from inferred effects. They can still reach a GC
safepoint, so LocalScan must treat them as safepoints regardless of
what the memory effects claim, and the effects must be stripped before
any post-lowering pass could use them against the now-explicit GC frame
stores.

This is the rooting half of re-landing JuliaLang#61394 (reverted in JuliaLang#62697,
see JuliaLang#62613): rooting no longer depends on pass ordering having stripped
the attributes first.

Assisted-by: Claude Code (Fable 5.1)
xal-0 pushed a commit that referenced this pull request Oct 6, 2026
Calls tagged "julia.safepoint" are Julia calls whose memory effects
codegen narrowed from inferred effects. They can still reach a GC
safepoint, so LocalScan must treat them as safepoints regardless of
what the memory effects claim, and the effects must be stripped before
any post-lowering pass could use them against the now-explicit GC frame
stores.

This is the rooting half of re-landing #61394 (reverted in #62697,
see #62613): rooting no longer depends on pass ordering having stripped
the attributes first.

Assisted-by: Claude Code (Fable 5.1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add_fn_attrs_for_effects marks calls that can write memory as memory(read: argmem)

2 participants