1eed78c41e
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.
121 lines
4.7 KiB
C
121 lines
4.7 KiB
C
/* 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);
|
|
}
|
|
}
|