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
35 KiB
raw-buf.1 — Intercept registry refactor — Implementation Plan
Parent spec:
docs/specs/0054-raw-buf.mdFor agentic workers: REQUIRED SUB-SKILL: use the
implementskill 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_sighelper + theregistry_contains_all_legacy_armspin test. - Modify:
crates/ailang-codegen/src/lib.rs:43-48— addmod intercepts;. - Modify:
crates/ailang-codegen/src/lib.rs— bump visibility of fieldsbody,locals,block_terminated(currently private) topub(crate)so sibling-module free fns can read/write them. Bump visibility of helper methodsemit_compare_ladder,emit_ordering_arm,emit_direct_int_icmp_intercept,lower_ctor(currently private;start_block,fresh_ssa,fresh_idare alreadypub(crate)) topub(crate). - Modify:
crates/ailang-codegen/src/lib.rs:2841-3147— replace the 300-line match body oftry_emit_primitive_instance_bodywith a 6-line shim that consultsintercepts::lookup. - Modify:
crates/ailang-codegen/src/lib.rs:1172(theintercept_emit_wants_alwaysinlinepredicate body) — rewrite asintercepts::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 testscrates/ailang-codegen/tests/eq_primitives_pin.rsandcrates/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.rswith the registry types, empty table, lookup, and sig-check helper.
//! 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 incrates/ailang-codegen/src/lib.rs(currently L43-48, alphabetically sorted:drop,escape,lambda,match_lower,subst,synth). Insertinterceptsbetweenescapeandlambdato 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 + populateINTERCEPTS) -
Modify:
crates/ailang-codegen/src/lib.rs(visibility bumps on fields + helper methods) -
Step 1: Bump visibility on the three
Emitterstruct 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
Emitterhelper 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 byemit_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) tocrates/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).
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).
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.
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.
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
INTERCEPTStable 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.
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_bodywith 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:
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(theintercept_emit_wants_alwaysinlinepredicate body) -
Step 1: Rewrite the
intercept_emit_wants_alwaysinlinepredicate 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:
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:
#[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.
-
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 tocrates/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_armstest) → 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).
-
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.
-
Type consistency.
Emitter<'_>is the struct name everywhere;Interceptis 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_bodyis preserved as a method onEmitter. All 18 emit-fn names follow the conventionemit_<lowered_name>where<lowered_name>is the legacy arm name with__→_and lowercased (eq__Str→emit_eq_str). -
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.
-
No commit steps. No
git commitanywhere in the task templates. -
Pin/replacement substring contiguity. Task 5's pin test asserts the 18 names exist in the registry. The replacement body for
INTERCEPTSin 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. -
Compile-gate vs deferred-caller ordering. The fn
try_emit_primitive_instance_bodykeeps 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. Theintercept_emit_wants_alwaysinlinepredicate signature also stays constant (only body changes in Task 4). -
Verification-command filter strings. Each
cargo testfilter 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_runsatcrates/ail/tests/e2e.rs:945;compare_primitives_smoke_prints_1_2_3_thriceatcrates/ail/tests/e2e.rs:932;float_compare_smoke_prints_true_true_falseatcrates/ail/tests/float_compare_smoke_e2e.rs:11;eq_ord_polymorphic_runs_end_to_endatcrates/ail/tests/eq_ord_e2e.rs:61. The new pinregistry_contains_all_legacy_armsis invented in this plan; the Task 5 Step 2 invocationcargo test -p ailang-codegen --lib intercepts::tests::registry_contains_all_legacy_armsfollows the standardcargo test --libform for in-source#[cfg(test)] mod tests. Thecargo test --workspaceinvocation has no filter — it runs the whole suite. Filter-zero-match risk: zero.