diff --git a/crates/ail/tests/snapshots/hello.ll b/crates/ail/tests/snapshots/hello.ll index 92d8867..26e1e9d 100644 --- a/crates/ail/tests/snapshots/hello.ll +++ b/crates/ail/tests/snapshots/hello.ll @@ -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 } diff --git a/crates/ail/tests/snapshots/list.ll b/crates/ail/tests/snapshots/list.ll index 6fbf429..de9700f 100644 --- a/crates/ail/tests/snapshots/list.ll +++ b/crates/ail/tests/snapshots/list.ll @@ -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 } diff --git a/crates/ail/tests/snapshots/max3.ll b/crates/ail/tests/snapshots/max3.ll index 55b11b8..8364022 100644 --- a/crates/ail/tests/snapshots/max3.ll +++ b/crates/ail/tests/snapshots/max3.ll @@ -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 } diff --git a/crates/ail/tests/snapshots/sum.ll b/crates/ail/tests/snapshots/sum.ll index dde44b5..e6920e5 100644 --- a/crates/ail/tests/snapshots/sum.ll +++ b/crates/ail/tests/snapshots/sum.ll @@ -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 } diff --git a/crates/ail/tests/snapshots/ws_main.ll b/crates/ail/tests/snapshots/ws_main.ll index 943dca9..69d4564 100644 --- a/crates/ail/tests/snapshots/ws_main.ll +++ b/crates/ail/tests/snapshots/ws_main.ll @@ -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 } diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 307d148..9df5754 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -473,6 +473,14 @@ fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy) -> Result // `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 { + 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}" + ); + } }