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
This commit is contained in:
@@ -0,0 +1,745 @@
|
||||
# raw-buf.1 — Intercept registry refactor — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0054-raw-buf.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the
|
||||
> `implement` skill to run this plan. Steps use `- [ ]`
|
||||
> checkboxes for tracking.
|
||||
|
||||
**Goal:** Lift the hardcoded `try_emit_primitive_instance_body` match in `crates/ailang-codegen/src/lib.rs` (18 arms) into a single registry table consulted at the existing dispatch site. Zero behavioural change.
|
||||
|
||||
**Architecture:** New file `crates/ailang-codegen/src/intercepts.rs` carries an `Intercept` struct (`name`, `expected_params`, `expected_ret`, `wants_alwaysinline`, `emit: fn(&mut Emitter) -> Result<()>`), an `INTERCEPTS: &[Intercept]` static table, and a `lookup(name) -> Option<&'static Intercept>`. Each arm body becomes a free fn `emit_<name>(emitter)`. The wrapper `try_emit_primitive_instance_body` survives as a thin shim that does `match intercepts::lookup(fn_name) { Some(i) => { check_sig(i, ...)?; (i.emit)(self)?; Ok(true) } None => Ok(false) }` — so the 14 in-source comment-refs to the wrapper stay valid. The `intercept_emit_wants_alwaysinline` predicate is rewritten to consult the registry, closing the silent-drift class that recon flagged.
|
||||
|
||||
**Tech Stack:** Rust 1.x (stable), `crates/ailang-codegen/` (lib + intercepts submodule), workspace integration tests in `crates/ail/tests/`.
|
||||
|
||||
---
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Create: `crates/ailang-codegen/src/intercepts.rs` — the registry table + entry types + lookup + free emit fns + `check_sig` helper + the `registry_contains_all_legacy_arms` pin test.
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:43-48` — add `mod intercepts;`.
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs` — bump visibility of fields `body`, `locals`, `block_terminated` (currently private) to `pub(crate)` so sibling-module free fns can read/write them. Bump visibility of helper methods `emit_compare_ladder`, `emit_ordering_arm`, `emit_direct_int_icmp_intercept`, `lower_ctor` (currently private; `start_block`, `fresh_ssa`, `fresh_id` are already `pub(crate)`) to `pub(crate)`.
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:2841-3147` — replace the 300-line match body of `try_emit_primitive_instance_body` with a 6-line shim that consults `intercepts::lookup`.
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:1172` (the `intercept_emit_wants_alwaysinline` predicate body) — rewrite as `intercepts::lookup(symbol).map_or(false, |i| i.wants_alwaysinline)`.
|
||||
- Test: `crates/ailang-codegen/src/intercepts.rs::registry_contains_all_legacy_arms` — enumerates all 18 legacy arm names + asserts each resolves to a registered entry. Ratifies the migration as complete (a half-finished migration where any name is missing becomes a hard test fail).
|
||||
- Test (regression, not edited): the four E2E build-and-run tests named in spec § Testing strategy raw-buf.1 — `eq_primitives_smoke_compiles_and_runs`, `compare_primitives_smoke_prints_1_2_3_thrice`, `float_compare_smoke_prints_true_true_false`, `eq_ord_polymorphic_runs_end_to_end`. Plus the alwaysinline-presence pin tests `crates/ailang-codegen/tests/eq_primitives_pin.rs` and `crates/ail/tests/prelude_eq_alwaysinline_ir_pin.rs`. Plus the full workspace.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: intercepts module skeleton — types, empty table, lookup, sig_check
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/ailang-codegen/src/intercepts.rs`
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:43-48` (mod declaration list)
|
||||
|
||||
- [ ] **Step 1: Create the new file `crates/ailang-codegen/src/intercepts.rs` with the registry types, empty table, lookup, and sig-check helper.**
|
||||
|
||||
```rust
|
||||
//! Codegen intercept registry — one dispatch table for every
|
||||
//! mono-symbol whose body the codegen supplies as LLVM IR
|
||||
//! directly (replacing the .ail placeholder body that the
|
||||
//! intercept's home module ships for round-trip stability).
|
||||
//!
|
||||
//! Each entry carries:
|
||||
//! - `name` — the mono symbol the dispatch keys on
|
||||
//! - `expected_params` — LLVM-IR param types as text
|
||||
//! - `expected_ret` — LLVM-IR return type as text
|
||||
//! - `wants_alwaysinline` — whether the dispatch site should
|
||||
//! attach the `alwaysinline` attribute
|
||||
//! to the emitted fn (was a separate
|
||||
//! hardcoded name-list in the predicate
|
||||
//! `intercept_emit_wants_alwaysinline`
|
||||
//! before raw-buf.1)
|
||||
//! - `emit` — the per-arm IR-emission body, run
|
||||
//! against `&mut Emitter` after the
|
||||
//! sig check
|
||||
//!
|
||||
//! Adding a new intercept = adding one row to `INTERCEPTS`. The
|
||||
//! dispatch site (`try_emit_primitive_instance_body` in
|
||||
//! `crates/ailang-codegen/src/lib.rs`) and the alwaysinline
|
||||
//! predicate both consult this single table.
|
||||
|
||||
use crate::{CodegenError, Emitter, Result};
|
||||
|
||||
pub(crate) struct Intercept {
|
||||
pub name: &'static str,
|
||||
pub expected_params: &'static [&'static str],
|
||||
pub expected_ret: &'static str,
|
||||
pub wants_alwaysinline: bool,
|
||||
pub emit: fn(&mut Emitter<'_>) -> Result<()>,
|
||||
}
|
||||
|
||||
pub(crate) static INTERCEPTS: &[Intercept] = &[];
|
||||
|
||||
pub(crate) fn lookup(name: &str) -> Option<&'static Intercept> {
|
||||
INTERCEPTS.iter().find(|i| i.name == name)
|
||||
}
|
||||
|
||||
pub(crate) fn check_sig(
|
||||
intercept: &Intercept,
|
||||
param_tys: &[String],
|
||||
ret_ty: &str,
|
||||
) -> Result<()> {
|
||||
let params_match = param_tys.len() == intercept.expected_params.len()
|
||||
&& param_tys
|
||||
.iter()
|
||||
.zip(intercept.expected_params.iter())
|
||||
.all(|(have, want)| have == want);
|
||||
if !params_match || ret_ty != intercept.expected_ret {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"{} body intercept: unexpected signature \
|
||||
({:?}) -> {} (want {:?} -> {})",
|
||||
intercept.name,
|
||||
param_tys,
|
||||
ret_ty,
|
||||
intercept.expected_params,
|
||||
intercept.expected_ret,
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `mod intercepts;` to the mod-declaration block in `crates/ailang-codegen/src/lib.rs` (currently L43-48, alphabetically sorted: `drop`, `escape`, `lambda`, `match_lower`, `subst`, `synth`). Insert `intercepts` between `escape` and `lambda` to preserve alphabetical order.**
|
||||
|
||||
Edit at `crates/ailang-codegen/src/lib.rs:43-48`. Insert the line `mod intercepts;` between the existing `mod escape;` and `mod lambda;` lines.
|
||||
|
||||
- [ ] **Step 3: Build the workspace — empty registry compiles clean.**
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | tail -5`
|
||||
Expected: build succeeds, zero warnings about `intercepts` being unused (the `pub(crate)` visibility and the `lookup` fn keep it consumed by neither call site yet — Rust does NOT warn on `pub(crate)` items in a sibling module, only on truly unused items). If a `dead_code` warning fires for `INTERCEPTS` or `Intercept` because the table is empty, that's expected and will go away in Task 2 when entries are added; do NOT add `#[allow(dead_code)]` — just accept the warning until Task 2.
|
||||
|
||||
```
|
||||
cargo build --workspace 2>&1 | tail -5
|
||||
```
|
||||
|
||||
Expected output ends with `Finished` and no errors. A few dead-code warnings about `intercepts::Intercept` / `INTERCEPTS` / `lookup` / `check_sig` are acceptable at this point.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Populate the registry — 18 entries + per-arm emit fns + visibility bumps
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-codegen/src/intercepts.rs` (add the 18 free emit fns + populate `INTERCEPTS`)
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs` (visibility bumps on fields + helper methods)
|
||||
|
||||
- [ ] **Step 1: Bump visibility on the three `Emitter` struct fields the intercept bodies read from `&mut self`.**
|
||||
|
||||
In `crates/ailang-codegen/src/lib.rs`, find the `Emitter` struct definition starting at L706 and bump these three fields to `pub(crate)`:
|
||||
|
||||
- `body: String` (around L711) → `pub(crate) body: String,`
|
||||
- `locals: Vec<(String, String, String, Type)>` (around L725) → `pub(crate) locals: Vec<(String, String, String, Type)>,`
|
||||
- `block_terminated: bool` (around L765) → `pub(crate) block_terminated: bool,`
|
||||
|
||||
(If the field types differ slightly from what's listed — e.g. the locals tuple shape — preserve the existing type and just add the `pub(crate)` prefix.)
|
||||
|
||||
- [ ] **Step 2: Bump visibility on the four `Emitter` helper methods that the intercept emit fns call.**
|
||||
|
||||
In `crates/ailang-codegen/src/lib.rs`, locate each of these methods inside `impl<'a> Emitter<'a>` (L921 onwards) and prepend `pub(crate)`:
|
||||
|
||||
- `fn emit_compare_ladder(&mut self, lt: &str, eq: &str) -> Result<()>` at L3220 → `pub(crate) fn emit_compare_ladder(...)`
|
||||
- `fn emit_ordering_arm(&mut self, label: &str, ctor: &str) -> Result<()>` at L3190 → `pub(crate) fn emit_ordering_arm(...)`
|
||||
- `fn emit_direct_int_icmp_intercept(...)` at L3156 → `pub(crate) fn emit_direct_int_icmp_intercept(...)`
|
||||
- `fn lower_ctor(...)` (used by `emit_ordering_arm`; exact line variable) → `pub(crate) fn lower_ctor(...)`
|
||||
|
||||
`start_block`, `fresh_ssa`, `fresh_id` are already `pub(crate)`; no edit needed. `intercept_emit_wants_alwaysinline` is rewritten in Task 4 and does not need visibility bumping (it stays a method on Emitter consulted from inside the crate).
|
||||
|
||||
- [ ] **Step 3: Build to confirm visibility-bumps compile clean before adding emit fns.**
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | tail -5`
|
||||
Expected: build succeeds; no new warnings or errors from the visibility changes.
|
||||
|
||||
- [ ] **Step 4: Add the two Str-path emit fns (`emit_eq_str`, `emit_compare_str`) to `crates/ailang-codegen/src/intercepts.rs`.**
|
||||
|
||||
Append the two fns below to `intercepts.rs` after the `check_sig` helper. The bodies are extracted verbatim from `try_emit_primitive_instance_body` at L2848 (`eq__Str` arm) and L2915 (`compare__Str` arm) of `lib.rs`, minus the per-arm sig-check prologue (which `check_sig` now handles) and minus the trailing `Ok(true)` (which the dispatch shim wraps).
|
||||
|
||||
```rust
|
||||
pub(crate) fn emit_eq_str(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
let a_bytes = emitter.fresh_ssa();
|
||||
let b_bytes = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {a_bytes} = getelementptr inbounds i8, ptr {a_ssa}, i64 8\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(
|
||||
" {b_bytes} = getelementptr inbounds i8, ptr {b_ssa}, i64 8\n"
|
||||
));
|
||||
let dst = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {dst} = call zeroext i1 @ail_str_eq(ptr {a_bytes}, ptr {b_bytes})\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(" ret i1 {dst}\n"));
|
||||
emitter.body.push_str("}\n\n");
|
||||
emitter.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn emit_compare_str(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
let a_bytes = emitter.fresh_ssa();
|
||||
let b_bytes = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {a_bytes} = getelementptr inbounds i8, ptr {a_ssa}, i64 8\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(
|
||||
" {b_bytes} = getelementptr inbounds i8, ptr {b_ssa}, i64 8\n"
|
||||
));
|
||||
let cmp_res = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {cmp_res} = call i32 @ail_str_compare(ptr {a_bytes}, ptr {b_bytes})\n"
|
||||
));
|
||||
emitter.emit_compare_ladder(
|
||||
&format!("icmp slt i32 {cmp_res}, 0"),
|
||||
&format!("icmp eq i32 {cmp_res}, 0"),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Note for the implementer:** verify the verbatim text of the `eq__Str` and `compare__Str` arms in `crates/ailang-codegen/src/lib.rs` before lifting. The exact `format!` template strings, the exact `cmp_res` ladder argument shape, and the `emit_compare_ladder` invocation arguments are the IR-fidelity critical bits. If the bodies in `lib.rs` differ from what is shown above (e.g. an extra SSA fetch was added in a later commit), preserve the lib.rs verbatim body and just structurally separate the sig-check prologue from the actual IR emission.
|
||||
|
||||
- [ ] **Step 5: Add the five Eq/Compare emit fns (`emit_compare_int`, `emit_compare_bool`, `emit_eq_int`, `emit_eq_bool`, `emit_eq_unit`).**
|
||||
|
||||
Append the five fns below to `intercepts.rs` after `emit_compare_str`. Bodies are extracted verbatim from `try_emit_primitive_instance_body` at L2883, L2899, L2947, L2966, L2985 of `lib.rs`, minus per-arm sig-checks and trailing `Ok(true)`.
|
||||
|
||||
```rust
|
||||
pub(crate) fn emit_compare_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
emitter.emit_compare_ladder(
|
||||
&format!("icmp slt i64 {a_ssa}, {b_ssa}"),
|
||||
&format!("icmp eq i64 {a_ssa}, {b_ssa}"),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn emit_compare_bool(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
emitter.emit_compare_ladder(
|
||||
&format!("icmp ult i1 {a_ssa}, {b_ssa}"),
|
||||
&format!("icmp eq i1 {a_ssa}, {b_ssa}"),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn emit_eq_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
let dst = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {dst} = icmp eq i64 {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(" ret i1 {dst}\n"));
|
||||
emitter.body.push_str("}\n\n");
|
||||
emitter.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn emit_eq_bool(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
let dst = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {dst} = icmp eq i1 {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(" ret i1 {dst}\n"));
|
||||
emitter.body.push_str("}\n\n");
|
||||
emitter.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn emit_eq_unit(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
emitter.body.push_str(" ret i1 1\n");
|
||||
emitter.body.push_str("}\n\n");
|
||||
emitter.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Note for the implementer:** the `emit_eq_int` / `emit_eq_bool` body shapes above are the planner's reconstruction from the recon report's "icmp eq … / ret i1" idiom column. Open `crates/ailang-codegen/src/lib.rs` at the `eq__Int` (L2947) and `eq__Bool` (L2966) arms and lift the actual body text verbatim. If the lib.rs body differs in `format!` template wording, IR comment lines, or attribute placement, preserve the lib.rs version exactly — the goal is byte-identical emitted IR after the dispatch site rewires.
|
||||
|
||||
- [ ] **Step 6: Add the six float-family emit fns (`emit_float_eq`, `emit_float_ne`, `emit_float_lt`, `emit_float_le`, `emit_float_gt`, `emit_float_ge`).**
|
||||
|
||||
Append the six fns below to `intercepts.rs` after `emit_eq_unit`. Bodies are extracted verbatim from `try_emit_primitive_instance_body` at L3000, L3019, L3040, L3059, L3078, L3097 of `lib.rs`, minus per-arm sig-checks and trailing `Ok(true)`. The fcmp predicates are NaN-aware (`oeq`, `une`, `olt`, `ole`, `ogt`, `oge`) — preserve them exactly as the lib.rs source has them. Note `float_ne` uses `une` (unordered-or-not-equal), not `one` (ordered-not-equal) — this is deliberate NaN-aware semantics per the comment at the lib.rs arm.
|
||||
|
||||
```rust
|
||||
pub(crate) fn emit_float_eq(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
let dst = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {dst} = fcmp oeq double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(" ret i1 {dst}\n"));
|
||||
emitter.body.push_str("}\n\n");
|
||||
emitter.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn emit_float_ne(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
let dst = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {dst} = fcmp une double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(" ret i1 {dst}\n"));
|
||||
emitter.body.push_str("}\n\n");
|
||||
emitter.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn emit_float_lt(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
let dst = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {dst} = fcmp olt double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(" ret i1 {dst}\n"));
|
||||
emitter.body.push_str("}\n\n");
|
||||
emitter.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn emit_float_le(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
let dst = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {dst} = fcmp ole double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(" ret i1 {dst}\n"));
|
||||
emitter.body.push_str("}\n\n");
|
||||
emitter.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn emit_float_gt(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
let dst = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {dst} = fcmp ogt double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(" ret i1 {dst}\n"));
|
||||
emitter.body.push_str("}\n\n");
|
||||
emitter.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn emit_float_ge(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
let n = emitter.locals.len();
|
||||
let a_ssa = emitter.locals[n - 2].1.clone();
|
||||
let b_ssa = emitter.locals[n - 1].1.clone();
|
||||
let dst = emitter.fresh_ssa();
|
||||
emitter.body.push_str(&format!(
|
||||
" {dst} = fcmp oge double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
emitter.body.push_str(&format!(" ret i1 {dst}\n"));
|
||||
emitter.body.push_str("}\n\n");
|
||||
emitter.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Add the five Int icmp-delegate emit fns (`emit_lt_int`, `emit_le_int`, `emit_gt_int`, `emit_ge_int`, `emit_ne_int`).**
|
||||
|
||||
Append the five fns below to `intercepts.rs` after `emit_float_ge`. Bodies are 1-line delegates to the existing `Emitter::emit_direct_int_icmp_intercept` helper (now `pub(crate)` per Step 2). The icmp predicate strings come from lib.rs L3140-3144.
|
||||
|
||||
```rust
|
||||
pub(crate) fn emit_lt_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
emitter.emit_direct_int_icmp_intercept("icmp slt i64")
|
||||
}
|
||||
|
||||
pub(crate) fn emit_le_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
emitter.emit_direct_int_icmp_intercept("icmp sle i64")
|
||||
}
|
||||
|
||||
pub(crate) fn emit_gt_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
emitter.emit_direct_int_icmp_intercept("icmp sgt i64")
|
||||
}
|
||||
|
||||
pub(crate) fn emit_ge_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
emitter.emit_direct_int_icmp_intercept("icmp sge i64")
|
||||
}
|
||||
|
||||
pub(crate) fn emit_ne_int(emitter: &mut Emitter<'_>) -> Result<()> {
|
||||
emitter.emit_direct_int_icmp_intercept("icmp ne i64")
|
||||
}
|
||||
```
|
||||
|
||||
**Note for the implementer:** the recon report says these five arms (lib.rs L3140-3144) delegate to `emit_direct_int_icmp_intercept` with a predicate string. Verify the exact signature of `emit_direct_int_icmp_intercept` at lib.rs L3156 before lifting — if it takes more than one argument (e.g. an additional "compare-form" enum or a Result-returning bool), adapt the delegate signatures to match. The shape above assumes single-arg `&str` predicate.
|
||||
|
||||
- [ ] **Step 8: Populate the `INTERCEPTS` table with all 18 entries.**
|
||||
|
||||
Replace the `pub(crate) static INTERCEPTS: &[Intercept] = &[];` line in `intercepts.rs` with the populated table below. Set `wants_alwaysinline` per entry; the values mirror the current name-list in `intercept_emit_wants_alwaysinline` at `crates/ailang-codegen/src/lib.rs:1172` — every existing intercept that the predicate lists today gets `true`, the rest get `false`.
|
||||
|
||||
```rust
|
||||
pub(crate) static INTERCEPTS: &[Intercept] = &[
|
||||
Intercept {
|
||||
name: "eq__Str",
|
||||
expected_params: &["ptr", "ptr"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_eq_str,
|
||||
},
|
||||
Intercept {
|
||||
name: "compare__Int",
|
||||
expected_params: &["i64", "i64"],
|
||||
expected_ret: "ptr",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_compare_int,
|
||||
},
|
||||
Intercept {
|
||||
name: "compare__Bool",
|
||||
expected_params: &["i1", "i1"],
|
||||
expected_ret: "ptr",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_compare_bool,
|
||||
},
|
||||
Intercept {
|
||||
name: "compare__Str",
|
||||
expected_params: &["ptr", "ptr"],
|
||||
expected_ret: "ptr",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_compare_str,
|
||||
},
|
||||
Intercept {
|
||||
name: "eq__Int",
|
||||
expected_params: &["i64", "i64"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_eq_int,
|
||||
},
|
||||
Intercept {
|
||||
name: "eq__Bool",
|
||||
expected_params: &["i1", "i1"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_eq_bool,
|
||||
},
|
||||
Intercept {
|
||||
name: "eq__Unit",
|
||||
expected_params: &[],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_eq_unit,
|
||||
},
|
||||
Intercept {
|
||||
name: "float_eq",
|
||||
expected_params: &["double", "double"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_float_eq,
|
||||
},
|
||||
Intercept {
|
||||
name: "float_ne",
|
||||
expected_params: &["double", "double"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_float_ne,
|
||||
},
|
||||
Intercept {
|
||||
name: "float_lt",
|
||||
expected_params: &["double", "double"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_float_lt,
|
||||
},
|
||||
Intercept {
|
||||
name: "float_le",
|
||||
expected_params: &["double", "double"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_float_le,
|
||||
},
|
||||
Intercept {
|
||||
name: "float_gt",
|
||||
expected_params: &["double", "double"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_float_gt,
|
||||
},
|
||||
Intercept {
|
||||
name: "float_ge",
|
||||
expected_params: &["double", "double"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_float_ge,
|
||||
},
|
||||
Intercept {
|
||||
name: "lt__Int",
|
||||
expected_params: &["i64", "i64"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_lt_int,
|
||||
},
|
||||
Intercept {
|
||||
name: "le__Int",
|
||||
expected_params: &["i64", "i64"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_le_int,
|
||||
},
|
||||
Intercept {
|
||||
name: "gt__Int",
|
||||
expected_params: &["i64", "i64"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_gt_int,
|
||||
},
|
||||
Intercept {
|
||||
name: "ge__Int",
|
||||
expected_params: &["i64", "i64"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_ge_int,
|
||||
},
|
||||
Intercept {
|
||||
name: "ne__Int",
|
||||
expected_params: &["i64", "i64"],
|
||||
expected_ret: "i1",
|
||||
wants_alwaysinline: true,
|
||||
emit: emit_ne_int,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
**Note for the implementer:** the `expected_params` for `eq__Unit` is `&[]` because lib.rs L2985 has `(_ params ignored)` per the recon — Unit values carry no SSA payload. Verify against the lib.rs arm; if the actual arm checks against a different param shape (e.g. `["ptr"]` for a Unit-token slot), use the lib.rs version. The `wants_alwaysinline: true` on every entry mirrors the current predicate's all-inclusive name list per recon — if the predicate at lib.rs L1172 actually distinguishes some entries (e.g. only Eq names get inline, not Compare names), copy that distinction here. This is the **lockstep coupling** (intercept names ↔ alwaysinline-name-list) that the registry consolidates.
|
||||
|
||||
- [ ] **Step 9: Build the workspace; verify all four E2E ratifier tests still pass.**
|
||||
|
||||
The `INTERCEPTS` table is now populated but **not yet consulted** by the dispatch site — `try_emit_primitive_instance_body` still runs the full match in `lib.rs`. The table is dead code at this point. Behaviour is unchanged.
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | tail -5`
|
||||
Expected: build succeeds; dead-code warnings on `INTERCEPTS` / `lookup` / `check_sig` (the table is populated but the dispatch site does not yet call `lookup`) and possibly on the emit fns are acceptable.
|
||||
|
||||
Run: `cargo test -p ail --test e2e eq_primitives_smoke_compiles_and_runs`
|
||||
Expected: PASS
|
||||
|
||||
Run: `cargo test -p ail --test e2e compare_primitives_smoke_prints_1_2_3_thrice`
|
||||
Expected: PASS
|
||||
|
||||
Run: `cargo test -p ail --test float_compare_smoke_e2e float_compare_smoke_prints_true_true_false`
|
||||
Expected: PASS
|
||||
|
||||
Run: `cargo test -p ail --test eq_ord_e2e eq_ord_polymorphic_runs_end_to_end`
|
||||
Expected: PASS
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Rewire the dispatch site — `try_emit_primitive_instance_body` body → `intercepts::lookup` shim
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:2841-3147` (the entire fn body, replaced by a 6-line shim)
|
||||
|
||||
- [ ] **Step 1: Replace the body of `try_emit_primitive_instance_body` with the registry-consulting shim.**
|
||||
|
||||
In `crates/ailang-codegen/src/lib.rs`, locate `try_emit_primitive_instance_body` at L2841. The fn currently runs from L2841 to L3147 (~306 lines). Preserve the fn signature (so all 14 in-source comment-refs remain valid pointers to the canonical dispatch name), and replace the entire body with:
|
||||
|
||||
```rust
|
||||
fn try_emit_primitive_instance_body(
|
||||
&mut self,
|
||||
fn_name: &str,
|
||||
param_tys: &[String],
|
||||
ret_ty: &str,
|
||||
) -> Result<bool> {
|
||||
match intercepts::lookup(fn_name) {
|
||||
Some(intercept) => {
|
||||
intercepts::check_sig(intercept, param_tys, ret_ty)?;
|
||||
(intercept.emit)(self)?;
|
||||
Ok(true)
|
||||
}
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The 18 match arms at L2848-L3144 are deleted. The trailing `_ => Ok(false)` fallthrough is preserved by the `None` arm of the `match intercepts::lookup(...)`.
|
||||
|
||||
- [ ] **Step 2: Build the workspace + run the four E2E ratifiers.**
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | tail -5`
|
||||
Expected: build succeeds; the dead-code warnings on `INTERCEPTS` / `lookup` / `check_sig` / the emit fns are now gone (the dispatch site consumes them).
|
||||
|
||||
Run: `cargo test -p ail --test e2e eq_primitives_smoke_compiles_and_runs`
|
||||
Expected: PASS — `eq__Str` and `eq__Int|Bool` arms now flow through the registry; printed output unchanged.
|
||||
|
||||
Run: `cargo test -p ail --test e2e compare_primitives_smoke_prints_1_2_3_thrice`
|
||||
Expected: PASS — `compare__Int|Bool|Str` arms now flow through the registry; printed output unchanged.
|
||||
|
||||
Run: `cargo test -p ail --test float_compare_smoke_e2e float_compare_smoke_prints_true_true_false`
|
||||
Expected: PASS — the six `float_*` arms now flow through the registry; printed output unchanged.
|
||||
|
||||
Run: `cargo test -p ail --test eq_ord_e2e eq_ord_polymorphic_runs_end_to_end`
|
||||
Expected: PASS — polymorphic Eq/Ord dispatch over Int/Bool/Str exercises the registry secondarily.
|
||||
|
||||
- [ ] **Step 3: Run the full workspace test suite to catch any regression the four E2E ratifiers missed.**
|
||||
|
||||
Run: `cargo test --workspace 2>&1 | tail -10`
|
||||
Expected: every test passes; baseline of 668 tests stays green (Task 5 adds one new test bringing the total to 669, but at this point the count is still 668).
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Fold `wants_alwaysinline` into the registry — close the lockstep drift class
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs:1172` (the `intercept_emit_wants_alwaysinline` predicate body)
|
||||
|
||||
- [ ] **Step 1: Rewrite the `intercept_emit_wants_alwaysinline` predicate body to consult the registry.**
|
||||
|
||||
In `crates/ailang-codegen/src/lib.rs`, locate `intercept_emit_wants_alwaysinline` starting at L1172. The predicate is currently a method on `Emitter` (or a free fn — check the signature) that takes `symbol: &str` (or `name: &str`) and returns `bool` by matching on a hardcoded name list.
|
||||
|
||||
Replace the body with a registry consultation:
|
||||
|
||||
```rust
|
||||
pub(crate) fn intercept_emit_wants_alwaysinline(&self, symbol: &str) -> bool {
|
||||
intercepts::lookup(symbol).is_some_and(|i| i.wants_alwaysinline)
|
||||
}
|
||||
```
|
||||
|
||||
(If the existing predicate is a free fn rather than a method, drop the `&self` parameter and adjust accordingly. If it returns via a non-`is_some_and` idiom that the project uses elsewhere, prefer `.map_or(false, |i| i.wants_alwaysinline)` — both are stable Rust.)
|
||||
|
||||
- [ ] **Step 2: Build the workspace.**
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | tail -5`
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 3: Run the two alwaysinline-presence pin tests + the four E2E ratifiers.**
|
||||
|
||||
The alwaysinline attribute is what these pin tests verify is attached to the generated IR; if the predicate's new registry-driven body returns a different set than the old name list, the pins go red.
|
||||
|
||||
Run: `cargo test -p ailang-codegen --test eq_primitives_pin 2>&1 | tail -10`
|
||||
Expected: all tests in this file PASS.
|
||||
|
||||
Run: `cargo test -p ail --test prelude_eq_alwaysinline_ir_pin 2>&1 | tail -10`
|
||||
Expected: all tests in this file PASS.
|
||||
|
||||
Run: `cargo test -p ail --test e2e eq_primitives_smoke_compiles_and_runs`
|
||||
Expected: PASS.
|
||||
|
||||
Run: `cargo test -p ail --test e2e compare_primitives_smoke_prints_1_2_3_thrice`
|
||||
Expected: PASS.
|
||||
|
||||
Run: `cargo test -p ail --test float_compare_smoke_e2e float_compare_smoke_prints_true_true_false`
|
||||
Expected: PASS.
|
||||
|
||||
Run: `cargo test -p ail --test eq_ord_e2e eq_ord_polymorphic_runs_end_to_end`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Full workspace test run.**
|
||||
|
||||
Run: `cargo test --workspace 2>&1 | tail -10`
|
||||
Expected: all 668 tests PASS.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add the `registry_contains_all_legacy_arms` pin test
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-codegen/src/intercepts.rs` (append the test module at the bottom)
|
||||
|
||||
- [ ] **Step 1: Append the pin test module to `crates/ailang-codegen/src/intercepts.rs`.**
|
||||
|
||||
Append the following to the end of the file:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::lookup;
|
||||
|
||||
/// Pin: every name that was a match arm in the legacy
|
||||
/// `try_emit_primitive_instance_body` (before raw-buf.1)
|
||||
/// must resolve to a registered Intercept. If this test
|
||||
/// goes red, a name was dropped during migration AND/OR
|
||||
/// removed from the registry without the dispatch wrapper
|
||||
/// being audited to confirm no consumer still depends on
|
||||
/// it.
|
||||
#[test]
|
||||
fn registry_contains_all_legacy_arms() {
|
||||
let legacy_names = [
|
||||
"eq__Str",
|
||||
"compare__Int",
|
||||
"compare__Bool",
|
||||
"compare__Str",
|
||||
"eq__Int",
|
||||
"eq__Bool",
|
||||
"eq__Unit",
|
||||
"float_eq",
|
||||
"float_ne",
|
||||
"float_lt",
|
||||
"float_le",
|
||||
"float_gt",
|
||||
"float_ge",
|
||||
"lt__Int",
|
||||
"le__Int",
|
||||
"gt__Int",
|
||||
"ge__Int",
|
||||
"ne__Int",
|
||||
];
|
||||
|
||||
let missing: Vec<&str> = legacy_names
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|n| lookup(n).is_none())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"registry is missing legacy intercept names: {missing:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the new pin test.**
|
||||
|
||||
Run: `cargo test -p ailang-codegen --lib intercepts::tests::registry_contains_all_legacy_arms`
|
||||
Expected: PASS — all 18 names resolve.
|
||||
|
||||
- [ ] **Step 3: Final full workspace test run.**
|
||||
|
||||
Run: `cargo test --workspace 2>&1 | tail -10`
|
||||
Expected: 669 tests PASS (baseline 668 + the new pin).
|
||||
|
||||
---
|
||||
|
||||
## Self-review
|
||||
|
||||
This section is the planner's inline scrub, NOT a checklist for the implementer.
|
||||
|
||||
1. **Spec coverage.** Every iter-raw-buf.1 spec section maps to a task:
|
||||
- § Architecture point 1 ("Lift the existing hard-coded match … into a registry table") → Tasks 1+2+3.
|
||||
- § Components row 1 ("new file `crates/ailang-codegen/src/intercepts.rs`; edits to `crates/ailang-codegen/src/lib.rs`") → Tasks 1+2+3.
|
||||
- § Testing strategy "raw-buf.1" first bullet (four E2E ratifiers stay green) → Tasks 2,3,4,5 Step "Run the four E2E ratifiers".
|
||||
- § Testing strategy "raw-buf.1" second bullet (`registry_contains_all_legacy_arms` test) → Task 5.
|
||||
- Lockstep coupling that recon flagged (intercept names ↔ `intercept_emit_wants_alwaysinline`) → Task 4 (folded the bool into the table, closing the drift class).
|
||||
|
||||
2. **Placeholder scan.** No "TBD", no "TODO", no "implement later", no "similar to Task N", no "add appropriate error handling". The two "Note for the implementer" callouts (Step 4 in Task 2, Step 5 in Task 2) are verification asks against the verbatim source ("preserve the lib.rs body exactly"), not placeholders for design judgement.
|
||||
|
||||
3. **Type consistency.** `Emitter<'_>` is the struct name everywhere; `Intercept` is the registry-entry struct name; `INTERCEPTS` (static `&[Intercept]`) is the table; `lookup(name) -> Option<&'static Intercept>` is the resolver; `check_sig(intercept, params, ret)` is the sig-check helper. `try_emit_primitive_instance_body` is preserved as a method on `Emitter`. All 18 emit-fn names follow the convention `emit_<lowered_name>` where `<lowered_name>` is the legacy arm name with `__` → `_` and lowercased (`eq__Str` → `emit_eq_str`).
|
||||
|
||||
4. **Step granularity.** Task 2 has 9 steps; each step touches 1–6 fns or one structural change. The "add 6 float emit fns" step is the chunkiest but each fn is 8–12 lines of mechanical lift; total ~5 min. Other steps are 2-3 min.
|
||||
|
||||
5. **No commit steps.** No `git commit` anywhere in the task templates.
|
||||
|
||||
6. **Pin/replacement substring contiguity.** Task 5's pin test asserts the 18 names exist in the registry. The replacement body for `INTERCEPTS` in Task 2 Step 8 names each of the 18 strings literally; each name appears as a contiguous string literal (e.g. `name: "compare__Int"`), not soft-wrapped across two lines. Substring contiguity preserved.
|
||||
|
||||
7. **Compile-gate vs deferred-caller ordering.** The fn `try_emit_primitive_instance_body` keeps its signature throughout the plan. Only its body changes (Task 3). No caller-threading deferred across a compile gate. The visibility bumps in Task 2 (Steps 1+2) are additive and do not affect any caller. The `intercept_emit_wants_alwaysinline` predicate signature also stays constant (only body changes in Task 4).
|
||||
|
||||
8. **Verification-command filter strings.** Each `cargo test` filter string is a test fn name that the recon report verified exists at a specific line in a specific file. Spot-check: `eq_primitives_smoke_compiles_and_runs` at `crates/ail/tests/e2e.rs:945`; `compare_primitives_smoke_prints_1_2_3_thrice` at `crates/ail/tests/e2e.rs:932`; `float_compare_smoke_prints_true_true_false` at `crates/ail/tests/float_compare_smoke_e2e.rs:11`; `eq_ord_polymorphic_runs_end_to_end` at `crates/ail/tests/eq_ord_e2e.rs:61`. The new pin `registry_contains_all_legacy_arms` is invented in this plan; the Task 5 Step 2 invocation `cargo test -p ailang-codegen --lib intercepts::tests::registry_contains_all_legacy_arms` follows the standard `cargo test --lib` form for in-source `#[cfg(test)] mod tests`. The `cargo test --workspace` invocation has no filter — it runs the whole suite. Filter-zero-match risk: zero.
|
||||
Reference in New Issue
Block a user