Mutable Arrays

In this chapter, we will learn about mutable arrays in Pulse. An array is a contiguous collection of values of the same type. Similar to ref, arrays in Pulse can be allocated in the stack frame of the current function or in the heap—while the stack allocated arrays are reclaimed automatically (e.g., when the function returns), heap allocated arrays are explicitly managed by the programmer.

Pulse provides two array types: Pulse.Lib.Array.array t as the basic array type and Pulse.Lib.Vec.vec t for heap allocated arrays. To provide code reuse, functions that may operate over both stack and heap allocated arrays can be written using Pulse.Lib.Array.array t—the Pulse.Lib.Vec library provides back-and-forth coercions between vec t and array t.

array t

We illustrate the basics of array t with the help of the following example that reads an array:

fn read_i
  (#[@@@ Rust_generics_bounds ["Copy"]] t:Type)
  (arr:array t)
  (#p:perm)
  (#s:erased (Seq.seq t))
  (i:SZ.t { SZ.v i < Seq.length s })
  requires pts_to arr #p s
  returns x:t
  ensures pts_to arr #p s ** pure (x == Seq.index s (SZ.v i))
{
  arr.(i)
}

The library provides a points-to predicate pts_to arr #p s with the interpretation that in the current memory, the contents of arr are same as the (functional) sequence s:FStar.Seq.seq t. Like the pts_to predicate on reference, it is also indexed by an implicit fractional permission p, which distinguished shared, read-only access from exclusive read/write access.

In the arguments of read_i, the argument `s is erased, since it is for specification only.

Arrays can be read and written-to using indexes of type FStar.SizeT.t, a model of C size_t [1] in F*, provided that the index is within the array bounds—the refinement SZ.v i < Seq.length s enforces that the index is in bounds, where module SZ = FStar.SizeT. The function returns the i-th element of the array, the asserted by the postcondition vprop pure (x == Seq.index s (SZ.v i)). The body of the function uses the array read operator arr.(i).

As another example, let’s write to the i-th element of an array:

fn write_i (#t:Type) (arr:array t) (#s:erased (Seq.seq t)) (x:t) (i:SZ.t { SZ.v i < Seq.length s })
  requires pts_to arr s
  ensures pts_to arr (Seq.upd s (SZ.v i) x)
{
  arr.(i) <- x
}

The function uses the array write operator arr(i) <- x and the postcondition asserts that in the state when the function returns, the contents of the array are same as the sequence s updated at the index i.

While any permission suffices for reading, writing requires 1.0R. For example, implementing write_i without 1.0R is rejected, as shown below.

[@@ expect_failure]
```pulse
fn write_ip #t (arr:array t) (#p:perm) (#s:erased _) (x:t) (i:SZ.t { SZ.v i < Seq.length s })
  requires pts_to arr #p s
  ensures pts_to arr #p (Seq.upd s (SZ.v i) x)
{
  arr.(i) <- x
}
```

The library contains share and gather functions, similar to those for references, to divide and combine permissions on arrays.

We now look at a couple of examples that use arrays with conditionals, loops, existentials, and invariants, using many of the Pulse constructs we have seen so far.

Compare

Let’s implement a function that compares two arrays for equality:

fn compare
  (#[@@@ Rust_generics_bounds ["PartialEq"; "Copy"]] t:eqtype)
  #p1 #p2
  (a1 a2:A.array t)
  (l:SZ.t) 
  requires (
    A.pts_to a1 #p1 's1 **
    A.pts_to a2 #p2 's2 **
    pure (Seq.length 's1 == Seq.length 's2 /\ Seq.length 's2 == SZ.v l)
  )
  returns res:bool
  ensures (
    A.pts_to a1 #p1 's1 **
    A.pts_to a2 #p2 's2 **
    pure (res <==> Seq.equal 's1 's2)
  )

The function takes two arrays a1 and a2 as input, and returns a boolean. The postcondition pure (res <==> Seq.equal 's1 's2) specifies that the boolean is true if and only if the sequence representations of the two arrays are equal. Since the function only reads the arrays, it is parametric in the permissions p1 and p2 on the two arrays. Note that the type parameter t has type eqtype, requiring that values of type t support decidable equality.

One way to implement compare is to use a while loop, reading the two arrays using a mutable counter and checking that the corresponding elements are equal.

{
  let mut i = 0sz;
  while (
    let vi = !i;
    if (vi <^ l) {
      let v1 = a1.(vi);
      let v2 = a2.(vi);
      (v1 = v2)
    } else {
      false
    }
  )
  invariant b.
  exists* vi. ( 
    R.pts_to i vi **
    A.pts_to a1 #p1 's1 **
    A.pts_to a2 #p2 's2 **
    pure (
      SZ.v vi <= SZ.v l /\
      (b == (SZ.v vi < SZ.v l && Seq.index 's1 (SZ.v vi) = Seq.index 's2 (SZ.v vi))) /\
      (forall (i:nat). i < SZ.v vi ==> Seq.index 's1 i == Seq.index 's2 i)
    )
  )
  {
    let vi = !i; 
    i := vi +^ 1sz
  };
  let vi = !i;
  let res = vi = l;
  res
}

The loop invariant states that (a) the arrays are pointwise equal up to the current value of the counter, and (b) the boolean b is true if and only if the current value of the counter is less than the length of the arrays and the arrays are equal at that index. While (a) helps proving the final postcondition of compare, (b) is required to maintain the invariant after the counter is incremented in the loop body.

Copy

As our next example, let’s implement a copy function that copies the contents of the array a2 to a1.

fn copy
  (#[@@@ Rust_generics_bounds ["Copy"]] t:Type0)
  (a1 a2:A.array t)
  (l:SZ.t)
  (#p2:perm)
  requires (
    A.pts_to a1 's1 **
    A.pts_to a2 #p2 's2 **
    pure (Seq.length 's1 == Seq.length 's2 /\ Seq.length 's2 == SZ.v l)
  )
  ensures (
    (exists* s1. A.pts_to a1 s1 ** pure (Seq.equal s1 's2)) **
    A.pts_to a2 #p2 's2
  )
{
  let mut i = 0sz;
  while (
    let vi = !i;
    (vi <^ l)
  )
  invariant b.
  exists* vi s1. ( 
    R.pts_to i vi **
    A.pts_to a1 s1 **
    A.pts_to a2 #p2 's2 **
    pure (
      Seq.length s1 == Seq.length 's2 /\
      SZ.v vi <= SZ.v l /\
      (b == (SZ.v vi < SZ.v l)) /\
      (forall (i:nat). i < SZ.v vi ==> Seq.index s1 i == Seq.index 's2 i)
    )
  )
  {
    let vi = !i;
    let v = a2.(vi);
    a1.(vi) <- v;
    i := vi +^ 1sz
  }
}

The loop invariant existentially abstracts over the contents of a1, and maintains that up to the current loop counter, the contents of the two arrays are equal. Rest of the code is straightforward, the loop conditional checks that the loop counter is less than the array lengths and the loop body copies one element at a time.

The reader will notice that the postcondition of copy is a little convoluted. A better signature would be the following, where we directly state that the contents of a1 are same as 's2:

fn copy2
  (#[@@@ Rust_generics_bounds ["Copy"]] t:Type0)
  (a1 a2:A.array t)
  (l:SZ.t)
  (#p2:perm)
  requires (
    A.pts_to a1 's1 **
    A.pts_to a2 #p2 's2 **
    pure (Seq.length 's1 == Seq.length 's2 /\ Seq.length 's2 == SZ.v l)
  )
  ensures (
    A.pts_to a1 's2 **
    A.pts_to a2 #p2 's2
  )

We can implement this signature, but it requires one step of rewriting at the end after the while loop to get the postcondition in this exact shape:

  // after the loop
  with v1 s1. _; //bind existentially bound witnesses from the invariant
  Seq.lemma_eq_elim s1 's2; //call an F* lemma to prove that s1 == 's2
  ()

We could also rewrite the predicates explicitly, as we saw in a previous chapter.

Stack allocated arrays

Stack arrays can be allocated using the expression [| v; n |]. It allocates an array of size n, with all the array elements initialized to v. The size n must be compile-time constant. It provides the postcondition that the newly create array points to a length n sequence of v. The following example allocates two arrays on the stack and compares them using the compare function above.

fn compare_stack_arrays ()
  requires emp
  ensures emp
{
  // |- emp
  let mut a1 = [| 0; 2sz |];
  // a1:array int |- pts_to a1 (Seq.create 0 (SZ.v 2))
  let mut a2 = [| 0; 2sz |];
  // a1 a2:array int |- pts_to a1 (Seq.create 0 (SZ.v 2)) ** pts_to a2 (Seq.create 0 (SZ.v 2))
  let b = compare a1 a2 2sz;
  assert (pure b)
}

As with the stack references, stack arrays don’t need to be deallocated or dropped, they are reclaimed automatically when the function returns. As a result, returning them from the function is not allowed:

[@@ expect_failure]
```pulse
fn ret_stack_array ()
  requires emp
  returns a:array int
  ensures pts_to a (Seq.create 2 0)
{
  let mut a1 = [| 0; 2sz |];
  a1  // cannot prove pts_to a (Seq.create 0 2) in the context emp
}
```

Heap allocated arrays

The library Pulse.Lib.Vec provides the type vec t, for heap-allocated arrays: vec is to array as box is to ref.

Similar to array, vec is accompanied with a pts_to assertion with support for fractional permissions, share and gather for dividing and combining permissions, and read and write functions. However, unlike array, the Vec library provides allocation and free functions.

module V = Pulse.Lib.Vec

```pulse 
fn heap_arrays ()
  requires emp
  returns a:V.vec int
  ensures V.pts_to a (Seq.create 2 0)
{
  let a1 = V.alloc 0 2sz;
  let a2 = V.alloc 0 2sz;
  V.free a1;
  a2  // returning vec is ok
}
```

As with the heap references, heap allocated arrays can be coerced to array using the coercion vec_to_array. To use the coercion, it is often required to convert Vec.pts_to to Array.pts_to back-and-forth; the library provides to_array_pts_to and to_vec_pts_to lemmas for this purpose.

The following example illustrates the pattern. It copies the contents of a stack array into a heap array, using the copy2 function we wrote above.

fn copy_app ([@@@ Rust_mut_binder] v:V.vec int)
  requires exists* s. V.pts_to v s ** pure (Seq.length s == 2)
  ensures V.pts_to v (Seq.create 2 0)
{
  let mut a = [| 0; 2sz |];
  // v, s |- V.pts_to v s ** ...
  V.to_array_pts_to v;
  // v, s |- pts_to (vec_to_array v) s ** ...
  copy2 (V.vec_to_array v) a 2sz;
  // v, s |- pts_to (vec_to_array v) (Seq.create 2 0) ** ...
  V.to_vec_pts_to v
  // v, s |- V.pts_to v (Seq.create 2 0) ** ...
}

Note how the assertion for v transforms from V.pts_to to pts_to (the points-to assertion for arrays) and back. It means that array algorithms and routines can be implemented with the array t type, and then can be reused for both stack- and heap-allocated arrays.

Finally, though the name vec a evokes the Rust std::Vec library, we don’t yet support automatic resizing.