rc: opt-in alloc/free stats counter for diagnosing leaks

Two non-atomic uint64_t counters in runtime/rc.c, incremented from
ailang_rc_alloc and the to-zero branch of ailang_rc_dec. An
__attribute__((constructor)) registers an atexit handler IFF the
AILANG_RC_STATS env var is non-empty at startup; the handler prints

    ailang_rc_stats: allocs=N frees=M live=K

to stderr. Default-disabled so production binaries stay quiet.

Used by the e2e test infrastructure for assertions about RC
correctness (e.g. tail-recursive list-sum must not leak outer cells)
and by the bencher / debugger when diagnosing leak shape from a
fixture run. Single-threaded; non-atomic — same scope as the rest
of runtime/rc.c. The two unconditional increments on the hot paths
are negligible relative to the libc malloc/free already there.
This commit is contained in:
2026-05-08 14:18:02 +02:00
parent bb7611f85e
commit fc5f4590f8
+45
View File
@@ -59,6 +59,49 @@ static inline ailang_rc_header_t *header_of(void *payload) {
return (ailang_rc_header_t *)((uint8_t *)payload - HEADER_SIZE);
}
/* ---------------------------------------------------------------------------
* Iter 18g.0: opt-in alloc/free stats counter.
*
* Two non-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.
*
* Output is gated by the `AILANG_RC_STATS` env var: when set to a
* non-empty value, an `atexit` handler prints
*
* ailang_rc_stats: allocs=N frees=M live=K
*
* to stderr. The default-disabled path keeps production binaries quiet
* and adds only two unconditional `++` operations to the hot path —
* negligible relative to the libc malloc/free already on each path,
* 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
* 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 void ailang_rc_stats_atexit(void) {
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));
}
__attribute__((constructor))
static void ailang_rc_stats_install(void) {
const char *flag = getenv("AILANG_RC_STATS");
if (flag != NULL && flag[0] != '\0') {
atexit(ailang_rc_stats_atexit);
}
}
/* Allocate `size` bytes of payload, prefixed by an 8-byte refcount
* header initialised to 1. Returns a pointer to the payload.
*
@@ -79,6 +122,7 @@ void *ailang_rc_alloc(size_t size) {
*hdr = 1;
void *payload = (uint8_t *)block + HEADER_SIZE;
memset(payload, 0, size);
g_rc_alloc_count++;
return payload;
}
@@ -128,6 +172,7 @@ void ailang_rc_dec(void *payload) {
*hdr -= 1;
if (*hdr == 0) {
free(hdr);
g_rc_free_count++;
}
}