Skip to content

Commit f5efac0

Browse files
authored
vstd: fix unsound RangeInclusive::end_bound spec for exhausted ranges (#2687)
1 parent a9fc68c commit f5efac0

2 files changed

Lines changed: 126 additions & 3 deletions

File tree

source/rust_verify_test/tests/slices.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,98 @@ test_verify_one_file! {
893893
} => Ok(())
894894
}
895895

896+
// verus-lang/verus#2674: a fresh range's end bound is still Included, but an
897+
// exhausted one is not - both directions in one file (one expected failure).
898+
test_verify_one_file! {
899+
#[test] test_range_inclusive_end_bound_exhausted verus_code! {
900+
use vstd::prelude::*;
901+
use std::ops::{Bound, RangeBounds};
902+
903+
fn fresh() {
904+
let r = 1u8..=5u8;
905+
let end_is_included = match r.end_bound() {
906+
Bound::Included(_) => true,
907+
_ => false,
908+
};
909+
assert(end_is_included);
910+
}
911+
912+
fn exhausted() {
913+
let mut r = 1u8..=1u8;
914+
let _ = r.next();
915+
let end_is_included = match r.end_bound() {
916+
Bound::Included(_) => true,
917+
_ => false,
918+
};
919+
assert(end_is_included); // FAILS
920+
}
921+
} => Err(err) => assert_one_fails(err)
922+
}
923+
924+
// Same fact one level down: `slice_range_end` (used by `copy_within`) treats
925+
// an exhausted range's end as excluded, not included.
926+
test_verify_one_file! {
927+
#[test] test_slice_range_end_treats_exhausted_range_as_excluded verus_code! {
928+
use std::ops::RangeInclusive;
929+
use vstd::prelude::*;
930+
use vstd::std_specs::range::{slice_range_end, RangeInclusiveView};
931+
932+
proof fn correct(r: RangeInclusive<usize>)
933+
requires
934+
r@ == (RangeInclusiveView { start: 2, end: 2, exhausted: true }),
935+
{
936+
assert(slice_range_end(&r, 5) == 2);
937+
}
938+
939+
proof fn rejects_included(r: RangeInclusive<usize>)
940+
requires
941+
r@ == (RangeInclusiveView { start: 2, end: 2, exhausted: true }),
942+
{
943+
assert(slice_range_end(&r, 5) == 3); // FAILS
944+
}
945+
} => Err(err) => assert_one_fails(err)
946+
}
947+
948+
// `is_empty()`: not empty fresh, empty once exhausted - even though
949+
// `start <= end` alone (1 <= 1) would wrongly say otherwise.
950+
test_verify_one_file! {
951+
#[test] test_range_inclusive_is_empty verus_code! {
952+
use vstd::prelude::*;
953+
954+
fn fresh_is_not_empty() {
955+
let r = 1u8..=5u8;
956+
let empty = r.is_empty();
957+
assert(!empty);
958+
}
959+
960+
fn exhausted_is_empty() {
961+
let mut r = 1u8..=1u8;
962+
let _ = r.next();
963+
let empty = r.is_empty();
964+
assert(empty);
965+
assert(!empty); // FAILS: exhausted despite start <= end
966+
}
967+
} => Err(err) => assert_one_fails(err)
968+
}
969+
970+
test_verify_one_file! {
971+
#[test] test_spec_range_inclusive_is_empty_inverted_range verus_code! {
972+
use std::ops::RangeInclusive;
973+
use vstd::prelude::*;
974+
use vstd::std_specs::range::{spec_range_inclusive_is_empty, RangeInclusiveView};
975+
976+
// start > end (never valid, never exhausted) is empty too - not
977+
// exercised by test_range_inclusive_is_empty above, which only
978+
// reaches emptiness via a real .next() call.
979+
proof fn test_inverted(r: RangeInclusive<u8>)
980+
requires
981+
r@ == (RangeInclusiveView { start: 5u8, end: 1u8, exhausted: false }),
982+
{
983+
assert(spec_range_inclusive_is_empty(&r));
984+
}
985+
} => Ok(())
986+
}
987+
896988
test_verify_one_file! {
897989
#[test] test_split_at_checked verus_code! {
898990
use vstd::prelude::*;

source/vstd/std_specs/range.rs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,22 @@ pub assume_specification<Idx: PartialOrd<Idx>, U>[ RangeInclusive::<Idx>::contai
8888
== r.contains_spec(i),
8989
;
9090

91+
// A range is empty once its iterator is exhausted, or if it was never valid
92+
// to begin with (start > end).
93+
pub open spec fn spec_range_inclusive_is_empty<Idx: PartialOrd<Idx>>(
94+
r: &RangeInclusive<Idx>,
95+
) -> bool {
96+
!r@.start.is_le(&r@.end) || r@.exhausted
97+
}
98+
99+
pub assume_specification<Idx: PartialOrd<Idx>>[ RangeInclusive::<Idx>::is_empty ](
100+
r: &RangeInclusive<Idx>,
101+
) -> (res: bool) where Idx: PartialOrd<Idx>
102+
ensures
103+
<Idx as PartialOrdSpec<Idx>>::obeys_partial_cmp_spec() ==> res
104+
== spec_range_inclusive_is_empty(r),
105+
;
106+
91107
pub assume_specification<Idx>[ RangeInclusive::<Idx>::new ](start: Idx, end: Idx) -> (ret:
92108
core::ops::RangeInclusive<Idx>)
93109
ensures
@@ -270,11 +286,22 @@ pub assume_specification<'s, T>[ <RangeInclusive<T> as RangeBounds<T>>::start_bo
270286
spec_bound(result) == SpecBound::Included(&range@.start),
271287
;
272288

289+
// Shared with `RangeBoundsSpecImpl::spec_end_bound` below, so the two can't
290+
// drift apart: `end_bound()` returns `Included` while the range is not
291+
// exhausted and `Excluded` after it is exhausted.
292+
pub open spec fn spec_range_inclusive_end_bound<T>(r: &RangeInclusive<T>) -> SpecBound<&T> {
293+
if r@.exhausted {
294+
SpecBound::Excluded(&r@.end)
295+
} else {
296+
SpecBound::Included(&r@.end)
297+
}
298+
}
299+
273300
pub assume_specification<'s, T>[ <RangeInclusive<T> as RangeBounds<T>>::end_bound ](
274301
range: &'s RangeInclusive<T>,
275302
) -> (result: Bound<&'s T>)
276303
ensures
277-
spec_bound(result) == SpecBound::Included(&range@.end),
304+
spec_bound(result) == spec_range_inclusive_end_bound(range),
278305
;
279306

280307
pub assume_specification<'s, T>[ <RangeToInclusive<T> as RangeBounds<T>>::start_bound ](
@@ -371,7 +398,7 @@ impl<T> RangeBoundsSpecImpl<T> for RangeInclusive<T> {
371398
}
372399

373400
open spec fn spec_end_bound(&self) -> SpecBound<&T> {
374-
SpecBound::Included(&self@.end)
401+
spec_range_inclusive_end_bound(self)
375402
}
376403
}
377404

@@ -449,7 +476,11 @@ impl<T> RangeBoundsSpecImpl<T> for RangeInclusive<&T> {
449476
}
450477

451478
open spec fn spec_end_bound(&self) -> SpecBound<&T> {
452-
SpecBound::Included(self@.end)
479+
if self@.exhausted {
480+
SpecBound::Excluded(self@.end)
481+
} else {
482+
SpecBound::Included(self@.end)
483+
}
453484
}
454485
}
455486

0 commit comments

Comments
 (0)