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