Equality via extensionality

The specification libraries section introduced the extensional equality operator =~= to check equivalence for collection types like Seq, Set, and Map. Extensional equality proves that two collections are equal by proving that the collections contain equal elements. This can be used, for example, to prove that the sequences s1, s2, and s3 are equal because they have the same elements (0, 10, 20, 30, 40), even though this sequences were constructed in different ways:

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 }

Assertions like assert(s1 =~= s2) are common for proving equality via extensionality. Note that by default, Verus promotes == to =~= inside assert, ensures, and invariant, so that, for example, assert(s1 == s2) actually means assert(s1 =~= s2). This is convenient in many cases where extensional equality is merely a minor step in a larger proof, and you don’t want to clutter the proof with low-level details about equality. For proofs where extensional equality is the key step, you may want to explicitly write assert(s1 =~= s2) for documentation’s sake.

(If you don’t want Verus to auto-promote == to =~=, perhaps because you want to see exactly where extensional equality is really needed, the #[verifier::auto_ext_equal(...)] and #![verifier::auto_ext_equal(...)] attributes can override Verus’s default behavior. See ext_equal.rs for examples.)

Extensional equality for structs and enums

Suppose that a struct or enum datatype has a field containing Seq, Set, and Map, and suppose that we’d like to prove that two values of the datatype are equal. We could do this by using =~= on each field individually:

struct Foo { a: Seq<int>, b: Set<int>, } proof fn ext_equal_struct() { let f1 = Foo { a: seq![1, 2, 3], b: set!{4, 5, 6} }; let f2 = Foo { a: seq![1, 2].push(3), b: set!{5, 6}.insert(4) }; // assert(f1 == f2); // FAILS -- need to use =~= first assert(f1.a =~= f2.a); // succeeds assert(f1.b =~= f2.b); // succeeds assert(f1 == f2); // succeeds, now that we've used =~= on .a and .b }

However, it’s rather painful to use =~= on each field every time to check for equivalence. To help with this, Verus supports the #[verifier::ext_equal] attribute to mark datatypes that need extensionality on Seq, Set, Map, Multiset, spec_fn fields or fields of other #[verifier::ext_equal] datatypes. For example:

#[verifier::ext_equal] // necessary for invoking =~= on the struct struct Foo { a: Seq<int>, b: Set<int>, } proof fn ext_equal_struct() { let f1 = Foo { a: seq![1, 2, 3], b: set!{4, 5, 6} }; let f2 = Foo { a: seq![1, 2].push(3), b: set!{5, 6}.insert(4) }; assert(f1 =~= f2); // succeeds }

(Note: adding #[verifier::ext_equal] does not change the meaning of ==; it just makes it more convenient to use =~= to prove == on datatypes.)

Deep extensional equality

Collection datatypes like sequences and sets can contain other collection datatypes as elements (for example, a sequence of sequences, or set of sequences). The =~= operator only applies extensionality to the top-level collection, not to the nested elements of the collection. To also apply extensionality to the elements, Verus provides a “deep” extensional equality operator =~~= that handles arbitrary nesting of collections, spec_fn, and datatypes. For example:

proof fn ext_equal_nested() { let inner: Set<int> = set!{1, 2, 3}; let s1: Seq<Set<int>> = seq![inner]; let s2 = s1.update(0, s1[0].insert(1)); let s3 = s1.update(0, s1[0].insert(2).insert(3)); // assert(s2 =~= s3); // FAILS assert(s2 =~~= s3); // succeeds let s4: Seq<Seq<Set<int>>> = seq![s1]; let s5: Seq<Seq<Set<int>>> = seq![s2]; assert(s4 =~~= s5); // succeeds }

The same applies to spec_fn, as in:

#[verifier::ext_equal] // necessary for invoking =~= on the struct struct Bar { a: spec_fn(int) -> int, } proof fn ext_equal_fnspec(n: int) { // basic case let f1 = (|i: int| i + 1); let f2 = (|i: int| 1 + i); assert(f1 =~= f2); // succeeds // struct case let b1 = Bar { a: |i: int| if i == 1 { i } else { 1 } }; let b2 = Bar { a: |i: int| 1int }; assert(b1 =~= b2); // succeeds // nested case let i1 = (|i: int| i + 2); let i2 = (|i: int| 2 + i); let n1: Seq<spec_fn(int) -> int> = seq![i1]; let n2: Seq<spec_fn(int) -> int> = seq![i2]; // assert(n1 =~= n2); // FAILS assert(n1 =~~= n2); // succeeds }