# 23.3 — Ord Class + Primitive Instances — Implementation Plan > **Parent spec:** `docs/specs/2026-05-10-23-eq-ord-prelude.md` (Components row 23.3) > > **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` > to run this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Ship `class Ord a extends Eq where compare : (a borrow, a borrow) -> Ordering` in the auto-loaded prelude with three primitive instances (Ord Int, Ord Bool, Ord Str), backed by a new `ail_str_compare` in `runtime/str.c`. After this iter, a user program can call `compare x y` on Int / Bool / Str and the monomorphiser will synthesise `compare__Int` / `compare__Bool` / `compare__Str` top-level fns in the prelude module that codegen lowers as three-way branch ladders constructing the `Ordering` ADT values (LT / EQ / GT). **Architecture:** All three `compare__T` symbols are hand-rolled via the codegen intercept established in iter 23.2 — `compare__Bool` cannot ride the existing primitive `<` dispatch because `builtin_binop_typed` (`crates/ailang-codegen/src/synth.rs:260-294`) has Int and Float arms only, and bringing Bool into that table is a language-wide decision that does not belong here. Hand-rolling all three keeps the family consistent: the IR shape is identical (a type-specific LT-test branch + an EQ-test branch + a GT default branch, each constructing the matching Ordering ctor via `lower_ctor` for alloc-strategy correctness). The new `ail_str_compare` in `runtime/str.c` normalises libc strcmp's signed result to exactly -1 / 0 / +1 so the codegen branch ladder can compare against constant 0 cleanly. The intercept doc comment from iter 23.2.2 is corrected (it speculated that compare__Int/Bool would ride natural lowering; the language facts dictate otherwise). Decision 11's single-superclass closure (`Ord` extends `Eq` ⇒ every `instance Ord T` requires `instance Eq T` to exist in the workspace) is automatically satisfied: iter 23.2 shipped the three Eq instances. No new check-side mechanism needed. **Tech Stack:** `runtime/str.c` (one new C function), `crates/ailang-codegen/src/lib.rs` (IR header decl + intercept arms + a small block-emission helper), `crates/ailang-core/src/workspace.rs` (prelude-load test extension), `examples/prelude.ail.json` (class + 3 instances), `examples/compare_primitives_smoke.ail.json` (new fixture). --- **Files this plan creates or modifies:** - Modify: `runtime/str.c` — append `ail_str_compare`. - Modify: `crates/ailang-codegen/src/lib.rs` — header gains `declare i32 @ail_str_compare(ptr, ptr)`; `try_emit_primitive_instance_body` gains three arms (`compare__Int`, `compare__Bool`, `compare__Str`); new private helper `emit_ordering_arm`; doc on `try_emit_primitive_instance_body` corrected. - Modify: `examples/prelude.ail.json` — append `class Ord a` + three instance defs. Existing Eq / Ordering / class machinery stays. - Modify: `crates/ailang-core/src/workspace.rs` — extend `loads_workspace_auto_injects_prelude` to assert `class Ord` + Ord instances on Int / Bool / Str. - Create: `examples/compare_primitives_smoke.ail.json` — E2E fixture calling `compare` on each primitive with LT, EQ, GT pairs. - Modify: `crates/ail/tests/e2e.rs` — new E2E test `compare_primitives_smoke_compiles_and_runs`. - Modify: `crates/ailang-codegen/src/lib.rs` `mod tests` — three IR-shape integration tests (one per primitive). --- ## Task 1: `runtime/str.c::ail_str_compare` + IR declaration **Files:** - Modify: `runtime/str.c` — append `ail_str_compare`. - Modify: `crates/ailang-codegen/src/lib.rs:~482` — add `declare i32 @ail_str_compare(ptr, ptr)` after the existing `@ail_str_eq` declaration. - Test: append a codegen unit test asserting the new IR header line is present. - [ ] **Step 1: Write the failing test** Append to the existing `#[cfg(test)] mod tests` block in `crates/ailang-codegen/src/lib.rs`: ```rust /// Iter 23.3: `runtime/str.c::ail_str_compare` backs the prelude's /// `compare__Str` mono symbol (see `try_emit_primitive_instance_body`). /// The declaration is unconditional in the IR header alongside /// `@ail_str_eq`; this test pins the header line so a regression /// that drops it would fail at link time only on programs that /// actually call `compare` on a Str. #[test] fn ir_header_declares_ail_str_compare() { let m = Module { schema: SCHEMA.into(), name: "t".into(), imports: vec![], defs: vec![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 i32 @ail_str_compare(ptr, ptr)"), "header missing @ail_str_compare declaration; ir was:\n{ir}" ); } ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test --workspace --lib ir_header_declares_ail_str_compare` Expected: FAIL — `header missing @ail_str_compare declaration`. - [ ] **Step 3: Append `ail_str_compare` to `runtime/str.c`** After the existing `ail_str_eq` function in `runtime/str.c`, append: ```c /* Three-way comparison of NUL-terminated strings. Normalises libc * strcmp's signed-int output to exactly -1 / 0 / +1 so the codegen * caller can branch on `icmp slt i32 result, 0` and * `icmp eq i32 result, 0` against constants directly without * worrying about strcmp implementations that return values outside * {-1, 0, +1}. Heap-Str ABI milestone will replace the body with * length-prefixed comparison without changing the signature. */ int ail_str_compare(const char *a, const char *b) { int r = strcmp(a, b); if (r < 0) return -1; if (r > 0) return 1; return 0; } ``` - [ ] **Step 4: Add the `@ail_str_compare` declaration to the IR header** In `crates/ailang-codegen/src/lib.rs`, directly after the existing `out.push_str("declare zeroext i1 @ail_str_eq(ptr, ptr)\n");` line (~line 482, added in iter 23.2.2), insert: ```rust // Iter 23.3: `runtime/str.c::ail_str_compare` backs the prelude's // `compare__Str` mono symbol (see `try_emit_primitive_instance_body` // `compare__Str` arm below). Declared unconditionally on the same // rationale as `@ail_str_eq` — the .o is supplied by the // unconditionally-linked `runtime/str.c`, and clang -O2 dead- // strips the symbol when no caller exists. Returns i32 normalised // to {-1, 0, +1} so the branch ladder in the intercept can compare // against constant 0 directly. out.push_str("declare i32 @ail_str_compare(ptr, ptr)\n"); ``` - [ ] **Step 5: Run the test to verify it passes** Run: `cargo test --workspace --lib ir_header_declares_ail_str_compare` Expected: PASS. - [ ] **Step 6: Re-run the existing test suite to confirm no regressions** Run: `cargo build --workspace && cargo test --workspace` Expected: PASS. Snapshot fixtures under `crates/ail/tests/snapshots/` will gain one new line each (the new `declare` line) — refresh via `UPDATE_SNAPSHOTS=1` if the snapshot tests fail and the only diff is the additive `@ail_str_compare` declaration. Include refreshed snapshots in the commit. - [ ] **Step 7: Commit** ```bash git add runtime/str.c crates/ailang-codegen/src/lib.rs crates/ail/tests/snapshots git commit -m "iter 23.3.1: runtime ail_str_compare + IR header declaration" ``` (Omit the `crates/ail/tests/snapshots` path from `git add` if no snapshot files were modified.) --- ## Task 2: Codegen — three `compare__T` arms in `try_emit_primitive_instance_body` **Files:** - Modify: `crates/ailang-codegen/src/lib.rs::try_emit_primitive_instance_body` (~line 2452): add three new match arms (`compare__Int`, `compare__Bool`, `compare__Str`); correct the doc comment at ~line 2437; add a private helper `emit_ordering_arm` directly below the intercept. - Modify: `crates/ailang-codegen/src/lib.rs` `mod tests` — three unit tests on synthetic FnDefs (one per compare__T arm). - [ ] **Step 1: Write the failing tests** Append to the existing `#[cfg(test)] mod tests` block in `crates/ailang-codegen/src/lib.rs`: ```rust /// 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], } } ``` Note: the exact field set on `TypeDef` may vary (`drop_iterative` is the iter-18e field). If the field name differs in the current codebase, inspect `crates/ailang-core/src/ast.rs::TypeDef` and adjust. If `Ctor` has additional fields, default them as the rest of the codebase does. - [ ] **Step 2: Run the three tests to verify they fail** Run: ``` cargo test --workspace --lib compare_int_mono_symbol_emits_branch_ladder compare_bool_mono_symbol_emits_branch_ladder compare_str_mono_symbol_emits_ail_str_compare_call ``` Expected: ALL THREE FAIL — `compare__T body must contain ...` (intercept arms not yet wired). - [ ] **Step 3: Add the private helper `emit_ordering_arm`** In `crates/ailang-codegen/src/lib.rs`, directly below `try_emit_primitive_instance_body` (currently ending ~line 2495), insert: ```rust /// 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(()) } ``` - [ ] **Step 4: Correct the iter 23.2.2 doc comment** In `crates/ailang-codegen/src/lib.rs`, find the doc comment on `try_emit_primitive_instance_body` (around line 2467 — the line that reads `/// methods (\`eq__Int\`, \`eq__Bool\`, \`compare__Int\`, \`compare__Bool\`)`). Replace the lines describing the future arms with the corrected shape: ```rust /// 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. ``` Use Edit with the precise old text in the current file to substitute. Read the surrounding doc-comment context first to make the old_string unique. - [ ] **Step 5: Add the three compare arms to the match block** Inside `try_emit_primitive_instance_body`'s `match fn_name { ... }`, add three arms next to the existing `"eq__Str"` arm: ```rust "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) } ``` - [ ] **Step 6: Run the three unit tests to verify they pass** Run: ``` cargo test --workspace --lib compare_int_mono_symbol_emits_branch_ladder compare_bool_mono_symbol_emits_branch_ladder compare_str_mono_symbol_emits_ail_str_compare_call ``` Expected: ALL THREE PASS. - [ ] **Step 7: Run the full codegen test suite to confirm no regressions** Run: `cargo test --workspace -p ailang-codegen` Expected: PASS. - [ ] **Step 8: Commit** ```bash git add crates/ailang-codegen/src/lib.rs git commit -m "iter 23.3.2: codegen — three compare__T intercept arms + emit_ordering_arm helper" ``` --- ## Task 3: Prelude — `class Ord a extends Eq` + three instances **Files:** - Modify: `examples/prelude.ail.json` — append `class Ord` def + three `instance Ord T` defs after the existing Eq defs. - Modify: `crates/ailang-core/src/workspace.rs::loads_workspace_auto_injects_prelude` test — append assertions for `class Ord` + Ord instances. - [ ] **Step 1: Write the failing test** In `crates/ailang-core/src/workspace.rs::loads_workspace_auto_injects_prelude`, after the existing Eq-presence + Eq-instances assertion block (added in iter 23.2.3), append: ```rust // Iter 23.3: prelude also ships the `Ord` class plus three // primitive instances (Ord Int, Ord Bool, Ord Str). Ord // extends Eq (Decision 11 single-superclass closure), so its // shape is the same as Eq's instance set; the loader uses the // superclass walk to verify the Eq instances exist. assert!( prelude.defs.iter().any(|d| matches!( d, crate::ast::Def::Class(c) if c.name == "Ord" )), "prelude must contain Ord class def" ); let ord_instance_types: Vec<&str> = prelude .defs .iter() .filter_map(|d| match d { crate::ast::Def::Instance(i) if i.class == "Ord" => { if let crate::ast::Type::Con { name, .. } = &i.type_ { Some(name.as_str()) } else { None } } _ => None, }) .collect(); assert!( ord_instance_types.contains(&"Int"), "prelude must contain `instance Ord Int`; saw: {ord_instance_types:?}" ); assert!( ord_instance_types.contains(&"Bool"), "prelude must contain `instance Ord Bool`; saw: {ord_instance_types:?}" ); assert!( ord_instance_types.contains(&"Str"), "prelude must contain `instance Ord Str`; saw: {ord_instance_types:?}" ); ``` - [ ] **Step 2: Run test to verify it fails** Run: `cargo test --workspace -p ailang-core loads_workspace_auto_injects_prelude` Expected: FAIL — `prelude must contain Ord class def`. - [ ] **Step 3: Extend `examples/prelude.ail.json` with Ord + 3 instances** After the last `Eq Str` instance def in `examples/prelude.ail.json` (before the closing `]` of the outer `defs` array), append (with a leading comma after the `Eq Str` instance's closing `}`): ```json , { "kind": "class", "name": "Ord", "param": "a", "superclass": { "class": "Eq", "type": "a" }, "doc": "Total ordering. Ships in milestone 23 alongside Eq. `compare x y` returns LT, EQ, or GT (the three-ctor Ordering ADT also in the prelude). Decision 11's single-superclass closure requires `instance Eq T` for every `instance Ord T` — the three Ord instances below pair with the three Eq instances shipped in iter 23.2.3.", "methods": [ { "name": "compare", "type": { "k": "fn", "params": [ { "k": "var", "name": "a" }, { "k": "var", "name": "a" } ], "param_modes": ["borrow", "borrow"], "ret": { "k": "con", "name": "Ordering" }, "effects": [] } } ] }, { "kind": "instance", "class": "Ord", "type": { "k": "con", "name": "Int" }, "doc": "Ord Int. The lambda body shape is a placeholder for round-trip stability; the codegen intercept `try_emit_primitive_instance_body::\"compare__Int\"` emits a three-way `icmp slt` / `icmp eq` branch ladder constructing LT / EQ / GT.", "methods": [ { "name": "compare", "body": { "t": "lam", "params": ["x", "y"], "paramTypes": [ { "k": "var", "name": "a" }, { "k": "var", "name": "a" } ], "retType": { "k": "con", "name": "Ordering" }, "body": { "t": "ctor", "type": "Ordering", "ctor": "EQ", "args": [] } } } ] }, { "kind": "instance", "class": "Ord", "type": { "k": "con", "name": "Bool" }, "doc": "Ord Bool. Body lowered via `try_emit_primitive_instance_body::\"compare__Bool\"` — `icmp ult i1` LT-test, `icmp eq i1` EQ-test, GT default.", "methods": [ { "name": "compare", "body": { "t": "lam", "params": ["x", "y"], "paramTypes": [ { "k": "var", "name": "a" }, { "k": "var", "name": "a" } ], "retType": { "k": "con", "name": "Ordering" }, "body": { "t": "ctor", "type": "Ordering", "ctor": "EQ", "args": [] } } } ] }, { "kind": "instance", "class": "Ord", "type": { "k": "con", "name": "Str" }, "doc": "Ord Str. Body lowered via `try_emit_primitive_instance_body::\"compare__Str\"` — `call i32 @ail_str_compare(ptr, ptr)` then branch on slt-0 / eq-0 against the normalised {-1, 0, +1} return.", "methods": [ { "name": "compare", "body": { "t": "lam", "params": ["x", "y"], "paramTypes": [ { "k": "var", "name": "a" }, { "k": "var", "name": "a" } ], "retType": { "k": "con", "name": "Ordering" }, "body": { "t": "ctor", "type": "Ordering", "ctor": "EQ", "args": [] } } } ] } ``` The placeholder body returns `EQ` — codegen never lowers it (intercept fires first), but the typechecker validates the body against the declared return type `Ordering`. `EQ` is a zero-arg Ordering ctor, so the body is well-typed. - [ ] **Step 4: Run the prelude-load test to verify it passes** Run: `cargo test --workspace -p ailang-core loads_workspace_auto_injects_prelude` Expected: PASS. - [ ] **Step 5: Run the full workspace test suite** Run: `cargo build --workspace && cargo test --workspace` Expected: PASS. If any pre-existing test fails because of the new Ord defs in the prelude (e.g. a fixture that defines its own `class Ord` or method `compare` and now collides on `MethodNameCollision`), STOP and report `NEEDS_CONTEXT` with the failing test names — do NOT modify unrelated tests to make them pass. (The iter 23.2.3-prep commits already renamed fixture-side `class Eq.eq` and `class Ord.lt` to `TEq.teq` / `TOrd.tlt`; if any test fixture still defines `compare` as a method name on its own class, that would be a new collision.) - [ ] **Step 6: Commit** ```bash git add examples/prelude.ail.json crates/ailang-core/src/workspace.rs git commit -m "iter 23.3.3: prelude — class Ord a extends Eq + Ord Int/Bool/Str instances" ``` --- ## Task 4: E2E fixture + IR-shape integration tests **Files:** - Create: `examples/compare_primitives_smoke.ail.json` — fixture calling `compare` on Int/Bool/Str with LT/EQ/GT pairs, matching the result against the three Ordering ctors, printing 1/2/3. - Modify: `crates/ail/tests/e2e.rs` — new test `compare_primitives_smoke_compiles_and_runs`. - Modify: `crates/ailang-codegen/src/lib.rs` `mod tests` — three IR-shape integration tests on the new fixture (mirror the iter 23.2.4 pattern). - [ ] **Step 1: Write the failing E2E test** In `crates/ail/tests/e2e.rs`, after the existing `eq_primitives_smoke_compiles_and_runs` test (added in iter 23.2.4), append: ```rust /// Iter 23.3: end-to-end coverage for the auto-loaded `Ord` class /// + three primitive instances. The fixture calls `compare` on /// Int / Bool / Str with three pairs each (an LT pair, an EQ /// pair, a GT pair), matches the result against LT / EQ / GT, /// and prints 1 / 2 / 3 respectively. The monomorphiser /// synthesises `compare__Int` / `compare__Bool` / `compare__Str` /// in the prelude module; codegen's `try_emit_primitive_instance_body` /// intercept emits the branch ladder; the binary prints nine /// lines in the order LT (1), EQ (2), GT (3), per type. #[test] fn compare_primitives_smoke_compiles_and_runs() { let stdout = build_and_run("compare_primitives_smoke.ail.json"); assert_eq!(stdout, "1\n2\n3\n1\n2\n3\n1\n2\n3\n"); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: `cargo test --workspace -p ail compare_primitives_smoke_compiles_and_runs` Expected: FAIL — `examples/compare_primitives_smoke.ail.json` does not exist; `ail build` exits non-zero. - [ ] **Step 3: Write the fixture** Create `examples/compare_primitives_smoke.ail.json`: ```json { "schema": "ailang/v0", "name": "compare_primitives_smoke", "imports": [], "defs": [ { "kind": "fn", "name": "ord_to_int", "doc": "Map an Ordering to a printable Int. LT → 1, EQ → 2, GT → 3. Used by main to render the result of each `compare` call as a single line of stdout.", "type": { "k": "fn", "params": [{ "k": "con", "name": "Ordering" }], "ret": { "k": "con", "name": "Int" }, "effects": [] }, "params": ["o"], "body": { "t": "match", "scrutinee": { "t": "var", "name": "o" }, "arms": [ { "pat": { "p": "ctor", "ctor": "LT", "fields": [] }, "body": { "t": "lit", "lit": { "kind": "int", "value": 1 } } }, { "pat": { "p": "ctor", "ctor": "EQ", "fields": [] }, "body": { "t": "lit", "lit": { "kind": "int", "value": 2 } } }, { "pat": { "p": "ctor", "ctor": "GT", "fields": [] }, "body": { "t": "lit", "lit": { "kind": "int", "value": 3 } } } ] } }, { "kind": "fn", "name": "main", "type": { "k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" }, "effects": ["IO"] }, "params": [], "body": { "t": "seq", "lhs": { "t": "do", "op": "io/print_int", "args": [{ "t": "app", "fn": { "t": "var", "name": "ord_to_int" }, "args": [{ "t": "app", "fn": { "t": "var", "name": "compare" }, "args": [ { "t": "lit", "lit": { "kind": "int", "value": 1 } }, { "t": "lit", "lit": { "kind": "int", "value": 2 } } ] }] }] }, "rhs": { "t": "seq", "lhs": { "t": "do", "op": "io/print_int", "args": [{ "t": "app", "fn": { "t": "var", "name": "ord_to_int" }, "args": [{ "t": "app", "fn": { "t": "var", "name": "compare" }, "args": [ { "t": "lit", "lit": { "kind": "int", "value": 5 } }, { "t": "lit", "lit": { "kind": "int", "value": 5 } } ] }] }] }, "rhs": { "t": "seq", "lhs": { "t": "do", "op": "io/print_int", "args": [{ "t": "app", "fn": { "t": "var", "name": "ord_to_int" }, "args": [{ "t": "app", "fn": { "t": "var", "name": "compare" }, "args": [ { "t": "lit", "lit": { "kind": "int", "value": 9 } }, { "t": "lit", "lit": { "kind": "int", "value": 3 } } ] }] }] }, "rhs": { "t": "seq", "lhs": { "t": "do", "op": "io/print_int", "args": [{ "t": "app", "fn": { "t": "var", "name": "ord_to_int" }, "args": [{ "t": "app", "fn": { "t": "var", "name": "compare" }, "args": [ { "t": "lit", "lit": { "kind": "bool", "value": false } }, { "t": "lit", "lit": { "kind": "bool", "value": true } } ] }] }] }, "rhs": { "t": "seq", "lhs": { "t": "do", "op": "io/print_int", "args": [{ "t": "app", "fn": { "t": "var", "name": "ord_to_int" }, "args": [{ "t": "app", "fn": { "t": "var", "name": "compare" }, "args": [ { "t": "lit", "lit": { "kind": "bool", "value": true } }, { "t": "lit", "lit": { "kind": "bool", "value": true } } ] }] }] }, "rhs": { "t": "seq", "lhs": { "t": "do", "op": "io/print_int", "args": [{ "t": "app", "fn": { "t": "var", "name": "ord_to_int" }, "args": [{ "t": "app", "fn": { "t": "var", "name": "compare" }, "args": [ { "t": "lit", "lit": { "kind": "bool", "value": true } }, { "t": "lit", "lit": { "kind": "bool", "value": false } } ] }] }] }, "rhs": { "t": "seq", "lhs": { "t": "do", "op": "io/print_int", "args": [{ "t": "app", "fn": { "t": "var", "name": "ord_to_int" }, "args": [{ "t": "app", "fn": { "t": "var", "name": "compare" }, "args": [ { "t": "lit", "lit": { "kind": "str", "value": "alpha" } }, { "t": "lit", "lit": { "kind": "str", "value": "beta" } } ] }] }] }, "rhs": { "t": "seq", "lhs": { "t": "do", "op": "io/print_int", "args": [{ "t": "app", "fn": { "t": "var", "name": "ord_to_int" }, "args": [{ "t": "app", "fn": { "t": "var", "name": "compare" }, "args": [ { "t": "lit", "lit": { "kind": "str", "value": "same" } }, { "t": "lit", "lit": { "kind": "str", "value": "same" } } ] }] }] }, "rhs": { "t": "do", "op": "io/print_int", "args": [{ "t": "app", "fn": { "t": "var", "name": "ord_to_int" }, "args": [{ "t": "app", "fn": { "t": "var", "name": "compare" }, "args": [ { "t": "lit", "lit": { "kind": "str", "value": "zulu" } }, { "t": "lit", "lit": { "kind": "str", "value": "alpha" } } ] }] }] } } } } } } } } } } ] } ``` Pair pattern per type: (LT-result, EQ-result, GT-result): - Int: `(1,2)` → LT, `(5,5)` → EQ, `(9,3)` → GT. - Bool: `(false, true)` → LT, `(true, true)` → EQ, `(true, false)` → GT. - Str: `("alpha","beta")` → LT, `("same","same")` → EQ, `("zulu","alpha")` → GT. Expected stdout: `"1\n2\n3\n1\n2\n3\n1\n2\n3\n"`. - [ ] **Step 4: Run E2E test to verify it passes** Run: `cargo test --workspace -p ail compare_primitives_smoke_compiles_and_runs` Expected: PASS — stdout is `"1\n2\n3\n1\n2\n3\n1\n2\n3\n"`. If the test fails with stdout `"2\n2\n2\n2\n2\n2\n2\n2\n2\n"`, that means the intercept did not fire and the placeholder `EQ` body in the prelude got lowered instead. Verify in `try_emit_primitive_instance_body` that the new arms match the fn names exactly (case-sensitive, double-underscore separator). If the test fails with a different stdout pattern, inspect the emitted IR by running: ``` ail emit-ir examples/compare_primitives_smoke.ail.json ``` and grep for `@ail_prelude_compare__T` to verify the branch ladder shape. Do NOT loosen the assertion; understand the regression first. - [ ] **Step 5: Add three codegen IR-shape integration tests** Append to the existing `#[cfg(test)] mod tests` block in `crates/ailang-codegen/src/lib.rs` (after the iter-23.2.4 integration tests for `eq__T`): ```rust /// Iter 23.3: integration with the auto-loaded prelude. Compiling /// a workspace that calls `compare` on Int produces a /// mono-synthesised `@ail_prelude_compare__Int` whose body /// contains the `icmp slt i64` LT-test and the `icmp eq i64` /// EQ-test characteristic of the intercept's branch ladder. #[test] fn compare_int_call_produces_branch_ladder_mono_fn() { use ailang_core::load_workspace; use ailang_check::{check_workspace, monomorphise_workspace}; let manifest = env!("CARGO_MANIFEST_DIR"); let entry = std::path::Path::new(manifest) .parent().unwrap().parent().unwrap() .join("examples").join("compare_primitives_smoke.ail.json"); let ws = load_workspace(&entry).expect("load workspace"); let diags = check_workspace(&ws); assert!( diags.iter().all(|d| !matches!(d.severity, ailang_check::Severity::Error)), "unexpected check_workspace errors: {diags:?}" ); let ws = monomorphise_workspace(&ws).expect("monomorphise"); let ir = lower_workspace(&ws).expect("lower"); assert!( ir.contains("@ail_prelude_compare__Int"), "ir must contain mono-synthesised @ail_prelude_compare__Int; ir was:\n{ir}" ); assert!( ir.contains("icmp slt i64"), "compare__Int body must contain `icmp slt i64`; ir was:\n{ir}" ); assert!( ir.contains("icmp eq i64"), "compare__Int body must contain `icmp eq i64`; ir was:\n{ir}" ); } /// Iter 23.3: same shape for `compare__Bool`. `icmp ult i1` /// LT-test + `icmp eq i1` EQ-test. #[test] fn compare_bool_call_produces_branch_ladder_mono_fn() { use ailang_core::load_workspace; use ailang_check::{check_workspace, monomorphise_workspace}; let manifest = env!("CARGO_MANIFEST_DIR"); let entry = std::path::Path::new(manifest) .parent().unwrap().parent().unwrap() .join("examples").join("compare_primitives_smoke.ail.json"); let ws = load_workspace(&entry).expect("load workspace"); let diags = check_workspace(&ws); assert!( diags.iter().all(|d| !matches!(d.severity, ailang_check::Severity::Error)), "unexpected check_workspace errors: {diags:?}" ); let ws = monomorphise_workspace(&ws).expect("monomorphise"); let ir = lower_workspace(&ws).expect("lower"); assert!( ir.contains("@ail_prelude_compare__Bool"), "ir must contain mono-synthesised @ail_prelude_compare__Bool; ir was:\n{ir}" ); assert!( ir.contains("icmp ult i1"), "compare__Bool body must contain `icmp ult i1`; ir was:\n{ir}" ); assert!( ir.contains("icmp eq i1"), "compare__Bool body must contain `icmp eq i1`; ir was:\n{ir}" ); } /// Iter 23.3: `compare__Str` calls `@ail_str_compare` (runtime /// primitive in `runtime/str.c`) and branches on the normalised /// {-1, 0, +1} result. Asserts the call site and the two branch /// tests on the result. #[test] fn compare_str_call_produces_ail_str_compare_call_mono_fn() { use ailang_core::load_workspace; use ailang_check::{check_workspace, monomorphise_workspace}; let manifest = env!("CARGO_MANIFEST_DIR"); let entry = std::path::Path::new(manifest) .parent().unwrap().parent().unwrap() .join("examples").join("compare_primitives_smoke.ail.json"); let ws = load_workspace(&entry).expect("load workspace"); let diags = check_workspace(&ws); assert!( diags.iter().all(|d| !matches!(d.severity, ailang_check::Severity::Error)), "unexpected check_workspace errors: {diags:?}" ); let ws = monomorphise_workspace(&ws).expect("monomorphise"); let ir = lower_workspace(&ws).expect("lower"); assert!( ir.contains("@ail_prelude_compare__Str"), "ir must contain mono-synthesised @ail_prelude_compare__Str; ir was:\n{ir}" ); assert!( ir.contains("call i32 @ail_str_compare("), "compare__Str body must call @ail_str_compare; ir was:\n{ir}" ); assert!( ir.contains("icmp slt i32"), "compare__Str body must contain `icmp slt i32` (LT-test against 0); ir was:\n{ir}" ); assert!( ir.contains("icmp eq i32"), "compare__Str body must contain `icmp eq i32` (EQ-test against 0); ir was:\n{ir}" ); } ``` - [ ] **Step 6: Run the three integration tests** Run: ``` cargo test --workspace --lib compare_int_call_produces_branch_ladder_mono_fn compare_bool_call_produces_branch_ladder_mono_fn compare_str_call_produces_ail_str_compare_call_mono_fn ``` Expected: ALL THREE PASS. - [ ] **Step 7: Run the full workspace test suite** Run: `cargo build --workspace && cargo test --workspace` Expected: FULL workspace green. - [ ] **Step 8: Commit** ```bash git add examples/compare_primitives_smoke.ail.json crates/ail/tests/e2e.rs crates/ailang-codegen/src/lib.rs git commit -m "iter 23.3.4: e2e smoke fixture + ir-shape integration tests for compare__T mono symbols" ``` --- ## Task 5: JOURNAL entry **Files:** - Modify: `docs/JOURNAL.md` — append a new entry at the END of the file (chronological order with most recent last, matching iter 23.1 and 23.2 entries). - [ ] **Step 1: Append the JOURNAL entry** Append to `docs/JOURNAL.md`: ```markdown ## 2026-05-10 — Iteration 23.3: Ord class + three primitive instances Third iteration of milestone 23. Ships `class Ord a extends Eq where compare : (a borrow, a borrow) -> Ordering` in the auto- loaded prelude plus three primitive instances (Ord Int, Ord Bool, Ord Str). User programs can now call `compare x y` on any of the three primitives; the monomorphiser synthesises `compare__Int` / `compare__Bool` / `compare__Str` top-level fns in the prelude module and codegen's `try_emit_primitive_instance_body` intercept hand-rolls a three-way branch ladder for each: - `compare__Int` → `icmp slt i64` LT-test + `icmp eq i64` EQ-test, constructing LT / EQ / GT via `lower_ctor`. - `compare__Bool` → `icmp ult i1` LT-test + `icmp eq i1` EQ-test, same ctor construction. - `compare__Str` → `call i32 @ail_str_compare(...)` followed by `icmp slt i32 _, 0` LT-test + `icmp eq i32 _, 0` EQ-test. All three are hand-rolled, unlike iter 23.2's eq family where `eq__Int` / `eq__Bool` rode the natural `lower_eq` dispatch. The reason: `builtin_binop_typed` (`crates/ailang-codegen/src/synth.rs:260-294`) only handles Int and Float for the comparison ops `< / <= / > / >=` — Bool is missing. A surface-level `if x < y { LT } else if x == y { EQ } else { GT }` body for `compare__Bool` would fail at codegen. Bringing Bool into the primitive comparison table is a language-wide design decision that does not belong in this iter; hand-rolling all three compare arms keeps the family consistent and the IR shape predictable. The iter-23.2.2 doc comment that speculated compare__Int/Bool would ride natural lowering is corrected. The Str runtime primitive `ail_str_compare` lives in `runtime/str.c` alongside `ail_str_eq`; it normalises libc strcmp's signed result to exactly {-1, 0, +1} so the codegen branch ladder can compare against constant 0 directly. The .o is supplied by the same unconditional clang compile+link wired up in iter 23.2.1; no additional build-side changes. Decision 11's single-superclass closure (every `instance Ord T` requires `instance Eq T`) is automatically satisfied — iter 23.2 shipped the three Eq instances on the same types. No new check-side machinery. Coverage: - Three codegen unit tests on synthetic FnDefs pin each compare arm's IR shape (`compare_*_mono_symbol_emits_*`). - Three IR-shape integration tests on the real `examples/compare_primitives_smoke.ail.json` workspace (`compare_*_call_produces_*_mono_fn`) verify the full typecheck → monomorphise → codegen pipeline. - One E2E test `compare_primitives_smoke_compiles_and_runs` exercises the full pipeline through clang + the produced binary, asserting stdout `"1\n2\n3\n1\n2\n3\n1\n2\n3\n"` (LT=1, EQ=2, GT=3 per type × 3 types). - Prelude-load test extended to assert `class Ord` + Ord instances on Int / Bool / Str. Per-task commits: - `` iter 23.3.1: runtime ail_str_compare + IR header declaration - `` iter 23.3.2: codegen — three compare__T intercept arms + emit_ordering_arm helper - `` iter 23.3.3: prelude — class Ord a extends Eq + Ord Int/Bool/Str instances - `` iter 23.3.4: e2e smoke fixture + ir-shape integration tests for compare__T mono symbols - `` iter 23.3.5: journal entry — Ord class + primitive instances (Replace `` with the actual commit SHAs before committing. Use `git log --oneline -5` to retrieve them.) Out of scope for this iter (covered by later 23.x): - Free top-level utility functions `ne` / `lt` / `le` / `gt` / `ge` (23.4). - Float-NoInstance diagnostic + DESIGN.md amendment (23.5). Workspace at iter-23.3 close: full `cargo test --workspace` green. Cross-language and the three regression scripts not re-run for this iter; they run at milestone-23 close. ``` - [ ] **Step 2: Replace `` placeholders with actual SHAs** Run `git log --oneline -5` to retrieve the SHAs of the four prior task commits (23.3.1 through 23.3.4). Edit `docs/JOURNAL.md` to substitute the actual SHAs. (Iter 23.3.5 — this commit — gets its SHA only after the commit lands; leave that one as a placeholder or trim the bullet.) - [ ] **Step 3: Commit** ```bash git add docs/JOURNAL.md git commit -m "iter 23.3.5: journal entry — Ord class + primitive instances" ``` --- ## Self-review 1. **Spec coverage**: Components row 23.3 names five deliverables — (a) `class Ord a extends Eq where compare`, (b) three instances (Ord Int / Bool / Str), (c) extend `runtime/str.c` with `ail_str_compare`, (d) codegen arms for `compare__Int` / `compare__Bool` / `compare__Str`, (e) the match-on-primitive-comparison-result branch ladder. Mapped: (c) in Task 1; (d)+(e) in Task 2; (a)+(b) in Task 3. Coverage tests in Task 4. ✓ 2. **Placeholder scan**: no `TBD`, `TODO`, `similar to`, `implement later`, `add appropriate ...` in the body. The `` placeholders in Task 5 Step 1 are explicit and resolved in Step 2. ✓ 3. **Type consistency**: `compare__Int`, `compare__Bool`, `compare__Str`, `ail_str_compare`, `emit_ordering_arm`, `try_emit_primitive_instance_body`, `Ordering`, `LT/EQ/GT`, `compare_primitives_smoke.ail.json`, `loads_workspace_auto_injects_prelude` — names match across tasks. ✓ 4. **Step granularity**: every step is one shell command or one small code insertion; longest insertions (the three intercept arms in Task 2 Step 5, the fixture JSON in Task 4 Step 3) are each a single Edit / Write. ✓