# 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) |
