Skip to content

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

Open
wants to merge 32 commits into
base: main
Choose a base branch
from

Conversation

thanhnguyen-aws
Copy link
Contributor

@thanhnguyen-aws thanhnguyen-aws commented Jun 6, 2025

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 form

for pat in expr {
    body
}

is transformed into a while loop:

let kaniiter = kani::KaniIntoIter::kani_into_iter(expr); \\ init_iter_stmt
let kaniiterlen = kani::KaniIter::len(&kaniiter); \\ init_len_stmt
if kaniiterlen > 0 {
    let mut kaniindex = 0; \\ init_index_stmt
    kani::assume (kani::KaniIter::assumption(&kaniiter)); \\ iter_assume_stmt
    let pat = kani::KaniIter::first(&kaniiter); \\ init_pat_stmt
    while kaniindex < kaniiterlen {
        kani::assume (kani::KaniIter::assumption(&kaniiter)); \\ iter_assume_stmt
        let pat = kani::KaniIter::indexing(&kaniiter, kaniindex); \\ pat_assign_stmt
        kaniindex = kaniindex + 1; \\ increase_iter_stmt
        body
    }
}

Note that

  1. expr's type is required to impl KaniIntoIter trait, which is the override impl of Rust's IntoIter (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().

  2. The init_index_stmt and init_pat_stmt support writing the loop-invariant properties that involve pat and kaniindex

  3. The iter_assume_stmt assumes some truths about allocation, so that the user does not need to specify them in the loop invariant

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.

@thanhnguyen-aws thanhnguyen-aws requested a review from a team as a code owner June 6, 2025 20:47
@github-actions github-actions bot added the Z-EndToEndBenchCI Tag a PR to run benchmark CI label Jun 6, 2025
@carolynzech carolynzech marked this pull request as draft June 10, 2025 02:09
@thanhnguyen-aws thanhnguyen-aws marked this pull request as ready for review June 11, 2025 19:15
Copy link
Contributor

@carolynzech carolynzech left a 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:

  1. 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.

  1. 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) )]

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"?

@zhassan-aws
Copy link
Contributor

Can you expand the PR description and/or the comments in library/kani_macros/src/sysroot/loop_contracts/mod.rs to explain the idea behind the implementation? It is hard to infer what this is doing just through reading the code.

Copy link

@rod-chapman rod-chapman left a 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).
Copy link

@rod-chapman rod-chapman Jun 19, 2025

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:

  1. The use and meaning of the "kaniindex" name.
  2. 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.
  3. 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.
  4. 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).

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.

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.

Copy link
Contributor Author

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.

  1. I updated the document (loop-contracts.md file), which includes the kaniindex.
  2. You can find the example in this file too.
  3. 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.
  4. 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 of a to be isize::MAX. However, until array theory is supported (we are working on it), the performance is unscalable w.r.t array size (slice length).
  5. You can find examples of function-contracts in tests/expected/function-contracts.

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.

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?

Copy link
Contributor

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?)

Copy link
Contributor Author

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

This one: #4143 (comment)

@github-actions github-actions bot added the Z-CompilerBenchCI Tag a PR to run benchmark CI label Jun 24, 2025
Comment on lines +171 to +172
1. `kaniindex` : the position (index) of the current iteration in the iterator.
2. `kaniiterlen` : the length of the iterator.
Copy link
Contributor

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?

Copy link
Contributor

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 = ...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. 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].
  2. 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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor

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.

Comment on lines 72 to 79
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) }
}
Copy link
Contributor

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
Copy link
Contributor

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).
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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!

@tautschnig
Copy link
Member

The PR description says "for loop for 4 types: array, slice, Iter, Vec, Range, StepBy, Map, Chain, Zip, Enumerate." -- that's 10 items enumerated here, not 4?

Copy link
Member

@tautschnig tautschnig left a 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!

thanhnguyen-aws and others added 3 commits July 25, 2025 09:35
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
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the name change?

Copy link
Contributor

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.
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the _xxxx suffix?

Copy link
Contributor Author

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();
Copy link
Contributor

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;
Copy link
Contributor

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) {
Copy link
Contributor

@zhassan-aws zhassan-aws Jul 25, 2025

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);
Copy link
Contributor

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?

thanhnguyen-aws and others added 7 commits July 28, 2025 08:59
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Z-CompilerBenchCI Tag a PR to run benchmark CI Z-EndToEndBenchCI Tag a PR to run benchmark CI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants