fix(backend): float != must be UNORDERED so nan != nan is true [F0.9]

emitCmpNe lowered float `!=` to `LLVMRealONE` (ordered not-equal), which
is false when either operand is NaN. That made `nan != nan` false in
native code — breaking the canonical `x != x` NaN test, making `!=`
non-complementary with `==` for NaN, and disagreeing with the interpreter.

Change the float predicate to `LLVMRealUNE` (unordered not-equal): true
if either operand is NaN OR they are unequal. For all non-NaN operands
`UNE` ≡ `ONE`, so only NaN-involving comparisons change (toward correct).
The integer predicate (`LLVMIntNE`) and `emitCmpEq` (`OEQ`) are unchanged,
so `nan == nan` stays false and `!=` is now the exact complement of `==`.

- Regression: examples/0150-types-float-ne-unordered-nan.sx (fails before,
  passes after; also pins #run/comptime == runtime agreement).
- specs.md: documents float comparison / NaN semantics (Operators).
- Resolves issue 0091 (issues/0091-float-ne-ordered-nan.md).
This commit is contained in:
agra
2026-06-04 17:04:41 +03:00
parent b5a2535ab6
commit 5afbc65414
7 changed files with 137 additions and 1 deletions

View File

@@ -0,0 +1,79 @@
# 0091 — float `!=` lowers to ORDERED not-equal, so `nan != nan` is false in native code
> **RESOLVED** (F0.9). Root cause: `emitCmpNe` in `src/backend/llvm/ops.zig`
> passed `c.LLVMRealONE` (ordered not-equal) as the float predicate. Fix:
> `c.LLVMRealONE` → `c.LLVMRealUNE` (unordered not-equal). The integer predicate
> `LLVMIntNE` and `emitCmpEq` (`OEQ`) are unchanged. For all non-NaN operands
> `UNE` ≡ `ONE`, so only NaN-involving float `!=` changes (toward correct).
> Regression test: `examples/0150-types-float-ne-unordered-nan.sx`. Spec note
> added to `specs.md` (Operators → "Float comparison and NaN").
## Symptom
The LLVM backend lowers float `!=` to `LLVMRealONE` (ordered not-equal), which
returns **false** when either operand is NaN. Consequences:
- Observed: `nan != nan` evaluates to **false** (via `sx run`).
- Expected: **true**`!=` must be the logical complement of `==`, and the
canonical NaN-detection idiom `x != x` must be true for a NaN.
This makes `==` and `!=` non-complementary for NaN: `nan == nan` is false
(correct, `OEQ`) AND `nan != nan` is also false (wrong, `ONE`). It silently
breaks the standard NaN check used throughout numerical code
(`if x != x { /* NaN */ }`): NaN is never detected at runtime.
## Reproduction (accessor-free)
NaN is produced as `0.0 / 0.0` — no numeric-limit accessor required:
```sx
#import "modules/std.sx";
main :: () {
z := 0.0;
n := z / z; // NaN
print("ne={} eq={}\n", n != n, n == n); // observed: ne=false eq=false
} // correct: ne=true eq=false
```
`./zig-out/bin/sx run <repro>.sx` printed `ne=false eq=false` before the fix.
After the fix it prints `ne=true eq=false`. Non-NaN comparisons are unchanged
(`1.0 != 2.0` true, `1.0 != 1.0` false). The `#run`/comptime path (JIT-compiled
through the same backend) and the native runtime path agree in both states.
## Root cause
`src/backend/llvm/ops.zig`, `emitCmpNe`:
```zig
pub fn emitCmpNe(self: Ops, instruction: *const Inst, bin: BinOp) void {
self.e.emitCmp(bin, instruction.ty, c.LLVMIntNE, c.LLVMRealONE);
// ^^^^^^^^^^^^^^^ ordered
}
```
`LLVMRealONE` = ordered not-equal (false if either operand is NaN). The IEEE/C
`!=` is `LLVMRealUNE` (unordered not-equal → true if either is NaN). For all
NON-NaN operands `UNE` and `ONE` are identical, so the fix changes behavior only
for the NaN case — bringing native codegen in line with `==` (`OEQ`) and with
the interpreter's `evalCmp` (`.ne => lf != rf`, which is unordered in Zig).
`emitCmpNe` is the sole float-`!=` lowering site (dispatched from
`src/ir/emit_llvm.zig` `cmp_ne``ops().emitCmpNe`). There is no second backend
path (no `fcmp one` appears in any `.ir` snapshot; `src/codegen.zig` has no
float-`!=` lowering).
## Fix
```zig
pub fn emitCmpNe(self: Ops, instruction: *const Inst, bin: BinOp) void {
self.e.emitCmp(bin, instruction.ty, c.LLVMIntNE, c.LLVMRealUNE);
}
```
## Regression test
`examples/0150-types-float-ne-unordered-nan.sx` asserts (runtime, exit 0):
`nan != nan` true, `nan == nan` false, `nan != 1.0` true, `nan == 1.0` false,
the finite cases (`1.0 != 2.0` true, `1.0 != 1.0` false, `2.0 != 2.0` false),
and that the `#run` comptime `nan != nan` matches the runtime one. It fails on
the pre-fix compiler (`nan != nan: false`) and passes after.