//! 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, }, // raw-buf.4: the 12 scope-qualified RawBuf ops. Symbols are // `RawBuf_{new,get,set,size}__{Int,Float,Bool}` (raw-buf.3 // scope-qualified mono mangling). Slab layout: // `[ size:i64 @0 ][ elem_0 @8 ][ elem_1 @8+w ]…`; element widths // Int/Float = 8, Bool = 1. `own`/`borrow (con RawBuf T)` both // lower to `ptr`. The header is always i64, so `new`/`size` are // element-type-independent; `get`/`set` carry the element type. Intercept { name: "RawBuf_new__Int", expected_params: &["i64"], expected_ret: "ptr", wants_alwaysinline: false, emit: emit_rawbuf_new_int, }, Intercept { name: "RawBuf_get__Int", expected_params: &["ptr", "i64"], expected_ret: "i64", wants_alwaysinline: false, emit: emit_rawbuf_get_int, }, Intercept { name: "RawBuf_set__Int", expected_params: &["ptr", "i64", "i64"], expected_ret: "ptr", wants_alwaysinline: false, emit: emit_rawbuf_set_int, }, Intercept { name: "RawBuf_size__Int", expected_params: &["ptr"], expected_ret: "i64", wants_alwaysinline: false, emit: emit_rawbuf_size_int, }, Intercept { name: "RawBuf_new__Float", expected_params: &["i64"], expected_ret: "ptr", wants_alwaysinline: false, emit: emit_rawbuf_new_float, }, Intercept { name: "RawBuf_get__Float", expected_params: &["ptr", "i64"], expected_ret: "double", wants_alwaysinline: false, emit: emit_rawbuf_get_float, }, Intercept { name: "RawBuf_set__Float", expected_params: &["ptr", "i64", "double"], expected_ret: "ptr", wants_alwaysinline: false, emit: emit_rawbuf_set_float, }, Intercept { name: "RawBuf_size__Float", expected_params: &["ptr"], expected_ret: "i64", wants_alwaysinline: false, emit: emit_rawbuf_size_float, }, Intercept { name: "RawBuf_new__Bool", expected_params: &["i64"], expected_ret: "ptr", wants_alwaysinline: false, emit: emit_rawbuf_new_bool, }, Intercept { name: "RawBuf_get__Bool", expected_params: &["ptr", "i64"], expected_ret: "i1", wants_alwaysinline: false, emit: emit_rawbuf_get_bool, }, Intercept { name: "RawBuf_set__Bool", expected_params: &["ptr", "i64", "i1"], expected_ret: "ptr", wants_alwaysinline: false, emit: emit_rawbuf_set_bool, }, Intercept { name: "RawBuf_size__Bool", expected_params: &["ptr"], expected_ret: "i64", wants_alwaysinline: false, emit: emit_rawbuf_size_bool, }, ]; 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. // --------------------------------------------------------------- /// Emit the `getelementptr inbounds i8, ptr , i64 8` that walks /// an IR-Str pointer (which lands on the `len` field of the packed /// slab) forward to its bytes pointer, returning the fresh SSA holding /// the result. Shared by `emit_eq_str` / `emit_compare_str`. Because /// `fresh_ssa` allocates sequentially, calling this twice in a row /// yields the same `%vK`, `%vK+1` pair — and the same two GEP lines in /// the same order — as the inlined form it replaces, so the emitted IR /// is byte-identical. fn emit_str_bytes_gep(emitter: &mut Emitter<'_>, src: &str) -> String { let bytes = emitter.fresh_ssa(); emitter.body.push_str(&format!( " {bytes} = getelementptr inbounds i8, ptr {src}, i64 8\n" )); bytes } 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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 = emit_str_bytes_gep(emitter, &a_ssa); let b_bytes = emit_str_bytes_gep(emitter, &b_ssa); 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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 = emit_str_bytes_gep(emitter, &a_ssa); let b_bytes = emit_str_bytes_gep(emitter, &b_ssa); 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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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 params = emitter.last_param_ssas(2); let a_ssa = params[0].clone(); let b_ssa = params[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(|_| ()) } // --------------------------------------------------------------- // raw-buf.4: RawBuf op emits over an `@ailang_rc_alloc` slab. // Slab layout `[ size:i64 @0 ][ elem_0 @8 ][ elem_1 @8+w ]…`. The // rc-header is auto-prepended by `@ailang_rc_alloc` (it returns the // payload ptr); the i64 size header lives at payload offset 0, the // elements follow at offset 8. Element widths: Int/Float = 8, Bool // = 1. `new`/`size` are byte-identical across element types (the // header is always i64); `get`/`set` carry the element load/store // type and offset width. Intercepts run only under `--alloc=rc`, so // `@ailang_rc_alloc` is hardcoded (not `alloc.fn_name()`). // --------------------------------------------------------------- // Parameterised cores. The three element variants (Int, Float, Bool) // differ in exactly two axes: the per-element byte width used in the // index `mul` / capacity `mul` (Int=8, Float=8, Bool=1) and the LLVM // load/store type used by `get`/`set` (Int=i64, Float=double, Bool=i1). // `new`/`size` carry only the width axis (their slab header is always // i64). Each core emits the IR for one operation; the public per-type // wrappers below pass the right `(width, elem_ty)` pair, so the emitted // IR for every variant is byte-identical to the pre-dedup form. fn emit_rawbuf_new(emitter: &mut Emitter<'_>, elem_width: u64) -> Result<()> { let cap = emitter.last_param_ssas(1)[0].clone(); // i64 capacity let elems_bytes = emitter.fresh_ssa(); let total = emitter.fresh_ssa(); let slab = emitter.fresh_ssa(); emitter .body .push_str(&format!(" {elems_bytes} = mul i64 {cap}, {elem_width}\n")); emitter.body.push_str(&format!(" {total} = add i64 {elems_bytes}, 8\n")); emitter .body .push_str(&format!(" {slab} = call ptr @ailang_rc_alloc(i64 {total})\n")); emitter.body.push_str(&format!(" store i64 {cap}, ptr {slab}\n")); emitter.body.push_str(&format!(" ret ptr {slab}\n")); emitter.body.push_str("}\n\n"); emitter.block_terminated = true; Ok(()) } fn emit_rawbuf_get( emitter: &mut Emitter<'_>, elem_width: u64, elem_ty: &str, ) -> Result<()> { let params = emitter.last_param_ssas(2); let b = params[0].clone(); let i = params[1].clone(); let off = emitter.fresh_ssa(); let byteoff = emitter.fresh_ssa(); let ptr = emitter.fresh_ssa(); let v = emitter.fresh_ssa(); emitter.body.push_str(&format!(" {off} = mul i64 {i}, {elem_width}\n")); emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n")); emitter.body.push_str(&format!( " {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n" )); emitter.body.push_str(&format!(" {v} = load {elem_ty}, ptr {ptr}\n")); emitter.body.push_str(&format!(" ret {elem_ty} {v}\n")); emitter.body.push_str("}\n\n"); emitter.block_terminated = true; Ok(()) } fn emit_rawbuf_set( emitter: &mut Emitter<'_>, elem_width: u64, elem_ty: &str, ) -> Result<()> { let params = emitter.last_param_ssas(3); let b = params[0].clone(); let i = params[1].clone(); let v = params[2].clone(); let off = emitter.fresh_ssa(); let byteoff = emitter.fresh_ssa(); let ptr = emitter.fresh_ssa(); emitter.body.push_str(&format!(" {off} = mul i64 {i}, {elem_width}\n")); emitter.body.push_str(&format!(" {byteoff} = add i64 {off}, 8\n")); emitter.body.push_str(&format!( " {ptr} = getelementptr inbounds i8, ptr {b}, i64 {byteoff}\n" )); emitter.body.push_str(&format!(" store {elem_ty} {v}, ptr {ptr}\n")); emitter.body.push_str(&format!(" ret ptr {b}\n")); emitter.body.push_str("}\n\n"); emitter.block_terminated = true; Ok(()) } fn emit_rawbuf_size(emitter: &mut Emitter<'_>) -> Result<()> { let b = emitter.last_param_ssas(1)[0].clone(); let sz = emitter.fresh_ssa(); emitter.body.push_str(&format!(" {sz} = load i64, ptr {b}\n")); emitter.body.push_str(&format!(" ret i64 {sz}\n")); emitter.body.push_str("}\n\n"); emitter.block_terminated = true; Ok(()) } // --- Int variants (element type i64, width 8) --- pub(crate) fn emit_rawbuf_new_int(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_new(emitter, 8) } pub(crate) fn emit_rawbuf_get_int(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_get(emitter, 8, "i64") } pub(crate) fn emit_rawbuf_set_int(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_set(emitter, 8, "i64") } pub(crate) fn emit_rawbuf_size_int(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_size(emitter) } // --- Float variants (element type double, width 8) --- pub(crate) fn emit_rawbuf_new_float(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_new(emitter, 8) } pub(crate) fn emit_rawbuf_get_float(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_get(emitter, 8, "double") } pub(crate) fn emit_rawbuf_set_float(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_set(emitter, 8, "double") } pub(crate) fn emit_rawbuf_size_float(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_size(emitter) } // --- Bool variants (element type i1, width 1) --- pub(crate) fn emit_rawbuf_new_bool(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_new(emitter, 1) } pub(crate) fn emit_rawbuf_get_bool(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_get(emitter, 1, "i1") } pub(crate) fn emit_rawbuf_set_bool(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_set(emitter, 1, "i1") } pub(crate) fn emit_rawbuf_size_bool(emitter: &mut Emitter<'_>) -> Result<()> { emit_rawbuf_size(emitter) } #[cfg(test)] mod tests { use super::{lookup, INTERCEPTS}; use ailang_check::mono::{mono_symbol, mono_symbol_n}; use ailang_core::ast::{Def, FnDef, Module, Term, Type}; use std::collections::BTreeSet; /// INTERCEPTS entries that intercept the monomorphised `__Int` /// specialisation of a polymorphic free fn carrying a REAL body /// (`ne = not (eq x y)`; `lt/le/gt/ge = match compare ...`). These /// are an optimisation class, not a compiler-supplied body — they /// legitimately have no `(intrinsic)` marker. Any change here is a /// deliberate registry-policy decision, not drift. const OPTIMISATION_ONLY: &[&str] = &["lt__Int", "le__Int", "gt__Int", "ge__Int", "ne__Int"]; /// Collect the mangled name of every `(intrinsic)` marker reachable /// in the kernel-tier source modules (prelude + raw_buf — the /// only modules where an intrinsic body is legal today). fn workspace_intrinsic_markers() -> BTreeSet { let mut markers = BTreeSet::new(); for module in [ ailang_surface::parse_prelude(), ailang_surface::parse_raw_buf(), ] { for def in &module.defs { match def { // raw-buf.3: a polymorphic top-level (intrinsic) op // type-scoped to a same-module TypeDef T (its // signature mentions `(con T …)`) expands to one // marker per element type in T's `param-in` set — // the same `T_f__` strings the mono pass mints // (mono::scoped_base + mono_symbol_n). One marker → N // entries. Def::Fn(f) if matches!(f.body, Term::Intrinsic) && matches!(f.ty, Type::Forall { .. }) => { match scope_typedef_and_elems(f, &module) { Some((tdef, elems)) => { for elem in elems { markers.insert(mono_symbol_n( &format!("{tdef}_{}", f.name), std::slice::from_ref(&elem), )); } } // a Forall intrinsic not scoped to a // param-in TypeDef keeps the bare name (no // such case ships today; future-proofing). None => { markers.insert(f.name.clone()); } } } // Top-level intrinsic fn: name is already the symbol // (float_eq, ...). Def::Fn(f) if matches!(f.body, Term::Intrinsic) => { markers.insert(f.name.clone()); } // Instance method whose lambda body is intrinsic: // the codegen symbol is mono_symbol(method, type). Def::Instance(inst) => { for m in &inst.methods { if let Term::Lam { body, .. } = &m.body { if matches!(**body, Term::Intrinsic) { markers.insert(mono_symbol(&m.name, &inst.type_)); } } } } _ => {} } } } markers } /// Bijection over the intrinsic-backed class: /// (A) every workspace intrinsic marker resolves to an INTERCEPTS entry; /// (B) every INTERCEPTS entry not on the optimisation-only allowlist /// has a workspace intrinsic marker. #[test] fn intercepts_bijection_with_intrinsic_markers() { let markers = workspace_intrinsic_markers(); let registry: BTreeSet = INTERCEPTS.iter().map(|i| i.name.to_string()).collect(); // (A) no intrinsic marker without a codegen intercept let orphan_markers: Vec<&String> = markers.iter().filter(|m| lookup(m).is_none()).collect(); assert!( orphan_markers.is_empty(), "intrinsic markers with no INTERCEPTS entry: {orphan_markers:?}" ); // (B) no non-allowlisted intercept without an intrinsic marker let orphan_entries: Vec<&String> = registry .iter() .filter(|n| !OPTIMISATION_ONLY.contains(&n.as_str())) .filter(|n| !markers.contains(*n)) .collect(); assert!( orphan_entries.is_empty(), "INTERCEPTS entries with no intrinsic marker (and not optimisation-only): {orphan_entries:?}" ); // Guard: the allowlist names must actually be in the registry — // a stale allowlist entry (name removed from INTERCEPTS) is drift. let stale_allow: Vec<&&str> = OPTIMISATION_ONLY .iter() .filter(|n| lookup(n).is_none()) .collect(); assert!( stale_allow.is_empty(), "optimisation-only allowlist names not in INTERCEPTS: {stale_allow:?}" ); } /// raw-buf.3: for a type-scoped polymorphic intrinsic fn `f`, return /// `(TypeDef-name, element-types)` where the TypeDef is the unique /// same-module `Def::Type` referenced via `(con T …)` in `f`'s /// signature and the element types are its `param-in` set expressed as /// `Type::Con` values (so `mono_symbol_n` produces the same suffix the /// mono pass mints). `None` if `f` references zero or several /// same-module TypeDefs. fn scope_typedef_and_elems(f: &FnDef, module: &Module) -> Option<(String, Vec)> { let typedef_names: BTreeSet<&str> = module .defs .iter() .filter_map(|d| match d { Def::Type(td) => Some(td.name.as_str()), _ => None, }) .collect(); // collect Con-heads referenced anywhere in f's type let mut referenced: BTreeSet = BTreeSet::new(); collect_con_heads(&f.ty, &mut referenced); let scoped: Vec<&String> = referenced .iter() .filter(|n| typedef_names.contains(n.as_str())) .collect(); let tdef = match scoped.as_slice() { [one] => (*one).clone(), _ => return None, }; let td = module.defs.iter().find_map(|d| match d { Def::Type(td) if td.name == tdef => Some(td), _ => None, })?; // param-in is keyed by the type var; take its allowed surface // names and build `Type::Con` element types. The element strings // (`Int`, `Float`) flow through `mono_symbol_n`'s // `primitive_surface_name` gate to the same `__Int` / `__Float` // suffixes the mono pass mints. let allowed = td.param_in.values().next()?; // single-var TypeDefs let elems: Vec = allowed .iter() .map(|s| Type::Con { name: s.clone(), args: vec![] }) .collect(); Some((tdef, elems)) } /// raw-buf.3: push every `Type::Con` head name reachable in `ty` /// into `out`. `borrow` / `own` are `ParamMode` metadata on /// `Type::Fn`, not wrapper `Type` variants, so the only recursive /// shapes are `Forall.body`, `Fn.params`/`ret`, and `Con.args`. fn collect_con_heads(ty: &Type, out: &mut BTreeSet) { match ty { Type::Con { name, args } => { out.insert(name.clone()); for a in args { collect_con_heads(a, out); } } Type::Fn { params, ret, .. } => { for p in params { collect_con_heads(p, out); } collect_con_heads(ret, out); } Type::Forall { body, .. } => collect_con_heads(body, out), Type::Var { .. } => {} } } }