fix(rc): GREEN — atomic global g_rc_* stats fallback counters (swarm-safe)

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.
This commit is contained in:
2026-05-19 01:57:06 +02:00
parent 427b687b95
commit 7bfa11e838
4 changed files with 123 additions and 14 deletions
+37 -14
View File
@@ -42,12 +42,20 @@
* ABI" > "Frozen value layout". A boundary-crossing single-ctor
* scalar record's box offsets MUST NOT move.
*
* Single-threaded: counter ops are non-atomic. AILang has no
* concurrency primitives yet; when it acquires them, atomic-vs-non-
* atomic becomes a separate decision per allocation kind (see
* Decision 10's "Does not commit to atomic refcounts" clause).
* Threading: the per-object refcount header ops and the per-ctx RC
* counters are non-atomic by design — a box never crosses a thread
* and each ailang_ctx_t is single-thread-per-ctx, so neither is
* contended. The two GLOBAL null-ctx fallback stats counters
* (g_rc_alloc_count / g_rc_free_count) ARE atomic (relaxed): a
* multi-threaded host using the null-ctx fallback path concurrently
* increments them, and the AILANG_RC_STATS atexit Σ must not lose
* updates. AILang itself still has no concurrency primitives;
* atomic-vs-non-atomic on the refcount header remains a separate
* future decision per Decision 10's "Does not commit to atomic
* refcounts" clause.
*/
#include <stdatomic.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
@@ -66,7 +74,7 @@ static inline ailang_rc_header_t *header_of(void *payload) {
/* ---------------------------------------------------------------------------
* Iter 18g.0: opt-in alloc/free stats counter.
*
* Two non-atomic 64-bit counters incremented from `ailang_rc_alloc` and
* Two atomic 64-bit counters incremented from `ailang_rc_alloc` and
* the to-zero branch of `ailang_rc_dec`. Their difference is the live
* cell count at any point in execution; at program exit a non-zero
* difference is a leak.
@@ -82,13 +90,22 @@ static inline ailang_rc_header_t *header_of(void *payload) {
* and the bench numbers in JOURNAL 18f.2 were taken with the counters
* compiled in but disabled, so the figures are still valid.
*
* Single-threaded; non-atomic. Same scope as the rest of `runtime/rc.c`.
* The counters are intentionally not exposed via FFI symbols — the
* Atomic (relaxed): a multi-threaded host can drive the null-ctx
* fallback path concurrently, so these two counters use
* `atomic_fetch_add_explicit(.., memory_order_relaxed)` on increment
* and `atomic_load_explicit(.., memory_order_relaxed)` on read.
* Relaxed is correct and sufficient: the counters are pure
* statistics with no happens-before obligation, and the atexit
* reader runs after all threads have joined (single-threaded at
* exit). The per-ctx counters and the refcount header stay
* non-atomic by design (single-thread-per-ctx; boxes never cross
* threads). The counters are intentionally not exposed via FFI
* symbols — the
* env-var-gated atexit print is the supported readback path, and that
* is sufficient for the e2e leak tests that consume the diagnostic.
* --------------------------------------------------------------------------- */
static uint64_t g_rc_alloc_count = 0;
static uint64_t g_rc_free_count = 0;
static _Atomic uint64_t g_rc_alloc_count = 0;
static _Atomic uint64_t g_rc_free_count = 0;
/* M2: per-thread embedding context. A scalar kernel allocates nothing,
* so this carries only the de-globalised RC accounting. Set by the
@@ -122,11 +139,15 @@ void ailang_ctx_free(ailang_ctx_t *ctx) {
}
static void ailang_rc_stats_atexit(void) {
uint64_t allocs =
atomic_load_explicit(&g_rc_alloc_count, memory_order_relaxed);
uint64_t frees =
atomic_load_explicit(&g_rc_free_count, memory_order_relaxed);
fprintf(stderr,
"ailang_rc_stats: allocs=%llu frees=%llu live=%lld\n",
(unsigned long long)g_rc_alloc_count,
(unsigned long long)g_rc_free_count,
(long long)(g_rc_alloc_count - g_rc_free_count));
(unsigned long long)allocs,
(unsigned long long)frees,
(long long)(allocs - frees));
}
__attribute__((constructor))
@@ -158,7 +179,8 @@ void *ailang_rc_alloc(size_t size) {
void *payload = (uint8_t *)block + HEADER_SIZE;
memset(payload, 0, size);
ailang_ctx_t *_ctx = __ail_tls_ctx;
if (_ctx != NULL) _ctx->alloc_count++; else g_rc_alloc_count++;
if (_ctx != NULL) _ctx->alloc_count++;
else atomic_fetch_add_explicit(&g_rc_alloc_count, 1, memory_order_relaxed);
return payload;
}
@@ -209,7 +231,8 @@ void ailang_rc_dec(void *payload) {
if (*hdr == 0) {
free(hdr);
ailang_ctx_t *_ctx = __ail_tls_ctx;
if (_ctx != NULL) _ctx->free_count++; else g_rc_free_count++;
if (_ctx != NULL) _ctx->free_count++;
else atomic_fetch_add_explicit(&g_rc_free_count, 1, memory_order_relaxed);
}
}