# iter bugfix-rc-global-stats-race — atomic global RC-stats fallback counters

**Date:** 2026-05-19
**Started from:** 427b687b9588ac8e1f96070ffc20f2644e9cd28c
**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
  in `ailang_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. RED `embed_rc_global_stats_race`
  went GREEN deterministically (3/3 runs, allocs == frees ==
  16_000_000); full `cargo test -p ail` green (39/39 result lines
  ok, all embed_* incl. the per-ctx tsan harnesses unaffected).

## Concerns

(none)

## Known debt

- `runtime/rc.c` comment 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
