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.
1417 lines
46 KiB
Markdown
1417 lines
46 KiB
Markdown
# Iter 23.5 — Prelude free fns + E2E — Implementation Plan
|
|
|
|
> **Parent spec:** `docs/specs/0014-23-eq-ord-prelude.md`
|
|
>
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
|
> to run this plan. Steps use `- [ ]` checkboxes for tracking. No
|
|
> task contains a `git commit` step — the implementer never commits
|
|
> (disc.1 discipline). The Boss commits the iter at the end.
|
|
|
|
**Goal:** Ship the five polymorphic free fns `ne`/`lt`/`le`/`gt`/`ge`
|
|
in `examples/prelude.ail.json` plus the three E2E fixtures
|
|
(positive, user-ADT, negative-Float) that close milestone 23.
|
|
|
|
**Architecture:** All work rides on top of the unified mono pass
|
|
shipped in iter 23.4 (commit `a1692a4`). The five free fns are
|
|
polymorphic `Def::Fn`s with class constraints; `monomorphise_workspace`
|
|
already specialises them via the free-fn arm. Bodies are LLM-natural
|
|
expressions: `ne` calls the `not` builtin over `eq`; `lt`/`le`/`gt`/`ge`
|
|
pattern-match on `compare` against `LT`/`GT` plus a wildcard. The
|
|
Float-NoInstance acceptance is a typecheck-time gate — the mono pass
|
|
never runs because constraint resolution fails first.
|
|
|
|
**Tech Stack:**
|
|
|
|
- `examples/prelude.ail.json` — five new top-level `kind: "fn"` entries
|
|
- `examples/eq_ord_polymorphic.ail.json` (new) — positive E2E fixture
|
|
- `examples/eq_ord_user_adt.ail.json` (new) — user-ADT E2E fixture
|
|
- `examples/eq_float_noinstance.ail.json` (new) — negative-Float fixture
|
|
- `crates/ail/tests/prelude_free_fns.rs` (new) — workspace tests per fn
|
|
- `crates/ail/tests/eq_ord_e2e.rs` (new) — three E2E binary-run tests
|
|
- `crates/ail/tests/eq_float_noinstance.rs` (new) — diagnostic pin
|
|
- `crates/ailang-check/src/lib.rs:1607-1613` — Float-aware NoInstance
|
|
message addendum at construction site
|
|
- `docs/DESIGN.md:1702` — §"Decision 11 / Prelude classes" amendment
|
|
- `docs/roadmap.md:44-64` — Post-22 Prelude entry split
|
|
|
|
---
|
|
|
|
## Files this plan creates or modifies
|
|
|
|
**Create:**
|
|
|
|
- `examples/prelude.ail.json` — extend with 5 new `Def::Fn`s (modify, not create)
|
|
- `examples/eq_ord_polymorphic.ail.json` — positive E2E
|
|
- `examples/eq_ord_user_adt.ail.json` — user-ADT E2E
|
|
- `examples/eq_float_noinstance.ail.json` — negative-Float E2E
|
|
- `crates/ail/tests/prelude_free_fns.rs` — workspace tests per fn
|
|
- `crates/ail/tests/eq_ord_e2e.rs` — binary-run E2E tests
|
|
- `crates/ail/tests/eq_float_noinstance.rs` — diagnostic pin
|
|
|
|
**Modify:**
|
|
|
|
- `examples/prelude.ail.json:221` — insertion point for the five free fns is between line 220 (`}` closing last instance) and line 221 (`]` closing `defs`)
|
|
- `crates/ailang-check/src/lib.rs:1607-1613` — Float-aware NoInstance addendum at construction site (current bare-message anchor at `lib.rs:563-566`)
|
|
- `docs/DESIGN.md:1702-1704` — insertion of Milestone-23 amendment paragraph between "see typeclasses spec amendments..." and "Primitive output goes through io/print_int..."
|
|
- `docs/roadmap.md:44-64` — split the Post-22 Prelude entry into shipped Eq/Ord half (`[x]`) and pending Show/print half (`[ ]`)
|
|
|
|
**Test:**
|
|
|
|
- `crates/ail/tests/prelude_free_fns.rs` — 5 functions: `ne_at_int`, `lt_at_int`, `le_at_str`, `gt_at_bool`, `ge_at_int` (and a hash-stability pin for `ne__Int` mirroring `mono_hash_stability.rs:34-41`)
|
|
- `crates/ail/tests/eq_ord_e2e.rs` — 3 functions: `eq_ord_polymorphic_runs_end_to_end`, `eq_ord_user_adt_runs_end_to_end`, `eq_ord_user_adt_eq_intbox_hash_stable`
|
|
- `crates/ail/tests/eq_float_noinstance.rs` — 1 function: `eq_at_float_fires_float_aware_noinstance`
|
|
|
|
---
|
|
|
|
## Task 1: Add `ne` free fn to prelude
|
|
|
|
The simplest free fn: `ne : forall a. Eq a => (a borrow, a borrow) -> Bool`
|
|
with body `not(eq(x, y))`. Uses the `not` builtin (registered at
|
|
`crates/ailang-check/src/builtins.rs:127`, signature `(Bool) -> Bool`).
|
|
Tests `ne` at Int (true and false cases) and pins `ne__Int`'s mono-symbol hash.
|
|
|
|
**Files:**
|
|
|
|
- Modify: `examples/prelude.ail.json:221` (insert before closing `]` of `defs`)
|
|
- Create: `crates/ail/tests/prelude_free_fns.rs` (this task scaffolds the file; later tasks extend it)
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Create `crates/ail/tests/prelude_free_fns.rs`:
|
|
|
|
```rust
|
|
use ailang_core::workspace::load_workspace;
|
|
use ailang_check::{check_workspace, monomorphise_workspace};
|
|
use std::path::PathBuf;
|
|
|
|
fn fixture(name: &str) -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../examples")
|
|
.join(name)
|
|
}
|
|
|
|
fn mono_symbol_names(workspace_name: &str) -> Vec<String> {
|
|
let ws = load_workspace(&fixture(workspace_name)).expect("load");
|
|
let diags = check_workspace(&ws);
|
|
assert!(diags.is_empty(), "typecheck failed: {:#?}", diags);
|
|
let mono = monomorphise_workspace(&ws).expect("mono");
|
|
mono.modules
|
|
.iter()
|
|
.flat_map(|m| m.defs.iter())
|
|
.filter_map(|d| match d {
|
|
ailang_core::ast::Def::Fn(f) => Some(f.name.clone()),
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn ne_at_int_produces_mono_symbol() {
|
|
let names = mono_symbol_names("ne_at_int_smoke.ail.json");
|
|
assert!(
|
|
names.iter().any(|n| n == "ne__Int"),
|
|
"expected ne__Int in workspace, found: {names:#?}"
|
|
);
|
|
}
|
|
```
|
|
|
|
Also create `examples/ne_at_int_smoke.ail.json`:
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "ne_at_int_smoke",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "main",
|
|
"type": {
|
|
"k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" },
|
|
"effects": ["IO"]
|
|
},
|
|
"params": [],
|
|
"body": {
|
|
"t": "do", "op": "io/print_int",
|
|
"args": [{
|
|
"t": "if",
|
|
"cond": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "ne" },
|
|
"args": [
|
|
{ "t": "lit", "lit": { "kind": "int", "value": 3 } },
|
|
{ "t": "lit", "lit": { "kind": "int", "value": 5 } }
|
|
]
|
|
},
|
|
"then": { "t": "lit", "lit": { "kind": "int", "value": 1 } },
|
|
"else": { "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
}]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `cargo test --workspace -p ail --test prelude_free_fns ne_at_int_produces_mono_symbol`
|
|
|
|
Expected: FAIL with "typecheck failed" (the prelude has no `ne` yet so `ne` is unresolved).
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
Edit `examples/prelude.ail.json`. Before the closing `]` of `defs`
|
|
(line 221), insert a new top-level `Def::Fn`:
|
|
|
|
```json
|
|
,
|
|
{
|
|
"kind": "fn",
|
|
"name": "ne",
|
|
"doc": "Polymorphic disequality. `ne x y` ≡ not (eq x y). Ships in milestone 23 as the Eq-class free helper.",
|
|
"type": {
|
|
"k": "forall",
|
|
"vars": ["a"],
|
|
"constraints": [{ "class": "Eq", "type": { "k": "var", "name": "a" } }],
|
|
"body": {
|
|
"k": "fn",
|
|
"params": [
|
|
{ "k": "var", "name": "a" },
|
|
{ "k": "var", "name": "a" }
|
|
],
|
|
"param_modes": ["borrow", "borrow"],
|
|
"ret": { "k": "con", "name": "Bool" },
|
|
"effects": []
|
|
}
|
|
},
|
|
"params": ["x", "y"],
|
|
"body": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "not" },
|
|
"args": [{
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "eq" },
|
|
"args": [
|
|
{ "t": "var", "name": "x" },
|
|
{ "t": "var", "name": "y" }
|
|
]
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
(The leading comma terminates the previous `instance` entry.)
|
|
|
|
- [ ] **Step 4: Run test to verify it passes**
|
|
|
|
Run: `cargo test --workspace -p ail --test prelude_free_fns ne_at_int_produces_mono_symbol`
|
|
|
|
Expected: PASS
|
|
|
|
---
|
|
|
|
## Task 2: Add `lt` / `le` / `gt` / `ge` free fns to prelude
|
|
|
|
Four polymorphic free fns with the same structural pattern:
|
|
`forall a. Ord a => (a borrow, a borrow) -> Bool`, body
|
|
pattern-matches `compare(x, y)` against an `Ordering` ctor and a wildcard.
|
|
The match-term shape mirrors the existing `examples/cmp_max_smoke.ail.json:21-32`
|
|
precedent (proven case-on-compare; bare ctor `LT` works without prelude qualification per `examples/ordering_match.ail.json`).
|
|
|
|
Pattern table:
|
|
|
|
| Fn | Hit-ctor | Hit-body | Wildcard-body |
|
|
|-----|----------|----------|---------------|
|
|
| `lt` | `LT` | `true` | `false` |
|
|
| `le` | `GT` | `false` | `true` |
|
|
| `gt` | `GT` | `true` | `false` |
|
|
| `ge` | `LT` | `false` | `true` |
|
|
|
|
**Files:**
|
|
|
|
- Modify: `examples/prelude.ail.json` (insert four more `Def::Fn`s after `ne`)
|
|
- Modify: `crates/ail/tests/prelude_free_fns.rs` (extend with four tests)
|
|
|
|
- [ ] **Step 1: Write the failing tests**
|
|
|
|
Extend `crates/ail/tests/prelude_free_fns.rs` with:
|
|
|
|
```rust
|
|
#[test]
|
|
fn lt_at_int_produces_mono_symbol() {
|
|
let names = mono_symbol_names("lt_at_int_smoke.ail.json");
|
|
assert!(
|
|
names.iter().any(|n| n == "lt__Int"),
|
|
"expected lt__Int in workspace, found: {names:#?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn le_at_str_produces_mono_symbol() {
|
|
let names = mono_symbol_names("le_at_str_smoke.ail.json");
|
|
assert!(
|
|
names.iter().any(|n| n == "le__Str"),
|
|
"expected le__Str in workspace, found: {names:#?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn gt_at_bool_produces_mono_symbol() {
|
|
let names = mono_symbol_names("gt_at_bool_smoke.ail.json");
|
|
assert!(
|
|
names.iter().any(|n| n == "gt__Bool"),
|
|
"expected gt__Bool in workspace, found: {names:#?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ge_at_int_produces_mono_symbol() {
|
|
let names = mono_symbol_names("ge_at_int_smoke.ail.json");
|
|
assert!(
|
|
names.iter().any(|n| n == "ge__Int"),
|
|
"expected ge__Int in workspace, found: {names:#?}"
|
|
);
|
|
}
|
|
```
|
|
|
|
Create the four corresponding smoke fixtures. Each follows the same
|
|
shape as `ne_at_int_smoke.ail.json` from Task 1, with the
|
|
`fn` name and arg types changed appropriately. For example,
|
|
`examples/lt_at_int_smoke.ail.json`:
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "lt_at_int_smoke",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "main",
|
|
"type": {
|
|
"k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" },
|
|
"effects": ["IO"]
|
|
},
|
|
"params": [],
|
|
"body": {
|
|
"t": "do", "op": "io/print_int",
|
|
"args": [{
|
|
"t": "if",
|
|
"cond": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "lt" },
|
|
"args": [
|
|
{ "t": "lit", "lit": { "kind": "int", "value": 3 } },
|
|
{ "t": "lit", "lit": { "kind": "int", "value": 5 } }
|
|
]
|
|
},
|
|
"then": { "t": "lit", "lit": { "kind": "int", "value": 1 } },
|
|
"else": { "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
}]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
For `le_at_str_smoke.ail.json`, swap `"name": "lt"` → `"name": "le"`
|
|
and the two int literals for two str literals:
|
|
`{ "t": "lit", "lit": { "kind": "str", "value": "abc" } }` and
|
|
`{ "t": "lit", "lit": { "kind": "str", "value": "abd" } }`.
|
|
|
|
For `gt_at_bool_smoke.ail.json`, use `"name": "gt"` and two bool
|
|
literals: `{ "t": "lit", "lit": { "kind": "bool", "value": true } }`
|
|
and `{ "t": "lit", "lit": { "kind": "bool", "value": false } }`.
|
|
|
|
For `ge_at_int_smoke.ail.json`, use `"name": "ge"` and two int
|
|
literals.
|
|
|
|
- [ ] **Step 2: Run tests to verify they fail**
|
|
|
|
Run: `cargo test --workspace -p ail --test prelude_free_fns lt_at_int_produces_mono_symbol`
|
|
|
|
Expected: FAIL with "typecheck failed" (`lt` not yet in prelude).
|
|
|
|
Same for `le_at_str`, `gt_at_bool`, `ge_at_int`. All four FAIL.
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
Edit `examples/prelude.ail.json`. After the `ne` entry from Task 1
|
|
(before the closing `]` of `defs`), insert four more `Def::Fn`s.
|
|
First, `lt`:
|
|
|
|
```json
|
|
,
|
|
{
|
|
"kind": "fn",
|
|
"name": "lt",
|
|
"doc": "Polymorphic strict-less-than. `lt x y` ≡ case compare x y of LT -> True; _ -> False. Ships in milestone 23 as the Ord-class free helper.",
|
|
"type": {
|
|
"k": "forall",
|
|
"vars": ["a"],
|
|
"constraints": [{ "class": "Ord", "type": { "k": "var", "name": "a" } }],
|
|
"body": {
|
|
"k": "fn",
|
|
"params": [
|
|
{ "k": "var", "name": "a" },
|
|
{ "k": "var", "name": "a" }
|
|
],
|
|
"param_modes": ["borrow", "borrow"],
|
|
"ret": { "k": "con", "name": "Bool" },
|
|
"effects": []
|
|
}
|
|
},
|
|
"params": ["x", "y"],
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "compare" },
|
|
"args": [
|
|
{ "t": "var", "name": "x" },
|
|
{ "t": "var", "name": "y" }
|
|
]
|
|
},
|
|
"arms": [
|
|
{
|
|
"pat": { "p": "ctor", "ctor": "LT", "fields": [] },
|
|
"body": { "t": "lit", "lit": { "kind": "bool", "value": true } }
|
|
},
|
|
{
|
|
"pat": { "p": "wild" },
|
|
"body": { "t": "lit", "lit": { "kind": "bool", "value": false } }
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Then `le` (hit-ctor `GT`, hit-body `false`, wild-body `true`):
|
|
|
|
```json
|
|
,
|
|
{
|
|
"kind": "fn",
|
|
"name": "le",
|
|
"doc": "Polymorphic less-than-or-equal. `le x y` ≡ case compare x y of GT -> False; _ -> True.",
|
|
"type": {
|
|
"k": "forall",
|
|
"vars": ["a"],
|
|
"constraints": [{ "class": "Ord", "type": { "k": "var", "name": "a" } }],
|
|
"body": {
|
|
"k": "fn",
|
|
"params": [
|
|
{ "k": "var", "name": "a" },
|
|
{ "k": "var", "name": "a" }
|
|
],
|
|
"param_modes": ["borrow", "borrow"],
|
|
"ret": { "k": "con", "name": "Bool" },
|
|
"effects": []
|
|
}
|
|
},
|
|
"params": ["x", "y"],
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "compare" },
|
|
"args": [
|
|
{ "t": "var", "name": "x" },
|
|
{ "t": "var", "name": "y" }
|
|
]
|
|
},
|
|
"arms": [
|
|
{
|
|
"pat": { "p": "ctor", "ctor": "GT", "fields": [] },
|
|
"body": { "t": "lit", "lit": { "kind": "bool", "value": false } }
|
|
},
|
|
{
|
|
"pat": { "p": "wild" },
|
|
"body": { "t": "lit", "lit": { "kind": "bool", "value": true } }
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Then `gt` (hit-ctor `GT`, hit-body `true`, wild-body `false`):
|
|
|
|
```json
|
|
,
|
|
{
|
|
"kind": "fn",
|
|
"name": "gt",
|
|
"doc": "Polymorphic strict-greater-than. `gt x y` ≡ case compare x y of GT -> True; _ -> False.",
|
|
"type": {
|
|
"k": "forall",
|
|
"vars": ["a"],
|
|
"constraints": [{ "class": "Ord", "type": { "k": "var", "name": "a" } }],
|
|
"body": {
|
|
"k": "fn",
|
|
"params": [
|
|
{ "k": "var", "name": "a" },
|
|
{ "k": "var", "name": "a" }
|
|
],
|
|
"param_modes": ["borrow", "borrow"],
|
|
"ret": { "k": "con", "name": "Bool" },
|
|
"effects": []
|
|
}
|
|
},
|
|
"params": ["x", "y"],
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "compare" },
|
|
"args": [
|
|
{ "t": "var", "name": "x" },
|
|
{ "t": "var", "name": "y" }
|
|
]
|
|
},
|
|
"arms": [
|
|
{
|
|
"pat": { "p": "ctor", "ctor": "GT", "fields": [] },
|
|
"body": { "t": "lit", "lit": { "kind": "bool", "value": true } }
|
|
},
|
|
{
|
|
"pat": { "p": "wild" },
|
|
"body": { "t": "lit", "lit": { "kind": "bool", "value": false } }
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Then `ge` (hit-ctor `LT`, hit-body `false`, wild-body `true`):
|
|
|
|
```json
|
|
,
|
|
{
|
|
"kind": "fn",
|
|
"name": "ge",
|
|
"doc": "Polymorphic greater-than-or-equal. `ge x y` ≡ case compare x y of LT -> False; _ -> True.",
|
|
"type": {
|
|
"k": "forall",
|
|
"vars": ["a"],
|
|
"constraints": [{ "class": "Ord", "type": { "k": "var", "name": "a" } }],
|
|
"body": {
|
|
"k": "fn",
|
|
"params": [
|
|
{ "k": "var", "name": "a" },
|
|
{ "k": "var", "name": "a" }
|
|
],
|
|
"param_modes": ["borrow", "borrow"],
|
|
"ret": { "k": "con", "name": "Bool" },
|
|
"effects": []
|
|
}
|
|
},
|
|
"params": ["x", "y"],
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "compare" },
|
|
"args": [
|
|
{ "t": "var", "name": "x" },
|
|
{ "t": "var", "name": "y" }
|
|
]
|
|
},
|
|
"arms": [
|
|
{
|
|
"pat": { "p": "ctor", "ctor": "LT", "fields": [] },
|
|
"body": { "t": "lit", "lit": { "kind": "bool", "value": false } }
|
|
},
|
|
{
|
|
"pat": { "p": "wild" },
|
|
"body": { "t": "lit", "lit": { "kind": "bool", "value": true } }
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `cargo test --workspace -p ail --test prelude_free_fns`
|
|
|
|
Expected: all 5 tests (`ne_at_int`, `lt_at_int`, `le_at_str`, `gt_at_bool`, `ge_at_int`) PASS.
|
|
|
|
---
|
|
|
|
## Task 3: Positive E2E — `eq_ord_polymorphic`
|
|
|
|
End-to-end test that compiles a workspace with a user-defined
|
|
polymorphic helper invoking the new prelude fns at three primitive
|
|
types (Int / Bool / Str), runs the resulting binary, and pins
|
|
stdout. The helper deliberately combines `lt` + `eq` to exercise
|
|
both Eq and Ord routes through the unified mono pass.
|
|
|
|
Helper signature: `fn at_most : forall a. Ord a => (a borrow, a borrow) -> Bool`
|
|
with body `not (gt x y)` — same as `le x y` but composed from
|
|
prelude fns explicitly (proves composition works, not just direct
|
|
prelude-fn calls).
|
|
|
|
**Files:**
|
|
|
|
- Create: `examples/eq_ord_polymorphic.ail.json`
|
|
- Create: `crates/ail/tests/eq_ord_e2e.rs`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Create `crates/ail/tests/eq_ord_e2e.rs`:
|
|
|
|
```rust
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
fn fixture_path(name: &str) -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../examples")
|
|
.join(name)
|
|
}
|
|
|
|
fn build_and_run(fixture: &str) -> String {
|
|
let src = fixture_path(fixture);
|
|
let out = std::env::temp_dir().join(format!("ail_{}.bin", fixture.replace('.', "_")));
|
|
|
|
let build = Command::new(env!("CARGO_BIN_EXE_ail"))
|
|
.args(["build", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
|
|
.output()
|
|
.expect("ail build");
|
|
assert!(
|
|
build.status.success(),
|
|
"ail build failed:\nstdout: {}\nstderr: {}",
|
|
String::from_utf8_lossy(&build.stdout),
|
|
String::from_utf8_lossy(&build.stderr),
|
|
);
|
|
|
|
let run = Command::new(&out).output().expect("run binary");
|
|
assert!(
|
|
run.status.success(),
|
|
"binary exited non-zero:\nstdout: {}\nstderr: {}",
|
|
String::from_utf8_lossy(&run.stdout),
|
|
String::from_utf8_lossy(&run.stderr),
|
|
);
|
|
String::from_utf8_lossy(&run.stdout).trim().to_string()
|
|
}
|
|
|
|
#[test]
|
|
fn eq_ord_polymorphic_runs_end_to_end() {
|
|
let stdout = build_and_run("eq_ord_polymorphic.ail.json");
|
|
// Helper `at_most(3, 5)` at Int = true → 1
|
|
// `at_most(true, false)` at Bool = false → 0
|
|
// `at_most("abc", "abd")` at Str = true → 1
|
|
// Concatenated via three io/print_int = "101"
|
|
assert_eq!(stdout, "101", "expected 101, got {stdout:?}");
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `cargo test --workspace -p ail --test eq_ord_e2e eq_ord_polymorphic_runs_end_to_end`
|
|
|
|
Expected: FAIL with "ail build failed" (fixture file does not exist yet).
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
Create `examples/eq_ord_polymorphic.ail.json`:
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "eq_ord_polymorphic",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "at_most",
|
|
"doc": "Composition of prelude `gt` with `not` — semantically equivalent to `le`. Used to exercise polymorphic-helper-over-prelude-fns composition through the unified mono pass.",
|
|
"type": {
|
|
"k": "forall",
|
|
"vars": ["a"],
|
|
"constraints": [{ "class": "Ord", "type": { "k": "var", "name": "a" } }],
|
|
"body": {
|
|
"k": "fn",
|
|
"params": [
|
|
{ "k": "var", "name": "a" },
|
|
{ "k": "var", "name": "a" }
|
|
],
|
|
"param_modes": ["borrow", "borrow"],
|
|
"ret": { "k": "con", "name": "Bool" },
|
|
"effects": []
|
|
}
|
|
},
|
|
"params": ["x", "y"],
|
|
"body": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "not" },
|
|
"args": [{
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "gt" },
|
|
"args": [
|
|
{ "t": "var", "name": "x" },
|
|
{ "t": "var", "name": "y" }
|
|
]
|
|
}]
|
|
}
|
|
},
|
|
{
|
|
"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": "if",
|
|
"cond": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "at_most" },
|
|
"args": [
|
|
{ "t": "lit", "lit": { "kind": "int", "value": 3 } },
|
|
{ "t": "lit", "lit": { "kind": "int", "value": 5 } }
|
|
]
|
|
},
|
|
"then": { "t": "lit", "lit": { "kind": "int", "value": 1 } },
|
|
"else": { "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
}]
|
|
},
|
|
"rhs": {
|
|
"t": "seq",
|
|
"lhs": {
|
|
"t": "do", "op": "io/print_int",
|
|
"args": [{
|
|
"t": "if",
|
|
"cond": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "at_most" },
|
|
"args": [
|
|
{ "t": "lit", "lit": { "kind": "bool", "value": true } },
|
|
{ "t": "lit", "lit": { "kind": "bool", "value": false } }
|
|
]
|
|
},
|
|
"then": { "t": "lit", "lit": { "kind": "int", "value": 1 } },
|
|
"else": { "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
}]
|
|
},
|
|
"rhs": {
|
|
"t": "do", "op": "io/print_int",
|
|
"args": [{
|
|
"t": "if",
|
|
"cond": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "at_most" },
|
|
"args": [
|
|
{ "t": "lit", "lit": { "kind": "str", "value": "abc" } },
|
|
{ "t": "lit", "lit": { "kind": "str", "value": "abd" } }
|
|
]
|
|
},
|
|
"then": { "t": "lit", "lit": { "kind": "int", "value": 1 } },
|
|
"else": { "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
NOTE: `"t": "seq"` is binary (`lhs`/`rhs`), not `items`-array. The
|
|
right-nested chain above matches the existing precedent in
|
|
`examples/eq_primitives_smoke.ail.json:34-90` and
|
|
`examples/compare_primitives_smoke.ail.json`. The `io/print_int`
|
|
sequence does NOT print a separator — the three printed ints
|
|
concatenate into one string. Helper to flatten an N-step chain:
|
|
write the first as `lhs`, then the rest as a right-nested `seq` in
|
|
`rhs`.
|
|
|
|
- [ ] **Step 4: Run test to verify it passes**
|
|
|
|
Run: `cargo test --workspace -p ail --test eq_ord_e2e eq_ord_polymorphic_runs_end_to_end`
|
|
|
|
Expected: PASS, stdout "101".
|
|
|
|
---
|
|
|
|
## Task 4: User-ADT E2E — `eq_ord_user_adt`
|
|
|
|
End-to-end test that defines a user ADT `IntBox = MkIntBox Int`, gives
|
|
it user-written `instance Eq IntBox` and `instance Ord IntBox`,
|
|
invokes a polymorphic helper at `IntBox`, and pins both the binary
|
|
stdout and the mono-symbol `eq__IntBox` hash for stability.
|
|
|
|
Verifies that the unified mono pass synthesises `eq__IntBox` from the
|
|
*user* instance body — not from the primitive `==` operator (which has
|
|
no IntBox dispatch). Mirrors the hash-pin pattern from
|
|
`crates/ail/tests/mono_hash_stability.rs:34-41`.
|
|
|
|
**Files:**
|
|
|
|
- Create: `examples/eq_ord_user_adt.ail.json`
|
|
- Modify: `crates/ail/tests/eq_ord_e2e.rs` (add two tests)
|
|
|
|
- [ ] **Step 1: Write the failing tests**
|
|
|
|
Extend `crates/ail/tests/eq_ord_e2e.rs` with:
|
|
|
|
```rust
|
|
use ailang_core::workspace::load_workspace;
|
|
use ailang_check::{check_workspace, monomorphise_workspace};
|
|
use ailang_core::canonical::type_hash;
|
|
use ailang_core::ast::Def;
|
|
|
|
#[test]
|
|
fn eq_ord_user_adt_runs_end_to_end() {
|
|
let stdout = build_and_run("eq_ord_user_adt.ail.json");
|
|
// Helper invoked at IntBox(3) vs IntBox(3) → eq = true → 1
|
|
// Helper invoked at IntBox(3) vs IntBox(5) → eq = false → 0
|
|
assert_eq!(stdout, "10", "expected 10, got {stdout:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn eq_ord_user_adt_eq_intbox_hash_stable() {
|
|
let ws = load_workspace(&fixture_path("eq_ord_user_adt.ail.json")).expect("load");
|
|
let diags = check_workspace(&ws);
|
|
assert!(diags.is_empty(), "typecheck failed: {diags:#?}");
|
|
let mono = monomorphise_workspace(&ws).expect("mono");
|
|
|
|
let eq_intbox = mono.modules.iter()
|
|
.flat_map(|m| m.defs.iter())
|
|
.find_map(|d| match d {
|
|
Def::Fn(f) if f.name == "eq__IntBox" => Some(f),
|
|
_ => None,
|
|
})
|
|
.expect("eq__IntBox not synthesised");
|
|
|
|
// Pin the canonical hash. If this changes, either the user-instance
|
|
// body emission shifted (legitimate refactor — re-record) or the
|
|
// unification accidentally rewired IntBox eq to the wrong source
|
|
// (regression — investigate).
|
|
let h = type_hash(&Def::Fn(eq_intbox.clone()));
|
|
assert_eq!(
|
|
h, "PIN_AFTER_FIRST_GREEN_RUN",
|
|
"eq__IntBox hash drifted — see mono_hash_stability.rs for the resolution pattern"
|
|
);
|
|
}
|
|
```
|
|
|
|
The hash string `"PIN_AFTER_FIRST_GREEN_RUN"` is a deliberate
|
|
placeholder; Step 4 records the actual hash after first green run
|
|
(see Step 4 instruction).
|
|
|
|
- [ ] **Step 2: Run tests to verify they fail**
|
|
|
|
Run: `cargo test --workspace -p ail --test eq_ord_e2e eq_ord_user_adt`
|
|
|
|
Expected: BOTH fail. `eq_ord_user_adt_runs_end_to_end` fails with
|
|
"ail build failed" (fixture missing). `eq_ord_user_adt_eq_intbox_hash_stable`
|
|
fails with "load" panic (same reason).
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
Create `examples/eq_ord_user_adt.ail.json`:
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "eq_ord_user_adt",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "type",
|
|
"name": "IntBox",
|
|
"vars": [],
|
|
"doc": "A one-field box around an Int, used to demonstrate that the unified mono pass synthesises eq__IntBox from a user-written instance, not from the primitive == operator.",
|
|
"ctors": [
|
|
{ "name": "MkIntBox", "fields": [{ "k": "con", "name": "Int" }] }
|
|
]
|
|
},
|
|
{
|
|
"kind": "instance",
|
|
"class": "Eq",
|
|
"type": { "k": "con", "name": "IntBox" },
|
|
"doc": "Eq IntBox by structural unwrap of the inner Int.",
|
|
"methods": [
|
|
{
|
|
"name": "eq",
|
|
"body": {
|
|
"t": "lam",
|
|
"params": ["a", "b"],
|
|
"paramTypes": [
|
|
{ "k": "con", "name": "IntBox" },
|
|
{ "k": "con", "name": "IntBox" }
|
|
],
|
|
"retType": { "k": "con", "name": "Bool" },
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": { "t": "var", "name": "a" },
|
|
"arms": [{
|
|
"pat": { "p": "ctor", "ctor": "MkIntBox", "fields": [{ "p": "var", "name": "ai" }] },
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": { "t": "var", "name": "b" },
|
|
"arms": [{
|
|
"pat": { "p": "ctor", "ctor": "MkIntBox", "fields": [{ "p": "var", "name": "bi" }] },
|
|
"body": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "==" },
|
|
"args": [
|
|
{ "t": "var", "name": "ai" },
|
|
{ "t": "var", "name": "bi" }
|
|
]
|
|
}
|
|
}]
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"kind": "instance",
|
|
"class": "Ord",
|
|
"type": { "k": "con", "name": "IntBox" },
|
|
"doc": "Ord IntBox by delegating to Int's compare on the inner field.",
|
|
"methods": [
|
|
{
|
|
"name": "compare",
|
|
"body": {
|
|
"t": "lam",
|
|
"params": ["a", "b"],
|
|
"paramTypes": [
|
|
{ "k": "con", "name": "IntBox" },
|
|
{ "k": "con", "name": "IntBox" }
|
|
],
|
|
"retType": { "k": "con", "name": "Ordering" },
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": { "t": "var", "name": "a" },
|
|
"arms": [{
|
|
"pat": { "p": "ctor", "ctor": "MkIntBox", "fields": [{ "p": "var", "name": "ai" }] },
|
|
"body": {
|
|
"t": "match",
|
|
"scrutinee": { "t": "var", "name": "b" },
|
|
"arms": [{
|
|
"pat": { "p": "ctor", "ctor": "MkIntBox", "fields": [{ "p": "var", "name": "bi" }] },
|
|
"body": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "compare" },
|
|
"args": [
|
|
{ "t": "var", "name": "ai" },
|
|
{ "t": "var", "name": "bi" }
|
|
]
|
|
}
|
|
}]
|
|
}
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"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": "if",
|
|
"cond": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "eq" },
|
|
"args": [
|
|
{
|
|
"t": "ctor", "type": "eq_ord_user_adt.IntBox", "ctor": "MkIntBox",
|
|
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 3 } }]
|
|
},
|
|
{
|
|
"t": "ctor", "type": "eq_ord_user_adt.IntBox", "ctor": "MkIntBox",
|
|
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 3 } }]
|
|
}
|
|
]
|
|
},
|
|
"then": { "t": "lit", "lit": { "kind": "int", "value": 1 } },
|
|
"else": { "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
}]
|
|
},
|
|
"rhs": {
|
|
"t": "do", "op": "io/print_int",
|
|
"args": [{
|
|
"t": "if",
|
|
"cond": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "eq" },
|
|
"args": [
|
|
{
|
|
"t": "ctor", "type": "eq_ord_user_adt.IntBox", "ctor": "MkIntBox",
|
|
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 3 } }]
|
|
},
|
|
{
|
|
"t": "ctor", "type": "eq_ord_user_adt.IntBox", "ctor": "MkIntBox",
|
|
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 5 } }]
|
|
}
|
|
]
|
|
},
|
|
"then": { "t": "lit", "lit": { "kind": "int", "value": 1 } },
|
|
"else": { "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
NOTE on `Term::Ctor` shape (verified against `ast.rs:436-446`):
|
|
`{ "t": "ctor", "type": "<TypeName>", "ctor": "<CtorName>", "args": [...] }`.
|
|
The `"type"` value may be qualified (`"<module>.<TypeName>"`) or
|
|
unqualified. Qualified form is used above; if the auto-loaded prelude
|
|
or the user-ADT module is in scope, the bare form `"type": "IntBox"`
|
|
also works.
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass + record hash**
|
|
|
|
Run: `cargo test --workspace -p ail --test eq_ord_e2e eq_ord_user_adt_runs_end_to_end`
|
|
|
|
Expected: PASS, stdout "10".
|
|
|
|
Then run: `cargo test --workspace -p ail --test eq_ord_e2e eq_ord_user_adt_eq_intbox_hash_stable`
|
|
|
|
Expected: FAIL with assertion message showing the *actual* hash
|
|
(e.g. `assertion failed: ... actual: "ab12cd34..."`).
|
|
|
|
Extract the actual hash from the failure message, edit
|
|
`crates/ail/tests/eq_ord_e2e.rs` to replace `"PIN_AFTER_FIRST_GREEN_RUN"`
|
|
with the extracted hash, re-run:
|
|
|
|
Run: `cargo test --workspace -p ail --test eq_ord_e2e eq_ord_user_adt_eq_intbox_hash_stable`
|
|
|
|
Expected: PASS.
|
|
|
|
(This is the same record-then-pin pattern used in
|
|
`crates/ail/tests/mono_hash_stability.rs` from iter 23.4.)
|
|
|
|
---
|
|
|
|
## Task 5: Negative Float-NoInstance E2E + diagnostic addendum
|
|
|
|
Add a Float-aware addendum to the `NoInstance` diagnostic message
|
|
emitted at `crates/ailang-check/src/lib.rs:1607-1613`. When the
|
|
unresolved constraint is `Eq Float` or `Ord Float`, append a
|
|
cross-reference to DESIGN.md §"Float semantics" so the LLM author
|
|
immediately learns the partial-Float story. Pin the addended message
|
|
verbatim via a new fixture + test.
|
|
|
|
**Open commitment (spec §336-338):** The exact wording is drafted
|
|
by the implementer in this Step. Constraint: the addendum MUST contain
|
|
the literal substring `Float` (so the message-contains-`Float`
|
|
assertion works regardless of phrasing) and SHOULD cross-reference
|
|
DESIGN.md §"Float semantics".
|
|
|
|
**Files:**
|
|
|
|
- Create: `examples/eq_float_noinstance.ail.json`
|
|
- Create: `crates/ail/tests/eq_float_noinstance.rs`
|
|
- Modify: `crates/ailang-check/src/lib.rs:1607-1613` (NoInstance construction site)
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Create `crates/ail/tests/eq_float_noinstance.rs`:
|
|
|
|
```rust
|
|
use ailang_core::workspace::load_workspace;
|
|
use ailang_check::check_workspace;
|
|
use std::path::PathBuf;
|
|
|
|
fn fixture(name: &str) -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../examples")
|
|
.join(name)
|
|
}
|
|
|
|
#[test]
|
|
fn eq_at_float_fires_float_aware_noinstance() {
|
|
let ws = load_workspace(&fixture("eq_float_noinstance.ail.json")).expect("load");
|
|
let diags = check_workspace(&ws);
|
|
|
|
assert!(!diags.is_empty(), "expected NoInstance diagnostic, got none");
|
|
|
|
let no_inst = diags.iter().find(|d| d.code == "no-instance")
|
|
.expect(&format!("expected diagnostic with code 'no-instance', got: {diags:#?}"));
|
|
|
|
assert!(
|
|
no_inst.message.contains("Float"),
|
|
"expected Float-aware NoInstance diagnostic, got message: {:?}",
|
|
no_inst.message
|
|
);
|
|
|
|
// Cross-ref to DESIGN.md §Float semantics — the LLM-author should
|
|
// be pointed at the canonical explanation of partial Float orderability.
|
|
assert!(
|
|
no_inst.message.contains("Float semantics") || no_inst.message.contains("DESIGN"),
|
|
"expected NoInstance message to cross-reference DESIGN.md §Float semantics, got: {:?}",
|
|
no_inst.message
|
|
);
|
|
}
|
|
```
|
|
|
|
Create `examples/eq_float_noinstance.ail.json`:
|
|
|
|
```json
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "eq_float_noinstance",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "main",
|
|
"type": {
|
|
"k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" },
|
|
"effects": ["IO"]
|
|
},
|
|
"params": [],
|
|
"body": {
|
|
"t": "do", "op": "io/print_int",
|
|
"args": [{
|
|
"t": "if",
|
|
"cond": {
|
|
"t": "app",
|
|
"fn": { "t": "var", "name": "eq" },
|
|
"args": [
|
|
{ "t": "lit", "lit": { "kind": "float", "bits": "3ff0000000000000" } },
|
|
{ "t": "lit", "lit": { "kind": "float", "bits": "4000000000000000" } }
|
|
]
|
|
},
|
|
"then": { "t": "lit", "lit": { "kind": "int", "value": 1 } },
|
|
"else": { "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
}]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
(Float literal shape `{ "kind": "float", "bits": "<hex-IEEE754>" }`
|
|
verified against `examples/floats.ail.json:26-27`. `3ff0000000000000`
|
|
= 1.0; `4000000000000000` = 2.0. Use raw hex IEEE 754 bits, not a
|
|
decimal value — the schema requires bit-stable hex form.)
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `cargo test --workspace -p ail --test eq_float_noinstance eq_at_float_fires_float_aware_noinstance`
|
|
|
|
Expected: FAIL with "expected Float-aware NoInstance diagnostic, got message:
|
|
'fn `main` calls `eq` requiring `instance Eq Float`, but no such instance exists in the workspace'"
|
|
— the current bare message has no "Float semantics" reference.
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
Modify `crates/ailang-check/src/lib.rs:1607-1613` (the `NoInstance`
|
|
construction site for `CheckError::NoInstance`).
|
|
|
|
The current construction emits the bare `thiserror`-templated message
|
|
from `lib.rs:563-566`:
|
|
|
|
```
|
|
"fn `{def}` calls `{method}` requiring `instance {class} {at_type}`, but no such instance exists in the workspace"
|
|
```
|
|
|
|
Refactor the construction site to append a Float-aware addendum
|
|
when the unresolved type is `Float` AND the class is `Eq` or `Ord`.
|
|
Pseudocode (the implementer adapts to the actual local variable
|
|
names at lib.rs:1607-1613):
|
|
|
|
```rust
|
|
let mut diag = Diagnostic::from(CheckError::NoInstance {
|
|
def: def_name.clone(),
|
|
method: method_name.clone(),
|
|
class: class_name.clone(),
|
|
at_type: at_type_surface.clone(),
|
|
});
|
|
|
|
// Float-aware addendum (iter 23.5): when the unresolved instance
|
|
// is on Float, point the LLM-author at the canonical Float
|
|
// semantics so they immediately learn the partial-Float story.
|
|
let is_float = matches!(&r_ty, Type::Con { name, .. } if name == "Float");
|
|
let is_eq_or_ord = class_name == "Eq" || class_name == "Ord";
|
|
if is_float && is_eq_or_ord {
|
|
diag.message = format!(
|
|
"{} — Float has no Eq/Ord instance by design; see DESIGN.md §\"Float semantics\".",
|
|
diag.message
|
|
);
|
|
}
|
|
|
|
diagnostics.push(diag);
|
|
```
|
|
|
|
Two structural choices the implementer must make:
|
|
|
|
1. **Mutating `diag.message` after construction.** If the `Diagnostic`
|
|
struct is constructed via `Display`-on-`CheckError` (and `message`
|
|
is therefore not directly assignable), the implementer either:
|
|
(a) makes `message` a public mutable field, or
|
|
(b) introduces a new `CheckError::NoInstanceFloat { ... }` variant
|
|
with its own `#[error(...)]` template and selects between
|
|
`NoInstance` / `NoInstanceFloat` at construction time.
|
|
Option (a) is the simpler diff; option (b) is the more typed/structural
|
|
solution. Either is acceptable — pick based on what the codebase
|
|
already does elsewhere for "diagnostic with situational addendum".
|
|
|
|
2. **Exact wording.** Constraint: must contain the substring "Float"
|
|
AND must contain either "Float semantics" or "DESIGN" (for the
|
|
cross-reference assertion). Beyond that, the implementer drafts.
|
|
|
|
- [ ] **Step 4: Run test to verify it passes**
|
|
|
|
Run: `cargo test --workspace -p ail --test eq_float_noinstance eq_at_float_fires_float_aware_noinstance`
|
|
|
|
Expected: PASS.
|
|
|
|
Also run the full workspace check to confirm no existing test relied
|
|
on the bare NoInstance message:
|
|
|
|
Run: `cargo test --workspace`
|
|
|
|
Expected: all pre-existing tests still PASS. If any existing test
|
|
asserted on a NoInstance message verbatim (none found in recon, but
|
|
possible), update its expected substring to match the new wording.
|
|
|
|
---
|
|
|
|
## Task 6: DESIGN.md amendment — §"Decision 11 / Prelude classes"
|
|
|
|
Amend `docs/DESIGN.md:1690-1742` to record that Eq/Ord shipped in
|
|
milestone 23. The "Milestone 22 ships no built-in Prelude classes"
|
|
wording (line 1692) stays as historical accuracy; the amendment is
|
|
inserted as a new paragraph between line 1702 (end of "see typeclasses
|
|
spec amendments...") and line 1704 (start of "Primitive output goes
|
|
through io/print_int..."), mirroring the tone of the iter-23.4
|
|
DESIGN.md amendment at `DESIGN.md:1513-1517`.
|
|
|
|
**Files:**
|
|
|
|
- Modify: `docs/DESIGN.md:1702` (insertion point)
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Pure-prose change; no Rust test is meaningful here. Substitute a
|
|
grep-based assertion: `git grep -l "Milestone 23 amends" docs/DESIGN.md`
|
|
must return `docs/DESIGN.md` after the edit.
|
|
|
|
Run: `git grep -l 'Milestone 23 amends' docs/DESIGN.md`
|
|
|
|
Expected (pre-edit): no output (exit 1).
|
|
|
|
- [ ] **Step 2: Verify the grep currently fails**
|
|
|
|
Same command as Step 1. Confirm empty output. (This is the "RED" state.)
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
Edit `docs/DESIGN.md`. After line 1702 (the line ending with
|
|
`"... see `docs/specs/0002-22-typeclasses.md` 'Amendments' for the substantive rationale."`),
|
|
insert two blank lines and the following paragraph block:
|
|
|
|
```markdown
|
|
Milestone 23 amends the above: the prelude now ships the `Ordering`
|
|
ADT, the `Eq` and `Ord` classes, primitive `Eq Int/Bool/Str` and
|
|
`Ord Int/Bool/Str` instances, and the five polymorphic free-fn helpers
|
|
`ne` / `lt` / `le` / `gt` / `ge`. `Show`, operator routing through
|
|
`Eq`/`Ord`, and `print`-rewire remain out of scope per their original
|
|
substantive reasons (heap-`Str` ABI, bench rebaseline). Float has
|
|
neither `Eq` nor `Ord` instance per §"Float semantics"; a polymorphic
|
|
helper invoked at Float fires `NoInstance` at typecheck with a
|
|
Float-aware diagnostic cross-referencing this section.
|
|
```
|
|
|
|
Leave line 1704 onwards (`Primitive output goes through io/print_int...`)
|
|
unchanged — it remains accurate after the amendment.
|
|
|
|
- [ ] **Step 4: Verify the grep now passes**
|
|
|
|
Run: `git grep -l 'Milestone 23 amends' docs/DESIGN.md`
|
|
|
|
Expected: `docs/DESIGN.md` (exit 0).
|
|
|
|
Visual-review the inserted paragraph for prose quality; fix any
|
|
typo or awkward phrasing inline.
|
|
|
|
---
|
|
|
|
## Task 7: Roadmap update — split Post-22 Prelude entry
|
|
|
|
Edit `docs/roadmap.md:44-64`. Per spec acceptance §8 and recon
|
|
finding (split is preferred over single-entry-checked): split the
|
|
existing single `[ ] Post-22 Prelude — ship Show/Eq/Ord ...` entry
|
|
into two adjacent entries — one `[x]` for the shipped Eq/Ord half,
|
|
one `[ ]` for the pending Show + print rewire half.
|
|
|
|
**Files:**
|
|
|
|
- Modify: `docs/roadmap.md:44-64`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
Grep-based assertion: post-edit, `docs/roadmap.md` must contain both
|
|
`[x] **\[milestone\]** Post-22 Prelude — Eq/Ord` AND
|
|
`[ ] **\[milestone\]** Post-22 Prelude — Show + print rewire`.
|
|
|
|
Run: `git grep -c 'Post-22 Prelude' docs/roadmap.md`
|
|
|
|
Expected (pre-edit): `1` (the single existing entry).
|
|
|
|
- [ ] **Step 2: Verify the count is currently 1**
|
|
|
|
Same command. Confirm output `1`.
|
|
|
|
- [ ] **Step 3: Write minimal implementation**
|
|
|
|
Edit `docs/roadmap.md`. Replace the entire block from line 44 through
|
|
line 64 (the existing single Post-22 Prelude entry with all its
|
|
sub-bullets) with the following two-entry block:
|
|
|
|
```markdown
|
|
- [x] **\[milestone\]** Post-22 Prelude — Eq/Ord — shipped milestone
|
|
23 (2026-05-12). `Ordering` ADT, `Eq`/`Ord` classes, primitive
|
|
instances on Int / Bool / Str, polymorphic free fns
|
|
`ne`/`lt`/`le`/`gt`/`ge`, runtime `ail_str_eq` + `ail_str_compare`,
|
|
end-to-end coverage including user-ADT-instance integration. Float
|
|
has neither instance by design; polymorphic helpers at Float fire
|
|
Float-aware `NoInstance` per DESIGN.md §"Float semantics".
|
|
|
|
- [ ] **\[milestone\]** Post-22 Prelude — Show + print rewire — ship
|
|
`Show` typeclass with `Show Int/Bool/Str/Float` instances; rewire
|
|
`print` through `Show.show`. Gated on heap-`Str` ABI runtime work
|
|
(`int_to_str` needs to allocate). Originally queued as 22b.4 in
|
|
the typeclass milestone, kept on hold pending demand and the
|
|
heap-`Str` prerequisite.
|
|
- context: brainstorm 2026-05-12 (iter 23.5 wrap). Heap-`Str` ABI
|
|
is the load-bearing prerequisite; until it ships, `Show Int`
|
|
cannot allocate a `Str` to return.
|
|
```
|
|
|
|
- [ ] **Step 4: Verify the count is now 2**
|
|
|
|
Run: `git grep -c 'Post-22 Prelude' docs/roadmap.md`
|
|
|
|
Expected: `2` (the two split entries).
|
|
|
|
Also confirm one is checked and one is not:
|
|
|
|
Run: `git grep '\[x\] \*\*\\\[milestone\\\]\*\* Post-22 Prelude — Eq/Ord' docs/roadmap.md`
|
|
|
|
Expected: exactly one match.
|
|
|
|
Run: `git grep '\[ \] \*\*\\\[milestone\\\]\*\* Post-22 Prelude — Show' docs/roadmap.md`
|
|
|
|
Expected: exactly one match.
|
|
|
|
---
|
|
|
|
## Task 8: Bench-regression check
|
|
|
|
Final gate before the iter is handed back to the Boss. The three
|
|
bench scripts must exit 0 (no regression) or be audit-ratified per
|
|
spec §248-249. Same gate iter 23.4 cleared (a1692a4) — same scripts.
|
|
|
|
**Files:**
|
|
|
|
- (no edits — verification only)
|
|
|
|
- [ ] **Step 1: Establish the gate**
|
|
|
|
The "test" for this task is the exit code of the three bench scripts.
|
|
No additional RED-state setup is needed; we simply run them.
|
|
|
|
- [ ] **Step 2: Run baseline check (pre-mono-extension)**
|
|
|
|
Run: `python3 bench/check.py`
|
|
|
|
Expected: exit 0.
|
|
|
|
Run: `python3 bench/compile_check.py`
|
|
|
|
Expected: exit 0.
|
|
|
|
Run: `python3 bench/cross_lang.py`
|
|
|
|
Expected: exit 0.
|
|
|
|
If any of the three exits non-zero with regression diagnostics,
|
|
inspect the regression: is it caused by the prelude growing by
|
|
five `Def::Fn`s (expected — the mono pass now has more work per
|
|
workspace that uses the prelude)? If so, the regression is
|
|
audit-ratifiable per spec §248-249. Record the regression magnitude
|
|
in the journal Concerns section; the Boss decides whether to
|
|
rebaseline at iter close.
|
|
|
|
- [ ] **Step 3: Verify cross-language ratio still holds**
|
|
|
|
`bench/cross_lang.py` compares AILang against the C reference. If
|
|
the ratio shifted measurably, note it in journal Concerns; if the
|
|
shift is within audit tolerance, no action needed.
|
|
|
|
- [ ] **Step 4: Mark gate cleared**
|
|
|
|
In the per-iter journal (written by the orchestrator in Phase 4),
|
|
record per-script exit codes in the `Stats` section.
|
|
|
|
---
|
|
|
|
## Self-review checklist
|
|
|
|
(Run by the planner before handoff; results recorded inline.)
|
|
|
|
1. **Spec coverage:**
|
|
- Acceptance §3 (prelude free fns) → Tasks 1 + 2 ✓
|
|
- Acceptance §4 (DESIGN.md Decision 11 amendment) → Task 6 ✓
|
|
- Acceptance §5 (workspace tests + E2E green) → Tasks 1, 2, 3, 4 ✓
|
|
- Acceptance §6 (bench regression ratified) → Task 8 ✓
|
|
- Acceptance §7 (polymorphic helper at three primitives + Float-NoInstance) → Tasks 3 + 5 ✓
|
|
- Acceptance §8 (roadmap update) → Task 7 ✓
|
|
|
|
2. **Placeholder scan:**
|
|
- No "TBD" / "TODO" / "implement later" / "fill in later".
|
|
- The one `"PIN_AFTER_FIRST_GREEN_RUN"` placeholder in Task 4 is
|
|
deliberate (record-then-pin pattern from `mono_hash_stability.rs`)
|
|
and Step 4 of Task 4 instructs the implementer to replace it
|
|
with the actual hash. This is not a plan failure — it's the
|
|
standard hash-pin procedure.
|
|
|
|
3. **Type consistency:**
|
|
- `mono_symbol_names` helper defined in Task 1, reused implicitly
|
|
by Task 2's tests (same file). ✓
|
|
- `build_and_run` / `fixture_path` helpers defined in Task 3,
|
|
reused by Task 4's tests (same file). ✓
|
|
- All fixture filenames match between fixture-creation and test-load. ✓
|
|
|
|
4. **Step granularity:**
|
|
- Every step is 2-5 minutes. Task 4 Step 3 (the user-ADT fixture
|
|
JSON) is the longest; it is one logical action (write the
|
|
fixture JSON) of ~5 minutes typing.
|
|
|
|
5. **No commit steps:** confirmed — no task contains `git commit`.
|
|
|
|
## Handoff
|
|
|
|
Plan file: `docs/plans/0035-iter-23.5.md`.
|
|
|
|
Hand off to `skills/implement` with: this plan path. Boss commits the
|
|
plan with subject `plan: iter 23.5 prelude free fns + E2E — 8 tasks`
|
|
before dispatching the implement orchestrator.
|