diff --git a/crates/ailang-codegen/src/intercepts.rs b/crates/ailang-codegen/src/intercepts.rs new file mode 100644 index 0000000..b26cba5 --- /dev/null +++ b/crates/ailang-codegen/src/intercepts.rs @@ -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` (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:?}" + ); + } +} diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index fce5223..7aeab03 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -42,6 +42,7 @@ use ailang_check::uniqueness::{infer_module, UniquenessTable}; mod drop; mod escape; +mod intercepts; mod lambda; mod match_lower; mod subst; @@ -708,7 +709,7 @@ struct Emitter<'a> { /// Name of the currently lowered module (for mangling). module_name: &'a str, header: String, - body: String, + pub(crate) body: String, /// String constants: content -> (global name (without `@`), llvm type length incl. \0) strings: BTreeMap, /// language `Str` literals interned as @@ -722,7 +723,7 @@ struct Emitter<'a> { /// The AILang type is recorded so that the codegen-side type tracker /// can derive substitutions at polymorphic call sites without /// 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. counter: u64, /// 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 /// fall-through `br` emission and to omit the value from a /// 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 /// function values. Populated whenever we lower a `Term::Var` to a /// 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 /// module so cross-module collisions are not a concern. fn intercept_emit_wants_alwaysinline(symbol: &str) -> bool { - if matches!( - symbol, - "eq__Int" | "eq__Bool" | "eq__Str" | "eq__Unit" - | "compare__Int" | "compare__Bool" | "compare__Str" - | "float_eq" | "float_ne" - | "float_lt" | "float_le" - | "float_gt" | "float_ge" - ) { + // Branch 1 — registry-driven (raw-buf.1). Every intercept entry + // carries its own `wants_alwaysinline` bool; the predicate + // consults the single table instead of a duplicate name-list. + // This closes the silent-drift class where the predicate's + // hardcoded list could diverge from the dispatch-arm set. + if intercepts::lookup(symbol).is_some_and(|i| i.wants_alwaysinline) { return true; } - // The polymorphic free helpers `ne` / `lt` / `le` / `gt` / - // `ge` mono into per-type bodies. At `_Int` (iter + // Branch 2 — polymorphic-body carve-out, NOT covered by the + // 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 // single direct `icmp` instructions in // `try_emit_primitive_instance_body` — same shape as @@ -1189,11 +1189,11 @@ impl<'a> Emitter<'a> { // (`_Bool`, `_Str`, user ADTs) they keep their polymorphic // body — a one-arm-deep `match (compare x y) (case LT ... ) // (case _ ... )` — which still folds at `-O2` provided the - // mono symbol carries `alwaysinline`. Both shapes benefit - // from the attribute; the stem-prefix match covers all T the - // mono pass produces uniformly so the bench-perf parity with - // the pre-milestone arithmetic-comparator emission is - // preserved end-to-end. + // mono symbol carries `alwaysinline`. The registry only covers + // the `_Int` specialisations; this stem-prefix match covers + // every other T the mono pass produces (`lt__Bool`, + // `ne__MyADT`, …), preserving the bench-perf parity that the + // pre-milestone arithmetic-comparator emission established. if let Some(stem) = symbol.split("__").next() { if matches!(stem, "ne" | "lt" | "le" | "gt" | "ge") { return true; @@ -2844,305 +2844,13 @@ impl<'a> Emitter<'a> { param_tys: &[String], ret_ty: &str, ) -> Result { - match fn_name { - "eq__Str" => { - if param_tys != ["ptr", "ptr"] || ret_ty != "i1" { - return Err(CodegenError::Internal(format!( - "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; + match intercepts::lookup(fn_name) { + Some(intercept) => { + intercepts::check_sig(intercept, param_tys, ret_ty)?; + (intercept.emit)(self)?; Ok(true) } - "compare__Int" => { - 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), + None => Ok(false), } } @@ -3153,7 +2861,7 @@ impl<'a> Emitter<'a> { /// `%r = %a, %b` and `ret i1 %r`, close the body. /// `icmp_op` is the full instruction-and-operand-type prefix /// (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, fn_name: &str, icmp_op: &str, @@ -3187,7 +2895,7 @@ impl<'a> Emitter<'a> { /// zero-field allocation; `lower_ctor` handles alloc-strategy /// variance (Gc / Bump / Rc) without the intercept duplicating /// 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); // term_ptr 0: synthetic call site, not present in the // 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 /// (e.g. `compare__Str`'s `@ail_str_compare` call) must be /// emitted by the caller before invoking this helper. - fn emit_compare_ladder( + pub(crate) fn emit_compare_ladder( &mut self, lt_test_instr: &str, eq_test_instr: &str,