diff --git a/docs/plans/boehm-retirement.1.md b/docs/plans/boehm-retirement.1.md new file mode 100644 index 0000000..d608a1b --- /dev/null +++ b/docs/plans/boehm-retirement.1.md @@ -0,0 +1,1030 @@ +# 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 { + lower_workspace_inner(ws, AllocStrategy::Gc, Target::Executable) +} +``` + +to: + +```rust +pub fn lower_workspace(ws: &Workspace) -> Result { + 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 { + lower_workspace_inner(ws, AllocStrategy::Gc, Target::StaticLib) +} +``` + +to: + +```rust +pub fn lower_workspace_staticlib(ws: &Workspace) -> Result { + 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(), "")` 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(, "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(), "")` 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 , align 8\`; on a miss it writes \`call ptr @GC_malloc(i64 )\`.` → `\`alloca i8, i64 , align 8\`; on a miss it writes \`call ptr @ailang_rc_alloc(i64 )\` (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 `) 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 `.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 `.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: ` 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. 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'`.