# 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 <ssa>`. 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(&lt_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(&gt_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(&lt_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(&gt_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(&lt_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(&gt_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:

- `<sha-1>` iter 23.3.1: runtime ail_str_compare + IR header
  declaration
- `<sha-2>` iter 23.3.2: codegen — three compare__T intercept
  arms + emit_ordering_arm helper
- `<sha-3>` iter 23.3.3: prelude — class Ord a extends Eq + Ord
  Int/Bool/Str instances
- `<sha-4>` iter 23.3.4: e2e smoke fixture + ir-shape integration
  tests for compare__T mono symbols
- `<sha-5>` iter 23.3.5: journal entry — Ord class + primitive
  instances

(Replace `<sha-N>` 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 `<sha-N>` 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
   `<sha-N>` 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. ✓
