Skip to content

Commit e53511e

Browse files
authored
Add a Kani function that checks if the range of a float is valid for conversion to int (#3742)
This introduces a new generic Kani function, `kani::float::float_to_int_in_range`, that can be used for checking if a floating-point value satisfies the range requirement of the `to_int_unchecked` methods, e.g https://doc.rust-lang.org/std/primitive.f32.html#method.to_int_unchecked and https://doc.rust-lang.org/std/primitive.f64.html#method.to_int_unchecked. Resolves #3711 By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.
1 parent 17f4ff1 commit e53511e

File tree

11 files changed

+166
-1
lines changed

11 files changed

+166
-1
lines changed

kani-compiler/src/codegen_cprover_gotoc/overrides/hooks.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
//! It would be too nasty if we spread around these sort of undocumented hooks in place, so
99
//! this module addresses this issue.
1010
11-
use crate::codegen_cprover_gotoc::GotocCtx;
1211
use crate::codegen_cprover_gotoc::codegen::{PropertyClass, bb_label};
12+
use crate::codegen_cprover_gotoc::{GotocCtx, utils};
1313
use crate::kani_middle::attributes;
1414
use crate::kani_middle::kani_functions::{KaniFunction, KaniHook};
1515
use crate::unwrap_or_return_codegen_unimplemented_stmt;
@@ -19,6 +19,7 @@ use rustc_middle::ty::TyCtxt;
1919
use rustc_smir::rustc_internal;
2020
use stable_mir::mir::mono::Instance;
2121
use stable_mir::mir::{BasicBlockIdx, Place};
22+
use stable_mir::ty::RigidTy;
2223
use stable_mir::{CrateDef, ty::Span};
2324
use std::collections::HashMap;
2425
use std::rc::Rc;
@@ -315,6 +316,53 @@ impl GotocHook for IsAllocated {
315316
}
316317
}
317318

319+
/// This is the hook for the `kani::float::float_to_int_in_range` intrinsic
320+
/// TODO: This should be replaced by a Rust function instead so that it's
321+
/// independent of the backend
322+
struct FloatToIntInRange;
323+
impl GotocHook for FloatToIntInRange {
324+
fn hook_applies(&self, _tcx: TyCtxt, _instance: Instance) -> bool {
325+
unreachable!("{UNEXPECTED_CALL}")
326+
}
327+
328+
fn handle(
329+
&self,
330+
gcx: &mut GotocCtx,
331+
instance: Instance,
332+
mut fargs: Vec<Expr>,
333+
assign_to: &Place,
334+
target: Option<BasicBlockIdx>,
335+
span: Span,
336+
) -> Stmt {
337+
assert_eq!(fargs.len(), 1);
338+
let float = fargs.remove(0);
339+
let target = target.unwrap();
340+
let loc = gcx.codegen_span_stable(span);
341+
342+
let generic_args = instance.args().0;
343+
let RigidTy::Float(float_ty) = *generic_args[0].expect_ty().kind().rigid().unwrap() else {
344+
unreachable!()
345+
};
346+
let integral_ty = generic_args[1].expect_ty().kind().rigid().unwrap().clone();
347+
348+
let is_in_range = utils::codegen_in_range_expr(
349+
&float,
350+
float_ty,
351+
integral_ty,
352+
gcx.symbol_table.machine_model(),
353+
)
354+
.cast_to(Type::CInteger(CIntType::Bool));
355+
356+
let pe = unwrap_or_return_codegen_unimplemented_stmt!(
357+
gcx,
358+
gcx.codegen_place_stable(assign_to, loc)
359+
)
360+
.goto_expr;
361+
362+
Stmt::block(vec![pe.assign(is_in_range, loc), Stmt::goto(bb_label(target), loc)], loc)
363+
}
364+
}
365+
318366
/// Encodes __CPROVER_pointer_object(ptr)
319367
struct PointerObject;
320368
impl GotocHook for PointerObject {
@@ -663,6 +711,7 @@ pub fn fn_hooks() -> GotocHooks {
663711
(KaniHook::PointerOffset, Rc::new(PointerOffset)),
664712
(KaniHook::UntrackedDeref, Rc::new(UntrackedDeref)),
665713
(KaniHook::InitContracts, Rc::new(InitContracts)),
714+
(KaniHook::FloatToIntInRange, Rc::new(FloatToIntInRange)),
666715
];
667716
GotocHooks {
668717
kani_lib_hooks: HashMap::from(kani_lib_hooks),

kani-compiler/src/kani_middle/kani_functions.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ pub enum KaniHook {
125125
Check,
126126
#[strum(serialize = "CoverHook")]
127127
Cover,
128+
// TODO: this is temporarily implemented as a hook, but should be implemented as an intrinsic
129+
#[strum(serialize = "FloatToIntInRangeHook")]
130+
FloatToIntInRange,
128131
#[strum(serialize = "InitContractsHook")]
129132
InitContracts,
130133
#[strum(serialize = "IsAllocatedHook")]

kani_metadata/src/unstable.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ pub enum UnstableFeature {
100100
UnstableOptions,
101101
/// The list subcommand [RFC 13](https://model-checking.github.io/kani/rfc/rfcs/0013-list.html)
102102
List,
103+
/// Kani APIs related to floating-point operations (e.g. `float_to_int_in_range`)
104+
FloatLib,
103105
}
104106

105107
impl UnstableFeature {

library/kani/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#![feature(ptr_metadata)]
2121
#![feature(f16)]
2222
#![feature(f128)]
23+
#![feature(convert_float_to_int)]
2324

2425
// Allow us to use `kani::` to access crate features.
2526
extern crate self as kani;

library/kani_core/src/float.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
//! This module contains functions useful for float-related checks
5+
6+
#[allow(clippy::crate_in_macro_def)]
7+
#[macro_export]
8+
macro_rules! generate_float {
9+
($core:path) => {
10+
use super::kani_intrinsic;
11+
use core::convert::FloatToInt;
12+
/// Returns whether the given float `value` satisfies the range
13+
/// condition of the `to_int_unchecked` methods, namely that the `value`
14+
/// after truncation is in range of the target `Int`
15+
///
16+
/// # Example:
17+
///
18+
/// ```no_run
19+
/// let f: f32 = 145.7;
20+
/// let fits_in_i8 = kani::float::float_to_int_in_range::<f32, i8>(f);
21+
/// // doesn't fit in `i8` because the value after truncation (`145.0`) is larger than `i8::MAX`
22+
/// assert!(!fits_in_i8);
23+
///
24+
/// let f: f64 = 1e6;
25+
/// let fits_in_u32 = kani::float::float_to_int_in_range::<f64, u32>(f);
26+
/// // fits in `u32` because the value after truncation (`1e6`) is smaller than `u32::MAX`
27+
/// assert!(fits_in_u32);
28+
/// ```
29+
#[crate::kani::unstable_feature(
30+
feature = "float-lib",
31+
issue = "none",
32+
reason = "experimental floating-point API"
33+
)]
34+
#[kanitool::fn_marker = "FloatToIntInRangeHook"]
35+
#[inline(never)]
36+
pub fn float_to_int_in_range<Float, Int>(value: Float) -> bool
37+
where
38+
Float: FloatToInt<Int>,
39+
{
40+
kani_intrinsic()
41+
}
42+
};
43+
}

library/kani_core/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#![feature(f128)]
2222

2323
mod arbitrary;
24+
mod float;
2425
mod mem;
2526
mod mem_init;
2627
mod models;
@@ -45,6 +46,10 @@ macro_rules! kani_lib {
4546
kani_core::generate_arbitrary!(core);
4647
kani_core::generate_models!();
4748

49+
pub mod float {
50+
kani_core::generate_float!(core);
51+
}
52+
4853
pub mod mem {
4954
kani_core::kani_mem!(core);
5055
}
@@ -61,6 +66,10 @@ macro_rules! kani_lib {
6166
kani_core::generate_arbitrary!(std);
6267
kani_core::generate_models!();
6368

69+
pub mod float {
70+
kani_core::generate_float!(std);
71+
}
72+
6473
pub mod mem {
6574
kani_core::kani_mem!(std);
6675
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
error[E0277]: the trait bound `i32: std::convert::FloatToInt<u8>` is not satisfied
2+
invalid_float.rs:
3+
| let _c = kani::float::float_to_int_in_range::<i32, u8>(i);
4+
| ^^^ the trait `std::convert::FloatToInt<u8>` is not implemented for `i32`
5+
|
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
//! This test checks that Kani emits an error when
5+
//! `kani::float::float_to_int_in_range` is instantiated with a non-float type
6+
7+
#[kani::proof]
8+
fn check_invalid_float() {
9+
let i: i32 = 5;
10+
let _c = kani::float::float_to_int_in_range::<i32, u8>(i);
11+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
error[E0277]: the trait bound `f32: std::convert::FloatToInt<bool>` is not satisfied
2+
invalid_int.rs:
3+
| let _c = kani::float::float_to_int_in_range::<f32, bool>(f);
4+
| ^^^ the trait `std::convert::FloatToInt<bool>` is not implemented for `f32`
5+
|
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
//! This test checks that Kani emits an error when
5+
//! `kani::float::float_to_int_in_range` is instantiated with a non-integer type
6+
7+
#[kani::proof]
8+
fn check_invalid_integer() {
9+
let f: f32 = kani::any();
10+
let _c = kani::float::float_to_int_in_range::<f32, bool>(f);
11+
}

0 commit comments

Comments
 (0)