DESIGN/JOURNAL: Decision 10 reframed — RC + LLM-author annotations

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.
This commit is contained in:
2026-05-08 01:52:40 +02:00
parent 65e280bb70
commit e3ecf4f0cd
2 changed files with 631 additions and 1 deletions
+343 -1
View File
@@ -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 18a18d) 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 13.)
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<Type>) ; `(borrow T)` in form-A
Type::Own(Box<Type>) ; `(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<Term> } ; `(clone X)` — explicit RC inc
Term::ReuseAs { source: Box<Term>, body: Box<Term> } ; `(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_<module>_<def>` — even in the
+288
View File
@@ -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
18a18d; 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 (18a18d), 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
(18ad) to 6 (18af).
- 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.