-
Notifications
You must be signed in to change notification settings - Fork 121
Add loop-contracts support for for
loop
#4143
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
base: main
Are you sure you want to change the base?
Conversation
1dc30e6
to
a5d568b
Compare
6377782
to
1a8dceb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't reviewed the implementation yet, these are just some nits. I have two requests to make reviewing on my end easier:
- This is a big PR, and it'd be much easier to review if each new piece of functionality was separated into its own commit along with its tests. I'm envisioning something like:
commit 1: code to support iter & expected tests
commit 2: code to support array & expected tests
commit 3: code to support slice & expected tests
commit 4: code to support vec & expected tests
Can you do an interactive rebase to make it look like this? It doesn't need to be exactly this--I'd just appreciate if 1) changes are committed with their tests and 2) commits irrelevant to the review (e.g., fix clippy, merges) are kept to a minimum. FWIW, I have a pre-commit hook that runs clippy, our formatting check, and copyright check so that I can't even commit unless those checks pass; I'd recommend it.
- Can you add more comments? I added some places that I think need some. I'd like to see each function (unless it's super simple) have a doc comment that describes 1) what it does and 2) why it exists (i.e., why we need it in the first place)
let a: [u8; 10] = kani::any(); | ||
kani::assume(kani::forall!(|i in (0,10)| a[i] <= 10)); | ||
|
||
#[kani::loop_invariant( kaniindex <= 10 && sum <= (kaniindex as u32 * 10) )] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is "kaniindex" here? Is that supposed to be connected to the value of the loop counter "i"? If so, why not just write "i"?
Can you expand the PR description and/or the comments in |
2a22aaf
to
8e69667
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please address my documentation requests.
@@ -168,7 +168,7 @@ Kani automatically contracts (instead of unwinds) all loops in the functions tha | |||
|
|||
Loop contracts comes with the following limitations. | |||
|
|||
1. `while` loops and `loop` loops are supported. The other kinds of loops are not supported: [`while let` loops](https://doc.rust-lang.org/reference/expressions/loop-expr.html#predicate-pattern-loops), and [`for` loops](https://doc.rust-lang.org/reference/expressions/loop-expr.html#iterator-loops). | |||
1. `while` loops, `loop` loops are supported. `for` loops are supported for array, slice, Iter, Vec, Range, StepBy, Chain, Zip, Map, and Enumerate. The other kinds of loops are not supported: [`while let` loops](https://doc.rust-lang.org/reference/expressions/loop-expr.html#predicate-pattern-loops). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You also need to document:
- The use and meaning of the "kaniindex" name.
- How iter().enumerate() can be used to get the index number of a for loop so it can be used in an invariant. Please illustrate this with a full example.
- Please give a full example of a nested loop with invariants - for example, forming the sum of a two-dimemsional array of integers, and proving the upper-bound of the result.
- Give an example of how to an iterator is applied with a for loop to iterate over a dynamic slice (i.e. a slice whose bounds are not known at compile time which is passed into a function as a formal parameter).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you write the examples, add them as new tests cases obviously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also like to see test cases where functions are declared inside a module with full pre/post-conditions contracts, rather than the "assume/assert test harness" style. We'll be using the former style in Crypto work, so you need to convince me that it really does work that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, thank you very much for your suggestion.
- I updated the document (loop-contracts.md file), which includes the kaniindex.
- You can find the example in this file too.
- The nested loop for
for
loop needs one extra component: the loop_assign clause because sometimes CBMC cannot infer it for the inner loop. I am implementing it and expect to finish next week. I will add that example after both PRs are merged. - Currently, Kani does not support fully unbounded slices, but it supports slices of arbitrary length. Please take a look at the file for_loop_for_slice, we consider a slice (
s
) unbounded when we set the length ofa
to beisize::MAX
. However, until array theory is supported (we are working on it), the performance is unscalable w.r.t array size (slice length). - You can find examples of function-contracts in tests/expected/function-contracts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Iteration and quantification over unbounded slices will be required for crypto work (e.g. where we're computing a function over a potentially very large array of bytes, such as a user-supplied message.)
I do not see the need for a scaling problem. The proofs can be generated in a fashion that is entirely symbolic with respect to the bounds of the slice. This is how it works in tools like Frama-C and SPARK Pro.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I have a slice parameter - something like
fn f(a : &[u8]) -> i32
but I have a pre-condition something like
a.len <= 100
Why is that a problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thanhnguyen-aws can you add @rod-chapman's example program here as a test (or a fixme test if it doesn't work?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@carolynzech Which program are you referring to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one: #4143 (comment)
33b34b2
to
746311a
Compare
91423cf
to
184eb14
Compare
184eb14
to
f9a3c2f
Compare
1. `kaniindex` : the position (index) of the current iteration in the iterator. | ||
2. `kaniiterlen` : the length of the iterator. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the point of exposing these values? I don't find the example compelling; I feel like if people want the position of the iterator they should just call enumerate
and write assertions about i
instead of us exposing a value?
Same goes for the length of the iterator; what's an example of something we might want to express but couldn't without exposing this value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, what happens if someone tries to use kaniindex
or kaniiterlen
as local variables in their loop, like let kaniindex = ...
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- We don't want to change user code in order to write a loop invariant. kaniiter and kaniindex are useful variables for loop-invariant, so I think it is helpful to provide them to the users. In case the users use enumerate, the index of the Enumerate Iterator is also different from kaniindex as only kaniindex is updated at the end of the loop body as demonstrated in [nested_forloop_for_enumerate.rs].
- All extra variables are put together with a loop inside a block {} (by the proc macro) , so if the user defines variables with similar names, the loop-invariant always takes the ones that is generated by kani. I don't think user may name their variables that way unless they want to "hack" kani.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tend to agree with @carolynzech here. It is customary to modify code for verification purposes, e.g. through adding "ghost" variables that are only used in invariants. I would vote for not exposing those variables.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if we don't expose those variables, how are people going to express those loop invariants? Above, Thanh already explained how the approach with enumerate()
wouldn't work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I discussed with @thanhnguyen-aws offline and the reason enumerate
wouldn't work is because of how we implemented it, but it should be possible to fix the implementation to make it work (@thanhnguyen-aws please correct me if I misunderstood).
One concern I raised with @thanhnguyen-aws is that exposing those variables means that we're committing to providing them for all possible iterator types. While an "index" shouldn't be an issue, "len" might not be easy to provide for all iterators (e.g. the Chars iterator). So if we decide to drop one of those variables in the future, this would break user code.
library/kani_core/src/iter.rs
Outdated
unsafe { &*self.ptr.wrapping_add(i) } | ||
} | ||
fn first(&self) -> Self::Item { | ||
unsafe { &*self.ptr } | ||
} | ||
fn assumption(&self) -> bool { | ||
unsafe { mem::is_allocated(self.ptr as *const (), self.len) } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you write // SAFETY
comments above all of the unsafe code to explain why it's safe, c.f. the safety docs:
Whenever we write an unsafe function, it is idiomatic to write a comment starting with SAFETY and explaining what the caller needs to do to call the function safely. Likewise, whenever we perform an unsafe operation, it is idiomatic to write a comment starting with SAFETY to explain how the safety rules are upheld.
use core_path::ops::Range; | ||
use core_path::slice::Iter; | ||
|
||
pub trait KaniIter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put doc comments throughout this macro describing the purpose of each trait and the method in each trait? I'm having trouble telling if these implementations are correct when I'm not sure of the purpose of each thing.
@@ -168,7 +168,7 @@ Kani automatically contracts (instead of unwinds) all loops in the functions tha | |||
|
|||
Loop contracts comes with the following limitations. | |||
|
|||
1. `while` loops and `loop` loops are supported. The other kinds of loops are not supported: [`while let` loops](https://doc.rust-lang.org/reference/expressions/loop-expr.html#predicate-pattern-loops), and [`for` loops](https://doc.rust-lang.org/reference/expressions/loop-expr.html#iterator-loops). | |||
1. `while` loops, `loop` loops are supported. `for` loops are supported for array, slice, Iter, Vec, Range, StepBy, Chain, Zip, Map, and Enumerate. The other kinds of loops are not supported: [`while let` loops](https://doc.rust-lang.org/reference/expressions/loop-expr.html#predicate-pattern-loops). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thanhnguyen-aws can you add @rod-chapman's example program here as a test (or a fixme test if it doesn't work?)
@@ -214,6 +214,104 @@ the name of its `kani::any()` function, and add the map to the loop latch too. | |||
On the CBMC site, `goto-instrument` will extract the map and instrument the customized | |||
havocing functions for the modifies targets. | |||
|
|||
### For-loop rewrite |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noticed this RFC has a section on loop-decreases
that seems to imply that decreases will always be required to prove termination, but we've written plenty of loops without it. Can you update that text to reflect the current implementation? Can be in a separate PR if you prefer. Also the status field at the top should be changed to unstable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loop-decrease
is not supported yet. As mentioned Limitation
section of loop-contract.md : "Kani doesn't check if a loop will always terminate in proofs with loop contracts. So it could be that some properties are proved successfully with Kani but actually are unreachable due to the non-termination of some loops." .
@@ -214,6 +214,104 @@ the name of its `kani::any()` function, and add the map to the loop latch too. | |||
On the CBMC site, `goto-instrument` will extract the map and instrument the customized | |||
havocing functions for the modifies targets. | |||
|
|||
### For-loop rewrite | |||
|
|||
When there is loop-invariant for `for-loop`, the `loop_invariant` procedural macros |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really nice explanation; thank you for writing it!
Co-authored-by: Carolyn Zech <carolynzech@gmail.com>
The PR description says " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
Co-authored-by: Michael Tautschnig <mt@debian.org>
Co-authored-by: Michael Tautschnig <mt@debian.org>
Co-authored-by: Michael Tautschnig <mt@debian.org>
@@ -1,13 +1,15 @@ | |||
# Loop Contracts | |||
# Loop Invariants |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the name change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the renaming is something that was agreed on, it should be done in a separate PR. Adding support for "for" loops already requires so many changes, so we should avoid convoluting it with other unrelated changes.
It captures something that does not change about every step of the loop. | ||
|
||
It is worth revisiting the discussion about [bounded proof](../../tutorial-loop-unwinding.md#bounded-proof) and | ||
[loop unwinding](../../tutorial-loop-unwinding.md#loops-unwinding-and-bounds). In short, bounds on the number of times Kani unwinds loops also bound the size of inputs, | ||
and hence result in a bounded proof. | ||
Loop contracts are used to abstract out loops as non-loop blocks to avoid loop unwinding, and hence remove the bounds on the inputs. | ||
Loop-invariants are used to abstract out loops as non-loop blocks to avoid loop unwinding, and hence remove the bounds on the inputs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please replace all occurrences of "Loop-invariants" with "Loop invariants" (i.e. remove the dash).
|
||
Check 11: simple_loop_with_loop_contracts.loop_modifies.1 | ||
Check 11: simple_loop_with_loop_invariants.loop_assigns.1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was loop_modifies
renamed to loop_assigns
in this PR? I'm not clear where this is done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was an accident when I replace all loop_assigns to loop_modifies, I will correct it.
|
||
```Rust | ||
let a : [u8,10] = kani::any(); | ||
let kani_iter_xxxx = kani::kani_into_iter(a); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the _xxxx
suffix?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_xxxx is a hash code that is generated from the position of the loop. It is a technique to hide proc-macro generated variables from user. We would apply the same technique to kani_index and kani_iter_len if we decide to hide them.
```Rust | ||
let a : [u8,10] = kani::any(); | ||
let kani_iter_xxxx = kani::kani_into_iter(a); | ||
let kaniiterlen = kani_iter_xxxx.len(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use snake case for variable names, e.g. kani_iter_len
.
let kani_iter_xxxx = kani::kani_into_iter(a); | ||
let kaniiterlen = kani_iter_xxxx.len(); | ||
let mut i = kani_iter_xxxx.first() | ||
let mut kaniindex = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here: kaniindex
-> kani_index
.
let mut i = kani_iter_xxxx.first() | ||
let mut kaniindex = 0; | ||
#[kani::loop_invariant(...)] | ||
while (kaniindex < kaniiterlen) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no parentheses for while/if conditions in Rust
let mut kaniindex = 0; | ||
#[kani::loop_invariant(...)] | ||
while (kaniindex < kaniiterlen) { | ||
i = kani_iter_xxxx.indexing(kaniindex); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does the indexing
method do? Where is it defined?
Co-authored-by: Zyad Hassan <88045115+zhassan-aws@users.noreply.github.com>
Co-authored-by: Zyad Hassan <88045115+zhassan-aws@users.noreply.github.com>
Co-authored-by: Zyad Hassan <88045115+zhassan-aws@users.noreply.github.com>
Co-authored-by: Zyad Hassan <88045115+zhassan-aws@users.noreply.github.com>
Co-authored-by: Zyad Hassan <88045115+zhassan-aws@users.noreply.github.com>
Co-authored-by: Zyad Hassan <88045115+zhassan-aws@users.noreply.github.com>
Co-authored-by: Zyad Hassan <88045115+zhassan-aws@users.noreply.github.com>
This PR adds loop-contracts support for
for
loop for 10 types: array, slice, Iter, Vec, Range, StepBy, Map, Chain, Zip, Enumerate.Main ideas:
for
loop implementation:A
for
loop of the formis transformed into a
while
loop:Note that
expr's type is required to
impl KaniIntoIter
trait, which is the overrideimpl
of Rust'sIntoIter
(see library/kani_core/src/iter.rs).Reason:
a) Reduce stack-call
b) Avoid types that cannot be havoc effectively (user cannot state the type invariant in the loop invariant due to private fields)
c) There is no generic way to handle Rust's
into_iter()
.The
init_index_stmt
andinit_pat_stmt
support writing the loop-invariant properties that involve pat and kaniindexThe
iter_assume_stmt
assumes some truths about allocation, so that the user does not need to specify them in the loop invariantBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.