Specification libraries: Seq, Set, ISet, Map, IMap
The Verus libraries contain types Seq<T>, Set<T>, ISet<T>,
Map<Key, Value>, and IMap<Key, Value>
for representing sequences, finite sets, potentially infinite sets,
finite maps, and potentially infinite maps in specifications.
In contrast to executable Rust collection datatypes in
std::collections,
the Seq, Set, ISet, Map, and IMap types
represent collections of arbitrary size.
For example, while the len() method of
std::collections::HashSet
returns a length of type usize,
which is bounded,
the len() methods of Seq, Set, and ISet each return
a length of type nat, which is unbounded.
Seq, Set, and Map objects are always finite.
ISet and IMap represent possibly-infinite sets and maps.
This allows specifications to talk about collections that
are larger than could be contained in the physical memory of a computer.
The possibly-infinite versions have one key benefit: You can construct them
without proving they’re finite. The finite versions have two key benefits.
First, they can be used in recursive types; e.g., an enum T can contain
fields of type Set<T> and Map<T, U> but not ISet<T> or IMap<T, U>.
Second, the verifier knows the finiteness property always holds, which can
prevent some SMT-time proof failure surprises. For instance, adding a new
element to a Set increases its len() by 1, but this doesn’t always hold
for an ISet since it might have infinite size.
Constructing and using Seq, Set, ISet, Map, and IMap
The seq!, set!, and map! macros construct values of type Seq, Set,
and Map with particular contents. The iset! and imap! macros construct
finite values of the possibly-infinite types ISet and IMap:
proof fn test_seq1() {
let s: Seq<int> = seq![0, 10, 20, 30, 40];
assert(s.len() == 5);
assert(s[2] == 20);
assert(s[3] == 30);
}
proof fn test_set1() {
let s: Set<int> = set![0, 10, 20, 30, 40];
assert(s.contains(20));
assert(s.contains(30));
assert(!s.contains(60));
let s: ISet<int> = iset![0, 10, 20, 30, 40];
assert(s.finite());
assert(s.contains(20));
assert(s.contains(30));
assert(!s.contains(60));
}
proof fn test_map1() {
let m: Map<int, int> = map![0 => 0, 10 => 100, 20 => 200, 30 => 300, 40 => 400];
assert(m.dom().contains(20));
assert(m.dom().contains(30));
assert(!m.dom().contains(60));
assert(m[20] == 200);
assert(m[30] == 300);
}
The macros above can only construct finite (literal) sequences, sets, and maps.
Sequences, potentially-infinite sets, and potentially-infinite maps can also
be constructed with Seq::new, ISet::new, and IMap::new.
A Set can only be constructed if it’s provably finite. There are several
ways to do this, including the following:
- For numeric types
T(e.g.,int,usize,i32,u64, etc.),Set::<T>::range(lo, hi)produces a set ofi: Tsuch thatlo <= i < hi. (See theFiniteRangetrait.) - For numeric types
Tother thanintandnat,Set::<T>::full().unwrap()produces a set of all values of typeT. (See theFiniteFulltrait.) - You can use one of the above techniques, then modify the
Setas desired usingSet::filterand/orSet::map. But be warned:Set::filteris fine, butSet::mapcan be problematic. This is because the meaning ofSet::mapis defined via the formulaself.map(f).contains(b) <==> exists|a: A| self.contains(a) && b == f(a). This use ofexistsgenerally makes proofs about the resulting set more difficult and less automated. Consider alternatives toSet::map, such asSet::map_byandset_build!, when possible. - You can construct a
Setusing theset_build!macro defined in the contributed library set_build.rs.
To construct a Map, construct its domain as a Set, then pass that as
the first parameter to Map::new.
proof fn test_seq2() {
let s: Seq<int> = Seq::new(5, |i: int| 10 * i);
assert(s.len() == 5);
assert(s[2] == 20);
assert(s[3] == 30);
}
proof fn test_set2() {
let s_finite: Set<int> = Set::range(0, 41).filter(|i: int| i % 10 == 0);
assert(s_finite.contains(20));
assert(s_finite.contains(30));
assert(!s_finite.contains(60));
let s: ISet<int> = ISet::new(|i: int| 0 <= i <= 40 && i % 10 == 0);
assert(s.contains(20));
assert(s.contains(30));
assert(!s.contains(60));
let s_infinite: ISet<int> = ISet::new(|i: int| i % 10 == 0);
assert(s_infinite.contains(20));
assert(s_infinite.contains(30));
assert(!s_infinite.contains(35));
}
proof fn test_map2() {
let m: IMap<int, int> = IMap::new(|i: int| 0 <= i <= 40 && i % 10 == 0, |i: int| 10 * i);
assert(m[20] == 200);
assert(m[30] == 300);
let m_infinite: IMap<int, int> = IMap::new(|i: int| i % 10 == 0, |i: int| 10 * i);
assert(m_infinite[20] == 200);
assert(m_infinite[30] == 300);
assert(m_infinite[90] == 900);
let m_finite: Map<int, int> = Map::new(Set::range(0, 41), |i: int| 10 * i);
assert(m_finite[20] == 200);
assert(m_finite[30] == 300);
}
Each Map<Key, Value> value has a domain of type Set<Key> given by .dom().
Likewise, each IMap<Key, Value> has a domain of type ISet<Key>.
In the test_map2 example above, each of m and m_finite has the finite
domain {0, 10, 20, 30, 40}
while m_infinite’s domain is the infinite set {..., -20, 10, 0, 10, 20, ...}.
Some constructors of Set<T> don’t necessarily produce a finite set; they
produce an Option<Set<T>> that’s only Some when the set is finite. Thus,
they’re only useful if you can prove finiteness (usually by recursion, using
the facts that ISet::empty() is finite and that ISet::insert preserves
finiteness). If you can’t (or don’t need to) prove that the set is finite,
just use an ISet instead. Examples of functions that produce an
Option<Set<T>> are:
Set::<T>::new(f)constructs a set of all members ofTsatisfying predicatef: spec_fn(T) -> bool.Set::<T>::full()constructs a set of all members ofT. IfThas traitFiniteFull, an ambient broadcast lemma is sufficient to know it’sSome.Set::<T>::complement(self)constructs a set of all members ofTnot inself.Set::<T>::new_from_iset(s)constructs a set of all members ofTin theISets.
For more operations, including sequence contenation (.add or +),
sequence update,
sequence subrange,
set union (.union or +),
set intersection (.intersect),
etc.,
see:
See also the API documentation.
Proving properties of Seq, Set, ISet, Map, and IMap
The SMT solver will prove some properties about Seq, Set, ISet, Map, and IMap automatically, as shown in the examples above. However, some other properties may require calling lemmas in the library or may require proofs by induction.
If two collections (Seq, Set, ISet, Map, or IMap) have the same elements,
Verus considers them to be equal.
This is known as equality via extensionality.
However, the SMT solver will in general not automatically recognize that
the two collections are equal
if the collections were constructed in different ways.
For example, the following 3 sequences are equal,
but calling check_eq fails:
proof fn check_eq(x: Seq<int>, y: Seq<int>)
requires
x == y,
{
}
proof fn test_eq_fail() {
let s1: Seq<int> = seq![0, 10, 20, 30, 40];
let s2: Seq<int> = seq![0, 10] + seq![20] + seq![30, 40];
let s3: Seq<int> = Seq::new(5, |i: int| 10 * i);
check_eq(s1, s2); // FAILS, even though s1 equals s2
check_eq(s1, s3); // FAILS, even though s1 equals s3
}
To convince the SMT solver that s1, s2, and s3 are equal,
we have to explicitly assert the equality via the extensional equality operator =~=,
rather than just the ordinary equality operator ==.
Using =~= forces the SMT solver
to check that all the elements of the collections are equal,
which it would not ordinarily do, so that the following succeeds:
proof fn check_eq_extensionally(x: Seq<int>, y: Seq<int>)
requires
x =~= y,
{
}
proof fn test_eq() {
let s1: Seq<int> = seq![0, 10, 20, 30, 40];
let s2: Seq<int> = seq![0, 10] + seq![20] + seq![30, 40];
let s3: Seq<int> = Seq::new(5, |i: int| 10 * i);
check_eq_extensionally(s1, s2); // succeeds
check_eq_extensionally(s1, s3); // succeeds
}
We can use assert(s1 =~= s2), for example, to prove that s1 equals s2
before calling the original check_eq:
proof fn check_eq(x: Seq<int>, y: Seq<int>)
requires
x == y,
{
}
proof fn test_eq2() {
let s1: Seq<int> = seq![0, 10, 20, 30, 40];
let s2: Seq<int> = seq![0, 10] + seq![20] + seq![30, 40];
let s3: Seq<int> = Seq::new(5, |i: int| 10 * i);
assert(s1 =~= s2);
assert(s1 =~= s3);
check_eq(s1, s2); // succeeds
check_eq(s1, s3); // succeeds
}
(Note that by default, Verus will automatically promote == to =~=
inside assert, ensures, and invariant,
so that, for example, assert(s1 == s2) actually means assert(s1 =~= s2).
See the Equality via extensionality section for more details.)
An ISet and a Set can never be equal because their types disagree;
should you find yourself needing to relate them, consider the Set::congruent
predicate, or convert the Set to an ISet with to_iset() before
checking for equality. (In non-library code, it is best practice to use
exclusively the finite or infinite variant, if feasible.)
Proofs about set cardinality (Set::len) and set finiteness (ISet::finite)
often require inductive proofs.
For example, the exact cardinality of the intersection of two sets
depends on which elements the two sets have in common.
If the two sets are disjoint,
the intersection’s cardinality will be 0,
but otherwise, the intersections’s cardinality will be some non-zero value.
Let’s try to prove that the intersection’s cardinality is no larger than
either of the two sets’ cardinalities.
Without loss of generality, we can just prove that
the intersection’s cardinality is no larger than the first set’s cardinality:
s1.intersect(s2).len() <= s1.len().
The proof (which is found in set_lib.rs)
is by induction on the size of the set s1.
In the induction step, we need to make s1 smaller,
which means we need to remove an element from it.
The two methods .choose and .remove allow us to choose
an arbitrary element from s1 and remove it:
let a = s1.choose();
... s1.remove(a) ...
Based on this, we expect an inductive proof to look something like the following,
where the inductive step removes s1.choose():
pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
ensures
s1.intersect(s2).len() <= s1.len(),
decreases
s1.len(),
{
if s1.is_empty() {
} else {
let a = s1.choose();
lemma_len_intersect(s1.remove(a), s2);
}
}
Unfortunately, Verus fails to verify this proof. Therefore, we’ll need to fill in the base case and induction case with some more detail. Before adding this detail to the code, let’s think about what a fully explicit proof might look like if we wrote it out by hand:
pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
ensures
s1.intersect(s2).len() <= s1.len(),
decreases
s1.len(),
{
if s1.is_empty() {
// s1 is the empty set.
// Therefore, s1.intersect(s2) is also empty.
// So both s1.len() and s1.intersect(s2).len() are 0,
// and 0 <= 0.
} else {
// s1 is not empty, so it has at least one element.
// Let a be an element from s1.
// Let s1' be the set s1 with the element a removed (i.e. s1' == s1 - {a}).
// Removing an element decreases the cardinality by 1, so s1'.len() == s1.len() - 1.
// By induction, s1'.intersect(s2).len() <= s1'.len(), so:
// (s1 - {a}).intersect(s2).len() <= s1'.len()
// (s1.intersect(s2) - {a}).len() <= s1'.len()
// (s1.intersect(s2) - {a}).len() <= s1.len() - 1
// case a in s1.intersect(s2):
// (s1.intersect(s2) - {a}).len() == s1.intersect(s2).len() - 1
// case a not in s1.intersect(s2):
// (s1.intersect(s2) - {a}).len() == s1.intersect(s2).len()
// In either case:
// s1.intersect(s2).len() <= (s1.intersect(s2) - {a}).len() + 1
// Putting all the inequalities together:
// s1.intersect(s2).len() <= (s1.intersect(s2) - {a}).len() + 1 <= (s1.len() - 1) + 1
// So:
// s1.intersect(s2).len() <= (s1.len() - 1) + 1
// s1.intersect(s2).len() <= s1.len()
}
}
For such a simple property, this is a surprisingly long proof! Fortunately, the SMT solver can automatically prove most of the steps written above. What it will not automatically prove, though, is any step requiring equality via extensionality, as discussed earlier. The two crucial steps requiring equality via extensionality are:
- “Therefore, s1.intersect(s2) is also empty.”
- Replacing
(s1 - {a}).intersect(s2)withs1.intersect(s2) - {a}
For these, we need to explicitly invoke =~=:
pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
ensures
s1.intersect(s2).len() <= s1.len(),
decreases
s1.len(),
{
if s1.is_empty() {
assert(s1.intersect(s2) =~= s1);
} else {
let a = s1.choose();
assert(s1.intersect(s2).remove(a) =~= s1.remove(a).intersect(s2));
lemma_len_intersect(s1.remove(a), s2);
}
}
With this, Verus and the SMT solver successfully complete the proof.
However, Verus and the SMT solver aren’t the only audience for this proof.
Anyone maintaining this code might want to know why we invoked =~=,
and we probably shouldn’t force them to work out the entire hand-written proof above
to rediscover this.
So although it’s not strictly necessary,
it’s probably polite to wrap the assertions in assert...by to indicate
the purpose of the =~=:
pub proof fn lemma_len_intersect<A>(s1: Set<A>, s2: Set<A>)
ensures
s1.intersect(s2).len() <= s1.len(),
decreases s1.len(),
{
if s1.is_empty() {
assert(s1.intersect(s2).len() == 0) by {
assert(s1.intersect(s2) =~= s1);
}
} else {
let a = s1.choose();
lemma_len_intersect(s1.remove(a), s2);
// by induction: s1.remove(a).intersect(s2).len() <= s1.remove(a).len()
assert(s1.intersect(s2).remove(a).len() <= s1.remove(a).len()) by {
assert(s1.intersect(s2).remove(a) =~= s1.remove(a).intersect(s2));
}
// simplifying ".remove(a).len()" yields s1.intersect(s2).len() <= s1.len())
}
}