runtime: half-retirement of Boehm — flip CLI default to --alloc=rc

The dual-allocator policy from 2026-05-08 made Boehm the CLI default
"for general workloads", with rc selected explicitly for real-time-
sensitive code. That asymmetry contradicted Decision 10: rc + uniqueness
inference is the canonical memory model AILang's typechecker enforces,
and the CLI was teaching the opposite mental model to users and prompt-
fed LLMs.

This iter flips the asymmetry without removing Boehm:

  - main.rs: default_value gc -> rc for build/run; help text rewritten
    so rc is the canonical path and gc is the parity oracle.
  - DESIGN.md Decision 9 retitled "RC canonical, Boehm parity oracle"
    and rewritten end-to-end. Migration plan step 6 updated to a
    two-step retirement (default-flip done, full removal gated).
  - DESIGN.md pipeline diagram: rc-first, gc-as-oracle.
  - JOURNAL: 2026-05-09 entry recording the call, what shipped,
    the bug the flip surfaced, the gating condition for full
    Boehm removal.

Bug surfaced by the flip and fixed in the same iter:
codegen/drop.rs build_pair_drop_fn emitted `getelementptr inbounds
{{ ptr, ptr }}, ...` via push_str (not format!), so the doubled
braces leaked verbatim into the IR. LLVM parsed it as a struct-of-
struct, the GEP referenced a non-existent field, and clang failed.
Invisible under the old gc default (no per-type drop fns) and
invisible to the 8 explicit _with_alloc("rc") tests (none of them
escaped a closure-with-captures). Caught immediately by two corpus
tests once rc became the baseline: closure_captures_let_n,
local_rec_as_value_capture_demo. Fix: single braces. Tests stay
as the regression guard.

The episode is the canonical justification for keeping the GC arm
as a parity oracle for now — a months-old codegen bug found by
flipping the baseline column.

288 passed / 0 failed / 3 ignored, unchanged.
This commit is contained in:
2026-05-09 00:14:56 +02:00
parent c79f7e2f00
commit 7e5c95f6c6
4 changed files with 168 additions and 40 deletions
+43 -27
View File
@@ -637,26 +637,38 @@ constructor-blocked recursions in `map`, `sort`, `insert`
remain unmarked — they cannot benefit from `musttail`
without a source-level rewrite.
## Decision 9: dual allocator — Boehm conservative GC + RC
## Decision 9: dual allocator — RC canonical, Boehm parity oracle
**Status: dual-allocator policy as of 2026-05-08.** Originally
framed (2026-05-07) as "transitional Boehm until the RC
pipeline (Iters 18a18g) lands and the default flips". The RC
pipeline landed and was validated end-to-end (live=0 on the
canonical bench fixture; tail latency 23× better than Boehm;
RSS lower than Boehm). The orchestrator's call on retirement
was to **keep both allocators live**: Boehm stays as the
default for general workloads, RC is selected via `--alloc=rc`
for real-time-sensitive workloads where bounded per-operation
latency matters more than peak throughput.
**Status: half-retirement as of 2026-05-09.** Originally framed
(2026-05-07) as "transitional Boehm until the RC pipeline
(Iters 18a18g) lands and the default flips"; then re-framed
(2026-05-08) as a symmetric dual-allocator policy with Boehm as
the CLI default. The 2026-05-09 revision flips the asymmetry to
match Decision 10:
The retirement question reopens if maintaining the Boehm path
costs significant attention (libgc upgrade pain, runtime
divergence, build complexity); until then the dual-allocator
asymmetry (Boehm = default, RC = ready alternative) is the
canonical state. Decision 10 (RC + uniqueness) holds as the
specification of the RC alternative; the rest of this section
documents the Boehm half of the dual model.
- **RC is canonical.** `--alloc=rc` is the CLI default for
`ail build` and `ail run`. The runtime AILang's memory model
(RC + uniqueness inference) is designed for. New examples,
benches, and corpus tests run under RC unless they explicitly
pin GC.
- **Boehm stays as a parity oracle.** `--alloc=gc` remains
reachable. Its load-bearing job is differential diagnosis: when
RC produces a segfault, refcount underflow, or wrong stdout, the
GC build of the same module is the cheap "memory bug or logic
bug?" probe. The end-to-end suite includes per-example
parity tests that run both backends and assert byte-identical
stdout — those tests are what make the oracle real.
- **`--alloc=bump`** is unchanged: a leak-only bench instrument,
not a production target.
Full Boehm retirement (drop libgc, remove the gc backend) reopens
when the parity oracle stops paying its keep — concretely, when a
few iter families ship without the gc arm catching anything that
the rc arm did not already catch. Until then, the cost of
keeping libgc as a build dependency is accepted in exchange for
diagnostic leverage. Decision 10 (RC + uniqueness) holds as the
specification of the canonical runtime; the rest of this section
documents the Boehm half, retained as the oracle.
The `--alloc=bump` mode introduced for the bench is a
measurement tool, not a production target.
@@ -1325,9 +1337,14 @@ monomorphised copies resolve to concrete drop fns).
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.
6. **Iter 18f:** RC validation bench. RC validated within target
on `bench/run.sh` (live=0; tail latency 23× better than Boehm;
RSS lower). Retirement of Boehm executes in two steps: first a
default-flip (2026-05-09 — `--alloc=rc` becomes the CLI default,
GC retained as parity oracle; see Decision 9 above); second a
full removal once the oracle stops paying its keep (gating
condition: a few iter families with no GC-only diagnostic
wins).
7. **Iter 19a / 19a.1 / 19b:** advisory `over-strict-mode` lint
+ precise sub-binder analysis with heap-type filter +
`FnDef.suppress` suppression mechanism with mandatory-reason.
@@ -1582,14 +1599,13 @@ canonical-JSON hash.
├─ lower to MIR (SSA-like, named SSA values)
├─ emit LLVM IR (.ll)
└─ clang -O2 *.ll -o binary
--alloc=gc → links libgc (@GC_malloc; transitional)
--alloc=rc → emits inc/dec (@ailang_rc_inc / _dec; canonical)
--alloc=rc → emits inc/dec (@ailang_rc_inc / _dec; canonical, default)
--alloc=gc → links libgc (@GC_malloc; parity oracle)
```
Two allocator backends share the same MIR. `--alloc=gc` links libgc
and is the transitional fallback (no inc/dec instrumentation, no mode
checks at codegen time). `--alloc=rc` is the canonical backend
committed to in Decision 10: the typechecker enforces
Two allocator backends share the same MIR. `--alloc=rc` is the
canonical backend committed to in Decision 10 and the CLI default
since 2026-05-09: the typechecker enforces
`(own)` / `(borrow)` modes, codegen emits `ailang_rc_inc` / `_dec`
calls at the points dictated by linearity, and `Term::Clone` /
`Term::ReuseAs` materialise into actual rc-bumps and in-place