Iter 18b: RC runtime + --alloc=rc routing

Wires up reference-counting allocator end-to-end without any
inc/dec emission. Programs run under --alloc=rc and produce
correct stdout (validated against --alloc=gc); they leak every
allocation, exactly like the pre-Boehm era. The point of 18b is
to establish the runtime contract before 18c adds the
inc/dec-emission codegen pass.

runtime/rc.c — new file. 8-byte uint64 refcount header
prepended to every payload; ailang_rc_alloc(size) returns a
ptr to the payload (header at ptr-8). ailang_rc_inc / dec are
declared but never called by codegen yet; they exist so 18c
can wire codegen against a stable runtime ABI. dec frees on
zero refcount but does NOT recursively dec child references —
that's 18c's job once it has per-ctor type info.

crates/ailang-codegen/src/lib.rs — AllocStrategy::Rc variant
added; fn_name() returns "ailang_rc_alloc". Single-line
extension because Iter 18a's bump path had already centralised
the allocator-symbol decision on fn_name() for all four
allocation sites.

crates/ail/src/main.rs — --alloc=rc accepted by Build/Run;
parse_alloc_strategy extended; locate_rc_runtime() helper
mirrors locate_bump_runtime; new Rc arm in build_to compiles
runtime/rc.c with clang -O2 -c and links the resulting .o
into the final binary (no -lgc).

E2E coverage: alloc_rc_produces_same_stdout_as_gc on
list.ail.json (42), alloc_rc_matches_gc_on_std_list_demo for
broader allocation-site coverage. Total e2e bundle: 51 tests
(was 49).

Hand-verified on:
  sum.ail.json --alloc=rc → 55
  list.ail.json --alloc=rc → 42
  borrow_own_demo.ail.json --alloc=rc → 3 then 6
  std_list_demo.ail.json --alloc=rc → matches --alloc=gc

cargo build/test --workspace green; git diff examples/ empty.
This commit is contained in:
2026-05-08 02:13:08 +02:00
parent 1e3697926b
commit 1eed78c41e
4 changed files with 271 additions and 5 deletions
+120
View File
@@ -0,0 +1,120 @@
/* 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 <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 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);
}
}