11 KiB
23 — Eq / Ord Prelude — Design Spec
Date: 2026-05-10 Status: Approved (user, 2026-05-10) Authors: Brummel (orchestrator) + Claude
Goal
Ship Eq and Ord typeclasses with primitive instances on Int,
Bool, and Str. Add the Ordering ADT (LT | EQ | GT) and five
free top-level utility functions (ne, lt, le, gt, ge)
defined through the class methods. Establish the partial-Eq/Ord
story for Float as a lived reality: Float has neither instance,
so polymorphic eq x y / compare x y over a Float fires
NoInstance at typecheck — exactly what DESIGN.md §"Float
semantics" prescribes.
Show, operator routing, print-rewire, and the heap-Str ABI are
out of scope here. Each is its own milestone with substantive
reasons (see "Out of scope").
Architecture
The milestone consumes the milestone-22 typeclass machinery
unchanged: class / instance schema, monomorphiser-emitted
<method>__<typesurfacename> symbols, instance-resolution at
typecheck. Three new artefacts:
- A prelude AILang module at
examples/prelude.ail.json— author-canonical Form-A JSON containing theEqandOrdclass definitions, six instances (Eq + Ord on Int / Bool / Str), theOrderingADT, and the five free utility functions. - Auto-loading of the prelude into every workspace via a small
change to
check_workspaceso the prelude module is reachable from any user module without an explicitimport. - Two new C-runtime functions in
runtime/str.c:ail_str_eq(a, b) -> bool(byte-equality on existing constant- string layouts) andail_str_compare(a, b) -> i32(lex comparison returning -1/0/+1). Both operate on existing constant-Str payloads; no heap allocation, no RC interaction. Codegen arms foreq__Strandcompare__Stremit calls to these.
Instance bodies for the non-Str primitives lower to existing IR
shapes: eq__Int to icmp eq i64, eq__Bool to icmp eq i1,
compare__Int to a branch ladder constructing LT / EQ / GT
ADT values per icmp slt / icmp eq results, compare__Bool
similarly.
The existing primitive operators (==, <, <=, >, >=)
remain primitive and per-type-routed at lowering, unchanged from
milestone 22. Routing them through the class methods is the
declared P2 follow-up; the redundancy between primitive == and
class eq x y on Int/Bool/Str is acknowledged and ratified for
this milestone as transitional.
Components (iterations)
| Iter | Scope |
|---|---|
| 23.1 | Ordering ADT (`data Ordering = LT |
| 23.2 | Add class Eq a where eq : (a borrow, a borrow) -> Bool + three instances (Eq Int, Eq Bool, Eq Str) to the prelude. New file runtime/str.c with ail_str_eq. Codegen arm for eq__Str emits call zext i1 @ail_str_eq(...). Codegen unit tests for eq__Int, eq__Bool, eq__Str. |
| 23.3 | Add class Ord a extends Eq where compare : (a borrow, a borrow) -> Ordering + three instances (Ord Int, Ord Bool, Ord Str). Extend runtime/str.c with ail_str_compare. Codegen arms for compare__Int, compare__Bool, compare__Str; each pattern-matches the underlying primitive comparison into the Ordering ADT. |
| 23.4 | Five free top-level functions in the prelude: ne : forall a. Eq a => (a borrow, a borrow) -> Bool (body: not (eq x y)); lt, le, gt, ge with forall a. Ord a => constraints, bodies pattern-matching compare against the right Ordering shape. |
| 23.5 | E2E fixtures: (a) positive — a polymorphic helper forall a. Ord a => (a borrow, a borrow) -> Bool invoked at three primitive types, prints true/false per case; (b) negative — eq f g where f, g : Float typecheck-rejects with NoInstance Eq Float matching DESIGN.md §"Float semantics"; (c) user-ADT integration — data IntBox = MkIntBox Int + instance Eq IntBox + instance Ord IntBox + polymorphic helper monomorphises to the user instances. DESIGN.md amendment: §"Decision 11 / Prelude (built-in) classes" updated to reflect Eq/Ord arrival in milestone 23 (Show still deferred). Roadmap moves Post-22 Prelude entry from [~] to [x] for the Eq/Ord half; Show half stays as a new entry. |
Data flow
Surface call (eq x y) with x : Int known from context:
- Typecheck.
eqresolves toforall a. Eq a => (a, a) -> Bool. Constraint solver matchesEq Intagainst theinstance Eq Intin the auto-loaded prelude; constraint discharged. - Monomorphisation (22b.3 path). Call rewritten to
eq__Int x y. Symbol synthesised in the same way 22's user-class fixture synthesisesfoo__IntBox. - Codegen.
eq__Intlowers to a singleicmp eq i64 %x, %yplus azext i1 i8(Bool is i8 in AILang's runtime).
For Str the codegen arm is a single call i1 @ail_str_eq(...)
into runtime/str.c. For compare on Int the arm is a branch
ladder (icmp slt for LT-or-not, icmp eq for EQ-or-GT)
returning the corresponding ADT-tag.
Polymorphic helpers (fn cmp_max : forall a. Ord a => (a, a) -> a)
work the same way: the typeclass-elaboration in 22b.3 inserts the
right monomorphised compare__T symbol per use site.
Error handling
NoInstance Eq Float/NoInstance Ord Float— fired at typecheck when class resolution selects Float. Diagnostic must cross-reference DESIGN.md §"Float semantics" so the LLM-author immediately learns the partial-Float story instead of guessing.NoInstance Eq <UserType>/NoInstance Ord <UserType>— re-uses 22's existing diagnostic path. No new wiring.OrphanInstance— Decision 11 unchanged. Prelude instances are non-orphan because both class and instance type live in the prelude module.- Codegen-side errors are not expected — the new arms (
eq__Str,compare__Str) are uniformly-shaped lowerings and reuse establishedlower_appmechanics. If a fall-through emerges, it must beCodegenError::Internal-shaped (per the iter-4.4 Floats fixup precedent), neverunimplemented!()panic.
Testing strategy
- Codegen unit tests per primitive method:
eq__Int,eq__Bool,eq__Str,compare__Int,compare__Bool,compare__Str. Each asserts the IR string contains the right intrinsic / call. - Workspace tests (
check_module/check_workspacepath):eqandcomparereturn correctBool/Orderingon each primitive.ne,lt,le,gt,geagree with their primitive- operator counterparts on a small suite of known cases.- Negative:
eq/compareover a Float typecheck-rejects with the right diagnostic message (gold-standard test that pins the message text, mirroring the Floats-milestone diagnostic style).
- E2E fixture
examples/eq_ord_polymorphic.ail.json: defines a polymorphic helper, calls it on three primitive types, prints the right output. Compiles and runs end-to-end. - Round-trip: the prelude module + every new fixture passes Form-A parser + canonicaliser bit-stable.
- Bench-regression check at milestone close: the three bench
scripts exit 0 with the prelude auto-loaded into every
workspace. (Hypothesis: prelude-load adds a constant-time
per-workspace overhead, no per-call regression. If a regression
fires, audit dispatches
ailang-bencher.)
Acceptance criteria
- Iters 23.1 → 23.5 JOURNAL entries committed.
- Workspace tests pass: all current tests + the new positive, negative, and user-ADT fixtures.
bench/check.py && bench/compile_check.py && bench/cross_lang.pyexit 0 (or any non-zero exit is ratified per audit-skill rules with a JOURNAL entry naming the iter that intentionally moved the metric).- A polymorphic helper
forall a. Ord a => (a borrow, a borrow) -> Boolcompiles, monomorphises at three primitive types, runs, and prints the expected output for at least Int and Str. - The same polymorphic helper called on a Float fires
NoInstance Ord Floatat typecheck with a Float-aware diagnostic message. - DESIGN.md §"Decision 11 / Prelude (built-in) classes" amended to reflect Eq/Ord shipping in milestone 23. The "Milestone 22 ships no built-in Prelude classes" wording stays as historical accuracy; a forward-pointing addendum names this milestone's delivery.
Out of scope (deferred, with substantive rationale)
Showtypeclass. Defers behind a heap-Str-ABI milestone.instance Show Intneedsint_to_strto allocate a heap-Str — a runtime primitive AILang does not have today (see Floats fieldtest's deferredfloat_to_str). Bundling Show with heap-Str ABI in this milestone would conflate two orthogonal axes (typeclass-instance shape vs. runtime-allocated-Str ABI). They run as separate milestones in sequence.- Operator routing through
Eq/Ord.==,<, etc. stay primitive this milestone. Routing them through the class methods is the declared P2 todo, gated on bench rebaseline confirmation that the indirection does not tank latency. Without routing, surface==on Int andeq x yare redundant — this is acknowledged and ratified as transitional for one milestone. print-rewire throughShow.show. Same heap-Str dependency asShow.Eq/Ordon Float. No instance per DESIGN.md §"Float semantics". A future iter that surfaces concrete LLM-author code benefiting fromFloatpartial-comparators (signaturecompare : Float -> Float -> Maybe Ordering) opens a separate brainstorm.deriving (Eq, Ord)for user ADTs. No auto-derivation; the user / LLM writesinstance Eq Foo where eq = ...explicitly. Future milestone gated on Feature-acceptance.Orderingtotal-ordering instance (Ord Ordering). Cute but no concrete demand. Skipped.
Known costs
- ~30 LOC of new C in
runtime/str.c. - ~60-80 lines of new AILang Form-A JSON in
examples/prelude.ail.json(class defs + instances + free fns + Ordering ADT). - ~6 new codegen arms (eq__T, compare__T for T ∈ {Int, Bool, Str}).
- Small
check_workspacechange for prelude auto-loading. - Transitional redundancy
==(primitive) ↔eq(class method) on Int/Bool/Str. Lifts in P2 routing milestone. - New typecheck arm for the Float-NoInstance gold-standard diagnostic message.
Open commitments (23.1 → 23.5 implementer)
- Prelude module path. Spec commits to
examples/prelude.ail.json. If the implementation surfaces a reason this is wrong (e.g.examples/is reserved for user- facing fixtures and a non-fixture path is more honest), the implementer raises it viaNEEDS_CONTEXT; the orchestrator picks the new home. Default holds otherwise. - Diagnostic text for
NoInstance Eq Float/NoInstance Ord Float. Implementer drafts a one-line Float-aware message; orchestrator reviews in spec compliance. Boolisi8ori1in IR? AILang's Bool surface representation isi8in the runtime ABI.eq__Boolproducesicmp eq i8+ the call site is responsible for staying in i8 Bool-conventions. Confirm against existing==Bool lowering inlower_eq.