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