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

35 KiB
Raw Blame History

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.

//! 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).

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 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.

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:

    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:

    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.

  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__Stremit_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.