Skip to content

Add panics_if precondition to express panic-freedom #4230

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion library/kani/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,4 @@
//! Here, the value stored in `a` is precomputed and remembered after the function
//! is called, even though the contents of `a` changed during the function execution.
//!
pub use super::{ensures, modifies, proof_for_contract, requires, stub_verified};
pub use super::{ensures, modifies, panics_if, proof_for_contract, requires, stub_verified};
24 changes: 23 additions & 1 deletion library/kani_macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,25 @@ pub fn requires(attr: TokenStream, item: TokenStream) -> TokenStream {
attr_impl::requires(attr, item)
}

/// Add a panic-freedom precondition to this function.
///
/// This is part of the function contract API, for more general information see
/// the [module-level documentation](../kani/contracts/index.html).
///
/// The contents of the attribute is a condition over the input values to the
/// annotated function. All Rust syntax is supported, even calling other
/// functions, but the computations must be side effect free, e.g. it cannot
/// perform I/O or use mutable memory.
///
/// Kani requires each function that uses a contract (this attribute or
/// [`requires`][macro@requires] or [`ensures`][macro@ensures]) to have at least one designated
/// [`proof_for_contract`][macro@proof_for_contract] harness for checking the
/// contract.
#[proc_macro_attribute]
pub fn panics_if(attr: TokenStream, item: TokenStream) -> TokenStream {
attr_impl::panics_if(attr, item)
}

/// Add a postcondition to this function.
///
/// This is part of the function contract API, for more general information see
Expand Down Expand Up @@ -453,7 +472,9 @@ mod sysroot {
mod contracts;
mod loop_contracts;

pub use contracts::{ensures, modifies, proof_for_contract, requires, stub_verified};
pub use contracts::{
ensures, modifies, panics_if, proof_for_contract, requires, stub_verified,
};
pub use loop_contracts::{loop_invariant, loop_modifies};

use super::*;
Expand Down Expand Up @@ -628,6 +649,7 @@ mod regular {
no_op!(unstable);
no_op!(unwind);
no_op!(requires);
no_op!(panics_if);
no_op!(ensures);
no_op!(modifies);
no_op!(proof_for_contract);
Expand Down
6 changes: 6 additions & 0 deletions library/kani_macros/src/sysroot/contracts/assert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ impl<'a> ContractConditionsHandler<'a> {
#(#body_stmts)*
})
}
ContractConditionsData::PanicsIf { attr } => {
quote!({
kani::assert(!(#attr), concat!("Panic: ", stringify!(#attr_copy)));
#(#body_stmts)*
})
}
ContractConditionsData::Ensures { attr } => {
let (remembers, ensures_clause) = build_ensures(attr);

Expand Down
6 changes: 6 additions & 0 deletions library/kani_macros/src/sysroot/contracts/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ impl<'a> ContractConditionsHandler<'a> {
#(#body_stmts)*
})
}
ContractConditionsData::PanicsIf { attr } => {
quote!({
kani::assume(!(#attr));
Copy link
Contributor

@remi-delmas-3000 remi-delmas-3000 Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we instead name that clause may_panic_if ? the current encoding checks that the function does not panic when the negation of the condition holds, not that it panics when the condition holds. The function may or may not panic when this condition does not hold.

Suggestion: add two clauses:

  • no_panic_if(P), specifies panic-freedom conditions (passes if function does not panic whenever the condition holds)

    • verified by checking that function does not panic under kani::assume(P);
    • or by checking that when function panics then !P held initially on inputs
  • panic_if(Q): specifies panic conditions (passes if function panics whenever the condition holds)

    • verified by checking that function panics under kani::assume(Q);
      • or by check that when function does not panics then !Q held initially on inputs
  • consistency check: assert!(!P || !Q); (are the conditions disjoint?)

  • completeness check : assert!(P || Q); (are we covering the whole input space?)

  • if both consistency and completeness hold we have a partition of the input domain

#(#body_stmts)*
})
}
ContractConditionsData::Ensures { attr } => {
let (remembers, ensures_clause) = build_ensures(attr);

Expand Down
9 changes: 6 additions & 3 deletions library/kani_macros/src/sysroot/contracts/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,21 @@ impl ContractFunctionState {

impl<'a> ContractConditionsHandler<'a> {
/// Initialize the handler. Constructs the required
/// [`ContractConditionsType`] depending on `is_requires`.
/// [`ContractConditionsType`] depending on `is_precondition`.
pub fn new(
is_requires: ContractConditionsType,
condition_kind: ContractConditionsType,
attr: TokenStream,
annotated_fn: &'a mut ItemFn,
attr_copy: TokenStream2,
) -> Result<Self, syn::Error> {
let mut output = TokenStream2::new();
let condition_type = match is_requires {
let condition_type = match condition_kind {
ContractConditionsType::Requires => {
ContractConditionsData::Requires { attr: syn::parse(attr)? }
}
ContractConditionsType::PanicsIf => {
ContractConditionsData::PanicsIf { attr: syn::parse(attr)? }
}
ContractConditionsType::Ensures => {
ContractConditionsData::Ensures { attr: syn::parse(attr)? }
}
Expand Down
28 changes: 19 additions & 9 deletions library/kani_macros/src/sysroot/contracts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@

//! Implementation of the function contracts code generation.
//!
//! The most exciting part is the handling of `requires` and `ensures`, the main
//! entry point to which is [`requires_ensures_main`]. Most of the code
//! The most exciting part is the handling of `requires`, `panics_if`, and `ensures`, the main
//! entry point to which is [`pre_post_main`]. Most of the code
//! generation for that is implemented on [`ContractConditionsHandler`] with
//! [`ContractFunctionState`] steering the code generation. The function state
//! implements a state machine in order to be able to handle multiple attributes
//! on the same function correctly.
//!
//! ## How the handling for `requires`, `modifies`, and `ensures` works.
//! ## How the handling for `requires`, `panics_if`, `modifies`, and `ensures` works.
//!
//! Our aim is to generate a "check" function that can be used to verify the
//! validity of the contract and a "replace" function that can be used as a
Expand Down Expand Up @@ -532,6 +532,10 @@ pub fn requires(attr: TokenStream, item: TokenStream) -> TokenStream {
contract_main(attr, item, ContractConditionsType::Requires)
}

pub fn panics_if(attr: TokenStream, item: TokenStream) -> TokenStream {
contract_main(attr, item, ContractConditionsType::PanicsIf)
}

pub fn ensures(attr: TokenStream, item: TokenStream) -> TokenStream {
contract_main(attr, item, ContractConditionsType::Ensures)
}
Expand Down Expand Up @@ -616,6 +620,7 @@ struct ContractConditionsHandler<'a> {
#[derive(Copy, Clone, Eq, PartialEq)]
enum ContractConditionsType {
Requires,
PanicsIf,
Ensures,
Modifies,
}
Expand All @@ -628,6 +633,10 @@ enum ContractConditionsData {
/// The contents of the attribute.
attr: Expr,
},
PanicsIf {
/// The contents of the attribute.
attr: Expr,
},
Ensures {
/// The contents of the attribute.
attr: ExprClosure,
Expand Down Expand Up @@ -658,22 +667,23 @@ impl<'a> ContractConditionsHandler<'a> {
}
}

/// The main meat of handling requires/ensures contracts.
/// The main meat of handling requires/panics_if/ensures contracts.
///
/// See the [module level documentation][self] for a description of how the code
/// generation works.
fn contract_main(
attr: TokenStream,
item: TokenStream,
is_requires: ContractConditionsType,
condition_kind: ContractConditionsType,
) -> TokenStream {
let attr_copy = TokenStream2::from(attr.clone());
let mut item_fn = parse_macro_input!(item as ItemFn);
let function_state = ContractFunctionState::from_attributes(&item_fn.attrs);
let handler = match ContractConditionsHandler::new(is_requires, attr, &mut item_fn, attr_copy) {
Ok(handler) => handler,
Err(e) => return e.into_compile_error().into(),
};
let handler =
match ContractConditionsHandler::new(condition_kind, attr, &mut item_fn, attr_copy) {
Ok(handler) => handler,
Err(e) => return e.into_compile_error().into(),
};

handler.dispatch_on(function_state).into()
}
10 changes: 10 additions & 0 deletions library/kani_macros/src/sysroot/contracts/replace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ impl<'a> ContractConditionsHandler<'a> {
#result
})
}
ContractConditionsData::PanicsIf { attr } => {
let Self { attr_copy, .. } = self;
let result = Ident::new(INTERNAL_RESULT_IDENT, Span::call_site());
quote!({
kani::assert(!(#attr), concat!("Panic: ", stringify!(#attr_copy)));
#(#before)*
#(#after)*
#result
})
}
ContractConditionsData::Ensures { attr } => {
let (remembers, ensures_clause) = build_ensures(attr);
let result = Ident::new(INTERNAL_RESULT_IDENT, Span::call_site());
Expand Down
13 changes: 13 additions & 0 deletions tests/kani/FunctionContracts/promoted_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,16 @@ fn check_static() {
fn check_mut_static() {
foo_mut_static(kani::any());
}

/// Add a contract using a mutable static.
#[kani::requires(&foo == unsafe { &FOO_MUT })]
#[kani::panics_if(foo.0 != 1)]
pub fn foo_mut_static_with_panics_if(foo: Foo) -> Foo {
assert!(foo.0 == 1);
foo
}

#[kani::proof_for_contract(foo_mut_static_with_panics_if)]
fn check_mut_static_with_panics_if() {
foo_mut_static_with_panics_if(kani::any());
}
Loading