Files
AILang/docs/specs/0048-boehm-retirement.md
T
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a
zero-padded 4-digit counter prefix that reflects creation order
(`NNNN-slug.md`). The counter is assigned per directory in strict
git-log creation order; ties broken alphabetically by original name.
The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is
dropped — the date is recoverable from git log and the counter
carries the ordering.

A file's counter is stable for the life of the file: never reassigned,
never reused, never compacted. Deleted files retire their counter;
subsequent files do not fill the gap. This is the property that lets
cross-references stay literal — refs use the full filename including
the counter (`design/contracts/0007-honesty-rule.md`) so they grep
cleanly and resolve directly without a glob step.

313 cross-references updated across .md/.rs/.toml/.c/.json files
(test pins, include_str! paths, design-INDEX entries, baseline notes,
runtime C comments, inter-contract markdown links incl. bare basename
and `../models/foo.md` forms).

CLAUDE.md gets a new "File-naming convention" section spelling out
the rule and rationale. skills/brainstorm/SKILL.md and
skills/planner/SKILL.md updated so new spec/plan creation produces
counter-prefixed names from the start.

The full test suite (cargo test --workspace) passes.
2026-05-28 13:31:31 +02:00

472 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Boehm full retirement — Design Spec
**Date:** 2026-05-20
**Status:** Draft — awaiting user spec review
**Authors:** Brummel (orchestrator) + Claude
## Goal
Retire the transitional Boehm-Demers-Weiser GC backend from the
AILang toolchain. After this milestone, RC is the canonical
production allocator, `--alloc=bump` is retained as the raw-alloc
bench-floor, and there is no GC code path. The design ledger,
the CLI surface, the codegen pipeline, the test suite, and the
bench harness all reflect a single-canonical-allocator project.
Resolves Gitea issue #4 (`Boehm full retirement`).
## Architecture
The Boehm backend was introduced as a transitional dual-allocator
during the pre-22 RC commitment window. Its standing role per
`design/models/0004-rc-uniqueness.md` was twofold: (a) **production
fallback** before RC was mature; (b) **parity oracle**
differential RC-vs-GC stdout assertions to catch RC bugs cheaply.
Both roles have expired:
- (a) RC is the CLI default and has been the canonical production
allocator since the memory-model commitment landed.
- (b) No recent debug iteration has used the GC oracle to triage
an RC bug. The differential e2e tests have not caught any RC
bug that fixed-stdout assertions would not have caught.
The retirement removes Boehm wholesale across six layers in one
cohesive iteration:
1. **CLI surface**`--alloc=gc` value rejected with
`unknown --alloc value` parser error; help text adjusted.
2. **Codegen**`AllocStrategy::Gc` variant deleted; the
`runtime_alloc_fn` arm returning `"GC_malloc"` removed; the
`lower_workspace` default switches from `AllocStrategy::Gc` to
`AllocStrategy::Rc`.
3. **Build / link** — the `clang` invocation in `crates/ail/src/main.rs`
loses its libgc-link branch entirely.
4. **Test suite** — pure-differential e2e tests deleted; RC-feature
tests with GC-as-backstop converted to fixed-stdout (the GC
build + the `assert_eq!(stdout_gc, stdout_rc, ...)` line drop,
the absolute `assert_eq!(stdout_rc.trim(), "<n>")` pin stays);
the codegen-internal negative-complement test that proves "no
drop fns under non-RC" retargets from `AllocStrategy::Gc` to
`AllocStrategy::Bump`; the M2 staticlib alloc-guard test loses
its gc-arm but keeps its bump-arm.
5. **Design ledger**`design/models/0004-rc-uniqueness.md` excises
the "Dual allocator — RC canonical, Boehm parity oracle"
section and the Boehm-Choice-block; the per-fn-alloca section's
language generalises from "on top of Boehm" to "on top of the
allocator". `design/models/0003-pipeline.md` drops the
`--alloc=gc → libgc` arm of the pipeline diagram and the
accompanying prose. The 1.3× RC-over-bump number is retained
in `rc-uniqueness.md` but reframed as a **bench-health
regression gate** (not a retirement gate); the closure-chain
±15% wider band is preserved analogously.
6. **Honesty pin**`crates/ailang-core/tests/docs_honesty_pin.rs`
removes its present-tense assertion that requires
`"--alloc=gc selects the transitional Boehm backend"` in
`pipeline.md`; in its place, the `design_md_has_no_wunschdenken`
set gains absence-pins against Boehm-zombie strings
(`"transitional Boehm"`, `"parity oracle"`, `"GC_malloc"`,
`"libgc"`) so the narrative cannot quietly re-emerge.
Bump survives unchanged — `AllocStrategy::Bump`, `runtime/bump.c`,
the `--alloc=bump` CLI flag, and `bench/run.sh`'s RC-vs-bump
comparison all remain. Bump's standing role is bench-floor for
RC-overhead measurement, not a production target.
## Concrete code shapes
This is a no-authoring-surface milestone — no new `.ail`
construct is added or removed. The clause-3 discriminator
(wrong code must fail) lives at the CLI layer:
### Must-fail CLI fixture (clause-3 discriminator)
```
$ ail build --alloc=gc examples/hello.ail -o /tmp/h
error: unknown --alloc value `gc` (expected `rc` or `bump`)
exit 2
```
### Unchanged-default north-star slice
```
$ cat examples/hello.ail
(module hello
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do (io print_str "hello")))))
$ ail build examples/hello.ail -o /tmp/h # default --alloc=rc
$ /tmp/h
hello
```
After retirement, every example under `examples/` continues to
build and run unchanged under the default `--alloc=rc`. The
`--alloc=bump` invocation still works for leak-tolerant bench
contexts.
### Implementation shape — codegen `AllocStrategy`
Before:
```rust
// crates/ailang-codegen/src/lib.rs
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AllocStrategy { Gc, Bump, Rc }
impl AllocStrategy {
fn runtime_alloc_fn(&self) -> &'static str {
match self {
AllocStrategy::Gc => "GC_malloc",
AllocStrategy::Bump => "bump_malloc",
AllocStrategy::Rc => "ailang_rc_alloc",
}
}
}
pub fn lower_workspace(ws: &Workspace) -> Result<String, Error> {
lower_workspace_inner(ws, AllocStrategy::Gc, Target::Executable)
}
```
After:
```rust
// crates/ailang-codegen/src/lib.rs
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AllocStrategy { Bump, Rc }
impl AllocStrategy {
fn runtime_alloc_fn(&self) -> &'static str {
match self {
AllocStrategy::Bump => "bump_malloc",
AllocStrategy::Rc => "ailang_rc_alloc",
}
}
}
pub fn lower_workspace(ws: &Workspace) -> Result<String, Error> {
lower_workspace_inner(ws, AllocStrategy::Rc, Target::Executable)
}
```
### Implementation shape — CLI parse arm
Before (`crates/ail/src/main.rs:2143-2148`):
```rust
match alloc.as_str() {
"gc" => Ok(ailang_codegen::AllocStrategy::Gc),
"bump" => Ok(ailang_codegen::AllocStrategy::Bump),
"rc" => Ok(ailang_codegen::AllocStrategy::Rc),
other => bail!("unknown --alloc value `{other}` (expected `gc`, `bump`, or `rc`)"),
}
```
After:
```rust
match alloc.as_str() {
"bump" => Ok(ailang_codegen::AllocStrategy::Bump),
"rc" => Ok(ailang_codegen::AllocStrategy::Rc),
other => bail!("unknown --alloc value `{other}` (expected `rc` or `bump`)"),
}
```
### Implementation shape — link branch
Before (`crates/ail/src/main.rs:2389ff`):
```rust
match strategy {
AllocStrategy::Gc => {
// libgc link; pthread/dl transitively
cmd.arg("-lgc");
}
AllocStrategy::Bump => {
// compile runtime/bump.c, link static
...
}
AllocStrategy::Rc => { ... }
}
```
After: the `Gc` arm is gone entirely; `match` exhausts on `Bump`
and `Rc`. The accompanying inline doc-comment block ("Boehm
conservative GC (the transitional dual-allocator). The lowered
IR calls @GC_malloc; libgc supplies it. …") is removed; the
remaining `Bump` and `Rc` arms keep their doc-comments unchanged.
### Implementation shape — staticlib alloc-guard
Before (`crates/ail/tests/embed_staticlib_alloc_guard.rs`):
```rust
#[test]
fn staticlib_gc_is_rejected() { ... } // expects unknown-alloc OR RC-only diag
#[test]
fn staticlib_bump_is_rejected() {
let out = build_staticlib_with_alloc("bump", "/tmp/ail_m2_guard_bump");
assert!(!out.status.success(), ...);
assert!(stderr.contains("staticlib (swarm) artefact is RC-only"), ...);
}
```
After: `staticlib_gc_is_rejected` deleted (the CLI parser now
rejects `gc` *before* the build is reached, so the staticlib-guard
no longer governs that case). `staticlib_bump_is_rejected` kept
unchanged. File-level doc-comment updated to drop the GC reference.
### Implementation shape — codegen negative-complement test
Before (`crates/ailang-codegen/src/lib.rs:3571-3576`):
```rust
// Negative complement: no drop fns under `--alloc=gc`.
let ir_gc = lower_workspace_with_alloc(&ws, AllocStrategy::Gc).unwrap();
assert!(!ir_gc.contains("@drop_rclist_IntList"), ...);
```
After:
```rust
// Negative complement: no drop fns under `--alloc=bump`
// (only RC emits per-type drop fns; bump leaks).
let ir_bump = lower_workspace_with_alloc(&ws, AllocStrategy::Bump).unwrap();
assert!(!ir_bump.contains("@drop_rclist_IntList"), ...);
```
The test's semantic ("non-RC allocators emit no drop fns") is
preserved; the witness allocator shifts from `Gc` to `Bump`.
### Implementation shape — honesty-pin inversion
Before (`crates/ailang-core/tests/docs_honesty_pin.rs:116-117`):
```rust
assert!(pipeline.contains("`--alloc=gc` selects the transitional Boehm backend"),
"models/0003-pipeline.md must describe Boehm present-tense, not as 'on the path to retirement'");
```
After: this assertion is **deleted**, and the
`design_md_has_no_wunschdenken` set gains four new absence-pins
in its place:
```rust
assert!(!d.contains("transitional Boehm"),
"design/: Boehm narrative is retired — must not re-emerge in the ledger");
assert!(!d.contains("parity oracle"),
"design/: Boehm-as-parity-oracle is retired narrative — git log carries the history");
assert!(!d.contains("GC_malloc"),
"design/: GC_malloc references are retired — RC + bump are the only allocators");
assert!(!d.contains("libgc"),
"design/: libgc references are retired — no GC link dependency exists anymore");
```
The corpus that `design_md_has_no_wunschdenken` scans
(`design_corpus()`) already includes `rc-uniqueness.md` and the
other ledger files, so no path-list change is needed. The
header doc-comment for the test file is updated to drop the
Boehm/Decision-9 phrase.
### Implementation shape — design/models excise (`rc-uniqueness.md`)
The `## Dual allocator — RC canonical, Boehm parity oracle` section
(lines 3-32 of the file) is deleted entirely. The `## Per-fn arena
via stack alloca` section is kept; its references to "Boehm" and
"@GC_malloc" generalise to "the allocator" / "@<runtime_alloc>"
respectively. The `## Memory model — RC + Uniqueness …` section
keeps the RC narrative; the parenthetical
"`(see the dual-allocator section above)`" is dropped; the
parenthetical naming Boehm as a transitional allocator is dropped;
the 1.3× target sentence is rewritten to drop the Boehm-retirement
framing:
Before:
> AILang's canonical memory model is reference counting with
> static uniqueness inference and explicit LLM-author annotations…
> Boehm becomes a transitional allocator and is retired when the
> RC pipeline matches the bump-allocator floor within an
> acceptable margin (target 1.3× on `bench/run.sh`).
After:
> AILang's canonical memory model is reference counting with
> static uniqueness inference and explicit LLM-author annotations.
> The RC pipeline tracks the bump-allocator raw-alloc floor: a
> bench-health regression gate requires RC overhead ≤ 1.3× bump
> on the linear/tree corpus, with a wider ±15% band on the
> closure-chain corpus (representational cost of the closure-pair
> layout). See `bench/run.sh` for the active check.
### Implementation shape — design/models excise (`pipeline.md`)
The pipeline diagram drops the libgc arm:
Before:
```
└─ clang -O2 *.ll -o binary
--alloc=rc → emits inc/dec (@ailang_rc_inc / _dec; canonical, default)
--alloc=gc → links libgc (@GC_malloc; parity oracle)
```
After:
```
└─ clang -O2 *.ll -o binary
--alloc=rc → emits inc/dec (@ailang_rc_inc / _dec; canonical, default)
--alloc=bump → links bump-floor (@bump_malloc; raw-alloc bench-floor)
```
The accompanying prose ("Two allocator backends share the same
MIR. … `--alloc=gc` selects the transitional Boehm backend …") is
rewritten to describe the rc/bump pair as the present state.
## Components
| Component | Touchpoints (precise location-class, not byte-exact) |
|---|---|
| Codegen | `crates/ailang-codegen/src/lib.rs``AllocStrategy` enum variant; `runtime_alloc_fn` match arm; default-strategy entry-point fns (`lower_workspace`, `lower_workspace_staticlib`); doc-comment block on the enum; the inline negative-complement codegen test currently using `AllocStrategy::Gc` |
| Codegen | `crates/ailang-codegen/src/escape.rs` — module-level doc comment generalises GC-specific language to allocator-agnostic |
| CLI parse | `crates/ail/src/main.rs:2143ff``--alloc` match arm; error-text wording |
| CLI build | `crates/ail/src/main.rs:2389ff` — the `AllocStrategy::Gc` arm in the clang invocation; the surrounding doc-comment block; the staticlib-guard diagnostic string (the "links the shared Boehm collector" phrasing) |
| Runtime docs | `runtime/bump.c` header doc — drop "mirrors `GC_malloc` from libgc" and "default Boehm-GC build" wording; bump's signature description stays |
| Runtime docs | `runtime/rc.c`, `runtime/str.c` — comment references to Boehm / `--alloc=gc` |
| E2E pure-differential | `crates/ail/tests/e2e.rs``gc_handles_recursive_list_construction` (line 194), `alloc_rc_produces_same_stdout_as_gc` (1400), `alloc_rc_matches_gc_on_std_list_demo` (1564) — **deleted** |
| E2E RC-feature w/ differential backstop | `crates/ail/tests/e2e.rs``reuse_as_drop_demo` (1475ff), `rc_box_drop` (1602ff), `rc_list_drop` (1628ff), `rc_list_drop_borrow` (1674ff), `pat_extract_partial_drop` (1762ff), `rc_own_param_drop` (1858ff), `rc_drop_iterative_long_list` (1981ff), `rc_pin_recurse_implicit` (2168ff), `rc_let_alias_implicit_param` (2396ff) — **the `stdout_gc` build call and the `assert_eq!(stdout_gc, stdout_rc, ...)` differential line dropped**; the absolute `assert_eq!(stdout_rc.trim(), "<n>")` pin stays; doc-comments cleaned of `--alloc=gc` references |
| E2E staticlib guard | `crates/ail/tests/embed_staticlib_alloc_guard.rs``staticlib_gc_is_rejected` deleted; `staticlib_bump_is_rejected` unchanged; file-level doc-comment updated |
| Bench harness | `bench/run.sh``bench_latency_implicit_gc` arm removed; all `--alloc=gc` build calls removed; comment "decisive number for Decision 10's Boehm-retirement target (1.3x)" → "RC-overhead-vs-bump bench-health gate (1.3× ceiling)"; comment "Boehm-fair Implicit @ gc" arm dropped |
| Design ledger | `design/models/0004-rc-uniqueness.md` — full Boehm sections excised (per "Concrete code shapes" above); 1.3× reframed as bench-health regression gate |
| Design ledger | `design/models/0003-pipeline.md` — pipeline diagram + accompanying prose updated (per above) |
| Design ledger | `design/contracts/0003-embedding-abi.md:43-44` — the present-tense sentence `ail build --emit=staticlib rejects --alloc=gc/--alloc=bump (the shared Boehm collector is not swarm-safe)` is rewritten to drop the gc clause (gc is now a CLI-parser-level unknown-value, not a staticlib-guard rejection) and to reframe the swarm-safety justification: only `--alloc=bump` is rejected by the staticlib guard, on the grounds that bump is a leak-only bench instrument, not the historical Boehm-collector justification |
| Honesty pin | `crates/ailang-core/tests/docs_honesty_pin.rs` — present-tense Boehm pin deleted; four absence-pins added (per above); test-file doc-comment header updated to drop the Boehm/Decision-9 phrase |
| Bencher agent | `skills/audit/agents/ailang-bencher.md` — the worked example "under heap pressure where Boehm GC has stop-the-world pauses" is rewritten to use an allocator-agnostic hypothesis (RC overhead under a workload that doesn't exercise the rc path is a natural replacement) |
The fixture `examples/gc_stress.ail` (target of the deleted
`gc_handles_recursive_list_construction` test) is **deleted**.
Verified at spec time: `grep -rn 'gc_stress' --include='*.rs'
--include='*.ail' --include='*.ail.json' --include='*.sh'` returns
exactly two hits — the e2e test call and the fixture itself —
both of which are removed by this iteration.
## Data flow
Removal-only milestone; no new data flow. The existing
post-retirement data flow is:
```
.ail.json → check (typecheck + uniqueness) → codegen
├─ AllocStrategy::Rc → @ailang_rc_inc / _dec + rc.c linked
└─ AllocStrategy::Bump → bump.c linked (bench only)
→ clang -O2 → binary
```
No surviving code path branches on Boehm/GC.
## Error handling
The only user-observable surface change is the CLI parser error:
| Input | Old behaviour | New behaviour |
|---|---|---|
| `ail build --alloc=gc foo.ail` | succeeds; links libgc; produces a Boehm-backed binary | exit 2; stderr: `error: unknown --alloc value \`gc\` (expected \`rc\` or \`bump\`)` |
| `ail build foo.ail` (default) | succeeds; defaults to gc historically — but actual default in current code is rc per the memory-model commitment | unchanged: defaults to rc, succeeds |
| `ail build --alloc=rc foo.ail` | unchanged | unchanged |
| `ail build --alloc=bump foo.ail` | unchanged | unchanged |
| `ail build --emit=staticlib --alloc=bump foo.ail` | rejected with "staticlib artefact is RC-only" | unchanged |
The host's libgc-installation status no longer affects any
`ail build`/`ail run` outcome.
## Testing strategy
The retirement RED-state is observable through several existing
green tests that *currently* depend on Boehm. Removal breaks
them; the iteration's GREEN-state is the simultaneous removal of
the Boehm path AND the breaking tests.
**RED→GREEN transitions in the iteration:**
1. `cargo test -p ailang-codegen` — the in-source codegen test
that exercises `AllocStrategy::Gc` fails to compile once the
enum variant is removed; the test is retargeted to `Bump`
inside the same iteration.
2. `cargo test -p ail --test e2e` — the pure-differential tests
are deleted; the RC-feature tests lose their `stdout_gc` build
call (which calls `build_and_run_with_alloc(example, "gc")`,
which would now panic). Each test is rewritten to drop the gc
call.
3. `cargo test -p ail --test embed_staticlib_alloc_guard` —
`staticlib_gc_is_rejected` is deleted; `staticlib_bump_is_rejected`
keeps passing.
4. `cargo test -p ailang-core --test docs_honesty_pin` — the
present-tense Boehm-anchor assertion fails the moment
`pipeline.md` drops the string; the assertion is deleted
inside the same iteration, and the four new absence-pins are
added to replace it.
**New protective assertions:**
- A `cargo test -p ail --test boehm_retirement_pin` (new file)
pins the CLI behaviour: `ail build --alloc=gc <example>` must
fail with exit code ≠ 0 and stderr containing
`unknown --alloc value`. This is the milestone-protecting E2E
for the retirement — guards against a future iter
reintroducing the gc arm without anyone noticing.
**Standing tests that must remain green:**
- All `workspace`-scope tests (`cargo test --workspace --quiet`).
- `design_index_pin`, `design_schema_drift`, `roundtrip-invariant`,
`effect_doc_honesty_pin`, `docs_honesty_pin` (with the new
absence-pins).
- Bench harness regression check `bench/check.py` — passes
unchanged (no compile-side compatibility break).
- `bench/compile_check.py`, `bench/cross_lang.py` — pass
unchanged.
- `bench/run.sh` runs to completion with rc-vs-bump arms only,
no gc arms.
**Field test:** because this is a no-authoring-surface milestone,
no `.ail` fieldtest is needed. The CLI must-fail fixture is the
clause-3 discriminator.
## Acceptance criteria
The milestone closes CLEAN when all of the following hold:
1. `cargo test --workspace --quiet` is fully green; the ~12
removed/converted tests are accounted for in the iter commit
body; no new failure.
2. `grep -rn "Boehm\|libgc\|GC_malloc\|--alloc=gc\|AllocStrategy::Gc" \
crates/ design/ runtime/ bench/run.sh skills/` returns matches
**only** in `crates/ailang-core/tests/docs_honesty_pin.rs` (the
four new absence-pins literally name the Boehm-zombie strings
to scan against). Anywhere else is a residual reference and
the iteration is not done.
3. `bench/check.py`, `bench/compile_check.py`, `bench/cross_lang.py`
all exit 0 against the existing baseline.
4. The CLI must-fail fixture exits ≠ 0 with the expected stderr
wording.
5. `design/models/0004-rc-uniqueness.md` and `design/models/0003-pipeline.md`
describe RC + bump as the present state; the honesty rule
(`design/contracts/0007-honesty-rule.md`) is upheld — no past-tense
"Boehm was kept as oracle" narrative remains; Boehm's history
is recorded in the iter's git-log commit body.
6. Issue #4 is closed by the iter commit's `closes #4` trailer.
Out of scope (deferred to separate work):
- The "Closure-pair slab / pool" optimisation (Gitea #3) that
would tighten the closure-chain ±15% bench-health band. Not
blocked by retirement; can ship anytime.
- Any further `AllocStrategy::Bump` rework (still a single-
variant bench instrument).
- Any bench-harness *recalibration* — that just closed
(bench-harness-recalibration audit, 2026-05-20); this milestone
preserves the calibration.