Section 10: Thread-Local Non-Atomic ARC
Context: Currently, ori_rt uses AtomicI64 with Relaxed/Release/Acquire ordering for all RC operations. This is correct for thread-shared values but wasteful for thread-local ones. Most values in most programs never cross thread boundaries — they’re created, used, and freed within a single thread.
Rust solved this by having two types: Rc (non-atomic, thread-local) and Arc (atomic, thread-safe). Ori doesn’t expose this distinction to the programmer — the compiler decides automatically.
Reference implementations:
- Rust
library/alloc/src/rc.rsvslibrary/alloc/src/sync.rs:RcusesCell<usize>(non-atomic),ArcusesAtomicUsize. Programmer chooses. - Swift: All RC is atomic by default, but
isKnownUniquelyReferenced()enables COW without RC overhead. No automatic non-atomic promotion. - CPython: GIL-protected — all RC is effectively non-atomic because only one thread runs at a time.
Architecture boundary: AIMS combines its Locality dimension with
call-graph/thread-boundary evidence and freezes
ThreadReachability::{Confined, PotentiallyShared} by stable allocation
identity. This is a safety requirement, not an opcode selection.
VmLayoutPlan and CompiledLayoutPlan independently choose a synchronization
mechanism and prove it satisfies that exact fact. A conservative atomic choice
may satisfy Confined; a plain-load/store choice cannot satisfy
PotentiallyShared.
No physical planner may re-run thread escape analysis.
Depends on: §08 (AIMS locality/lifetime facts), §09 (neutral OwnerBound
and OwnershipObservationFacts plus independently validated physical metadata).
10.1 AIMS Thread-Escape Evidence
File(s): compiler/ori_arc/src/aims/ interprocedural/effect analysis and
the executable artifact’s typed ThreadReachabilityFacts
Extend the AIMS analysis from §08 to track thread-boundary reachability. The
output is an exact stable-ID fact map, not RcAtomicity, a representation-owned
locality enum, or a runtime-helper choice.
-
Add
ThreadReachability::{Confined, PotentiallyShared}as the published fact carrier; incomplete or unknown evidence defaults conservatively toPotentiallyShared. -
Identify thread boundary operations:
spawn()— values captured by the spawned closure cross threadschan.send(value)— value crosses thread via channel- Global mutable state (if Ori adds it) — shared by all threads
- FFI calls with unknown thread behavior → conservative (ThreadShared)
-
Propagate thread-locality:
pub fn derive_thread_reachability( func: &ArcFunction, locality: &AllocationLocalityFacts, thread_edges: &ThreadEscapeEvidence, pool: &Pool, ) -> FxHashMap<AllocationSiteId, ThreadReachability> { let mut reachability = FxHashMap::default(); for alloc in func.allocations() { // Thread-boundary and unknown-call evidence takes precedence over // lexical lifetime: a function-local allocation captured by spawn is // still potentially shared. let fact = if thread_edges.may_cross_or_unknown(alloc) { ThreadReachability::PotentiallyShared } else if locality.is_block_or_function_local(alloc) || thread_edges.closed_world_proves_confined(alloc) { ThreadReachability::Confined } else { ThreadReachability::PotentiallyShared }; reachability.insert(alloc, fact); } reachability } -
Whole-program optimization:
- Apply RL-21 only when the closed program has no
spawn, channel operation, or FFI export of Ori-managed pointers - derive the proof from the shared callable/effect graph, not a backend scan
- when proven, AIMS freezes
ThreadReachability::Confinedfor every applicable allocation; each physical plan may then select its cheapest sufficient mechanism
- Apply RL-21 only when the closed program has no
-
/tpr-reviewpassed — independent review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — hygiene review clean. MUST run AFTER/tpr-reviewis clean. -
Subsection close-out (10.1) — MANDATORY before starting the next subsection. Run
/improve-toolingretrospectively on THIS subsection’s debugging journey (per.claude/skills/improve-tooling/SKILL.md“Per-Subsection Workflow”): whichdiagnostics/scripts you ran, where you addeddbg!/tracingcalls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE/commit-pushusing a valid conventional-commit type (build(diagnostics): ... — surfaced by section-10.1 retrospective—build/test/chore/ci/docsare valid;tools(...)is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 10.1: no tooling gaps”. Update this subsection’sstatusin section frontmatter tocomplete. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
10.2 Non-Atomic RC Runtime
File(s): compiler/ori_rt/src/rc/nonatomic.rs (new file inside rc/ module)
Module placement: Must live inside rc/ (e.g., rc/nonatomic.rs with mod nonatomic; in rc/mod.rs) to access call_drop_fn and rc_underflow_abort which are pub(super). Note: ori_rt allows unsafe (it is NOT in the #![deny(unsafe_code)] list). Every unsafe block MUST have a // SAFETY: comment.
Risk warning: Non-atomic RC on a value that is actually thread-shared causes
data-race UB. Soundness depends on the AIMS locality/thread-escape proof and on
validation that each physical synchronization plan satisfies the frozen
ThreadReachability; representation or backend reclassification is forbidden.
Memcheck cannot prove that invariant, so the section also requires structural carrier checks, cross-executor event parity, debug guards, and helgrind/TSAN evidence.
-
Add a debug-only RC mode guard before exposing any non-atomic runtime entry point:
- Store an
RcModeflag (AtomicvsNonAtomic) in debug builds only, either in a side table or a debug-only header word ori_rc_inc/ori_rc_decassert they are not touching an allocation marked non-atomicori_rc_inc_nonatomic/ori_rc_dec_nonatomicassert they are not touching an allocation marked atomic- Release builds pay zero cost for this guard
- This guard is mandatory; helgrind is a secondary verifier, not the only safety net
- Store an
-
Implement non-atomic RC operations:
#[no_mangle] pub unsafe extern "C" fn ori_rc_inc_nonatomic(data_ptr: *mut u8) { if data_ptr.is_null() { return; } // SAFETY: data_ptr was returned by ori_rc_alloc; strong_count is at data_ptr - 8. let rc_ptr = data_ptr.sub(8).cast::<i64>(); let count = *rc_ptr; // plain load (no atomic) if count >= MAX_REFCOUNT { std::process::abort(); } *rc_ptr = count + 1; // plain store (no atomic) } #[no_mangle] pub unsafe extern "C" fn ori_rc_dec_nonatomic( data_ptr: *mut u8, drop_fn: Option<extern "C" fn(*mut u8)>, ) { if data_ptr.is_null() { return; } // SAFETY: data_ptr was returned by ori_rc_alloc; strong_count is at data_ptr - 8. let rc_ptr = data_ptr.sub(8).cast::<i64>(); let count = *rc_ptr; // plain load (no atomic) // Underflow protection — matches ori_rc_dec (rc/mod.rs). // Always-on, not debug-only. Catches double-free bugs. if count <= 0 { rc_underflow_abort(data_ptr); } *rc_ptr = count - 1; // plain store (no atomic) if count == 1 { // Last reference — drop via abort-on-panic guard. // ori_rc_dec_nonatomic is nounwind; unwinding through it is UB. if let Some(f) = drop_fn { call_drop_fn(f, data_ptr); } } } -
Also provide width-specific non-atomic variants:
ori_rc_inc_nonatomic_i8,ori_rc_dec_nonatomic_i8ori_rc_inc_nonatomic_i16,ori_rc_dec_nonatomic_i16- Combines with §09 header compression
-
Every physical projection selects and validates its own runtime mechanism from the frozen AIMS fact; layout owns the actual header, width, storage, and synchronization sequence:
match (facts.thread_reachability(site), layout.rc_sync(site)) { (ThreadReachability::PotentiallyShared, RcSync::Atomic(ordering)) => emit_atomic_rc(layout.rc_width(site), ordering), (ThreadReachability::Confined, RcSync::Atomic(ordering)) => emit_atomic_rc(layout.rc_width(site), ordering), (ThreadReachability::Confined, RcSync::ThreadConfined) => emit_nonatomic_rc(layout.rc_width(site)), (ThreadReachability::PotentiallyShared, RcSync::ThreadConfined) => return Err(LayoutError::InsufficientThreadSafety { site }), } -
RL-19/20/21 freeze
ThreadReachabilityFacts. The existingRcAtomicityfield onRcInc/RcDecis transitional physical vocabulary and must be retired from the shared artifact once both planners consume the new fact map.Cross-thread or unknown evidence produces
PotentiallyShared(RL-20), proven thread-local evidence producesConfined(RL-19), and the closed-program condition produces all-applicableConfinedfacts (RL-21). VM, LLVM, native, compiled-WebAssembly, and JIT planners consume the same exact map;ReprPlan::rc_strategy()cannot override it.- Physical coalescing mode fence (supersedes TPR-06-1-codex’s carrier-specific shape): AIMS coalesces logical additional-credit and release events without an atomicity field. A physical optimizer may combine actions only after layout selection and must keep synchronization mode in its coalescing key. Add a negative pin showing that actions with incompatible physical
RcSyncplans are not merged. Do not preserveRcAtomicityin AIMS solely to service this backend optimization.
- Physical coalescing mode fence (supersedes TPR-06-1-codex’s carrier-specific shape): AIMS coalesces logical additional-credit and release events without an atomicity field. A physical optimizer may combine actions only after layout selection and must keep synchronization mode in its coalescing key. Add a negative pin showing that actions with incompatible physical
-
Tests (relocated IN from §06.3 per
routing.md §4— the NonAtomic-SELECTION tests require this section’s selecting backend to exercise; §06.3 keeps only the carrier-default SITE pins): pertests.md §Matrix Testing Rulesemantic + negative pairing —- Cross-thread escape produces
PotentiallyShared; each physical plan either selects a race-safe mechanism or is rejected. Pin evaluator/VM/LLVM debug/release/AOT behavior and cleanup parity. Use an FFI pointer export while the channel-send frontend path remains unavailable, retaining the skipped channel companion. - Intra-thread Sendable produces
Confined; negative pin: reverting RL-19 fails the fact test, while a conservative atomic physical plan remains legal. - Program with no spawn/channel + no FFI exporting Ori-managed pointers (per
RL-21) freezes all-applicable
Confinedfacts; separate projection pins prove the compiled plan selects non-atomic RC and the session-confined VM uses no synchronization it does not need.
- Cross-thread escape produces
-
/tpr-reviewpassed — independent review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — hygiene review clean. MUST run AFTER/tpr-reviewis clean. -
Subsection close-out (10.2) — MANDATORY before starting the next subsection. Run
/improve-toolingretrospectively on THIS subsection’s debugging journey (per.claude/skills/improve-tooling/SKILL.md“Per-Subsection Workflow”): whichdiagnostics/scripts you ran, where you addeddbg!/tracingcalls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE/commit-pushusing a valid conventional-commit type (build(diagnostics): ... — surfaced by section-10.2 retrospective—build/test/chore/ci/docsare valid;tools(...)is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 10.2: no tooling gaps”. Update this subsection’sstatusin section frontmatter tocomplete. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
10.3 Static Thread-Reachability Fence
File(s): AIMS verification plus shared executable validation
The production fact is static: a value that may cross a thread boundary is
PotentiallyShared for its whole realized lifetime. No runtime transition or
representation-owned reclassification exists.
Physical plans may choose different sufficient mechanisms without changing the fact.
-
Verify every channel send, spawn capture, and FFI-export path freezes
PotentiallySharedfor the transferred allocation. -
Treat incomplete/unknown provenance as
PotentiallyShared; never insert a late backend-local re-analysis. -
Reject a
VmLayoutPlanorCompiledLayoutPlanwhose synchronization capability is weaker than the exact bound fact. -
Require each adapter to preserve its validated mechanism across allocation, retain, release, COW, and drop. VM and compiled mechanisms need not be identical.
-
Keep dynamic mode switching outside this plan; it would require a new proved calculus rule, header protocol, and measured justification.
-
/tpr-reviewpassed — independent review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — hygiene review clean. MUST run AFTER/tpr-reviewis clean. -
Subsection close-out (10.3) — MANDATORY before starting the next subsection. Run
/improve-toolingretrospectively on THIS subsection’s debugging journey (per.claude/skills/improve-tooling/SKILL.md“Per-Subsection Workflow”): whichdiagnostics/scripts you ran, where you addeddbg!/tracingcalls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE/commit-pushusing a valid conventional-commit type (build(diagnostics): ... — surfaced by section-10.3 retrospective—build/test/chore/ci/docsare valid;tools(...)is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 10.3: no tooling gaps”. Update this subsection’sstatusin section frontmatter tocomplete. -
/sync-claudesection-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW. -
Repo hygiene check — run
diagnostics/repo-hygiene.sh --checkand clean any detected temp files.
10.4 Completion Checklist
Test matrix for §10 (write failing tests FIRST, verify they fail, then implement):
| Program pattern | Frozen AIMS fact | Physical-projection pin |
|---|---|---|
| Single-threaded program with many ownership events | Confined | Optimized compiled/VM plan variants select thread-confined bookkeeping; a conservative atomic variant remains valid and is measured separately |
| Closed program with no thread boundary or FFI pointer export | All applicable sites Confined | Optimized plan variants show zero unnecessary synchronization; validity does not require every sufficient plan to choose the same mechanism |
Multi-threaded: spawn() captures a list | Captured list PotentiallyShared; closure-local list Confined | Split race-safe/thread-confined mechanisms in both plans |
chan.send(value) — value crosses channel | value is PotentiallyShared for its realized lifetime | A thread-confined physical plan is rejected |
Value created after spawn() in spawned closure | Confined to that worker | May use thread-confined mechanics despite the program containing threads |
| Bounded count + confined value | bounded sharing + Confined | Compiled plan may combine narrow and non-atomic storage; VM chooses independently |
| Single-thread decrement to zero | Confined | Atomic and thread-confined plan variants have identical drop behavior |
- Write failing test matrix BEFORE implementation (verify tests fail with current all-atomic codegen)
- Single-threaded programs freeze
Confined; every admitted physical plan selects a sufficient mechanism. Optimized plan variants avoid unnecessary synchronization, while conservative atomic variants remain legal reference projections. - Multi-threaded programs freeze
PotentiallySharedonly for reachable shared values - Channel sends force the shared AIMS fact to
PotentiallyShared - Spawn captures force the shared AIMS fact to
PotentiallyShared - Width-specific non-atomic variants:
ori_rc_inc_nonatomic_i8,ori_rc_dec_nonatomic_i8,ori_rc_inc_nonatomic_i16,ori_rc_dec_nonatomic_i16(combines with §09) - Add semantic pin test: a single-threaded program produces ZERO atomic RC operations in LLVM IR (all ops are
ori_rc_*_nonatomic). This test can ONLY pass with thread-local analysis enabled. - Debug builds assert on RC-mode mismatches (atomic API used on non-atomic allocation or vice versa)
- Non-atomic RC operations are measurably faster (benchmark ≥ 20% improvement in RC-heavy workloads)
- Evaluator, VM, LLVM debug/release/AOT, and affected native/WASM/JIT paths preserve behavior and cleanup;
dual-exec-verify.shsupplies only the current evaluator/LLVM leg - Extend
diagnostics/valgrind-aot.shto accept an optional--tool=helgrindpassthrough flag:- Add
--helgrindflag to the script: when present, pass--tool=helgrind --fair-sched=yesto valgrind instead of--tool=memcheck - This is a concrete shell script change, not “invoke manually”
- File:
diagnostics/valgrind-aot.sh(modify the valgrind invocation line)
- Add
- Run helgrind on AOT binaries compiled from multi-threaded Ori programs (channel + spawn patterns):
./diagnostics/valgrind-aot.sh --helgrind tests/valgrind/threads/ - Create
tests/valgrind/threads/directory with at minimum:thread_local_only.ori— single-threaded program with many RC operations → no helgrind raceschannel_send.ori— program that sends values through a channel → helgrind must find no races
-
./test-all.shgreen -
./clippy-all.shgreen -
./diagnostics/dual-exec-verify.shpasses as the evaluator/LLVM leg of the broader matrix -
/tpr-reviewpassed — independent Codex review found no critical or major issues (or all findings triaged) -
/impl-hygiene-reviewpassed — implementation hygiene review clean (phase boundaries, SSOT, algorithmic DRY, naming). MUST run AFTER/tpr-reviewis clean. -
/improve-toolingretrospective completed — MANDATORY at section close, after both reviews are clean. Reflect on the section’s debugging journey (whichdiagnostics/scripts you ran, which command sequences you repeated, where you added ad-hocdbg!/tracingcalls, where output was hard to interpret) and identify any tool/log/diagnostic improvement that would have made this section materially easier OR that would help the next section touching this area. Implement every accepted improvement NOW (zero deferral) and commit each via SEPARATE/commit-push. The retrospective is mandatory even when nothing felt painful — that is exactly when blind spots accumulate. See.claude/skills/improve-tooling/SKILL.md“Retrospective Mode” for the full protocol.
Exit Criteria: AIMS freezes one validated ThreadReachability fact per
applicable allocation and every physical planner proves its mechanism satisfies
that exact fact without re-analysis.
Single-threaded benchmarks show zero unnecessary synchronization in VM traces and compiled IR where RL-21 applies; thread-boundary cases remain race-safe everywhere. The two projections may use different encodings.
Behavior, cleanup, race, and leak gates are green. Performance evidence reports speed and memory across mixed RC-heavy workloads rather than only LLVM instruction counts.
10.R Third Party Review Findings
-
[TPR-10-001][major]section-10-thread-local-arc.md:117-148— Non-atomic RC has no debug-mode safety net; analysis wrong → silent UB. Theori_rc_inc_nonatomic/ori_rc_dec_nonatomicfunctions use plain loads/stores (lines 120, 124:*rc_ptr). If thread escape analysis (§08+§10.1) is unsound for any value, concurrent access produces data race UB. The plan acknowledges this risk (line 111) but proposes no runtime fallback — only helgrind testing as a detection tool. No mechanism exists to verify at runtime that a value classified as thread-local is actually single-threaded. Action: Add a debug-mode#[cfg(debug_assertions)]per-allocation flag that records the RC mode (atomic vs non-atomic). Assert on mismatched access (e.g.,ori_rc_inccalled on an allocation marked non-atomic). Zero cost in release builds. This catches analysis bugs during development before they become silent data races in production. Consensus: 3/3 reviewers.
HISTORY
- 2026-07-14 — backend-neutral calculus correction: supersedes both the
earlier
ori_repr::escape::ThreadLocalityproposal and the intermediate “AIMS emitsRcAtomicity” correction. AIMS freezes onlyThreadReachability;VmLayoutPlanandCompiledLayoutPlanindependently select and validate synchronization, header, width, and instruction mechanics. The shippedRcAtomicityenum is a migration carrier, not final calculus vocabulary. - 2026-06-02 — RL-19/20/21 NonAtomic-selecting dispatch + its NonAtomic-selection tests relocated IN from
plans/aims-burden-tracking/section-06-phase7-mechanical-lowering.md §06.3(routing.md §4): §06.3 delivered theRcAtomicity { Atomic, NonAtomic }carrier SITE on theRcInc/RcDecArcInstrvariants (compiler/ori_arc/src/ir/instr.rs+ir/repr.rs), defaulted toAtomicat every construction site (reproducing the shipped unconditionally-atomic runtime RC primitives bit-for-bit). The dispatch that POPULATESRcAtomicity::NonAtomiccannot live in §06.3 — it requires §10.1’sThreadLocalitythread-escape analysis (compiler/ori_repr/src/escape/thread.rs), §10.2’s non-atomic runtime (ori_rc_inc_nonatomic/ori_rc_dec_nonatomic), and §10.2’s atomic-vs-non-atomic codegen selection (emit_atomic_rc/emit_nonatomic_rc), all owned HERE. PopulatingNonAtomicin §06.3 without §10’s backend would be an inert no-op (the carrier ignored by the unconditionally-atomic codegen). Perrouting.md §4MOVE-the-item discipline: §10.2 gains the carrier-population checkbox + the 3 NonAtomic-selection tests (cross-thread-escape FFI-export → Atomic; intra-thread Sendable → NonAtomic; whole-program RL-21 → all NonAtomic). The channel-send#skipcompanion is anchored to BUG-02-037 (channel constructionchannel<T>(buffer:)emits E2040 at the frontend — filed 2026-06-02; theProducer<T>/Consumer<T>channel-send thread-escape path is blocked until E2040 resolves, so the primary cross-thread exercise is FFI pointer export per RL-21). §06.3 retains only the carrier-default SITE pins; §06.3 checkboxes 1+2 + SITE-tests are[x]-DONE. The §10.4 test matrix already covers the whole-program + channel-send + spawn-capture rows; the relocated §10.2 tests reference them.