Skip to content

vstd: add IteratorSpec for core::iter::Enumerate - #2904

Open
arxgy wants to merge 1 commit into
verus-lang:mainfrom
arxgy:vstd-enumerate
Open

arxgy wants to merge 1 commit into
verus-lang:mainfrom
arxgy:vstd-enumerate

Conversation

@arxgy

@arxgy arxgy commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

vstd/std_specs/iter.rs provides IteratorSpec implementations for iterator adapters such as Rev, but not for core::iter::Enumerate. A function that consumes an enumerate therefore fails to verify:

fn tag_indices(v: Vec<u32>) -> (out: Vec<(usize, u32)>)
    ensures out@ == v@.map(|k: int, x: u32| ((k as usize), x)),
{
    let mut out: Vec<(usize, u32)> = Vec::new();
    let it = v.into_iter();
    let en = it.enumerate();
    for (i, x) in en {
        out.push((i, x));
    }
    out                     // fails: `Enumerate` is not supported (Enumerate has no IteratorSpec)
}

Fix

This PR adds an IteratorSpec for core::iter::Enumerate. It contributes:

  • remaining(): the items the iterator will yield. It is the inner iterator's remaining items, each paired with the index it will be reached at.
  • enumerate_postcondition: a broadcast axiom giving enumerate_iter(r) == i and enumerate_count(r) == 0, the inner iterator and the counter the result starts from.
  • peek(): the inner iterator's guess, paired with the same index, so that a for-loop's (i, x) pattern is usable in its invariant.
  • an open decrease(), will_return_none() and obeys_prophetic_iter_laws(), each forwarding to the inner iterator.

After introducing Enumerate in this PR, we can reason the Enumerate iterator in Verus. And the problem code above can be proved as below:

 fn tag_indices(v: Vec<u32>) -> (out: Vec<(usize, u32)>)
     ensures out@ == v@.map(|k: int, x: u32| ((k as usize), x)),
 {
     let mut out: Vec<(usize, u32)> = Vec::new();
     let it = v.into_iter();
     let en = it.enumerate();
-    for (i, x) in en {
+    for (i, x) in iter: en
+        invariant out@ == iter.seq().take(iter.index()),
+    {
         out.push((i, x));
     }
     out
 }

A for-loop-over-enumerate regression test is included.

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

@parno
parno self-requested a review September 6, 2026 20:14

@parno parno left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the contribution! I added some comments/suggestions.

// Item `k` of what's left to yield is the inner iterator's item `k`, tagged with
// the index it will be reached at: `count` items have been consumed already.
#[verifier::prophetic]
open spec fn remaining(&self) -> Seq<(usize, I::Item)> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In our other iterators, we have marked remaining, will_return_none, and decrease as closed, uninterp functions, and provided the necessary knowledge via the *_postcondition conditions. See, for example, the way Map is handled. For consistency, can we update this iterator accordingly?

{
// `w.len() == iter.index()` bounds the loop index by `usize::MAX`,
// which is what relating it to `Enumerate`'s `usize` index needs.
assert(i == iter.index());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this assert necessary for the proof or is it illustrative? Either way, the comment is a bit unclear.

}

// The `(i, x)` pattern is usable in the invariant (via `peek`).
// `Enumerate`'s index is a `usize`, so relating it to the (unbounded) loop

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not sure this line or the next is needed; it seems more confusing than clarifying.

}

test_verify_one_file! {
#[test] enumerate_spec_level verus_code! {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This test seems tautological, since it's just repeating the relevant definitions in remaining_of_fresh_enumerate, and the second one seems more about collect's functionality. Let's cut the test.

} => Ok(())
}

test_verify_one_file! {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not convinced this one is useful either.

@parno

parno commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

I also notice that the Rust docs say:

The method does no guarding against overflows, so enumerating more than usize::MAX elements either produces the wrong result or panics.
We might need to adjust your specs to account for that.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants