Peek
Recommended reading: 3.3: Peek
Ideally, we could implement peek
and try_peek
like we implemented pop
and try_pop
before. Like pop
, peek
can only be called if the list is non-empty, and it then always returns a reference to the element at the head of the list (type &T
). Similarly, try_peek
can be called on any list, but returns an Option<&T>
. The latter is currently not possible in Prusti, since structures containing references are not supported at the moment.
We can still implement peek
, but we just cannot do it by using try_peek
like before in pop
. Instead, we can reuse the already implemented and verified lookup
function! Since lookup
can return a reference to any element of the list, we can just call self.lookup(0)
inside of peek
:
// The next line is only required for doctests, you can ignore/remove it
extern crate prusti_contracts;
use prusti_contracts::*;
fn main() {}
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())]
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)]
#[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)))
)]
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))]
pub fn pop(&mut self) -> T {
self.try_pop().unwrap()
}
// // Not currently possible in Prusti
// pub fn try_peek(&self) -> Option<&T> {
// todo!()
// }
#[pure]
#[requires(!self.is_empty())]
pub fn peek(&self) -> &T {
self.lookup(0)
}
}
#[pure]
#[requires(index < link_len(link))]
fn link_lookup<T>(link: &Link<T>, index: usize) -> &T {
match link {
Some(node) => {
if index == 0 {
&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
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`
}
fn _test_peek() {
let mut list = List::new();
list.push(16);
prusti_assert!(list.peek() === list.lookup(0));
prusti_assert!(*list.peek() == 16);
list.push(5);
prusti_assert!(*list.peek() == 5);
list.pop();
prusti_assert!(*list.peek() == 16);
}
}
We can also write a test again, to see if our specification holds up in actual code:
// The next line is only required for doctests, you can ignore/remove it
extern crate prusti_contracts;
use prusti_contracts::*;
fn main() {}
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())]
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)]
#[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)))
)]
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))]
pub fn pop(&mut self) -> T {
self.try_pop().unwrap()
}
// // Not currently possible in Prusti
// pub fn try_peek(&self) -> Option<&T> {
// todo!()
// }
#[pure]
#[requires(!self.is_empty())]
pub fn peek(&self) -> &T {
self.lookup(0)
}
}
#[pure]
#[requires(index < link_len(link))]
fn link_lookup<T>(link: &Link<T>, index: usize) -> &T {
match link {
Some(node) => {
if index == 0 {
&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
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`
}
fn _test_peek() {
let mut list = List::new();
list.push(16);
prusti_assert!(list.peek() === list.lookup(0));
prusti_assert!(*list.peek() == 16);
list.push(5);
prusti_assert!(*list.peek() == 5);
list.pop();
prusti_assert!(*list.peek() == 16);
}
}
This verifies too, so it appears our implementation of peek
is correct.
The peek
method only returns an immutable reference, but what if you want to get a mutable reference? We will see how in the next chapter.
Here you can see the full code we have now:
// 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() {}
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())]
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)]
#[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)))
)]
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))]
pub fn pop(&mut self) -> T {
self.try_pop().unwrap()
}
// // Not currently possible in Prusti
// pub fn try_peek(&self) -> Option<&T> {
// todo!()
// }
#[pure]
#[requires(!self.is_empty())]
pub fn peek(&self) -> &T {
self.lookup(0)
}
}
#[pure]
#[requires(index < link_len(link))]
fn link_lookup<T>(link: &Link<T>, index: usize) -> &T {
match link {
Some(node) => {
if index == 0 {
&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
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`
}
fn _test_peek() {
let mut list = List::new();
list.push(16);
prusti_assert!(list.peek() === list.lookup(0));
prusti_assert!(*list.peek() == 16);
list.push(5);
prusti_assert!(*list.peek() == 5);
list.pop();
prusti_assert!(*list.peek() == 16);
}
}