!= 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. =(- 0.0 f64-inf)=: the decision wrote =(- f64-inf)=, and there is no unary minus.
Rules out Clojure's =##Inf= reader literal. 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 ** DONE A u64 constant above 2^63 cannot be written in decimal
CLOSED: [2026-09-25] CLOSED: [2026-09-25]
An integer written at or above 2^63 — a decimal up to 2^64 - 1, or hex with the 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."); ordered.");
("!=", "!= [equal? ...] bool", ("!=", "!= [equal? ...] bool",
"All different: (!= a b c) is true when every operand differs from every \ "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", ("<", "< [ordered? ...] bool",
"Less than, chained: (< a b c) is a < b and b < c, and every operand is \ "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 \ 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" | Tast.Ge -> if signed then "sge" else "uge"
| _ -> assert false | _ -> 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 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" | Tast.Le -> "ole" | Tast.Gt -> "ogt" | Tast.Ge -> "oge"
| _ -> assert false | _ -> 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. (* Parity, which on [ucomis] means "unordered": one of the operands was a NaN.
Nothing else in this file reads it. *) 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 (* [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 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. 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 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, comparison Flan has but one is LLVM's *ordered* one ([emit.ml]'s
one, olt, ...), which answers false for a NaN, and [setb] after an [fcmp_op]: oeq, olt, ...), which answers false for a NaN, and [setb] after
unordered compare answers true. So [<] and [<=] swap their operands and ask an unordered compare answers true. So [<] and [<=] swap their operands and
for a/ae, which are the two codes a NaN makes false; [=] and [!=] cannot be ask for a/ae, which are the two codes a NaN makes false; [=] cannot be
spelled by one code at all and take a second [setnp] beside them. 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 [(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 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" | _ -> unsupported "not a comparison"
let float_ordered (p : Tast.prim) = 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) = let is_cmp (p : Tast.prim) =
match p with 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; movzx8 f.b ~dst:rcx ~src:rcx;
and_rr f.b ~dst:rax ~src:rcx and_rr f.b ~dst:rax ~src:rcx
end 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 end else begin
load_loc f ~reg:rax la a.Tast.ty; load_loc f ~reg:rax la a.Tast.ty;
load_loc f ~reg:rcx lb b.Tast.ty; load_loc f ~reg:rcx lb b.Tast.ty;

View File

@ -60,6 +60,10 @@
(print " ") (print " ")
(println (if ok "ok" "WRONG"))) (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 (defn main [] i32
;; The integers, each printed as the exact decimal the expected output pins. ;; The integers, each printed as the exact decimal the expected output pins.
(println i8-max) (println i8-max)
@ -128,4 +132,13 @@
(say "f32-inf negated" (< (- (f32 0.0) f32-inf) (- (f32 0.0) f32-max))) (say "f32-inf negated" (< (- (f32 0.0) f32-inf) (- (f32 0.0) f32-max)))
(say "f64-nan" (not (= f64-nan f64-nan))) (say "f64-nan" (not (= f64-nan f64-nan)))
(say "f32-nan" (not (= f32-nan f32-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) 0)

View File

@ -5440,7 +5440,9 @@ level "1"
f32's least value negates its greatest ok\n\ f32's least value negates its greatest ok\n\
f64'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-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 in
outputs "type limits" "programs/limits.flan" limits_out; outputs "type limits" "programs/limits.flan" limits_out;
outputs ~opt:"-O0" "type limits, -O0" "programs/limits.flan" limits_out; outputs ~opt:"-O0" "type limits, -O0" "programs/limits.flan" limits_out;