832375f2ac
All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
1309 lines
48 KiB
Markdown
1309 lines
48 KiB
Markdown
# 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(<_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:
|
||
|
||
- `<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. ✓
|