From e3ecf4f0cdef44cf2645ebf8e59bc9789da6e1b0 Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 8 May 2026 01:52:40 +0200 Subject: [PATCH] =?UTF-8?q?DESIGN/JOURNAL:=20Decision=2010=20reframed=20?= =?UTF-8?q?=E2=80=94=20RC=20+=20LLM-author=20annotations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the earlier "RC + inference, no annotations" position with a two-layer architecture: inference (intra-fn) + mandatory LLM-author mode annotations on fn signatures (inter-fn). Adds structured rationale for rejecting region inference (regions assume stack-shaped lifetimes; real programs need non-stack-shaped lifetimes for caches / memo tables) and linear types as primary mechanism. Schema additions enumerated for the 18-series: Type::Borrow / Type::Own (Iter 18a) Term::Clone (Iter 18c) Term::ReuseAs (Iter 18d) TypeDef.drop_iterative (Iter 18e) Migration plan extended from 18a-d (4 iters) to 18a-f (6 iters). JOURNAL records the regions excursion, the scope-shape reduction, and the LLM-author lever as the structural advantage Decision 10 cashes in on. --- docs/DESIGN.md | 344 +++++++++++++++++++++++++++++++++++++++++++++++- docs/JOURNAL.md | 288 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 631 insertions(+), 1 deletion(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 15b6409..ce8a52c 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -543,7 +543,15 @@ constructor-blocked recursions in `map`, `sort`, `insert` remain unmarked — they cannot benefit from `musttail` without a source-level rewrite. -## Decision 9: memory management — Boehm conservative GC +## Decision 9: transitional allocator — Boehm conservative GC + +**Status: superseded by Decision 10 (RC + Uniqueness, 2026-05-08).** +This section documents the active runtime as of Iter 17a / the +GC bench, but it is no longer the canonical memory model. Boehm +remains linked behind `--alloc=gc` (the current default) until +the RC pipeline (Iters 18a–18d) is validated and the default +flips. The `--alloc=bump` mode introduced for the bench is a +measurement tool, not a production target. Through Iter 14e, every ADT box, lambda env, and closure pair was allocated with bare `malloc` and never freed. That worked for the @@ -660,6 +668,340 @@ The closure-pair and its env share an escape verdict — they have parallel lifetimes. If the closure pair is non-escaping, the env is too. +## Decision 10: memory model — RC + Uniqueness with LLM-author annotations + +**Committed 2026-05-08, after the GC bench (`bench/run.sh`) showed +Boehm contributing ~60% of runtime on allocation-heavy workloads +that hold the heap fully live. Sharpened later the same day: the +mainstream "RC + inference" position was extended with mandatory +LLM-author mode annotations (`borrow` / `own`), explicit `clone`, +first-class `reuse-as`, and `drop-iterative` data attrs.** + +The cost of GC is structurally in the allocate path — Boehm's +`GC_malloc` is ~2.8× slower than a bump pointer, and the bench +workloads exercised allocate cost without collection cost. +Tracing GC's irreducible variability cannot be tuned away; RC's +costs are bounded and analysable per program point. A corpus +committed to one memory model is expensive to switch — pre- +stdlib is the cheapest moment to commit. + +**Choice.** AILang's canonical memory model is reference counting +with static uniqueness inference **and explicit LLM-author +annotations on fn signatures**, in the lineage of Lean 4 / Roc / +Koka. Boehm becomes a transitional allocator (Decision 9) and is +retired when the RC pipeline matches the bump-allocator floor +within an acceptable margin (target 1.3× on `bench/run.sh`). + +The architecture has two layers: + +1. **Inference.** A post-typecheck pass produces a per-node + uniqueness side table. Codegen uses it to elide inc/dec + wherever provably redundant. +2. **LLM-author annotations.** Fn signatures carry mandatory + `(borrow T)` / `(own T)` mode markers. Authors mark + sharing-vs-consumption explicitly. The compiler verifies + rather than guesses. + +The combination plays to what LLMs are good at (writing slightly +more annotation per definition) and avoids what compilers are +bad at (proving sharing absent in the face of recursion + +closures + match). + +### Why not other memory models + +**Tracing GC.** Open-ended. Boehm's bench number (~60% allocate- +path overhead) is structural; tuning Boehm cannot move that +needle. A precise tracing GC would need read/write barriers, +root maps, generational machinery — months of work, with +irreducible pause-time variability at the end. The user's +framing was decisive: "Am GC kannst du ewig rumschrauben (und +bekommst trotzdem auch in 100 Jahren keine berechnbare +Performance)." + +**Region inference (Tofte/Talpin / MLton-style).** Considered +seriously; rejected. Regions tie lifetimes to dynamic scope — +every value lives in some region, regions stack on entry/exit, +deallocation is bulk-by-region. The reduction: a region is an +explicit allocator with sugar plus a static check that nothing +escapes its lifetime. The deeper problem: regions assume +**stack-shaped lifetimes**. Real programs have non-stack-shaped +lifetimes — caches, memo tables, registries, lookup structures +whose lifetimes are not nested. Regions would either force these +into a `letregion` at the top of `main` (everyone's allocator +becomes the root region — useless), or require a region per +cache variant (combinatorial). RC has no lifetime-shape +assumption; it works for any DAG. + +**Linear / ownership types as primary mechanism (Rust-style).** +Considered as a general-purpose alternative; rejected as primary. +Rust's borrow-checker is a great mechanism but requires the +author to thread lifetimes through every signature and accept +that some programs cannot be expressed without `unsafe`. For an +LLM-targeted language with recursion + closures everywhere, that +cost is too high — most of the source surface would be lifetime +annotations rather than logic. AILang uses a *subset* of these +ideas (mode annotations, linear consumption discipline) +selectively, layered on top of RC, where the annotations buy +concrete optimisations and never have to thread lifetimes +through callees. + +### The LLM-aware sharpening + +A mainstream RC implementation (think Lean 4 in default mode) +infers everything from naked AST plus a few optional hints. The +inference is conservative; whatever it can't prove unique becomes +shared and pays runtime inc/dec. AILang exploits its target +audience to push that conservative ceiling higher. + +Five mechanisms. + +**(1) Mandatory `(borrow T)` / `(own T)` on fn signatures.** + +``` +(fn list_length + (type (fn-type (params (borrow (List Int))) (ret (con Int)))) + ...) + +(fn sum_list_consume + (type (fn-type (params (own (List Int))) (ret (con Int)))) + ...) +``` + +`(borrow T)` declares the parameter is read-only and lives at +most until the call returns; the caller still owns it; the +callee performs no inc/dec on it. `(own T)` declares ownership +transfer; the callee consumes the value and is responsible for +its end-of-life. The declaration is structural (visible in JSON) +and binding (the typechecker rejects bodies that contradict it). + +For a Lean 4 / Roc author this is all *optional* and inferred +when omitted. AILang makes it mandatory because the LLM author +can carry the cognitive cost trivially, and the compiler gains a +precise contract at every call site instead of a probabilistic +guess. + +**(2) Linear-by-default consumption with explicit `(clone X)`.** + +In bodies, every binder is consumed by exactly one `own`-mode +use. If the LLM writes: + +``` +(let p (expensive_fn x) + (let r1 (consume_a p) ; consume_a takes (own); consumes p + (let r2 (consume_b p) ; ERROR: p already consumed + ...))) +``` + +the compiler emits a structured non-linear-use diagnostic with +concrete `suggested_rewrites`: + +- "make consume_a borrow": refactor consume_a's signature, no + body change at the call site; +- "explicit clone": insert `(clone p)` at the first use; +- "fuse traversal": replace the two separate calls with a fused fn. + +The LLM picks one. There is no implicit clone — sharing always +costs visible source. + +**(3) Reuse hints as first-class.** + +``` +(fn map_inc + (type (fn-type (params (own (List Int))) (ret (own (List Int))))) + (params xs) + (body + (match xs + (case Nil Nil) + (case (Cons h t) + (reuse-as xs (term-ctor List Cons (app + h 1) (app map_inc t))))))) +``` + +`(reuse-as SRC NEW-CTOR)` asks the codegen to allocate `NEW-CTOR` +in `SRC`'s memory slot. Compiler verifies: `SRC` is owned, this +is its last use, sizes match. On a hit: no malloc, no free — the +box is overwritten in place. On a miss: structured diagnostic +explains which precondition failed; the LLM either adjusts the +surrounding code or removes the hint. + +This matches Lean 4 / Roc reuse analysis but lifts it from +"compiler-inferred when possible" to "author-asserted, compiler- +verified". The LLM applies it everywhere it expects to fire and +lets the compiler bounce the request when it can't. + +**(4) `(drop-iterative)` annotation on data declarations.** + +``` +(data Tree (vars a) + (ctor Leaf) + (ctor Node a (Tree a) (Tree a)) + (drop-iterative)) +``` + +When the refcount of a `Tree` value reaches zero, the synthesised +dec-on-zero traversal is iterative (worklist + heap-allocated +stack) instead of recursive. Avoids stack overflow on deep +structures. The LLM adds the annotation where appropriate; the +compiler refuses to emit recursive dec-cascade on annotated types. + +**(5) Structured compiler diagnostics with `suggested_rewrites`.** + +Every RC-mode error (use-after-consume, mode-mismatch, +reuse-as-fail, drop-cascade-too-deep) emits a JSON object +containing the failure kind, the source span, and a list of +concrete rewrite suggestions in form-A AILang. The LLM consumes +these without prose-parsing. This is the missing half of the +LLM-as-author story: the language spec defines not only what +compiles, but what the compiler tells the author when it doesn't. + +### Language-design constraints (binding) + +The four constraints below are necessary preconditions for RC to +be sound and complete *without* a cycle-collector backstop. They +are not new — AILang already satisfies all four — but Decision 10 +makes them load-bearing rather than incidental: + +1. **Strict evaluation.** Every `Term::App` argument is fully + evaluated before the call. No laziness, no thunks. (Already + true.) +2. **No recursive value bindings.** `(let x EXPR ...)` evaluates + `EXPR` in a scope where `x` is *not* bound. Recursion is + exclusively via `Term::LetRec` (which binds a fn, not a value) + and module-level fn defs. (Already true.) +3. **No shared mutable refs.** Values are immutable once + constructed. There is no `ref`, `IORef`, `Mutex`, or any + primitive that allows a value to be mutated from a position + outside its allocation. (Already true.) +4. **ADTs are acyclic by construction.** Strict evaluation + + no-recursive-value-bindings + no-shared-mutable-refs together + guarantee that any value graph reachable from a binding is a + DAG. The reference graph has no cycles. (Follows from 1–3.) + +A future iter that proposes any of laziness, recursive value +bindings, shared mutable state, or any feature that creates +cycles must either prove the cycle is collectible by an +extension (e.g. linear ownership) or be rejected at design time. + +### Schema additions + +**Iter 18a — mode wrappers on `Type`.** + +``` +Type::Borrow(Box) ; `(borrow T)` in form-A +Type::Own(Box) ; `(own T)` in form-A +``` + +Both are transparent for unification (they erase to their inner +`Type` for HM purposes) but carry binding mode metadata for the +RC pipeline. Skipped during JSON serialisation when absent so +canonical hashes of every pre-18a fixture stay bit-identical. +The legacy `(con T)` form is treated as `(own T)` semantically +throughout the 18-series; a later iter (deferred) makes the +explicit annotation mandatory and rejects bare `(con T)` for +boxed parameter types. + +**Iter 18c/18d — new `Term` variants.** + +``` +Term::Clone { value: Box } ; `(clone X)` — explicit RC inc +Term::ReuseAs { source: Box, body: Box } ; `(reuse-as SRC NEW-CTOR)` +``` + +**Iter 18e — `TypeDef` attribute.** + +``` +TypeDef.drop_iterative: bool ; `(drop-iterative)` +``` + +All four are skipped during serialisation when absent / false / +None so canonical-JSON hashes of every fixture remain stable +until the fixture intentionally adopts the feature. + +### Inference algorithm (Iter 18c) + +Post-typecheck, post-`lift_letrecs`, pre-codegen pass over the +elaborated module. For each `Term` node that produces or binds a +boxed value, the pass computes a uniqueness flag: + +- **Unique:** at this program point, the reference is the only + outstanding reference to its referent. +- **Shared:** there may be multiple outstanding references. + +A reference is *unique* if every path from its allocation to the +current program point passes through exactly one binding. The +inference is a forward dataflow over the AST. The annotations +from 18a (`borrow` / `own`) provide the inter-fn contract; the +inference fills in intra-fn detail. (Implementer-level detail in +the Iter 18c brief.) + +### Codegen contract (Iter 18b, 18c) + +Memory layout (Iter 18b): + +- Every heap allocation has an 8-byte refcount header, followed + by the payload. `ailang_rc_alloc(size)` returns a pointer to + the *payload*; the header is at `ptr - 8`. +- `ailang_rc_inc(ptr)`: load `ptr - 8`, +1, store. Non-atomic + (single-threaded). +- `ailang_rc_dec(ptr)`: load, -1, store; if zero, recurse-dec + child references and `free(ptr - 8)`. For `(drop-iterative)` + types, the recursion is replaced by a worklist loop (Iter 18e). + +Codegen for `Term::Ctor` / `Term::Lam` env / closure pair under +`--memory=rc` calls `ailang_rc_alloc(SIZE)`. Iter 18b stops there +— inc/dec instrumentation is added in Iter 18c, once the +inference is wired up. Until then, `--memory=rc` deliberately +leaks like the pre-Boehm era; this is purely about plumbing. + +### Migration plan + +1. **Iter 18a:** `(borrow T)` / `(own T)` annotations as a + language feature. Schema, parser, JSON, typechecker. No + codegen change. `(con T)` ≡ `(own T)`. Existing fixtures + unchanged. +2. **Iter 18b:** RC runtime. `runtime/rc.c` with header layout + + alloc/inc/dec. Codegen `--memory=rc` routes allocation through + `rc_alloc`; **no inc/dec yet**. `--memory=gc` remains default. +3. **Iter 18c:** uniqueness inference + codegen inc/dec + instrumentation. Combines 18a's annotations with intra-fn + dataflow. Linear-by-default enforcement turns on. `(clone X)` + in the `Term` schema. +4. **Iter 18d:** reuse hints + reuse analysis. `(reuse-as ...)` + form. Codegen rewrites dec+malloc into in-place overwrite when + the precondition holds. +5. **Iter 18e:** `(drop-iterative)` data attr. Worklist-based + free for annotated types. +6. **Iter 18f:** RC validation bench. If RC within 1.3× of bump + on `bench/run.sh`, retire Boehm: flip default to RC, drop + `-lgc`, mark Decision 9 historical. + +### Adjacent extensions for mutability (out of Decision 10's scope) + +If future workloads need mutable arrays, hash tables, or other +inherently mutable primitives, the answer is **not** a tracing GC +backstop. The answer is a separate ownership/linear extension +that gates mutability behind static single-owner discipline. RC ++ uniqueness is the universal floor; ownership extends it for +specific high-performance primitives without rebreaking the +acyclicity invariant. Concrete design deferred until the need +materialises with a real workload. + +### What this Decision deliberately does not do + +- **Does not infer everything.** An earlier draft of this + Decision said "uniqueness is fully inferred" — that was the + mainstream RC position. The 2026-05-08 follow-up discussion + replaced it: AILang demands annotations *because* the LLM + author can produce them effortlessly. The compiler does + inference *plus* verification of contracts. +- **Does not require existing fixtures to migrate immediately.** + Iter 18a treats `(con T)` as `(own T)`. Existing JSON hashes + stay bit-identical until the fixture is intentionally updated. +- **Does not commit to atomic refcounts.** AILang is currently + single-threaded. If/when concurrency arrives, atomic-vs-non- + atomic will be a separate decision per allocation kind. +- **Does not introduce regions.** Regions were considered and + rejected; see "Why not other memory models" above. + ## Mangling scheme (Iter 5c) All AILang functions are mangled to `@ail__` — even in the diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index 9b9df10..9ed5419 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -5322,3 +5322,291 @@ this firmly past that envelope. Caveat below. consequence of how AILang is structured: an LLM author who ports a textbook recursive sum into AILang at scale will hit the stack ceiling unless they know about Decision 8. + +## 2026-05-08 — GC discussion: commit to RC + Uniqueness + +The bench numbers (~60% Boehm share, all of it in the allocate +path) precipitated the memory-management conversation that has +been gated since Iter 17a. The user reframed the question +sharply: tracing GC has irreducible variability that no amount +of tuning eliminates; RC must be committed to **now or never**, +because every iter shipped under the wrong model adds debt that +gets more expensive to migrate. With pre-stdlib code volume, +this is the cheapest moment. + +**Decision: AILang's canonical memory model is RC + Uniqueness.** +See `Decision 10` in `docs/DESIGN.md` for the architectural +write-up. Boehm becomes a transitional allocator (Decision 9 is +now annotated as superseded). The migration runs as Iters +18a–18d; the default flips when RC is within 1.3× of the bump +floor on `bench/run.sh`. + +**Why RC and not (a) keep Boehm, (b) build a precise tracing GC, +(c) linear / ownership types:** + +- **Keep Boehm.** Bench data shows the cost is structural in the + allocate path. Tuning Boehm cannot change that; the + `GC_malloc` fast path is what it is. Predictable performance + is unobtainable. +- **Precise tracing GC.** Larger investment than RC (read/write + barriers, root maps, generational/copying machinery, multiple + pause budgets). Solves a problem we don't have (cycle handling) + at the cost of one we do (predictable allocate cost). The + AILang language has no cycles by construction; paying for a + cycle-collector backstop is unjustified. +- **Linear / ownership types.** Push the bookkeeping to the + source surface. The author has to mark uniqueness explicitly. + For an LLM-targeted language, that is the worst possible + choice — LLMs are weak at long-range ownership reasoning, and + the value of AILang's "compiler does the bookkeeping" + positioning is exactly that the author doesn't think about it. + +**Why now and not later** (the user's framing, accepted in full): + +- RC and tracing GC produce fundamentally different LLVM IR + (inc/dec instrumentation everywhere vs. periodic safepoints + with root maps). They cannot be hot-swapped per binary; they + are the binary's runtime contract. A code corpus committed to + one cannot be migrated to the other except by recompilation + *and* re-validation. +- AILang has 11 demo fixtures and a small stdlib. The migration + cost is bounded. With 100k LOC it would not be. The cheapest + moment to commit is now. +- The four language-design constraints that make RC sound and + complete (strict / no recursive value bindings / no laziness / + no shared mutable refs) are already true of AILang + incidentally. Decision 10 makes them load-bearing — anything + that breaks them is rejected at design time, not handled by a + retrofit cycle-collector. + +**Lineage.** Lean 4 is the closest precedent (functional, RC + +uniqueness, ML-style type system, LLVM-ish backend, competitive +with OCaml's tracing GC). Roc has the most aggressive uniqueness +optimiser (LLVM, performance-focused dialect of Elm). Koka is +the effect-typed sibling, also with reuse analysis on LLVM. +AILang's profile (acyclic ADTs, strict, threaded ctors between +fns) matches all three; the Lean 4 / Roc shape is the more +direct fit because we share the no-effect-row-default +assumption. + +**What this Decision did NOT do.** + +- Did not start the implementation. Iter 18a (uniqueness + inference pass) is queued as the first concrete step. The + algorithm sketch in Decision 10 is orchestrator-level and + needs implementer-level refinement before it ships. +- Did not retire Boehm. Boehm stays under `--alloc=gc` (default) + through Iter 18d. Iter 18d's bench result triggers retirement; + the flip is mechanical at that point. +- Did not introduce annotations. Uniqueness is fully inferred. + Existing fixtures' AILang source is unchanged; their JSON + hashes remain bit-identical through the entire 18-series. +- Did not commit to atomic refcounts. AILang is single-threaded; + the question is deferred until concurrency primitives arrive + (which would themselves need their own design pass). + +**What changed in the repo.** + +- `docs/DESIGN.md`: Decision 9 header annotated as superseded by + Decision 10. Decision 10 added (~150 lines) covering the + commitment, the four binding constraints, the inference + algorithm sketch, the codegen contract sketch, the reuse + analysis sketch, the migration plan (18a–18d), and the + excluded mutability extensions. +- `docs/JOURNAL.md`: this entry. +- No code change. No fixture change. No test change. + +**Queue.** 16-series and 17a are closed. The 18-series queue is: +18a (uniqueness inference), 18b (codegen inc/dec behind +`--memory=rc`), 18c (reuse analysis), 18d (RC bench + Boehm +retirement decision). 18a is the first concrete iter; the +sketch in Decision 10 is the brief. + +## 2026-05-08 — Follow-up: regions considered, LLM-aware sharpening of RC + +Same day, same conversation thread, after the RC commitment was +written down. The user pumped the brakes: "Warte mal. Woher +wissen wir, dass GC wirklich so viel schneller ist? Und was ist +mit anderen Konzepten?" — and rejected my premature commit of +Decision 10. The conversation that followed widened the design +space, then narrowed back to RC on better grounds, and then +sharpened the RC design with LLM-specific mechanisms. The earlier +section of this entry (the moment of commitment) stays as it +was; this section records the surrounding discussion that +produced the version of Decision 10 currently in `DESIGN.md`. + +### Regions considered and rejected + +The user invoked Rust's borrow-checker as precedent: before +Rust, no one expected memory management to admit a third +non-{tracing-GC, manual-malloc} category. The follow-up question: +is there a similarly-non-obvious choice we are missing for +AILang? RC is the obvious next stop after rejecting GC; is it +also the *consequent* one? + +**Region inference (Tofte/Talpin / MLton-style)** got the most +serious look. The pitch: instead of per-object refcounts, every +value lives in some region; regions stack on entry/exit; when +a region exits, every allocation in it is bulk-freed. No +per-op cost, no cycle worries, and the compiler can synthesise +the regions automatically. + +The user reduced this in two moves. + +First: "ist dann eine region nicht einfach ein expliziter +allocator, der syntaktisch fancy in die Sprache eingebaut ist?" +Concession: yes. Regions = explicit arenas + inference of +which arena to use + a static check that nothing escapes its +arena. The mechanism is bog-standard. + +Second, on the three default cases I'd proposed (D1: fn-local +region, D2: caller-passed region, D3: explicit `letregion`): +"D1 ist exakt der Scope der Funktion. D2 ist der Scope der +aufrufenden Funktion. D3 ist ein malloc. Der Rest ist Zucker. +Oder?" Concession: exactly. Regions are scope-shaped lifetimes +with sugar. + +That reduction surfaced the genuinely consequential question: +**do AILang programs have stack-shaped lifetimes?** If yes, +regions work. If no, regions don't. + +The answer is no. Real programs need: + +- Caches whose lifetime is "until evicted by an LRU policy" — + not nested in any caller's scope. +- Memo tables whose lifetime is "across calls to the same + top-level fn" — also not stack-shaped. +- Lookup structures, registries, deduplication maps — all + similar. + +Regions cannot accommodate these without falling back to +"everything lives in the root region" (= no allocator) or +proliferating per-cache-variant regions (combinatorial). RC +makes no shape assumption; it works for any DAG. + +**Linear / ownership types as primary mechanism.** Briefly +considered. Rejected on the same grounds as in Decision 10: +threading lifetimes through every signature is a tax the LLM +would pay on every line of code, and some programs become +unexpressible without an `unsafe` escape hatch. Rust accepts +that trade-off; AILang doesn't have to. + +**RC + uniqueness wins** because it is the *universal* solution +— no lifetime-shape assumption, no inexpressibility frontier, +just one mechanism that works for any acyclic value graph. The +user closed the excursion: "Schätze wir sind wieder bei RC." + +### LLM-aware sharpening of RC + +With RC chosen, the user asked the question that mattered most: +"An welcher Stelle können wir ausnutzen, dass wir es mit LLMs +zu tun haben?" The mainstream RC implementation (Lean 4) infers +everything from naked AST plus a handful of optional hints; if +the inference is conservative, it falls back to runtime inc/dec. +AILang can do better because the LLM author can effortlessly +produce annotations that a human author would resist as +boilerplate. + +Five concrete mechanisms, all written into the new Decision 10: + +1. **Mandatory `(borrow T)` / `(own T)` on fn signatures.** The + single most consequential extension. Each fn parameter + declares whether it is borrowed (read-only, no inc/dec) or + owned (consumed, callee responsible for free). The compiler + gets a precise contract at every call site instead of a + probabilistic guess. The LLM treats it the same way it treats + the rest of the type — it writes the right annotation as a + matter of course. + +2. **Linear-by-default consumption with explicit `(clone X)`.** + Every binder is consumed by exactly one `own`-mode use. Two + uses → compile error with structured `suggested_rewrites` + (make first call borrow / insert explicit clone / fuse the + traversals). Sharing always costs visible source. + +3. **First-class `(reuse-as SRC NEW-CTOR)`.** Lean 4 / Roc / Koka + discover reuse opportunities by inference; AILang lifts it to + author assertion + compiler verification. The author writes + the hint everywhere it should fire; the compiler bounces it + when the precondition fails. Fewer compiler heuristics, more + author intent visible. + +4. **`(drop-iterative)` on data declarations.** Tells the + compiler to synthesise iterative dec-on-zero traversal + (worklist) instead of recursive cascade. Avoids stack + overflow on deep structures. The author marks types where + this matters. + +5. **Structured compiler diagnostics with `suggested_rewrites` + in form-A AILang.** Errors are JSON; the LLM consumes them + without prose-parsing and applies the rewrites directly. + This is the missing half of the LLM-as-author story. + +The combination: AILang's RC = Lean 4's RC with all the "be +strict" knobs flipped to mandatory because the LLM author can +tolerate them (and benefits from the precision they give the +compiler). + +### What changed + +- `docs/DESIGN.md` Decision 10: rewritten substantially. The + earlier "uniqueness is fully inferred / no annotations / no + schema change" framing is replaced with the LLM-aware + architecture above. New "Why not other memory models" section + records the regions excursion. Schema-additions section + enumerates the new wrappers (`Type::Borrow`, `Type::Own`), + new terms (`Term::Clone`, `Term::ReuseAs`), and new data attr + (`drop_iterative`). Migration plan extended from 4 iters + (18a–d) to 6 (18a–f). + +- Iter queue restructured: 18a is no longer "uniqueness + inference pass" but **"borrow/own annotations as a language + feature"** (schema + parser + typechecker, no codegen). 18b is + the RC runtime + alloc routing. 18c is the inference + naive + inc/dec instrumentation. 18d is reuse. 18e is drop-iterative. + 18f is bench + Boehm retirement. + +- `pre-rc` git tag on commit 65e280b (the bench commit) — marks + the last point at which Boehm was unambiguously the canonical + allocator. + +### Did anything surprise + +- **Three iterations on Decision 10 in one day.** Drafted as + "RC committed, no annotations". Committed prematurely (rejected + by user). Re-opened the design space (regions excursion). + Returned to RC with a sharper architecture (annotations + mandatory). The discipline this enforces: do not commit + decisions until the alternatives have been honestly walked. + +- **Regions were genuinely a candidate, not a strawman.** The + scope-shape reduction is the kind of insight that only surfaces + under adversarial questioning. If I had been left to write + Decision 10 alone, I would have shipped the inferior + inferred-only version because it is what the literature + defaults to. + +- **The LLM-author lever is real.** "Make annotations mandatory" + is a non-starter for human-targeted languages — every Rust + RFC fights about exactly this trade-off. For AILang it is + free: the LLM does not experience boilerplate as a cost. That + is a structural advantage of the target audience, and it is + the kind of advantage Decision 10 should be cashing in on + everywhere it can. + +### Queue (current) + +- **Iter 18a** (queued, in_progress): `(borrow T)` / `(own T)` + as schema + parser + JSON + typechecker. No codegen change. + `(con T)` ≡ `(own T)` for back-compat. New fixture exercises + both modes. +- **Iter 18b** (queued): `runtime/rc.c` (header layout + alloc/ + inc/dec). Codegen `--memory=rc` routes allocation through + `rc_alloc`; no inc/dec yet (deliberately leaks). +- **Iter 18c** (queued): uniqueness inference + naive inc/dec + instrumentation. `(clone X)` schema. Linear-by-default + enforcement turns on. +- **Iter 18d** (queued): reuse hints + reuse analysis. + `(reuse-as ...)`. +- **Iter 18e** (queued): `(drop-iterative)` + worklist free. +- **Iter 18f** (queued): RC bench + Boehm retirement decision.