Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Kani function that checks if the range of a float is valid for conversion to int #3742

Merged
merged 7 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion kani-compiler/src/codegen_cprover_gotoc/overrides/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
//! It would be too nasty if we spread around these sort of undocumented hooks in place, so
//! this module addresses this issue.

use crate::codegen_cprover_gotoc::GotocCtx;
use crate::codegen_cprover_gotoc::codegen::{PropertyClass, bb_label};
use crate::codegen_cprover_gotoc::{GotocCtx, utils};
use crate::kani_middle::attributes;
use crate::kani_middle::kani_functions::{KaniFunction, KaniHook};
use crate::unwrap_or_return_codegen_unimplemented_stmt;
Expand All @@ -19,6 +19,7 @@ use rustc_middle::ty::TyCtxt;
use rustc_smir::rustc_internal;
use stable_mir::mir::mono::Instance;
use stable_mir::mir::{BasicBlockIdx, Place};
use stable_mir::ty::RigidTy;
use stable_mir::{CrateDef, ty::Span};
use std::collections::HashMap;
use std::rc::Rc;
Expand Down Expand Up @@ -315,6 +316,53 @@ impl GotocHook for IsAllocated {
}
}

/// This is the hook for the `kani::float::float_to_int_in_range` intrinsic
/// TODO: This should be replaced by a Rust function instead so that it's
/// independent of the backend
struct FloatToIntInRange;
impl GotocHook for FloatToIntInRange {
fn hook_applies(&self, _tcx: TyCtxt, _instance: Instance) -> bool {
unreachable!("{UNEXPECTED_CALL}")
}

fn handle(
&self,
gcx: &mut GotocCtx,
instance: Instance,
mut fargs: Vec<Expr>,
assign_to: &Place,
target: Option<BasicBlockIdx>,
span: Span,
) -> Stmt {
assert_eq!(fargs.len(), 1);
let float = fargs.remove(0);
let target = target.unwrap();
let loc = gcx.codegen_span_stable(span);

let generic_args = instance.args().0;
let RigidTy::Float(float_ty) = *generic_args[0].expect_ty().kind().rigid().unwrap() else {
unreachable!()
};
let integral_ty = generic_args[1].expect_ty().kind().rigid().unwrap().clone();
celinval marked this conversation as resolved.
Show resolved Hide resolved

let is_in_range = utils::codegen_in_range_expr(
&float,
float_ty,
integral_ty,
gcx.symbol_table.machine_model(),
)
.cast_to(Type::CInteger(CIntType::Bool));

let pe = unwrap_or_return_codegen_unimplemented_stmt!(
zhassan-aws marked this conversation as resolved.
Show resolved Hide resolved
gcx,
gcx.codegen_place_stable(assign_to, loc)
)
.goto_expr;

Stmt::block(vec![pe.assign(is_in_range, loc), Stmt::goto(bb_label(target), loc)], loc)
}
}

/// Encodes __CPROVER_pointer_object(ptr)
struct PointerObject;
impl GotocHook for PointerObject {
Expand Down Expand Up @@ -663,6 +711,7 @@ pub fn fn_hooks() -> GotocHooks {
(KaniHook::PointerOffset, Rc::new(PointerOffset)),
(KaniHook::UntrackedDeref, Rc::new(UntrackedDeref)),
(KaniHook::InitContracts, Rc::new(InitContracts)),
(KaniHook::FloatToIntInRange, Rc::new(FloatToIntInRange)),
];
GotocHooks {
kani_lib_hooks: HashMap::from(kani_lib_hooks),
Expand Down
3 changes: 3 additions & 0 deletions kani-compiler/src/kani_middle/kani_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ pub enum KaniHook {
Check,
#[strum(serialize = "CoverHook")]
Cover,
// TODO: this is temporarily implemented as a hook, but should be implemented as an intrinsic
#[strum(serialize = "FloatToIntInRangeHook")]
FloatToIntInRange,
#[strum(serialize = "InitContractsHook")]
InitContracts,
#[strum(serialize = "IsAllocatedHook")]
Expand Down
2 changes: 2 additions & 0 deletions kani_metadata/src/unstable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ pub enum UnstableFeature {
UnstableOptions,
/// The list subcommand [RFC 13](https://model-checking.github.io/kani/rfc/rfcs/0013-list.html)
List,
/// Kani APIs related to floating-point operations (e.g. `float_to_int_in_range`)
FloatLib,
}

impl UnstableFeature {
Expand Down
1 change: 1 addition & 0 deletions library/kani/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#![feature(ptr_metadata)]
#![feature(f16)]
#![feature(f128)]
#![feature(convert_float_to_int)]

// Allow us to use `kani::` to access crate features.
extern crate self as kani;
Expand Down
43 changes: 43 additions & 0 deletions library/kani_core/src/float.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! This module contains functions useful for float-related checks

#[allow(clippy::crate_in_macro_def)]
#[macro_export]
macro_rules! generate_float {
($core:path) => {
use super::kani_intrinsic;
use core::convert::FloatToInt;
/// Returns whether the given float `value` satisfies the range
/// condition of the `to_int_unchecked` methods, namely that the `value`
/// after truncation is in range of the target `Int`
///
/// # Example:
///
/// ```no_run
/// let f: f32 = 145.7;
/// let fits_in_i8 = kani::float::float_to_int_in_range::<f32, i8>(f);
/// // doesn't fit in `i8` because the value after truncation (`145.0`) is larger than `i8::MAX`
/// assert!(!fits_in_i8);
///
/// let f: f64 = 1e6;
/// let fits_in_u32 = kani::float::float_to_int_in_range::<f64, u32>(f);
/// // fits in `u32` because the value after truncation (`1e6`) is smaller than `u32::MAX`
/// assert!(fits_in_u32);
/// ```
#[crate::kani::unstable_feature(
feature = "float-lib",
issue = "none",
reason = "experimental floating-point API"
)]
#[kanitool::fn_marker = "FloatToIntInRangeHook"]
#[inline(never)]
pub fn float_to_int_in_range<Float, Int>(value: Float) -> bool
where
Float: FloatToInt<Int>,
{
kani_intrinsic()
}
};
}
9 changes: 9 additions & 0 deletions library/kani_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#![feature(f128)]

mod arbitrary;
mod float;
mod mem;
mod mem_init;
mod models;
Expand All @@ -45,6 +46,10 @@ macro_rules! kani_lib {
kani_core::generate_arbitrary!(core);
kani_core::generate_models!();

pub mod float {
kani_core::generate_float!(core);
}

pub mod mem {
kani_core::kani_mem!(core);
}
Expand All @@ -61,6 +66,10 @@ macro_rules! kani_lib {
kani_core::generate_arbitrary!(std);
kani_core::generate_models!();

pub mod float {
kani_core::generate_float!(std);
}

pub mod mem {
kani_core::kani_mem!(std);
}
Expand Down
5 changes: 5 additions & 0 deletions tests/expected/float-to-int-in-range/invalid_float.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error[E0277]: the trait bound `i32: std::convert::FloatToInt<u8>` is not satisfied
invalid_float.rs:
| let _c = kani::float::float_to_int_in_range::<i32, u8>(i);
| ^^^ the trait `std::convert::FloatToInt<u8>` is not implemented for `i32`
|
11 changes: 11 additions & 0 deletions tests/expected/float-to-int-in-range/invalid_float.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! This test checks that Kani emits an error when
//! `kani::float::float_to_int_in_range` is instantiated with a non-float type
#[kani::proof]
fn check_invalid_float() {
let i: i32 = 5;
let _c = kani::float::float_to_int_in_range::<i32, u8>(i);
}
5 changes: 5 additions & 0 deletions tests/expected/float-to-int-in-range/invalid_int.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error[E0277]: the trait bound `f32: std::convert::FloatToInt<bool>` is not satisfied
invalid_int.rs:
| let _c = kani::float::float_to_int_in_range::<f32, bool>(f);
| ^^^ the trait `std::convert::FloatToInt<bool>` is not implemented for `f32`
|
11 changes: 11 additions & 0 deletions tests/expected/float-to-int-in-range/invalid_int.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! This test checks that Kani emits an error when
//! `kani::float::float_to_int_in_range` is instantiated with a non-integer type
#[kani::proof]
fn check_invalid_integer() {
let f: f32 = kani::any();
let _c = kani::float::float_to_int_in_range::<f32, bool>(f);
}
26 changes: 26 additions & 0 deletions tests/kani/FloatToIntInRange/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// kani-flags: -Zfloat-lib

//! This test checks that `kani::float::float_to_int_in_range` works as expected

#[kani::proof]
fn check_float_to_int_in_range() {
let f: f32 = 5.6;
let fits_in_u16 = kani::float::float_to_int_in_range::<f32, u16>(f);
assert!(fits_in_u16);
let i: u16 = unsafe { f.to_int_unchecked() };
assert_eq!(i, 5);

let f: f32 = 145.7;
let fits_in_i8 = kani::float::float_to_int_in_range::<f32, i8>(f);
// doesn't fit in `i8` because the value after truncation (`145.0`) is larger than `i8::MAX`
assert!(!fits_in_i8);

let f: f64 = 1e6;
let fits_in_u32 = kani::float::float_to_int_in_range::<f64, u32>(f);
// fits in `u32` because the value after truncation (`1e6`) is smaller than `u32::MAX`
assert!(fits_in_u32);
let i: u32 = unsafe { f.to_int_unchecked() };
assert_eq!(i, 1_000_000);
}
Loading