iter raw-buf.1-intercept-registry (DONE 5/5): try_emit_primitive_instance_body → registry table (refs #7)

Pure refactor — first iter of the raw-buf milestone. The
hardcoded 18-arm match in try_emit_primitive_instance_body
(crates/ailang-codegen/src/lib.rs L2841-3147 pre-iter, ~305
lines) becomes a 6-line shim that consults a single registry
table in the new sibling module crates/ailang-codegen/src/intercepts.rs.

Zero behavioural change. The wrapper name survives, all 14
in-source comment-refs to try_emit_primitive_instance_body stay
valid pointers to the canonical dispatch entry.

What landed (5 tasks):

  Task 1 — module skeleton: Intercept struct, INTERCEPTS static
  table (empty), lookup(name), check_sig(intercept, params, ret).
  mod intercepts; wired into lib.rs at L43.

  Task 2 — populate: 18 free emit fns lifted from the legacy
  match arms; INTERCEPTS populated with one entry per name. Three
  Emitter fields (body, locals, block_terminated) and three helper
  methods (emit_compare_ladder, emit_ordering_arm,
  emit_direct_int_icmp_intercept) bumped to pub(crate) so the
  sibling module can reach them. lower_ctor stayed at its existing
  visibility — not consumed by sibling-module fns.

  Task 3 — rewire dispatch: try_emit_primitive_instance_body body
  becomes `match intercepts::lookup(fn_name) { Some(i) => {
  check_sig(i, params, ret)?; (i.emit)(self)?; Ok(true) } None =>
  Ok(false) }`. The 305-line match collapses to 9 lines.

  Task 4 — fold wants_alwaysinline into the registry: every entry
  in INTERCEPTS carries wants_alwaysinline: bool. The predicate
  intercept_emit_wants_alwaysinline now consults the registry for
  branch 1 (the literal 13-name list the predicate hardcoded). One
  source of truth instead of two — closes the silent-drift class
  recon flagged.

  Task 5 — registry_contains_all_legacy_arms pin test: enumerates
  all 18 legacy names + asserts each resolves to a registered
  entry. A half-finished migration that drops any name becomes a
  hard test fail.

Two judgement calls the implementer made (both behaviour-
preserving, both worth knowing):

  (1) eq__Unit param shape: the plan named expected_params: &[]
  per the recon's "params ignored" reading. Reality: the call
  site delivers ["i8","i8"] (Unit lowers to i8 per codegen //!
  header). The registry entry uses ["i8","i8"]; the emit fn
  discards the operand SSAs and returns `ret i1 1` — semantically
  equivalent to the legacy arm that checked only ret_ty and
  ignored params. An inline rustdoc on the entry names the
  rationale.

  (2) intercept_emit_wants_alwaysinline has TWO branches, not one
  as the plan inferred. Branch 1 = the literal 13-name list (which
  the iter's drift-closure intent covers). Branch 2 = a stem-prefix
  carve-out matching {ne|lt|le|gt|ge}__* that covers polymorphic-
  body monomorphizations (lt__Bool, ne__Str, ne__MyADT, …) which
  are NOT registry entries and never were. A literal "replace with
  one-line registry lookup" would silently drop alwaysinline on
  those names, breaking the bench-perf parity established in iter
  operator-routing-eq-ord.1. Branch 2 stays as it was, with a
  fresh comment naming what the registry covers vs. what branch 2
  covers.

  Follow-up consideration (not in this iter): extend the Intercept
  variant to a predicate-only entry shape so branch 2 can fold
  into the registry as well, OR accept the carve-out as the final
  shape. Either is reasonable. Filed as a backlog idea (see Gitea
  issue created post-commit if labelled).

Verification:

  cargo test --workspace      → 667 passed, 0 failed, 2 ignored
                                (baseline was 666 + 2 ignored;
                                +1 from registry_contains_all_legacy_arms)

  Four E2E ratifiers GREEN (named in spec § Testing strategy
  raw-buf.1):
    - eq_primitives_smoke_compiles_and_runs (crates/ail/tests/e2e.rs)
    - compare_primitives_smoke_prints_1_2_3_thrice (crates/ail/tests/e2e.rs)
    - float_compare_smoke_prints_true_true_false
    - eq_ord_polymorphic_runs_end_to_end

  Alwaysinline pins GREEN (regression net for branch-2 carve-out):
    - crates/ailang-codegen/tests/eq_primitives_pin.rs
    - crates/ail/tests/prelude_eq_alwaysinline_ir_pin.rs

  New pin GREEN:
    - intercepts::tests::registry_contains_all_legacy_arms

Diff size: lib.rs −318 / +26 (net −292); intercepts.rs +488 (new
file). Net repository LOC delta: +196. The size reduction in
lib.rs is the wrong proxy — the value is the consolidation: one
table, one drift-closed predicate, one pin test enumerating the
migration surface.

The raw-buf.2 iter will rename crates/ailang-kernel-stub/ to
crates/ailang-kernel/ and reshape it as the family-crate that
hosts raw_buf alongside kernel_stub. The registry shipped here
is the substrate raw-buf.3 will plug RawBuf's 12 codegen
intercepts into.
This commit is contained in:
2026-05-29 11:06:55 +02:00
parent d1612d5463
commit 140a0c0ef7
2 changed files with 514 additions and 318 deletions
+488
View File
@@ -0,0 +1,488 @@
//! 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] = &[
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",
// Unit lowers to `i8` per the codegen type-mapping (see lib.rs
// //! header). The legacy arm checked only `ret_ty != "i1"` and
// ignored params; this entry restores that semantics under the
// centralised `check_sig` by naming the actual param shape the
// call site delivers. The emit fn discards the locals.
expected_params: &["i8", "i8"],
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,
},
];
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(())
}
// ---------------------------------------------------------------
// Per-intercept emit fns. Bodies lifted verbatim from the legacy
// `try_emit_primitive_instance_body` match arms in `lib.rs` (raw-buf.1
// migration). Per-arm sig-check prologues are removed because the
// dispatch shim runs `check_sig` first; trailing `Ok(true)` is
// replaced by `Ok(())` because the shim wraps the bool.
// ---------------------------------------------------------------
pub(crate) fn emit_eq_str(emitter: &mut Emitter<'_>) -> Result<()> {
// The two params are the two most recently pushed locals;
// `emit_fn` populated `self.locals` from `f.params` before
// dispatching here. Pull their SSA names without assuming
// a specific surface-level binder name.
let n = emitter.locals.len();
let a_ssa = emitter.locals[n - 2].1.clone();
let b_ssa = emitter.locals[n - 1].1.clone();
// IR-Str pointers now land on the
// `len`-field of the packed-struct slab; @ail_str_eq's
// strcmp-based body needs the bytes pointer 8 bytes
// further on.
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_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_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();
// IR-Str pointers now land on the
// `len`-field of the packed-struct slab; @ail_str_compare's
// strcmp-based body needs the bytes pointer 8 bytes
// further on.
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(())
}
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 r = emitter.fresh_ssa();
emitter.body.push_str(&format!(
" {r} = icmp eq i64 {a_ssa}, {b_ssa}\n"
));
emitter.body.push_str(&format!(" ret i1 {r}\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 r = emitter.fresh_ssa();
emitter.body.push_str(&format!(
" {r} = icmp eq i1 {a_ssa}, {b_ssa}\n"
));
emitter.body.push_str(&format!(" ret i1 {r}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
pub(crate) fn emit_eq_unit(emitter: &mut Emitter<'_>) -> Result<()> {
// Unit is single-inhabitant: all values compare equal.
// The fn signature still carries the operand slots; we
// discard them and return true unconditionally.
emitter.body.push_str(" ret i1 1\n");
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
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 r = emitter.fresh_ssa();
emitter.body.push_str(&format!(
" {r} = fcmp oeq double {a_ssa}, {b_ssa}\n"
));
emitter.body.push_str(&format!(" ret i1 {r}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
pub(crate) fn emit_float_ne(emitter: &mut Emitter<'_>) -> Result<()> {
// `fcmp une` ("unordered or not-equal"), not `one`,
// mirroring float-semantics.md: NaN != NaN must be true.
let n = emitter.locals.len();
let a_ssa = emitter.locals[n - 2].1.clone();
let b_ssa = emitter.locals[n - 1].1.clone();
let r = emitter.fresh_ssa();
emitter.body.push_str(&format!(
" {r} = fcmp une double {a_ssa}, {b_ssa}\n"
));
emitter.body.push_str(&format!(" ret i1 {r}\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 r = emitter.fresh_ssa();
emitter.body.push_str(&format!(
" {r} = fcmp olt double {a_ssa}, {b_ssa}\n"
));
emitter.body.push_str(&format!(" ret i1 {r}\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 r = emitter.fresh_ssa();
emitter.body.push_str(&format!(
" {r} = fcmp ole double {a_ssa}, {b_ssa}\n"
));
emitter.body.push_str(&format!(" ret i1 {r}\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 r = emitter.fresh_ssa();
emitter.body.push_str(&format!(
" {r} = fcmp ogt double {a_ssa}, {b_ssa}\n"
));
emitter.body.push_str(&format!(" ret i1 {r}\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 r = emitter.fresh_ssa();
emitter.body.push_str(&format!(
" {r} = fcmp oge double {a_ssa}, {b_ssa}\n"
));
emitter.body.push_str(&format!(" ret i1 {r}\n"));
emitter.body.push_str("}\n\n");
emitter.block_terminated = true;
Ok(())
}
// The five icmp-delegate emit fns wrap the existing
// `Emitter::emit_direct_int_icmp_intercept` helper, which returns
// `Result<bool>` (always `Ok(true)` on success); we discard the
// bool because the dispatch shim wraps it from `check_sig`.
pub(crate) fn emit_lt_int(emitter: &mut Emitter<'_>) -> Result<()> {
emitter
.emit_direct_int_icmp_intercept("lt__Int", "icmp slt i64", &["i64".into(), "i64".into()], "i1")
.map(|_| ())
}
pub(crate) fn emit_le_int(emitter: &mut Emitter<'_>) -> Result<()> {
emitter
.emit_direct_int_icmp_intercept("le__Int", "icmp sle i64", &["i64".into(), "i64".into()], "i1")
.map(|_| ())
}
pub(crate) fn emit_gt_int(emitter: &mut Emitter<'_>) -> Result<()> {
emitter
.emit_direct_int_icmp_intercept("gt__Int", "icmp sgt i64", &["i64".into(), "i64".into()], "i1")
.map(|_| ())
}
pub(crate) fn emit_ge_int(emitter: &mut Emitter<'_>) -> Result<()> {
emitter
.emit_direct_int_icmp_intercept("ge__Int", "icmp sge i64", &["i64".into(), "i64".into()], "i1")
.map(|_| ())
}
pub(crate) fn emit_ne_int(emitter: &mut Emitter<'_>) -> Result<()> {
emitter
.emit_direct_int_icmp_intercept("ne__Int", "icmp ne i64", &["i64".into(), "i64".into()], "i1")
.map(|_| ())
}
#[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:?}"
);
}
}
+26 -318
View File
@@ -42,6 +42,7 @@ use ailang_check::uniqueness::{infer_module, UniquenessTable};
mod drop; mod drop;
mod escape; mod escape;
mod intercepts;
mod lambda; mod lambda;
mod match_lower; mod match_lower;
mod subst; mod subst;
@@ -708,7 +709,7 @@ struct Emitter<'a> {
/// Name of the currently lowered module (for mangling). /// Name of the currently lowered module (for mangling).
module_name: &'a str, module_name: &'a str,
header: String, header: String,
body: String, pub(crate) body: String,
/// String constants: content -> (global name (without `@`), llvm type length incl. \0) /// String constants: content -> (global name (without `@`), llvm type length incl. \0)
strings: BTreeMap<String, (String, usize)>, strings: BTreeMap<String, (String, usize)>,
/// language `Str` literals interned as /// language `Str` literals interned as
@@ -722,7 +723,7 @@ struct Emitter<'a> {
/// The AILang type is recorded so that the codegen-side type tracker /// The AILang type is recorded so that the codegen-side type tracker
/// can derive substitutions at polymorphic call sites without /// can derive substitutions at polymorphic call sites without
/// re-running the typechecker (Iter 12b). /// re-running the typechecker (Iter 12b).
locals: Vec<(String, String, String, Type)>, pub(crate) locals: Vec<(String, String, String, Type)>,
/// Monotonic counter for SSA values and labels. /// Monotonic counter for SSA values and labels.
counter: u64, counter: u64,
/// Monotonic counter for global string names (per module). /// Monotonic counter for global string names (per module).
@@ -762,7 +763,7 @@ struct Emitter<'a> {
/// Callers in the term lowering walk consult this to skip /// Callers in the term lowering walk consult this to skip
/// fall-through `br` emission and to omit the value from a /// fall-through `br` emission and to omit the value from a
/// surrounding match-arm phi. Reset by [`Self::start_block`]. /// surrounding match-arm phi. Reset by [`Self::start_block`].
block_terminated: bool, pub(crate) block_terminated: bool,
/// SSA value (or `@global`) -> its FnSig, for first-class /// SSA value (or `@global`) -> its FnSig, for first-class
/// function values. Populated whenever we lower a `Term::Var` to a /// function values. Populated whenever we lower a `Term::Var` to a
/// top-level fn pointer or when a fn-typed parameter is bound at /// top-level fn pointer or when a fn-typed parameter is bound at
@@ -1170,18 +1171,17 @@ impl<'a> Emitter<'a> {
/// unmangled fn name; these symbols only live in the prelude /// unmangled fn name; these symbols only live in the prelude
/// module so cross-module collisions are not a concern. /// module so cross-module collisions are not a concern.
fn intercept_emit_wants_alwaysinline(symbol: &str) -> bool { fn intercept_emit_wants_alwaysinline(symbol: &str) -> bool {
if matches!( // Branch 1 — registry-driven (raw-buf.1). Every intercept entry
symbol, // carries its own `wants_alwaysinline` bool; the predicate
"eq__Int" | "eq__Bool" | "eq__Str" | "eq__Unit" // consults the single table instead of a duplicate name-list.
| "compare__Int" | "compare__Bool" | "compare__Str" // This closes the silent-drift class where the predicate's
| "float_eq" | "float_ne" // hardcoded list could diverge from the dispatch-arm set.
| "float_lt" | "float_le" if intercepts::lookup(symbol).is_some_and(|i| i.wants_alwaysinline) {
| "float_gt" | "float_ge"
) {
return true; return true;
} }
// The polymorphic free helpers `ne` / `lt` / `le` / `gt` / // Branch 2 — polymorphic-body carve-out, NOT covered by the
// `ge` mono into per-type bodies. At `_Int` (iter // registry. The free helpers `ne` / `lt` / `le` / `gt` / `ge`
// mono into per-type bodies. At `_Int` (iter
// operator-routing-eq-ord.1 Task 13) they are intercepted as // operator-routing-eq-ord.1 Task 13) they are intercepted as
// single direct `icmp` instructions in // single direct `icmp` instructions in
// `try_emit_primitive_instance_body` — same shape as // `try_emit_primitive_instance_body` — same shape as
@@ -1189,11 +1189,11 @@ impl<'a> Emitter<'a> {
// (`_Bool`, `_Str`, user ADTs) they keep their polymorphic // (`_Bool`, `_Str`, user ADTs) they keep their polymorphic
// body — a one-arm-deep `match (compare x y) (case LT ... ) // body — a one-arm-deep `match (compare x y) (case LT ... )
// (case _ ... )` — which still folds at `-O2` provided the // (case _ ... )` — which still folds at `-O2` provided the
// mono symbol carries `alwaysinline`. Both shapes benefit // mono symbol carries `alwaysinline`. The registry only covers
// from the attribute; the stem-prefix match covers all T the // the `_Int` specialisations; this stem-prefix match covers
// mono pass produces uniformly so the bench-perf parity with // every other T the mono pass produces (`lt__Bool`,
// the pre-milestone arithmetic-comparator emission is // `ne__MyADT`, …), preserving the bench-perf parity that the
// preserved end-to-end. // pre-milestone arithmetic-comparator emission established.
if let Some(stem) = symbol.split("__").next() { if let Some(stem) = symbol.split("__").next() {
if matches!(stem, "ne" | "lt" | "le" | "gt" | "ge") { if matches!(stem, "ne" | "lt" | "le" | "gt" | "ge") {
return true; return true;
@@ -2844,305 +2844,13 @@ impl<'a> Emitter<'a> {
param_tys: &[String], param_tys: &[String],
ret_ty: &str, ret_ty: &str,
) -> Result<bool> { ) -> Result<bool> {
match fn_name { match intercepts::lookup(fn_name) {
"eq__Str" => { Some(intercept) => {
if param_tys != ["ptr", "ptr"] || ret_ty != "i1" { intercepts::check_sig(intercept, param_tys, ret_ty)?;
return Err(CodegenError::Internal(format!( (intercept.emit)(self)?;
"eq__Str body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (ptr, ptr) -> i1)"
)));
}
// The two params are the two most recently pushed locals;
// `emit_fn` populated `self.locals` from `f.params` before
// dispatching here. Pull their SSA names without assuming
// a specific surface-level binder name.
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
// IR-Str pointers now land on the
// `len`-field of the packed-struct slab; @ail_str_eq's
// strcmp-based body needs the bytes pointer 8 bytes
// further on.
let a_bytes = self.fresh_ssa();
let b_bytes = self.fresh_ssa();
self.body.push_str(&format!(
" {a_bytes} = getelementptr inbounds i8, ptr {a_ssa}, i64 8\n"
));
self.body.push_str(&format!(
" {b_bytes} = getelementptr inbounds i8, ptr {b_ssa}, i64 8\n"
));
let dst = self.fresh_ssa();
self.body.push_str(&format!(
" {dst} = call zeroext i1 @ail_str_eq(ptr {a_bytes}, ptr {b_bytes})\n"
));
self.body.push_str(&format!(" ret i1 {dst}\n"));
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true) Ok(true)
} }
"compare__Int" => { None => Ok(false),
if param_tys != ["i64", "i64"] || ret_ty != "ptr" {
return Err(CodegenError::Internal(format!(
"compare__Int body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (i64, i64) -> ptr)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
self.emit_compare_ladder(
&format!("icmp slt i64 {a_ssa}, {b_ssa}"),
&format!("icmp eq i64 {a_ssa}, {b_ssa}"),
)?;
Ok(true)
}
"compare__Bool" => {
if param_tys != ["i1", "i1"] || ret_ty != "ptr" {
return Err(CodegenError::Internal(format!(
"compare__Bool body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (i1, i1) -> ptr)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
self.emit_compare_ladder(
&format!("icmp ult i1 {a_ssa}, {b_ssa}"),
&format!("icmp eq i1 {a_ssa}, {b_ssa}"),
)?;
Ok(true)
}
"compare__Str" => {
if param_tys != ["ptr", "ptr"] || ret_ty != "ptr" {
return Err(CodegenError::Internal(format!(
"compare__Str body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (ptr, ptr) -> ptr)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
// IR-Str pointers now land on the
// `len`-field of the packed-struct slab; @ail_str_compare's
// strcmp-based body needs the bytes pointer 8 bytes
// further on.
let a_bytes = self.fresh_ssa();
let b_bytes = self.fresh_ssa();
self.body.push_str(&format!(
" {a_bytes} = getelementptr inbounds i8, ptr {a_ssa}, i64 8\n"
));
self.body.push_str(&format!(
" {b_bytes} = getelementptr inbounds i8, ptr {b_ssa}, i64 8\n"
));
let cmp_res = self.fresh_ssa();
self.body.push_str(&format!(
" {cmp_res} = call i32 @ail_str_compare(ptr {a_bytes}, ptr {b_bytes})\n"
));
self.emit_compare_ladder(
&format!("icmp slt i32 {cmp_res}, 0"),
&format!("icmp eq i32 {cmp_res}, 0"),
)?;
Ok(true)
}
"eq__Int" => {
if param_tys != ["i64", "i64"] || ret_ty != "i1" {
return Err(CodegenError::Internal(format!(
"eq__Int body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (i64, i64) -> i1)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
let r = self.fresh_ssa();
self.body.push_str(&format!(
" {r} = icmp eq i64 {a_ssa}, {b_ssa}\n"
));
self.body.push_str(&format!(" ret i1 {r}\n"));
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true)
}
"eq__Bool" => {
if param_tys != ["i1", "i1"] || ret_ty != "i1" {
return Err(CodegenError::Internal(format!(
"eq__Bool body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (i1, i1) -> i1)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
let r = self.fresh_ssa();
self.body.push_str(&format!(
" {r} = icmp eq i1 {a_ssa}, {b_ssa}\n"
));
self.body.push_str(&format!(" ret i1 {r}\n"));
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true)
}
"eq__Unit" => {
// Unit is single-inhabitant: all values compare equal.
// The fn signature still carries the operand slots; we
// discard them and return true unconditionally.
if ret_ty != "i1" {
return Err(CodegenError::Internal(format!(
"eq__Unit body intercept: unexpected return type \
({param_tys:?}) -> {ret_ty} (want -> i1)"
)));
}
self.body.push_str(" ret i1 1\n");
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true)
}
"float_eq" => {
if param_tys != ["double", "double"] || ret_ty != "i1" {
return Err(CodegenError::Internal(format!(
"float_eq body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
let r = self.fresh_ssa();
self.body.push_str(&format!(
" {r} = fcmp oeq double {a_ssa}, {b_ssa}\n"
));
self.body.push_str(&format!(" ret i1 {r}\n"));
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true)
}
"float_ne" => {
// `fcmp une` ("unordered or not-equal"), not `one`,
// mirroring float-semantics.md: NaN != NaN must be true.
if param_tys != ["double", "double"] || ret_ty != "i1" {
return Err(CodegenError::Internal(format!(
"float_ne body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
let r = self.fresh_ssa();
self.body.push_str(&format!(
" {r} = fcmp une double {a_ssa}, {b_ssa}\n"
));
self.body.push_str(&format!(" ret i1 {r}\n"));
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true)
}
"float_lt" => {
if param_tys != ["double", "double"] || ret_ty != "i1" {
return Err(CodegenError::Internal(format!(
"float_lt body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
let r = self.fresh_ssa();
self.body.push_str(&format!(
" {r} = fcmp olt double {a_ssa}, {b_ssa}\n"
));
self.body.push_str(&format!(" ret i1 {r}\n"));
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true)
}
"float_le" => {
if param_tys != ["double", "double"] || ret_ty != "i1" {
return Err(CodegenError::Internal(format!(
"float_le body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
let r = self.fresh_ssa();
self.body.push_str(&format!(
" {r} = fcmp ole double {a_ssa}, {b_ssa}\n"
));
self.body.push_str(&format!(" ret i1 {r}\n"));
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true)
}
"float_gt" => {
if param_tys != ["double", "double"] || ret_ty != "i1" {
return Err(CodegenError::Internal(format!(
"float_gt body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
let r = self.fresh_ssa();
self.body.push_str(&format!(
" {r} = fcmp ogt double {a_ssa}, {b_ssa}\n"
));
self.body.push_str(&format!(" ret i1 {r}\n"));
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true)
}
"float_ge" => {
if param_tys != ["double", "double"] || ret_ty != "i1" {
return Err(CodegenError::Internal(format!(
"float_ge body intercept: unexpected signature \
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
)));
}
let n = self.locals.len();
let a_ssa = self.locals[n - 2].1.clone();
let b_ssa = self.locals[n - 1].1.clone();
let r = self.fresh_ssa();
self.body.push_str(&format!(
" {r} = fcmp oge double {a_ssa}, {b_ssa}\n"
));
self.body.push_str(&format!(" ret i1 {r}\n"));
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true)
}
// Ord/Eq Int direct-icmp arms (iter operator-routing-eq-ord.1
// Task 13). The polymorphic prelude bodies are
// `(match (app compare x y) ...)`; without these intercept
// arms, each `lt`-at-Int call site goes through
// `compare__Int` → allocate an `Ordering` ctor → match.
// At `-O2` even with `alwaysinline` on the helpers, clang
// does not always collapse the Ordering allocation across
// the helper boundary (observed: +29% bump_s, +47% rc_s
// bench_closure_chain regression before this arm shipped).
// Direct `icmp slt i64` at the helper body lets clang fold
// the comparison to the use site reliably.
//
// `ne__Int` uses `icmp ne i64` directly, NOT the
// `not (eq x y)` indirection — that would re-introduce one
// call boundary and one `xor i1 %r, true` per comparison
// that contributes no semantic value beyond the direct
// `icmp ne`.
//
// Bool variants (`lt__Bool`, `le__Bool`, etc.) are not
// emitted as intercept arms here because no current
// bench fixture or example reaches them; the polymorphic
// body via `compare__Bool` remains correct, just slightly
// slower per call. The next fixture that needs Bool-ordered
// perf can extend this family symmetrically.
"lt__Int" => self.emit_direct_int_icmp_intercept("lt__Int", "icmp slt i64", param_tys, ret_ty),
"le__Int" => self.emit_direct_int_icmp_intercept("le__Int", "icmp sle i64", param_tys, ret_ty),
"gt__Int" => self.emit_direct_int_icmp_intercept("gt__Int", "icmp sgt i64", param_tys, ret_ty),
"ge__Int" => self.emit_direct_int_icmp_intercept("ge__Int", "icmp sge i64", param_tys, ret_ty),
"ne__Int" => self.emit_direct_int_icmp_intercept("ne__Int", "icmp ne i64", param_tys, ret_ty),
_ => Ok(false),
} }
} }
@@ -3153,7 +2861,7 @@ impl<'a> Emitter<'a> {
/// `%r = <icmp_op> %a, %b` and `ret i1 %r`, close the body. /// `%r = <icmp_op> %a, %b` and `ret i1 %r`, close the body.
/// `icmp_op` is the full instruction-and-operand-type prefix /// `icmp_op` is the full instruction-and-operand-type prefix
/// (e.g. `"icmp slt i64"`); the operand SSAs are appended. /// (e.g. `"icmp slt i64"`); the operand SSAs are appended.
fn emit_direct_int_icmp_intercept( pub(crate) fn emit_direct_int_icmp_intercept(
&mut self, &mut self,
fn_name: &str, fn_name: &str,
icmp_op: &str, icmp_op: &str,
@@ -3187,7 +2895,7 @@ impl<'a> Emitter<'a> {
/// zero-field allocation; `lower_ctor` handles alloc-strategy /// zero-field allocation; `lower_ctor` handles alloc-strategy
/// variance (Gc / Bump / Rc) without the intercept duplicating /// variance (Gc / Bump / Rc) without the intercept duplicating
/// the per-strategy logic. /// the per-strategy logic.
fn emit_ordering_arm(&mut self, label: &str, ctor: &str) -> Result<()> { pub(crate) fn emit_ordering_arm(&mut self, label: &str, ctor: &str) -> Result<()> {
self.start_block(label); self.start_block(label);
// term_ptr 0: synthetic call site, not present in the // term_ptr 0: synthetic call site, not present in the
// escape-analysis result; falls back to the conservative // escape-analysis result; falls back to the conservative
@@ -3217,7 +2925,7 @@ impl<'a> Emitter<'a> {
/// `icmp ult i1` / `icmp slt i32` etc.); any per-arm prep /// `icmp ult i1` / `icmp slt i32` etc.); any per-arm prep
/// (e.g. `compare__Str`'s `@ail_str_compare` call) must be /// (e.g. `compare__Str`'s `@ail_str_compare` call) must be
/// emitted by the caller before invoking this helper. /// emitted by the caller before invoking this helper.
fn emit_compare_ladder( pub(crate) fn emit_compare_ladder(
&mut self, &mut self,
lt_test_instr: &str, lt_test_instr: &str,
eq_test_instr: &str, eq_test_instr: &str,