diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 35f5b2c..623753a 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -2458,12 +2458,7 @@ 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 + /// not produce the spec-mandated IR shape. Returns `Ok(true)` if /// the body was emitted (including the closing `}` and a final /// `\n\n`); `emit_fn` skips its normal body-lowering branch but /// MUST still run its post-body steps (deferred-thunk flush and @@ -2471,10 +2466,19 @@ impl<'a> Emitter<'a> { /// top-level fn gets so it is reachable as a `Term::Var` value). /// `Ok(false)` lets `emit_fn` continue with 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. + /// Currently inhabited arms: `eq__Str` (ships in 23.2.2) and + /// `compare__Int`, `compare__Bool`, `compare__Str` (this iter + /// 23.3). The two eq Int/Bool primitives (`eq__Int`, `eq__Bool`) + /// ride the natural `lower_eq` dispatch via their lambda body + /// `(== x y)` and do not need a hand-rolled body. The three + /// `compare` arms must be hand-rolled because there is no + /// type-polymorphic primitive that returns an Ordering ADT + /// value (Decision: `builtin_binop_typed` covers Int and Float + /// only — Bool is missing — so a surface-level + /// `if x < y { LT } else if x == y { EQ } else { GT }` body + /// would fail at codegen for `compare__Bool`). Hand-rolling all + /// three keeps the family consistent and the IR shape + /// predictable. fn try_emit_primitive_instance_body( &mut self, fn_name: &str, @@ -2505,10 +2509,150 @@ impl<'a> Emitter<'a> { self.block_terminated = true; 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(); + let lt_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {lt_test} = icmp slt i64 {a_ssa}, {b_ssa}\n" + )); + let id = self.fresh_id(); + let lt_label = format!("cmp_lt_{id}"); + let after_lt_label = format!("cmp_after_lt_{id}"); + self.body.push_str(&format!( + " br i1 {lt_test}, label %{lt_label}, label %{after_lt_label}\n" + )); + self.emit_ordering_arm(<_label, "LT")?; + self.start_block(&after_lt_label); + let eq_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {eq_test} = icmp eq i64 {a_ssa}, {b_ssa}\n" + )); + let eq_label = format!("cmp_eq_{id}"); + let gt_label = format!("cmp_gt_{id}"); + self.body.push_str(&format!( + " br i1 {eq_test}, label %{eq_label}, label %{gt_label}\n" + )); + self.emit_ordering_arm(&eq_label, "EQ")?; + self.emit_ordering_arm(>_label, "GT")?; + self.body.push_str("}\n\n"); + self.block_terminated = true; + 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(); + let lt_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {lt_test} = icmp ult i1 {a_ssa}, {b_ssa}\n" + )); + let id = self.fresh_id(); + let lt_label = format!("cmp_lt_{id}"); + let after_lt_label = format!("cmp_after_lt_{id}"); + self.body.push_str(&format!( + " br i1 {lt_test}, label %{lt_label}, label %{after_lt_label}\n" + )); + self.emit_ordering_arm(<_label, "LT")?; + self.start_block(&after_lt_label); + let eq_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {eq_test} = icmp eq i1 {a_ssa}, {b_ssa}\n" + )); + let eq_label = format!("cmp_eq_{id}"); + let gt_label = format!("cmp_gt_{id}"); + self.body.push_str(&format!( + " br i1 {eq_test}, label %{eq_label}, label %{gt_label}\n" + )); + self.emit_ordering_arm(&eq_label, "EQ")?; + self.emit_ordering_arm(>_label, "GT")?; + self.body.push_str("}\n\n"); + self.block_terminated = true; + 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(); + let cmp_res = self.fresh_ssa(); + self.body.push_str(&format!( + " {cmp_res} = call i32 @ail_str_compare(ptr {a_ssa}, ptr {b_ssa})\n" + )); + let lt_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {lt_test} = icmp slt i32 {cmp_res}, 0\n" + )); + let id = self.fresh_id(); + let lt_label = format!("cmp_lt_{id}"); + let after_lt_label = format!("cmp_after_lt_{id}"); + self.body.push_str(&format!( + " br i1 {lt_test}, label %{lt_label}, label %{after_lt_label}\n" + )); + self.emit_ordering_arm(<_label, "LT")?; + self.start_block(&after_lt_label); + let eq_test = self.fresh_ssa(); + self.body.push_str(&format!( + " {eq_test} = icmp eq i32 {cmp_res}, 0\n" + )); + let eq_label = format!("cmp_eq_{id}"); + let gt_label = format!("cmp_gt_{id}"); + self.body.push_str(&format!( + " br i1 {eq_test}, label %{eq_label}, label %{gt_label}\n" + )); + self.emit_ordering_arm(&eq_label, "EQ")?; + self.emit_ordering_arm(>_label, "GT")?; + self.body.push_str("}\n\n"); + self.block_terminated = true; + Ok(true) + } _ => Ok(false), } } + /// Iter 23.3: emit one arm of the `compare__T` branch ladder. + /// Starts a fresh basic block labelled `label`, constructs the + /// matching `Ordering` ctor (LT / EQ / GT) via `lower_ctor`, and + /// emits a `ret ptr `. Used three times per `compare__T` + /// arm in `try_emit_primitive_instance_body`. The ctor is 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<()> { + self.start_block(label); + // term_ptr 0: synthetic call site, not present in the + // escape-analysis result; falls back to the conservative + // "escapes → heap allocate" default. Safe for the Ordering + // return value (the caller owns it after `ret`). + let (ssa, _llvm_ty) = self.lower_ctor( + "Ordering", + ctor, + &[], + 0, + )?; + self.body.push_str(&format!(" ret ptr {ssa}\n")); + self.block_terminated = true; + Ok(()) + } + /// 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` @@ -3696,4 +3840,116 @@ mod tests { "header missing @ail_str_compare declaration; ir was:\n{ir}" ); } + + /// Iter 23.3: codegen intercepts a fn named `compare__Int` and + /// emits a three-way branch ladder: `icmp slt i64 a, b` → + /// LT-block; else `icmp eq i64 a, b` → EQ-block; else GT-block. + /// Each block constructs the matching Ordering ctor via the + /// existing `lower_ctor` path. Pinned with a synthetic FnDef so + /// the test does not depend on prelude auto-injection. + #[test] + fn compare_int_mono_symbol_emits_branch_ladder() { + let m = synth_compare_module("compare__Int", Type::int(), "i64"); + let ir = emit_ir(&m).unwrap(); + assert!( + ir.contains("icmp slt i64"), + "compare__Int body must contain `icmp slt i64`; ir:\n{ir}" + ); + assert!( + ir.contains("icmp eq i64"), + "compare__Int body must contain `icmp eq i64`; ir:\n{ir}" + ); + } + + /// Iter 23.3: same shape for `compare__Bool`. The LT-test uses + /// `icmp ult i1` (unsigned: false=0 ult true=1 gives the natural + /// Bool ordering false < true); the EQ-test uses `icmp eq i1`. + #[test] + fn compare_bool_mono_symbol_emits_branch_ladder() { + let m = synth_compare_module("compare__Bool", Type::bool_(), "i1"); + let ir = emit_ir(&m).unwrap(); + assert!( + ir.contains("icmp ult i1"), + "compare__Bool body must contain `icmp ult i1`; ir:\n{ir}" + ); + assert!( + ir.contains("icmp eq i1"), + "compare__Bool body must contain `icmp eq i1`; ir:\n{ir}" + ); + } + + /// Iter 23.3: `compare__Str` calls `@ail_str_compare` to get the + /// normalised {-1, 0, +1}, then branches: slt 0 → LT, eq 0 → EQ, + /// else GT. + #[test] + fn compare_str_mono_symbol_emits_ail_str_compare_call() { + let m = synth_compare_module("compare__Str", Type::str_(), "ptr"); + let ir = emit_ir(&m).unwrap(); + assert!( + ir.contains("call i32 @ail_str_compare("), + "compare__Str body must call @ail_str_compare; ir:\n{ir}" + ); + assert!( + ir.contains("icmp slt i32"), + "compare__Str body must compare result `slt i32` against 0; ir:\n{ir}" + ); + assert!( + ir.contains("icmp eq i32"), + "compare__Str body must compare result `eq i32` against 0; ir:\n{ir}" + ); + } + + /// Test helper: minimal two-module workspace where the "prelude" + /// module declares `data Ordering = LT | EQ | GT` and an + /// instance-fn shell (the intercept overrides the body), and the + /// entry module's `main` is a Unit no-op so `emit_ir` returns a + /// well-formed program. Used by the three `compare_*` tests above. + fn synth_compare_module(fn_name: &str, param_ail_ty: Type, _llvm_param_ty: &str) -> Module { + let ordering = Def::Type(TypeDef { + name: "Ordering".into(), + vars: vec![], + ctors: vec![ + Ctor { name: "LT".into(), fields: vec![] }, + Ctor { name: "EQ".into(), fields: vec![] }, + Ctor { name: "GT".into(), fields: vec![] }, + ], + doc: None, + drop_iterative: false, + }); + // Ordering is `ptr` at the LLVM level (boxed ADT). + let compare = Def::Fn(FnDef { + name: fn_name.into(), + ty: Type::Fn { + params: vec![param_ail_ty.clone(), param_ail_ty.clone()], + ret: Box::new(Type::Con { name: "Ordering".into(), args: vec![] }), + effects: vec![], + param_modes: vec![ParamMode::Borrow, ParamMode::Borrow], + ret_mode: ParamMode::Implicit, + }, + params: vec!["x".into(), "y".into()], + body: Term::Lit { lit: Literal::Unit }, // placeholder; intercept overrides + suppress: vec![], + doc: None, + }); + let main_def = 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, + }); + Module { + schema: SCHEMA.into(), + name: "prelude".into(), + imports: vec![], + defs: vec![ordering, compare, main_def], + } + } }