Files
AILang/docs/plans/boehm-retirement.1.md
T
Brummel ad0a8d8786 plan: boehm-retirement.1 — atomic single-iter execution layout
Decomposes the Boehm-retirement spec (50dc478) into 10 bite-sized
tasks. Task 1 ships the RED milestone-pin first (TDD); Task 2 is
the compile-gated atomic core (drops `AllocStrategy::Gc` variant +
the `Default` derive + threads every caller in codegen + main.rs in
one cohesive task, so the workspace-build gate at the end is
satisfiable). Tasks 3-9 cover the scrub work that cascades off the
core (e2e suite, doc-comments, bench harness, design ledger,
honesty-pin inversion, example fixtures + agent prompts, IR
snapshots). Task 10 is the acceptance gate.

Plan-recon (DONE_WITH_CONCERNS) surfaced 9 spec gaps; the plan
absorbs all 9:
  - `runtime_alloc_fn` → actual identifier `fn_name` (no rename)
  - 5 checked-in IR snapshots (`hello.ll`, `list.ll`, `max3.ll`,
    `sum.ll`, `ws_main.ll`) regenerate via `UPDATE_SNAPSHOTS=1`
  - `design/contracts/scope-boundaries.md:67,114-127` Boehm block
    + `examples/gc_stress.ail.json` dead-reference
  - `design/contracts/memory-model.md:232` "pre-Boehm era" phrase
  - `bench/check.py:91,94` `implicit @ gc` arm label map
  - `crates/ailang-codegen/src/match_lower.rs:40,113`
    `@GC_malloc` doc-comments
  - `skills/implement/agents/ailang-implementer.md:95,185` Decision
    10/Boehm references
  - `AllocStrategy`'s `Default` derive (no callers — strip)
  - e2e.rs spec line numbers refer to gc-call lines, not test-fn
    header lines

Plan self-review caught a planner-grade defect that recon had
not flagged: `bench/check.py:62` hardcodes `"gc(s)" in line` as
the throughput-table header sentinel and `:72` hardcodes the
9-column count — both must shift in lockstep with the
`bench/run.sh` 9→6 column compaction; without the pair-edit,
`parse_throughput_table` silently returns an empty dict and the
"green" check pins nothing. Task 5 now carries Steps 4a-4c (header
+ count + field-set) as a coherent unit, plus the explicit pin in
the rationale.

Net delta on landing: 1 new test, ~30 modified files, 5
regenerated IR snapshots, 1 deleted fixture. Workspace passing-
test count shifts by `-3` (4 deletions − 1 new). The iter commit
will close Gitea #4 via the trailer.

refs #4
2026-05-20 20:24:22 +02:00

1031 lines
54 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 Retirement (iter .1) — Implementation Plan
> **Parent spec:** `docs/specs/2026-05-20-boehm-retirement.md`
>
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
**Goal:** Atomic removal of the transitional Boehm GC backend across
codegen, CLI, link branch, test suite, bench harness, design ledger,
honesty-pin, runtime headers, example fixtures, agent prompts, and
IR snapshots. After this iter: `AllocStrategy` has two variants
(`Rc`, `Bump`), `--alloc=gc` is rejected at CLI parse with
`unknown --alloc value`, the libgc link arm is gone, the
Boehm-grep returns hits only inside the new absence-pins in
`docs_honesty_pin.rs`, all workspace tests stay green, and the
bench harness runs rc-vs-bump only.
**Architecture:** One atomic compile-gated cut (Task 2) brings the
codebase from "compiles with Gc variant" to "compiles without Gc
variant" — this is the only task that touches the type system; all
later tasks are scrub-style (delete strings, rewrite paragraphs,
update tables). TDD discipline: Task 1 ships a RED milestone-pin
that asserts `ail build --alloc=gc` fails; it is GREEN after Task 2
and stays green forever.
**Tech Stack:** Rust (workspace `ailang-codegen`, `ailang-core`,
`ail`); LLVM IR snapshots in `crates/ail/tests/snapshots/`;
shell + Python in `bench/`; C runtime in `runtime/`; Markdown in
`design/` + `skills/`; AILang `.ail` examples in `examples/`.
---
## Files this plan creates or modifies
**Create:**
- `crates/ail/tests/boehm_retirement_pin.rs` — milestone-protecting CLI must-fail E2E
**Modify (compile-checked Rust):**
- `crates/ailang-codegen/src/lib.rs:158-164,186-191,237-242,281-295,3495,3571-3576` + comment-only sites at `:146-147,530,781,802,1040,1211`
- `crates/ail/src/main.rs:141-142,2143-2148,2189,2263-2264,2365-2366,2389-2395,2425,2497-2498`
- `crates/ailang-codegen/src/escape.rs:5,96` — module `//!` + doc-comment
- `crates/ailang-codegen/src/match_lower.rs:40,113` — doc-comments
- `crates/ail/tests/e2e.rs:186-203,1396-1408,1432,1475,1559-1573,1602,1614,1628,1665,1674,1752,1762,1841,1858,1975-1976,1981,2160,2168,2392,2396` — 3 deletions + 9 stripping conversions + doc-comment scrub
- `crates/ail/tests/embed_staticlib_alloc_guard.rs:1-4,20-32` — file `//!` doc + delete `staticlib_gc_is_rejected`
- `crates/ailang-core/tests/docs_honesty_pin.rs:30,55-73,116-117` — invert Boehm anchors
- `crates/ailang-core/tests/design_index_pin.rs:166` — drop `"pre-Boehm"` from protected-exception list
**Modify (text / scripts / data):**
- `runtime/bump.c:4,10-11,29` — header comment scrub
- `runtime/rc.c:38,167,169` — comment scrub
- `runtime/str.c:32` — comment scrub
- `bench/run.sh:3-11,65,149-167,170-205` — drop gc-arm + columns + latency-bench gc-fixture
- `bench/check.py:75-97` — drop gc-keyed throughput fields + drop `"implicit @ gc"` arm mapping
- `bench/baseline.json` — drop gc-keyed entries (via `bench/check.py --update-baseline` after a clean `bench/run.sh`)
- `design/models/rc-uniqueness.md:3-32,71-79,132-156,170-183` — excise + reframe
- `design/models/pipeline.md:14-28` — pipeline diagram + prose
- `design/contracts/scope-boundaries.md:67,114-127` — rewrite Boehm block + `@GC_malloc` reference
- `design/contracts/memory-model.md:232` — drop `"pre-Boehm era"`
- `design/contracts/embedding-abi.md:42-44` — drop gc clause from staticlib-guard prose
- `skills/audit/agents/ailang-bencher.md` — multi-site Boehm rewrite (recon enumerated ≈18 sites)
- `skills/implement/agents/ailang-implementer.md:95,185` — drop Decision-10/Boehm references
- `examples/gc_stress.ail`**DELETE entire file** (47 lines)
- `examples/bench_latency_implicit.ail:4,10,12,15,23,150,185` — doc-comment scrub
- `examples/bench_latency_explicit.ail:12-13` — doc-comment scrub
- `examples/escape_local_demo.ail:10` — doc-comment scrub
- `examples/reuse_as_demo.ail:13,17` — doc-comment scrub
- `examples/rc_pin_recurse_implicit.ail:8` — doc-comment scrub
**Regenerate (UPDATE_SNAPSHOTS):**
- `crates/ail/tests/snapshots/hello.ll`
- `crates/ail/tests/snapshots/list.ll`
- `crates/ail/tests/snapshots/max3.ll`
- `crates/ail/tests/snapshots/sum.ll`
- `crates/ail/tests/snapshots/ws_main.ll`
---
## Task 1: RED — milestone-protecting CLI must-fail E2E
**Goal:** Lay down the test that proves Boehm is retired. Today it
FAILS (because `ail build --alloc=gc` currently succeeds); after
Task 2 it PASSES and stays green forever.
**Files:**
- Create: `crates/ail/tests/boehm_retirement_pin.rs`
- [ ] **Step 1: Create the RED test file**
Write `crates/ail/tests/boehm_retirement_pin.rs`:
```rust
//! Milestone pin: `ail build --alloc=gc` must FAIL at CLI parse
//! time after Boehm full retirement. The transitional Boehm
//! backend was removed; the only accepted `--alloc` values are
//! `rc` (canonical, CLI default) and `bump` (raw-alloc bench-floor).
//!
//! This pin protects against a future iter accidentally
//! reintroducing the `Gc` variant or its CLI parse arm.
use std::process::Command;
fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") }
#[test]
fn ail_build_rejects_alloc_gc_with_unknown_value_error() {
let out = Command::new(ail_bin())
.args(["build", "examples/hello.ail", "--alloc=gc", "-o", "/tmp/ail_boehm_retirement_pin"])
.current_dir(env!("CARGO_MANIFEST_DIR").to_string() + "/../..")
.output()
.expect("spawn ail build");
assert!(
!out.status.success(),
"expected `ail build --alloc=gc` to FAIL after Boehm retirement; exit was success.\nstderr:\n{}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("unknown --alloc value"),
"expected stderr to contain `unknown --alloc value`; got:\n{stderr}"
);
assert!(
stderr.contains("`gc`"),
"expected stderr to name the offending value `gc`; got:\n{stderr}"
);
}
```
- [ ] **Step 2: Run the test to verify it FAILS (RED)**
Run: `cargo test -p ail --test boehm_retirement_pin ail_build_rejects_alloc_gc_with_unknown_value_error`
Expected: FAIL. Failure mode: the assert `!out.status.success()` fires because today `ail build --alloc=gc` returns exit 0. The exact panic message is:
```
expected `ail build --alloc=gc` to FAIL after Boehm retirement; exit was success.
```
This RED state is the iteration's starting point. It turns GREEN after Task 2.
---
## Task 2: Core variant removal + compile-gated caller threading
**Goal:** Atomically remove `AllocStrategy::Gc` from the enum and
every caller in the workspace, in one cohesive edit. The compile
gate at the end of this task requires the entire workspace to
build, so every caller must be threaded inside this task — no
deferrals. After this task the Task-1 RED test turns GREEN.
**Files:**
- Modify: `crates/ailang-codegen/src/lib.rs` (enum, default flips, in-source negative-complement test, `Default` derive)
- Modify: `crates/ail/src/main.rs` (CLI parse, link branch, staticlib-guard diagnostic)
- [ ] **Step 1: Drop the `Gc` variant from `AllocStrategy`**
Edit `crates/ailang-codegen/src/lib.rs:158-164` from:
```rust
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum AllocStrategy {
#[default]
Gc,
Bump,
Rc,
}
```
to:
```rust
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AllocStrategy {
Bump,
Rc,
}
```
Rationale: the `Default` derive + `#[default]` marker on `Gc` is removed entirely. Recon confirmed (compile-driven `grep` for `AllocStrategy::default` / `unwrap_or_default`) that no caller in the workspace invokes `AllocStrategy::default()`, so the trait derivation has no semantic role to preserve.
- [ ] **Step 2: Drop the `Gc` arm from `fn_name`**
Edit `crates/ailang-codegen/src/lib.rs:186-191` from:
```rust
fn fn_name(&self) -> &'static str {
match self {
AllocStrategy::Gc => "GC_malloc",
AllocStrategy::Bump => "bump_malloc",
AllocStrategy::Rc => "ailang_rc_alloc",
}
}
```
to:
```rust
fn fn_name(&self) -> &'static str {
match self {
AllocStrategy::Bump => "bump_malloc",
AllocStrategy::Rc => "ailang_rc_alloc",
}
}
```
Note: spec §"Concrete code shapes" names this method `runtime_alloc_fn`; recon confirmed the actual identifier is `fn_name`. No rename — `fn_name` stays.
- [ ] **Step 3: Flip the default-strategy entry points**
Edit `crates/ailang-codegen/src/lib.rs:281-283` from:
```rust
pub fn lower_workspace(ws: &Workspace) -> Result<String> {
lower_workspace_inner(ws, AllocStrategy::Gc, Target::Executable)
}
```
to:
```rust
pub fn lower_workspace(ws: &Workspace) -> Result<String> {
lower_workspace_inner(ws, AllocStrategy::Rc, Target::Executable)
}
```
Edit `crates/ailang-codegen/src/lib.rs:293-295` from:
```rust
pub fn lower_workspace_staticlib(ws: &Workspace) -> Result<String> {
lower_workspace_inner(ws, AllocStrategy::Gc, Target::StaticLib)
}
```
to:
```rust
pub fn lower_workspace_staticlib(ws: &Workspace) -> Result<String> {
lower_workspace_inner(ws, AllocStrategy::Rc, Target::StaticLib)
}
```
- [ ] **Step 4: Retarget the in-source negative-complement codegen test**
Edit `crates/ailang-codegen/src/lib.rs:3571-3576` from:
```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"),
"gc IR should not declare/define any per-type drop fn. IR was:\n{ir_gc}"
);
```
to:
```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"),
"bump IR should not declare/define any per-type drop fn. IR was:\n{ir_bump}"
);
```
Also update the doc-comment at `:3495` from `Negative complement: under \`--alloc=gc\` no drop fn is` to `Negative complement: under \`--alloc=bump\` no drop fn is`.
- [ ] **Step 5: Drop the `Gc` arm from CLI parse**
Edit `crates/ail/src/main.rs:2143-2148` from:
```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`)"),
}
```
to:
```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`)"),
}
```
- [ ] **Step 6: Drop the `Gc` arm from the link-branch match**
Edit `crates/ail/src/main.rs:2389-2395` (the `match strategy { AllocStrategy::Gc => { … cmd.arg("-lgc"); … } …}` block). Delete the entire `AllocStrategy::Gc => { … }` arm, including the inline comment block at `:2390-2395` that starts with `// Boehm conservative GC (the transitional dual-allocator). The lowered // IR calls @GC_malloc; libgc supplies it. Pthread/dl are // pulled in transitively via libgc.so on Linux, so a single // -lgc flag is enough.` and ends at the comma after the `Gc` arm's closing brace.
The surviving match (only `AllocStrategy::Bump => { … }` and `AllocStrategy::Rc => { … }` arms) is now exhaustive after Step 1's enum-variant removal — Rust's exhaustiveness check confirms this at compile time, and any miss is a hard `error[E0004]` that surfaces in Step 9.
- [ ] **Step 7: Rewrite the staticlib-guard diagnostic**
Edit `crates/ail/src/main.rs:2497-2498` from:
```rust
"staticlib (swarm) artefact is RC-only — `--alloc=gc` links the \
shared Boehm collector, which is not swarm-safe; use `--alloc=rc`"
```
to:
```rust
"staticlib (swarm) artefact is RC-only — `--alloc=bump` links a \
leak-only bench instrument, not swarm-safe; use `--alloc=rc`"
```
Critical: preserve the exact prefix `staticlib (swarm) artefact is RC-only` verbatim. The surviving test `staticlib_bump_is_rejected` asserts `stderr.contains("staticlib (swarm) artefact is RC-only")` — keeping the prefix keeps that test green.
- [ ] **Step 8: Update the clap-derive doc-comment on the `--alloc` flag**
Edit `crates/ail/src/main.rs:141-142` from:
```rust
/// (`@GC_malloc`, libgc); it is kept as a differential parity
/// designed for. `gc` retains the Boehm conservative GC path
```
to a two-line clap-doc that mentions only `rc` (canonical default) and `bump` (bench-floor). Surrounding clap-doc text near this enum-arg may need a coherent rewrite — the implementer reads the full clap-doc block (`:135-150` area) and produces an internally consistent two-allocator description:
```rust
/// Allocator backend. `rc` is the canonical production allocator
/// (reference counting + uniqueness inference); `bump` is a raw-alloc
/// bench-floor (no free; leak-tolerant; bench-only, not a production
/// target).
```
- [ ] **Step 9: Compile-gate the whole workspace**
Run: `cargo build --workspace --tests 2>&1 | tail -40`
Expected output: `0 errors` in any crate. If any `error[E0599]`, `error[E0004]`, `error[E0432]`, or `error[E0061]` surfaces, the offending site is a missed caller of `AllocStrategy::Gc` — return to Steps 1-8 and thread it inside this task. **DO NOT defer to a later task.**
- [ ] **Step 10: Verify Task-1 RED test now GREEN**
Run: `cargo test -p ail --test boehm_retirement_pin ail_build_rejects_alloc_gc_with_unknown_value_error`
Expected: PASS. `ail build --alloc=gc` now fails at the CLI parser (Step 5) with stderr containing `unknown --alloc value` and `\`gc\``.
---
## Task 3: E2E test suite cleanup
**Goal:** Delete the 3 pure-differential e2e tests and their fixture; strip the gc-arm from the 9 RC-feature tests that used GC as a differential backstop (keep their absolute fixed-stdout pin); delete `staticlib_gc_is_rejected`. After this task, no test in the workspace builds against `--alloc=gc`.
**Files:**
- Modify: `crates/ail/tests/e2e.rs` (3 fn deletions + 9 gc-arm strippings + doc-comment cleanup)
- Modify: `crates/ail/tests/embed_staticlib_alloc_guard.rs` (1 fn deletion + file-doc update)
- Delete: `examples/gc_stress.ail`
- [ ] **Step 1: Delete the 3 pure-differential e2e tests**
In `crates/ail/tests/e2e.rs`, delete entire blocks:
- Lines 186-203: the `/// stress the Boehm conservative GC integration end-to-end. …` doc-comment + `#[test]` + `fn gc_handles_recursive_list_construction() { … }` body.
- Lines 1396-1408: the doc-comment + `#[test]` + `fn alloc_rc_produces_same_stdout_as_gc() { … }` body.
- Lines 1559-1573: the doc-comment + `#[test]` + `fn alloc_rc_matches_gc_on_std_list_demo() { … }` body.
- [ ] **Step 2: Strip the gc-arm from 9 RC-feature tests**
For each test below, remove the `let stdout_gc = build_and_run_with_alloc(example, "gc");` (or fixture-literal) build call AND the differential assertion `assert_eq!(stdout_gc, stdout_rc, "…")`. Keep the absolute `assert_eq!(stdout_rc.trim(), "<n>")` pin and the `build_and_run_with_alloc(example, "rc")` (or equivalent) call.
The 9 tests, mapped by gc-call line (recon-verified):
| Test fn (`#[test]` line) | gc-call line | Differential assert line |
|---|---|---|
| `reuse_as_demo_under_rc_uses_inplace_rewrite` (`:1446`) | 1475 | — (no differential) |
| `alloc_rc_emits_dec_for_unique_let_bound_box` (`:1600`) | 1602 | 1606 |
| `alloc_rc_recursive_list_sum` (`:1626`) | 1628 | 1634 |
| `alloc_rc_borrow_only_recursive_list_drop` (`:1672`) | 1674 | 1680 |
| `alloc_rc_partial_drop_skips_moved_keeps_wildcarded` (`:1760`) | 1762 | 1768 |
| `alloc_rc_own_param_dec_at_fn_return` (`:1856`) | 1858 | 1864 |
| `alloc_rc_drop_iterative_handles_million_cell_list` (`:1979`) | 1981 | 1987 |
| `alloc_rc_pattern_bind_in_implicit_fn_does_not_dec_borrowed_children` (`:2166`) | 2168 | 2174 |
| `alloc_rc_let_alias_of_implicit_param_does_not_dec_borrowed_children` (`:2394`) | 2396 | 2402 |
For each test:
1. Delete the line `let stdout_gc = build_and_run_with_alloc(<example>, "gc");`
2. Delete the line `assert_eq!(stdout_gc, stdout_rc, "alloc=rc must match alloc=gc …");` (if present — `reuse_as_demo_under_rc_uses_inplace_rewrite` does not have one).
3. Leave the absolute `assert_eq!(stdout_rc.trim(), "<n>")` pin (or `assert_eq!(stdout_gc.trim(), "9")` in `reuse_as_demo_under_rc_uses_inplace_rewrite` — rename the binding to `stdout_rc` first, since we are dropping the `_gc` build call).
For `reuse_as_demo_under_rc_uses_inplace_rewrite` specifically (line 1446ff), the body currently builds gc, asserts `stdout_gc.trim() == "9"`, then builds rc; rewrite to: build rc only, assert `stdout_rc.trim() == "9"`. The absolute pin (`= "9"`) is the surviving correctness pin.
- [ ] **Step 3: Scrub gc-arm doc-comments in surviving tests**
Per-test doc-comments at `:1432`, `:1614`, `:1665`, `:1752`, `:1841`, `:1975-1976`, `:2160`, `:2392` reference `--alloc=gc` as the comparison oracle. Rewrite each block to describe the test as an absolute RC-correctness pin (the absolute stdout assertion is the spec; no oracle reference). Replace phrases like "Stdout matches `--alloc=gc` byte-for-byte (`11`)" with "Stdout is `11` (the head element)"; replace "produces the same stdout (allocator-equivalence backstop; 1M cells at 24B each ≈ 24MB, well within Boehm's …)" with a self-contained describing-the-fixture comment.
- [ ] **Step 4: Delete the `examples/gc_stress.ail` fixture**
Run: `git rm examples/gc_stress.ail`
Expected: file deleted from working tree (still unstaged-as-deletion via `git rm` since Boss-style commit is later).
Verify: `grep -rn 'gc_stress' --include='*.rs' --include='*.ail' --include='*.ail.json' --include='*.sh' 2>/dev/null` returns NO hits after Step 1 + Step 4 land.
- [ ] **Step 5: Delete `staticlib_gc_is_rejected` from the staticlib-guard test**
In `crates/ail/tests/embed_staticlib_alloc_guard.rs`:
- Delete lines `:20-32` (the entire `#[test] fn staticlib_gc_is_rejected() { … }` block).
- Keep `:34-46` (`staticlib_bump_is_rejected`) unchanged.
- Edit the file-level `//!` doc at `:1-4` from:
```rust
//! M2: `ail build --emit=staticlib` is RC-only. `--alloc=gc`/`--alloc=bump`
//! must fail the build (the shared Boehm collector / bench stub are not
//! swarm-safe). RED until the Task-4 CLI guard lands: today the alloc
//! strategy is passed straight through and the build SUCCEEDS.
```
to:
```rust
//! M2: `ail build --emit=staticlib` is RC-only. `--alloc=bump`
//! must fail the build (the bump bench stub is leak-only and
//! not swarm-safe). `--alloc=gc` no longer exists as a CLI value
//! (Boehm full retirement); rejection of `gc` now happens at the
//! CLI parser and is pinned by `tests/boehm_retirement_pin.rs`.
```
- [ ] **Step 6: Run the affected test files to verify GREEN**
Run: `cargo test -p ail --test e2e 2>&1 | tail -20`
Expected: all surviving tests pass. The 3 deleted tests no longer appear in the output; the 9 stripped tests pass without `stdout_gc`. `0 failed`.
Run: `cargo test -p ail --test embed_staticlib_alloc_guard 2>&1`
Expected: `running 1 test` (only `staticlib_bump_is_rejected` survives) + `test result: ok. 1 passed`.
---
## Task 4: Code doc-comment + comment scrub
**Goal:** Scrub all in-code comments and doc-comments (rust + C) that still mention Boehm, `GC_malloc`, `libgc`, or `--alloc=gc`. After this task, the codebase under `crates/` and `runtime/` has zero textual Boehm references except inside the new honesty-pin absence-pins (Task 7).
**Files:**
- Modify: `crates/ailang-codegen/src/lib.rs:146-147,237-242,287-292,530,781,802,1040,1211`
- Modify: `crates/ailang-codegen/src/escape.rs:5,96`
- Modify: `crates/ailang-codegen/src/match_lower.rs:40,113`
- Modify: `crates/ail/src/main.rs:2189,2263-2264,2365-2366,2425`
- Modify: `runtime/bump.c:4,10-11,29`
- Modify: `runtime/rc.c:38,167,169`
- Modify: `runtime/str.c:32`
- [ ] **Step 1: Codegen `lib.rs` module-level doc + per-fn docs**
Edit each site to drop GC-specific naming:
- `:146-147`: `\`Gc\` is the default (Boehm conservative GC).` → delete; replace surrounding two lines so the enum doc reads `\`Rc\` is the default (canonical production allocator; reference counting + uniqueness). \`Bump\` is a raw-alloc bench-floor (leak-only; bench harness only).`
- `:237-242` and `:287-292`: doc-comments on `lower_workspace_with_alloc` / `lower_workspace_staticlib` — strip the `AllocStrategy::Gc` references; the two surviving strategies are `Rc` (default) and `Bump`.
- `:530`: inline comment about `-lgc` → drop or rewrite to bump-only context.
- `:781`: doc-comment `Such allocations lower to \`alloca\` instead of \`@GC_malloc\`.` → `Such allocations lower to \`alloca\` instead of the runtime allocator (\`@ailang_rc_alloc\` or \`@bump_malloc\`).`
- `:802`: doc-comment naming `--alloc=gc`/`--alloc=bump` → rewrite to `--alloc=bump` only (the gc reference is gone; bump still has no drop fn).
- `:1040`: inline comment `// GC_malloc (Boehm conservative collector, Iter 14f).` → rewrite to allocator-agnostic (`// the runtime allocator (Iter 14f's escape-analysis target).`)
- `:1211`: inline comment `between \`alloca\` (non-escaping) and \`@GC_malloc\` (escaping)` → `between \`alloca\` (non-escaping) and the runtime allocator (escaping).`
- [ ] **Step 2: `escape.rs` and `match_lower.rs`**
`crates/ailang-codegen/src/escape.rs:5`: module `//!` mentioning `@GC_malloc` → `@ailang_rc_alloc` or allocator-agnostic phrasing (the latter is preferred — escape analysis is allocator-independent).
`crates/ailang-codegen/src/escape.rs:96`: doc-comment naming `@GC_malloc` → same allocator-agnostic phrasing.
`crates/ailang-codegen/src/match_lower.rs:40`: `/// @GC_malloc` doc-comment → drop or rewrite to the surviving allocator's name.
`crates/ailang-codegen/src/match_lower.rs:113`: `// @GC_malloc for everything else` → `// the runtime allocator for everything else`.
- [ ] **Step 3: `crates/ail/src/main.rs` non-link/parse comments**
- `:2189`: doc-comment `compiles this stub and links it instead of \`-lgc\`` → `compiles this stub for the bump bench-floor build`.
- `:2263-2264`: doc-comment `\`@bump_malloc\` instead of \`@GC_malloc\` and links \`runtime/bump.c\` in lieu of \`-lgc\`` → `\`@bump_malloc\` for the bump bench-floor; links \`runtime/bump.c\` statically`.
- `:2365-2366`: comment `Under --alloc=gc / --alloc=bump the alloc strategy still governs ADT allocation (GC_malloc / bump_malloc); rc.c` → `Under --alloc=bump the alloc strategy still governs ADT allocation (bump_malloc); rc.c`.
- `:2425`: comment `\`@ailang_rc_alloc\`-vs-\`@GC_malloc\` selection in the IR` → `\`@ailang_rc_alloc\`-vs-\`@bump_malloc\` selection in the IR`.
- [ ] **Step 4: `runtime/*.c` header comments**
`runtime/bump.c:4`: `against the default Boehm-GC build.` → `as the bench-floor allocator paired with the canonical RC runtime.`
`runtime/bump.c:10-11`: `The signature mirrors \`GC_malloc\` from libgc: \`void *bump_malloc(size_t)\`. The codegen lowers every \`call ptr @GC_malloc\` to \`call ptr @bump_malloc\` …` → `The function signature \`void *bump_malloc(size_t)\` is the bench-floor allocator interface; codegen lowers ADT/lambda/closure-pair allocation sites to \`call ptr @bump_malloc\` when the bench harness selects \`--alloc=bump\`.`
`runtime/bump.c:29`: `Matches the alignment Boehm gives us.` → `8-byte alignment matches the ADT box layout (one or two \`ptr\`s).`
`runtime/rc.c:38`: layout comment naming `GC_malloc` → rewrite to allocator-agnostic (the comment is about box layout, not allocator). E.g. `Layout fixed across all allocators (RC and bump) — the header sits at byte offset 0; the payload starts at byte 8.`
`runtime/rc.c:167`: `Boehm's behaviour on OOM is also "abort", so this matches.` → drop the sentence; OOM-behaviour is documented as `abort()` for the RC runtime without needing a comparison to Boehm.
`runtime/rc.c:169`: `\`GC_malloc\`'s contract` → drop or rephrase to allocator-agnostic.
`runtime/str.c:32`: `binaries that do not pull in \`runtime/rc.c\` (i.e. \`--alloc=gc\` and \`--alloc=bump\`…)` → `binaries that do not pull in \`runtime/rc.c\` (i.e. \`--alloc=bump\` bench builds).`
- [ ] **Step 5: Compile-gate**
Run: `cargo build --workspace --tests 2>&1 | tail -10`
Expected: `0 errors`. Comments are not load-bearing, but doc-comment misformatting can fail cargo-doc — surface any warning the rust toolchain raises and fix inline.
- [ ] **Step 6: Per-region grep verification**
Run: `grep -rn -E 'Boehm|libgc|GC_malloc|--alloc=gc' crates/ailang-codegen/ crates/ail/src/ runtime/ 2>/dev/null`
Expected: zero hits.
---
## Task 5: Bench harness rework
**Goal:** Drop the gc-arm from `bench/run.sh` (throughput table columns + latency-bench gc fixture) and update `bench/check.py` to parse the new (gc-less) output. Update `bench/baseline.json` to the post-retirement schema via `bench/check.py --update-baseline`. After this task: bench runs use only `bump` and `rc` modes; `bench/check.py` exits 0 against the regenerated baseline.
**Files:**
- Modify: `bench/run.sh`
- Modify: `bench/check.py`
- Modify: `bench/baseline.json` (regenerated)
- [ ] **Step 1: `bench/run.sh` — header comments + modes array**
`:3-11`: rewrite the header block from:
```bash
# GC-overhead bench harness (Bench iter).
#
# Builds each fixture twice — `--alloc=gc` (Boehm conservative GC) and
# `--alloc=bump` (no-free 256 MB arena from `runtime/bump.c`). Runs each
# binary N times, drops the slowest run, takes the median wall time.
# The bump number minus the gc number is the upper-bound cost of GC.
#
# Output: a table with gc-median, bump-median, overhead %, and max RSS
# for both modes. Designed to be captured verbatim into a commit body.
```
to:
```bash
# RC-overhead bench harness.
#
# Builds each fixture twice — `--alloc=rc` (canonical RC runtime) and
# `--alloc=bump` (no-free 256 MB arena from `runtime/bump.c`, the
# raw-alloc bench-floor). Runs each binary N times, drops the slowest
# run, takes the median wall time. The rc-over-bump ratio is the
# bench-health regression gate.
#
# Output: a table with bump-median, rc-median, rc/bump ratio, and max
# RSS for both modes. Designed to be captured verbatim into a commit
# body.
```
`:65`: change `modes=(gc bump rc)` to `modes=(bump rc)`.
- [ ] **Step 2: `bench/run.sh` — throughput table columns**
`:149-167`: rewrite the throughput-table block. Replace the 9-column header at `:155-157` with a 6-column header:
```bash
# Header.
printf "%-22s | %10s | %10s | %10s | %12s | %12s\n" \
"workload" "bump(s)" "rc(s)" "rc/bump" "bump RSS(KB)" "rc RSS(KB)"
printf -- "-----------------------+------------+------------+------------+--------------+--------------\n"
```
Replace the per-fixture loop at `:159-168` with:
```bash
for f in "${fixtures[@]}"; do
read -r bp_t bp_r < <(median_run "$OUTDIR/${f}_bump")
read -r rc_t rc_r < <(median_run "$OUTDIR/${f}_rc")
# Guard against bump_t == 0 (LLVM-folded sub-microsecond fixtures).
rc_ratio=$(awk -v r="$rc_t" -v b="$bp_t" 'BEGIN { if (b+0 == 0) printf "n/a"; else printf "%.2fx", r / b }')
printf "%-22s | %10s | %10s | %10s | %12s | %12s\n" \
"$f" "$bp_t" "$rc_t" "$rc_ratio" "$bp_r" "$rc_r"
done
```
`:153-154`: rewrite the comment from `the decisive number for Decision 10's Boehm-retirement target (1.3x).` to `the RC-overhead-vs-bump bench-health regression gate (1.3× ceiling on linear/tree corpus, ±15% on closure-chain).`
- [ ] **Step 3: `bench/run.sh` — latency bench gc fixture**
`:170-205`: rewrite the latency-bench block. Replace the comment at `:170-180` (mentioning "Boehm-fair Implicit @ gc, RC-fair explicit @ rc"). Drop the gc build call at `:190` (`"$AIL" build --opt=-O2 --alloc=gc "$LAT_IMPL_SRC" -o "$OUTDIR/bench_latency_implicit_gc"`). Drop the `"$PY" "$LAT_HARNESS" "$OUTDIR/bench_latency_implicit_gc" --runs 5 --label "implicit @ gc (Boehm-fair)"` line at `:200`.
The latency block now runs two arms only: `explicit @ rc` and `implicit @ rc`. Update the comment to describe this — the two surviving arms are the RC-canonical measurements (explicit mode at rc + implicit mode at rc as control); the historical "Boehm-fair" arm is excised.
- [ ] **Step 4a: `bench/check.py` — update throughput-table header detection + column count**
Edit `bench/check.py:62` from:
```python
if line.startswith("workload") and "gc(s)" in line:
in_table = True
```
to:
```python
if line.startswith("workload") and "bump(s)" in line:
in_table = True
```
Edit `bench/check.py:72` from:
```python
if len(cells) != 9:
continue
```
to:
```python
if len(cells) != 6:
continue
```
Critical: this `bench/check.py:62` header-sentinel + `:72` column-count are hardcoded to match `bench/run.sh`'s table shape. Without updating both, `parse_throughput_table` silently returns an empty dict (no rows enter the table because the gc(s) sentinel never matches), and every workload metric reads as missing — a degenerate "passing" check that pins nothing. Pair-edit with Step 1-3's `bench/run.sh` table-shape change.
- [ ] **Step 4b: `bench/check.py` — drop gc-keyed throughput fields**
Edit `bench/check.py:75-85` from:
```python
out[workload] = {
"gc_s": float(cells[1]),
"bump_s": float(cells[2]),
"rc_s": float(cells[3]),
"gc_over_bump": float(cells[4].rstrip("x")),
"rc_over_bump": float(cells[5].rstrip("x")),
"gc_rss_kb": float(cells[6]),
"bump_rss_kb": float(cells[7]),
"rc_rss_kb": float(cells[8]),
}
```
to:
```python
out[workload] = {
"bump_s": float(cells[1]),
"rc_s": float(cells[2]),
"rc_over_bump": float(cells[3].rstrip("x")),
"bump_rss_kb": float(cells[4]),
"rc_rss_kb": float(cells[5]),
}
```
Cell indices shift from {1,2,3,4,5,6,7,8} (9-column table) to {1,2,3,4,5} (6-column table) matching the new `bench/run.sh` output format.
- [ ] **Step 4c: `bench/check.py` — drop the `implicit @ gc` arm label mapping**
Edit `bench/check.py:91-97` from:
```python
# Latency arm header looks like: "=== implicit @ gc (Boehm-fair) ===".
# We map the leading prose label to the canonical arm key in baseline.json.
ARM_LABEL_TO_KEY = {
"implicit @ gc": "implicit_at_gc",
"explicit @ rc": "explicit_at_rc",
"implicit @ rc": "implicit_at_rc",
}
```
to:
```python
# Latency arm header looks like: "=== explicit @ rc (RC-fair) ===".
# We map the leading prose label to the canonical arm key in baseline.json.
ARM_LABEL_TO_KEY = {
"explicit @ rc": "explicit_at_rc",
"implicit @ rc": "implicit_at_rc",
}
```
- [ ] **Step 5: Regenerate `bench/baseline.json`**
Per the `bench/check.py` Usage block (lines 16-21), the canonical invocations are:
- `bench/check.py` — spawns `bench/run.sh -n 5` internally
- `bench/check.py --from-file out.txt` — consumes captured output
- `cat out.txt | bench/check.py --stdin` — consumes stdin
- `bench/check.py --update-baseline` — re-runs and writes new baseline
Run: `bench/check.py --update-baseline 2>&1 | tail -20`
Expected: the script runs `bench/run.sh -n 5`, parses the post-retirement table (now with 6 columns) and the post-retirement latency block (now without the `implicit @ gc` arm), and writes the new `bench/baseline.json`. Output ends with a `wrote new baseline` (or similar) confirmation.
Verify the new baseline shape: `python3 -c 'import json; b=json.load(open("bench/baseline.json")); w=next(iter(b["throughput"].values())); print(sorted(w.keys()))'`
Expected: a sorted list of metric keys with NO `gc_s` / `gc_over_bump` / `gc_rss_kb` entries. Surviving keys: `bump_s`, `rc_s`, `rc_over_bump`, `bump_rss_kb`, `rc_rss_kb` (and per-metric tolerance entries if the baseline schema nests them).
Run: `python3 -c 'import json; b=json.load(open("bench/baseline.json")); print(sorted(b.get("latency",{}).keys()))'`
Expected: list contains `explicit_at_rc` and `implicit_at_rc`, NOT `implicit_at_gc`.
- [ ] **Step 6: Verify `bench/check.py` exits 0 on the regenerated baseline**
Run: `bench/check.py 2>&1; echo "exit: $?"`
Expected: `exit: 0`. The just-regenerated baseline matches a fresh run (the same `bench/run.sh -n 5` produces output within the per-metric tolerance against itself); no `gc_*` keys are demanded.
---
## Task 6: Design ledger updates
**Goal:** Bring the design ledger into post-retirement present-tense honesty. After this task: `design/models/rc-uniqueness.md` and `design/models/pipeline.md` describe RC + bump as the only allocators; `design/contracts/scope-boundaries.md` and `design/contracts/memory-model.md` carry no Boehm prose; `design/contracts/embedding-abi.md:42-44` reframes the staticlib-guard justification around bump.
**Files:**
- Modify: `design/models/rc-uniqueness.md`
- Modify: `design/models/pipeline.md`
- Modify: `design/contracts/scope-boundaries.md`
- Modify: `design/contracts/memory-model.md`
- Modify: `design/contracts/embedding-abi.md`
- [ ] **Step 1: `design/models/rc-uniqueness.md` — excise Boehm sections**
Delete `:3-32` (the entire `## Dual allocator — RC canonical, Boehm parity oracle` section, from the heading down to the last sentence "the rest of this section documents the Boehm half, retained as the oracle.").
Delete `:34-69` (the `**Choice: Boehm-Demers-Weiser conservative GC.**` block and its full `Rationale:` + `Trade-offs accepted:` bullet lists, ending with "Users without it get a link-time error from clang, not a silent failure.").
- [ ] **Step 2: `design/models/rc-uniqueness.md` — generalise Per-fn arena section**
Edit `:71-79` (the `## Per-fn arena via stack \`alloca\`` section) to generalise the Boehm-specific language:
`This optimisation is layered on top of Boehm in its simplest form.` → `This optimisation is layered on top of the canonical RC runtime.`
`instead of \`@GC_malloc\`. Allocations that may escape continue to use \`@GC_malloc\`. The Boehm collector is still linked and unchanged; this is purely an optimisation above the floor.` → `instead of the runtime allocator. Allocations that may escape continue to use the runtime allocator. The runtime is unaffected; escape analysis is purely an optimisation above the floor.`
Edit `:115-127` (the `**Codegen integration.**` block):
- `:124-125`: `\`alloca i8, i64 <size>, align 8\`; on a miss it writes \`call ptr @GC_malloc(i64 <size>)\`.` → `\`alloca i8, i64 <size>, align 8\`; on a miss it writes \`call ptr @ailang_rc_alloc(i64 <size>)\` (or the bump-mode equivalent).`
- [ ] **Step 3: `design/models/rc-uniqueness.md` — memory-model section reframe**
Edit `:132-156` (the `## Memory model — RC + Uniqueness …` section). Rewrite the opening paragraph at `:134-144` to drop the Boehm-bench framing:
Before (`:134-144`):
> **The GC bench (`bench/run.sh`) showed Boehm contributing a substantial fraction of runtime on allocation-heavy workloads that hold the heap fully live (raw numbers in `bench/orchestrator-stats/`; prior bench iter commit bodies record the runs). The mainstream "RC + inference" position is 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 structurally 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.
After:
> **AILang commits to reference counting with static uniqueness inference as the canonical memory model, extended with mandatory LLM-author mode annotations (`borrow` / `own`), explicit `clone`, first-class `reuse-as`, and `drop-iterative` data attrs.**
>
> RC's costs are bounded and analysable per program point; the canonical position is "RC + inference" sharpened with the five LLM-author mechanisms below. A corpus committed to one memory model is expensive to switch — the commitment lives in the contracts (`memory-model.md`, `language-constraints.md`).
Then edit `:150-156` (the **Choice.** paragraph) from:
> **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 (see the dual-allocator section above) and is retired when the RC pipeline matches the bump-allocator floor within an acceptable margin (target 1.3× on `bench/run.sh`).
to:
> **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. 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.
- [ ] **Step 4: `design/models/rc-uniqueness.md` — closure-chain wider band**
Edit `:170-183` to drop Boehm-retirement framing. The phrase `a Boehm-retirement follow-up` at `:173` becomes `a future closure-pair slab/pool optimisation`. The phrase `excluded from the Boehm-retirement gate until a slab/pool answer ships` at `:179-180` becomes `excluded from the linear/tree 1.3× regression gate; the closure-chain corpus has its own ±15% band until a slab/pool optimisation ships`.
- [ ] **Step 5: `design/models/pipeline.md` — pipeline diagram + prose**
Edit `:14-17` (pipeline diagram) from:
```
└─ clang -O2 *.ll -o binary
--alloc=rc → emits inc/dec (@ailang_rc_inc / _dec; canonical, default)
--alloc=gc → links libgc (@GC_malloc; parity oracle)
```
to:
```
└─ 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)
```
Edit `:19-28` (the accompanying prose). Replace:
> Two allocator backends share the same MIR. `--alloc=rc` is the canonical backend committed to in the [memory model](../contracts/memory-model.md) and the CLI default. 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 rewrites respectively. `--alloc=gc` selects the transitional Boehm backend (see [RC + uniqueness](rc-uniqueness.md)); `--alloc=rc` is the canonical backend and the CLI default.
with:
> Two allocator backends share the same MIR. `--alloc=rc` is the canonical backend committed to in the [memory model](../contracts/memory-model.md) and the CLI default; 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 rewrites respectively. `--alloc=bump` selects the raw-alloc bench-floor (`runtime/bump.c`, no free, leak-only) and is used by `bench/run.sh` to measure RC overhead against the structurally cheapest allocator — it is not a production target.
- [ ] **Step 6: `design/contracts/scope-boundaries.md` — rewrite Boehm block**
Edit `:67` (the `==` polymorphic codegen list). The current `@printf` / `@GC_malloc` parenthetical should become `@printf` / `@ailang_rc_alloc` (or just `@printf` — the canonical declared-extern set in the LLVM IR header; the implementer reads the header at codegen-lib emit-time and writes whichever extern is actually paired with `@printf` in the current header).
Edit `:114-127` (the `**Memory management via Boehm conservative GC**` bullet). Rewrite to:
```markdown
- **Memory management via reference counting + uniqueness inference**
(see [RC + uniqueness](../models/rc-uniqueness.md)), with
**per-fn arena via stack `alloca` for non-escaping allocations**
layered on top. Every ADT box, lambda env, and closure pair
allocates either via `@ailang_rc_alloc` (escaping; RC-managed
with inc/dec instrumentation per the memory model) or via LLVM
`alloca` (non-escaping; freed at fn return). The decision is
made by an escape-analysis pre-pass over the fn body — see the
"Per-fn arena via stack `alloca`" subsection of
[RC + uniqueness](../models/rc-uniqueness.md). The per-fn-arena
path is exercised end-to-end by
`examples/escape_local_demo.ail.json`.
```
The `examples/gc_stress.ail.json` reference is dropped entirely (file does not exist; the comment was always doc-archaeology pointing at a non-existent file). The `examples/std_list_stress.ail.json` reference is dropped similarly — its purpose ("Boehm-only soak tests") no longer exists.
- [ ] **Step 7: `design/contracts/memory-model.md` — drop "pre-Boehm era"**
Edit `:230-232` from:
> Codegen for `Term::Ctor` / `Term::Lam` env / closure pair under `--alloc=rc` calls `ailang_rc_alloc(SIZE)`. The initial RC plumbing stops there — inc/dec instrumentation is added once the inference is wired up. Until then, `--alloc=rc` deliberately leaks like the pre-Boehm era; this is purely about plumbing.
to (post-retirement, the "until then" branch is closed since RC inc/dec instrumentation is wired up):
> Codegen for `Term::Ctor` / `Term::Lam` env / closure pair under `--alloc=rc` calls `ailang_rc_alloc(SIZE)`; inc/dec instrumentation is emitted per the uniqueness inference. `--alloc=bump` selects the bench-floor allocator, which leaks by design (no inc/dec, no free); it is bench-only and never a production target.
- [ ] **Step 8: `design/contracts/embedding-abi.md` — staticlib-guard prose**
Edit `:42-44` from:
> `ail build --emit=staticlib` rejects `--alloc=gc`/`--alloc=bump`
> (the shared Boehm collector is not swarm-safe). The value/record
to:
> `ail build --emit=staticlib` rejects `--alloc=bump` (the bench
> stub is leak-only and not swarm-safe; `--alloc=gc` no longer
> exists as a CLI value — see the Boehm-retirement iter). The
> value/record
- [ ] **Step 9: Per-file grep verification**
Run: `grep -rn -iE 'Boehm|libgc|GC_malloc|--alloc=gc' design/ 2>/dev/null`
Expected: zero hits.
---
## Task 7: Honesty-pin inversion + design_index protected-exception update
**Goal:** Flip `crates/ailang-core/tests/docs_honesty_pin.rs` from
asserting a present-tense Boehm anchor to asserting absence of
four Boehm-zombie strings, so the ledger cannot quietly re-grow
the retired narrative. Also drop the `"pre-Boehm"` protected-
exception entry from `design_index_pin.rs` since Task 6 removed
that phrase from `memory-model.md`.
**Files:**
- Modify: `crates/ailang-core/tests/docs_honesty_pin.rs`
- Modify: `crates/ailang-core/tests/design_index_pin.rs`
- [ ] **Step 1: Delete the present-tense Boehm anchor assertion**
Edit `crates/ailang-core/tests/docs_honesty_pin.rs:116-117`. Delete the two lines:
```rust
assert!(pipeline.contains("`--alloc=gc` selects the transitional Boehm backend"),
"models/pipeline.md must describe Boehm present-tense, not as 'on the path to retirement'");
```
- [ ] **Step 2: Add four absence-pins to `design_md_has_no_wunschdenken`**
Edit `crates/ailang-core/tests/docs_honesty_pin.rs:54-73` (the `design_md_has_no_wunschdenken` test body). Append four new `assert!(!d.contains(…))` assertions to the existing block:
```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");
```
- [ ] **Step 3: Update the file-level doc-comment**
Edit `crates/ailang-core/tests/docs_honesty_pin.rs:28-38` (the `design_corpus()` preamble doc-comment block). Replace the phrase `the Boehm/Decision-9 narrative (\`models/rc-uniqueness.md\`)` with `the RC + bump memory-model narrative (\`models/rc-uniqueness.md\`)` — the comment lists what the corpus scans, and the description should match post-retirement reality.
- [ ] **Step 4: Drop `"pre-Boehm"` from the protected-exception list**
Edit `crates/ailang-core/tests/design_index_pin.rs:166`. The current comment lists protected-exception strings including `"pre-Boehm"`. Since Task 6 Step 7 removed that phrase from `memory-model.md`, the protected-exception entry is stale. Drop just the `"pre-Boehm"` token from the comment-listed set; leave the other protected-exception entries (e.g. `"pre-existing"`, `"pre-set"`, `"pre-tail-call"`) untouched.
If the protected-exception list is enforced by code (not just comment), drop the corresponding code entry as well. Implementer reads `:165-180` area for the actual mechanism.
- [ ] **Step 5: Run docs_honesty_pin to verify GREEN**
Run: `cargo test -p ailang-core --test docs_honesty_pin 2>&1 | tail -20`
Expected: all tests pass (4 tests: `design_md_has_no_wunschdenken`, `design_md_has_no_doc_archaeology`, `design_md_present_tense_anchors_present`, `prose_roundtrip_md_has_no_wunschdenken`, `form_a_scalar_param_carveout_present_and_old_rule_absent`). The new absence-pins fire green because Tasks 4 + 6 already scrubbed `transitional Boehm` / `parity oracle` / `GC_malloc` / `libgc` from the ledger.
Run: `cargo test -p ailang-core --test design_index_pin 2>&1 | tail -10`
Expected: all tests pass.
---
## Task 8: Example fixture + agent prompt scrub
**Goal:** Scrub Boehm references from `.ail` fixture doc-comments
(no surface impact, only header prose) and from skill-agent
prompts. After this task, `examples/` and `skills/` carry no
Boehm prose.
**Files:**
- Modify: `examples/bench_latency_implicit.ail`
- Modify: `examples/bench_latency_explicit.ail`
- Modify: `examples/escape_local_demo.ail`
- Modify: `examples/reuse_as_demo.ail`
- Modify: `examples/rc_pin_recurse_implicit.ail`
- Modify: `skills/audit/agents/ailang-bencher.md`
- Modify: `skills/implement/agents/ailang-implementer.md`
- [ ] **Step 1: `examples/bench_latency_implicit.ail` doc-comment scrub**
The recon-enumerated lines `:4,10,12,15,23,150,185` contain header-prose references to Boehm. The `.ail` surface itself is untouched — these are top-of-file `(comment …)` blocks or inline comments. Each Boehm phrase rewrites to allocator-agnostic or post-retirement framing: e.g. "Boehm-fair Implicit @ gc" → "the implicit-mode latency curve at rc"; "under Boehm" → "under rc"; etc. The implementer reads each line in context and produces a coherent rewrite — there is no schema invariant at play.
Run the AILang round-trip after edit: `cargo test -p ailang-surface --test round_trip 2>&1 | tail -10` — must stay green (the round-trip pins the lossless print/parse property; a doc-comment change doesn't affect AST identity, so the .ail.json hash is unchanged, but the round-trip is the canonical sanity check).
- [ ] **Step 2: Per-fixture scrub**
For each fixture below, replace Boehm-mentioning doc-comment text with allocator-agnostic prose; do NOT touch any `(fn …)`, `(type …)`, `(body …)`, `(params …)` form — the changes are comment-only:
- `examples/bench_latency_explicit.ail:12-13` — `--alloc=gc` hypothesis reference
- `examples/escape_local_demo.ail:10` — "Boehm's heap is not touched at all"
- `examples/reuse_as_demo.ail:13,17` — `--alloc=gc` doc-comment
- `examples/rc_pin_recurse_implicit.ail:8` — `--alloc=gc` doc-comment
Verify each: the file still parses (`ail check <path>`) and still round-trips (the `cargo test -p ailang-surface --test round_trip` suite covers every fixture in `examples/`).
- [ ] **Step 3: `skills/audit/agents/ailang-bencher.md` rewrite**
Recon enumerated ≈18 Boehm-mentioning sites in this agent prompt. The pattern: the bencher agent uses Boehm as a worked example of a hypothesis-driven bench ("RC has better tail latency than Boehm under heap pressure because Boehm has stop-the-world pauses").
Rewrite each site to use an allocator-agnostic worked example. The natural replacement: a hypothesis comparing **two RC variants** or **RC vs. bump**, NOT RC vs. retired Boehm. Suggested replacement framing for the head sections:
- "RC has better tail latency than Boehm under heap pressure" → "RC under explicit `(own)` mode has better throughput than RC under implicit mode on allocation-heavy workloads — because explicit-mode lets `reuse-as` fire deterministically." (or similar — the implementer picks an example natural to the agent's tutorial purpose).
The implementer reads the full agent file, identifies the recurring "worked example uses Boehm" pattern, and produces a coherent replacement that maintains the agent's pedagogical structure. Each replacement may change wording around it; aim for one cohesive read, not 18 disconnected substitutions.
- [ ] **Step 4: `skills/implement/agents/ailang-implementer.md` rewrite**
Lines `:95,185`:
- `:95`: "Decision 10. Boehm is…" — rewrite to describe the canonical RC commitment without invoking Decision 10 (which referenced Boehm-retirement as a decision); the agent's purpose is to teach the implementer about the canonical memory model, so rewrite to a present-tense statement about RC + uniqueness.
- `:185`: "Implicit-mode RC numbers are tied with Boehm" — rewrite to a worked example that doesn't reference Boehm.
- [ ] **Step 5: Grep verification**
Run: `grep -rn -iE 'Boehm|libgc|GC_malloc|--alloc=gc' examples/ skills/ 2>/dev/null`
Expected: zero hits.
---
## Task 9: IR snapshot regeneration
**Goal:** The 5 checked-in IR snapshots (`hello.ll`, `list.ll`,
`max3.ll`, `sum.ll`, `ws_main.ll`) contain literal `declare ptr
@GC_malloc(i64)` and (in `list.ll`) `call ptr @GC_malloc(...)`.
After Task 2 flipped the codegen default from `Gc` to `Rc`, the
snapshots are stale. The `ir_snapshot_*` tests will fail until
the bytes are regenerated.
**Files:**
- Regenerate: `crates/ail/tests/snapshots/hello.ll`
- Regenerate: `crates/ail/tests/snapshots/list.ll`
- Regenerate: `crates/ail/tests/snapshots/max3.ll`
- Regenerate: `crates/ail/tests/snapshots/sum.ll`
- Regenerate: `crates/ail/tests/snapshots/ws_main.ll`
- [ ] **Step 1: Verify the snapshot tests currently FAIL**
Run: `cargo test -p ail --test ir_snapshot 2>&1 | tail -30`
Expected: 5 tests run, all 5 fail with `IR snapshot mismatch`. The failure messages will name the `.actual` files where the new IR was written (in the snapshot dir under `<example>.ll.actual`).
This is the post-Task-2 RED state for the snapshot suite. (Recon-confirmed: the `ir_snapshot.rs` harness writes the actual to `<snapshot>.ll.actual` when there's a mismatch, and panics with a diff hint.)
- [ ] **Step 2: Regenerate via UPDATE_SNAPSHOTS**
Run: `UPDATE_SNAPSHOTS=1 cargo test -p ail --test ir_snapshot 2>&1 | tail -20`
Expected: 5 tests print `updated snapshot: <path>` lines. Each `.ll` file is overwritten with the new IR.
- [ ] **Step 3: Verify the snapshots now contain `@ailang_rc_alloc` (not `@GC_malloc`)**
Run: `grep -l 'GC_malloc' crates/ail/tests/snapshots/ 2>/dev/null`
Expected: no output (no file contains `GC_malloc`).
Run: `grep -l 'ailang_rc_alloc' crates/ail/tests/snapshots/ 2>/dev/null`
Expected: at least `list.ll` and possibly `hello.ll` / others (depending on whether the example actually allocates — `sum.ll` and `max3.ll` may not allocate at all if their bodies are sum/max-of-literals, which the compiler folds).
- [ ] **Step 4: Verify the snapshot tests now PASS**
Run: `cargo test -p ail --test ir_snapshot 2>&1 | tail -10`
Expected: `test result: ok. 5 passed`.
- [ ] **Step 5: Clean up any leftover `.ll.actual` files**
Run: `rm -f crates/ail/tests/snapshots/*.ll.actual`
The `.actual` files are scratch — they appear only on a failing comparison and are not version-controlled. Removing them keeps the working tree clean for the final acceptance gate.
---
## Task 10: Final acceptance gate
**Goal:** Run the spec's six acceptance criteria end-to-end and confirm CLEAN before handing off to Boss for commit.
**Files:** (no edits — verification only)
- [ ] **Step 1: Full workspace test suite**
Run: `cargo test --workspace --quiet 2>&1 | tail -15`
Expected: every line ends with `test result: ok. <n> passed; 0 failed`. No `failed` count > 0 anywhere.
The total `passed` count is lower than the pre-iter count by exactly 4 (`gc_handles_recursive_list_construction` + `alloc_rc_produces_same_stdout_as_gc` + `alloc_rc_matches_gc_on_std_list_demo` + `staticlib_gc_is_rejected`) and higher by 1 (the new `ail_build_rejects_alloc_gc_with_unknown_value_error`) — net delta `-3`. The Boss accounts for this in the iter commit body.
- [ ] **Step 2: Boehm-grep clean except inside honesty-pin absence assertions**
Run: `grep -rn -E 'Boehm|libgc|GC_malloc|--alloc=gc|AllocStrategy::Gc' crates/ design/ runtime/ bench/run.sh bench/check.py skills/ examples/ 2>/dev/null`
Expected: matches ONLY in `crates/ailang-core/tests/docs_honesty_pin.rs` (the four new absence-pins reference the Boehm-zombie strings as scan literals — that is correct and required for the pins to function). Any other hit is a residual reference and Task 4 / Task 6 / Task 8 was incomplete.
- [ ] **Step 3: Bench harness check exits 0**
Run: `bench/check.py 2>&1; echo "exit: $?"`
Expected: `exit: 0`. (The script reads the current bench output and compares against `bench/baseline.json` — both regenerated in Task 5.)
- [ ] **Step 4: CLI must-fail fixture**
Run: `cargo build --release -p ail 2>&1 | tail -5 && ./target/release/ail build examples/hello.ail --alloc=gc -o /tmp/h 2>&1; echo "exit: $?"`
Expected: stderr contains `unknown --alloc value` and `\`gc\``; `exit: 2` (or any non-zero — the contract is non-success, not a specific code).
- [ ] **Step 5: Honesty rule + design-doc present-tense**
Read `design/models/rc-uniqueness.md` and `design/models/pipeline.md` end-to-end:
- No `Boehm` / `libgc` / `GC_malloc` / `--alloc=gc` / `parity oracle` / `transitional` (in the allocator sense) appears.
- Present-tense describes RC (canonical) + bump (bench-floor) only.
- The 1.3× ceiling is framed as a bench-health regression gate, not a retirement target.
- [ ] **Step 6: Issue #4 close-trailer note**
The iter commit body (Boss-authored) MUST end with `closes #4` on its own line so Gitea auto-closes issue #4 on push. This is a verification note for the Boss, not a Task-runnable step — but the planner records it here so the implement orchestrator's stats file flags the issue-trailer expectation in its end-report.
---
## Done state
After Task 10 reports CLEAN:
- 1 new test, 0 modified core invariants, ~30 modified files, 5 regenerated IR snapshots, 1 deleted fixture.
- `cargo test --workspace --quiet` green; bench-harness green; honesty-pin green; round-trip green.
- The Boehm-grep returns hits only inside the honesty-pin absence assertions.
- The iter is ready for Boss-side `git status` inspection and a single cohesive `git commit -m 'iter boehm-retirement.1 (DONE 10/10): retire the transitional Boehm GC path … closes #4'`.