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
+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.