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