A cast of NaN or an infinity to an integer says so by name, on both backends

This commit is contained in:
Joseph Ferano 2026-09-25 06:58:06 +07:00
parent 5557594f31
commit 6b9d1fa644
7 changed files with 84 additions and 9 deletions

View File

@ -1517,6 +1517,8 @@ let arith_rem_zero = 1
let arith_div_overflow = 2 let arith_div_overflow = 2
let arith_rem_overflow = 3 let arith_rem_overflow = 3
let arith_cast_range = 4 let arith_cast_range = 4
let arith_cast_nan = 5
let arith_cast_inf = 6
(* ArithError's fields are i64 and an operand may be narrower, so every (* ArithError's fields are i64 and an operand may be narrower, so every
operand is widened on the way into the condition — signed or not according operand is widened on the way into the condition — signed or not according
@ -1668,11 +1670,27 @@ let check_cast f ~guard loc (src : Types.fkind) (k : Types.ikind) v =
ins f "%s = fcmp olt double %s, %s" b v (dbl hi_f); ins f "%s = fcmp olt double %s, %s" b v (dbl hi_f);
let ok = fresh f in let ok = fresh f in
ins f "%s = and i1 %s, %s" ok a b; ins f "%s = and i1 %s, %s" ok a b;
(* NaN and the infinities are named rather than reported as out of range:
they are not values that overshot the type, they have no integer at
all. Worked out on the cold path, so the guard is still two compares. *)
signal_block f loc ~guard ok (fun id nn -> signal_block f loc ~guard ok (fun id nn ->
let nan = fresh f in
ins f "%s = fcmp uno double %s, %s" nan v v;
let pinf = fresh f in
ins f "%s = fcmp oeq double %s, %s" pinf v (dbl infinity);
let ninf = fresh f in
ins f "%s = fcmp oeq double %s, %s" ninf v (dbl neg_infinity);
let inf = fresh f in
ins f "%s = or i1 %s, %s" inf pinf ninf;
let c1 = fresh f in
ins f "%s = select i1 %s, i32 %d, i32 %d" c1 inf arith_cast_inf
arith_cast_range;
let code = fresh f in
ins f "%s = select i1 %s, i32 %d, i32 %s" code nan arith_cast_nan c1;
ins f ins f
"call void @flan_arith_error(ptr %s, i64 %d, i32 %d, i64 %Ld, i64 %Ld, \ "call void @flan_arith_error(ptr %s, i64 %d, i32 %s, i64 %Ld, i64 %Ld, \
ptr %s)" ptr %s)"
id nn arith_cast_range lo_i hi_i xfer_param) id nn code lo_i hi_i xfer_param)
end end
(* [at] is strict: the last valid index is len - 1. *) (* [at] is strict: the last valid index is len - 1. *)

View File

@ -123,9 +123,11 @@ let source = {flan|
;; 0 (/ a 0) 1 (% a 0) ;; 0 (/ a 0) 1 (% a 0)
;; 2 (/ min -1) 3 (% min -1) ;; 2 (/ min -1) 3 (% min -1)
;; 4 a float to integer cast whose value does not fit ;; 4 a float to integer cast whose value does not fit
;; 5 a float to integer cast of NaN
;; 6 a float to integer cast of an infinity
;; ;;
;; `lhs` and `rhs` are the two operands for codes 0 through 3 and the ;; `lhs` and `rhs` are the two operands for codes 0 through 3 and the
;; destination type's representable range for code 4 — the violated condition ;; destination type's representable range for codes 4 through 6 — the violated condition
;; written as a range, which is what flan_slice_promise_error already does ;; written as a range, which is what flan_slice_promise_error already does
;; with BoundsError's fields. Two meanings over two fields rather than two ;; with BoundsError's fields. Two meanings over two fields rather than two
;; condition types, so that a handler writes one clause and not five. The ;; condition types, so that a handler writes one clause and not five. The

View File

@ -2812,8 +2812,30 @@ and check_cast f (loc : Loc.t) (src : Types.fkind) (k : Types.ikind) =
ucomis f.b ~f64 ~a:1 ~c:xmm0; ucomis f.b ~f64 ~a:1 ~c:xmm0;
jcc_lbl f.b ~cc:cc_a ok; jcc_lbl f.b ~cc:cc_a ok;
lbl f.b bad; lbl f.b bad;
(* Which code, on the cold path: NaN and the infinities are named rather
than reported as out of range. xmm0 still holds the value here. *)
let call = new_label f "nofitcall" and notnan = new_label f "notnan"
and isinf = new_label f "isinf" in
imm_into f ~reg:rax (Int64.of_int Emit.arith_cast_range); imm_into f ~reg:rax (Int64.of_int Emit.arith_cast_range);
store_int f.b ~src:rax ~mm:(Frame so) ~size:8; store_int f.b ~src:rax ~mm:(Frame so) ~size:8;
ucomis f.b ~f64 ~a:xmm0 ~c:xmm0;
jcc_lbl f.b ~cc:cc_np notnan;
imm_into f ~reg:rax (Int64.of_int Emit.arith_cast_nan);
store_int f.b ~src:rax ~mm:(Frame so) ~size:8;
jmp_lbl f.b call;
lbl f.b notnan;
let kpinf = float_const f infinity ~f64
and kninf = float_const f neg_infinity ~f64 in
fload f.b ~dst:1 ~mm:(Sym (kpinf, 0)) ~f64;
ucomis f.b ~f64 ~a:xmm0 ~c:1;
jcc_lbl f.b ~cc:cc_e isinf;
fload f.b ~dst:1 ~mm:(Sym (kninf, 0)) ~f64;
ucomis f.b ~f64 ~a:xmm0 ~c:1;
jcc_lbl f.b ~cc:cc_ne call;
lbl f.b isinf;
imm_into f ~reg:rax (Int64.of_int Emit.arith_cast_inf);
store_int f.b ~src:rax ~mm:(Frame so) ~size:8;
lbl f.b call;
imm_into f ~reg:rax lo_i; imm_into f ~reg:rax lo_i;
store_int f.b ~src:rax ~mm:(Frame sa) ~size:8; store_int f.b ~src:rax ~mm:(Frame sa) ~size:8;
imm_into f ~reg:rax hi_i; imm_into f ~reg:rax hi_i;

View File

@ -969,7 +969,9 @@ enum {
FLAN_ARITH_REM_ZERO = 1, FLAN_ARITH_REM_ZERO = 1,
FLAN_ARITH_DIV_OVERFLOW = 2, FLAN_ARITH_DIV_OVERFLOW = 2,
FLAN_ARITH_REM_OVERFLOW = 3, FLAN_ARITH_REM_OVERFLOW = 3,
FLAN_ARITH_CAST_RANGE = 4 FLAN_ARITH_CAST_RANGE = 4,
FLAN_ARITH_CAST_NAN = 5,
FLAN_ARITH_CAST_INF = 6
}; };
typedef struct { int32_t op; int64_t lhs, rhs; } flan_arith_cond; typedef struct { int32_t op; int64_t lhs, rhs; } flan_arith_cond;
@ -1005,6 +1007,19 @@ static void flan_arith_fail(const uint8_t *loc, int64_t loclen, int32_t op,
op == FLAN_ARITH_DIV_OVERFLOW ? "/" : "%", (long long)lhs, op == FLAN_ARITH_DIV_OVERFLOW ? "/" : "%", (long long)lhs,
(long long)rhs); (long long)rhs);
break; break;
/* NaN and the infinities did not overshoot the range: no integer is
* their value, whatever the type. Saying "does not fit" reads as too big. */
case FLAN_ARITH_CAST_NAN:
fprintf(stderr,
"%.*s: this value is NaN, which has no integer value to cast to\n",
(int)loclen, (const char *)loc);
break;
case FLAN_ARITH_CAST_INF:
fprintf(stderr,
"%.*s: this value is infinite, which has no integer value to cast "
"to\n",
(int)loclen, (const char *)loc);
break;
default: default:
fprintf(stderr, fprintf(stderr,
"%.*s: this value does not fit the integer type it is cast to, " "%.*s: this value does not fit the integer type it is cast to, "

View File

@ -180,7 +180,8 @@
;; NaN fails both halves of the range test, which is deliberate: a NaN ;; NaN fails both halves of the range test, which is deliberate: a NaN
;; cast to an integer is exactly as undefined as a value out of range, ;; cast to an integer is exactly as undefined as a value out of range,
;; and an unordered comparison would have waved it through. ;; and an unordered comparison would have waved it through. Its code is
;; 5, which names NaN, rather than 4, which says it overshot the range.
(cast-frame (/ (f64 0.0) (f64 0.0))) (cast-frame (/ (f64 0.0) (f64 0.0)))
(show "op" (i64 op)) (show "op" (i64 op))

View File

@ -76,6 +76,12 @@
;; double there, which agree because every bound is a power of two and is ;; double there, which agree because every bound is a power of two and is
;; exact in both. ;; exact in both.
(= n 9) (print (i32 wide)) (= n 9) (print (i32 wide))
;; NaN and the infinities have no integer value at all, so the message
;; names them rather than a range they did not overshoot. An f32 source
;; for each too, since the two backends test it at different widths.
(= 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))))
:else (println "?")) :else (println "?"))
0)) 0))

View File

@ -2543,8 +2543,8 @@ let () =
no overflow case because it has no most-negative value, and a float no overflow case because it has no most-negative value, and a float
division by zero, which is an infinity and is a defined answer this division by zero, which is an infinity and is a defined answer this
language has no business refusing. *) language has no business refusing. *)
let arith ?opt () = let arith ?opt ?x86 () =
let exe = compile ?opt "programs/arith.flan" in let exe = compile ?opt ?x86 "programs/arith.flan" in
let traps name arg reason = let traps name arg reason =
let code, text = run exe (Some arg) in let code, text = run exe (Some arg) in
if code <> 134 if code <> 134
@ -2587,7 +2587,7 @@ let () =
would have waved it through into an fptosi that is as undefined for a would have waved it through into an fptosi that is as undefined for a
NaN as it is for 1e300. *) NaN as it is for 1e300. *)
traps "NaN cast to an integer" "7" traps "NaN cast to an integer" "7"
"does not fit the integer type it is cast to"; "this value is NaN, which has no integer value to cast to";
(* One width down, and this is the case the two backends disagreed about (* One width down, and this is the case the two backends disagreed about
*silently* rather than both dying: x86 loaded the operands *silently* rather than both dying: x86 loaded the operands
sign-extended into 64-bit registers, divided there and truncated on sign-extended into 64-bit registers, divided there and truncated on
@ -2604,10 +2604,21 @@ let () =
and this is the row that says so rather than the comment. *) and this is the row that says so rather than the comment. *)
traps "an f32 too large for an i32" "9" traps "an f32 too large for an i32" "9"
"which holds [-2147483648 2147483647]"; "which holds [-2147483648 2147483647]";
(* NaN and the infinities are named: they did not overshoot a range,
they have no integer value at all. *)
traps "an infinity cast to an integer" "10"
"this value is infinite, which has no integer value to cast to";
traps "an f32 negative infinity cast to a u8" "11"
"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";
(try Sys.remove exe with Sys_error _ -> ()) (try Sys.remove exe with Sys_error _ -> ())
in in
arith (); arith ();
arith ~opt:"-O0" (); arith ~opt:"-O0" ();
(* The x86 backend picks the code with its own instructions, so the three
named cases are asserted there as well as in the survey. *)
arith ~x86:true ();
(* The release build drops the guards. Asserted on the IR and not by (* The release build drops the guards. Asserted on the IR and not by
running an unchecked program, for the reason the bounds case gives: an running an unchecked program, for the reason the bounds case gives: an
@ -2646,7 +2657,7 @@ let () =
op 2\nlhs -9223372036854775808\nrhs -1\nop 3\n\ op 2\nlhs -9223372036854775808\nrhs -1\nop 3\n\
cast 3\ncast -3\n\ cast 3\ncast -3\n\
op 4\nlhs -9223372036854775808\nrhs 9223372036854775807\nop 4\n\ op 4\nlhs -9223372036854775808\nrhs 9223372036854775807\nop 4\n\
cast8 12\nlhs -128\nrhs 127\nop 4\n\ cast8 12\nlhs -128\nrhs 127\nop 5\n\
lit 4611686018427387903\nu 14\n\ lit 4611686018427387903\nu 14\n\
frames 4\nskipped 8\ncleaned 12\n" frames 4\nskipped 8\ncleaned 12\n"
in in