/* AILang reference-counting runtime — Iter 18b. * * This is the allocator + counter primitives for `ail build --memory=rc`. * It establishes the memory layout (8-byte refcount header preceding * every allocation) and the three runtime entry points the codegen will * eventually call: ailang_rc_alloc / ailang_rc_inc / ailang_rc_dec. * * Iter 18b deliberately stops at the *layout* and the *alloc*. The * codegen routes `Term::Ctor` / `Term::Lam` env / closure-pair sites * through `ailang_rc_alloc` instead of `GC_malloc` / `bump_malloc`, but * does NOT yet emit `inc` or `dec` calls anywhere. Programs running * under `--memory=rc` therefore leak every allocation — the same * behaviour as the pre-Boehm era. This is intentional: the next iter * (18c) ships uniqueness inference and the codegen pass that emits * inc/dec. 18b is purely about plumbing the allocator and validating * that compiled programs still produce correct output under the new * allocator. * * Layout: * * high address ┐ * │ payload (size bytes, 8-byte aligned) * ┤ ← returned pointer (`p`) * │ uint64_t refcount ← header (8 bytes) * low address ┘ ← ailang_rc_alloc's internal allocation * * The returned pointer points to the *payload*. The header is at * `p - 8`. Codegen treats the returned pointer exactly like a * `GC_malloc`-returned pointer; it stores the ADT tag at offset 0, * fields from offset 8, env-cells from offset 0 in lambda envs, etc. * * 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). */ #include #include #include #include #include /* Header lives in the 8 bytes preceding every payload. */ typedef uint64_t ailang_rc_header_t; #define HEADER_SIZE ((size_t)sizeof(ailang_rc_header_t)) static inline ailang_rc_header_t *header_of(void *payload) { return (ailang_rc_header_t *)((uint8_t *)payload - HEADER_SIZE); } /* Allocate `size` bytes of payload, prefixed by an 8-byte refcount * header initialised to 1. Returns a pointer to the payload. * * Aborts on out-of-memory; AILang has no exception machinery yet, and * Boehm's behaviour on OOM is also "abort", so this matches. * * Zero-initialises the payload to match `GC_malloc`'s contract — codegen * may rely on uninitialised fields reading as zero in some paths. */ void *ailang_rc_alloc(size_t size) { void *block = malloc(HEADER_SIZE + size); if (block == NULL) { fprintf(stderr, "ailang_rc_alloc: out of memory (requested payload %zu bytes)\n", size); abort(); } ailang_rc_header_t *hdr = (ailang_rc_header_t *)block; *hdr = 1; void *payload = (uint8_t *)block + HEADER_SIZE; memset(payload, 0, size); return payload; } /* Refcount += 1. No-op on null (codegen never asks for inc on a known- * null pointer, but defensive — top-level fn-value pointers may be * null-env closure pairs in static memory which must not be incremented). */ void ailang_rc_inc(void *payload) { if (payload == NULL) { return; } /* Heuristic for "static, do not touch": the static closure-pair env * pointers (Iter 8b) live in the LLVM data segment, not in heap * memory we allocated. We cannot trivially distinguish them at * runtime without a flag bit; for Iter 18b, we accept that inc on * static memory is undefined behaviour. Iter 18c's codegen will * elide inc/dec for known-static pointers, so this path will not * be reached for them in practice. */ ailang_rc_header_t *hdr = header_of(payload); *hdr += 1; } /* Refcount -= 1. If it reaches zero, frees the underlying block. * * Iter 18b deliberately does NOT recursively dec child references. * That requires per-type traversal info (which fields are pointer- * typed, which are unboxed), which is added in Iter 18c when the * codegen learns to emit per-ctor `dec` cascades. For now, free-on- * zero just frees the box; any boxed children leak. * * Iter 18b never emits `dec` calls from codegen, so this fn is * effectively dead code in 18b. It exists so the runtime ABI is * complete and 18c can wire codegen up against a stable surface. */ void ailang_rc_dec(void *payload) { if (payload == NULL) { return; } ailang_rc_header_t *hdr = header_of(payload); if (*hdr == 0) { fprintf(stderr, "ailang_rc_dec: refcount underflow at %p (already zero)\n", payload); abort(); } *hdr -= 1; if (*hdr == 0) { free(hdr); } }