Files
AILang/docs/plans/0104-raw-buf.1-intercept-registry.md
T
Brummel d1612d5463 plan: raw-buf.1-intercept-registry — 5-task atomic codegen-registry refactor (refs #7)
Decomposes raw-buf.1 (parent spec docs/specs/0054-raw-buf.md
§ Architecture point 1, § Components row 1) into 5 tasks:

  Task 1 — intercepts module skeleton (3 steps)
  Task 2 — populate the table: 18 emit fns + visibility bumps (9 steps)
  Task 3 — rewire the dispatch site to a thin shim (3 steps)
  Task 4 — fold wants_alwaysinline into the registry (4 steps)
  Task 5 — registry_contains_all_legacy_arms pin test (3 steps)

Three substantive decisions made at plan time beyond what the spec
dictates, surfaced by the plan-recon report:

1. The match arm count is 18, not the ~8 the spec sentence hand-listed
   (eq__Str, compare__Int|Bool|Str, float_*). Five additional arms
   (eq__Int, eq__Bool, eq__Unit, plus the lt|le|gt|ge|ne__Int
   direct-icmp family) shipped after the spec sentence was written.
   All 18 are absorbed by the registry; the pin test enumerates all 18.

2. The `intercept_emit_wants_alwaysinline` predicate at lib.rs:1172 is
   a separate hardcoded name-list that mirrors the dispatch arms.
   This is a silent-drift class (regression seen earlier in the cycle
   on compare__Int perf at +29-47% when the lists diverged). The plan
   folds wants_alwaysinline into the Intercept entry as a per-row bool
   and rewrites the predicate to consult the registry — one source of
   truth instead of two.

3. The wrapper fn `try_emit_primitive_instance_body` survives as a
   thin shim (`match intercepts::lookup(name) { Some(i) => ..., None
   => Ok(false) }`) rather than being deleted. Trade-off: a 6-line
   wrapper vs. a 14-file comment-ref churn. The wrapper wins —
   landmark name preserved, all in-source doc comments stay valid.

Plan honours the project's compile-gate discipline: fn signatures
stay stable throughout (only bodies change), no caller-threading is
deferred across a build gate. Visibility bumps in Task 2
(pub(crate) on three Emitter fields and four helper methods) are
additive and do not change any caller's signature.

Verification commands in each Run step name actual test fns the
plan-recon confirmed exist at specific lines; no filter-zero-match
risk. Pre-flight workspace baseline: 668 tests; post-iter:
669 (the new pin).

Handoff target: skills/implement on docs/plans/0104-raw-buf.1-intercept-registry.md
2026-05-29 10:53:08 +02:00

746 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 16 fns or one structural change. The "add 6 float emit fns" step is the chunkiest but each fn is 812 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.