/* 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); } } /* --------------------------------------------------------------------------- * Iter 18e: drop worklist. * * Backs the `(drop-iterative)` data attribute. When a type is annotated * `(drop-iterative)`, codegen emits `drop__` with an iterative-with- * worklist body in place of the recursive cascade. The worklist is a * heap-allocated stretchy buffer of `void*` pointers — one entry per * not-yet-processed cell. Each entry is mono-typed to T (the annotated * ADT being dropped); fields of T whose type is `T` itself are pushed, * fields whose type is a different ADT call that ADT's drop fn directly. * (See `Emitter::emit_iterative_drop_fn_for_type` in the codegen for the * IR shape and the same-type / different-type dispatch.) * * Strategy: heap-allocated buffer, doubled on overflow. We chose this * over a stack-allocated small buffer (overcomplicates the IR seam — the * codegen body would need to track "which buffer is live") and over Lean * 4's "thread the worklist through one of the cell's own pointer slots" * technique (requires the codegen to know which slot of each ctor is * "free to repurpose" — non-trivial since AILang ctors are heterogeneous * and slot 0 is always the tag). The runtime-helper approach keeps the * IR-level body of `drop__` small: three calls (new / push / pop / * free) drive the loop. * * Precedent: Lean 4's `lean_dec_ref_cold` and Roc's iterative-free path * both use a worklist to break tail recursion in their drop cascades. * Lean threads the worklist through field slots (the "in-place" variant); * we use a separate heap buffer because AILang's ctor layout makes slot * repurposing fragile. The semantic invariant matches: every cell whose * refcount reaches zero is dec'd exactly once, regardless of cascade * depth, without consuming proportional C stack space. * * Single-threaded; non-atomic. Same scope as the rest of `runtime/rc.c`. * --------------------------------------------------------------------------- */ typedef struct { void **data; /* heap buffer of `cap` pointers; null once freed */ size_t len; /* number of live entries (always <= cap) */ size_t cap; /* current capacity in slots */ } ailang_drop_worklist_t; /* Initial capacity. 16 slots * 8 bytes = 128 bytes — small enough that * very-shallow drops don't waste memory, large enough that 16-deep * cascades (very common) never realloc. Doubled on overflow. */ #define DROP_WORKLIST_INIT_CAP ((size_t)16) void *ailang_drop_worklist_new(void) { ailang_drop_worklist_t *wl = malloc(sizeof(ailang_drop_worklist_t)); if (wl == NULL) { fprintf(stderr, "ailang_drop_worklist_new: out of memory (header)\n"); abort(); } wl->data = malloc(DROP_WORKLIST_INIT_CAP * sizeof(void *)); if (wl->data == NULL) { fprintf(stderr, "ailang_drop_worklist_new: out of memory (initial buffer)\n"); abort(); } wl->len = 0; wl->cap = DROP_WORKLIST_INIT_CAP; return (void *)wl; } /* Push `payload` onto the worklist. Skips null payloads — pushed nulls * would dispatch on `load i64, ptr null` at pop time and segfault, so * we filter here. The check is symmetric with `ailang_rc_dec`'s null * guard (a null payload is a no-op everywhere in the rc runtime). */ void ailang_drop_worklist_push(void *wl_opaque, void *payload) { if (payload == NULL) { return; } ailang_drop_worklist_t *wl = (ailang_drop_worklist_t *)wl_opaque; if (wl->len == wl->cap) { size_t new_cap = wl->cap * 2; void **new_data = realloc(wl->data, new_cap * sizeof(void *)); if (new_data == NULL) { fprintf(stderr, "ailang_drop_worklist_push: out of memory (grow to %zu slots)\n", new_cap); abort(); } wl->data = new_data; wl->cap = new_cap; } wl->data[wl->len] = payload; wl->len += 1; } /* Pop one payload from the worklist. Returns NULL when the worklist is * empty. Since `push` filters nulls, a returned null is unambiguous and * can be used by the IR body as the loop-exit sentinel. */ void *ailang_drop_worklist_pop(void *wl_opaque) { ailang_drop_worklist_t *wl = (ailang_drop_worklist_t *)wl_opaque; if (wl->len == 0) { return NULL; } wl->len -= 1; return wl->data[wl->len]; } /* Free the worklist itself. Called once at the end of the iterative * drop loop. Does NOT free any payloads still in the buffer — the IR * body must drain the buffer first via repeated `pop` calls before * calling free. */ void ailang_drop_worklist_free(void *wl_opaque) { if (wl_opaque == NULL) { return; } ailang_drop_worklist_t *wl = (ailang_drop_worklist_t *)wl_opaque; free(wl->data); free(wl); }