505eb8484e
Post-audit field test of the operator-routing-eq-ord milestone (closed via5170b6a+4d45bc6). Five `.ail` Surface-form fixtures under `examples/fieldtest/` written by an LLM-author working strictly from the design/ ledger + public examples (no `crates/` / `runtime/` / `bench/` reads — Iron Law honoured): eqord_1_fizzbuzz.ail — FizzBuzz [1..15] via (app eq …) on Int+Str and (app gt …) on Int eqord_2_rational_eq.ail — data Rational + user-instance (class prelude.Eq) by cross-mult; inner Int-eq nested in instance body eqord_3_newton_sqrt.ail — Newton's method via float_lt (convergence + fabs) and float_eq (zero-guard) eqord_4_float_ord_must_fail.ail — (app lt 1.5 2.5) must reject at typecheck with float_lt hint eqord_5_float_eq_must_fail.ail — (app eq 1.5 1.5) must reject at typecheck with float_eq hint Findings: [working] x4 — primitive Eq/Ord at Int/Bool/Str/Unit; user-ADT Eq with nested primitive eq calls; Float named-fn surface (float_eq/float_lt/etc.); NoInstance Eq/Ord Float diagnostic with float_eq / float_lt addendum. The milestone's central thesis (class-dispatch is the only comparison surface; Float opts out via named fns) is LLM-natural — every (app eq …) / (app gt …) / (app float_lt …) call I wrote compiled on first try with no diagnostic friction. [spec_gap] x1 — `docs/specs/2026-05-20-operator-routing-eq-ord.md:113` north-star example writes bare `(class Eq)` where the language requires `(class prelude.Eq)`. An LLM-author who copies the spec verbatim hits `bare-cross-module-class-ref`. The milestone spec is the highest-information reference an LLM-author consults; the bare-vs-qualified drift mis-primes the pattern. Resolution: tighten-the-design-ledger — fix the spec example inline (separate follow-up commit). The existing `examples/eq_ord_user_adt.ail` already uses the qualified form, so the corpus is consistent; only the spec drifted. [friction] x1 — `(app compare 1.5 2.5)` at Float fires the same diagnostic as `(app lt …)` at Float, naming `float_eq / float_lt (and siblings)` as the alternative. But the alternatives don't return Ordering; an LLM-author who wanted three-way LT/EQ/GT can't satisfy that with float_lt + float_eq alone. The spec (lines 433-436) acknowledged this case in commentary — "no float_compare ships; build it from float_lt + float_eq if you need three-way" — but the diagnostic doesn't say so. Resolution: file as Gitea backlog issue for a follow-up tidy iteration (codegen-side: branch the Float-aware NoInstance addendum on the called method-name; the `compare` arm gets a one-sentence "no float_compare; build it from float_lt + float_eq if you need three-way" addendum). Status: clean — no bugs, no blockers, the milestone surface is solidly LLM-usable. Both non-working findings are tractable forward-fixes that don't disturb the milestone's core contracts. Spec: docs/specs/2026-05-21-fieldtest-operator-routing-eq-ord.md
13 KiB
13 KiB
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
gtat 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), writesinstance prelude.Eq Rationalusing cross-multiplication (a/b == c/diffa*d == b*c), drives four comparisons inmain. - Why it fits: Axis 2 north-star. The user-instance body's inner
(app eq (app * a d) (app * b c))dispatches toprelude.eq__Int, while the outer(app eq r1 r2)from main dispatches to this module'seq__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) / 2to 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).fabsimplemented inline asif (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 of3.16228is libc-%g-shaped perfloat-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 atail 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-fnfloat_ltwhen the inferred operand type is Float. - Outcome:
ail checkexits 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/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 atail check. - Why it fits: Axis 4 companion. Verifies the Eq-class half of the Float diagnostic.
- Outcome:
ail checkexits 1 with stderr substitutingEqforOrdandeqforcompare; 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 Rationalbody 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 innereqresolves 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 writinginstance 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 explicitfloat_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 asfloat_eqrather than hiding behindeq" — 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 whetherfloat_eq n 0.0was the right zero-test (it is, because-0.0 == 0.0per 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 checkinvocations exit 1; both diagnostics name the right class (EqvsOrd), the right method call site (fn main calls eq/calls compare— note the Ord helperltis reported via its desugar tocompare), the named-fn familyfloat_eq / float_lt (and siblings …), the IEEE-754 design rationale, and the contract pathdesign/contracts/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 atfloat_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.rstest.
[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.ailwrites(class prelude.Eq)because I copied the pattern from the existingeq_ord_user_adt.ail, not from the spec. - What happened: the design spec at
docs/specs/2026-05-20-operator-routing-eq-ord.md:113writes(class Eq)(bare) in the §"Concrete code shapes" north-star example program. An LLM-author who copies that pattern verbatim hits the diagnostic:The diagnostic is excellent (lists the qualified candidate), but the spec primed the wrong pattern.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"] - 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/2026-05-20-operator-routing-eq-ord.md(lines 109-133) to use(class prelude.Eq), and consider whether the same drift exists indesign/contracts/prelude-classes.mdordesign/models/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 — namesfloat_eq / float_lt (and siblings …). None of the suggested replacements give the three-way LT/EQ/GT result thatcomparereturns. An LLM-author who specifically reached forcomparebecause they want to drive anOrdering-match on Float reads this diagnostic and is left guessing whether they should pattern onfloat_lt+float_eqthemselves 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/Orduse case namesfloat_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 offloat_compareis 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 nofloat_comparebecause IEEE-754 ordering is partial; build the three-way decision fromfloat_lt+float_eqif 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) |