Implementing Clone
As a finishing touch, let’s implement Clone
for TreeMap<K, V>
.
The main trick here will be in figuring out the correct specification for TreeMap::<K, V>::Clone
.
Naturally, such an implementation will require both K: Clone
and V: Clone
.
However, to write a sensible clone implementation for the tree, we have to consider
what the implementations of K::clone
and V::clone
actually do.
Generally speaking, Verus imposes no constraints on the implementations of Clone
,
so it is not necessarily true that a clone()
call will return a value that is spec-equal
to its input.
With this in mind, to simplify this example,
we’re going to prove the following signature for TreeMap<K, V>::clone
:
impl<K: Copy + TotalOrdered, V: Clone> Clone for TreeMap<K, V> {
fn clone(&self) -> (res: Self)
ensures self@.dom() =~= res@.dom(),
forall |key| #[trigger] res@.dom().contains(key) ==>
call_ensures(V::clone, (&self@[key],), res@[key])
{
...
}
}
We explain the details of this signature below.
Dealing with K::clone
In order to clone all the keys, we need K::clone
to respect the ordering of elements;
otherwise during a clone operation,
we’d need to re-sort all the keys so that the resulting tree would be valid.
However, it’s unlikely that is desirable behavior. If K::clone
doesn’t respect the
TotalOrdered
implementation, it’s likely a user bug.
A general way to handle this would be to require that Clone
actually be compatible
with the total-ordering in some sense.
However, you’ll
recall from the previous section that we’re already simplifying the “total ordered” specification
a bit. Likewise, we’re going to continue to keep things simple here by also requiring
that K: Copy
.
As a result, we’ll be able to prove that our TreeMap
clone implementation can preserve
all keys exactly, even when compared via spec equality. That is, we’ll be able to
ensure that self@.dom() =~= res@.dom()
.
Dealing with V::clone
So what about V
? Again, we don’t know a priori what V::clone
does. It might return
a value unequal to the imput; it might even be nondeterminstic. Therefore,
a cloned TreeMap
may have different values than the original.
In order to specify TreeMap::<K, V>::clone
as generically as possible, we choose
to write its ensures clause in terms of the ensures clause for V::clone
.
This can be done using call_ensures
.
The predicate call_ensures(V::clone, (&self@[key],), res@[key])
effectively says
“self@[key]
and res@[key]
are a possible input-output pair for V::clone
”.
Understanding the implications of the signature
Let’s do a few examples.
First, consider cloning a TreeMap::<u64, u32>
. The Verus standard library provides
a specification for u32::clone
; it’s the same as a copy, i.e., a cloned u32
always
equals the input. As a result, we can deduce that cloning a TreeMap::<u64, u32>
will
preserve its view
exactly. We can prove this using extensional equality.
fn test_clone_u32(tree_map: TreeMap<u64, u32>) {
let tree_map2 = tree_map.clone();
assert(tree_map2@ =~= tree_map@);
}
We can do the same for any type where clone
guarantees spec-equality. Here’s another
example with a user-defined type.
struct IntWrapper {
pub int_value: u32,
}
impl Clone for IntWrapper {
fn clone(&self) -> (s: Self)
ensures s == *self
{
IntWrapper { int_value: self.int_value }
}
}
fn test_clone_int_wrapper(tree_map: TreeMap<u64, IntWrapper>) {
let tree_map2 = tree_map.clone();
assert(tree_map2@ =~= tree_map@);
}
This works because of the postcondition on IntWrapper::clone
, that is, ensures *s == self
.
If you’re new to this style, it might seem initially surprising that
IntWrapper::clone
has any effect on the verification of test_clone_int_wrapper
, since
it doesn’t directly call IntWrapper::clone
. In this case, the postcondition is referenced
indirectly via TreeMap<u64, IntWrapper>:clone
.
Let’s do one more example, this time with a less precise clone function.
struct WeirdInt {
pub int_value: u32,
pub other: u32,
}
impl Clone for WeirdInt {
fn clone(&self) -> (s: Self)
ensures s.int_value == self.int_value
{
WeirdInt { int_value: self.int_value, other: 0 }
}
}
fn test_clone_weird_int(tree_map: TreeMap<u64, WeirdInt>) {
let tree_map2 = tree_map.clone();
// assert(tree_map2@ =~= tree_map@); // this would fail
assert(tree_map2@.dom() == tree_map@.dom());
assert(forall |k| tree_map@.dom().contains(k) ==>
tree_map2@[k].int_value == tree_map@[k].int_value);
}
This example is a bit pathological; our struct, WeirdInt
, has an extra field that doesn’t
get cloned. You could imagine real-life scenarios that have this property (for example,
if every struct needs to have a unique identifier). Anyway, the postcondition of
WeirdInt::clone
doesn’t say both objects are equal, only that the int_value
fields are equal.
This postcondition can then be inferred for each value in the map, as shown.
Implementing TreeMap::<K, V>::Clone
.
As usual, we write the implementation as a recursive function.
It’s not necessary to implement Node::Clone
; one could instead just implement a normal
recursive function as a helper for TreeMap::Clone
; but it’s more Rust-idiomatic to do it
this way. This lets us call Option<Node<K, V>>::Clone
in the implementation of TreeMap::clone
(the spec for Option::clone
is provided by
vstd). However, you can see that there are a few ‘gotchas’ that need
to be worked around.
impl<K: Copy + TotalOrdered, V: Clone> Clone for Node<K, V> {
fn clone(&self) -> (res: Self)
ensures
self.well_formed() ==> res.well_formed(),
self.as_map().dom() =~= res.as_map().dom(),
forall |key| #[trigger] res.as_map().dom().contains(key) ==>
call_ensures(V::clone, (&self.as_map()[key],), res.as_map()[key])
{
// TODO(fixme): Assigning V::clone to a variable is a hack needed to work around
// this issue: https://github.com/verus-lang/verus/issues/1348
let v_clone = V::clone;
let res = Node {
key: self.key,
value: v_clone(&self.value),
// Ordinarily, we would use Option<Node>::clone rather than inlining
// the case statement here; we write it this way to work around
// this issue: https://github.com/verus-lang/verus/issues/1346
left: (match &self.left {
Some(node) => Some(Box::new((&**node).clone())),
None => None,
}),
right: (match &self.right {
Some(node) => Some(Box::new((&**node).clone())),
None => None,
}),
};
proof {
assert(Node::optional_as_map(res.left).dom() =~=
Node::optional_as_map(self.left).dom());
assert(Node::optional_as_map(res.right).dom() =~=
Node::optional_as_map(self.right).dom());
}
return res;
}
}
impl<K: Copy + TotalOrdered, V: Clone> Clone for TreeMap<K, V> {
fn clone(&self) -> (res: Self)
ensures self@.dom() =~= res@.dom(),
forall |key| #[trigger] res@.dom().contains(key) ==>
call_ensures(V::clone, (&self@[key],), res@[key])
{
proof {
use_type_invariant(self);
}
TreeMap {
// This calls Option<Node<K, V>>::Clone
root: self.root.clone(),
}
}
}
Full source
The full source for this example can be found here.