Files
sx/issues/0091-float-ne-ordered-nan.md
agra 5afbc65414 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).
2026-06-04 17:04:41 +03:00

3.4 KiB

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.LLVMRealONEc.LLVMRealUNE (unordered not-equal). The integer predicate LLVMIntNE and emitCmpEq (OEQ) are unchanged. For all non-NaN operands UNEONE, 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:

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

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_neops().emitCmpNe). There is no second backend path (no fcmp one appears in any .ir snapshot; src/codegen.zig has no float-!= lowering).

Fix

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.