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:
35
examples/0150-types-float-ne-unordered-nan.sx
Normal file
35
examples/0150-types-float-ne-unordered-nan.sx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Float `!=` is UNORDERED not-equal: `nan != nan` is true (the canonical
|
||||||
|
// `x != x` NaN idiom), and `!=` is the exact complement of `==` for every
|
||||||
|
// float input — including NaN, where `nan == nan` is false (ordered `==`).
|
||||||
|
// For all non-NaN operands unordered `!=` matches ordered `!=`, so finite
|
||||||
|
// comparisons are unchanged. The native backend agrees with the interpreter.
|
||||||
|
//
|
||||||
|
// Regression (issue 0091): the LLVM backend lowered float `!=` to ordered
|
||||||
|
// not-equal (LLVMRealONE), so `nan != nan` was false in native code.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
// Produce a genuine NaN without any numeric-limit accessor: 0.0 / 0.0.
|
||||||
|
z := 0.0;
|
||||||
|
nan := z / z;
|
||||||
|
|
||||||
|
// The fix: `!=` is unordered, `==` is ordered.
|
||||||
|
print("nan != nan: {}\n", nan != nan); // true
|
||||||
|
print("nan == nan: {}\n", nan == nan); // false
|
||||||
|
print("nan != 1.0: {}\n", nan != 1.0); // true
|
||||||
|
print("nan == 1.0: {}\n", nan == 1.0); // false
|
||||||
|
|
||||||
|
// Complementarity holds for finite operands too (unchanged behavior).
|
||||||
|
print("1.0 != 2.0: {}\n", 1.0 != 2.0); // true
|
||||||
|
print("1.0 != 1.0: {}\n", 1.0 != 1.0); // false
|
||||||
|
print("2.0 != 2.0: {}\n", 2.0 != 2.0); // false
|
||||||
|
|
||||||
|
// Native codegen converges with the comptime interpreter.
|
||||||
|
print("comptime nan != nan: {}\n", #run nan_ne_nan());
|
||||||
|
}
|
||||||
|
|
||||||
|
nan_ne_nan :: () -> bool {
|
||||||
|
z := 0.0;
|
||||||
|
n := z / z;
|
||||||
|
return n != n;
|
||||||
|
}
|
||||||
1
examples/expected/0150-types-float-ne-unordered-nan.exit
Normal file
1
examples/expected/0150-types-float-ne-unordered-nan.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
nan != nan: true
|
||||||
|
nan == nan: false
|
||||||
|
nan != 1.0: true
|
||||||
|
nan == 1.0: false
|
||||||
|
1.0 != 2.0: true
|
||||||
|
1.0 != 1.0: false
|
||||||
|
2.0 != 2.0: false
|
||||||
|
comptime nan != nan: true
|
||||||
79
issues/0091-float-ne-ordered-nan.md
Normal file
79
issues/0091-float-ne-ordered-nan.md
Normal 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.
|
||||||
9
specs.md
9
specs.md
@@ -85,6 +85,15 @@ GLSL;
|
|||||||
| `<<=` | left shift assign |
|
| `<<=` | left shift assign |
|
||||||
| `>>=` | right shift assign |
|
| `>>=` | right shift assign |
|
||||||
|
|
||||||
|
**Float comparison and NaN.** Float `==` is *ordered* and `!=` is *unordered*,
|
||||||
|
matching IEEE 754: `==` is false whenever either operand is NaN (`nan == x` is
|
||||||
|
false for every `x`, including `nan`), and `!=` is true whenever either operand
|
||||||
|
is NaN (`nan != x` is true for every `x`, including `nan`). So `!=` is the exact
|
||||||
|
complement of `==` for all float inputs, and the canonical NaN test `x != x` is
|
||||||
|
true exactly when `x` is NaN. The ordered relations `<`, `<=`, `>`, `>=` are all
|
||||||
|
false when either operand is NaN. For all non-NaN operands these reduce to the
|
||||||
|
ordinary comparisons. Native codegen and the comptime interpreter agree on this.
|
||||||
|
|
||||||
### Delimiters and Punctuation
|
### Delimiters and Punctuation
|
||||||
|
|
||||||
| Token | Meaning |
|
| Token | Meaning |
|
||||||
|
|||||||
@@ -274,7 +274,10 @@ pub const Ops = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn emitCmpNe(self: Ops, instruction: *const Inst, bin: BinOp) void {
|
pub fn emitCmpNe(self: Ops, instruction: *const Inst, bin: BinOp) void {
|
||||||
self.e.emitCmp(bin, instruction.ty, c.LLVMIntNE, c.LLVMRealONE);
|
// Float `!=` is UNORDERED not-equal: true if either operand is NaN, so
|
||||||
|
// `nan != nan` is true (IEEE 754 / the `x != x` NaN idiom) and `!=` stays
|
||||||
|
// the exact complement of `==` (OEQ). UNE == ONE for all non-NaN operands.
|
||||||
|
self.e.emitCmp(bin, instruction.ty, c.LLVMIntNE, c.LLVMRealUNE);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn emitCmpLt(self: Ops, instruction: *const Inst, bin: BinOp) void {
|
pub fn emitCmpLt(self: Ops, instruction: *const Inst, bin: BinOp) void {
|
||||||
|
|||||||
Reference in New Issue
Block a user