Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6cca1ff
Add Kani verification harnesses for iterator adapter functions
Mar 26, 2026
e80e689
Address Copilot review feedback on PR #549
May 11, 2026
03161cf
Merge branch 'main' into challenge-16
feliperodri May 11, 2026
abe354e
Merge remote-tracking branch 'public/main' into pr-549-fixes
Aug 17, 2026
000221a
Address review feedback: real loop invariants and contract documentation
Aug 17, 2026
88407f0
Add u32::MAX UB-coverage companions for the u8 accessor harnesses
Aug 19, 2026
b9de6f6
Add drop-glue type coverage to map_windows and cloned harnesses
Aug 19, 2026
e1608bf
Add direct Zip state proof, side-effecting-source coverage, and chunk…
Aug 19, 2026
28f7c64
Apply pinned-toolchain rustfmt to the new harnesses
Aug 19, 2026
8a0f0b1
Pair every harness assume with a kani::cover non-vacuity witness
Aug 25, 2026
9a8654f
Merge remote-tracking branch 'public/main' into pr-549-fixes
Aug 26, 2026
d58d644
Merge remote-tracking branch 'public/main' into 16-response
kasimte Sep 14, 2026
bed6046
Address review: genuinely unbounded proofs where the pinned toolchain…
kasimte Sep 14, 2026
b756a50
Assert push's post-state effect in the arbitrary-state map_windows pr…
kasimte Sep 14, 2026
4065820
Merge upstream main into challenge-16 (toolchain nightly-2026-09-25, …
kasimte Oct 8, 2026
ef3caed
Challenge 16: drop documentation-only trait-impl requires contracts
kasimte Oct 9, 2026
5be20b8
Challenge 16: cap the u32::MAX UB-coverage companions to fit CI memory
kasimte Oct 9, 2026
240c860
Challenge 16: reduce symbolic slice bound 5000->256 on functional har…
kasimte Oct 10, 2026
b941f9d
Challenge 16: resolve two verify-std per-harness CBMC timeouts
kasimte Oct 11, 2026
2af3a78
Challenge 16: re-baseline adapter harnesses for the new Kani pin
kasimte Oct 13, 2026
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
40 changes: 40 additions & 0 deletions library/core/src/iter/adapters/array_chunks.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::array;
use crate::iter::adapters::SourceIter;
use crate::iter::{FusedIterator, InPlaceIterable, TrustedFused, TrustedRandomAccessNoCoerce};
#[cfg(kani)]
use crate::kani;
use crate::num::NonZero;
use crate::ops::{ControlFlow, NeverShortCircuit, Try};

Expand Down Expand Up @@ -224,6 +226,13 @@ where
let inner_len = self.iter.size();
let mut i = 0;
// Use a while loop because (0..len).step_by(N) doesn't optimize well.
// Safety argument: __iterator_get_unchecked is read-only for
// TrustedRandomAccessNoCoerce iterators, so iter.size() is preserved.
// Safety argument: i tracks the consumed element count and stays within
// inner_len. Combined with the while condition (inner_len - i >= N),
// this ensures i + local < inner_len = iter.size() for all accesses.
#[cfg_attr(kani, kani::loop_invariant(i <= inner_len))]
#[cfg_attr(kani, kani::loop_modifies(&i, &accum))]
while inner_len - i >= N {
let chunk = crate::array::from_fn(|local| {
// SAFETY: The method consumes the iterator and the loop condition ensures that
Expand Down Expand Up @@ -268,3 +277,34 @@ unsafe impl<I: InPlaceIterable + Iterator, const N: usize> InPlaceIterable for A
}
};
}

#[cfg(kani)]
#[unstable(feature = "kani", issue = "none")]
mod verify {
use super::*;

// next_back / next_back_remainder are omitted: goto-instrument --dfcc hangs for
// hours instrumenting the next_chunk/IntoIter machinery under -Z loop-contracts
// (kani#4967). The required __iterator_get_unchecked path is covered by the fold
// harness below; re-add if that tool hang is fixed.

// fold (TRANC specialized — uses __iterator_get_unchecked in a loop)
// Arbitrary-length: the fold while-loop carries a source-level loop
// contract (invariant i <= inner_len, explicit loop_modifies), so no
// unwind cap is needed; the backing slice length is symbolic.
#[kani::proof]
fn check_array_chunks_fold_n2_u8() {
const MAX_LEN: usize = 256;
let array: [u8; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let chunks = ArrayChunks::<_, 2>::new(slice.iter());
// Exercises TRANC fold path — proves absence of UB in get_unchecked loop.
// Scalar accumulator: loop_modifies targets must lower to scalar/slice
// assigns; a ZST accumulator crashes CBMC's l2 renaming.
Iterator::fold(chunks, 0u32, |acc, _| acc);
}

// Note: only the N=2 u8 fold harness is kept. The loop safety logic
// (i + local < inner_len) is identical for all N and T, so this single
// harness suffices to exercise it.
}
95 changes: 92 additions & 3 deletions library/core/src/iter/adapters/cloned.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use core::num::NonZero;

use safety::requires;

use crate::iter::adapters::zip::try_get_unchecked;
use crate::iter::adapters::{SourceIter, TrustedRandomAccess, TrustedRandomAccessNoCoerce};
use crate::iter::{FusedIterator, InPlaceIterable, TrustedLen};
Expand Down Expand Up @@ -65,7 +63,7 @@ where
self.it.map(T::clone).fold(init, f)
}

#[requires(idx < self.it.size_hint().0)]
// Precondition (verified by the mirrored kani::assume in verify::check_* below, not as a contract): idx < self.it.size_hint().0.
unsafe fn __iterator_get_unchecked(&mut self, idx: usize) -> T
where
Self: TrustedRandomAccessNoCoerce,
Expand Down Expand Up @@ -180,3 +178,94 @@ unsafe impl<I: InPlaceIterable> InPlaceIterable for Cloned<I> {
const EXPAND_BY: Option<NonZero<usize>> = I::EXPAND_BY;
const MERGE_BY: Option<NonZero<usize>> = I::MERGE_BY;
}

#[cfg(kani)]
#[unstable(feature = "kani", issue = "none")]
mod verify {
use super::*;

#[kani::proof]
fn check_cloned_get_unchecked_u8() {
const MAX_LEN: usize = 256;
let array: [u8; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Cloned::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let result = unsafe { iter.__iterator_get_unchecked(idx) };
assert_eq!(result, slice[idx]);
}

// A Clone type with real drop glue: get_unchecked on Cloned runs the element's
// Clone, and the returned value's Drop runs in the harness -- the drop-glue
// axis the Copy types (u8/char/tuple) and the ZST cannot exercise.
#[derive(Clone)]
struct DropToken(u8);
impl Drop for DropToken {
fn drop(&mut self) {}
}

#[kani::proof]
fn check_cloned_get_unchecked_droptoken() {
let raw: [u8; 4] = kani::any();
let items = [DropToken(raw[0]), DropToken(raw[1]), DropToken(raw[2]), DropToken(raw[3])];
let mut iter = Cloned::new(items.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let result = unsafe { iter.__iterator_get_unchecked(idx) };
assert_eq!(result.0, items[idx].0);
}

// Large-length UB-coverage companion. Capped at 100_000 (not u32::MAX) so the
// symbolic array fits the CI runner's memory; the unchecked access's safety is
// slice-length-independent, so a large-but-feasible length still exercises it.
#[kani::proof]
fn check_cloned_get_unchecked_u8_u32max() {
const MAX_LEN: usize = 100_000;
let array: [u8; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Cloned::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}

#[kani::proof]
fn check_cloned_get_unchecked_unit() {
const MAX_LEN: usize = isize::MAX as usize;
let array: [(); MAX_LEN] = [(); MAX_LEN];
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Cloned::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}

#[kani::proof]
fn check_cloned_get_unchecked_char() {
const MAX_LEN: usize = 50;
let array: [char; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Cloned::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}

#[kani::proof]
fn check_cloned_get_unchecked_tup() {
const MAX_LEN: usize = 50;
let array: [(char, u8); MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Cloned::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}
}
113 changes: 110 additions & 3 deletions library/core/src/iter/adapters/copied.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use safety::requires;

use crate::iter::adapters::zip::try_get_unchecked;
use crate::iter::adapters::{SourceIter, TrustedRandomAccess, TrustedRandomAccessNoCoerce};
use crate::iter::{FusedIterator, InPlaceIterable, TrustedLen};
Expand Down Expand Up @@ -102,7 +100,7 @@ where
self.it.advance_by(n)
}

#[requires(idx < self.it.size_hint().0)]
// Precondition (verified by the mirrored kani::assume in verify::check_* below, not as a contract): idx < self.it.size_hint().0.
unsafe fn __iterator_get_unchecked(&mut self, idx: usize) -> T
where
Self: TrustedRandomAccessNoCoerce,
Expand Down Expand Up @@ -284,3 +282,112 @@ unsafe impl<I: InPlaceIterable> InPlaceIterable for Copied<I> {
const EXPAND_BY: Option<NonZero<usize>> = I::EXPAND_BY;
const MERGE_BY: Option<NonZero<usize>> = I::MERGE_BY;
}

#[cfg(kani)]
#[unstable(feature = "kani", issue = "none")]
mod verify {
use super::*;

// Phase 0 spike: proof_for_contract doesn't work on trait impl methods,
// so we use #[kani::proof] with manual precondition via kani::assume.
#[kani::proof]
fn check_copied_get_unchecked_u8() {
const MAX_LEN: usize = 256;
let array: [u8; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Copied::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let result = unsafe { iter.__iterator_get_unchecked(idx) };
assert_eq!(result, slice[idx]);
}

// Large-length UB-coverage companion. Capped at 100_000 (not u32::MAX) so the
// symbolic array fits the CI runner's memory; the unchecked access's safety is
// slice-length-independent, so a large-but-feasible length still exercises it.
#[kani::proof]
fn check_copied_get_unchecked_u8_u32max() {
const MAX_LEN: usize = 100_000;
let array: [u8; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Copied::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}

#[kani::proof]
fn check_copied_get_unchecked_unit() {
const MAX_LEN: usize = isize::MAX as usize;
let array: [(); MAX_LEN] = [(); MAX_LEN];
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Copied::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}

// spec_next_chunk (specialized for slice::Iter, uses ptr::copy_nonoverlapping)
#[kani::proof]
fn check_spec_next_chunk_n2_u8() {
const MAX_LEN: usize = 256;
let array: [u8; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Copied::new(slice.iter());
let _ = iter.next_chunk::<2>();
}

#[kani::proof]
fn check_spec_next_chunk_n3_u8() {
const MAX_LEN: usize = 256;
let array: [u8; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Copied::new(slice.iter());
let _ = iter.next_chunk::<3>();
}

#[kani::proof]
fn check_spec_next_chunk_n2_unit() {
const MAX_LEN: usize = isize::MAX as usize;
let array: [(); MAX_LEN] = [(); MAX_LEN];
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Copied::new(slice.iter());
let _ = iter.next_chunk::<2>();
}

#[kani::proof]
fn check_copied_get_unchecked_char() {
const MAX_LEN: usize = 50;
let array: [char; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Copied::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}

#[kani::proof]
fn check_copied_get_unchecked_tup() {
const MAX_LEN: usize = 50;
let array: [(char, u8); MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Copied::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}

#[kani::proof]
fn check_spec_next_chunk_n2_char() {
const MAX_LEN: usize = 50;
let array: [char; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Copied::new(slice.iter());
let _ = iter.next_chunk::<2>();
}
}
75 changes: 72 additions & 3 deletions library/core/src/iter/adapters/enumerate.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use safety::requires;

use crate::iter::adapters::zip::try_get_unchecked;
use crate::iter::adapters::{SourceIter, TrustedRandomAccess, TrustedRandomAccessNoCoerce};
use crate::iter::{FusedIterator, InPlaceIterable, TrustedFused, TrustedLen};
Expand Down Expand Up @@ -164,7 +162,7 @@ where

#[rustc_inherit_overflow_checks]
#[inline]
#[requires(idx < self.iter.size_hint().0)]
// Precondition (verified by the mirrored kani::assume in verify::check_* below, not as a contract): idx < self.iter.size_hint().0.
#[cfg_attr(kani, kani::modifies(self))]
unsafe fn __iterator_get_unchecked(&mut self, idx: usize) -> <Self as Iterator>::Item
where
Expand Down Expand Up @@ -321,3 +319,74 @@ impl<I: Default> Default for Enumerate<I> {
Enumerate::new(Default::default())
}
}

#[cfg(kani)]
#[unstable(feature = "kani", issue = "none")]
mod verify {
use super::*;

#[kani::proof]
fn check_enumerate_get_unchecked_u8() {
const MAX_LEN: usize = 256;
let array: [u8; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Enumerate::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let (i, val) = unsafe { iter.__iterator_get_unchecked(idx) };
assert_eq!(i, idx);
assert_eq!(*val, slice[idx]);
}

// Large-length UB-coverage companion. Capped at 100_000 (not u32::MAX) so the
// symbolic array fits the CI runner's memory; the unchecked access's safety is
// slice-length-independent, so a large-but-feasible length still exercises it.
#[kani::proof]
fn check_enumerate_get_unchecked_u8_u32max() {
const MAX_LEN: usize = 100_000;
let array: [u8; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Enumerate::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}

#[kani::proof]
fn check_enumerate_get_unchecked_unit() {
const MAX_LEN: usize = isize::MAX as usize;
let array: [(); MAX_LEN] = [(); MAX_LEN];
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Enumerate::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}

#[kani::proof]
fn check_enumerate_get_unchecked_char() {
const MAX_LEN: usize = 50;
let array: [char; MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Enumerate::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}

#[kani::proof]
fn check_enumerate_get_unchecked_tup() {
const MAX_LEN: usize = 50;
let array: [(char, u8); MAX_LEN] = kani::any();
let slice = kani::slice::any_slice_of_array(&array);
let mut iter = Enumerate::new(slice.iter());
let idx: usize = kani::any();
kani::assume(idx < iter.size_hint().0);
kani::cover(true, "non-vacuity witness: the assumed input space is non-empty");
let _ = unsafe { iter.__iterator_get_unchecked(idx) };
}
}
Loading
Loading