Skip to content

Commit 1c73890

Browse files
fix saturating float-to-int cast (#4573)
The issue is that Kani computes int_max_as_float by casting u64::MAX to f64. But u64::MAX (2^64 - 1) isn't exactly representable in f64, so it rounds *up* to 2^64. The old code used a greater-than operation, which would mean a float value equal to exactly 2^64 fails this check, falls through the raw truncation path, and produces a wrong result instead of saturating to u64::MAX. Instead we use greater-than-or-equal so that the boundary value 2^64 correctly saturates. This is safe for types where the max IS exactly representable. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses. Co-authored-by: Felipe R. Monteiro <felisous@amazon.com>
1 parent 1f83c79 commit 1c73890

2 files changed

Lines changed: 19 additions & 3 deletions

File tree

kani-compiler/src/codegen_cprover_gotoc/codegen/rvalue.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,7 +1198,7 @@ impl GotocCtx<'_, '_> {
11981198
// Note: We convert the integer bounds to float for comparison.
11991199
// For very large integer types (i128, u128), the float conversion may lose precision,
12001200
// but this is acceptable because:
1201-
// 1. Any float value that's truly larger than MAX_INT will still compare as > MAX_INT
1201+
// 1. Any float value that's truly larger than MAX_INT will still compare as >= MAX_INT
12021202
// 2. Any float value that's truly smaller than MIN_INT will still compare as < MIN_INT
12031203
let int_min_as_float = self.int_bounds_as_float(dst_ty, false, src_ty);
12041204
let int_max_as_float = self.int_bounds_as_float(dst_ty, true, src_ty);
@@ -1208,11 +1208,11 @@ impl GotocCtx<'_, '_> {
12081208

12091209
// Check for special cases:
12101210
// 1. isnan(src) -> result is 0
1211-
// 2. src > int_max -> result is MAX
1211+
// 2. src >= int_max -> result is MAX
12121212
// 3. src < int_min -> result is MIN (or 0 for unsigned)
12131213
// 4. Otherwise -> truncate toward zero
12141214
let is_nan = src_expr.clone().is_nan();
1215-
let above_max = src_expr.clone().gt(int_max_as_float.clone());
1215+
let above_max = src_expr.clone().ge(int_max_as_float.clone());
12161216
let below_min = src_expr.clone().lt(int_min_as_float);
12171217

12181218
// The truncated value (normal cast behavior for values in range)

tests/kani/Cast/float_to_int_saturating.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright Kani Contributors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

4+
#![feature(f128)]
5+
46
//! Regression test for float-to-int saturating cast (GitHub issue #4536).
57
//! Since Rust 1.45, `as` performs saturating casts from float to int.
68
@@ -68,3 +70,17 @@ fn check_f32_truncation() {
6870
let y: i8 = (-99.9f32) as i8;
6971
assert!(y == -99);
7072
}
73+
74+
#[kani::proof]
75+
fn check_f64_to_u64_saturation() {
76+
let x: f64 = u64::MAX as f64; // = 2^64 in f64
77+
let y: u64 = x as u64;
78+
kani::assert(y == u64::MAX, "matches");
79+
}
80+
81+
#[kani::proof]
82+
fn check_f64_to_u128_saturation() {
83+
let x: f64 = u128::MAX as f64; // = 2^128 in f64
84+
let y: u128 = x as u128;
85+
kani::assert(y == u128::MAX, "matches");
86+
}

0 commit comments

Comments
 (0)