Making it all Generic
Recommended reading: 3.2: Generic
Just like the corresponding chapter in the "Learning Rust With Entirely Too Many Linked Lists" book, we will change our list to have a generic element type T
, not just i32
. For this, we go through our code an add the generic parameter T
where required. The compiler really helps for this, since it will mark where a generic parameter is needed.
If you do this process with Prusti, at some point you will encounter the following error:
[E0369] binary operation `==` cannot be applied to type `T`.
This is because the generic type T
might not implement PartialEq
and thus not have an equality function ==
that could be called on it like i32
does. Since we only used ==
inside of specifications, we can fix this problems by using snapshot equality ===
instead.
Here you can see where some of the changes were done (expand to see the full changes):
// The next line is only required for doctests, you can ignore/remove it
extern crate prusti_contracts;
use prusti_contracts::*;
fn main() {}
// Make the types generic:
pub struct List<T> {
head: Link<T>,
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
#[extern_spec(std::mem)]
#[ensures(snap(dest) === src)]
#[ensures(result === old(snap(dest)))]
fn replace<T>(dest: &mut T, src: T) -> T;
// Specs for std::option::Option<T>::unwrap(self) (and others) can be found here (work in progress):
// https://github.com/viperproject/prusti-dev/pull/1249/files#diff-bccda07f8a48357687e26408251041072c7470c188092fb58439de39974bdab5R47-R49
#[extern_spec]
impl<T> std::option::Option<T> {
#[requires(self.is_some())]
#[ensures(old(self) === Some(result))]
pub fn unwrap(self) -> T;
#[pure]
#[ensures(result == matches!(self, None))]
pub const fn is_none(&self) -> bool;
#[pure]
#[ensures(result == matches!(self, Some(_)))]
pub const fn is_some(&self) -> bool;
#[ensures(result === old(snap(self)))]
#[ensures(self.is_none())]
pub fn take(&mut self) -> Option<T>;
}
impl<T> List<T> {
// ...
#[pure]
pub fn len(&self) -> usize {
link_len(&self.head)
}
#[pure]
fn is_empty(&self) -> bool {
matches!(self.head, None)
}
#[ensures(result.len() == 0)]
pub fn new() -> Self {
List { head: None }
}
#[pure]
#[requires(index < self.len())]
// Return type is changed from `T` to `&T`
pub fn lookup(&self, index: usize) -> &T {
link_lookup(&self.head, index)
}
#[ensures(self.len() == old(self.len()) + 1)]
#[ensures(snap(self.lookup(0)) === elem)] // Here we add a `snap`
#[ensures(forall(|i: usize| (i < old(self.len())) ==>
old(self.lookup(i)) === self.lookup(i + 1)))]
pub fn push(&mut self, elem: T) {
// ...
let new_node = Box::new(Node {
elem,
next: self.head.take(),
});
self.head = Some(new_node);
}
predicate! {
// two-state predicate to check if the head of a list was correctly removed
fn head_removed(&self, prev: &Self) -> bool {
self.len() == prev.len() - 1 // The length will decrease by 1
&& forall(|i: usize| // Every element will be shifted forwards by one
(1 <= i && i < prev.len())
==> prev.lookup(i) === self.lookup(i - 1))
}
}
#[ensures(old(self.is_empty()) ==>
result.is_none() &&
self.is_empty()
)]
#[ensures(!old(self.is_empty()) ==>
self.head_removed(&old(snap(self)))
&&
result === Some(snap(old(snap(self)).lookup(0)))
)]
// Return type changed from `Option<i32>`
pub fn try_pop(&mut self) -> Option<T> {
// ...
match self.head.take() { // Replace mem::swap with the buildin Option::take
None => None,
Some(node) => {
self.head = node.next;
Some(node.elem)
}
}
}
#[requires(!self.is_empty())]
#[ensures(self.head_removed(&old(snap(self))))]
#[ensures(result === old(snap(self)).lookup(0))]
// Return type changed from `i32`
pub fn pop(&mut self) -> T {
self.try_pop().unwrap()
}
}
#[pure]
#[requires(index < link_len(link))]
// Return type is changed from `T` to `&T`
fn link_lookup<T>(link: &Link<T>, index: usize) -> &T {
match link {
Some(node) => {
if index == 0 {
// Here we return a reference to `elem` instead of the `elem` itself
&node.elem
} else {
link_lookup(&node.next, index - 1)
}
}
None => unreachable!(),
}
}
#[pure]
fn link_len<T>(link: &Link<T>) -> usize {
match link {
None => 0,
Some(node) => 1 + link_len(&node.next),
}
}
#[cfg(prusti)]
mod prusti_tests {
use super::*;
fn _test_list(){
// ...
let mut list = List::new(); // create an new, empty list
prusti_assert!(list.is_empty() && list.len() == 0); // list should be empty
list.push(5);
list.push(10);
prusti_assert!(!list.is_empty() && list.len() == 2); // length correct
// Here we can just dereference the lookup with `*`, since `i32` is `Copy`:
prusti_assert!(*list.lookup(0) == 10); // head is 10
prusti_assert!(*list.lookup(1) == 5); // 5 got pushed back correctly
let x = list.pop();
prusti_assert!(x == 10); // pop returns the value that was added last
match list.try_pop() {
Some(y) => assert!(y == 5),
None => unreachable!()
}
let z = list.try_pop();
prusti_assert!(list.is_empty() && list.len() == 0); // length correct
prusti_assert!(z.is_none()); // `try_pop` on an empty list should return `None`
}
}
This code still fails to compile, this time with an error from the function link_lookup
:
[E0507] cannot move out of `node.elem` which is behind a shared reference.
[Note] move occurs because `node.elem` has type `T`, which does not implement the `Copy` trait
To fix this, we will change List::lookup
and link_lookup
to return a reference to the element at index i
, instead of the element itself. This was not needed for i32
, since it implements the Copy
trait. For a general type T
, returning it by value would move it out of the list, which we don't want. By returning a reference instead, the lookups will work for any type T
, because the element stays in the list.
In addition to returning a reference, we will have to adjust some of the places where lookup
is used, mostly by dereferencing or using snap
on the returned reference:
// The next line is only required for doctests, you can ignore/remove it
extern crate prusti_contracts;
use prusti_contracts::*;
fn main() {}
// Make the types generic:
pub struct List<T> {
head: Link<T>,
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
#[extern_spec(std::mem)]
#[ensures(snap(dest) === src)]
#[ensures(result === old(snap(dest)))]
fn replace<T>(dest: &mut T, src: T) -> T;
// Specs for std::option::Option<T>::unwrap(self) (and others) can be found here (work in progress):
// https://github.com/viperproject/prusti-dev/pull/1249/files#diff-bccda07f8a48357687e26408251041072c7470c188092fb58439de39974bdab5R47-R49
#[extern_spec]
impl<T> std::option::Option<T> {
#[requires(self.is_some())]
#[ensures(old(self) === Some(result))]
pub fn unwrap(self) -> T;
#[pure]
#[ensures(result == matches!(self, None))]
pub const fn is_none(&self) -> bool;
#[pure]
#[ensures(result == matches!(self, Some(_)))]
pub const fn is_some(&self) -> bool;
#[ensures(result === old(snap(self)))]
#[ensures(self.is_none())]
pub fn take(&mut self) -> Option<T>;
}
impl<T> List<T> {
// ...
#[pure]
pub fn len(&self) -> usize {
link_len(&self.head)
}
#[pure]
fn is_empty(&self) -> bool {
matches!(self.head, None)
}
#[ensures(result.len() == 0)]
pub fn new() -> Self {
List { head: None }
}
#[pure]
#[requires(index < self.len())]
// Return type is changed from `T` to `&T`
pub fn lookup(&self, index: usize) -> &T {
link_lookup(&self.head, index)
}
#[ensures(self.len() == old(self.len()) + 1)]
#[ensures(snap(self.lookup(0)) === elem)] // Here we add a `snap`
#[ensures(forall(|i: usize| (i < old(self.len())) ==>
old(self.lookup(i)) === self.lookup(i + 1)))]
pub fn push(&mut self, elem: T) {
// ...
let new_node = Box::new(Node {
elem,
next: self.head.take(),
});
self.head = Some(new_node);
}
predicate! {
// two-state predicate to check if the head of a list was correctly removed
fn head_removed(&self, prev: &Self) -> bool {
self.len() == prev.len() - 1 // The length will decrease by 1
&& forall(|i: usize| // Every element will be shifted forwards by one
(1 <= i && i < prev.len())
==> prev.lookup(i) === self.lookup(i - 1))
}
}
#[ensures(old(self.is_empty()) ==>
result.is_none() &&
self.is_empty()
)]
#[ensures(!old(self.is_empty()) ==>
self.head_removed(&old(snap(self)))
&&
result === Some(snap(old(snap(self)).lookup(0)))
)]
// Return type changed from `Option<i32>`
pub fn try_pop(&mut self) -> Option<T> {
// ...
match self.head.take() { // Replace mem::swap with the buildin Option::take
None => None,
Some(node) => {
self.head = node.next;
Some(node.elem)
}
}
}
#[requires(!self.is_empty())]
#[ensures(self.head_removed(&old(snap(self))))]
#[ensures(result === old(snap(self)).lookup(0))]
// Return type changed from `i32`
pub fn pop(&mut self) -> T {
self.try_pop().unwrap()
}
}
#[pure]
#[requires(index < link_len(link))]
// Return type is changed from `T` to `&T`
fn link_lookup<T>(link: &Link<T>, index: usize) -> &T {
match link {
Some(node) => {
if index == 0 {
// Here we return a reference to `elem` instead of the `elem` itself
&node.elem
} else {
link_lookup(&node.next, index - 1)
}
}
None => unreachable!(),
}
}
#[pure]
fn link_len<T>(link: &Link<T>) -> usize {
match link {
None => 0,
Some(node) => 1 + link_len(&node.next),
}
}
#[cfg(prusti)]
mod prusti_tests {
use super::*;
fn _test_list(){
// ...
let mut list = List::new(); // create an new, empty list
prusti_assert!(list.is_empty() && list.len() == 0); // list should be empty
list.push(5);
list.push(10);
prusti_assert!(!list.is_empty() && list.len() == 2); // length correct
// Here we can just dereference the lookup with `*`, since `i32` is `Copy`:
prusti_assert!(*list.lookup(0) == 10); // head is 10
prusti_assert!(*list.lookup(1) == 5); // 5 got pushed back correctly
let x = list.pop();
prusti_assert!(x == 10); // pop returns the value that was added last
match list.try_pop() {
Some(y) => assert!(y == 5),
None => unreachable!()
}
let z = list.try_pop();
prusti_assert!(list.is_empty() && list.len() == 0); // length correct
prusti_assert!(z.is_none()); // `try_pop` on an empty list should return `None`
}
}
After all these changes, Prusti is able to verify the code again, so now our linked list can be used to store elements of any type, not just i32
!
If you want to see the full code after all the changes, expand the following code block.
// Expand to see full code up to this chapter
// The next line is only required for doctests, you can ignore/remove it
extern crate prusti_contracts;
use prusti_contracts::*;
fn main() {}
// Make the types generic:
pub struct List<T> {
head: Link<T>,
}
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
#[extern_spec(std::mem)]
#[ensures(snap(dest) === src)]
#[ensures(result === old(snap(dest)))]
fn replace<T>(dest: &mut T, src: T) -> T;
// Specs for std::option::Option<T>::unwrap(self) (and others) can be found here (work in progress):
// https://github.com/viperproject/prusti-dev/pull/1249/files#diff-bccda07f8a48357687e26408251041072c7470c188092fb58439de39974bdab5R47-R49
#[extern_spec]
impl<T> std::option::Option<T> {
#[requires(self.is_some())]
#[ensures(old(self) === Some(result))]
pub fn unwrap(self) -> T;
#[pure]
#[ensures(result == matches!(self, None))]
pub const fn is_none(&self) -> bool;
#[pure]
#[ensures(result == matches!(self, Some(_)))]
pub const fn is_some(&self) -> bool;
#[ensures(result === old(snap(self)))]
#[ensures(self.is_none())]
pub fn take(&mut self) -> Option<T>;
}
impl<T> List<T> {
// ...
#[pure]
pub fn len(&self) -> usize {
link_len(&self.head)
}
#[pure]
fn is_empty(&self) -> bool {
matches!(self.head, None)
}
#[ensures(result.len() == 0)]
pub fn new() -> Self {
List { head: None }
}
#[pure]
#[requires(index < self.len())]
// Return type is changed from `T` to `&T`
pub fn lookup(&self, index: usize) -> &T {
link_lookup(&self.head, index)
}
#[ensures(self.len() == old(self.len()) + 1)]
#[ensures(snap(self.lookup(0)) === elem)] // Here we add a `snap`
#[ensures(forall(|i: usize| (i < old(self.len())) ==>
old(self.lookup(i)) === self.lookup(i + 1)))]
pub fn push(&mut self, elem: T) {
// ...
let new_node = Box::new(Node {
elem,
next: self.head.take(),
});
self.head = Some(new_node);
}
predicate! {
// two-state predicate to check if the head of a list was correctly removed
fn head_removed(&self, prev: &Self) -> bool {
self.len() == prev.len() - 1 // The length will decrease by 1
&& forall(|i: usize| // Every element will be shifted forwards by one
(1 <= i && i < prev.len())
==> prev.lookup(i) === self.lookup(i - 1))
}
}
#[ensures(old(self.is_empty()) ==>
result.is_none() &&
self.is_empty()
)]
#[ensures(!old(self.is_empty()) ==>
self.head_removed(&old(snap(self)))
&&
result === Some(snap(old(snap(self)).lookup(0)))
)]
// Return type changed from `Option<i32>`
pub fn try_pop(&mut self) -> Option<T> {
// ...
match self.head.take() { // Replace mem::swap with the buildin Option::take
None => None,
Some(node) => {
self.head = node.next;
Some(node.elem)
}
}
}
#[requires(!self.is_empty())]
#[ensures(self.head_removed(&old(snap(self))))]
#[ensures(result === old(snap(self)).lookup(0))]
// Return type changed from `i32`
pub fn pop(&mut self) -> T {
self.try_pop().unwrap()
}
}
#[pure]
#[requires(index < link_len(link))]
// Return type is changed from `T` to `&T`
fn link_lookup<T>(link: &Link<T>, index: usize) -> &T {
match link {
Some(node) => {
if index == 0 {
// Here we return a reference to `elem` instead of the `elem` itself
&node.elem
} else {
link_lookup(&node.next, index - 1)
}
}
None => unreachable!(),
}
}
#[pure]
fn link_len<T>(link: &Link<T>) -> usize {
match link {
None => 0,
Some(node) => 1 + link_len(&node.next),
}
}
#[cfg(prusti)]
mod prusti_tests {
use super::*;
fn _test_list(){
// ...
let mut list = List::new(); // create an new, empty list
prusti_assert!(list.is_empty() && list.len() == 0); // list should be empty
list.push(5);
list.push(10);
prusti_assert!(!list.is_empty() && list.len() == 2); // length correct
// Here we can just dereference the lookup with `*`, since `i32` is `Copy`:
prusti_assert!(*list.lookup(0) == 10); // head is 10
prusti_assert!(*list.lookup(1) == 5); // 5 got pushed back correctly
let x = list.pop();
prusti_assert!(x == 10); // pop returns the value that was added last
match list.try_pop() {
Some(y) => assert!(y == 5),
None => unreachable!()
}
let z = list.try_pop();
prusti_assert!(list.is_empty() && list.len() == 0); // length correct
prusti_assert!(z.is_none()); // `try_pop` on an empty list should return `None`
}
}