Skip to content

Commit b45a315

Browse files
committed
Fix float-to-int saturating cast bug
Since Rust 1.45, the `as` keyword performs a *saturating cast* when casting from float to int. See [Rust Reference: Numeric cast](https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast). Co-authored-by: Kani autonomous agent Resolves model-checking#4536
1 parent 864cd08 commit b45a315

4 files changed

Lines changed: 236 additions & 4 deletions

File tree

cprover_bindings/src/goto_program/expr.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ pub enum UnaryOperator {
262262
IsDynamicObject,
263263
/// `isfinite(self)`
264264
IsFinite,
265+
/// `isinf(self)`
266+
IsInf,
267+
/// `isnan(self)`
268+
IsNan,
265269
/// `!self`
266270
Not,
267271
/// `__CPROVER_OBJECT_SIZE(self)`
@@ -1402,7 +1406,7 @@ impl Expr {
14021406
Bitnot | BitReverse | Bswap | Popcount => arg.typ.is_integer(),
14031407
CountLeadingZeros { .. } | CountTrailingZeros { .. } => arg.typ.is_integer(),
14041408
IsDynamicObject | ObjectSize | PointerObject => arg.typ().is_pointer(),
1405-
IsFinite => arg.typ().is_floating_point(),
1409+
IsFinite | IsInf | IsNan => arg.typ().is_floating_point(),
14061410
PointerOffset => arg.typ == Type::void_pointer(),
14071411
Not => arg.typ.is_bool(),
14081412
UnaryMinus => arg.typ().is_numeric(),
@@ -1415,7 +1419,7 @@ impl Expr {
14151419
CountLeadingZeros { .. } | CountTrailingZeros { .. } => Type::unsigned_int(32),
14161420
ObjectSize | PointerObject => Type::size_t(),
14171421
PointerOffset => Type::ssize_t(),
1418-
IsDynamicObject | IsFinite | Not => Type::bool(),
1422+
IsDynamicObject | IsFinite | IsInf | IsNan | Not => Type::bool(),
14191423
Popcount => Type::unsigned_int(32),
14201424
}
14211425
}
@@ -1451,6 +1455,16 @@ impl Expr {
14511455
self.unop(IsFinite)
14521456
}
14531457

1458+
/// `isinf(self)`
1459+
pub fn is_inf(self) -> Self {
1460+
self.unop(IsInf)
1461+
}
1462+
1463+
/// `isnan(self)`
1464+
pub fn is_nan(self) -> Self {
1465+
self.unop(IsNan)
1466+
}
1467+
14541468
/// `-self`
14551469
pub fn neg(self) -> Expr {
14561470
self.unop(UnaryMinus)

cprover_bindings/src/irep/to_irep.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ impl ToIrepId for UnaryOperator {
117117
UnaryOperator::CountTrailingZeros { .. } => IrepId::CountTrailingZeros,
118118
UnaryOperator::IsDynamicObject => IrepId::IsDynamicObject,
119119
UnaryOperator::IsFinite => IrepId::IsFinite,
120+
UnaryOperator::IsInf => IrepId::Isinf,
121+
UnaryOperator::IsNan => IrepId::Isnan,
120122
UnaryOperator::Not => IrepId::Not,
121123
UnaryOperator::ObjectSize => IrepId::ObjectSize,
122124
UnaryOperator::PointerObject => IrepId::PointerObject,

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

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ use rustc_public::mir::{
2727
};
2828
use rustc_public::rustc_internal;
2929
use rustc_public::ty::{
30-
Binder, ClosureKind, ExistentialPredicate, IntTy, RigidTy, Size, Ty, TyConst, TyKind, UintTy,
31-
VariantIdx,
30+
Binder, ClosureKind, ExistentialPredicate, FloatTy, IntTy, RigidTy, Size, Ty, TyConst, TyKind,
31+
UintTy, VariantIdx,
3232
};
3333
use std::collections::BTreeMap;
3434
use tracing::{debug, trace, warn};
@@ -1086,6 +1086,15 @@ impl GotocCtx<'_, '_> {
10861086
let src_ty_kind = src_ty.kind();
10871087
let dst_ty_kind = dst_ty.kind();
10881088

1089+
// Float to integer casting requires special handling for saturating semantics.
1090+
// Since Rust 1.45, the `as` keyword performs a saturating cast when casting
1091+
// from float to int. See https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast
1092+
if let TyKind::RigidTy(RigidTy::Float(_)) = src_ty_kind
1093+
&& dst_ty_kind.is_integral()
1094+
{
1095+
return self.codegen_float_to_int_saturating_cast(src, dst_ty);
1096+
}
1097+
10891098
// number casting
10901099
if src_ty_kind.is_numeric() && dst_ty_kind.is_numeric() {
10911100
return self.codegen_operand_stable(src).cast_to(self.codegen_ty_stable(dst_ty));
@@ -1162,6 +1171,143 @@ impl GotocCtx<'_, '_> {
11621171
}
11631172
}
11641173

1174+
/// Codegen a float-to-int cast with Rust's saturating semantics.
1175+
///
1176+
/// Since Rust 1.45, the `as` keyword performs a *saturating cast* when casting from float to int:
1177+
/// - If the value is NaN, the result is 0
1178+
/// - If the value exceeds the upper bound, the result is MAX
1179+
/// - If the value is below the lower bound, the result is MIN (or 0 for unsigned types)
1180+
/// - If the value is positive infinity, the result is MAX
1181+
/// - If the value is negative infinity, the result is MIN (or 0 for unsigned types)
1182+
/// - Otherwise, the value is truncated toward zero
1183+
///
1184+
/// See: https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast
1185+
fn codegen_float_to_int_saturating_cast(&mut self, src: &Operand, dst_ty: Ty) -> Expr {
1186+
let src_expr = self.codegen_operand_stable(src);
1187+
let dst_goto_ty = self.codegen_ty_stable(dst_ty);
1188+
let mm = self.symbol_table.machine_model();
1189+
1190+
// Get the integer type bounds
1191+
let int_min = dst_goto_ty.min_int_expr(mm);
1192+
let int_max = dst_goto_ty.max_int_expr(mm);
1193+
1194+
// Get the source float type for creating bound constants
1195+
let src_ty = self.operand_ty_stable(src);
1196+
1197+
// Get the integer bounds as float constants for comparison.
1198+
// Note: We convert the integer bounds to float for comparison.
1199+
// For very large integer types (i128, u128), the float conversion may lose precision,
1200+
// but this is acceptable because:
1201+
// 1. Any float value that's truly larger than MAX_INT will still compare as > MAX_INT
1202+
// 2. Any float value that's truly smaller than MIN_INT will still compare as < MIN_INT
1203+
let int_min_as_float = self.int_bounds_as_float(dst_ty, false, src_ty);
1204+
let int_max_as_float = self.int_bounds_as_float(dst_ty, true, src_ty);
1205+
1206+
// Determine if the destination type is signed
1207+
let dst_is_signed = matches!(dst_ty.kind(), TyKind::RigidTy(RigidTy::Int(_)));
1208+
1209+
// Check for special cases:
1210+
// 1. isnan(src) -> result is 0
1211+
// 2. src > int_max -> result is MAX
1212+
// 3. src < int_min -> result is MIN (or 0 for unsigned)
1213+
// 4. Otherwise -> truncate toward zero
1214+
let is_nan = src_expr.clone().is_nan();
1215+
let above_max = src_expr.clone().gt(int_max_as_float.clone());
1216+
let below_min = src_expr.clone().lt(int_min_as_float);
1217+
1218+
// The truncated value (normal cast behavior for values in range)
1219+
let truncated = src_expr.cast_to(dst_goto_ty.clone());
1220+
1221+
// Build the nested ternary expression:
1222+
// isnan ? 0 : (above_max ? MAX : (below_min ? MIN : truncated))
1223+
let int_zero = dst_goto_ty.zero();
1224+
let min_value = if dst_is_signed { int_min } else { int_zero.clone() };
1225+
1226+
// Note: For positive infinity, above_max will be true.
1227+
// For negative infinity, below_min will be true.
1228+
// So infinity cases are handled by the bounds checks.
1229+
below_min
1230+
.ternary(min_value, above_max.ternary(int_max, is_nan.ternary(int_zero, truncated)))
1231+
}
1232+
1233+
/// Convert an integer bound (min or max) to a float constant for the given float type.
1234+
fn int_bounds_as_float(&self, int_ty: Ty, is_max: bool, float_ty: Ty) -> Expr {
1235+
let mm = self.symbol_table.machine_model();
1236+
1237+
// Get the integer bound value
1238+
let bound_value: f64 = match int_ty.kind() {
1239+
TyKind::RigidTy(RigidTy::Int(int_kind)) => {
1240+
if is_max {
1241+
match int_kind {
1242+
IntTy::I8 => i8::MAX as f64,
1243+
IntTy::I16 => i16::MAX as f64,
1244+
IntTy::I32 => i32::MAX as f64,
1245+
IntTy::I64 => i64::MAX as f64,
1246+
IntTy::I128 => i128::MAX as f64,
1247+
IntTy::Isize => {
1248+
if mm.pointer_width == 32 {
1249+
i32::MAX as f64
1250+
} else {
1251+
i64::MAX as f64
1252+
}
1253+
}
1254+
}
1255+
} else {
1256+
match int_kind {
1257+
IntTy::I8 => i8::MIN as f64,
1258+
IntTy::I16 => i16::MIN as f64,
1259+
IntTy::I32 => i32::MIN as f64,
1260+
IntTy::I64 => i64::MIN as f64,
1261+
IntTy::I128 => i128::MIN as f64,
1262+
IntTy::Isize => {
1263+
if mm.pointer_width == 32 {
1264+
i32::MIN as f64
1265+
} else {
1266+
i64::MIN as f64
1267+
}
1268+
}
1269+
}
1270+
}
1271+
}
1272+
TyKind::RigidTy(RigidTy::Uint(uint_kind)) => {
1273+
if is_max {
1274+
match uint_kind {
1275+
UintTy::U8 => u8::MAX as f64,
1276+
UintTy::U16 => u16::MAX as f64,
1277+
UintTy::U32 => u32::MAX as f64,
1278+
UintTy::U64 => u64::MAX as f64,
1279+
UintTy::U128 => u128::MAX as f64,
1280+
UintTy::Usize => {
1281+
if mm.pointer_width == 32 {
1282+
u32::MAX as f64
1283+
} else {
1284+
u64::MAX as f64
1285+
}
1286+
}
1287+
}
1288+
} else {
1289+
0.0 // MIN for unsigned is always 0
1290+
}
1291+
}
1292+
_ => unreachable!("Expected integer type"),
1293+
};
1294+
1295+
// Create the float constant in the appropriate float type
1296+
match float_ty.kind() {
1297+
TyKind::RigidTy(RigidTy::Float(FloatTy::F16)) => {
1298+
Expr::float16_constant(bound_value as f16)
1299+
}
1300+
TyKind::RigidTy(RigidTy::Float(FloatTy::F32)) => {
1301+
Expr::float_constant(bound_value as f32)
1302+
}
1303+
TyKind::RigidTy(RigidTy::Float(FloatTy::F64)) => Expr::double_constant(bound_value),
1304+
TyKind::RigidTy(RigidTy::Float(FloatTy::F128)) => {
1305+
Expr::float128_constant(bound_value as f128)
1306+
}
1307+
_ => unreachable!("Expected float type"),
1308+
}
1309+
}
1310+
11651311
/// "Pointer casts" are particular kinds of pointer-to-pointer casts.
11661312
/// See the [`PointerCoercion`] type for specifics.
11671313
/// Note that this does not include all casts involving pointers,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
//! Regression test for float-to-int saturating cast (GitHub issue #4536).
5+
//! Since Rust 1.45, `as` performs saturating casts from float to int.
6+
7+
// Test case from issue #4536
8+
#[kani::proof]
9+
fn check_issue_4536_f32_to_u8() {
10+
let x: f32 = 300.0;
11+
let y: u8 = x as u8;
12+
assert!(y == 255); // Should saturate to u8::MAX
13+
}
14+
15+
// f32 saturating casts
16+
#[kani::proof]
17+
fn check_f32_to_u8_above_max() {
18+
let y: u8 = 300.0f32 as u8;
19+
assert!(y == u8::MAX);
20+
}
21+
22+
#[kani::proof]
23+
fn check_f32_to_u8_below_min() {
24+
let y: u8 = (-10.0f32) as u8;
25+
assert!(y == 0);
26+
}
27+
28+
#[kani::proof]
29+
fn check_f32_to_u8_nan() {
30+
let y: u8 = f32::NAN as u8;
31+
assert!(y == 0);
32+
}
33+
34+
#[kani::proof]
35+
fn check_f32_to_u8_infinity() {
36+
let y: u8 = f32::INFINITY as u8;
37+
assert!(y == u8::MAX);
38+
}
39+
40+
#[kani::proof]
41+
fn check_f32_to_i8_above_max() {
42+
let y: i8 = 200.0f32 as i8;
43+
assert!(y == i8::MAX);
44+
}
45+
46+
#[kani::proof]
47+
fn check_f32_to_i8_below_min() {
48+
let y: i8 = (-200.0f32) as i8;
49+
assert!(y == i8::MIN);
50+
}
51+
52+
// f64 saturating casts
53+
#[kani::proof]
54+
fn check_f64_to_u8_above_max() {
55+
let y: u8 = 300.0f64 as u8;
56+
assert!(y == u8::MAX);
57+
}
58+
59+
#[kani::proof]
60+
fn check_f64_to_i8_below_min() {
61+
let y: i8 = (-200.0f64) as i8;
62+
assert!(y == i8::MIN);
63+
}
64+
65+
// In-range casts should truncate toward zero
66+
#[kani::proof]
67+
fn check_f32_truncation() {
68+
let y: i8 = (-99.9f32) as i8;
69+
assert!(y == -99);
70+
}

0 commit comments

Comments
 (0)