Skip to content

Commit e6067e9

Browse files
committed
Add a Kani function that checks if the range of a float is valid for conversion to int
1 parent 0ebf089 commit e6067e9

File tree

10 files changed

+174
-2
lines changed

10 files changed

+174
-2
lines changed

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

Lines changed: 51 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,54 @@ 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().clone()
344+
else {
345+
unreachable!()
346+
};
347+
let integral_ty = generic_args[1].expect_ty().kind().rigid().unwrap().clone();
348+
349+
let is_in_range = utils::codegen_in_range_expr(
350+
&float,
351+
float_ty,
352+
integral_ty,
353+
gcx.symbol_table.machine_model(),
354+
)
355+
.cast_to(Type::CInteger(CIntType::Bool));
356+
357+
let pe = unwrap_or_return_codegen_unimplemented_stmt!(
358+
gcx,
359+
gcx.codegen_place_stable(assign_to, loc)
360+
)
361+
.goto_expr;
362+
363+
Stmt::block(vec![pe.assign(is_in_range, loc), Stmt::goto(bb_label(target), loc)], loc)
364+
}
365+
}
366+
318367
/// Encodes __CPROVER_pointer_object(ptr)
319368
struct PointerObject;
320369
impl GotocHook for PointerObject {
@@ -663,6 +712,7 @@ pub fn fn_hooks() -> GotocHooks {
663712
(KaniHook::PointerOffset, Rc::new(PointerOffset)),
664713
(KaniHook::UntrackedDeref, Rc::new(UntrackedDeref)),
665714
(KaniHook::InitContracts, Rc::new(InitContracts)),
715+
(KaniHook::FloatToIntInRange, Rc::new(FloatToIntInRange)),
666716
];
667717
GotocHooks {
668718
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-compiler/src/kani_middle/transform/kani_intrinsics.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use crate::args::ExtraChecks;
1111
use crate::kani_middle::abi::LayoutOf;
1212
use crate::kani_middle::attributes::KaniAttributes;
13-
use crate::kani_middle::kani_functions::{KaniFunction, KaniIntrinsic, KaniModel};
13+
use crate::kani_middle::kani_functions::{KaniFunction, KaniHook, KaniIntrinsic, KaniModel};
1414
use crate::kani_middle::transform::body::{
1515
CheckType, InsertPosition, MutableBody, SourceInstruction,
1616
};
@@ -22,6 +22,8 @@ use crate::kani_middle::transform::check_values::{build_limits, ty_validity_per_
2222
use crate::kani_middle::transform::{TransformPass, TransformationType};
2323
use crate::kani_queries::QueryDb;
2424
use rustc_middle::ty::TyCtxt;
25+
use rustc_smir::rustc_internal;
26+
use stable_mir::CrateDef;
2527
use stable_mir::mir::mono::Instance;
2628
use stable_mir::mir::{
2729
AggregateKind, BasicBlock, BinOp, Body, ConstOperand, Local, Mutability, Operand, Place,
@@ -77,6 +79,16 @@ impl TransformPass for IntrinsicGeneratorPass {
7779
// This is handled in contracts pass for now.
7880
KaniIntrinsic::WriteAny | KaniIntrinsic::AnyModifies => (false, body),
7981
}
82+
} else if let Some(kani_hook) =
83+
attributes.fn_marker().and_then(|name| KaniHook::from_str(name.as_str()).ok())
84+
{
85+
match kani_hook {
86+
KaniHook::FloatToIntInRange => {
87+
IntrinsicGeneratorPass::check_float_to_int_in_range_valid_types(tcx, instance);
88+
(false, body)
89+
}
90+
_ => (false, body),
91+
}
8092
} else {
8193
(false, body)
8294
}
@@ -606,6 +618,24 @@ impl IntrinsicGeneratorPass {
606618
Place::from(RETURN_LOCAL),
607619
);
608620
}
621+
622+
fn check_float_to_int_in_range_valid_types(tcx: TyCtxt, instance: Instance) {
623+
let generic_args = instance.args().0;
624+
let arg0 = generic_args[0].expect_ty();
625+
if !arg0.kind().is_float() {
626+
let msg = format!(
627+
"Invalid type for first argument of `float_to_int_in_range` intrinsic. Expected a float type. Got `{arg0}`"
628+
);
629+
tcx.dcx().span_err(rustc_internal::internal(tcx, instance.def.span()), msg);
630+
}
631+
let arg1 = generic_args[1].expect_ty();
632+
if !arg1.kind().is_integral() {
633+
let msg = format!(
634+
"Invalid type for second argument of `float_to_int_in_range` intrinsic. Expected an integer type. Got `{arg1}`"
635+
);
636+
tcx.dcx().span_err(rustc_internal::internal(tcx, instance.def.span()), msg);
637+
}
638+
}
609639
}
610640

611641
/// Build an Rvalue `Some(val)`.

library/kani_core/src/float.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
#[macro_export]
7+
macro_rules! generate_float {
8+
($core:path) => {
9+
/// Returns whether the given float `value` satisfies the range
10+
/// condition of the `to_int_unchecked` methods, namely that the `value`
11+
/// after truncation is in range of the target `Int`
12+
///
13+
/// # Example:
14+
///
15+
/// ```no_run
16+
/// let f: f32 = 145.7;
17+
/// let fits_in_i8 = kani::float::float_to_int_in_range::<f32, i8>(f);
18+
/// // doesn't fit in `i8` because the value after truncation (`145.0`) is larger than `i8::MAX`
19+
/// assert!(!fits_in_i8);
20+
///
21+
/// let f: f64 = 1e6;
22+
/// let fits_in_u32 = kani::float::float_to_int_in_range::<f64, u32>(f);
23+
/// // fits in `u32` because the value after truncation (`1e6`) is smaller than `u32::MAX`
24+
/// assert!(fits_in_u32);
25+
/// ```
26+
#[kanitool::fn_marker = "FloatToIntInRangeHook"]
27+
pub fn float_to_int_in_range<Float, Int>(value: Float) -> bool {
28+
kani::kani_intrinsic()
29+
}
30+
};
31+
}

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
error: Invalid type for first argument of `float_to_int_in_range` intrinsic. Expected a float type. Got `i32`
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
error: Invalid type for second argument of `float_to_int_in_range` intrinsic. Expected an integer type. Got `bool`
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+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
//! This test checks that `kani::float::float_to_int_in_range` works as expected
5+
6+
#[kani::proof]
7+
fn check_float_to_int_in_range() {
8+
let f: f32 = 5.6;
9+
let fits_in_u16 = kani::float::float_to_int_in_range::<f32, u16>(f);
10+
assert!(fits_in_u16);
11+
let i: u16 = unsafe { f.to_int_unchecked() };
12+
assert_eq!(i, 5);
13+
14+
let f: f32 = 145.7;
15+
let fits_in_i8 = kani::float::float_to_int_in_range::<f32, i8>(f);
16+
// doesn't fit in `i8` because the value after truncation (`145.0`) is larger than `i8::MAX`
17+
assert!(!fits_in_i8);
18+
19+
let f: f64 = 1e6;
20+
let fits_in_u32 = kani::float::float_to_int_in_range::<f64, u32>(f);
21+
// fits in `u32` because the value after truncation (`1e6`) is smaller than `u32::MAX`
22+
assert!(fits_in_u32);
23+
let i: u32 = unsafe { f.to_int_unchecked() };
24+
assert_eq!(i, 1_000_000);
25+
}

0 commit comments

Comments
 (0)