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

Support for inductively verifying contracts of recursive functions #2809

Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
291e6d0
Contract support for inductive verification of recursion
JustusAdam Oct 5, 2023
18d7271
Fix test cases, formatting
JustusAdam Oct 5, 2023
6ea4a1b
Update Documentation
JustusAdam Oct 5, 2023
92c595a
Formatting test cases
JustusAdam Oct 5, 2023
481e7b5
More whitespace
JustusAdam Oct 5, 2023
ad7c2de
This whitespace stuff is getting ridiculous
JustusAdam Oct 5, 2023
65a2a17
AAttach require any
JustusAdam Oct 5, 2023
e20eccf
Merge branch 'main' into inductive-recursion-for-function-contracts
JustusAdam Oct 8, 2023
a31176c
Fix names in test cases
JustusAdam Oct 8, 2023
ba9ea4e
Apply suggestions from code review
JustusAdam Oct 10, 2023
6902193
Suggestions from Zyad
JustusAdam Oct 10, 2023
0817544
Merge branch 'main' into inductive-recursion-for-function-contracts
JustusAdam Oct 10, 2023
832f92c
Better self detector
JustusAdam Oct 11, 2023
081f7d6
Unused name
JustusAdam Oct 11, 2023
5ebfac5
Fix import error, add fail on two test case
JustusAdam Oct 11, 2023
67795ba
fmt
JustusAdam Oct 11, 2023
6c58d37
Added test case for, and fixed self detector
JustusAdam Oct 11, 2023
8ad2ae0
Actually run the new test in regression
JustusAdam Oct 11, 2023
9cd5ed1
Expand test case to also fail on postcondition
JustusAdam Oct 11, 2023
dd45198
Explanation for test case
JustusAdam Oct 11, 2023
d2e3379
fmt
JustusAdam Oct 11, 2023
281e6c1
A few missing bits of documentation.
JustusAdam Oct 17, 2023
e651f6e
Apply suggestions from code review
JustusAdam Oct 17, 2023
3623c16
Merge branch 'main' into inductive-recursion-for-function-contracts
JustusAdam Oct 17, 2023
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
23 changes: 21 additions & 2 deletions library/kani/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@
//! one specification attribute is considered to "have a contract" and any
//! absent specification type defaults to its most general interpretation
//! (`true`). All functions with not a single specification attribute are
//! considered "not to have a contract" and are ineligible for use as the
//! target of a [`proof_for_contract`][macro@proof_for_contract] of
//! considered "not to have a contract" and are ineligible for use as the target
//! of a [`proof_for_contract`][macro@proof_for_contract] of
//! [`stub_verified`][macro@stub_verified] attribute.
//!
//! ## Contract Use Attributes Overview
Expand Down Expand Up @@ -170,4 +170,23 @@
//!
//! Unlike `proof_for_contract` multiple `stub_verified` attributes are allowed
//! on the same proof harness though they must target different functions.
//!
//! ## Inductive Verification
//!
//! Function contracts by default use inductive verification to efficiently
//! verify recursive functions. In inductive verification a recursive function
//! is executed once and every recursive call is instead uses the contract
JustusAdam marked this conversation as resolved.
Show resolved Hide resolved
//! replacement. In this way a great many recursive calls can be checked with a
JustusAdam marked this conversation as resolved.
Show resolved Hide resolved
//! single verification pass.
//!
//! The downside of inductive verification is that the return value of a
//! contracted function must implement `kani::Arbitrary`. Due to restrictions to
//! code generation in proc macros the contract macros cannot determine reliably
JustusAdam marked this conversation as resolved.
Show resolved Hide resolved
//! in all cases whether a given function with a contract is recursive. As a
//! result it conservatively sets up inductive verification for every function
//! and requires the `kani::Arbitrary` constraint for contract checks.
//!
//! If you feel strongly about this issue you can join the discussion on issue
//! [#2823](https://github.com/model-checking/kani/issues/2823) to enable
//! opt-out of inductive verification.
pub use super::{ensures, proof_for_contract, requires, stub_verified};
302 changes: 272 additions & 30 deletions library/kani_macros/src/sysroot/contracts.rs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions scripts/kani-regression.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ cargo test -p kani-compiler
cargo test -p kani-driver
cargo test -p kani_metadata
cargo test -p kani --lib # skip doc tests.
# Test the actual macros, skipping doc tests and enabling extra traits for "syn"
# so we can debug print AST
RUSTFLAGS=--cfg=kani_sysroot cargo test -p kani_macros --features syn/extra-traits --lib

# Declare testing suite information (suite and mode)
TESTS=(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
max.assertion\
assertion\
- Status: FAILURE\
- Description: "result == x"\
in function max
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
max.assertion\
assertion\
- Status: SUCCESS\
- Description: "result == x || result == y"\
in function max
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
div.arithmetic_overflow\
arithmetic_overflow\
- Status: SUCCESS\
- Description: "attempt to divide by zero"\
in function div
Expand Down
12 changes: 12 additions & 0 deletions tests/expected/function-contract/fail_on_two.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
assertion\
- Status: FAILURE\
- Description: "internal error: entered unreachable code: fail on two"

Failed Checks: internal error: entered unreachable code: fail on two


assertion\
- Status: FAILURE\
- Description: "result < 3"

Failed Checks: result < 3
44 changes: 44 additions & 0 deletions tests/expected/function-contract/fail_on_two.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// kani-flags: -Zfunction-contracts

JustusAdam marked this conversation as resolved.
Show resolved Hide resolved
//! The test cases try to ensure applying the hypothesis is only done in
//! inductive verification of a function call. E.g. on every second encounter of
//! the called function in its own call stack rather than globally in the
//! program.
//!
//! In each case we have a recursive function that is called and we expect that
//! the recursive call within the first call has the hypothesis applied. (That's
//! not actually tested here but separately.)
//!
//! Then we call the function again and we've set up the cases such that *if*
//! the actual body is used then that call with fail (once because of panic,
//! once because the postcondition is violated). If instead the hypothesis (e.g.
//! contract replacement) is used we'd expect the verification to succeed.

#[kani::ensures(result < 3)]
fn fail_on_two(i: i32) -> i32 {
match i {
0 => fail_on_two(i + 1),
1 => 2,
_ => unreachable!("fail on two"),
JustusAdam marked this conversation as resolved.
Show resolved Hide resolved
}
}

#[kani::proof_for_contract(fail_on_two)]
fn harness() {
let first = fail_on_two(0);
let _ = fail_on_two(first);
}

#[kani::ensures(result < 3)]
fn fail_on_two_in_postcondition(i: i32) -> i32 {
let j = i + 1;
if i < 2 { fail_on_two_in_postcondition(j) } else { j }
}

#[kani::proof_for_contract(fail_on_two_in_postcondition)]
fn harness2() {
let first = fail_on_two_in_postcondition(1);
let _ = fail_on_two_in_postcondition(first);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
gcd.assertion\
assertion\
- Status: FAILURE\
- Description: "result != 0 && x % result == 0 && y % result == 0"\
in function gcd
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
gcd.assertion\
assertion\
- Status: FAILURE\
- Description: "result != 0 && x % result == 1 && y % result == 0"\
in function gcd\
Expand Down
3 changes: 3 additions & 0 deletions tests/expected/function-contract/gcd_rec_code_fail.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Failed Checks: result != 0 && x % result == 0 && y % result == 0

VERIFICATION:- FAILED
24 changes: 24 additions & 0 deletions tests/expected/function-contract/gcd_rec_code_fail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// kani-flags: -Zfunction-contracts
type T = u8;

/// Euclid's algorithm for calculating the GCD of two numbers
#[kani::requires(x != 0 && y != 0)]
#[kani::ensures(result != 0 && x % result == 0 && y % result == 0)]
fn gcd(x: T, y: T) -> T {
let mut max = x;
let mut min = y;
if min > max {
let val = max;
max = min;
min = val;
}
let res = max % min;
if res == 0 { min } else { gcd(min, res + 1) }
}

#[kani::proof_for_contract(gcd)]
fn simple_harness() {
let _ = gcd(kani::any(), kani::any());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
assertion\
- Status: SUCCESS\
- Description: "x != 0 && y != 0"

assertion\
- Status: SUCCESS\
- Description: "result != 0 && x % result == 0 && y % result == 0"

VERIFICATION:- SUCCESSFUL
68 changes: 68 additions & 0 deletions tests/expected/function-contract/gcd_rec_comparison_pass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// kani-flags: -Zfunction-contracts
type T = u8;

/// Euclid's algorithm for calculating the GCD of two numbers
#[kani::requires(x != 0 && y != 0)]
#[kani::ensures(result != 0 && x % result == 0 && y % result == 0)]
fn gcd(x: T, y: T) -> T {
let mut max = x;
let mut min = y;
if min > max {
let val = max;
max = min;
min = val;
}

let res = max % min;
if res == 0 { min } else { gcd(min, res) }
}

struct Frac {
pub num: T,
pub den: T,
}

impl Frac {
// constructor
pub fn new(num: T, den: T) -> Self {
Frac { num, den }
}

/// Method to simplify fraction
/// For example, `Frac { num: 10, den: 15 }` gets simplified to
/// `Frac { num: 2, num: 3 }`
pub fn simplify(&self) -> Frac {
let gcd = gcd(self.num, self.den);
Frac::new(self.num / gcd, self.den / gcd)
}

pub fn check_equals(&self, f2: Frac) {
assert_eq!(self.num % f2.num, 0);
assert_eq!(self.den % f2.den, 0);
let gcd1 = self.num / f2.num;
let gcd2 = self.den / f2.den;
assert_eq!(gcd1, gcd2);
}
}

#[kani::proof_for_contract(gcd)]
#[kani::unwind(12)]
fn harness_as_check() {
gcd_harness()
}

fn gcd_harness() {
// Needed to avoid having `free` be removed as unused function. This is
// because DFCC contract enforcement assumes that a definition for `free`
// exists.
let _ = Box::new(9_usize);
let num: T = kani::any();
let den: T = kani::any();
kani::assume(num != 0);
kani::assume(den != 0);
let frac = Frac::new(num, den);
let simplified_frac = frac.simplify();
frac.check_equals(simplified_frac);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Failed Checks: result != 0 && x % result == 1 && y % result == 0

VERIFICATION:- FAILED
25 changes: 25 additions & 0 deletions tests/expected/function-contract/gcd_rec_contract_fail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// kani-flags: -Zfunction-contracts
type T = u8;

/// Euclid's algorithm for calculating the GCD of two numbers
#[kani::requires(x != 0 && y != 0)]
// Changed `0` to `1` in `x % result == 0` to mess with this contract
#[kani::ensures(result != 0 && x % result == 1 && y % result == 0)]
fn gcd(x: T, y: T) -> T {
let mut max = x;
let mut min = y;
if min > max {
let val = max;
max = min;
min = val;
}

let res = max % min;
if res == 0 { min } else { gcd(min, res) }
}
#[kani::proof_for_contract(gcd)]
fn simple_harness() {
let _ = gcd(kani::any(), kani::any());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Frac::check_equals.assertion\
- Status: SUCCESS\
- Description: "assertion failed: self.num % f2.num == 0"

Frac::check_equals.assertion\
- Status: SUCCESS\
- Description: "assertion failed: self.den % f2.den == 0"

Frac::check_equals.assertion\
- Status: SUCCESS\
- Description: "assertion failed: gcd1 == gcd2"

gcd.assertion\
- Status: SUCCESS\
- Description: "x != 0 && y != 0"

VERIFICATION:- SUCCESSFUL
68 changes: 68 additions & 0 deletions tests/expected/function-contract/gcd_rec_replacement_pass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// kani-flags: -Zfunction-contracts
type T = u8;

/// Euclid's algorithm for calculating the GCD of two numbers
#[kani::requires(x != 0 && y != 0)]
#[kani::ensures(result != 0 && x % result == 0 && y % result == 0)]
fn gcd(x: T, y: T) -> T {
let mut max = x;
let mut min = y;
if min > max {
let val = max;
max = min;
min = val;
}

let res = max % min;
if res == 0 { min } else { gcd(min, res) }
}

struct Frac {
pub num: T,
pub den: T,
}

impl Frac {
// constructor
pub fn new(num: T, den: T) -> Self {
Frac { num, den }
}

/// Method to simplify fraction
/// For example, `Frac { num: 10, den: 15 }` gets simplified to
/// `Frac { num: 2, num: 3 }`
pub fn simplify(&self) -> Frac {
let gcd = gcd(self.num, self.den);
Frac::new(self.num / gcd, self.den / gcd)
}

pub fn check_equals(&self, f2: Frac) {
assert_eq!(self.num % f2.num, 0);
assert_eq!(self.den % f2.den, 0);
let gcd1 = self.num / f2.num;
let gcd2 = self.den / f2.den;
assert_eq!(gcd1, gcd2);
}
}

#[kani::proof]
#[kani::stub_verified(gcd)]
fn gcd_as_replace() {
gcd_harness()
}

fn gcd_harness() {
// Needed to avoid having `free` be removed as unused function. This is
// because DFCC contract enforcement assumes that a definition for `free`
// exists.
let _ = Box::new(9_usize);
let num: T = kani::any();
let den: T = kani::any();
kani::assume(num != 0);
kani::assume(den != 0);
let frac = Frac::new(num, den);
let simplified_frac = frac.simplify();
frac.check_equals(simplified_frac);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
assertion\
- Status: SUCCESS\
- Description: "x != 0 && y != 0"

assertion\
- Status: SUCCESS\
- Description: "result != 0 && x % result == 0 && y % result == 0"

VERIFICATION:- SUCCESSFUL
25 changes: 25 additions & 0 deletions tests/expected/function-contract/gcd_rec_simple_pass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// kani-flags: -Zfunction-contracts
type T = u8;

/// Euclid's algorithm for calculating the GCD of two numbers
#[kani::requires(x != 0 && y != 0)]
#[kani::ensures(result != 0 && x % result == 0 && y % result == 0)]
fn gcd(x: T, y: T) -> T {
let mut max = x;
let mut min = y;
if min > max {
let val = max;
max = min;
min = val;
}

let res = max % min;
if res == 0 { min } else { gcd(min, res) }
}

#[kani::proof_for_contract(gcd)]
fn simple_harness() {
let _ = gcd(kani::any(), kani::any());
}
2 changes: 1 addition & 1 deletion tests/expected/function-contract/gcd_success.expected
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
gcd.assertion\
assertion\
- Status: SUCCESS\
- Description: "result != 0 && x % result == 0 && y % result == 0"\
in function gcd
Expand Down
Loading