bugfix-rc-global-stats-race, GREEN stage. RED is the separate
audit-trail commit 427b687.
g_rc_alloc_count / g_rc_free_count (runtime/rc.c:90-91) were plain
static uint64_t; the __ail_tls_ctx == NULL fallback at rc.c:161
(alloc) and rc.c:212 (dec-to-zero) did a non-atomic ++. A
multi-threaded host using the global fallback path (no
ailang_ctx_new) raced the read-modify-write and lost increments, so
the AILANG_RC_STATS atexit Σ under-counted non-deterministically.
Not a memory bug — Ctx:!Send keeps every box on one thread; only
the global statistics Σ was wrong (the M5 iter-2 symbol-fan
leak-proof finding, b724cd1; user-approved Option A bounce-back
resolution).
Fix (runtime/rc.c only, 37+/14-): the two globals are now
_Atomic uint64_t; atomic_fetch_add_explicit(.., memory_order_relaxed)
at the two null-ctx fallback ++ sites; atomic_load_explicit(..,
relaxed) snapshot in ailang_rc_stats_atexit (its sole reader).
Relaxed is correct and sufficient — pure statistics, no
happens-before obligation, atexit reader runs after all worker
threads joined. Per-ctx counters and the per-object refcount header
stay non-atomic BY DESIGN (single-thread-per-ctx; boxes never cross
threads) — out of scope, untouched. Frozen value layout / ABI
offsets / host-free rule untouched. The genuinely-still-
single-threaded drop-worklist doc note was correctly left unchanged
(false correction refused); the two stale atomicity doc blocks
corrected for honesty.
Boss-verified independently: RED -> GREEN deterministically 3/3
(jitter gone, allocs==frees==16_000_000); full cargo test -p ail
green (every binary 0 failed — per-ctx tsan harnesses + embed
e2e unaffected; rc.c links into every ail binary, strong regression
gate); scope = runtime/rc.c + journal + stats only; RED files
unchanged. Unblocks resuming the M5 leak-proof.
Includes the per-iter journal, stats, and the INDEX.md line.
3.1 KiB
iter bugfix-rc-global-stats-race — atomic global RC-stats fallback counters
Date: 2026-05-19
Started from: 427b687b95
Status: DONE
Tasks completed: 1 of 1
Summary
The two null-ctx fallback RC-stats counters g_rc_alloc_count /
g_rc_free_count (runtime/rc.c) were plain static uint64_t with a
non-atomic ++ at the __ail_tls_ctx == NULL arm of ailang_rc_alloc
and the to-zero branch of ailang_rc_dec. A multi-threaded host that
uses the global fallback path (no ailang_ctx_new) raced the
read-modify-write and lost increments, so the AILANG_RC_STATS atexit
Σ under-counted non-deterministically. Not a memory bug — no box
crosses a thread; only the global statistics Σ was wrong. Fix
(user-approved Option A, 2026-05-19): the two global counters are now
_Atomic uint64_t, incremented via
atomic_fetch_add_explicit(.., memory_order_relaxed) and read via
atomic_load_explicit(.., memory_order_relaxed) in the atexit printer
(its only reader). Relaxed is correct and sufficient: pure statistics
with no happens-before obligation, and the atexit reader runs after all
worker threads have joined (single-threaded at exit). The per-ctx
counters and the per-object refcount header stay non-atomic by design
(single-thread-per-ctx; boxes never cross threads) — explicitly out of
scope. Scope held to runtime/rc.c only.
Per-task notes
- bugfix-rc-global-stats-race.1: made
g_rc_alloc_count/g_rc_free_count_Atomic uint64_t; added<stdatomic.h>;atomic_fetch_add_explicit(.., relaxed)at the two null-ctx fallback++sites;atomic_load_explicit(.., relaxed)snapshot inailang_rc_stats_atexit(the sole reader). Corrected the two now-stale doc blocks (file header comment + the 18g.0 stats block) to state the global counters are atomic while per-ctx + header stay non-atomic by design. Left the rc.c drop-worklist "Single-threaded; non-atomic" note unchanged — it refers only to the per-call drop-worklist, which is genuinely still single-threaded, so correcting it would be a false correction the constraint explicitly forbids. REDembed_rc_global_stats_racewent GREEN deterministically (3/3 runs, allocs == frees == 16_000_000); fullcargo test -p ailgreen (39/39 result lines ok, all embed_* incl. the per-ctx tsan harnesses unaffected).
Concerns
(none)
Known debt
runtime/rc.ccomment near the 18g.0 block ("adds only two unconditional++operations to the hot path") is now slightly imprecise: the global path is a relaxed atomic add and is conditional on_ctx == NULL. Left verbatim deliberately — it is a performance aside (not an atomicity claim), it predates and was already imprecise after the M2 per-ctx split, and the atomicity statement is now correctly carried by the adjacent rewritten block; editing it would exceed the named doc-honesty drift the minimal-fix constraint scoped this iter to.
Blocked detail
(n/a — DONE)
Files touched
- runtime/rc.c
Stats
bench/orchestrator-stats/2026-05-19-iter-bugfix-rc-global-stats-race.json