From 14bac84f536adc850e6392637f20bbfeb17c6a09 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 11:27:49 +0700 Subject: [PATCH] != over floats is unordered, so a NaN is unequal to itself on both backends as it already was on the dyn side --- TODO.org | 6 ------ lib/check.ml | 3 ++- lib/emit.ml | 5 ++++- lib/x86.ml | 22 +++++++++++++++------- test/programs/limits.flan | 13 +++++++++++++ test/test_acceptance.ml | 4 +++- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/TODO.org b/TODO.org index 081c65c5..fb114628 100644 --- a/TODO.org +++ b/TODO.org @@ -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 diff --git a/lib/check.ml b/lib/check.ml index e5106991..6d06c7cd 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -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 \ diff --git a/lib/emit.ml b/lib/emit.ml index 800e4a71..f2d40eb2 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -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 diff --git a/lib/x86.ml b/lib/x86.ml index 9d6f15a3..6b353da0 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -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; diff --git a/test/programs/limits.flan b/test/programs/limits.flan index bd284465..c38bdb4e 100644 --- a/test/programs/limits.flan +++ b/test/programs/limits.flan @@ -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) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index cbb7a993..a88787ea 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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;