From cf04bfc8fd0fbf6c9f14ba716b19cf4ecb2a891a Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 25 Sep 2026 10:15:36 +0700 Subject: [PATCH] A u64 converts to and from a float on x86 as it does under LLVM, across the top half of its range --- TODO.org | 9 +++-- lib/x86.ml | 44 ++++++++++++++++++++++-- runtime/flan_rt.c | 16 ++++++--- test/programs/arith.flan | 3 ++ test/programs/u64-float.flan | 65 ++++++++++++++++++++++++++++++++++++ test/test_acceptance.ml | 25 ++++++++++++++ 6 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 test/programs/u64-float.flan diff --git a/TODO.org b/TODO.org index 8ac565ac..82a223d3 100644 --- a/TODO.org +++ b/TODO.org @@ -1092,9 +1092,12 @@ build included, reaches =emit_globals_init= as =Emit.startup_plan='s straight into their symbol are =Tast.const_init= ones, which neither transfer nor read anything. Nothing to change. -** TODO A u64 converted to f64 is signed on x86 -=(f64 (u64 18446744073709551615))= prints =1.84467e+19= under LLVM and =-1= under -=--x86=, with a literal or a run-time =u64= alike. +** DONE A u64 converted to f64 is signed on x86 +CLOSED: [2026-09-25] +=--x86= converts =u64= to and from =f64= and =f32= the way LLVM's =uitofp= and +=fptoui= do: the halve-and-double sequence one way, subtract 2^63 and set the top +bit the other, ties rounding to even. =test/programs/u64-float.flan= runs on both +backends. A cast that misses =u64='s range reports it as =[0 18446744073709551615]=. ** DONE An aggregate built in place never reads its own destination CLOSED: [2026-09-25] diff --git a/lib/x86.ml b/lib/x86.ml index 168c54cc..ed4c8abe 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -1333,6 +1333,7 @@ let option_lay f (t : Types.t) = let cc_e = 4 and cc_ne = 5 let cc_b = 2 and cc_ae = 3 and cc_be = 6 and cc_a = 7 let cc_l = 12 and cc_ge = 13 and cc_le = 14 and cc_g = 15 +let cc_s = 8 let int_cc ~signed (p : Tast.prim) = match p, signed with @@ -3419,9 +3420,29 @@ and cast f (a : Tast.expr) (target : Types.t) dst = cvtss2sd f.b ~dst:xmm0 ~src:xmm0; fstore f.b ~src:xmm0 ~mm:(lmem f dst ~scratch:r11) ~f64:(f64_of dst_t) | false, true -> + let f64 = f64_of dst_t in load_loc f ~reg:rax l src_t; - cvtsi2f f.b ~f64:(f64_of dst_t) ~dst:xmm0 ~src:rax; - fstore f.b ~src:xmm0 ~mm:(lmem f dst ~scratch:r11) ~f64:(f64_of dst_t) + if src_t = Types.Int Types.U64 then begin + (* [cvtsi2sd] reads the register as signed, so a u64 with the top bit + set is halved first and doubled after. The bit shifted out is ORed + back into the lowest one, which keeps the halved value from landing + exactly on a tie it was not on, so the one rounding the conversion + does is the rounding of the whole value. *) + let big = new_label f "u2fbig" and done_ = new_label f "u2fdone" in + test_rr f.b ~a:rax ~c:rax; + jcc_lbl f.b ~cc:cc_s big; + cvtsi2f f.b ~f64 ~dst:xmm0 ~src:rax; + jmp_lbl f.b done_; + lbl f.b big; + mov_rr f.b ~dst:rcx ~src:rax; + shr_imm f.b ~dst:rcx ~n:1; + and_imm f.b ~dst:rax 1; + or_rr f.b ~dst:rcx ~src:rax; + cvtsi2f f.b ~f64 ~dst:xmm0 ~src:rcx; + farith f.b ~op:0x58 ~f64 ~dst:xmm0 ~src:xmm0; + lbl f.b done_ + end else cvtsi2f f.b ~f64 ~dst:xmm0 ~src:rax; + fstore f.b ~src:xmm0 ~mm:(lmem f dst ~scratch:r11) ~f64 | true, false -> fload f.b ~dst:xmm0 ~mm:(lmem f l ~scratch:r11) ~f64:(f64_of src_t); (* [cvttsd2si] answers a fixed "integer indefinite" for a value out of @@ -3431,7 +3452,24 @@ and cast f (a : Tast.expr) (target : Types.t) dst = (match src_t, dst_t with | Types.Float sk, Types.Int k -> check_cast f a.Tast.loc sk k | _ -> ()); - cvttf2si f.b ~f64:(f64_of src_t) ~dst:rax ~src:xmm0; + let f64 = f64_of src_t in + if dst_t = Types.Int Types.U64 then begin + (* [cvttsd2si] answers only for the signed range, so a value at 2^63 or + above has 2^63 taken off before the conversion and the top bit set + after it. A NaN compares unordered and takes the first branch. *) + let big = new_label f "f2ubig" and done_ = new_label f "f2udone" in + fload f.b ~dst:1 ~mm:(Sym (float_const f (ldexp 1.0 63) ~f64, 0)) ~f64; + ucomis f.b ~f64 ~a:xmm0 ~c:1; + jcc_lbl f.b ~cc:cc_ae big; + cvttf2si f.b ~f64 ~dst:rax ~src:xmm0; + jmp_lbl f.b done_; + lbl f.b big; + farith f.b ~op:0x5c ~f64 ~dst:xmm0 ~src:1; + cvttf2si f.b ~f64 ~dst:rax ~src:xmm0; + imm_into f ~reg:rcx Int64.min_int; + xor_rr f.b ~dst:rax ~src:rcx; + lbl f.b done_ + end else cvttf2si f.b ~f64 ~dst:rax ~src:xmm0; store_loc f ~reg:rax dst dst_t (* ── Call frame information ──────────────────────────────────────────── *) diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index 1332d9f6..986cc6a0 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -1020,11 +1020,19 @@ static void flan_arith_fail(const uint8_t *loc, int64_t loclen, int32_t op, "to\n", (int)loclen, (const char *)loc); break; + /* An unsigned type's range starts at zero and a signed one's below it, so + * the lower bound says how to read the upper one: u64's is all ones. */ default: - fprintf(stderr, - "%.*s: this value does not fit the integer type it is cast to, " - "which holds [%lld %lld]\n", - (int)loclen, (const char *)loc, (long long)lhs, (long long)rhs); + if (lhs == 0) + fprintf(stderr, + "%.*s: this value does not fit the integer type it is cast to, " + "which holds [0 %llu]\n", + (int)loclen, (const char *)loc, (unsigned long long)rhs); + else + fprintf(stderr, + "%.*s: this value does not fit the integer type it is cast to, " + "which holds [%lld %lld]\n", + (int)loclen, (const char *)loc, (long long)lhs, (long long)rhs); break; } rt_die(); diff --git a/test/programs/arith.flan b/test/programs/arith.flan index 9a9d0601..e8d200da 100644 --- a/test/programs/arith.flan +++ b/test/programs/arith.flan @@ -82,6 +82,9 @@ (= n 10) (print (i32 (/ (f64 1.0) (f64 0.0)))) (= n 11) (print (u8 (/ (f32 -1.0) (f32 0.0)))) (= n 12) (print (i32 (/ (f32 0.0) (f32 0.0)))) + ;; A u64 destination, whose upper bound is all ones and is printed as + ;; the unsigned number it is. + (= n 13) (print (u64 (* huge 1e-280))) :else (println "?")) 0)) diff --git a/test/programs/u64-float.flan b/test/programs/u64-float.flan new file mode 100644 index 00000000..ee0f8fa4 --- /dev/null +++ b/test/programs/u64-float.flan @@ -0,0 +1,65 @@ +;;;; Conversions between the unsigned integers and the floats, across the top +;;;; half of u64's range, where the value does not fit a signed register. Both +;;;; backends print the same lines. Each case is written twice: once through a +;;;; global, so the conversion happens at run time, and once on a literal. +;;;; +;;;; A float is printed by converting it back to an integer, because the +;;;; printed float has six digits and every case here differs past the sixth. + +(defonce top u64 18446744073709551615) ; 2^64 - 1 +(defonce half1 u64 9223372036854775809) ; 2^63 + 1 +(defonce tie0 u64 9223372036854776832) ; 2^63 + 1024, a tie that rounds down to even +(defonce tie1 u64 9223372036854778880) ; 2^63 + 3072, a tie that rounds up to even +(defonce above u64 9223372036854776833) ; 2^63 + 1025, just past a tie +(defonce small u64 12345) +(defonce ftie0 u64 9223372586610589696) ; 2^63 + 2^39, an f32 tie that rounds down +(defonce fup u64 9223372586610589697) ; 2^63 + 2^39 + 1 +(defonce ftie1 u64 9223373686122217472) ; 2^63 + 3*2^39, an f32 tie that rounds up +(defonce u32top u32 4294967295) + +(defonce d63 f64 9223372036854775808.0) ; 2^63 +(defonce dmax f64 18446744073709549568.0) ; the largest f64 below 2^64 +(defonce dbelow f64 9223372036854774784.0) ; the largest f64 below 2^63 +(defonce d15 f64 1.5e19) +(defonce dsmall f64 7.9) +(defonce s63 f32 9223372036854775808.0) +(defonce smax f32 18446742974197923840.0) ; the largest f32 below 2^64 +(defonce d32 f64 4294967295.0) +(defonce d3e9 f64 3e9) + +(defn main [] i32 + ;; u64 to f64. + (println (f64 top)) + (println (u64 (* (f64 top) 0.5))) + (println (u64 (f64 half1))) + (println (u64 (f64 tie0))) + (println (u64 (f64 tie1))) + (println (u64 (f64 above))) + (println (u64 (f64 small))) + (println (f64 (u64 18446744073709551615))) + (println (u64 (f64 (u64 9223372036854778880)))) + ;; u64 to f32. + (println (u64 (* (f32 top) (f32 0.5)))) + (println (u64 (f32 ftie0))) + (println (u64 (f32 fup))) + (println (u64 (f32 ftie1))) + (println (u64 (f32 small))) + (println (u64 (f32 (u64 9223372586610589697)))) + ;; u32 to both. + (println (u64 (f64 u32top))) + (println (u64 (f32 u32top))) + ;; f64 and f32 to u64. + (println (u64 d63)) + (println (u64 dmax)) + (println (u64 dbelow)) + (println (u64 d15)) + (println (u64 dsmall)) + (println (u64 s63)) + (println (u64 smax)) + (println (u64 18446744073709549568.0)) + (println (u64 (f32 1.5e19))) + ;; f64 to u32. + (println (u32 d32)) + (println (u32 d3e9)) + (println (u32 4294967295.0)) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index eb357e3b..c92484a2 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -2639,6 +2639,8 @@ let () = "this value is infinite, which has no integer value to cast to"; traps "an f32 NaN cast to an integer" "12" "this value is NaN, which has no integer value to cast to"; + traps "a float too large for a u64" "13" + "which holds [0 18446744073709551615]"; (try Sys.remove exe with Sys_error _ -> ()) in arith (); @@ -2772,6 +2774,29 @@ let () = outputs ~x86:true "an aggregate that reads its destination sees the old value, --x86" "programs/self-read.flan" self_read_out; + (* Conversions between u64 and the floats across the top half of u64's + range, where [cvtsi2sd] and [cvttsd2si] read the register as signed: + x86 answered -1 for (f64 (u64 18446744073709551615)). The expected + lines are what LLVM's [uitofp] and [fptoui] give, ties included. *) + let u64_float_out = + "1.84467e+19\n9223372036854775808\n9223372036854775808\n\ + 9223372036854775808\n9223372036854779904\n9223372036854777856\n\ + 12345\n1.84467e+19\n9223372036854779904\n9223372036854775808\n\ + 9223372036854775808\n9223373136366403584\n9223374235878031360\n\ + 12345\n9223373136366403584\n4294967295\n4294967296\n\ + 9223372036854775808\n18446744073709549568\n9223372036854774784\n\ + 15000000000000000000\n7\n9223372036854775808\n\ + 18446742974197923840\n18446744073709549568\n15000000520515485696\n\ + 4294967295\n3000000000\n4294967295\n" + in + outputs "u64 and the floats convert across the whole range" + "programs/u64-float.flan" u64_float_out; + outputs ~opt:"-O0" + "u64 and the floats convert across the whole range, -O0" + "programs/u64-float.flan" u64_float_out; + outputs ~x86:true + "u64 and the floats convert across the whole range, --x86" + "programs/u64-float.flan" u64_float_out; (* ── Packages: the link follows the program ──────────────────────── A package's C and linker arguments used to come with the import,