!= over floats is unordered, so a NaN is unequal to itself on both backends as it already was on the dyn side

This commit is contained in:
Joseph Ferano 2026-09-25 11:27:49 +07:00
parent 0b9ae9318e
commit 14bac84f53
6 changed files with 37 additions and 16 deletions

View File

@ -146,12 +146,6 @@ missed, so a program's own binding of one wins. Negative infinity is
=(- 0.0 f64-inf)=: the decision wrote =(- f64-inf)=, and there is no unary minus.
Rules out Clojure's =##Inf= reader literal.
** TODO (!= x x) is false for a NaN
=!== on floats is LLVM's ordered =one= on both backends (=lib/emit.ml= =fcmp_op=,
=lib/x86.ml= =float_cc=), so =(!= f64-nan f64-nan)= is =false= where C, Odin and
IEEE 754 say =true=; =(not (= x x))= is the only NaN test that works. Changing it
to =une= is a decision about what =!== means.
** DONE A u64 constant above 2^63 cannot be written in decimal
CLOSED: [2026-09-25]
An integer written at or above 2^63 — a decimal up to 2^64 - 1, or hex with the

View File

@ -10064,7 +10064,8 @@ let builtins : (string * string * string) list =
ordered.");
("!=", "!= [equal? ...] bool",
"All different: (!= a b c) is true when every operand differs from every \
other, so (!= 1 2 1) is false. Over everything = accepts.");
other, so (!= 1 2 1) is false. Over everything = accepts. A float NaN \
is != to everything, itself included.");
("<", "< [ordered? ...] bool",
"Less than, chained: (< a b c) is a < b and b < c, and every operand is \
evaluated once. Machine numbers and enums only — ordering a handle \

View File

@ -1966,8 +1966,11 @@ let icmp_op signed = function
| Tast.Ge -> if signed then "sge" else "uge"
| _ -> assert false
(* [!=] is unordered and the rest are ordered, so a NaN is unequal to
everything, itself included, and neither less, greater nor equal: IEEE 754's
answers, and C's and Odin's. *)
let fcmp_op = function
| Tast.Eq -> "oeq" | Tast.Ne -> "one" | Tast.Lt -> "olt"
| Tast.Eq -> "oeq" | Tast.Ne -> "une" | Tast.Lt -> "olt"
| Tast.Le -> "ole" | Tast.Gt -> "ogt" | Tast.Ge -> "oge"
| _ -> assert false

View File

@ -1317,18 +1317,20 @@ let int_cc ~signed (p : Tast.prim) =
(* Parity, which on [ucomis] means "unordered": one of the operands was a NaN.
Nothing else in this file reads it. *)
let cc_np = 11
let cc_p = 10 and cc_np = 11
(* [ucomis] sets the flags the *unsigned* codes read, whichever way the
operands are signed, so a float comparison never uses l/g — and it sets
CF, ZF and PF all at once when either operand is a NaN.
That last part is why this is not simply the unsigned table. Every
comparison Flan has is LLVM's *ordered* one ([emit.ml]'s [fcmp_op]: oeq,
one, olt, ...), which answers false for a NaN, and [setb] after an
unordered compare answers true. So [<] and [<=] swap their operands and ask
for a/ae, which are the two codes a NaN makes false; [=] and [!=] cannot be
spelled by one code at all and take a second [setnp] beside them.
comparison Flan has but one is LLVM's *ordered* one ([emit.ml]'s
[fcmp_op]: oeq, olt, ...), which answers false for a NaN, and [setb] after
an unordered compare answers true. So [<] and [<=] swap their operands and
ask for a/ae, which are the two codes a NaN makes false; [=] cannot be
spelled by one code at all and takes a second [setnp] beside it. The one is
[!=], which is [une] — true for a NaN, as IEEE 754 and C have it — and is
[setne] or'd with [setp].
[(not (= x x))] is how [format-f64] in the prelude detects a NaN, and it is
the whole of the difference: with [sete] alone, [(/ 0.0 0.0)] formatted as
@ -1344,7 +1346,7 @@ let float_cc (p : Tast.prim) =
| _ -> unsupported "not a comparison"
let float_ordered (p : Tast.prim) =
match p with Tast.Eq | Tast.Ne -> true | _ -> false
match p with Tast.Eq -> true | _ -> false
let is_cmp (p : Tast.prim) =
match p with
@ -3179,6 +3181,12 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst =
movzx8 f.b ~dst:rcx ~src:rcx;
and_rr f.b ~dst:rax ~src:rcx
end
else if p = Tast.Ne then begin
movzx8 f.b ~dst:rax ~src:rax;
setcc f.b ~cc:cc_p ~dst:rcx;
movzx8 f.b ~dst:rcx ~src:rcx;
or_rr f.b ~dst:rax ~src:rcx
end
end else begin
load_loc f ~reg:rax la a.Tast.ty;
load_loc f ~reg:rcx lb b.Tast.ty;

View File

@ -60,6 +60,10 @@
(print " ")
(println (if ok "ok" "WRONG")))
(defn ne-f64 [a f64 b f64] bool (!= a b))
(defn ne-f32 [a f32 b f32] bool (!= a b))
(defn ne-dyn [a dyn b dyn] bool (!= a b))
(defn main [] i32
;; The integers, each printed as the exact decimal the expected output pins.
(println i8-max)
@ -128,4 +132,13 @@
(say "f32-inf negated" (< (- (f32 0.0) f32-inf) (- (f32 0.0) f32-max)))
(say "f64-nan" (not (= f64-nan f64-nan)))
(say "f32-nan" (not (= f32-nan f32-nan)))
;; != is the one unordered comparison: a NaN is unequal to everything,
;; itself included, and the dyn side agrees. The operands arrive as
;; parameters so that no constant folder answers in the backend's place.
(say "f64-nan != itself" (ne-f64 f64-nan f64-nan))
(say "f32-nan != itself" (ne-f32 f32-nan f32-nan))
(say "!= over ordinary floats"
(and (ne-f64 1.0 2.0) (not (ne-f64 1.5 1.5))
(ne-f32 (f32 1.0) (f32 2.0)) (not (ne-f32 (f32 1.5) (f32 1.5)))))
(say "a dyn NaN != itself" (ne-dyn f64-nan f64-nan))
0)

View File

@ -5440,7 +5440,9 @@ level "1"
f32's least value negates its greatest ok\n\
f64's least value negates its greatest ok\n\
f64-inf ok\nf32-inf ok\nf64-inf negated ok\nf32-inf negated ok\n\
f64-nan ok\nf32-nan ok\n"
f64-nan ok\nf32-nan ok\n\
f64-nan != itself ok\nf32-nan != itself ok\n\
!= over ordinary floats ok\na dyn NaN != itself ok\n"
in
outputs "type limits" "programs/limits.flan" limits_out;
outputs ~opt:"-O0" "type limits, -O0" "programs/limits.flan" limits_out;