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
54 KiB
Boehm Retirement (iter .1) — Implementation Plan
Parent spec:
docs/specs/2026-05-20-boehm-retirement.mdFor agentic workers: REQUIRED SUB-SKILL: use
skills/implementto 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,1211crates/ail/src/main.rs:141-142,2143-2148,2189,2263-2264,2365-2366,2389-2395,2425,2497-2498crates/ailang-codegen/src/escape.rs:5,96— module//!+ doc-commentcrates/ailang-codegen/src/match_lower.rs:40,113— doc-commentscrates/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 scrubcrates/ail/tests/embed_staticlib_alloc_guard.rs:1-4,20-32— file//!doc + deletestaticlib_gc_is_rejectedcrates/ailang-core/tests/docs_honesty_pin.rs:30,55-73,116-117— invert Boehm anchorscrates/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 scrubruntime/rc.c:38,167,169— comment scrubruntime/str.c:32— comment scrubbench/run.sh:3-11,65,149-167,170-205— drop gc-arm + columns + latency-bench gc-fixturebench/check.py:75-97— drop gc-keyed throughput fields + drop"implicit @ gc"arm mappingbench/baseline.json— drop gc-keyed entries (viabench/check.py --update-baselineafter a cleanbench/run.sh)design/models/rc-uniqueness.md:3-32,71-79,132-156,170-183— excise + reframedesign/models/pipeline.md:14-28— pipeline diagram + prosedesign/contracts/scope-boundaries.md:67,114-127— rewrite Boehm block +@GC_mallocreferencedesign/contracts/memory-model.md:232— drop"pre-Boehm era"design/contracts/embedding-abi.md:42-44— drop gc clause from staticlib-guard proseskills/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 referencesexamples/gc_stress.ail— DELETE entire file (47 lines)examples/bench_latency_implicit.ail:4,10,12,15,23,150,185— doc-comment scrubexamples/bench_latency_explicit.ail:12-13— doc-comment scrubexamples/escape_local_demo.ail:10— doc-comment scrubexamples/reuse_as_demo.ail:13,17— doc-comment scrubexamples/rc_pin_recurse_implicit.ail:8— doc-comment scrub
Regenerate (UPDATE_SNAPSHOTS):
crates/ail/tests/snapshots/hello.llcrates/ail/tests/snapshots/list.llcrates/ail/tests/snapshots/max3.llcrates/ail/tests/snapshots/sum.llcrates/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:
//! 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,Defaultderive) -
Modify:
crates/ail/src/main.rs(CLI parse, link branch, staticlib-guard diagnostic) -
Step 1: Drop the
Gcvariant fromAllocStrategy
Edit crates/ailang-codegen/src/lib.rs:158-164 from:
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum AllocStrategy {
#[default]
Gc,
Bump,
Rc,
}
to:
#[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
Gcarm fromfn_name
Edit crates/ailang-codegen/src/lib.rs:186-191 from:
fn fn_name(&self) -> &'static str {
match self {
AllocStrategy::Gc => "GC_malloc",
AllocStrategy::Bump => "bump_malloc",
AllocStrategy::Rc => "ailang_rc_alloc",
}
}
to:
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:
pub fn lower_workspace(ws: &Workspace) -> Result<String> {
lower_workspace_inner(ws, AllocStrategy::Gc, Target::Executable)
}
to:
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:
pub fn lower_workspace_staticlib(ws: &Workspace) -> Result<String> {
lower_workspace_inner(ws, AllocStrategy::Gc, Target::StaticLib)
}
to:
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:
// 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:
// 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 istoNegative complement: under `--alloc=bump` no drop fn is`.
- Step 5: Drop the
Gcarm from CLI parse
Edit crates/ail/src/main.rs:2143-2148 from:
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:
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
Gcarm 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:
"staticlib (swarm) artefact is RC-only — `--alloc=gc` links the \
shared Boehm collector, which is not swarm-safe; use `--alloc=rc`"
to:
"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
--allocflag
Edit crates/ail/src/main.rs:141-142 from:
/// (`@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:
/// 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:
- Delete the line
let stdout_gc = build_and_run_with_alloc(<example>, "gc"); - Delete the line
assert_eq!(stdout_gc, stdout_rc, "alloc=rc must match alloc=gc …");(if present —reuse_as_demo_under_rc_uses_inplace_rewritedoes not have one). - Leave the absolute
assert_eq!(stdout_rc.trim(), "<n>")pin (orassert_eq!(stdout_gc.trim(), "9")inreuse_as_demo_under_rc_uses_inplace_rewrite— rename the binding tostdout_rcfirst, since we are dropping the_gcbuild 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.ailfixture
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_rejectedfrom 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-4from:
//! 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:
//! 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.rsmodule-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-242and:287-292: doc-comments onlower_workspace_with_alloc/lower_workspace_staticlib— strip theAllocStrategy::Gcreferences; the two surviving strategies areRc(default) andBump. -
:530: inline comment about-lgc→ drop or rewrite to bump-only context. -
:781: doc-commentSuch 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=bumponly (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 commentbetween \alloca` (non-escaping) and `@GC_malloc` (escaping)→between `alloca` (non-escaping) and the runtime allocator (escaping).` -
Step 2:
escape.rsandmatch_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.rsnon-link/parse comments -
:2189: doc-commentcompiles 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: commentUnder --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/*.cheader 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:
# 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:
# 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:
# 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:
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:
if line.startswith("workload") and "gc(s)" in line:
in_table = True
to:
if line.startswith("workload") and "bump(s)" in line:
in_table = True
Edit bench/check.py:72 from:
if len(cells) != 9:
continue
to:
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:
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:
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 theimplicit @ gcarm label mapping
Edit bench/check.py:91-97 from:
# 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:
# 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— spawnsbench/run.sh -n 5internallybench/check.py --from-file out.txt— consumes captured outputcat out.txt | bench/check.py --stdin— consumes stdinbench/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.pyexits 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 inbench/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), explicitclone, first-classreuse-as, anddrop-iterativedata attrs.The cost of GC is structurally in the allocate path — Boehm's
GC_mallocis 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), explicitclone, first-classreuse-as, anddrop-iterativedata 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.shfor 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=rcis the canonical backend committed to in the memory model and the CLI default. The typechecker enforces(own)/(borrow)modes, codegen emitsailang_rc_inc/_deccalls at the points dictated by linearity, andTerm::Clone/Term::ReuseAsmaterialise into actual rc-bumps and in-place rewrites respectively.--alloc=gcselects the transitional Boehm backend (see RC + uniqueness);--alloc=rcis the canonical backend and the CLI default.
with:
Two allocator backends share the same MIR.
--alloc=rcis the canonical backend committed to in the memory model and the CLI default; the typechecker enforces(own)/(borrow)modes, codegen emitsailang_rc_inc/_deccalls at the points dictated by linearity, andTerm::Clone/Term::ReuseAsmaterialise into actual rc-bumps and in-place rewrites respectively.--alloc=bumpselects the raw-alloc bench-floor (runtime/bump.c, no free, leak-only) and is used bybench/run.shto 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:
- **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::Lamenv / closure pair under--alloc=rccallsailang_rc_alloc(SIZE). The initial RC plumbing stops there — inc/dec instrumentation is added once the inference is wired up. Until then,--alloc=rcdeliberately 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::Lamenv / closure pair under--alloc=rccallsailang_rc_alloc(SIZE); inc/dec instrumentation is emitted per the uniqueness inference.--alloc=bumpselects 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=staticlibrejects--alloc=gc/--alloc=bump(the shared Boehm collector is not swarm-safe). The value/record
to:
ail build --emit=staticlibrejects--alloc=bump(the bench stub is leak-only and not swarm-safe;--alloc=gcno 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:
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:
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`)withthe 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.aildoc-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=gchypothesis referenceexamples/escape_local_demo.ail:10— "Boehm's heap is not touched at all"examples/reuse_as_demo.ail:13,17—--alloc=gcdoc-commentexamples/rc_pin_recurse_implicit.ail:8—--alloc=gcdoc-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.mdrewrite
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 letsreuse-asfire 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.mdrewrite
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.actualfiles
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 --quietgreen; 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 statusinspection and a single cohesivegit commit -m 'iter boehm-retirement.1 (DONE 10/10): retire the transitional Boehm GC path … closes #4'.