1be2c1f145
Eighteen markdown list items in module- and item-doc comments had continuation lines that were not indented to align under the item text, so rustdoc rendered them as separate paragraphs and broke the list. Corrected the indentation (and, at two sites where a general wrap-up sentence followed a sublist, separated it with a blank doc line so it does not mis-attach to the last item). Doc whitespace only; no prose reworded, no code changed. Workspace clippy is now warning-clean.
876 lines
31 KiB
Rust
876 lines
31 KiB
Rust
//! Codegen intercept registry — one dispatch table for every
|
|
//! mono-symbol whose body the codegen supplies as LLVM IR
|
|
//! directly (replacing the .ail placeholder body that the
|
|
//! intercept's home module ships for round-trip stability).
|
|
//!
|
|
//! Each entry carries:
|
|
//! - `name` — the mono symbol the dispatch keys on
|
|
//! - `expected_params` — LLVM-IR param types as text
|
|
//! - `expected_ret` — LLVM-IR return type as text
|
|
//! - `wants_alwaysinline` — whether the dispatch site should
|
|
//! attach the `alwaysinline` attribute
|
|
//! to the emitted fn (was a separate
|
|
//! hardcoded name-list in the predicate
|
|
//! `intercept_emit_wants_alwaysinline`
|
|
//! before raw-buf.1)
|
|
//! - `emit` — the per-arm IR-emission body, run
|
|
//! against `&mut Emitter` after the
|
|
//! sig check
|
|
//!
|
|
//! Adding a new intercept = adding one row to `INTERCEPTS`. The
|
|
//! dispatch site (`try_emit_primitive_instance_body` in
|
|
//! `crates/ailang-codegen/src/lib.rs`) and the alwaysinline
|
|
//! predicate both consult this single table.
|
|
|
|
use crate::{CodegenError, Emitter, Result};
|
|
|
|
pub(crate) struct Intercept {
|
|
pub name: &'static str,
|
|
pub expected_params: &'static [&'static str],
|
|
pub expected_ret: &'static str,
|
|
pub wants_alwaysinline: bool,
|
|
pub emit: fn(&mut Emitter<'_>) -> Result<()>,
|
|
}
|
|
|
|
pub(crate) static INTERCEPTS: &[Intercept] = &[
|
|
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 <src>, 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<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(|_| ())
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// 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<String> {
|
|
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__<elem>` 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<String> =
|
|
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<Type>)> {
|
|
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<String> = 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<Type> = 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<String>) {
|
|
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 { .. } => {}
|
|
}
|
|
}
|
|
}
|