# Fieldtest — operator-routing-eq-ord — 2026-05-21 **Status:** Draft — awaiting orchestrator triage **Author:** ailang-fieldtester (dispatched by skills/fieldtest) ## Scope The `operator-routing-eq-ord` milestone removed the surface comparator names `==` / `!=` / `<` / `<=` / `>` / `>=` from the language. Equality and ordering are now class methods that resolve via `prelude.Eq` / `prelude.Ord` class-dispatch — `eq` / `ne` / `lt` / `le` / `gt` / `ge` / `compare` over the primitive instances (Int, Bool, Str, Unit) plus any user ADT with a hand-written instance. Float has no Eq/Ord instance by design; the six monomorphic prelude fns `float_eq` / `float_ne` / `float_lt` / `float_le` / `float_gt` / `float_ge` are the explicit named-fn surface for IEEE-aware Float comparison, each lowered to a single `fcmp` via the codegen intercept with `alwaysinline`. ## Examples ### `examples/fieldtest/eqord_1_fizzbuzz.ail` — FizzBuzz over [1..15] - **What it does:** classic FizzBuzz, dispatching equality on `(app eq (app % n K) 0)` for K in {3, 5, 15}, plus the empty-string guard `(app eq label "")`, plus a tail-recursive loop driver bounded by `(app gt i stop)`. - **Why it fits:** exercises the primitive Eq path at Int and Str simultaneously, plus the Ord-class free helper `gt` at Int — the three most-reached-for shapes in everyday integer code. - **Outcome:** compiles, runs, matches expected stdout (`1\n2\nFizz\n4\nBuzz\nFizz\n7\n8\nFizz\nBuzz\n11\nFizz\n13\n14\nFizzBuzz\n`). ### `examples/fieldtest/eqord_2_rational_eq.ail` — Rational with hand-written Eq - **What it does:** defines `data Rational (ctor MkRational Int Int)`, writes `instance prelude.Eq Rational` using cross-multiplication (`a/b == c/d` iff `a*d == b*c`), drives four comparisons in `main`. - **Why it fits:** Axis 2 north-star. The user-instance body's inner `(app eq (app * a d) (app * b c))` dispatches to `prelude.eq__Int`, while the outer `(app eq r1 r2)` from main dispatches to this module's `eq__Rational`. End-to-end: user-instance → prelude class method → primitive intercept. - **Outcome:** compiles, runs, matches expected stdout (`true\nfalse\ntrue\nfalse\n`). ### `examples/fieldtest/eqord_3_newton_sqrt.ail` — Newton's method via float_lt / float_eq - **What it does:** Newton-Raphson iteration `x_{k+1} = (x_k + n/x_k) / 2` to compute sqrt. Convergence loop guards on `(app float_lt (app fabs (app - xnew x)) tol)`; pathological-zero guard on `(app float_eq n 0.0)`. `fabs` implemented inline as `if (app float_lt x 0.0) then -x else x`. - **Why it fits:** Axis 3 north-star. Demonstrates the LLM-natural shape of the Float-comparison named-fn family in numerical code. Mirrors how Rust/Python/Swift guide users toward bespoke Float-aware methods (`f64::abs`, `math.isclose`). - **Outcome:** compiles, runs, matches expected stdout (`2.0\n3.16228\n0.0\n1.0\n`). The Show Float rendering of `3.16228` is libc-`%g`-shaped per `float-semantics.md` (the exact IEEE-correct value 3.1622776601683795 truncates in the default Show width). Acceptable. ### `examples/fieldtest/eqord_4_float_ord_must_fail.ail` — Ord at Float rejection - **What it does:** single-statement program `(app print (if (app lt 1.5 2.5) 1 0))`. Must fail at `ail check`. - **Why it fits:** Axis 4 north-star. Verifies the diagnostic that steers an LLM-author from the polymorphic `lt` (an Ord-class free helper) toward the named-fn `float_lt` when the inferred operand type is Float. - **Outcome:** `ail check` exits 1 with stderr: ``` error: [no-instance] main: fn `main` calls `compare` requiring `instance prelude.Ord Float`, but no such instance exists in the workspace — Float has no Eq/Ord instance by design (partial orderability per IEEE-754); use float_eq / float_lt (and siblings float_ne / float_le / float_gt / float_ge) for explicit IEEE-aware comparison. See design/contracts/0005-float-semantics.md. ``` ### `examples/fieldtest/eqord_5_float_eq_must_fail.ail` — Eq at Float rejection (companion) - **What it does:** single-statement program `(app print (if (app eq 1.5 1.5) 1 0))`. Must fail at `ail check`. - **Why it fits:** Axis 4 companion. Verifies the Eq-class half of the Float diagnostic. - **Outcome:** `ail check` exits 1 with stderr substituting `Eq` for `Ord` and `eq` for `compare`; rest of the addendum identical. ## Findings ### [working] Primitive Eq/Ord at Int/Bool/Str/Unit is friction-free - **Example(s):** eqord_1_fizzbuzz (Int eq, Str eq, Int gt), eqord_2_rational_eq (Int eq inside ADT-instance body). - **What happened:** every `(app eq …)` and `(app gt …)` call I wrote compiled on first try, lowered correctly, and produced the expected output. No friction in the surface shape, no diagnostic needed. - **Why it's `working`:** the milestone's central thesis (class-dispatch is the surface for all comparison) holds — there is no second thought during authoring about which spelling to use, and no ambiguity about which primitive instance dispatches. The FizzBuzz fixture is the natural form an LLM would emit on the prompt "write FizzBuzz in AILang". - **Recommended downstream action:** carry-on. Worth keeping the fixtures around as regression protection against any future re-introduction of operator-name surface. ### [working] User-ADT Eq with nested primitive eq calls Just Works - **Example(s):** eqord_2_rational_eq. - **What happened:** the `instance prelude.Eq Rational` body wrote itself naturally — match both operands, cross-multiply, compare with inner `(app eq …)` on Int. No coercion ceremony, no manual qualifier on the inner call (the inner `eq` resolves through the same class-dispatch as the outer, just at a different inferred type). The end-to-end run produced the correct cross-multiplication results. - **Why it's `working`:** this is the milestone's clause-1 evidence — the form was structurally impossible before the milestone (the old built-in `==` rejected user ADTs at codegen with a generic error; even after writing `instance Eq Point`, the inner `(app eq a1 a2)` on Int would route via the dead polymorphic-`==` path with no visible gain). Post-milestone, the LLM-author's mental model ("classes do equality") matches the implementation surface 1:1. - **Recommended downstream action:** carry-on. ### [working] Float-comparison named-fn surface is LLM-natural - **Example(s):** eqord_3_newton_sqrt. - **What happened:** writing `(app float_lt x 0.0)`, `(app float_eq n 0.0)`, and `(app float_lt (app fabs …) tol)` felt entirely natural while composing the Newton iteration. The named-fn surface is no more verbose than the deleted operator surface (`(app < x 0.0)` was three tokens, `(app float_lt x 0.0)` is three tokens), and the explicit `float_` prefix is an active reminder during authoring that this is an IEEE-partial operation rather than a structural-equality one. - **Why it's `working`:** the spec's clause-3 framing — "the NaN-comparison anti-pattern stays visible in code as `float_eq` rather than hiding behind `eq`" — held up in practice. Newton's method is exactly the kind of numerical code where mistaking Float-eq for structural-eq would cause real bugs; the explicit spelling made me pause and think about whether `float_eq n 0.0` was the right zero-test (it is, because `-0.0 == 0.0` per IEEE, which is the correct guard). - **Recommended downstream action:** carry-on. ### [working] NoInstance Eq/Ord Float diagnostic is informative and actionable - **Example(s):** eqord_4_float_ord_must_fail, eqord_5_float_eq_must_fail. - **What happened:** both `ail check` invocations exit 1; both diagnostics name the right class (`Eq` vs `Ord`), the right method call site (`fn main calls eq` / `calls compare` — note the Ord helper `lt` is reported via its desugar to `compare`), the named-fn family `float_eq / float_lt (and siblings …)`, the IEEE-754 design rationale, and the contract path `design/contracts/0005-float-semantics.md`. An LLM-author who hits this diagnostic learns the entire replacement surface in one read. - **Why it's `working`:** this is the milestone's clause-2 evidence — the diagnostic carries enough information for an LLM that has only the stderr to converge on the right call site without re-reading the spec. Acceptance-criterion #3 ("Float-aware hint pointing at `float_eq`") is comfortably met. - **Recommended downstream action:** carry-on. The diagnostic is the load-bearing surface for clean-break removal of the operator names; protect it with the existing `eq_float_noinstance.rs` test. ### [spec_gap] Spec's own example program uses bare `(class Eq)` but the language requires `(class prelude.Eq)` - **Example(s):** confirmed by a /tmp probe; my own `eqord_2_rational_eq.ail` writes `(class prelude.Eq)` because I copied the pattern from the existing `eq_ord_user_adt.ail`, not from the spec. - **What happened:** the design spec at `docs/specs/0049-operator-routing-eq-ord.md:113` writes `(class Eq)` (bare) in the §"Concrete code shapes" north-star example program. An LLM-author who copies that pattern verbatim hits the diagnostic: ``` error: [bare-cross-module-class-ref] module `…` contains bare class name `Eq` that does not resolve to a local class; candidates from imports: ["prelude.Eq"] ``` The diagnostic is excellent (lists the qualified candidate), but the spec primed the wrong pattern. - **Why it's `spec_gap`:** the milestone spec is the primary reference for the milestone-defining surface; if the spec's own example doesn't typecheck verbatim, then either the spec is wrong (most likely) or bare class names are a feature the language should in fact support (unlikely — qualified is the established style per every existing fixture). The honest action is to update the spec's example to use `(class prelude.Eq)`. This is one-line of doc drift, but it is the highest-information reference an LLM-author consults when picking up the milestone surface. - **Recommended downstream action:** tighten the design ledger — fix the example in `docs/specs/0049-operator-routing-eq-ord.md` (lines 109-133) to use `(class prelude.Eq)`, and consider whether the same drift exists in `design/contracts/0017-prelude-classes.md` or `design/models/0005-typeclasses.md`. A single forward-fix doc commit. ### [friction] `(app compare …)` at Float diagnostic does not acknowledge there is no `float_compare` - **Example(s):** /tmp probe writing `(app compare 1.5 2.5)` inside a match-on-Ordering. - **What happened:** the diagnostic is identical to the `(app lt …)` diagnostic — names `float_eq / float_lt (and siblings …)`. None of the suggested replacements give the three-way LT/EQ/GT result that `compare` returns. An LLM-author who specifically reached for `compare` because they want to drive an `Ordering`-match on Float reads this diagnostic and is left guessing whether they should pattern on `float_lt` + `float_eq` themselves to reconstruct the three-way decision, or whether some other surface exists. - **Why it's `friction`:** the spec at lines 433-436 acknowledged this case explicitly ("`compare`/`Ord` use case names `float_lt` / `float_compare` (the latter doesn't ship as a fn …) — so the hint says 'use float_lt / float_eq for explicit IEEE-aware comparison'"). So the absence of `float_compare` is deliberate. But the diagnostic doesn't tell the author *that* — it just lists the replacements without explaining the gap. A one-line addendum ("note: there is no `float_compare` because IEEE-754 ordering is partial; build the three-way decision from `float_lt` + `float_eq` if you need it") would close the gap without changing any code path. - **Recommended downstream action:** plan a tidy iteration that extends the float-aware diagnostic to differentiate on the called method — when the call site is `compare`, append the "no float_compare exists, build it from float_lt + float_eq" sentence. Scoped to one branch in the diagnostic addendum, no contract change. ## Recommendation summary | Finding | Class | Action | |---|---|---| | Primitive Eq/Ord at Int/Bool/Str/Unit friction-free | working | carry-on | | User-ADT Eq with nested primitive eq Just Works | working | carry-on | | Float-comparison named-fn surface is LLM-natural | working | carry-on | | NoInstance Eq/Ord Float diagnostic is informative | working | carry-on | | Spec example uses bare `(class Eq)` but language needs `(class prelude.Eq)` | spec_gap | tighten the design ledger (doc fix) | | `(app compare …)` at Float diagnostic doesn't name the missing `float_compare` | friction | plan a tidy iteration (diagnostic addendum) |