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:
+343
-1
@@ -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<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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user