fix(check): branch Float NoInstance addendum on method name (#25)

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
This commit is contained in:
2026-06-01 20:34:20 +02:00
parent d8be95dd44
commit 05c3c018de
3 changed files with 74 additions and 9 deletions
+42
View File
@@ -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
);
}
+27 -9
View File
@@ -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
+5
View File
@@ -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))))