From 5cd0a3400ac99ea0beb3dc114588bd5876f09768 Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 8 May 2026 02:14:40 +0200 Subject: [PATCH] =?UTF-8?q?JOURNAL:=20Iter=2018b=20=E2=80=94=20RC=20runtim?= =?UTF-8?q?e=20+=20--alloc=3Drc=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records that the codegen change was a one-line extension because the 18a bump path had centralised the allocator-symbol decision on fn_name(). Documents the runtime ABI (8-byte refcount header, ailang_rc_alloc/inc/dec) and the deliberate no-inc/dec posture: programs leak intentionally under --alloc=rc; 18c lights up the actual reference counting. Next-step plan for 18c is split into three sub-iters (clone schema / linearity check / inference + codegen) since each wants its own verification cycle. --- docs/JOURNAL.md | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index ec6c60c..bf112d7 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -5837,3 +5837,149 @@ emitted yet; programs leak intentionally. The point is to validate that compiled programs run correctly under the new allocator before 18c adds the inc/dec instrumentation that gives the runtime contract its teeth. + +## Iter 18b — RC runtime (`runtime/rc.c`) + codegen `--alloc=rc` routing + +Pure plumbing iter. Establishes the RC runtime ABI and wires +`--alloc=rc` through the codegen + linker, but does NOT emit +any `inc` or `dec` calls. Programs running under `--alloc=rc` +allocate via `ailang_rc_alloc` and never free — the same +behaviour as pre-Boehm AILang. The point is to validate that +compiled programs still produce correct stdout under the new +allocator before 18c lights up the actual reference counting. + +### Runtime ABI (`runtime/rc.c`) + +Memory layout: an 8-byte `uint64_t` refcount header is prepended +to every payload. `ailang_rc_alloc(size)` allocates `size + 8` +bytes via libc `malloc`, sets the header to `1`, zero-initialises +the payload (matching `GC_malloc`'s contract), and returns a +pointer to the *payload*. The header is at `payload - 8`. + +`ailang_rc_inc(p)` increments the header at `p - 8`. +`ailang_rc_dec(p)` decrements; on zero-refcount it `free()`s +the underlying block. **Crucially**, dec does NOT recursively +dec child references in 18b — that requires per-type field- +layout info, which is 18c's job to wire up. For 18b, both `inc` +and `dec` are dead code from codegen's perspective; the codegen +emits zero calls to either. They exist as ABI placeholders so +18c can wire codegen against a stable surface. + +The runtime is single-threaded (counter ops are non-atomic); +when AILang acquires concurrency primitives, atomic-vs-non- +atomic becomes a separate decision per allocation kind, per +the "Does not commit to atomic refcounts" clause in Decision 10. + +### Codegen + +`AllocStrategy::Rc` was a one-line addition to the existing +enum. `fn_name()` returns `"ailang_rc_alloc"` for it. The rest +of the codegen — the `declare ptr @(i64)` line, the four +allocation sites (`lower_ctor`, lambda env, closure pair) — is +already parameterised on `alloc.fn_name()` from the bump-bench +work, so adding a third strategy required zero further codegen +edits. This is the "Iter 18a's bump path centralised the +allocator decision; 18b just adds a new value" payoff that the +implementer flagged for the JOURNAL. + +### CLI + +`--alloc=rc` is accepted by both `Build` and `Run`. +`locate_rc_runtime()` mirrors `locate_bump_runtime` — searches +upward from the binary and from CWD for `runtime/rc.c`. The +link arm compiles `runtime/rc.c` with `clang -O2 -c` and passes +the resulting `.o` to the main link command. **No `-lgc`** — +rc.c uses libc only. + +### E2E coverage + +Two new tests: + +- `alloc_rc_produces_same_stdout_as_gc` builds and runs + `list.ail.json` under both `--alloc=gc` and `--alloc=rc`, + asserts they produce identical stdout (`42`). +- `alloc_rc_matches_gc_on_std_list_demo` does the same for + `std_list_demo.ail.json` — broader allocation coverage + (length / sum / reverse / take/drop chained), every fold + goes through `ailang_rc_alloc`. + +E2E bundle: 51 tests (was 49 pre-18b, 50 with the 18a addition). +`cargo build/test --workspace` green. `git diff examples/` +empty — no fixture changes. + +### Hand-tested + +``` +$ cargo run --bin ail -- run examples/sum.ail.json --alloc=rc +55 + +$ cargo run --bin ail -- run examples/list.ail.json --alloc=rc +42 + +$ cargo run --bin ail -- run examples/borrow_own_demo.ail.json --alloc=rc +3 +6 + +$ cargo run --bin ail -- run examples/std_list_demo.ail.json --alloc=rc +... (matches --alloc=gc) +``` + +All correct. Programs allocate, produce output, exit. Memory +leaks all the way through (no `dec` is ever emitted), but at +the bench/test scales we run, the leak is bounded (<100 MB) and +the OS reclaims on process exit. + +### Did anything surprise + +- **Codegen change was a single line.** The 18a bump path had + already done the heavy lifting of centralising the allocator- + symbol decision on a single `fn_name()` method; 18b just adds + another arm to that match. This is the kind of compounding + return that pays for the bench iter retroactively — it built + the abstraction, 18b reuses it for free. + +- **No need for a `Type::fn_implicit` migration in 18b.** The + 18a JOURNAL flagged this as a possible cleanup; 18b confirmed + it's not blocking anything. The padding-on-read trick handles + every codegen-side construction site silently. Cleanup + remains optional, deferred to 18c if it has a reason. + +- **Boehm and RC have nearly the same emitted IR.** The two + binaries differ only in (a) the `declare ptr @(i64)` + symbol name and (b) the linker argument (`-lgc` vs. + `runtime/rc.o`). Allocation behaviour at the source level is + identical from codegen's perspective. This is what we wanted + — 18c's inc/dec emission can be added orthogonally without + re-architecting the allocation pipeline. + +### Next + +Iter 18c is the substantive next step. Three sub-pieces: + +1. **`Term::Clone { value }`** as a new schema variant. Author- + spelled explicit RC inc, like Lean 4's `(@Clone)` syntax. + Schema + parser + printer + typechecker passthrough. + +2. **Linearity check** as a new pass over the typechecked AST. + For functions with all-explicit-mode params, verify each + binder is consumed exactly once (or borrowed indefinitely). + Use-after-consume → structured diagnostic with + `suggested_rewrites`. Functions with any `Implicit` param + are exempt — that's the back-compat lane while existing + fixtures stay unannotated. + +3. **Uniqueness inference + codegen inc/dec emission.** + Post-typecheck dataflow over AST builds a side table + `BTreeMap`. Codegen consumes the table: + on every `Term::Var` of a shared reference that escapes its + binder, emit `call void @ailang_rc_inc(ptr %p)`. On every + binder going out of scope, emit `call void @ailang_rc_dec`. + The inference erases inc/dec on provably unique references + — that's the optimisation that closes the bump-allocator + gap on hot loops. + +18c is at least a 3-agent-run iter; tackle it as 18c.1 (clone +schema), 18c.2 (linearity check + suggested_rewrites), 18c.3 +(inference + codegen) sequentially. Don't attempt all three in +one pass — each builds on the previous and wants its own +verification cycle.