From fc5f4590f81b5eecfea3c51e005bfec6bc8623ae Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 8 May 2026 14:18:02 +0200 Subject: [PATCH] rc: opt-in alloc/free stats counter for diagnosing leaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- runtime/rc.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/runtime/rc.c b/runtime/rc.c index 5bcb381..c1bfbf5 100644 --- a/runtime/rc.c +++ b/runtime/rc.c @@ -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++; } }