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).
3.4 KiB
0091 — float != lowers to ORDERED not-equal, so nan != nan is false in native code
RESOLVED (F0.9). Root cause:
emitCmpNeinsrc/backend/llvm/ops.zigpassedc.LLVMRealONE(ordered not-equal) as the float predicate. Fix:c.LLVMRealONE→c.LLVMRealUNE(unordered not-equal). The integer predicateLLVMIntNEandemitCmpEq(OEQ) are unchanged. For all non-NaN operandsUNE≡ONE, so only NaN-involving float!=changes (toward correct). Regression test:examples/0150-types-float-ne-unordered-nan.sx. Spec note added tospecs.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 != nanevaluates to false (viasx run). - Expected: true —
!=must be the logical complement of==, and the canonical NaN-detection idiomx != xmust 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_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
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.