# raw-buf.1 — Intercept registry refactor — Implementation Plan

> **Parent spec:** `docs/specs/0054-raw-buf.md`
>
> **For agentic workers:** REQUIRED SUB-SKILL: use the
> `implement` skill to run this plan. Steps use `- [ ]`
> checkboxes for tracking.

**Goal:** Lift the hardcoded `try_emit_primitive_instance_body` match in `crates/ailang-codegen/src/lib.rs` (18 arms) into a single registry table consulted at the existing dispatch site. Zero behavioural change.

**Architecture:** New file `crates/ailang-codegen/src/intercepts.rs` carries an `Intercept` struct (`name`, `expected_params`, `expected_ret`, `wants_alwaysinline`, `emit: fn(&mut Emitter) -> Result<()>`), an `INTERCEPTS: &[Intercept]` static table, and a `lookup(name) -> Option<&'static Intercept>`. Each arm body becomes a free fn `emit_<name>(emitter)`. The wrapper `try_emit_primitive_instance_body` survives as a thin shim that does `match intercepts::lookup(fn_name) { Some(i) => { check_sig(i, ...)?; (i.emit)(self)?; Ok(true) } None => Ok(false) }` — so the 14 in-source comment-refs to the wrapper stay valid. The `intercept_emit_wants_alwaysinline` predicate is rewritten to consult the registry, closing the silent-drift class that recon flagged.

**Tech Stack:** Rust 1.x (stable), `crates/ailang-codegen/` (lib + intercepts submodule), workspace integration tests in `crates/ail/tests/`.

---

**Files this plan creates or modifies:**

- Create: `crates/ailang-codegen/src/intercepts.rs` — the registry table + entry types + lookup + free emit fns + `check_sig` helper + the `registry_contains_all_legacy_arms` pin test.
- Modify: `crates/ailang-codegen/src/lib.rs:43-48` — add `mod intercepts;`.
- Modify: `crates/ailang-codegen/src/lib.rs` — bump visibility of fields `body`, `locals`, `block_terminated` (currently private) to `pub(crate)` so sibling-module free fns can read/write them. Bump visibility of helper methods `emit_compare_ladder`, `emit_ordering_arm`, `emit_direct_int_icmp_intercept`, `lower_ctor` (currently private; `start_block`, `fresh_ssa`, `fresh_id` are already `pub(crate)`) to `pub(crate)`.
- Modify: `crates/ailang-codegen/src/lib.rs:2841-3147` — replace the 300-line match body of `try_emit_primitive_instance_body` with a 6-line shim that consults `intercepts::lookup`.
- Modify: `crates/ailang-codegen/src/lib.rs:1172` (the `intercept_emit_wants_alwaysinline` predicate body) — rewrite as `intercepts::lookup(symbol).map_or(false, |i| i.wants_alwaysinline)`.
- Test: `crates/ailang-codegen/src/intercepts.rs::registry_contains_all_legacy_arms` — enumerates all 18 legacy arm names + asserts each resolves to a registered entry. Ratifies the migration as complete (a half-finished migration where any name is missing becomes a hard test fail).
- Test (regression, not edited): the four E2E build-and-run tests named in spec § Testing strategy raw-buf.1 — `eq_primitives_smoke_compiles_and_runs`, `compare_primitives_smoke_prints_1_2_3_thrice`, `float_compare_smoke_prints_true_true_false`, `eq_ord_polymorphic_runs_end_to_end`. Plus the alwaysinline-presence pin tests `crates/ailang-codegen/tests/eq_primitives_pin.rs` and `crates/ail/tests/prelude_eq_alwaysinline_ir_pin.rs`. Plus the full workspace.

---

### Task 1: intercepts module skeleton — types, empty table, lookup, sig_check

**Files:**
- Create: `crates/ailang-codegen/src/intercepts.rs`
- Modify: `crates/ailang-codegen/src/lib.rs:43-48` (mod declaration list)

- [ ] **Step 1: Create the new file `crates/ailang-codegen/src/intercepts.rs` with the registry types, empty table, lookup, and sig-check helper.**

```rust
//! Codegen intercept registry — one dispatch table for every
//! mono-symbol whose body the codegen supplies as LLVM IR
//! directly (replacing the .ail placeholder body that the
//! intercept's home module ships for round-trip stability).
//!
//! Each entry carries:
//!  - `name`                — the mono symbol the dispatch keys on
//!  - `expected_params`     — LLVM-IR param types as text
//!  - `expected_ret`        — LLVM-IR return type as text
//!  - `wants_alwaysinline`  — whether the dispatch site should
//!                            attach the `alwaysinline` attribute
//!                            to the emitted fn (was a separate
//!                            hardcoded name-list in the predicate
//!                            `intercept_emit_wants_alwaysinline`
//!                            before raw-buf.1)
//!  - `emit`                — the per-arm IR-emission body, run
//!                            against `&mut Emitter` after the
//!                            sig check
//!
//! Adding a new intercept = adding one row to `INTERCEPTS`. The
//! dispatch site (`try_emit_primitive_instance_body` in
//! `crates/ailang-codegen/src/lib.rs`) and the alwaysinline
//! predicate both consult this single table.

use crate::{CodegenError, Emitter, Result};

pub(crate) struct Intercept {
    pub name: &'static str,
    pub expected_params: &'static [&'static str],
    pub expected_ret: &'static str,
    pub wants_alwaysinline: bool,
    pub emit: fn(&mut Emitter<'_>) -> Result<()>,
}

pub(crate) static INTERCEPTS: &[Intercept] = &[];

pub(crate) fn lookup(name: &str) -> Option<&'static Intercept> {
    INTERCEPTS.iter().find(|i| i.name == name)
}

pub(crate) fn check_sig(
    intercept: &Intercept,
    param_tys: &[String],
    ret_ty: &str,
) -> Result<()> {
    let params_match = param_tys.len() == intercept.expected_params.len()
        && param_tys
            .iter()
            .zip(intercept.expected_params.iter())
            .all(|(have, want)| have == want);
    if !params_match || ret_ty != intercept.expected_ret {
        return Err(CodegenError::Internal(format!(
            "{} body intercept: unexpected signature \
             ({:?}) -> {} (want {:?} -> {})",
            intercept.name,
            param_tys,
            ret_ty,
            intercept.expected_params,
            intercept.expected_ret,
        )));
    }
    Ok(())
}
```

- [ ] **Step 2: Add `mod intercepts;` to the mod-declaration block in `crates/ailang-codegen/src/lib.rs` (currently L43-48, alphabetically sorted: `drop`, `escape`, `lambda`, `match_lower`, `subst`, `synth`). Insert `intercepts` between `escape` and `lambda` to preserve alphabetical order.**

Edit at `crates/ailang-codegen/src/lib.rs:43-48`. Insert the line `mod intercepts;` between the existing `mod escape;` and `mod lambda;` lines.

- [ ] **Step 3: Build the workspace — empty registry compiles clean.**

Run: `cargo build --workspace 2>&1 | tail -5`
Expected: build succeeds, zero warnings about `intercepts` being unused (the `pub(crate)` visibility and the `lookup` fn keep it consumed by neither call site yet — Rust does NOT warn on `pub(crate)` items in a sibling module, only on truly unused items). If a `dead_code` warning fires for `INTERCEPTS` or `Intercept` because the table is empty, that's expected and will go away in Task 2 when entries are added; do NOT add `#[allow(dead_code)]` — just accept the warning until Task 2.

```
cargo build --workspace 2>&1 | tail -5
```

Expected output ends with `Finished` and no errors. A few dead-code warnings about `intercepts::Intercept` / `INTERCEPTS` / `lookup` / `check_sig` are acceptable at this point.

---

### Task 2: Populate the registry — 18 entries + per-arm emit fns + visibility bumps

**Files:**
- Modify: `crates/ailang-codegen/src/intercepts.rs` (add the 18 free emit fns + populate `INTERCEPTS`)
- Modify: `crates/ailang-codegen/src/lib.rs` (visibility bumps on fields + helper methods)

- [ ] **Step 1: Bump visibility on the three `Emitter` struct fields the intercept bodies read from `&mut self`.**

In `crates/ailang-codegen/src/lib.rs`, find the `Emitter` struct definition starting at L706 and bump these three fields to `pub(crate)`:

- `body: String` (around L711) → `pub(crate) body: String,`
- `locals: Vec<(String, String, String, Type)>` (around L725) → `pub(crate) locals: Vec<(String, String, String, Type)>,`
- `block_terminated: bool` (around L765) → `pub(crate) block_terminated: bool,`

(If the field types differ slightly from what's listed — e.g. the locals tuple shape — preserve the existing type and just add the `pub(crate)` prefix.)

- [ ] **Step 2: Bump visibility on the four `Emitter` helper methods that the intercept emit fns call.**

In `crates/ailang-codegen/src/lib.rs`, locate each of these methods inside `impl<'a> Emitter<'a>` (L921 onwards) and prepend `pub(crate)`:

- `fn emit_compare_ladder(&mut self, lt: &str, eq: &str) -> Result<()>` at L3220 → `pub(crate) fn emit_compare_ladder(...)`
- `fn emit_ordering_arm(&mut self, label: &str, ctor: &str) -> Result<()>` at L3190 → `pub(crate) fn emit_ordering_arm(...)`
- `fn emit_direct_int_icmp_intercept(...)` at L3156 → `pub(crate) fn emit_direct_int_icmp_intercept(...)`
- `fn lower_ctor(...)` (used by `emit_ordering_arm`; exact line variable) → `pub(crate) fn lower_ctor(...)`

`start_block`, `fresh_ssa`, `fresh_id` are already `pub(crate)`; no edit needed. `intercept_emit_wants_alwaysinline` is rewritten in Task 4 and does not need visibility bumping (it stays a method on Emitter consulted from inside the crate).

- [ ] **Step 3: Build to confirm visibility-bumps compile clean before adding emit fns.**

Run: `cargo build --workspace 2>&1 | tail -5`
Expected: build succeeds; no new warnings or errors from the visibility changes.

- [ ] **Step 4: Add the two Str-path emit fns (`emit_eq_str`, `emit_compare_str`) to `crates/ailang-codegen/src/intercepts.rs`.**

Append the two fns below to `intercepts.rs` after the `check_sig` helper. The bodies are extracted verbatim from `try_emit_primitive_instance_body` at L2848 (`eq__Str` arm) and L2915 (`compare__Str` arm) of `lib.rs`, minus the per-arm sig-check prologue (which `check_sig` now handles) and minus the trailing `Ok(true)` (which the dispatch shim wraps).

```rust
pub(crate) fn emit_eq_str(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    let a_bytes = emitter.fresh_ssa();
    let b_bytes = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {a_bytes} = getelementptr inbounds i8, ptr {a_ssa}, i64 8\n"
    ));
    emitter.body.push_str(&format!(
        "  {b_bytes} = getelementptr inbounds i8, ptr {b_ssa}, i64 8\n"
    ));
    let dst = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {dst} = call zeroext i1 @ail_str_eq(ptr {a_bytes}, ptr {b_bytes})\n"
    ));
    emitter.body.push_str(&format!("  ret i1 {dst}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}

pub(crate) fn emit_compare_str(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    let a_bytes = emitter.fresh_ssa();
    let b_bytes = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {a_bytes} = getelementptr inbounds i8, ptr {a_ssa}, i64 8\n"
    ));
    emitter.body.push_str(&format!(
        "  {b_bytes} = getelementptr inbounds i8, ptr {b_ssa}, i64 8\n"
    ));
    let cmp_res = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {cmp_res} = call i32 @ail_str_compare(ptr {a_bytes}, ptr {b_bytes})\n"
    ));
    emitter.emit_compare_ladder(
        &format!("icmp slt i32 {cmp_res}, 0"),
        &format!("icmp eq i32 {cmp_res}, 0"),
    )?;
    Ok(())
}
```

**Note for the implementer:** verify the verbatim text of the `eq__Str` and `compare__Str` arms in `crates/ailang-codegen/src/lib.rs` before lifting. The exact `format!` template strings, the exact `cmp_res` ladder argument shape, and the `emit_compare_ladder` invocation arguments are the IR-fidelity critical bits. If the bodies in `lib.rs` differ from what is shown above (e.g. an extra SSA fetch was added in a later commit), preserve the lib.rs verbatim body and just structurally separate the sig-check prologue from the actual IR emission.

- [ ] **Step 5: Add the five Eq/Compare emit fns (`emit_compare_int`, `emit_compare_bool`, `emit_eq_int`, `emit_eq_bool`, `emit_eq_unit`).**

Append the five fns below to `intercepts.rs` after `emit_compare_str`. Bodies are extracted verbatim from `try_emit_primitive_instance_body` at L2883, L2899, L2947, L2966, L2985 of `lib.rs`, minus per-arm sig-checks and trailing `Ok(true)`.

```rust
pub(crate) fn emit_compare_int(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    emitter.emit_compare_ladder(
        &format!("icmp slt i64 {a_ssa}, {b_ssa}"),
        &format!("icmp eq i64 {a_ssa}, {b_ssa}"),
    )?;
    Ok(())
}

pub(crate) fn emit_compare_bool(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    emitter.emit_compare_ladder(
        &format!("icmp ult i1 {a_ssa}, {b_ssa}"),
        &format!("icmp eq i1 {a_ssa}, {b_ssa}"),
    )?;
    Ok(())
}

pub(crate) fn emit_eq_int(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    let dst = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {dst} = icmp eq i64 {a_ssa}, {b_ssa}\n"
    ));
    emitter.body.push_str(&format!("  ret i1 {dst}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}

pub(crate) fn emit_eq_bool(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    let dst = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {dst} = icmp eq i1 {a_ssa}, {b_ssa}\n"
    ));
    emitter.body.push_str(&format!("  ret i1 {dst}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}

pub(crate) fn emit_eq_unit(emitter: &mut Emitter<'_>) -> Result<()> {
    emitter.body.push_str("  ret i1 1\n");
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}
```

**Note for the implementer:** the `emit_eq_int` / `emit_eq_bool` body shapes above are the planner's reconstruction from the recon report's "icmp eq … / ret i1" idiom column. Open `crates/ailang-codegen/src/lib.rs` at the `eq__Int` (L2947) and `eq__Bool` (L2966) arms and lift the actual body text verbatim. If the lib.rs body differs in `format!` template wording, IR comment lines, or attribute placement, preserve the lib.rs version exactly — the goal is byte-identical emitted IR after the dispatch site rewires.

- [ ] **Step 6: Add the six float-family emit fns (`emit_float_eq`, `emit_float_ne`, `emit_float_lt`, `emit_float_le`, `emit_float_gt`, `emit_float_ge`).**

Append the six fns below to `intercepts.rs` after `emit_eq_unit`. Bodies are extracted verbatim from `try_emit_primitive_instance_body` at L3000, L3019, L3040, L3059, L3078, L3097 of `lib.rs`, minus per-arm sig-checks and trailing `Ok(true)`. The fcmp predicates are NaN-aware (`oeq`, `une`, `olt`, `ole`, `ogt`, `oge`) — preserve them exactly as the lib.rs source has them. Note `float_ne` uses `une` (unordered-or-not-equal), not `one` (ordered-not-equal) — this is deliberate NaN-aware semantics per the comment at the lib.rs arm.

```rust
pub(crate) fn emit_float_eq(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    let dst = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {dst} = fcmp oeq double {a_ssa}, {b_ssa}\n"
    ));
    emitter.body.push_str(&format!("  ret i1 {dst}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}

pub(crate) fn emit_float_ne(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    let dst = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {dst} = fcmp une double {a_ssa}, {b_ssa}\n"
    ));
    emitter.body.push_str(&format!("  ret i1 {dst}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}

pub(crate) fn emit_float_lt(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    let dst = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {dst} = fcmp olt double {a_ssa}, {b_ssa}\n"
    ));
    emitter.body.push_str(&format!("  ret i1 {dst}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}

pub(crate) fn emit_float_le(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    let dst = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {dst} = fcmp ole double {a_ssa}, {b_ssa}\n"
    ));
    emitter.body.push_str(&format!("  ret i1 {dst}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}

pub(crate) fn emit_float_gt(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    let dst = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {dst} = fcmp ogt double {a_ssa}, {b_ssa}\n"
    ));
    emitter.body.push_str(&format!("  ret i1 {dst}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}

pub(crate) fn emit_float_ge(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let a_ssa = emitter.locals[n - 2].1.clone();
    let b_ssa = emitter.locals[n - 1].1.clone();
    let dst = emitter.fresh_ssa();
    emitter.body.push_str(&format!(
        "  {dst} = fcmp oge double {a_ssa}, {b_ssa}\n"
    ));
    emitter.body.push_str(&format!("  ret i1 {dst}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}
```

- [ ] **Step 7: Add the five Int icmp-delegate emit fns (`emit_lt_int`, `emit_le_int`, `emit_gt_int`, `emit_ge_int`, `emit_ne_int`).**

Append the five fns below to `intercepts.rs` after `emit_float_ge`. Bodies are 1-line delegates to the existing `Emitter::emit_direct_int_icmp_intercept` helper (now `pub(crate)` per Step 2). The icmp predicate strings come from lib.rs L3140-3144.

```rust
pub(crate) fn emit_lt_int(emitter: &mut Emitter<'_>) -> Result<()> {
    emitter.emit_direct_int_icmp_intercept("icmp slt i64")
}

pub(crate) fn emit_le_int(emitter: &mut Emitter<'_>) -> Result<()> {
    emitter.emit_direct_int_icmp_intercept("icmp sle i64")
}

pub(crate) fn emit_gt_int(emitter: &mut Emitter<'_>) -> Result<()> {
    emitter.emit_direct_int_icmp_intercept("icmp sgt i64")
}

pub(crate) fn emit_ge_int(emitter: &mut Emitter<'_>) -> Result<()> {
    emitter.emit_direct_int_icmp_intercept("icmp sge i64")
}

pub(crate) fn emit_ne_int(emitter: &mut Emitter<'_>) -> Result<()> {
    emitter.emit_direct_int_icmp_intercept("icmp ne i64")
}
```

**Note for the implementer:** the recon report says these five arms (lib.rs L3140-3144) delegate to `emit_direct_int_icmp_intercept` with a predicate string. Verify the exact signature of `emit_direct_int_icmp_intercept` at lib.rs L3156 before lifting — if it takes more than one argument (e.g. an additional "compare-form" enum or a Result-returning bool), adapt the delegate signatures to match. The shape above assumes single-arg `&str` predicate.

- [ ] **Step 8: Populate the `INTERCEPTS` table with all 18 entries.**

Replace the `pub(crate) static INTERCEPTS: &[Intercept] = &[];` line in `intercepts.rs` with the populated table below. Set `wants_alwaysinline` per entry; the values mirror the current name-list in `intercept_emit_wants_alwaysinline` at `crates/ailang-codegen/src/lib.rs:1172` — every existing intercept that the predicate lists today gets `true`, the rest get `false`.

```rust
pub(crate) static INTERCEPTS: &[Intercept] = &[
    Intercept {
        name: "eq__Str",
        expected_params: &["ptr", "ptr"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_eq_str,
    },
    Intercept {
        name: "compare__Int",
        expected_params: &["i64", "i64"],
        expected_ret: "ptr",
        wants_alwaysinline: true,
        emit: emit_compare_int,
    },
    Intercept {
        name: "compare__Bool",
        expected_params: &["i1", "i1"],
        expected_ret: "ptr",
        wants_alwaysinline: true,
        emit: emit_compare_bool,
    },
    Intercept {
        name: "compare__Str",
        expected_params: &["ptr", "ptr"],
        expected_ret: "ptr",
        wants_alwaysinline: true,
        emit: emit_compare_str,
    },
    Intercept {
        name: "eq__Int",
        expected_params: &["i64", "i64"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_eq_int,
    },
    Intercept {
        name: "eq__Bool",
        expected_params: &["i1", "i1"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_eq_bool,
    },
    Intercept {
        name: "eq__Unit",
        expected_params: &[],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_eq_unit,
    },
    Intercept {
        name: "float_eq",
        expected_params: &["double", "double"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_float_eq,
    },
    Intercept {
        name: "float_ne",
        expected_params: &["double", "double"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_float_ne,
    },
    Intercept {
        name: "float_lt",
        expected_params: &["double", "double"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_float_lt,
    },
    Intercept {
        name: "float_le",
        expected_params: &["double", "double"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_float_le,
    },
    Intercept {
        name: "float_gt",
        expected_params: &["double", "double"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_float_gt,
    },
    Intercept {
        name: "float_ge",
        expected_params: &["double", "double"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_float_ge,
    },
    Intercept {
        name: "lt__Int",
        expected_params: &["i64", "i64"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_lt_int,
    },
    Intercept {
        name: "le__Int",
        expected_params: &["i64", "i64"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_le_int,
    },
    Intercept {
        name: "gt__Int",
        expected_params: &["i64", "i64"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_gt_int,
    },
    Intercept {
        name: "ge__Int",
        expected_params: &["i64", "i64"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_ge_int,
    },
    Intercept {
        name: "ne__Int",
        expected_params: &["i64", "i64"],
        expected_ret: "i1",
        wants_alwaysinline: true,
        emit: emit_ne_int,
    },
];
```

**Note for the implementer:** the `expected_params` for `eq__Unit` is `&[]` because lib.rs L2985 has `(_ params ignored)` per the recon — Unit values carry no SSA payload. Verify against the lib.rs arm; if the actual arm checks against a different param shape (e.g. `["ptr"]` for a Unit-token slot), use the lib.rs version. The `wants_alwaysinline: true` on every entry mirrors the current predicate's all-inclusive name list per recon — if the predicate at lib.rs L1172 actually distinguishes some entries (e.g. only Eq names get inline, not Compare names), copy that distinction here. This is the **lockstep coupling** (intercept names ↔ alwaysinline-name-list) that the registry consolidates.

- [ ] **Step 9: Build the workspace; verify all four E2E ratifier tests still pass.**

The `INTERCEPTS` table is now populated but **not yet consulted** by the dispatch site — `try_emit_primitive_instance_body` still runs the full match in `lib.rs`. The table is dead code at this point. Behaviour is unchanged.

Run: `cargo build --workspace 2>&1 | tail -5`
Expected: build succeeds; dead-code warnings on `INTERCEPTS` / `lookup` / `check_sig` (the table is populated but the dispatch site does not yet call `lookup`) and possibly on the emit fns are acceptable.

Run: `cargo test -p ail --test e2e eq_primitives_smoke_compiles_and_runs`
Expected: PASS

Run: `cargo test -p ail --test e2e compare_primitives_smoke_prints_1_2_3_thrice`
Expected: PASS

Run: `cargo test -p ail --test float_compare_smoke_e2e float_compare_smoke_prints_true_true_false`
Expected: PASS

Run: `cargo test -p ail --test eq_ord_e2e eq_ord_polymorphic_runs_end_to_end`
Expected: PASS

---

### Task 3: Rewire the dispatch site — `try_emit_primitive_instance_body` body → `intercepts::lookup` shim

**Files:**
- Modify: `crates/ailang-codegen/src/lib.rs:2841-3147` (the entire fn body, replaced by a 6-line shim)

- [ ] **Step 1: Replace the body of `try_emit_primitive_instance_body` with the registry-consulting shim.**

In `crates/ailang-codegen/src/lib.rs`, locate `try_emit_primitive_instance_body` at L2841. The fn currently runs from L2841 to L3147 (~306 lines). Preserve the fn signature (so all 14 in-source comment-refs remain valid pointers to the canonical dispatch name), and replace the entire body with:

```rust
    fn try_emit_primitive_instance_body(
        &mut self,
        fn_name: &str,
        param_tys: &[String],
        ret_ty: &str,
    ) -> Result<bool> {
        match intercepts::lookup(fn_name) {
            Some(intercept) => {
                intercepts::check_sig(intercept, param_tys, ret_ty)?;
                (intercept.emit)(self)?;
                Ok(true)
            }
            None => Ok(false),
        }
    }
```

The 18 match arms at L2848-L3144 are deleted. The trailing `_ => Ok(false)` fallthrough is preserved by the `None` arm of the `match intercepts::lookup(...)`.

- [ ] **Step 2: Build the workspace + run the four E2E ratifiers.**

Run: `cargo build --workspace 2>&1 | tail -5`
Expected: build succeeds; the dead-code warnings on `INTERCEPTS` / `lookup` / `check_sig` / the emit fns are now gone (the dispatch site consumes them).

Run: `cargo test -p ail --test e2e eq_primitives_smoke_compiles_and_runs`
Expected: PASS — `eq__Str` and `eq__Int|Bool` arms now flow through the registry; printed output unchanged.

Run: `cargo test -p ail --test e2e compare_primitives_smoke_prints_1_2_3_thrice`
Expected: PASS — `compare__Int|Bool|Str` arms now flow through the registry; printed output unchanged.

Run: `cargo test -p ail --test float_compare_smoke_e2e float_compare_smoke_prints_true_true_false`
Expected: PASS — the six `float_*` arms now flow through the registry; printed output unchanged.

Run: `cargo test -p ail --test eq_ord_e2e eq_ord_polymorphic_runs_end_to_end`
Expected: PASS — polymorphic Eq/Ord dispatch over Int/Bool/Str exercises the registry secondarily.

- [ ] **Step 3: Run the full workspace test suite to catch any regression the four E2E ratifiers missed.**

Run: `cargo test --workspace 2>&1 | tail -10`
Expected: every test passes; baseline of 668 tests stays green (Task 5 adds one new test bringing the total to 669, but at this point the count is still 668).

---

### Task 4: Fold `wants_alwaysinline` into the registry — close the lockstep drift class

**Files:**
- Modify: `crates/ailang-codegen/src/lib.rs:1172` (the `intercept_emit_wants_alwaysinline` predicate body)

- [ ] **Step 1: Rewrite the `intercept_emit_wants_alwaysinline` predicate body to consult the registry.**

In `crates/ailang-codegen/src/lib.rs`, locate `intercept_emit_wants_alwaysinline` starting at L1172. The predicate is currently a method on `Emitter` (or a free fn — check the signature) that takes `symbol: &str` (or `name: &str`) and returns `bool` by matching on a hardcoded name list.

Replace the body with a registry consultation:

```rust
    pub(crate) fn intercept_emit_wants_alwaysinline(&self, symbol: &str) -> bool {
        intercepts::lookup(symbol).is_some_and(|i| i.wants_alwaysinline)
    }
```

(If the existing predicate is a free fn rather than a method, drop the `&self` parameter and adjust accordingly. If it returns via a non-`is_some_and` idiom that the project uses elsewhere, prefer `.map_or(false, |i| i.wants_alwaysinline)` — both are stable Rust.)

- [ ] **Step 2: Build the workspace.**

Run: `cargo build --workspace 2>&1 | tail -5`
Expected: build succeeds.

- [ ] **Step 3: Run the two alwaysinline-presence pin tests + the four E2E ratifiers.**

The alwaysinline attribute is what these pin tests verify is attached to the generated IR; if the predicate's new registry-driven body returns a different set than the old name list, the pins go red.

Run: `cargo test -p ailang-codegen --test eq_primitives_pin 2>&1 | tail -10`
Expected: all tests in this file PASS.

Run: `cargo test -p ail --test prelude_eq_alwaysinline_ir_pin 2>&1 | tail -10`
Expected: all tests in this file PASS.

Run: `cargo test -p ail --test e2e eq_primitives_smoke_compiles_and_runs`
Expected: PASS.

Run: `cargo test -p ail --test e2e compare_primitives_smoke_prints_1_2_3_thrice`
Expected: PASS.

Run: `cargo test -p ail --test float_compare_smoke_e2e float_compare_smoke_prints_true_true_false`
Expected: PASS.

Run: `cargo test -p ail --test eq_ord_e2e eq_ord_polymorphic_runs_end_to_end`
Expected: PASS.

- [ ] **Step 4: Full workspace test run.**

Run: `cargo test --workspace 2>&1 | tail -10`
Expected: all 668 tests PASS.

---

### Task 5: Add the `registry_contains_all_legacy_arms` pin test

**Files:**
- Modify: `crates/ailang-codegen/src/intercepts.rs` (append the test module at the bottom)

- [ ] **Step 1: Append the pin test module to `crates/ailang-codegen/src/intercepts.rs`.**

Append the following to the end of the file:

```rust
#[cfg(test)]
mod tests {
    use super::lookup;

    /// Pin: every name that was a match arm in the legacy
    /// `try_emit_primitive_instance_body` (before raw-buf.1)
    /// must resolve to a registered Intercept. If this test
    /// goes red, a name was dropped during migration AND/OR
    /// removed from the registry without the dispatch wrapper
    /// being audited to confirm no consumer still depends on
    /// it.
    #[test]
    fn registry_contains_all_legacy_arms() {
        let legacy_names = [
            "eq__Str",
            "compare__Int",
            "compare__Bool",
            "compare__Str",
            "eq__Int",
            "eq__Bool",
            "eq__Unit",
            "float_eq",
            "float_ne",
            "float_lt",
            "float_le",
            "float_gt",
            "float_ge",
            "lt__Int",
            "le__Int",
            "gt__Int",
            "ge__Int",
            "ne__Int",
        ];

        let missing: Vec<&str> = legacy_names
            .iter()
            .copied()
            .filter(|n| lookup(n).is_none())
            .collect();

        assert!(
            missing.is_empty(),
            "registry is missing legacy intercept names: {missing:?}"
        );
    }
}
```

- [ ] **Step 2: Run the new pin test.**

Run: `cargo test -p ailang-codegen --lib intercepts::tests::registry_contains_all_legacy_arms`
Expected: PASS — all 18 names resolve.

- [ ] **Step 3: Final full workspace test run.**

Run: `cargo test --workspace 2>&1 | tail -10`
Expected: 669 tests PASS (baseline 668 + the new pin).

---

## Self-review

This section is the planner's inline scrub, NOT a checklist for the implementer.

1. **Spec coverage.** Every iter-raw-buf.1 spec section maps to a task:
   - § Architecture point 1 ("Lift the existing hard-coded match … into a registry table") → Tasks 1+2+3.
   - § Components row 1 ("new file `crates/ailang-codegen/src/intercepts.rs`; edits to `crates/ailang-codegen/src/lib.rs`") → Tasks 1+2+3.
   - § Testing strategy "raw-buf.1" first bullet (four E2E ratifiers stay green) → Tasks 2,3,4,5 Step "Run the four E2E ratifiers".
   - § Testing strategy "raw-buf.1" second bullet (`registry_contains_all_legacy_arms` test) → Task 5.
   - Lockstep coupling that recon flagged (intercept names ↔ `intercept_emit_wants_alwaysinline`) → Task 4 (folded the bool into the table, closing the drift class).

2. **Placeholder scan.** No "TBD", no "TODO", no "implement later", no "similar to Task N", no "add appropriate error handling". The two "Note for the implementer" callouts (Step 4 in Task 2, Step 5 in Task 2) are verification asks against the verbatim source ("preserve the lib.rs body exactly"), not placeholders for design judgement.

3. **Type consistency.** `Emitter<'_>` is the struct name everywhere; `Intercept` is the registry-entry struct name; `INTERCEPTS` (static `&[Intercept]`) is the table; `lookup(name) -> Option<&'static Intercept>` is the resolver; `check_sig(intercept, params, ret)` is the sig-check helper. `try_emit_primitive_instance_body` is preserved as a method on `Emitter`. All 18 emit-fn names follow the convention `emit_<lowered_name>` where `<lowered_name>` is the legacy arm name with `__` → `_` and lowercased (`eq__Str` → `emit_eq_str`).

4. **Step granularity.** Task 2 has 9 steps; each step touches 1–6 fns or one structural change. The "add 6 float emit fns" step is the chunkiest but each fn is 8–12 lines of mechanical lift; total ~5 min. Other steps are 2-3 min.

5. **No commit steps.** No `git commit` anywhere in the task templates.

6. **Pin/replacement substring contiguity.** Task 5's pin test asserts the 18 names exist in the registry. The replacement body for `INTERCEPTS` in Task 2 Step 8 names each of the 18 strings literally; each name appears as a contiguous string literal (e.g. `name: "compare__Int"`), not soft-wrapped across two lines. Substring contiguity preserved.

7. **Compile-gate vs deferred-caller ordering.** The fn `try_emit_primitive_instance_body` keeps its signature throughout the plan. Only its body changes (Task 3). No caller-threading deferred across a compile gate. The visibility bumps in Task 2 (Steps 1+2) are additive and do not affect any caller. The `intercept_emit_wants_alwaysinline` predicate signature also stays constant (only body changes in Task 4).

8. **Verification-command filter strings.** Each `cargo test` filter string is a test fn name that the recon report verified exists at a specific line in a specific file. Spot-check: `eq_primitives_smoke_compiles_and_runs` at `crates/ail/tests/e2e.rs:945`; `compare_primitives_smoke_prints_1_2_3_thrice` at `crates/ail/tests/e2e.rs:932`; `float_compare_smoke_prints_true_true_false` at `crates/ail/tests/float_compare_smoke_e2e.rs:11`; `eq_ord_polymorphic_runs_end_to_end` at `crates/ail/tests/eq_ord_e2e.rs:61`. The new pin `registry_contains_all_legacy_arms` is invented in this plan; the Task 5 Step 2 invocation `cargo test -p ailang-codegen --lib intercepts::tests::registry_contains_all_legacy_arms` follows the standard `cargo test --lib` form for in-source `#[cfg(test)] mod tests`. The `cargo test --workspace` invocation has no filter — it runs the whole suite. Filter-zero-match risk: zero.
