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.
547 lines
24 KiB
Markdown
547 lines
24 KiB
Markdown
# Operator routing through Eq / Ord — Design Spec
|
||
|
||
**Date:** 2026-05-20
|
||
**Status:** Draft — awaiting user spec review
|
||
**Authors:** Brummel (orchestrator) + Claude
|
||
|
||
Resolves Gitea #1. Realises the "P2 follow-up" called out in
|
||
`examples/prelude.ail:9` — routing the surface comparator names
|
||
through the `prelude.Eq` / `prelude.Ord` classes instead of the
|
||
hard-coded built-in comparator table.
|
||
|
||
## Goal
|
||
|
||
Today AILang has two parallel paths for comparison:
|
||
|
||
- **Built-in operators** `==`, `!=`, `<`, `<=`, `>`, `>=` — special-
|
||
cased in codegen with hardcoded polymorphism over fixed primitive
|
||
type sets (`==` over `{Int, Bool, Str, Unit, Float}`; the other
|
||
five over `{Int, Float}`). User ADTs are rejected at codegen with
|
||
a generic error.
|
||
- **Typeclass methods** `prelude.Eq.eq` and `prelude.Ord.compare`
|
||
with instances for `{Int, Bool, Str}`, plus the polymorphic free-
|
||
fn helpers `ne`/`lt`/`le`/`gt`/`ge`. Float and Unit have no Eq/Ord
|
||
instance by design (Float: partial orderability per
|
||
[float-semantics](../../design/contracts/0005-float-semantics.md);
|
||
Unit: oversight that today's `(app == () ())` builtin path hides).
|
||
|
||
The two paths are redundant — `instance Eq Int` body literally
|
||
calls `(app == x y)`, so the class layer today delegates back to
|
||
the builtin. The redundancy is structurally observable: every
|
||
release pays the cost of maintaining both. User ADTs cannot
|
||
participate in `==` at all, even when the LLM-author has written
|
||
`instance Eq Point` — the operator is sealed at the primitive
|
||
types.
|
||
|
||
After this milestone, the LLM-author writes `(app eq p1 p2)` for
|
||
user types and gets the natural typeclass-dispatched behaviour;
|
||
`==`/`!=`/`<`/`<=`/`>`/`>=` as identifiers are gone from the
|
||
language; Float comparison is named explicitly via `float_eq` /
|
||
`float_lt` / etc. (no IEEE-NaN-bug-class smuggled in via a
|
||
polymorphic `eq`).
|
||
|
||
## Architecture
|
||
|
||
Three layers shift in lockstep:
|
||
|
||
**Surface / parser:** unchanged. The Form-A tokens `==` / `!=` / `<`
|
||
/ `<=` / `>` / `>=` remain legal identifier strings at the parser
|
||
level — there is no syntactic change. They are simply not
|
||
recognised at typecheck or codegen.
|
||
|
||
**Typechecker:** the entries for `==` / `!=` / `<` / `<=` / `>` /
|
||
`>=` in the builtin signature table
|
||
(`crates/ailang-check/src/builtins.rs::install` — comparator loop
|
||
at lines 96-98, polymorphic `==` Forall block at lines 109-125,
|
||
list-side mirror at lines 303-308) are removed. Surface use of these
|
||
names resolves like any other unknown identifier — first via the
|
||
class-method-dispatch index per
|
||
[method-dispatch](../../design/contracts/0016-method-dispatch.md), which
|
||
finds no candidate (the method name is `eq`, not `==`), then via
|
||
fn lookup, which also finds none, then fails with the standard
|
||
`unknown variable` diagnostic. The diagnostic does NOT special-case
|
||
the comparator names with a "did you mean `eq`?" hint — the
|
||
milestone is a clean break, not a transition aid; AILang has no
|
||
external users requiring migration.
|
||
|
||
All `(app eq …)` / `(app compare …)` / `(app ne …)` / `(app lt …)`
|
||
/ etc. resolve through the existing class-method-dispatch machinery
|
||
documented in
|
||
[method-dispatch](../../design/contracts/0016-method-dispatch.md).
|
||
No new dispatch rule.
|
||
|
||
**Codegen:** `try_emit_primitive_instance_body` in
|
||
`crates/ailang-codegen/src/lib.rs` (the intercept already used for
|
||
`eq__Str`, `compare__Int`, `compare__Bool`, `compare__Str`)
|
||
gains three new arms — `eq__Int`, `eq__Bool`, `eq__Unit`. Every
|
||
primitive instance body emitted via this intercept carries an
|
||
`alwaysinline` attribute on the generated LLVM function so the
|
||
`-O0` build path collapses the trivial body to its single
|
||
`icmp`/`fcmp`/`call` instruction at every use site, matching
|
||
today's direct-emit IR shape under `-O0` as well as `-O2`.
|
||
|
||
Six new prelude free functions ship for Float comparison:
|
||
`float_eq`, `float_ne`, `float_lt`, `float_le`, `float_gt`,
|
||
`float_ge`, each `Float -> Float -> Bool` without a class
|
||
constraint. Their bodies are intercept-lowered to the corresponding
|
||
`fcmp` instruction (`oeq` / `une` / `olt` / `ole` / `ogt` / `oge`),
|
||
preserving the bit-exact IEEE semantics
|
||
[float-semantics](../../design/contracts/0005-float-semantics.md)
|
||
guarantees today for `==` / `<` etc. on Float — the guarantee is
|
||
transferred from the deleted operator names to the named Float
|
||
fns.
|
||
|
||
The lit-pattern desugar in
|
||
`crates/ailang-core/src/desugar.rs::build_eq` (line 1099) rewrites
|
||
`(pat-lit "hi")` to `(if (eq sv "hi") body fall_k)` instead of the
|
||
current `(if (== sv "hi") body fall_k)` — the literal symbol
|
||
`Term::Var { name: "==" }` at line 1109 becomes `name: "eq"`.
|
||
This is the single live `==`-emitting desugar site; everything
|
||
else mechanically migrates as fixture or test-scaffold work, not
|
||
as desugar-pass changes. This is the non-obvious mechanical
|
||
follow: lit patterns inherit equality dispatch by construction.
|
||
|
||
## Concrete code shapes
|
||
|
||
### The LLM-author program this milestone enables
|
||
|
||
```
|
||
(module eq_user_adt_smoke
|
||
(data Point
|
||
(ctor Point (con Int) (con Int)))
|
||
(instance
|
||
(class prelude.Eq)
|
||
(type (con Point))
|
||
(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 p1 p2))
|
||
(app print (app eq p1 p3)))))))))
|
||
```
|
||
|
||
Expected stdout: `true\nfalse\n`. This is the feature-acceptance
|
||
clause-1 evidence: the LLM-natural form that is **structurally
|
||
impossible to write today**. Today `(app == p1 p2)` is rejected at
|
||
codegen (Point is not in the polymorphic `==` type set);
|
||
`(app eq p1 p2)` fires `NoInstance Eq Point` even after the author
|
||
writes the instance, because the body's nested `(app eq a1 a2)` on
|
||
Int delegates to `(app == x y)` which then has to thread back via
|
||
the polymorphic-`==` mechanism — the chain works but the author
|
||
gets the same outcome as writing `==` directly, no gain.
|
||
|
||
After this milestone, both the outer `(app eq p1 p2)` and the
|
||
nested `(app eq a1 a2)` resolve via the canonical class-dispatch
|
||
path: the outer to `eq_user_adt_smoke.eq__Point` (the user
|
||
instance), the nested to `prelude.eq__Int` (the primitive
|
||
instance, intercept-lowered to `icmp eq i64` and inlined back to
|
||
the call site).
|
||
|
||
### The Klausel-3 discriminator (must fail at typecheck)
|
||
|
||
```
|
||
(module eq_float_must_fail
|
||
(fn main
|
||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||
(params)
|
||
(body (app print (app eq 1.5 1.5)))))
|
||
```
|
||
|
||
Must produce a typecheck-time diagnostic — `NoInstance Eq Float`
|
||
with a follow-up sentence `Float has no Eq instance by design
|
||
(partial orderability per design/contracts/0005-float-semantics.md); use
|
||
float_eq for explicit IEEE-aware comparison`. The diagnostic is the
|
||
existing `NoInstance` channel; the Float-aware addendum is the
|
||
existing Float-specific hint in `crates/ailang-check/src/lib.rs`
|
||
around line 860 (which already special-cases Eq/Ord-at-Float
|
||
diagnostics), extended to name `float_eq` / `float_lt` as the
|
||
explicit alternative.
|
||
|
||
### The Float-with-named-fn happy path
|
||
|
||
```
|
||
(module float_compare_smoke
|
||
(fn main
|
||
(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)))))))
|
||
```
|
||
|
||
Expected stdout: `true\ntrue\nfalse\n`. Demonstrates that Float
|
||
comparison remains available via the named-fn route. The
|
||
implementation lowers each `float_*` call to a single `fcmp`
|
||
instruction via the intercept mechanism — IR-byte-equivalent to
|
||
today's `(app == 1.5 1.5)` after the operator name is removed.
|
||
|
||
### Migration pattern for existing fixtures
|
||
|
||
```
|
||
; examples/eq_demo.ail (current)
|
||
(seq (app print (app == 5 5))
|
||
(seq (app print (app == true false))
|
||
(app print (app == "hi" "hi"))))
|
||
|
||
; examples/eq_demo.ail (after this milestone)
|
||
(seq (app print (app eq 5 5))
|
||
(seq (app print (app eq true false))
|
||
(app print (app eq "hi" "hi"))))
|
||
```
|
||
|
||
≈10 fixtures under `examples/` use one or more of the deleted
|
||
operator names. Each migrates to the corresponding class-method
|
||
or named-fn (Float case): `==` → `eq`, `!=` → `ne`, `<` → `lt`,
|
||
`<=` → `le`, `>` → `gt`, `>=` → `ge`. The Float-operator uses in
|
||
`bench_compute_collatz.ail` (and any other Float-touching bench
|
||
fixture) migrate to `float_lt` / `float_eq` / etc. The exact
|
||
fixture list is enumerated by the planner via
|
||
`grep -rln '(app == \|(app != \|(app < \|(app <= \|(app > \|(app >=' examples/`.
|
||
|
||
### Implementation shape (secondary — supporting detail)
|
||
|
||
The load-bearing code changes (planner derives exact paths/line
|
||
numbers; this section establishes shape):
|
||
|
||
`crates/ailang-codegen/src/synth.rs::builtin_binop_typed` — the
|
||
comparator arms vanish:
|
||
|
||
```rust
|
||
// Before — 10 arms for ==/!=/</<=/>/>= over Int+Float
|
||
match (name, is_int, is_float) {
|
||
("+", true, _) => Some(("add", "i64", "i64")),
|
||
("+", _, true) => Some(("fadd", "double", "double")),
|
||
// ... arithmetic ...
|
||
("!=", true, _) => Some(("icmp ne", "i64", "i1")),
|
||
("<", true, _) => Some(("icmp slt", "i64", "i1")),
|
||
("<=", true, _) => Some(("icmp sle", "i64", "i1")),
|
||
(">", true, _) => Some(("icmp sgt", "i64", "i1")),
|
||
(">=", true, _) => Some(("icmp sge", "i64", "i1")),
|
||
("!=", _, true) => Some(("fcmp une", "double", "i1")),
|
||
("<", _, true) => Some(("fcmp olt", "double", "i1")),
|
||
("<=", _, true) => Some(("fcmp ole", "double", "i1")),
|
||
(">", _, true) => Some(("fcmp ogt", "double", "i1")),
|
||
(">=", _, true) => Some(("fcmp oge", "double", "i1")),
|
||
_ => None,
|
||
}
|
||
|
||
// After — arithmetic only
|
||
match (name, is_int, is_float) {
|
||
("+", true, _) => Some(("add", "i64", "i64")),
|
||
("+", _, true) => Some(("fadd", "double", "double")),
|
||
("-", true, _) => Some(("sub", "i64", "i64")),
|
||
("-", _, true) => Some(("fsub", "double", "double")),
|
||
("*", true, _) => Some(("mul", "i64", "i64")),
|
||
("*", _, true) => Some(("fmul", "double", "double")),
|
||
("/", true, _) => Some(("sdiv", "i64", "i64")),
|
||
("/", _, true) => Some(("fdiv", "double", "double")),
|
||
("%", true, _) => Some(("srem", "i64", "i64")),
|
||
_ => None,
|
||
}
|
||
```
|
||
|
||
`crates/ailang-codegen/src/lib.rs` — the `==`-polymorphic-dispatch
|
||
special case (lib.rs around 2947 plus the Int/Bool/Str/Unit
|
||
matching) is gone; replaced by `try_emit_primitive_instance_body`
|
||
arms for the new symbol set:
|
||
|
||
```rust
|
||
// New arms in try_emit_primitive_instance_body's match
|
||
match instance_key {
|
||
"eq__Int" => emit_icmp_eq("i64"), // new
|
||
"eq__Bool" => emit_icmp_eq("i1"), // new
|
||
"eq__Unit" => emit_const_i1_true(), // new
|
||
"eq__Str" => /* existing — @ail_str_eq */,
|
||
"compare__Int" | "compare__Bool" | "compare__Str" => /* existing */,
|
||
// 6 new Float-named-fn arms
|
||
"float_eq" => emit_fcmp("oeq", "double"),
|
||
"float_ne" => emit_fcmp("une", "double"),
|
||
"float_lt" => emit_fcmp("olt", "double"),
|
||
"float_le" => emit_fcmp("ole", "double"),
|
||
"float_gt" => emit_fcmp("ogt", "double"),
|
||
"float_ge" => emit_fcmp("oge", "double"),
|
||
_ => return None,
|
||
}
|
||
```
|
||
|
||
Each emit_* helper adds `attributes #N = { alwaysinline … }` to
|
||
the generated function header so the `-O0` build path inlines the
|
||
single-instruction body at every call site, matching today's
|
||
direct-emit shape.
|
||
|
||
`examples/prelude.ail` — three additions, three rewrites:
|
||
|
||
```
|
||
; New: Eq Unit instance (preserves today's `==`-on-Unit capability).
|
||
(instance
|
||
(class Eq)
|
||
(type (con Unit))
|
||
(doc "Eq Unit. Body is constant true — Unit is single-inhabitant
|
||
so all values compare equal. Lowered via
|
||
try_emit_primitive_instance_body::eq__Unit to `ret i1 1`.")
|
||
(method eq
|
||
(body (lam (params (typed x a) (typed y a)) (ret (con Bool))
|
||
(body true)))))
|
||
|
||
; Eq Int — body becomes a placeholder (lambda returning false);
|
||
; codegen intercept overrides to icmp eq i64. Matches Ord Int's
|
||
; existing placeholder pattern.
|
||
(instance
|
||
(class Eq)
|
||
(type (con Int))
|
||
(doc "Eq Int. Body is placeholder for round-trip stability;
|
||
codegen intercept emits `icmp eq i64` with alwaysinline.")
|
||
(method eq
|
||
(body (lam (params (typed x a) (typed y a)) (ret (con Bool))
|
||
(body false)))))
|
||
|
||
; Eq Bool — same placeholder shape.
|
||
; Eq Str — body unchanged in shape (already placeholder-style),
|
||
; intercept already exists.
|
||
|
||
; New Float-fns: six entries.
|
||
(fn float_eq
|
||
(doc "IEEE-aware Float equality. `float_eq x y` returns true iff
|
||
both operands are non-NaN and bit-equal. Lowered to
|
||
`fcmp oeq double`. Replaces the milestone-deleted `(app == x y)`
|
||
on Float.")
|
||
(type (fn-type (params (con Float) (con Float)) (ret (con Bool))))
|
||
(params x y)
|
||
(body (lam (params (typed x (con Float)) (typed y (con Float))) (ret (con Bool))
|
||
(body false))))
|
||
; ... float_ne / float_lt / float_le / float_gt / float_ge analogous.
|
||
|
||
; Line-9 comment on `class Eq` (the "P2 follow-up" note) is removed —
|
||
; routing-through-Eq.eq is no longer follow-up, it's the present.
|
||
```
|
||
|
||
## Components
|
||
|
||
The milestone touches seven layers, all in one cohesive iter:
|
||
|
||
1. **Typechecker builtin table** (`crates/ailang-check/src/builtins.rs`)
|
||
— six operator-name entries removed (`install` lines 96-98 +
|
||
109-125); list-side mirror at lines 303-308 trimmed; the in-
|
||
source `mod tests` helpers `eq_app` and the polymorphic-`==`
|
||
regression tests (lib.rs:6092, lib.rs:6220 — both inside
|
||
`#[cfg(test)]`) migrate to the `eq` symbol so the test suite
|
||
stays green over the new dispatch path.
|
||
2. **Codegen builtin binop table** (`crates/ailang-codegen/src/synth.rs`)
|
||
— ten comparator arms removed from `builtin_binop_typed`; the
|
||
table reduces to the arithmetic core.
|
||
3. **Codegen primitive-instance intercept**
|
||
(`crates/ailang-codegen/src/lib.rs`) — `try_emit_primitive_instance_body`
|
||
gains three Eq arms (`eq__Int`, `eq__Bool`, `eq__Unit`) and six
|
||
Float-fn arms (`float_eq`/`float_ne`/`float_lt`/`float_le`/`float_gt`/
|
||
`float_ge`); each generated function header carries `alwaysinline`.
|
||
The existing `==`-polymorphic-dispatch lowering (lib.rs around
|
||
2947, the Int/Bool/Str/Unit/Float matching for `==`) is deleted.
|
||
4. **Lit-pattern desugar** (`crates/ailang-core/src/desugar.rs`)
|
||
— `build_eq` (line 1099) substitutes `eq` for `==` in the
|
||
generated `(if … body fall_k)` form. This is the only live
|
||
desugar-pass change. Float-pattern rejection per
|
||
`float-semantics.md` is unaffected (still hard-rejected at
|
||
typecheck, before desugar runs).
|
||
|
||
*In-source test scaffolds* in the same file
|
||
(`desugar.rs:2414`, inside `#[cfg(test)] mod tests`) and in
|
||
`crates/ailang-check/src/lib.rs` (lines 6092 and 6220, also
|
||
`#[cfg(test)]`) construct hand-built `Term::Var { name: "==" }`
|
||
in AST literals to drive `==`-specific assertions; these
|
||
migrate to `name: "eq"` alongside the production change.
|
||
Counted as test-scaffold migration, not desugar-pass work.
|
||
5. **Prelude module** (`examples/prelude.ail`) — adds `instance Eq Unit`;
|
||
rewrites `Eq Int` / `Eq Bool` bodies to placeholder pattern
|
||
(codegen intercept does the work); adds six `float_*` free fns;
|
||
removes the "P2 follow-up" comment on `class Eq`. Prelude module
|
||
hash will shift; the hash pin in
|
||
`crates/ailang-surface/tests/prelude_module_hash_pin.rs`
|
||
regenerates as part of the iter.
|
||
6. **Fixture migration** — ≈10 files under `examples/` rewritten
|
||
from operator-form to method-form. The planner enumerates the
|
||
exact set via grep and assigns it as a discrete task.
|
||
7. **Contracts** — three updates:
|
||
- `design/contracts/0005-float-semantics.md` lines 10-14: the
|
||
guarantees about `+`/`-`/etc. and `==`/`<`/`!=` lowering to
|
||
single LLVM instructions are retained for arithmetic but
|
||
transferred from `==`/`<`/`!=` to `float_eq`/`float_lt`/`float_ne`/etc.
|
||
for the comparison set. The NaN-spelling caveat is unchanged.
|
||
- `design/contracts/0017-prelude-classes.md`: instance list extends
|
||
with `Eq Unit`; a new paragraph documents the six `float_*` fns
|
||
as the Float-comparison surface; the Float-no-Eq/Ord clause
|
||
gains a `→ use float_eq` cross-reference.
|
||
- `design/contracts/0013-typeclasses.md`: no change. Class schema,
|
||
dispatch rule, and diagnostics are stable; the milestone only
|
||
activates existing machinery for `eq`/`compare` over more
|
||
types.
|
||
|
||
## Data flow
|
||
|
||
A surface `(app eq p1 p2)` flows through the existing pipeline:
|
||
|
||
1. **Parse** — `Term::App { fn: Var "eq", args: [p1, p2] }`. No
|
||
change from today.
|
||
2. **Typecheck** — class-method-dispatch (per
|
||
`design/contracts/0016-method-dispatch.md`) consults
|
||
`method_to_candidate_classes["eq"]` → `{prelude.Eq}`. Singleton
|
||
class survivor; type-driven filter against the workspace
|
||
registry for the resolved type:
|
||
- `p1, p2 : Int` → `prelude.eq__Int`
|
||
- `p1, p2 : Point` → `eq_user_adt_smoke.eq__Point`
|
||
- `p1, p2 : Float` → no instance → `NoInstance Eq Float` with
|
||
Float-aware hint pointing at `float_eq`.
|
||
3. **Mono** — synth produces the instance-body symbol (existing
|
||
pass; per `design/contracts/0013-typeclasses.md` invariants 1-3).
|
||
4. **Codegen** — emits a call to the resolved instance fn. For
|
||
primitive instances, the body is intercept-lowered to a single
|
||
icmp/fcmp/call; `alwaysinline` attribute ensures the call folds
|
||
at every use site under `-O0` as well as `-O2`. For user-ADT
|
||
instances, codegen emits the full lambda body normally; the
|
||
call to the instance fn is normal (no intercept).
|
||
|
||
The data-flow is conceptually identical to today's `show` /
|
||
`compare` paths — the milestone unifies `eq` into the same shape.
|
||
|
||
## Error handling
|
||
|
||
Three diagnostic situations:
|
||
|
||
**`NoInstance Eq Float` / `NoInstance Ord Float`** — fires at
|
||
typecheck for any `(app eq …)` or `(app compare …)` (or
|
||
`ne`/`lt`/`le`/`gt`/`ge`) on Float. The existing Float-aware
|
||
addendum (lib.rs:860-873) is extended:
|
||
|
||
```
|
||
Eq has no instance at Float — Float has no Eq/Ord instance by
|
||
design (partial orderability per design/contracts/0005-float-semantics.md);
|
||
use float_eq for explicit IEEE-aware comparison.
|
||
```
|
||
|
||
`compare`/`Ord` use case names `float_lt` / `float_compare` (the
|
||
latter doesn't ship as a fn — `compare` returns `Ordering`, no
|
||
Float equivalent — so the hint says "use float_lt / float_eq for
|
||
explicit IEEE-aware comparison").
|
||
|
||
**`NoInstance Eq <UserType>`** — fires when the LLM-author calls
|
||
`(app eq p1 p2)` on a user type without having written the
|
||
instance. Standard `NoInstance` channel; no special-case wording.
|
||
The author writes `instance Eq <UserType>` by hand (no deriving).
|
||
|
||
**`unknown variable: ==` (et al.)** — fires when the LLM-author
|
||
writes `(app == 5 5)` after the milestone. The diagnostic does
|
||
NOT carry a "did you mean `eq`?" hint. This is a clean break, not
|
||
a transition; AILang has no external users to migrate. The lack of
|
||
hint is documented in the `prelude-classes.md` contract update
|
||
("comparator-operator names are not part of the language").
|
||
|
||
## Testing strategy
|
||
|
||
Five test artefacts, all in `crates/ail/tests/`:
|
||
|
||
1. **`eq_user_adt_smoke_e2e.rs`** — drives the north-star fixture
|
||
`examples/eq_user_adt_smoke.ail` through `ail build` + binary
|
||
execution; asserts stdout `"true\nfalse\n"`. This is the
|
||
milestone-defining E2E. Protects: user-ADT-Eq path works end-to-
|
||
end, including cross-module reference from user-instance body
|
||
to `prelude.eq__Int`.
|
||
2. **`eq_float_must_fail_pin.rs`** — drives the must-fail fixture
|
||
`examples/eq_float_must_fail.ail`; asserts `ail check` exits
|
||
non-zero with stderr containing `NoInstance Eq Float` AND
|
||
`float_eq`. Protects: the Klausel-3 discriminator stays
|
||
discriminative; Float-aware diagnostic stays informative.
|
||
3. **`float_compare_smoke_e2e.rs`** — drives the Float-named-fn
|
||
fixture; asserts stdout `"true\ntrue\nfalse\n"`. Protects: the
|
||
six `float_*` fns lower correctly to fcmp; replacement surface
|
||
for the deleted Float operators stays functional.
|
||
4. **`operator_names_unbound_pin.rs`** — drives a minimal fixture
|
||
`(app == 5 5)`; asserts `ail check` exits non-zero with stderr
|
||
containing `unknown variable: ==`. Protects: the operator-name
|
||
removal is durable; no codegen path silently re-introduces
|
||
them.
|
||
5. **`prelude_eq_alwaysinline_ir_pin.rs`** — compiles
|
||
`(app eq 5 5)` with `--emit-ir --opt=O0`; asserts the resulting
|
||
IR contains either `icmp eq i64` directly at the call site OR
|
||
the `alwaysinline` attribute on `@prelude_eq__Int` such that the
|
||
inliner is guaranteed to fold under `-O2`. Protects: the
|
||
`alwaysinline` mitigation is wired (not silently dropped); the
|
||
bench-gate risk is structurally bounded.
|
||
|
||
Plus: the bench corpus (latency + throughput + cross-lang) runs as
|
||
the acceptance gate. The expectation is 0/0/0 regressions; if
|
||
regressions appear, the `alwaysinline`-mitigation needs
|
||
investigation (likely codegen-bug) or the spec needs revisiting
|
||
toward α (call-site intercept).
|
||
|
||
Existing tests stay green by virtue of the fixture migration —
|
||
every `==`/`<`/etc. in test fixtures is rewritten alongside the
|
||
language change. The lit-pattern regression suite
|
||
(if exists in `crates/ailang-check/tests/` per planner-recon)
|
||
exercises `(case (pat-lit "hi") …)` paths after the desugar
|
||
rewrites to `eq`; these stay green provided the desugar is updated
|
||
correctly.
|
||
|
||
## Acceptance criteria
|
||
|
||
The milestone ships when all of the following hold:
|
||
|
||
1. `cargo test --workspace --quiet` — all binaries `0 failed`.
|
||
2. `examples/eq_user_adt_smoke.ail` exists and builds and runs and
|
||
prints `"true\nfalse\n"`; the corresponding E2E is green.
|
||
3. `examples/eq_float_must_fail.ail` exists and `ail check` rejects
|
||
it with `NoInstance Eq Float` and a `float_eq` hint.
|
||
4. `examples/float_compare_smoke.ail` exists and runs and prints
|
||
`"true\ntrue\nfalse\n"`.
|
||
5. `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`
|
||
returns matches only in comment / doc context, not as live
|
||
`env.globals.insert(...)`, `Type::Var { name: "==" }`, or
|
||
`Some((..., "...))` table entries.
|
||
Documentation comments referring historically to the removed
|
||
names are permitted only in commit-bodies and contract-history
|
||
sections, not as live code paths.
|
||
6. `grep -rln '(app == \|(app != \|(app < \|(app <= \|(app > \|(app >=' examples/`
|
||
returns zero matches (full fixture migration complete).
|
||
7. `examples/prelude.ail` contains `instance Eq Unit` and the six
|
||
`float_*` fns; the line-9 "P2 follow-up" comment on `class Eq`
|
||
is removed.
|
||
8. `bench/check.py` exit 0; `bench/compile_check.py` exit 0;
|
||
`bench/cross_lang.py` exit 0 (full bench-corpus green against
|
||
the post-milestone baseline regenerated by the iter).
|
||
9. `design/contracts/0005-float-semantics.md` updated to name `float_eq`
|
||
/ `float_lt` / `float_ne` / etc. as the comparison guarantees
|
||
(comparison guarantees on `==`/`<`/`!=` removed; arithmetic
|
||
guarantees on `+`/`-`/`*`/`/` unchanged).
|
||
10. `design/contracts/0017-prelude-classes.md` updated: `Eq Unit` in the
|
||
instance list; new section on the six Float-named comparison
|
||
fns; the Float-no-Eq/Ord clause carries a `→ use float_eq`
|
||
cross-reference.
|
||
11. Closes Gitea #1 via the iter-commit `closes #1` trailer.
|
||
|
||
Out of scope (tracked separately):
|
||
|
||
- **Deriving for Eq/Ord** — `typeclasses.md:177` "No deriving"
|
||
stands. LLM-author writes instance bodies by hand. If deriving
|
||
becomes a real friction point post-milestone, that is its own
|
||
spec (likely tied to Gitea #2 "22c typeclass corpus expansion").
|
||
- **Parameterised-ADT instances** (`instance Eq (List a)`, etc.)
|
||
— requires multi-parameter-class / constraint-propagation
|
||
infrastructure, part of Gitea #2.
|
||
- **`Eq` / `Ord` for the `Ordering` ADT itself** — no use case;
|
||
`Ordering` is consumed via `match`, not compared.
|
||
- **`and` / `or` as Builtins** — north-star fixture uses
|
||
`(if … … false)` for short-circuit conjunction. Adding `and` /
|
||
`or` as separate fns or operators is a separate concern; no
|
||
forcing here.
|