iter 23.2.2: codegen — declare @ail_str_eq + eq__Str body intercept

This commit is contained in:
2026-05-10 21:55:19 +02:00
parent cc2d6944c1
commit 736064adf4
6 changed files with 127 additions and 0 deletions
+1
View File
@@ -8,6 +8,7 @@ declare i32 @printf(ptr, ...)
declare i32 @puts(ptr)
declare ptr @GC_malloc(i64)
declare i32 @strcmp(ptr, ptr)
declare zeroext i1 @ail_str_eq(ptr, ptr)
declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_hello_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_hello_main_adapter, ptr null }
+1
View File
@@ -8,6 +8,7 @@ declare i32 @printf(ptr, ...)
declare i32 @puts(ptr)
declare ptr @GC_malloc(i64)
declare i32 @strcmp(ptr, ptr)
declare zeroext i1 @ail_str_eq(ptr, ptr)
declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_list_sum_list_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_list_sum_list_adapter, ptr null }
+1
View File
@@ -8,6 +8,7 @@ declare i32 @printf(ptr, ...)
declare i32 @puts(ptr)
declare ptr @GC_malloc(i64)
declare i32 @strcmp(ptr, ptr)
declare zeroext i1 @ail_str_eq(ptr, ptr)
declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_max3_max_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_max3_max_adapter, ptr null }
+1
View File
@@ -8,6 +8,7 @@ declare i32 @printf(ptr, ...)
declare i32 @puts(ptr)
declare ptr @GC_malloc(i64)
declare i32 @strcmp(ptr, ptr)
declare zeroext i1 @ail_str_eq(ptr, ptr)
declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_sum_sum_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_sum_sum_adapter, ptr null }
+1
View File
@@ -8,6 +8,7 @@ declare i32 @printf(ptr, ...)
declare i32 @puts(ptr)
declare ptr @GC_malloc(i64)
declare i32 @strcmp(ptr, ptr)
declare zeroext i1 @ail_str_eq(ptr, ptr)
declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_ws_lib_add_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_ws_lib_add_adapter, ptr null }
+122
View File
@@ -473,6 +473,14 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result<String>
// `icmp eq i32 0`. NUL-terminated strings make this a one-liner;
// libc supplies `strcmp` so no extra link flag is needed.
out.push_str("declare i32 @strcmp(ptr, ptr)\n");
// Iter 23.2: `runtime/str.c::ail_str_eq` backs the prelude's
// `eq__Str` mono symbol (see `try_emit_primitive_instance_body`).
// Declared unconditionally — the codegen intercept emits a call
// to it whenever the monomorphiser synthesises `eq__Str` in any
// module; the .o supplying the symbol is linked unconditionally
// by `ail build` (see `crates/ail/src/main.rs::build_to`). The
// `zeroext i1` return matches clang's lowering of C `_Bool`.
out.push_str("declare zeroext i1 @ail_str_eq(ptr, ptr)\n");
// Floats iter 4.4: saturating fp-to-int intrinsic for
// float_to_int_truncate. NaN → 0, +Inf → i64::MAX, -Inf →
// i64::MIN, finite-out-of-range saturates, finite-in-range
@@ -1051,6 +1059,13 @@ impl<'a> Emitter<'a> {
self.body.push_str(&sig);
self.start_block("entry");
// Iter 23.2: primitive-instance body intercept. Returns true if
// the body was emitted in full; we then skip the normal body
// lowering for this fn.
if self.try_emit_primitive_instance_body(&f.name, &llvm_param_tys, &llvm_ret)? {
return Ok(());
}
let (val, val_ty) = self.lower_term(&f.body)?;
if !self.block_terminated {
if val_ty != llvm_ret {
@@ -2410,6 +2425,56 @@ impl<'a> Emitter<'a> {
}
}
/// Iter 23.2: hand-rolled body for monomorphiser-synthesised
/// primitive instance methods whose natural lambda-lowering would
/// not produce the spec-mandated IR shape. Currently the only
/// inhabitant is `eq__Str`, which must call `@ail_str_eq` (the
/// stable AILang-namespaced Str-equality ABI in `runtime/str.c`)
/// rather than the `@strcmp + icmp eq i32 0` shape that
/// `lower_eq`'s Str arm would emit if we let the lambda body
/// `(== x y)` go through the normal path. Returns `Ok(true)` if
/// the body was emitted (caller must return immediately from
/// `emit_fn`); `Ok(false)` lets `emit_fn` continue with the
/// normal body lowering.
///
/// Future iters add `compare__Str` here (23.3); int/bool primitive
/// methods (`eq__Int`, `eq__Bool`, `compare__Int`, `compare__Bool`)
/// ride the natural `lower_eq` / `lower_compare` dispatch and do
/// not need a hand-rolled body.
fn try_emit_primitive_instance_body(
&mut self,
fn_name: &str,
param_tys: &[String],
ret_ty: &str,
) -> Result<bool> {
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();
let dst = self.fresh_ssa();
self.body.push_str(&format!(
" {dst} = call zeroext i1 @ail_str_eq(ptr {a_ssa}, ptr {b_ssa})\n"
));
self.body.push_str(&format!(" ret i1 {dst}\n"));
self.body.push_str("}\n\n");
self.block_terminated = true;
Ok(true)
}
_ => Ok(false),
}
}
/// Iter 16e: lower a `==` call after the two operands have been
/// emitted. Dispatches on the resolved AIL type of the arg side
/// (both sides have the same type after typecheck). The `_a_ll`
@@ -3359,4 +3424,61 @@ mod tests {
"format string `%g\\n` not interned: {ir}"
);
}
/// Iter 23.2: codegen intercepts a fn named `eq__Str` (the
/// monomorphiser's synthesised symbol for `Eq Str.eq`) and emits
/// a two-instruction body that calls @ail_str_eq, regardless of
/// what the lambda body in the source would lower to. Pinned with
/// a synthetic FnDef so the test does not depend on prelude
/// auto-injection.
#[test]
fn eq_str_mono_symbol_emits_ail_str_eq_call() {
let m = Module {
schema: SCHEMA.into(),
name: "prelude".into(),
imports: vec![],
defs: vec![
Def::Fn(FnDef {
name: "eq__Str".into(),
ty: Type::Fn {
params: vec![Type::str_(), Type::str_()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![ParamMode::Borrow, ParamMode::Borrow],
ret_mode: ParamMode::Implicit,
},
params: vec!["x".into(), "y".into()],
// Body is a placeholder — the intercept must
// ignore it. We use Lit::Bool false so even if the
// intercept misfires the test still compiles.
body: Term::Lit { lit: Literal::Bool { value: false } },
suppress: vec![],
doc: None,
}),
Def::Fn(FnDef {
name: "main".into(),
ty: Type::Fn {
params: vec![],
ret: Box::new(Type::unit()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec![],
body: Term::Lit { lit: Literal::Unit },
suppress: vec![],
doc: None,
}),
],
};
let ir = emit_ir(&m).unwrap();
assert!(
ir.contains("declare zeroext i1 @ail_str_eq(ptr, ptr)"),
"header missing @ail_str_eq declaration; ir was:\n{ir}"
);
assert!(
ir.contains("call zeroext i1 @ail_str_eq("),
"eq__Str body must call @ail_str_eq; ir was:\n{ir}"
);
}
}