From 05c3c018def048aab215d9c1b2a3669531bc8f20 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 1 Jun 2026 20:34:20 +0200 Subject: [PATCH] fix(check): branch Float NoInstance addendum on method name (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Float-aware `NoInstance` addendum emitted the same hint for every Eq/Ord method at Float: "use float_eq / float_lt (and siblings ...)". For `eq`/`ne`/`lt`/`le`/`gt`/`ge` that is correct — each has a drop-in boolean `float_*` sibling. For `compare` it is incomplete and misleading: `compare` returns three-way `Ordering`, but no `float_compare` ships (a deliberate non-ship, docs/specs/2026-05-20-operator-routing-eq-ord.md). An LLM author who reached for `compare` to get three-way ordering was pointed at boolean siblings as if a drop-in existed, and left guessing whether to reconstruct it themselves. Fix: branch the addendum on the called method. The boolean arm keeps the existing wording; the `compare` arm instead names the deliberate `float_compare` omission and points at building a three-way `Ordering` from `float_lt` + `float_eq`. One added branch on the existing `method` field (already carried by `CheckError::NoInstance`), no new machinery; the structured `ctx` is untouched. Out of scope (the issue's deliberate non-ship): shipping a `float_compare` fn. The friction was that the diagnostic did not acknowledge the omission — now it does. RED-first, inline: a one-branch diagnostic-text change at a single site whose surrounding code was already loaded while working the adjacent #52 fix this session (CLAUDE.md "context already loaded" carve-out). TDD discipline preserved by the new RED test `compare_at_float_names_deliberate_no_float_compare` over a new minimal fixture `examples/compare_float_noinstance.ail` (`compare` at Float); it failed RED on the old shared wording and passes GREEN on the branch. The pre-existing `eq_at_float_fires_float_aware_noinstance` still passes (eq arm unchanged). Verification: full ailang-check lib suite (121) + full ail test suite green; live CLI renders both arms correctly. closes #25 --- crates/ail/tests/eq_float_noinstance.rs | 42 +++++++++++++++++++++++++ crates/ailang-check/src/lib.rs | 36 +++++++++++++++------ examples/compare_float_noinstance.ail | 5 +++ 3 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 examples/compare_float_noinstance.ail diff --git a/crates/ail/tests/eq_float_noinstance.rs b/crates/ail/tests/eq_float_noinstance.rs index 3a08cad..74d6e74 100644 --- a/crates/ail/tests/eq_float_noinstance.rs +++ b/crates/ail/tests/eq_float_noinstance.rs @@ -52,3 +52,45 @@ fn eq_at_float_fires_float_aware_noinstance() { no_inst.message ); } + +/// #25: the Float-aware `NoInstance` addendum branches on the called +/// method. `compare` returns a three-way `Ordering`, but no `float_compare` +/// ships — a deliberate non-ship per the operator-routing-eq-ord spec. The +/// `eq`/`ne`/`lt`/`le`/`gt`/`ge` arm keeps the existing wording (the boolean +/// `float_*` siblings are drop-in replacements); the `compare` arm must +/// instead acknowledge the omission and point the author at building a +/// three-way result from `float_lt` + `float_eq`, rather than listing the +/// boolean siblings as if a `float_compare` existed. +#[test] +fn compare_at_float_names_deliberate_no_float_compare() { + let ws = load_workspace(&fixture("compare_float_noinstance.ail")).expect("load"); + let diags = check_workspace(&ws); + + let no_inst = diags + .iter() + .find(|d| d.code == "no-instance") + .unwrap_or_else(|| panic!("expected diagnostic with code 'no-instance', got: {diags:#?}")); + + assert!( + no_inst.message.contains("Float"), + "expected Float-aware NoInstance diagnostic, got: {:?}", + no_inst.message + ); + assert!( + no_inst.message.contains("design/contracts/0005-float-semantics.md"), + "expected the float-semantics contract cross-reference, got: {:?}", + no_inst.message + ); + // The compare-specific branch: name the deliberate `float_compare` + // non-ship and the `float_lt` + `float_eq` building blocks. + assert!( + no_inst.message.contains("float_compare"), + "expected the compare addendum to name the deliberate `float_compare` non-ship, got: {:?}", + no_inst.message + ); + assert!( + no_inst.message.contains("float_lt") && no_inst.message.contains("float_eq"), + "expected the compare addendum to name float_lt + float_eq as the three-way building blocks, got: {:?}", + no_inst.message + ); +} diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 181d93c..2179649 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -994,18 +994,36 @@ impl CheckError { // IEEE-754), and the polymorphic prelude helpers // (`ne` / `lt` / `le` / `gt` / `ge` / direct `eq` / `compare`) // are the natural surface where the LLM hits this. - if let CheckError::NoInstance { class, at_type, .. } = self.inner() { + if let CheckError::NoInstance { class, method, at_type, .. } = self.inner() { // `class` carries the qualified workspace-key shape // (`prelude.Eq` / `prelude.Ord`), not the bare name. if (class == "prelude.Eq" || class == "prelude.Ord") && at_type == "Float" { - d.message = format!( - "{} — 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.", - d.message - ); + // #25: branch on the called method. The boolean comparison + // methods (`eq` / `ne` / `lt` / `le` / `gt` / `ge`) have + // drop-in `float_*` siblings; `compare` does not — no + // `float_compare` ships (a deliberate non-ship, the + // operator-routing-eq-ord spec). Acknowledge the omission + // and point at the three-way building blocks instead of + // listing boolean siblings as if a drop-in existed. + if method == "compare" { + d.message = format!( + "{} — Float has no Eq/Ord instance by design (partial \ + orderability per IEEE-754); no float_compare ships — \ + build a three-way Ordering from float_lt + float_eq \ + if you need it. \ + See design/contracts/0005-float-semantics.md.", + d.message + ); + } else { + d.message = format!( + "{} — 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.", + d.message + ); + } } else if class == "prelude.Show" { // Show-aware addendum on `NoInstance`. Fires // for any type lacking a Show instance — most commonly diff --git a/examples/compare_float_noinstance.ail b/examples/compare_float_noinstance.ail new file mode 100644 index 0000000..7bd4ae7 --- /dev/null +++ b/examples/compare_float_noinstance.ail @@ -0,0 +1,5 @@ +(module compare_float_noinstance + (fn cmp + (type (fn-type (params) (ret (con Ordering)))) + (params) + (body (app compare 1.0 2.0))))