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.
1308 lines
71 KiB
Markdown
1308 lines
71 KiB
Markdown
# operator-routing-eq-ord.1 — Implementation Plan
|
|
|
|
> **Parent spec:** `docs/specs/0049-operator-routing-eq-ord.md`
|
|
>
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
|
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
|
|
|
**Goal:** Drop the surface comparator names `==` / `!=` / `<` / `<=` / `>` / `>=` from the language, route the LLM-author's `(app eq …)` / `(app compare …)` / `(app ne|lt|le|gt|ge …)` through the existing `prelude.Eq` / `prelude.Ord` class-dispatch path, ship six named Float-comparison fns (`float_eq` / `float_ne` / `float_lt` / `float_le` / `float_gt` / `float_ge`) so Float keeps comparability without an `Eq Float` / `Ord Float` instance, and emit primitive instance bodies with `alwaysinline` so the `-O0` IR shape stays at one instruction per comparison.
|
|
|
|
**Architecture:** One cohesive iteration across seven layers — typchecker builtins, codegen builtin-binop table, codegen primitive-instance intercept (extended with three new Eq arms + six new float-fn arms + `alwaysinline` attribute emission), codegen `==`-polymorphic-dispatch path (`lower_eq` whole fn) deleted, lit-pattern desugar (`build_eq` substitutes `eq`), prelude module (Eq Unit added, six float-fns added, primitive instance bodies become placeholders), and full migration of 60 fixtures + 8 in-source `#[cfg(test)] mod tests` AST-literal scaffolds + 3 hash-pin re-pins + 2 IR-snapshot regenerations + 5 contract-file updates.
|
|
|
|
**Tech Stack:** `crates/ailang-check` (typechecker, in-source mod tests), `crates/ailang-codegen` (intercept + fn-attribute emission + `lower_eq` deletion), `crates/ailang-core` (desugar), `crates/ailang-prose` (binop_info cleanup), `examples/prelude.ail` (canonical class declarations), full E2E test suite (`crates/ail/tests/`), bench corpus (`bench/check.py` / `compile_check.py` / `cross_lang.py`).
|
|
|
|
---
|
|
|
|
**Files this plan creates or modifies:**
|
|
|
|
- Create: `examples/eq_user_adt_smoke.ail` — north-star fixture (Point ADT + user `instance Eq Point` + Unit-eq line to ensure RED today)
|
|
- Create: `examples/eq_float_must_fail.ail` — Klausel-3 discriminator (must fail at typecheck with `NoInstance Eq Float` + `float_eq` hint)
|
|
- Create: `examples/float_compare_smoke.ail` — Float named-fn happy path
|
|
- Create: `examples/operator_unbound_check.ail` — minimal `(app == 5 5)` fixture for the unbound-pin
|
|
- Create: `crates/ail/tests/eq_user_adt_smoke_e2e.rs` — drives the north-star, asserts `"true\ntrue\nfalse\n"` (Unit, then Point eq, then Point neq)
|
|
- Create: `crates/ail/tests/eq_float_must_fail_pin.rs` — drives `ail check`, asserts non-zero + `NoInstance Eq Float` AND `float_eq` substrings
|
|
- Create: `crates/ail/tests/float_compare_smoke_e2e.rs` — drives the float-named-fn fixture, asserts `"true\ntrue\nfalse\n"`
|
|
- Create: `crates/ail/tests/operator_names_unbound_pin.rs` — drives `(app == 5 5)`, asserts `ail check` exits non-zero with stderr containing `unknown variable: ==`
|
|
- Create: `crates/ail/tests/prelude_eq_alwaysinline_ir_pin.rs` — compiles with `--emit-ir --opt=O0`; asserts the generated `define` for `@ail_prelude_eq__Int` carries the `alwaysinline` attribute
|
|
- Modify: `examples/prelude.ail:9` — drop "P2 follow-up" comment on `class Eq`
|
|
- Modify: `examples/prelude.ail:12-29` — `Eq Int` / `Eq Bool` / `Eq Str` instance bodies become placeholder shape (`(body false)` lambda); doc-comments updated
|
|
- Modify: `examples/prelude.ail` (insert after Eq Str instance, before Ord) — new `instance Eq Unit`
|
|
- Modify: `examples/prelude.ail` (insert after `print` fn, before final paren) — six new `float_*` free fns (`float_eq`/`float_ne`/`float_lt`/`float_le`/`float_gt`/`float_ge`)
|
|
- Modify: `crates/ailang-check/src/builtins.rs:96-98` — delete the `["!=", "<", "<=", ">", ">="]` comparator-loop install
|
|
- Modify: `crates/ailang-check/src/builtins.rs:100-125` — delete `==` polymorphic Forall block + its 6-line comment band
|
|
- Modify: `crates/ailang-check/src/builtins.rs:303-308` — delete the 6 list-side mirror rows for `==` / `!=` / `<` / `<=` / `>` / `>=`
|
|
- Modify: `crates/ailang-codegen/src/synth.rs:263-267` — delete the 5 Int comparator arms in `builtin_binop_typed`
|
|
- Modify: `crates/ailang-codegen/src/synth.rs:268-276` — delete the 5 Float comparator arms (`fcmp une`/`olt`/`ole`/`ogt`/`oge`); the comment band at 260-262 + 268-271 rewrites to "arithmetic-only" framing
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:68-73` — `is_arithmetic_or_comparison_op` reduces to arithmetic-only; rename to `is_arithmetic_op` + update doc-comment lines 60-67
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:2133` — delete the `if name == "=="` short-circuit in `lower_app`
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:2529` — delete the `name == "=="` clause in `is_static_callee`
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:2879-2964` — delete the entire `lower_eq` fn
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:1213-1259` — `emit_fn` gains conditional `alwaysinline` attribute emission on the `define` line for symbols that match the alwaysinline-allowlist
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:2691-2806` — `try_emit_primitive_instance_body` gains 9 new arms (`eq__Int`, `eq__Bool`, `eq__Unit`, `float_eq`, `float_ne`, `float_lt`, `float_le`, `float_gt`, `float_ge`)
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:3263-end` — in-source `mod tests` AST-literal sites at lines 3371, 3419, 3729-3734, 4354 migrate from `==` to `eq` (or delete tests whose semantics are tied to the now-deleted `lower_eq` rejection wording)
|
|
- Modify: `crates/ailang-core/src/desugar.rs:1099` — `build_eq` function emits `Term::Var { name: "eq" }` instead of `"=="` at line 1109; doc-comment at 1098 updated
|
|
- Modify: `crates/ailang-core/src/desugar.rs:2414` — in-source `mod tests` AST-literal migrates from `==` to `eq`
|
|
- Modify: `crates/ailang-check/src/lib.rs:856-880` — Float-aware NoInstance addendum extended to name `float_eq` / `float_lt` as the explicit alternative
|
|
- Modify: `crates/ailang-check/src/lib.rs:5656` — in-source `mod tests` AST-literal migrates from `>=` to `ge`
|
|
- Modify: `crates/ailang-check/src/lib.rs:6092` — in-source `mod tests` AST-literal migrates from `==` to `eq` (the `eq_app` helper at 6088 and callers at 6097-6128)
|
|
- Modify: `crates/ailang-check/src/lib.rs:6220` — second in-source `mod tests` AST-literal migrates from `==` to `eq`
|
|
- Modify: `crates/ailang-check/src/builtins.rs:413-432` — in-source `mod tests` `<` smoke tests migrate to `lt` (or delete if redundant with class-dispatch coverage)
|
|
- Modify: 60 fixtures under `examples/` — full bulk migration of `(app == …)` → `(app eq …)`, `(app != …)` → `(app ne …)`, `(app < …)` → `(app lt …)`, etc. Float-typed operand sites migrate to `(app float_eq …)` / `(app float_lt …)` / etc. The grep `grep -rln '(app == \|(app != \|(app < \|(app <= \|(app > \|(app >=' examples/` enumerates the live set; expected count drops from 60 to 0.
|
|
- Modify: `examples/eq_ord_user_adt.ail:21` — `(app == ai bi)` → `(app eq ai bi)` (lockstep with `crates/ail/tests/eq_ord_e2e.rs:134` body-hash re-pin)
|
|
- Modify: `examples/eq_demo.ail:1-23` — doc-comment header rewrites from the polymorphic-`==` story to the class-dispatch story
|
|
- Modify: `crates/ail/tests/e2e.rs:1351-1360` — `eq_demo` test doc-comment rewrites alongside the fixture
|
|
- Modify: `crates/ailang-surface/tests/prelude_module_hash_pin.rs:16` — regenerate prelude module hash literal
|
|
- Modify: `crates/ail/tests/mono_hash_stability.rs:50-55` — re-pin six body-hash literals for `eq__Int`/`eq__Bool`/`eq__Str`/`compare__Int`/`compare__Bool`/`compare__Str`
|
|
- Modify: `crates/ail/tests/eq_ord_e2e.rs:134` — re-pin `eq__IntBox` body-hash literal (`3c4cf040cb4e8bb2` → new value)
|
|
- Modify: `crates/ail/tests/eq_float_noinstance.rs:32-44` — assertion extended to include `float_eq` substring in the diagnostic
|
|
- Modify: `crates/ail/tests/snapshots/sum.ll` — regenerate via `UPDATE_SNAPSHOTS=1` (sum.ail uses `==` in lit-pattern desugar)
|
|
- Modify: `crates/ail/tests/snapshots/max3.ll` — regenerate via `UPDATE_SNAPSHOTS=1` (max3.ail uses `>` directly)
|
|
- Modify: `crates/ailang-prose/src/lib.rs:546-563` — delete the 6 comparator arms from `binop_info` (dead code after operator-name removal)
|
|
- Modify: `crates/ailang-prose/src/lib.rs:1952, 2020-2062` — delete the 3 in-source `mod tests` that pinned comparator-infix rendering
|
|
- Modify: `design/contracts/0005-float-semantics.md:10-31, :98-99` — transfer comparison guarantees from `==` / `<` / `!=` to `float_eq` / `float_lt` / `float_ne` (arithmetic guarantees on `+` / `-` / `*` / `/` unchanged); `==` / `<` / `!=` mentions removed from the present-tense surface description
|
|
- Modify: `design/contracts/0017-prelude-classes.md:5-13` — instance list extends with `Eq Unit`; new paragraph on the six `float_*` fns; Float-no-Eq/Ord clause gains `→ use float_eq` cross-reference
|
|
- Modify: `design/contracts/0011-str-abi.md:40-42` — rewrite "`==`, `<`, … REMAIN primitive operators" clause to describe the class-method dispatch path
|
|
- Modify: `design/contracts/0010-scope-boundaries.md:42-96` — operator-name mentions and `Pattern::Lit`-desugar-to-`==` clauses rewrite to the class-dispatch present
|
|
- Modify: `design/models/0001-authoring-surface.md:58` — drop `==` / `<=` from the operator-example list; the example becomes arithmetic-only or names class methods explicitly
|
|
|
|
---
|
|
|
|
## Per-task expected-state map
|
|
|
|
After each task lands, the workspace test suite is in a known partial-pass state. Task 12 (acceptance gate) is the only "all green" milestone.
|
|
|
|
| After Task | New tests green? | Existing tests still green? | Notes |
|
|
|---|---|---|---|
|
|
| 1 (bootstrap) | none (all RED) | yes | New fixtures + RED tests added; no behaviour shift |
|
|
| 2 (codegen intercept + alwaysinline) | `prelude_eq_alwaysinline_ir_pin` GREEN | yes | Intercept overrides today's `(app == x y)` body for primitive instances; output unchanged |
|
|
| 3 (prelude reshape + hash re-pin) | none new | yes | Primitive bodies become placeholders; intercept does the work; pins re-pinned |
|
|
| 4 (fixture migration) | none new | yes (all `eq` dispatches today) | All 60 fixtures use `eq`/`lt`/etc.; `==` no longer in any fixture |
|
|
| 5 (test-scaffold migration) | none new | yes | In-source mod tests use `eq` in AST literals |
|
|
| 6 (lit-pattern desugar) | none new | yes | `build_eq` emits `eq`; lit-pattern paths route through class-dispatch |
|
|
| 7 (delete dead operator machinery) | `operator_names_unbound_pin` GREEN | yes (all callers migrated by tasks 4-6) | `==` / `<` / etc. become unknown identifiers |
|
|
| 8 (Float NoInstance hint + assertion extend) | `eq_float_must_fail_pin` GREEN | yes (existing `eq_float_noinstance` assertion extended) | Diagnostic mentions `float_eq` |
|
|
| 9 (IR snapshot regeneration) | none new | yes | `sum.ll` / `max3.ll` regenerated |
|
|
| 10 (prose cleanup) | none new | yes | Dead binop_info arms + tests deleted |
|
|
| 11 (contract updates) | none new | yes | Honesty-rule compliance for 5 design files |
|
|
| 12 (acceptance gate) | `eq_user_adt_smoke_e2e`, `float_compare_smoke_e2e` GREEN (Unit-eq path needs Task 2's intercept arm + Task 3's prelude instance; Float-fn path needs both same; user-Point path is already covered by today's eq dispatch — see TDD note in Task 1) | yes | Full workspace `0 failed`; bench corpus exit 0 |
|
|
|
|
---
|
|
|
|
## Task 1: Bootstrap — RED tests + new fixtures
|
|
|
|
**Files:**
|
|
- Create: `examples/eq_user_adt_smoke.ail` (north-star)
|
|
- Create: `examples/eq_float_must_fail.ail` (Klausel-3 discriminator)
|
|
- Create: `examples/float_compare_smoke.ail` (Float happy path)
|
|
- Create: `examples/operator_unbound_check.ail` (unbound-`==` fixture)
|
|
- Create: `crates/ail/tests/eq_user_adt_smoke_e2e.rs`
|
|
- Create: `crates/ail/tests/eq_float_must_fail_pin.rs`
|
|
- Create: `crates/ail/tests/float_compare_smoke_e2e.rs`
|
|
- Create: `crates/ail/tests/operator_names_unbound_pin.rs`
|
|
- Create: `crates/ail/tests/prelude_eq_alwaysinline_ir_pin.rs`
|
|
|
|
- [ ] **Step 1: Write the north-star fixture**
|
|
|
|
Create `examples/eq_user_adt_smoke.ail`:
|
|
|
|
```
|
|
(module eq_user_adt_smoke
|
|
(data Point
|
|
(doc "Two-Int-field record. The milestone-defining north-star: an LLM author writes `instance Eq Point` and calls `(app eq p1 p2)` end-to-end through class-dispatch.")
|
|
(ctor Point (con Int) (con Int)))
|
|
(instance
|
|
(class prelude.Eq)
|
|
(type (con Point))
|
|
(doc "Eq Point by structural comparison of the two Int fields. The nested `(app eq a1 a2)` calls dispatch to prelude.Eq.eq's Int instance.")
|
|
(method eq
|
|
(body (lam (params (typed p1 (con Point)) (typed p2 (con Point))) (ret (con Bool)) (body (match p1
|
|
(case (pat-ctor Point a1 b1) (match p2
|
|
(case (pat-ctor Point a2 b2) (if (app eq a1 a2) (app eq b1 b2) false))))))))))
|
|
(fn main
|
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
|
(params)
|
|
(body
|
|
(let p1 (term-ctor Point 1 2)
|
|
(let p2 (term-ctor Point 1 2)
|
|
(let p3 (term-ctor Point 1 3)
|
|
(seq (app print (app eq (lit-unit) (lit-unit)))
|
|
(seq (app print (app eq p1 p2))
|
|
(app print (app eq p1 p3)))))))))
|
|
```
|
|
|
|
- [ ] **Step 2: Write the north-star E2E test**
|
|
|
|
Create `crates/ail/tests/eq_user_adt_smoke_e2e.rs`:
|
|
|
|
```rust
|
|
//! North-star E2E for the operator-routing-eq-ord milestone:
|
|
//! exercises Eq Unit (new instance), user-ADT Eq Point with a
|
|
//! hand-written instance, and nested-Int Eq dispatch — all
|
|
//! through the class-method path with no `==` callee anywhere.
|
|
//!
|
|
//! Today this test fires `NoInstance Eq Unit` at typecheck
|
|
//! (Unit lacks an Eq instance), proving RED-first before the
|
|
//! milestone lands.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
#[test]
|
|
fn eq_user_adt_smoke_prints_unit_then_point_eq_then_neq() {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
let repo_root = PathBuf::from(manifest_dir).join("../..");
|
|
let fixture = repo_root.join("examples/eq_user_adt_smoke.ail");
|
|
let bin = repo_root.join("target/debug/eq_user_adt_smoke_test_bin");
|
|
|
|
let build = Command::new(env!("CARGO_BIN_EXE_ail"))
|
|
.arg("build")
|
|
.arg(&fixture)
|
|
.arg("-o")
|
|
.arg(&bin)
|
|
.output()
|
|
.expect("ail build failed to spawn");
|
|
assert!(
|
|
build.status.success(),
|
|
"ail build failed: stderr={}",
|
|
String::from_utf8_lossy(&build.stderr)
|
|
);
|
|
|
|
let run = Command::new(&bin)
|
|
.output()
|
|
.expect("binary failed to spawn");
|
|
assert!(run.status.success(), "binary failed: stderr={}", String::from_utf8_lossy(&run.stderr));
|
|
assert_eq!(
|
|
String::from_utf8_lossy(&run.stdout).as_ref(),
|
|
"true\ntrue\nfalse\n",
|
|
"expected Unit-eq then Point-eq then Point-neq"
|
|
);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Write the must-fail fixture**
|
|
|
|
Create `examples/eq_float_must_fail.ail`:
|
|
|
|
```
|
|
(module eq_float_must_fail
|
|
(fn main
|
|
(doc "Klausel-3 discriminator. After operator-routing-eq-ord, `(app eq 1.5 1.5)` must fire `NoInstance Eq Float` with a follow-up sentence naming `float_eq` as the explicit IEEE-aware alternative. Today the diagnostic already fires (no Eq Float instance) but does not name `float_eq` — the milestone adds that hint.")
|
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
|
(params)
|
|
(body (app print (app eq 1.5 1.5)))))
|
|
```
|
|
|
|
- [ ] **Step 4: Write the must-fail pin**
|
|
|
|
Create `crates/ail/tests/eq_float_must_fail_pin.rs`:
|
|
|
|
```rust
|
|
//! Pin: `(app eq 1.5 1.5)` must fail typecheck with NoInstance
|
|
//! Eq Float AND the diagnostic must mention `float_eq` as the
|
|
//! explicit alternative. Today the NoInstance fires (per
|
|
//! eq_float_noinstance.rs) but `float_eq` is not in the message
|
|
//! — the milestone adds that hint.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
#[test]
|
|
fn eq_float_must_fail_diagnostic_names_float_eq() {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
let repo_root = PathBuf::from(manifest_dir).join("../..");
|
|
let fixture = repo_root.join("examples/eq_float_must_fail.ail");
|
|
|
|
let out = Command::new(env!("CARGO_BIN_EXE_ail"))
|
|
.arg("check")
|
|
.arg(&fixture)
|
|
.output()
|
|
.expect("ail check failed to spawn");
|
|
|
|
assert!(
|
|
!out.status.success(),
|
|
"expected typecheck failure, got success: stdout={}",
|
|
String::from_utf8_lossy(&out.stdout)
|
|
);
|
|
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(
|
|
stderr.contains("NoInstance") && stderr.contains("Float"),
|
|
"expected NoInstance + Float, got: {stderr}"
|
|
);
|
|
assert!(
|
|
stderr.contains("float_eq"),
|
|
"expected diagnostic to name `float_eq` as the explicit alternative; got: {stderr}"
|
|
);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Write the Float-named-fn happy-path fixture**
|
|
|
|
Create `examples/float_compare_smoke.ail`:
|
|
|
|
```
|
|
(module float_compare_smoke
|
|
(fn main
|
|
(doc "Float comparison via the named-fn surface that replaces the deleted operator names. Each float_* call lowers to a single fcmp via try_emit_primitive_instance_body.")
|
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
|
(params)
|
|
(body
|
|
(seq (app print (app float_eq 1.5 1.5))
|
|
(seq (app print (app float_lt 1.0 2.0))
|
|
(app print (app float_eq 1.0 0.0)))))))
|
|
```
|
|
|
|
- [ ] **Step 6: Write the Float-named-fn E2E**
|
|
|
|
Create `crates/ail/tests/float_compare_smoke_e2e.rs`:
|
|
|
|
```rust
|
|
//! E2E for the six float_* named comparison fns. After the
|
|
//! milestone, Float comparability survives via float_eq / float_lt
|
|
//! / etc. (no Eq Float instance, no `==` callee). Today this test
|
|
//! fires `unknown variable: float_eq` since the symbol does not
|
|
//! exist — RED-first.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
#[test]
|
|
fn float_compare_smoke_prints_true_true_false() {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
let repo_root = PathBuf::from(manifest_dir).join("../..");
|
|
let fixture = repo_root.join("examples/float_compare_smoke.ail");
|
|
let bin = repo_root.join("target/debug/float_compare_smoke_test_bin");
|
|
|
|
let build = Command::new(env!("CARGO_BIN_EXE_ail"))
|
|
.arg("build")
|
|
.arg(&fixture)
|
|
.arg("-o")
|
|
.arg(&bin)
|
|
.output()
|
|
.expect("ail build failed to spawn");
|
|
assert!(
|
|
build.status.success(),
|
|
"ail build failed: stderr={}",
|
|
String::from_utf8_lossy(&build.stderr)
|
|
);
|
|
|
|
let run = Command::new(&bin)
|
|
.output()
|
|
.expect("binary failed to spawn");
|
|
assert!(run.status.success(), "binary failed: stderr={}", String::from_utf8_lossy(&run.stderr));
|
|
assert_eq!(
|
|
String::from_utf8_lossy(&run.stdout).as_ref(),
|
|
"true\ntrue\nfalse\n",
|
|
"expected float_eq then float_lt then float_eq mismatch"
|
|
);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 7: Write the operator-unbound fixture**
|
|
|
|
Create `examples/operator_unbound_check.ail`:
|
|
|
|
```
|
|
(module operator_unbound_check
|
|
(fn main
|
|
(doc "Pin fixture. After operator-routing-eq-ord, `(app == 5 5)` must fail typecheck with `unknown variable: ==` — the operator names are no longer in the language. Today this typechecks (== is polymorphic over Int) — RED-first.")
|
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
|
(params)
|
|
(body (app print (app == 5 5)))))
|
|
```
|
|
|
|
- [ ] **Step 8: Write the operator-unbound pin**
|
|
|
|
Create `crates/ail/tests/operator_names_unbound_pin.rs`:
|
|
|
|
```rust
|
|
//! Pin: the operator names `==` / `!=` / `<` / `<=` / `>` / `>=`
|
|
//! are NOT part of the language. Surface use of any of them must
|
|
//! resolve as `unknown variable: ==` (et al.). Today they all
|
|
//! typecheck (== polymorphic, the others monomorphic-Int) —
|
|
//! RED-first.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
#[test]
|
|
fn comparator_operator_name_is_unbound() {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
let repo_root = PathBuf::from(manifest_dir).join("../..");
|
|
let fixture = repo_root.join("examples/operator_unbound_check.ail");
|
|
|
|
let out = Command::new(env!("CARGO_BIN_EXE_ail"))
|
|
.arg("check")
|
|
.arg(&fixture)
|
|
.output()
|
|
.expect("ail check failed to spawn");
|
|
|
|
assert!(
|
|
!out.status.success(),
|
|
"expected typecheck failure for `(app == 5 5)`; got success: stdout={}",
|
|
String::from_utf8_lossy(&out.stdout)
|
|
);
|
|
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
assert!(
|
|
stderr.contains("unknown variable") && stderr.contains("=="),
|
|
"expected `unknown variable: ==` diagnostic, got: {stderr}"
|
|
);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 9: Write the alwaysinline IR pin**
|
|
|
|
Create `crates/ail/tests/prelude_eq_alwaysinline_ir_pin.rs`:
|
|
|
|
```rust
|
|
//! Pin: the codegen mechanism that emits primitive Eq/Ord
|
|
//! instance bodies (try_emit_primitive_instance_body) MUST
|
|
//! attach an `alwaysinline` attribute to the generated LLVM
|
|
//! function definition so the `-O0` build path inlines the
|
|
//! single-instruction body at every use site, matching the IR
|
|
//! shape the deleted `lower_eq` direct-emit produced. Without
|
|
//! `alwaysinline`, `ail run` (which defaults to -O0) pays a
|
|
//! function-call overhead per comparison.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
#[test]
|
|
fn prelude_eq_int_carries_alwaysinline_attribute_in_emitted_ir() {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
let repo_root = PathBuf::from(manifest_dir).join("../..");
|
|
let fixture = repo_root.join("examples/eq_demo.ail");
|
|
|
|
let out = Command::new(env!("CARGO_BIN_EXE_ail"))
|
|
.arg("emit-ir")
|
|
.arg(&fixture)
|
|
.output()
|
|
.expect("ail emit-ir failed to spawn");
|
|
assert!(
|
|
out.status.success(),
|
|
"ail emit-ir failed: stderr={}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
let ir = String::from_utf8_lossy(&out.stdout);
|
|
// Either form is acceptable:
|
|
// define i1 @ail_prelude_eq__Int(i64, i64) alwaysinline { ... }
|
|
// (attribute appended to the define line). The grep checks for
|
|
// both the symbol presence and the alwaysinline annotation in
|
|
// the same line as the define.
|
|
let saw_define_with_alwaysinline = ir
|
|
.lines()
|
|
.any(|line| line.contains("define") && line.contains("eq__Int") && line.contains("alwaysinline"));
|
|
assert!(
|
|
saw_define_with_alwaysinline,
|
|
"expected `define … eq__Int … alwaysinline` in emitted IR; full IR follows:\n{ir}"
|
|
);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 10: Run all 5 new tests; verify they FAIL with the expected RED shape**
|
|
|
|
Run: `cargo test --workspace --quiet eq_user_adt_smoke_prints_unit_then_point_eq_then_neq eq_float_must_fail_diagnostic_names_float_eq float_compare_smoke_prints_true_true_false comparator_operator_name_is_unbound prelude_eq_int_carries_alwaysinline_attribute_in_emitted_ir 2>&1 | tail -20`
|
|
|
|
Expected: all 5 tests in the run summary line as FAILED. The failure messages are:
|
|
- `eq_user_adt_smoke_...` — `ail build` fails (Unit-eq line fires `NoInstance Eq Unit`)
|
|
- `eq_float_must_fail_...` — `ail check` fails (good) but stderr does NOT contain `float_eq` → assertion-fail on the float_eq substring
|
|
- `float_compare_smoke_...` — `ail build` fails (`unknown variable: float_eq`)
|
|
- `comparator_operator_name_is_unbound` — `ail check` SUCCEEDS (`==` typechecks today) → assertion-fail on `!out.status.success()`
|
|
- `prelude_eq_int_carries_alwaysinline_attribute_in_emitted_ir` — `ail emit-ir` succeeds but IR contains no `alwaysinline` → assertion-fail on the grep
|
|
|
|
All 5 RED ratifies the start-of-milestone state. If any test passes here, the spec assumption is wrong — bounce back to brainstorm.
|
|
|
|
- [ ] **Step 11: Run `cargo build --workspace` to confirm the new files compile-check cleanly**
|
|
|
|
Run: `cargo build --workspace --tests 2>&1 | tail -5`
|
|
Expected: `Finished … profile [unoptimized + debuginfo] target(s) in …` (0 errors). The new test files are pure Rust + `Command` calls; the new `.ail` fixtures aren't compiled by `cargo build`.
|
|
|
|
---
|
|
|
|
## Task 2: Codegen — alwaysinline emission + new intercept arms
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:1213-1259` (`emit_fn` extension for `alwaysinline`)
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:2691-2806` (`try_emit_primitive_instance_body` 9 new arms)
|
|
|
|
This task is purely additive: new intercept arms for `eq__Int` / `eq__Bool` / `eq__Unit` / `float_eq` / `float_ne` / `float_lt` / `float_le` / `float_gt` / `float_ge`. Existing prelude `eq__Int` body is today `(app == x y)` — once the intercept matches `eq__Int`, it overrides this body with a direct `icmp eq i64`, producing the same end result via a different path. `alwaysinline` attribute attaches to the generated `define` line for every fn whose name matches the alwaysinline-allowlist.
|
|
|
|
- [ ] **Step 1: Read `emit_fn` to locate the exact `define` line**
|
|
|
|
Run: `sed -n '1180,1260p' crates/ailang-codegen/src/lib.rs`
|
|
Expected: the function definition includes the line that builds the `define <ret> @<mangled_name>(<params>) {` header. Identify the exact line (likely a `format!` or `push_str` call building this string) — the alwaysinline attribute is appended to it.
|
|
|
|
- [ ] **Step 2: Add the alwaysinline-allowlist helper**
|
|
|
|
In `crates/ailang-codegen/src/lib.rs`, add this fn near `emit_fn` (e.g. directly above it):
|
|
|
|
```rust
|
|
/// Returns `true` when the codegen should emit the LLVM
|
|
/// `alwaysinline` attribute on this fn's `define` line. The
|
|
/// allowlist is the set of primitive-instance bodies emitted by
|
|
/// `try_emit_primitive_instance_body` whose IR is so trivial
|
|
/// (one `icmp` / `fcmp` / `call` plus a `ret`) that `-O0` build
|
|
/// paths pay an unacceptable per-call overhead without inlining,
|
|
/// while `-O2` would inline these unconditionally anyway. The
|
|
/// list mirrors the intercept arm set in
|
|
/// `try_emit_primitive_instance_body` for the equality /
|
|
/// ordering / Float-comparison primitives.
|
|
fn intercept_emit_wants_alwaysinline(symbol: &str) -> bool {
|
|
matches!(
|
|
symbol,
|
|
"eq__Int" | "eq__Bool" | "eq__Str" | "eq__Unit"
|
|
| "compare__Int" | "compare__Bool" | "compare__Str"
|
|
| "float_eq" | "float_ne"
|
|
| "float_lt" | "float_le"
|
|
| "float_gt" | "float_ge"
|
|
)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Inject `alwaysinline` into the `define` header**
|
|
|
|
In `emit_fn` (around line 1213-1259), at the point where the `define <ret> @<mangled_name>(<params>) {` line is built, append ` alwaysinline` between the `)` and the `{` when `intercept_emit_wants_alwaysinline(<symbol>)` is true. The exact patch depends on whether the header is built as a single `format!` or as multiple `push_str` calls; example for the `format!`-style:
|
|
|
|
```rust
|
|
// Before
|
|
self.body.push_str(&format!(
|
|
"define {ret} @{name}({params}) {{\n"
|
|
));
|
|
|
|
// After
|
|
let attrs = if intercept_emit_wants_alwaysinline(unmangled_symbol) {
|
|
" alwaysinline"
|
|
} else {
|
|
""
|
|
};
|
|
self.body.push_str(&format!(
|
|
"define {ret} @{name}({params}){attrs} {{\n"
|
|
));
|
|
```
|
|
|
|
The exact unmangled symbol name reachable at this site is the planner-recon-unknown — read the surrounding `emit_fn` code to identify it (likely a `def.name` or `fn_def.name` field already in scope; if not, the symbol is reachable from the `match instance_key` arm in `try_emit_primitive_instance_body` and the attribute should attach there instead).
|
|
|
|
- [ ] **Step 4: Add the 9 new intercept arms to `try_emit_primitive_instance_body`**
|
|
|
|
In `crates/ailang-codegen/src/lib.rs:2691-2806`, locate the `match instance_key` (or equivalent) inside `try_emit_primitive_instance_body`. Today it has arms for `eq__Str`, `compare__Int`, `compare__Bool`, `compare__Str`. Add:
|
|
|
|
```rust
|
|
"eq__Int" => {
|
|
// Eq Int. Lowers to `icmp eq i64`. The `alwaysinline`
|
|
// attribute on the fn header (see `emit_fn`) collapses the
|
|
// call to a single icmp at every use site under `-O0`.
|
|
body.push_str(" %r = icmp eq i64 %0, %1\n");
|
|
body.push_str(" ret i1 %r\n");
|
|
Some(body)
|
|
}
|
|
"eq__Bool" => {
|
|
body.push_str(" %r = icmp eq i1 %0, %1\n");
|
|
body.push_str(" ret i1 %r\n");
|
|
Some(body)
|
|
}
|
|
"eq__Unit" => {
|
|
// Unit is single-inhabitant; all Units compare equal.
|
|
// Params are typed as Unit (zero-width) but the fn signature
|
|
// still carries them; we discard the operands and return true.
|
|
body.push_str(" ret i1 1\n");
|
|
Some(body)
|
|
}
|
|
"float_eq" => {
|
|
body.push_str(" %r = fcmp oeq double %0, %1\n");
|
|
body.push_str(" ret i1 %r\n");
|
|
Some(body)
|
|
}
|
|
"float_ne" => {
|
|
// `fcmp une` ("unordered or not-equal") not `one`, mirroring
|
|
// float-semantics.md's `!=` guarantee — NaN != NaN must be true.
|
|
body.push_str(" %r = fcmp une double %0, %1\n");
|
|
body.push_str(" ret i1 %r\n");
|
|
Some(body)
|
|
}
|
|
"float_lt" => {
|
|
body.push_str(" %r = fcmp olt double %0, %1\n");
|
|
body.push_str(" ret i1 %r\n");
|
|
Some(body)
|
|
}
|
|
"float_le" => {
|
|
body.push_str(" %r = fcmp ole double %0, %1\n");
|
|
body.push_str(" ret i1 %r\n");
|
|
Some(body)
|
|
}
|
|
"float_gt" => {
|
|
body.push_str(" %r = fcmp ogt double %0, %1\n");
|
|
body.push_str(" ret i1 %r\n");
|
|
Some(body)
|
|
}
|
|
"float_ge" => {
|
|
body.push_str(" %r = fcmp oge double %0, %1\n");
|
|
body.push_str(" ret i1 %r\n");
|
|
Some(body)
|
|
}
|
|
```
|
|
|
|
The exact identifier names (`body`, `instance_key`, `params`) match the surrounding match-arm pattern — read 2691-2806 first to confirm. The Unit body assumes the LLVM type for Unit is a zero-width or untouched single-bit; if the fn signature requires `%0`/`%1` operands of a specific Unit type, adjust accordingly (recon-confirmed shape).
|
|
|
|
- [ ] **Step 5: Build the workspace; expect 0 errors**
|
|
|
|
Run: `cargo build --workspace --tests 2>&1 | tail -5`
|
|
Expected: `Finished …`. No errors. The new arms are pure-additive; the alwaysinline emission is conditioned on the allowlist (today, fns matching the existing arms `eq__Str`/`compare__Int`/etc.).
|
|
|
|
- [ ] **Step 6: Run the alwaysinline IR pin; expect it to FLIP to GREEN**
|
|
|
|
Run: `cargo test --workspace --quiet prelude_eq_int_carries_alwaysinline_attribute_in_emitted_ir 2>&1 | tail -5`
|
|
Expected: PASS. The `eq_demo.ail` fixture contains `(app == 5 5)` which today lowers via `lower_eq` (direct emit, no fn-call); after Task 2, no, wait — the prelude `eq__Int` body is still `(app == x y)` today, so `eq` calls dispatch via class to prelude.eq__Int, which the new intercept arm overrides. The emitted IR for `@ail_prelude_eq__Int` carries the alwaysinline attribute. `eq_demo.ail` calls `(app == 5 5)` directly (not `(app eq 5 5)`), so the IR-pin's substring check on `define … eq__Int … alwaysinline` is reached via the prelude's eq__Int symbol being EMITTED into the IR alongside main. If the IR doesn't reach for prelude.eq__Int at all (because eq_demo only uses ==), change the fixture in step 1 of the pin to instead invoke `(app eq …)` — adjust the test fixture path to a fixture that DOES call `eq`. Suggestion: use `examples/eq_ord_user_adt.ail` which calls `(app eq …)` on IntBox (which calls eq on Int internally).
|
|
|
|
If the test does not turn GREEN at this gate, investigate: either the allowlist is mis-keyed (compare `intercept_emit_wants_alwaysinline`'s match arm names to the actual `instance_key` strings inside `try_emit_primitive_instance_body`), or the attribute injection site in `emit_fn` is bypassed for the codegen path used by intercept-emitted bodies.
|
|
|
|
- [ ] **Step 7: Run the full workspace test suite; expect existing tests still green**
|
|
|
|
Run: `cargo test --workspace --quiet 2>&1 | tail -10`
|
|
Expected: same `0 failed` shape as before Task 2 plus `prelude_eq_int_carries_alwaysinline_attribute_in_emitted_ir` flipping to PASS. Per-suite pass counts in the per-suite summary lines must match `git stash`-prior counts (re-derive on failure). The other 4 RED tests stay RED.
|
|
|
|
If any existing test breaks: the most likely cause is the alwaysinline allowlist matching too broadly (i.e. attaching the attribute to a fn that LLVM refuses to inline for other reasons — e.g. external linkage). Narrow the allowlist to the exact mono-symbol names in the intercept arms and re-run.
|
|
|
|
---
|
|
|
|
## Task 3: Prelude reshape — Eq Unit, six float_* fns, placeholder bodies, hash re-pin
|
|
|
|
**Files:**
|
|
- Modify: `examples/prelude.ail` (multiple sites: line 9 comment drop, lines 12-29 body placeholders, insert Eq Unit, insert six float_* fns)
|
|
- Modify: `crates/ailang-surface/tests/prelude_module_hash_pin.rs:16` (hash literal re-pin)
|
|
- Modify: `crates/ail/tests/mono_hash_stability.rs:50-55` (six body-hash pins re-pin)
|
|
|
|
- [ ] **Step 1: Drop the "P2 follow-up" comment on `class Eq`**
|
|
|
|
Edit `examples/prelude.ail:9` — the line currently reads:
|
|
|
|
```
|
|
(doc "Structural equality. Ships in milestone 23 alongside Ord. The primitive `==` operator stays as the surface-level comparator this iter; routing `==` through `Eq.eq` is the declared P2 follow-up (see docs/specs/2026-05-10-23-eq-ord-prelude.md, `Out of scope`).")
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```
|
|
(doc "Structural equality. The class-method `eq` is the surface-level comparator; `==` as a surface name is not part of the language. Primitive instances Eq Int / Bool / Str / Unit are lowered via try_emit_primitive_instance_body in the codegen.")
|
|
```
|
|
|
|
- [ ] **Step 2: Rewrite Eq Int / Eq Bool instance bodies to placeholder shape**
|
|
|
|
Edit `examples/prelude.ail:12-23` — the Eq Int and Eq Bool instances currently have bodies `(app == x y)`. Replace both bodies with placeholder shape mirroring the existing Ord Int's:
|
|
|
|
```
|
|
(instance
|
|
(class prelude.Eq)
|
|
(type (con Int))
|
|
(doc "Eq Int. Body is placeholder for round-trip stability; codegen intercept try_emit_primitive_instance_body::eq__Int emits `icmp eq i64` with the alwaysinline attribute.")
|
|
(method eq
|
|
(body (lam (params (typed x a) (typed y a)) (ret (con Bool)) (body false)))))
|
|
(instance
|
|
(class prelude.Eq)
|
|
(type (con Bool))
|
|
(doc "Eq Bool. Body is placeholder for round-trip stability; codegen intercept try_emit_primitive_instance_body::eq__Bool emits `icmp eq i1` with the alwaysinline attribute.")
|
|
(method eq
|
|
(body (lam (params (typed x a) (typed y a)) (ret (con Bool)) (body false)))))
|
|
```
|
|
|
|
Eq Str's body stays unchanged in shape (it's already placeholder-style with the intercept handling it).
|
|
|
|
- [ ] **Step 3: Insert Eq Unit instance**
|
|
|
|
Edit `examples/prelude.ail`, immediately after the Eq Str instance (before `(class Ord …)`), insert:
|
|
|
|
```
|
|
(instance
|
|
(class prelude.Eq)
|
|
(type (con Unit))
|
|
(doc "Eq Unit. Unit is single-inhabitant so all values compare equal. Body is placeholder; codegen intercept try_emit_primitive_instance_body::eq__Unit emits `ret i1 1`.")
|
|
(method eq
|
|
(body (lam (params (typed x a) (typed y a)) (ret (con Bool)) (body true)))))
|
|
```
|
|
|
|
- [ ] **Step 4: Insert six float_* free fns**
|
|
|
|
Edit `examples/prelude.ail`, immediately after the `(fn print …)` block (before the final closing paren of the module), insert:
|
|
|
|
```
|
|
(fn float_eq
|
|
(doc "IEEE Float equality. `float_eq x y` returns true iff both operands are non-NaN and bit-equal. Lowered to `fcmp oeq double` via try_emit_primitive_instance_body::float_eq with alwaysinline. Replaces the milestone-deleted polymorphic `==` on Float.")
|
|
(type (fn-type (params (con Float) (con Float)) (ret (con Bool))))
|
|
(params x y)
|
|
(body false))
|
|
(fn float_ne
|
|
(doc "IEEE Float disequality. `float_ne nan nan` returns true (unordered-or-not-equal per IEEE-754). Lowered to `fcmp une double`.")
|
|
(type (fn-type (params (con Float) (con Float)) (ret (con Bool))))
|
|
(params x y)
|
|
(body false))
|
|
(fn float_lt
|
|
(doc "IEEE Float strict less-than. `float_lt nan x` returns false for any x (unordered). Lowered to `fcmp olt double`.")
|
|
(type (fn-type (params (con Float) (con Float)) (ret (con Bool))))
|
|
(params x y)
|
|
(body false))
|
|
(fn float_le
|
|
(doc "IEEE Float less-than-or-equal. Lowered to `fcmp ole double`.")
|
|
(type (fn-type (params (con Float) (con Float)) (ret (con Bool))))
|
|
(params x y)
|
|
(body false))
|
|
(fn float_gt
|
|
(doc "IEEE Float strict greater-than. Lowered to `fcmp ogt double`.")
|
|
(type (fn-type (params (con Float) (con Float)) (ret (con Bool))))
|
|
(params x y)
|
|
(body false))
|
|
(fn float_ge
|
|
(doc "IEEE Float greater-than-or-equal. Lowered to `fcmp oge double`.")
|
|
(type (fn-type (params (con Float) (con Float)) (ret (con Bool))))
|
|
(params x y)
|
|
(body false))
|
|
```
|
|
|
|
(Body `false` is placeholder; the codegen intercept overrides each with the corresponding `fcmp`.)
|
|
|
|
- [ ] **Step 5: Re-pin the prelude module hash**
|
|
|
|
Run: `cargo test --workspace --quiet -p ailang-surface --test prelude_module_hash_pin prelude_parse_yields_canonical_hash 2>&1 | tail -10`
|
|
Expected: FAIL with a message of the form `prelude module hash drifted: expected 3abe0d3fa3c11c99, got <NEW_HASH>`. Capture `<NEW_HASH>`.
|
|
|
|
Edit `crates/ailang-surface/tests/prelude_module_hash_pin.rs:16` — replace the existing literal `"3abe0d3fa3c11c99"` with `<NEW_HASH>`. Update any accompanying comment naming the prior value to add the operator-routing-eq-ord re-pin context (e.g. `// 2026-05-20 operator-routing-eq-ord re-pinned (prior: 3abe0d3fa3c11c99): Eq Int/Bool body shape flips to placeholder, Eq Unit added, six float_* fns added, line-9 comment rewritten.`).
|
|
|
|
Re-run the pin: `cargo test --workspace --quiet -p ailang-surface --test prelude_module_hash_pin 2>&1 | tail -5`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 6: Re-pin the six body-hash literals in `mono_hash_stability.rs`**
|
|
|
|
Run: `cargo test --workspace --quiet --test mono_hash_stability 2>&1 | tail -20`
|
|
Expected: 6 assertion failures of the form `eq__Int body hash drifted — see mono_hash_stability.rs for the resolution pattern; captured: <NEW_HASH_X>`. Capture each.
|
|
|
|
Edit `crates/ail/tests/mono_hash_stability.rs:50-55` (and surrounding context) — replace each of the six hash literals (`eq__Int`, `eq__Bool`, `eq__Str`, `compare__Int`, `compare__Bool`, `compare__Str`) with their captured values. Add a comment band noting the operator-routing-eq-ord re-pin reason: bodies become placeholder per prelude reshape.
|
|
|
|
Re-run: `cargo test --workspace --quiet --test mono_hash_stability 2>&1 | tail -5`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 7: Run the full workspace test suite**
|
|
|
|
Run: `cargo test --workspace --quiet 2>&1 | tail -10`
|
|
Expected: `0 failed` summary in the run-level line. The 4 still-RED tests from Task 1 stay RED; `prelude_eq_int_carries_alwaysinline_attribute_in_emitted_ir` from Task 2 stays GREEN. All other tests stay GREEN. If any existing test fails: the codegen intercept arms emit a different IR shape than the existing dispatch produces for the same call — investigate.
|
|
|
|
---
|
|
|
|
## Task 4: Fixture migration — 60 .ail files + lockstep eq_ord_e2e body-hash re-pin
|
|
|
|
**Files:**
|
|
- Modify: ~60 `.ail` files under `examples/` (enumerate via grep)
|
|
- Modify: `examples/eq_ord_user_adt.ail:21` (`(app == ai bi)` → `(app eq ai bi)`)
|
|
- Modify: `crates/ail/tests/eq_ord_e2e.rs:134` (body-hash re-pin)
|
|
- Modify: `examples/eq_demo.ail:1-23` (doc-comment header rewrites)
|
|
- Modify: `crates/ail/tests/e2e.rs:1351-1360` (doc-comment of `eq_demo` test)
|
|
|
|
- [ ] **Step 1: Enumerate the full fixture set**
|
|
|
|
Run: `grep -rln '(app == \|(app != \|(app < \|(app <= \|(app > \|(app >=' examples/ | sort`
|
|
Expected: ~60 file paths. Save the list as the working set.
|
|
|
|
- [ ] **Step 2: Per-file migration (manual substitution per file)**
|
|
|
|
For each file in the list:
|
|
|
|
```
|
|
(app == x y) → (app eq x y)
|
|
(app != x y) → (app ne x y)
|
|
(app < x y) → (app lt x y)
|
|
(app <= x y) → (app le x y)
|
|
(app > x y) → (app gt x y)
|
|
(app >= x y) → (app ge x y)
|
|
```
|
|
|
|
For Float-operand call sites (operand typed Float — recon-noted candidates: `bench_compute_collatz.ail`, `mut_sum_floats.ail`, `floats_1_newton_sqrt.ail`, `floats_3_safe_division.ail`; verify each via inspecting the operand types in the fixture), use the `float_*` variants instead:
|
|
|
|
```
|
|
(app == x y) where x,y : Float → (app float_eq x y)
|
|
(app != x y) where x,y : Float → (app float_ne x y)
|
|
... etc.
|
|
```
|
|
|
|
The bulk-substitution is NOT a regex-replace because Float operands need the `float_*` form, not `eq`/`ne`/etc. Each fixture is touched once per call site; visually inspect the operand types around each call before substituting.
|
|
|
|
- [ ] **Step 3: Migrate the user-ADT body in `eq_ord_user_adt.ail`**
|
|
|
|
Edit `examples/eq_ord_user_adt.ail:21`:
|
|
|
|
```
|
|
; Before:
|
|
(case (pat-ctor MkIntBox ai) (match b
|
|
(case (pat-ctor MkIntBox bi) (app == ai bi))))))))))
|
|
|
|
; After:
|
|
(case (pat-ctor MkIntBox ai) (match b
|
|
(case (pat-ctor MkIntBox bi) (app eq ai bi))))))))))
|
|
```
|
|
|
|
This shifts the user-instance body-hash, requiring the eq_ord_e2e re-pin in step 5.
|
|
|
|
- [ ] **Step 4: Rewrite `eq_demo.ail` doc-comment**
|
|
|
|
Edit `examples/eq_demo.ail:1-23` — the current 23-line doc-comment describes the polymorphic-`==` story (iter 16e). Rewrite the doc-comment to describe the class-dispatch story: `eq` dispatches via `prelude.Eq.eq`'s primitive instances for Int / Bool / Str / Unit; `compare` dispatches via `prelude.Ord.compare` for Int / Bool / Str. Strip references to `lower_eq`, `polymorphic ==`, `16e`. Keep the expected-stdout block at the end.
|
|
|
|
- [ ] **Step 5: Rewrite the `eq_demo` test doc-comment in `e2e.rs`**
|
|
|
|
Edit `crates/ail/tests/e2e.rs:1351-1360` — rewrite the doc-comment to match the new fixture story. The test body is unchanged (stdout pin is identical).
|
|
|
|
- [ ] **Step 6: Re-pin the eq__IntBox body hash**
|
|
|
|
Run: `cargo test --workspace --quiet --test eq_ord_e2e eq_ord_user_adt_eq_intbox_hash_stable 2>&1 | tail -10`
|
|
Expected: FAIL with `eq__IntBox body hash drifted — ...; captured: <NEW_HASH>`. Capture `<NEW_HASH>`.
|
|
|
|
Edit `crates/ail/tests/eq_ord_e2e.rs:134` — replace `"3c4cf040cb4e8bb2"` with `<NEW_HASH>`. Add comment context: `// 2026-05-20 operator-routing-eq-ord re-pinned (prior: 3c4cf040cb4e8bb2): user-instance body's inner == migrates to eq.`
|
|
|
|
Re-run: `cargo test --workspace --quiet --test eq_ord_e2e 2>&1 | tail -5`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 7: Verify zero remaining operator-form occurrences**
|
|
|
|
Run: `grep -rln '(app == \|(app != \|(app < \|(app <= \|(app > \|(app >=' examples/ | wc -l`
|
|
Expected: `0`. If non-zero, identify the missed files and re-run step 2 on each.
|
|
|
|
- [ ] **Step 8: Run the workspace test suite; expect `0 failed`**
|
|
|
|
Run: `cargo test --workspace --quiet 2>&1 | tail -10`
|
|
Expected: `0 failed` across all binaries. The four remaining RED tests from Task 1 stay RED (Unit-eq path / Float-fn path / unknown-`==` / alwaysinline are state from earlier tasks). All other tests stay GREEN — every fixture's `(app eq …)` / `(app lt …)` / etc. now dispatches via the class layer that has been working since well before this milestone.
|
|
|
|
---
|
|
|
|
## Task 5: Test-scaffold migration — all 8 in-source `#[cfg(test)]` AST-literal sites
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-check/src/lib.rs:5656` (`>=` → `ge`)
|
|
- Modify: `crates/ailang-check/src/lib.rs:6088-6092` (`eq_app` helper + caller)
|
|
- Modify: `crates/ailang-check/src/lib.rs:6216-6220` (second `==` literal site)
|
|
- Modify: `crates/ailang-check/src/builtins.rs:413-432` (`<` smoke tests at lines 416, 428 — recon-noted)
|
|
- Modify: `crates/ailang-core/src/desugar.rs:2414`
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:3371` (in-source `mod tests` `==` literal)
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:3419` (in-source `mod tests` `==` literal)
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:3729-3734` (`cmp("==", …)` Float-arith tests)
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:4354` (in-source `mod tests` `==` literal)
|
|
|
|
- [ ] **Step 1: Migrate `crates/ailang-check/src/lib.rs:5656`**
|
|
|
|
Locate `Term::Var { name: ">=".into() }` at line 5656; replace `">="` with `"ge"`. The surrounding test is in `#[cfg(test)] mod tests` so the migration changes the test's AST-literal-construction without changing what it tests semantically (post-milestone, `ge` resolves via class-dispatch the way `>=` does today).
|
|
|
|
- [ ] **Step 2: Migrate `crates/ailang-check/src/lib.rs:6088-6092` `eq_app` helper**
|
|
|
|
Locate the `eq_app` fn helper at line ~6088 and its callers; replace `Term::Var { name: "==".into() }` with `name: "eq".into()` at line 6092.
|
|
|
|
- [ ] **Step 3: Migrate `crates/ailang-check/src/lib.rs:6220`**
|
|
|
|
Locate the second AST-literal site at line 6220; replace `name: "==".into()` with `name: "eq".into()`.
|
|
|
|
- [ ] **Step 4: Migrate `crates/ailang-check/src/builtins.rs:413-432` `<` smoke tests**
|
|
|
|
Locate the in-source `mod tests` at lines 413-432 using `app("<", …)`. Per recon, these are `<`-builtin smoke tests against `synth_in_builtins_env`. After Task 7 deletes `<` from the builtin table, these tests fail at typecheck because `<` is no longer a known identifier.
|
|
|
|
Two options:
|
|
(a) Migrate `app("<", …)` to `app("lt", …)` — tests then exercise the class-dispatch path for `lt` (which is itself the spec-intended behaviour for the new surface).
|
|
(b) Delete the tests as redundant with class-dispatch coverage from `eq_ord_e2e`.
|
|
|
|
Choose (a) for continuity (more pin coverage). Apply: replace each `app("<", …)` with `app("lt", …)`. Doc-comments referencing `<` in the test body update alongside.
|
|
|
|
- [ ] **Step 5: Migrate `crates/ailang-core/src/desugar.rs:2414`**
|
|
|
|
Locate the AST-literal site at line 2414 inside `#[cfg(test)] mod tests` (mod-block starts at line 1604). The literal is `Term::Var { name: "==".into() }`. Replace `"=="` with `"eq"`. The surrounding test constructs an AST sample; semantic outcome of the test is unaffected — it pins a desugar-result shape downstream of where `==`/`eq` is purely a symbol name.
|
|
|
|
- [ ] **Step 6: Migrate `crates/ailang-codegen/src/lib.rs:3371, :3419, :4354`**
|
|
|
|
Locate each of the three in-source `mod tests` AST-literal sites. Per recon, these tests pin `lower_eq`-rejection-error wording for ADT/Fn types being passed to `==`. After Task 7 deletes `lower_eq` and `==` from the builtin table, there is no `lower_eq`-rejection path to test.
|
|
|
|
Two options:
|
|
(a) Migrate the test fixtures to use `eq` and update assertions to assert the new `NoInstance Eq <ADT>` typecheck-time diagnostic (which fires earlier and replaces the codegen rejection).
|
|
(b) Delete the three tests as obsolete — the rejection mechanism the tests pinned is gone by structure.
|
|
|
|
Choose (b) — delete the three tests as obsolete. Rationale: the user-ADT `NoInstance` diagnostic is already pinned via `eq_ord_e2e`'s negative case (no instance → fail at typecheck), so deletion does not lose coverage. Add a one-line `// removed YYYY-MM-DD per operator-routing-eq-ord: lower_eq deletion makes this rejection path unreachable; coverage moves to NoInstance at typecheck` comment at each deletion site (commit-body documentation only — these are deletions in test code, no live-narrative impact).
|
|
|
|
- [ ] **Step 7: Migrate `crates/ailang-codegen/src/lib.rs:3729-3734`**
|
|
|
|
Locate the `cmp("==", …)` and `cmp("<", …)` calls in the Float-arith smoke tests at lines 3729-3734. These tests pin the synth `builtin_binop_typed` table arms for Float comparators. After Task 8 deletes those arms, the tests fail.
|
|
|
|
Migrate to the new symbol-name surface: replace `cmp("==", …)` with `app("float_eq", …)` and `cmp("<", …)` with `app("float_lt", …)`. The test's helper fn `cmp` may need to be renamed or adjusted depending on its signature; if the helper hard-codes Int/Float dispatch, replace its call with a direct `app(…)` builder. Adjust the assertion to expect the IR shape produced by the `try_emit_primitive_instance_body::float_eq`/`float_lt` intercept (i.e., the `fcmp oeq double` / `fcmp olt double` lines remain, but emerge from a generated `@ail_prelude_float_eq` / `@ail_prelude_float_lt` body, not from a direct-emit `==`/`<` path).
|
|
|
|
If migration is intractable for these specific tests (i.e. the test fundamentally pins direct-emit IR shape that no longer exists), delete the two tests and add the same one-line comment as step 6.
|
|
|
|
- [ ] **Step 8: Build + run full workspace**
|
|
|
|
Run: `cargo test --workspace --quiet 2>&1 | tail -10`
|
|
Expected: `0 failed`. All migrated in-source tests pass under their new class-dispatch semantics (or are deleted). The 4 still-RED tests from Task 1 stay RED.
|
|
|
|
---
|
|
|
|
## Task 6: Lit-pattern desugar — `build_eq` substitutes `eq` for `==`
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-core/src/desugar.rs:1099` (`build_eq` fn)
|
|
|
|
- [ ] **Step 1: Patch the `==` symbol in `build_eq`**
|
|
|
|
In `crates/ailang-core/src/desugar.rs`, locate `build_eq` at line 1099. Find line 1109 reading:
|
|
|
|
```rust
|
|
name: "==".to_string(),
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```rust
|
|
name: "eq".to_string(),
|
|
```
|
|
|
|
- [ ] **Step 2: Update the doc-comment on `build_eq`**
|
|
|
|
The doc-comment at lines ~1095-1099 currently describes the function as emitting `==`. Rewrite to:
|
|
|
|
```rust
|
|
/// Builds the equality-test AST node for a literal pattern. Lowers
|
|
/// `(pat-lit <lit>)` to `(if (eq sv <lit>) <body> <fall_k>)`. The
|
|
/// emitted `eq` resolves via prelude.Eq's class-method dispatch
|
|
/// (per design/contracts/0016-method-dispatch.md) — Int/Bool/Str/Unit
|
|
/// patterns all route through the corresponding primitive
|
|
/// instance.
|
|
///
|
|
/// Float-Literal patterns are hard-rejected at typecheck
|
|
/// (`CheckError::FloatPatternNotAllowed`, per
|
|
/// design/contracts/0005-float-semantics.md) before this fn is reached,
|
|
/// so the `eq`-dispatch never encounters Float.
|
|
fn build_eq(scrutinee: Term, lit: &Literal) -> Term {
|
|
```
|
|
|
|
- [ ] **Step 3: Run lit-pattern tests; expect 0 failures**
|
|
|
|
Run: `cargo test --workspace --quiet --test e2e lit_pat_demo 2>&1 | tail -10`
|
|
Expected: PASS. The `lit_pat_demo` fixture's `(case (pat-lit "hi") …)` arms now desugar to `(if (eq sv "hi") …)`, which dispatches via `prelude.Eq.eq` for Str — same end result, different path. The fixture's stdout pin is identical.
|
|
|
|
- [ ] **Step 4: Run the full workspace suite**
|
|
|
|
Run: `cargo test --workspace --quiet 2>&1 | tail -10`
|
|
Expected: `0 failed`. Lit-pattern desugar's substitution is purely a symbol rename; downstream typecheck and codegen handle the new symbol identically to the old.
|
|
|
|
---
|
|
|
|
## Task 7: Delete dead operator machinery
|
|
|
|
After Tasks 1-6 land, no live code path references `==` / `!=` / `<` / `<=` / `>` / `>=` as a value-level identifier — fixtures migrated (Task 4), test scaffolds migrated (Task 5), lit-pattern desugar emits `eq` (Task 6). The builtin-table entries, the codegen lowering paths, and the in-source ADT-rejection tests are dead code. This task deletes all of them in one cohesive sweep.
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-check/src/builtins.rs:96-98` (delete comparator install loop)
|
|
- Modify: `crates/ailang-check/src/builtins.rs:100-125` (delete `==` polymorphic Forall + comment band)
|
|
- Modify: `crates/ailang-check/src/builtins.rs:303-308` (delete list-side mirror rows)
|
|
- Modify: `crates/ailang-codegen/src/synth.rs:263-276` (delete 10 comparator arms in `builtin_binop_typed`)
|
|
- Modify: `crates/ailang-codegen/src/synth.rs:268-271` (comment-band rewrite — drop pre-iter-4 historical reference, frame as arithmetic-only table)
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:68-73` (`is_arithmetic_or_comparison_op` → `is_arithmetic_op`; comment update at 60-67)
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:2133` (delete `if name == "=="` short-circuit in `lower_app`)
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:2529` (delete `name == "=="` clause in `is_static_callee`)
|
|
- Modify: `crates/ailang-codegen/src/lib.rs:2879-2964` (delete `lower_eq` whole fn)
|
|
|
|
- [ ] **Step 1: Delete typchecker builtin entries**
|
|
|
|
In `crates/ailang-check/src/builtins.rs`:
|
|
|
|
Lines 96-98 currently read:
|
|
```rust
|
|
for op in ["!=", "<", "<=", ">", ">="] {
|
|
env.globals.insert(op.into(), poly_a_a_to_bool());
|
|
}
|
|
```
|
|
Delete these three lines.
|
|
|
|
Lines 100-125 currently read (the `==` Forall block plus 7-line comment band):
|
|
```rust
|
|
// `==` is polymorphic — `forall a. (a, a) -> Bool`. Codegen
|
|
// dispatches on the resolved arg type at the call site:
|
|
// `Int` → `icmp eq i64`, `Bool` → `icmp eq i1`, `Str` → `@strcmp`,
|
|
// `Unit` → constant `i1 1`, `Float` → `fcmp oeq double`. ADT/Fn
|
|
// equality is rejected at codegen with a clear "== not supported
|
|
// for type X" error. The ordering ops (`<`, `<=`, `>`, `>=`, `!=`)
|
|
// share the same polymorphic shape but with `Bool` return; `==`
|
|
// stays in its own arm only because the codegen-side dispatch
|
|
// table has historically been per-op.
|
|
env.globals.insert(
|
|
"==".into(),
|
|
Type::Forall {
|
|
vars: vec!["a".into()],
|
|
constraints: vec![],
|
|
body: Box::new(Type::Fn {
|
|
params: vec![
|
|
Type::Var { name: "a".into() },
|
|
Type::Var { name: "a".into() },
|
|
],
|
|
ret: Box::new(Type::bool_()),
|
|
effects: vec![],
|
|
param_modes: vec![],
|
|
ret_mode: ailang_core::ast::ParamMode::Implicit,
|
|
}),
|
|
},
|
|
);
|
|
```
|
|
Delete all 26 lines (the comment band + the `env.globals.insert("==", …)` block).
|
|
|
|
Lines 303-308 (inside `pub fn list()`) currently read:
|
|
```rust
|
|
("==", "forall a. (a, a) -> Bool"),
|
|
("!=", "forall a. (a, a) -> Bool"),
|
|
("<", "forall a. (a, a) -> Bool"),
|
|
("<=", "forall a. (a, a) -> Bool"),
|
|
(">", "forall a. (a, a) -> Bool"),
|
|
(">=", "forall a. (a, a) -> Bool"),
|
|
```
|
|
Delete all six rows.
|
|
|
|
- [ ] **Step 2: Delete codegen builtin_binop_typed comparator arms**
|
|
|
|
In `crates/ailang-codegen/src/synth.rs:263-276`, delete the 10 comparator arms (5 Int + 5 Float). The arithmetic arms at lines 251-259 remain.
|
|
|
|
In the same fn's preceding comment band (lines 260-272), rewrite the historical reference. Current text discusses the iter-4.2/4.3 split where Int comparators stayed and Float comparators were added; replace with:
|
|
|
|
```rust
|
|
// `builtin_binop_typed` is now arithmetic-only. Comparators
|
|
// (`==`/`!=`/`<`/`<=`/`>`/`>=`) were removed in iter
|
|
// operator-routing-eq-ord.1 — surface comparison routes through
|
|
// the prelude.Eq/Ord class-method dispatch, with primitive
|
|
// instance bodies emitted via try_emit_primitive_instance_body
|
|
// in lib.rs (Eq Int/Bool/Str/Unit → icmp; Ord Int/Bool/Str
|
|
// → icmp + ctor; float_eq/float_ne/float_lt/le/gt/ge → fcmp).
|
|
```
|
|
|
|
- [ ] **Step 3: Update `is_arithmetic_or_comparison_op`**
|
|
|
|
In `crates/ailang-codegen/src/lib.rs:68-73`, the fn currently includes `==` / `!=` / `<` / `<=` / `>` / `>=` in its match. After deletion of the comparator arms, rename the fn to `is_arithmetic_op` and reduce its match to `+` / `-` / `*` / `/` / `%`. Update the doc-comment at lines 60-67 alongside (drop the comparator-related sentences).
|
|
|
|
Find all callers of `is_arithmetic_or_comparison_op` via `git grep -n 'is_arithmetic_or_comparison_op' crates/`. Update each call site to the new name.
|
|
|
|
- [ ] **Step 4: Delete `==` short-circuit in `lower_app`**
|
|
|
|
In `crates/ailang-codegen/src/lib.rs:2126-2144`, locate the `if name == "=="` short-circuit (the recon-noted line at 2133). Delete the entire `if`-block (and its body — the recon hint is "lines 2126-2144").
|
|
|
|
After deletion, `lower_app` falls through to the standard fn-call path for any `name`; since `==` is no longer a known identifier (deleted from builtins), the typechecker rejects `(app == …)` at an earlier stage, so this branch can never be reached.
|
|
|
|
- [ ] **Step 5: Delete `name == "=="` clause in `is_static_callee`**
|
|
|
|
In `crates/ailang-codegen/src/lib.rs:2529`, the predicate currently lists `name == "=="` as one of the static-callee patterns. Delete that clause.
|
|
|
|
- [ ] **Step 6: Delete `lower_eq` whole fn**
|
|
|
|
In `crates/ailang-codegen/src/lib.rs:2879-2964`, delete the entire `lower_eq` fn (signature + body). Also delete its doc-comment (the preceding `///` lines).
|
|
|
|
Find any leftover callers (should be none after step 4):
|
|
```
|
|
git grep -n 'lower_eq\b' crates/
|
|
```
|
|
Expected: 0 hits (the only caller was the `==` short-circuit in `lower_app`, deleted in step 4). If any remain, delete them.
|
|
|
|
- [ ] **Step 7: Build the workspace; expect 0 errors**
|
|
|
|
Run: `cargo build --workspace --tests 2>&1 | tail -10`
|
|
Expected: `Finished …` (0 errors). Compile-gate ratifies: nothing in the production tree calls `lower_eq`, references the deleted builtin-globals entries, or references the deleted `builtin_binop_typed` comparator arms. The deletions are fully internally coherent.
|
|
|
|
If `error[E0061]` or `error[E0425]` (`cannot find function`) appears: a caller was missed in steps 4-6 — `git grep` the missing symbol and update.
|
|
|
|
- [ ] **Step 8: Run the full workspace test suite**
|
|
|
|
Run: `cargo test --workspace --quiet 2>&1 | tail -10`
|
|
Expected: `0 failed`. After deletion, the `operator_names_unbound_pin` test flips to GREEN — `(app == 5 5)` typechecks to `unknown variable: ==`. The four other Task-1 tests:
|
|
- `eq_user_adt_smoke_e2e` — depends on Eq Unit instance (added Task 3); should now GREEN (the inner `(app eq a1 a2)` on Int still works via class-dispatch with Task 2's intercept)
|
|
- `float_compare_smoke_e2e` — depends on float_* fns (added Task 3); should now GREEN
|
|
- `eq_float_must_fail_pin` — still RED (diagnostic does not yet mention `float_eq` — Task 8 handles)
|
|
- `prelude_eq_int_carries_alwaysinline_attribute_in_emitted_ir` — stays GREEN (was Task 2)
|
|
|
|
If `eq_user_adt_smoke_e2e` or `float_compare_smoke_e2e` is still RED: the class-dispatch path for `eq__Unit` or `float_eq` isn't reaching the intercept. Investigate the mono-symbol naming convention (do mono-pass-emitted user instances really resolve to `prelude.eq__Unit` / `prelude.float_eq` as the intercept arms key on?).
|
|
|
|
---
|
|
|
|
## Task 8: Float-aware NoInstance addendum — name `float_eq` / `float_lt` as the alternative
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-check/src/lib.rs:856-880` (extend Float-aware diagnostic)
|
|
- Modify: `crates/ail/tests/eq_float_noinstance.rs:32-44` (extend assertion to require `float_eq` substring)
|
|
|
|
- [ ] **Step 1: Extend the Float-aware diagnostic wording**
|
|
|
|
In `crates/ailang-check/src/lib.rs:856-880`, the existing addendum reads (around line 868-872):
|
|
|
|
```rust
|
|
if (class == "prelude.Eq" || class == "prelude.Ord") && at_type == "Float" {
|
|
d.message = format!(
|
|
"{} — Float has no Eq/Ord instance by design (partial \
|
|
orderability per IEEE-754); see design/contracts/0005-float-semantics.md.",
|
|
d.message
|
|
);
|
|
} else if class == "prelude.Show" {
|
|
```
|
|
|
|
Extend the wording to mention `float_eq` and `float_lt`:
|
|
|
|
```rust
|
|
if (class == "prelude.Eq" || class == "prelude.Ord") && at_type == "Float" {
|
|
d.message = format!(
|
|
"{} — Float has no Eq/Ord instance by design (partial \
|
|
orderability per IEEE-754); use float_eq / float_lt \
|
|
(and siblings) for explicit IEEE-aware comparison. \
|
|
See design/contracts/0005-float-semantics.md.",
|
|
d.message
|
|
);
|
|
} else if class == "prelude.Show" {
|
|
```
|
|
|
|
- [ ] **Step 2: Run `eq_float_must_fail_pin`; expect GREEN**
|
|
|
|
Run: `cargo test --workspace --quiet --test eq_float_must_fail_pin 2>&1 | tail -10`
|
|
Expected: PASS. The diagnostic now contains `float_eq` substring.
|
|
|
|
- [ ] **Step 3: Extend the existing `eq_float_noinstance.rs` assertion**
|
|
|
|
In `crates/ail/tests/eq_float_noinstance.rs:32-44`, the existing test asserts the diagnostic contains `Float` and the `float-semantics.md` doc reference. Extend the assertion to also require `float_eq`:
|
|
|
|
```rust
|
|
// Existing (around line 38-44):
|
|
assert!(
|
|
stderr.contains("Float") && stderr.contains("design/contracts/0005-float-semantics.md"),
|
|
"expected Float-aware addendum, got: {stderr}"
|
|
);
|
|
|
|
// Replace with:
|
|
assert!(
|
|
stderr.contains("Float") && stderr.contains("design/contracts/0005-float-semantics.md") && stderr.contains("float_eq"),
|
|
"expected Float-aware addendum with float_eq hint, got: {stderr}"
|
|
);
|
|
```
|
|
|
|
Run: `cargo test --workspace --quiet --test eq_float_noinstance 2>&1 | tail -5`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 4: Run the full workspace test suite**
|
|
|
|
Run: `cargo test --workspace --quiet 2>&1 | tail -10`
|
|
Expected: `0 failed` summary line. All 5 RED-bootstrap tests from Task 1 are now GREEN.
|
|
|
|
---
|
|
|
|
## Task 9: IR snapshot regeneration
|
|
|
|
**Files:**
|
|
- Modify: `crates/ail/tests/snapshots/sum.ll`
|
|
- Modify: `crates/ail/tests/snapshots/max3.ll`
|
|
|
|
`sum.ail` uses `==` (lit-pattern desugar) and `max3.ail` uses `>` directly. After Tasks 4 and 6 land, the desugar emits `eq` and the source fixtures use `eq`/`gt` — the resulting IR shape shifts from direct `icmp` to a call into `@ail_prelude_eq__Int` (or `@ail_prelude_gt`) with the `alwaysinline` attribute. The checked-in snapshots no longer match; they regenerate.
|
|
|
|
- [ ] **Step 1: Regenerate snapshots via UPDATE_SNAPSHOTS=1**
|
|
|
|
Run: `UPDATE_SNAPSHOTS=1 cargo test --workspace --quiet -p ail --test ir_snapshot 2>&1 | tail -10`
|
|
Expected: regeneration completes; the 5 snapshot files (`hello.ll`, `list.ll`, `max3.ll`, `sum.ll`, `ws_main.ll`) are touched. Of these, `sum.ll` and `max3.ll` shift content meaningfully (they use comparison surface); `hello.ll`, `list.ll`, `ws_main.ll` are likely unchanged (they don't use comparisons) but the test framework may re-write them anyway — that's fine.
|
|
|
|
- [ ] **Step 2: Re-run the snapshot test without UPDATE_SNAPSHOTS**
|
|
|
|
Run: `cargo test --workspace --quiet -p ail --test ir_snapshot 2>&1 | tail -5`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 3: Manual visual inspection**
|
|
|
|
Read `crates/ail/tests/snapshots/sum.ll` and `max3.ll`. Confirm the comparison IR shape is now:
|
|
|
|
```
|
|
%r = call i1 @ail_prelude_eq__Int(i64 ..., i64 ...) ...
|
|
; or with alwaysinline attribute on the define for eq__Int:
|
|
define i1 @ail_prelude_eq__Int(i64, i64) alwaysinline { ... }
|
|
```
|
|
|
|
And the `lower_eq` direct-emit path (`icmp eq i64` inline in `main`'s body) is GONE.
|
|
|
|
If the snapshot still contains an inline `icmp` from a direct-emit path: a callsite was missed in Task 7. Investigate.
|
|
|
|
---
|
|
|
|
## Task 10: Prose-projection cleanup — delete dead comparator binop_info arms
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-prose/src/lib.rs:546-563` (delete 6 comparator arms in `binop_info`)
|
|
- Modify: `crates/ailang-prose/src/lib.rs:1952` (delete first comparator-infix test)
|
|
- Modify: `crates/ailang-prose/src/lib.rs:2020-2062` (delete second & third comparator-infix tests)
|
|
|
|
After the operator-name deletion, `Term::App { fn: Var "==" }` (et al.) can only appear in JSON state that bypasses typecheck (hand-crafted, error-path). Prose-render's `binop_info` table currently treats these as infix operators — but the language no longer recognises them as such, so prose-rendering them as infix is misleading. Falling through to the standard fn-call form is more honest.
|
|
|
|
- [ ] **Step 1: Delete the 6 comparator arms in `binop_info`**
|
|
|
|
In `crates/ailang-prose/src/lib.rs:546-563`, locate `fn binop_info` (or the equivalent table-keyed-on-operator-name structure). Delete the 6 arms for `==` / `!=` / `<` / `<=` / `>` / `>=`. The arithmetic arms (`+` / `-` / `*` / `/` / `%`) remain unchanged.
|
|
|
|
- [ ] **Step 2: Delete the 3 comparator-infix tests in `mod tests`**
|
|
|
|
Locate the in-source `#[cfg(test)] mod tests` at lines 1952 and 2020-2062. Identify the 3 tests that pin comparator-infix rendering — recon notes them at line 1952 and the 2020-2062 range. Delete each test fn (signature + body + the `#[test]` attribute line). Add a one-line `// removed YYYY-MM-DD per operator-routing-eq-ord: ==/!=/</<=/>/>= are no longer surface operators; their AST forms (if reachable via error paths) render via the standard fn-call shape, not infix.` comment at the deletion site of the first test.
|
|
|
|
- [ ] **Step 3: Run the prose-suite tests**
|
|
|
|
Run: `cargo test --workspace --quiet -p ailang-prose 2>&1 | tail -10`
|
|
Expected: `0 failed`. The arithmetic-infix tests stay GREEN. The deleted comparator-infix tests are gone (no replacement).
|
|
|
|
- [ ] **Step 4: Run the full workspace test suite**
|
|
|
|
Run: `cargo test --workspace --quiet 2>&1 | tail -10`
|
|
Expected: `0 failed`.
|
|
|
|
---
|
|
|
|
## Task 11: Contract updates — 5 design files
|
|
|
|
**Files:**
|
|
- Modify: `design/contracts/0005-float-semantics.md:10-31, :98-99`
|
|
- Modify: `design/contracts/0017-prelude-classes.md:5-13`
|
|
- Modify: `design/contracts/0011-str-abi.md:40-42`
|
|
- Modify: `design/contracts/0010-scope-boundaries.md:42-96` (operator-name mentions + Pattern::Lit-to-`==` clause)
|
|
- Modify: `design/models/0001-authoring-surface.md:58`
|
|
|
|
- [ ] **Step 1: Update `float-semantics.md`**
|
|
|
|
In `design/contracts/0005-float-semantics.md:10-14`, the bullet currently reads:
|
|
|
|
```markdown
|
|
- Every individual builtin (`+`/`-`/`*`/`/`/`neg`/`<`/`==`/...) lowers
|
|
to a single LLVM IR instruction on the Float arm:
|
|
`fadd/fsub/fmul/fdiv double`, `fneg double`, `fcmp olt/ole/ogt/oge
|
|
double`, `fcmp oeq double`, `fcmp une double` (for `!=`). On a
|
|
fixed `(target triple, LLVM version)` pair, the bit pattern of
|
|
the result of any single op is reproducible.
|
|
```
|
|
|
|
Rewrite as:
|
|
|
|
```markdown
|
|
- Every individual arithmetic builtin (`+`/`-`/`*`/`/`/`neg`) lowers
|
|
to a single LLVM IR instruction on the Float arm: `fadd/fsub/fmul/fdiv
|
|
double`, `fneg double`. Float comparison goes through the named
|
|
prelude fns `float_eq` / `float_ne` / `float_lt` / `float_le` /
|
|
`float_gt` / `float_ge` (Float has no Eq/Ord instance — see
|
|
[Prelude (built-in) classes](prelude-classes.md)); each fn lowers
|
|
to a single `fcmp` instruction (`oeq` / `une` / `olt` / `ole` /
|
|
`ogt` / `oge` respectively) via the codegen intercept with the
|
|
`alwaysinline` attribute, so the call folds to one instruction
|
|
at every use site. On a fixed `(target triple, LLVM version)`
|
|
pair, the bit pattern of the result of any single op is
|
|
reproducible.
|
|
```
|
|
|
|
Lines 22-31 — the present-tense `==`/`!=` paragraphs (NaN-`==`-returns-false; `!=` returns true on NaN) — rewrite to name `float_eq` / `float_ne` instead of the operators:
|
|
|
|
```markdown
|
|
- `float_eq` returns `false` whenever either operand is NaN
|
|
(`fcmp oeq` is the ordered-equal predicate; ordered = both
|
|
operands non-NaN).
|
|
- `float_ne` returns `true` whenever either operand is NaN
|
|
(`fcmp une` is the unordered-or-not-equal predicate). This matches Rust
|
|
`f64::ne` and IEEE-`!=` exactly.
|
|
```
|
|
|
|
Line 98-99 (NaN-rendering caveat) — strip the `==`/`<` mentions; reword the cross-reference to use the named-fn surface.
|
|
|
|
- [ ] **Step 2: Update `prelude-classes.md`**
|
|
|
|
In `design/contracts/0017-prelude-classes.md:5-13`, the current text describes Eq/Ord with primitive instances for Int/Bool/Str. Update:
|
|
|
|
- Extend the instance list with Eq Unit.
|
|
- Add a paragraph after the existing Eq/Ord/Show description naming the six `float_*` fns as the Float-comparison surface (each `Float -> Float -> Bool` without a class constraint; lowered to a single `fcmp` via codegen intercept; replaces the deleted polymorphic `==`/`<`/etc. operators on Float).
|
|
- The Float-no-Eq/Ord clause gains `→ use float_eq / float_lt for explicit IEEE-aware comparison` as a one-line cross-reference.
|
|
|
|
- [ ] **Step 3: Update `str-abi.md:40-42`**
|
|
|
|
In `design/contracts/0011-str-abi.md:40-42`, the current clause reads (paraphrase from recon): "`==`, `<`, `<=`, `>`, `>=` REMAIN primitive operators". Rewrite to describe the class-method dispatch path: equality on Str dispatches via `prelude.Eq.eq` (Str instance, lowered to `@ail_str_eq` via try_emit_primitive_instance_body::eq__Str); ordering on Str dispatches via `prelude.Ord.compare` (Str instance, lowered to `@ail_str_compare`).
|
|
|
|
- [ ] **Step 4: Update `scope-boundaries.md:42-96`**
|
|
|
|
Read `design/contracts/0010-scope-boundaries.md:42-96` in full. Per recon, multiple clauses describe the dying surface:
|
|
- "`==` is polymorphic"
|
|
- "`!=` for Float uses `fcmp UNE`"
|
|
- "`Pattern::Lit` to `Term::If` on `==`"
|
|
- "any literal kind whose `==` is supported"
|
|
|
|
For each:
|
|
- "`==` is polymorphic" → rewrite to "`eq` (class-method) dispatches via `prelude.Eq` per arg type"
|
|
- "`!=` for Float uses `fcmp UNE`" → rewrite to "`float_ne` lowers to `fcmp une`"
|
|
- "`Pattern::Lit` to `Term::If` on `==`" → rewrite to "`Pattern::Lit` desugars to `(if (eq sv lit) body fall_k)` via desugar::build_eq"
|
|
- "any literal kind whose `==` is supported" → rewrite to "any literal kind whose `eq` dispatch resolves to a known primitive instance (Int/Bool/Str/Unit; Float Patterns are hard-rejected per float-semantics.md)"
|
|
|
|
- [ ] **Step 5: Update `authoring-surface.md:58`**
|
|
|
|
In `design/models/0001-authoring-surface.md:58`, the current example lists `==`, `<=` among operators. Drop `==` and `<=` from the example; the operator list becomes arithmetic-only (`+`, `-`, `*`, `/`, `%`). Add a follow-up sentence: "Comparison and equality are class methods (`eq` / `compare`), not operators — see [Prelude classes](../contracts/0017-prelude-classes.md)."
|
|
|
|
- [ ] **Step 6: Verify honesty-rule pin still passes**
|
|
|
|
Run: `cargo test --workspace --quiet --test docs_honesty_pin 2>&1 | tail -10`
|
|
Expected: PASS. The honesty-pin scans for forward-intent / future-tense / aspirational language; the contract updates introduce no such language (all rewrites are present-tense and describe present behaviour).
|
|
|
|
If a new failure surfaces (e.g. a token like `polymorphic ==` is somehow flagged), fix the wording inline.
|
|
|
|
- [ ] **Step 7: Run the full workspace test suite**
|
|
|
|
Run: `cargo test --workspace --quiet 2>&1 | tail -10`
|
|
Expected: `0 failed`.
|
|
|
|
---
|
|
|
|
## Task 12: Acceptance gate — workspace + bench + spec criteria
|
|
|
|
**Files:**
|
|
- Read-only verification across the whole working tree.
|
|
|
|
- [ ] **Step 1: Full workspace test suite**
|
|
|
|
Run: `cargo test --workspace 2>&1 | tail -30`
|
|
Expected: every binary line ends `0 failed`. Total pass-count delta vs. the milestone start equals the 5 new tests added in Task 1, minus any test-scaffold deletions in Tasks 5/10. Capture the per-binary pass counts for the iter commit body.
|
|
|
|
- [ ] **Step 2: Bench corpus — throughput + latency**
|
|
|
|
Run: `bench/check.py 2>&1 | tail -20`
|
|
Expected: exit 0; `0 regressed` across all metrics. The expectation is the post-`alwaysinline` IR shape collapses primitive-eq calls to single icmp instructions under -O2 (deterministic), matching the pre-milestone direct-emit performance.
|
|
|
|
If regressed metrics > 0: the `alwaysinline` mitigation is not folding the call at the expected use site. Investigate:
|
|
1. Inspect the regressed workload's `--emit-ir --opt=O2` output; confirm the call is inlined (no `call @ail_prelude_eq__Int` remains).
|
|
2. If the call is inlined but performance is worse: there's a different codegen-shape change (e.g. extra `phi` node, missed dead-block elimination); deeper investigation.
|
|
3. If the call is NOT inlined: `alwaysinline` attribute is silently being stripped — investigate `emit_fn`'s attribute emission for that symbol.
|
|
|
|
- [ ] **Step 3: Bench corpus — compile**
|
|
|
|
Run: `bench/compile_check.py 2>&1 | tail -10`
|
|
Expected: exit 0; `0 regressed`.
|
|
|
|
- [ ] **Step 4: Bench corpus — cross-language**
|
|
|
|
Run: `bench/cross_lang.py 2>&1 | tail -10`
|
|
Expected: exit 0; `0 regressed`.
|
|
|
|
- [ ] **Step 5: Acceptance grep — no live `==` / `<` / etc. in deleted layers**
|
|
|
|
Run:
|
|
```
|
|
grep -rn '"=="\|"!="\|"<="\|">="\|"<"\|">"' crates/ailang-check/src/builtins.rs crates/ailang-codegen/src/synth.rs crates/ailang-codegen/src/lib.rs crates/ailang-core/src/desugar.rs 2>&1 | grep -v '^\s*//' | head -30
|
|
```
|
|
Expected: zero live (non-comment) hits in `env.globals.insert("==", …)`, `Type::Var { name: "==" }`, or `Some((..., "...))` table-entry shapes. Comment-only hits (historical references in commit-body documentation) are acceptable.
|
|
|
|
If live hits remain: a deletion site was missed in Task 7 — find and remove.
|
|
|
|
- [ ] **Step 6: Acceptance grep — no live operator-form in any fixture**
|
|
|
|
Run: `grep -rln '(app == \|(app != \|(app < \|(app <= \|(app > \|(app >=' examples/ | wc -l`
|
|
Expected: `0`.
|
|
|
|
- [ ] **Step 7: Prelude content check**
|
|
|
|
Run: `grep -E 'instance.*Eq.*Unit|fn float_eq|P2 follow-up' examples/prelude.ail`
|
|
Expected:
|
|
- `(class prelude.Eq)` followed within 5 lines by `(type (con Unit))` — `instance Eq Unit` present
|
|
- `(fn float_eq` present
|
|
- NO `P2 follow-up` (the line-9 comment has been removed)
|
|
|
|
- [ ] **Step 8: Spec acceptance criteria 1-11 cross-check**
|
|
|
|
Walk the spec's `## Acceptance criteria` section (criteria 1-11) one-by-one. Each criterion has a corresponding step above OR is structurally guaranteed by the per-task gates. Confirm each. If any criterion is unmet, the iter is BLOCKED — write to `BLOCKED.md` per the implement-skill protocol with the specific unmet criterion named.
|
|
|
|
- [ ] **Step 9: Write the iter stats file**
|
|
|
|
The implement-orchestrator writes the per-iter stats file directly. Path: `bench/orchestrator-stats/2026-05-20-iter-operator-routing-eq-ord.1.json`. The implementer skill specifies the format.
|
|
|
|
- [ ] **Step 10: Hand off to Boss for commit**
|
|
|
|
Working tree contains the iter's full set of changes. Boss inspects via `git status` / `git diff --stat`, drafts iter commit body, commits as `iter operator-routing-eq-ord.1 (DONE N/N): drop comparator builtins, route through Eq/Ord` with `closes #1` trailer.
|