Existential Quantification
A very common specification style in Pulse involves the use of the existential quantifier. Before we can start to write interesting examples, let’s take a brief look at how existential quantification works.
As mentioned in the introduction to Pulse, one
of the connectives of Pulse’s separation logic is the existential
quantifier. Its syntax is similar to F*’s existential quantifier,
except it is written exists*
instead of just exists
, and its
body is a vprop
, as in the examples shown below.
exists* (v:nat). pts_to x v
exists* v. pts_to x v
exists* v1 v2. pts_to x v1 ** pts_to y v2
...
Some simple examples
Looking back to the assign
example from the previous chapter
(shown below), you may have wondered why we bothered to bind a logical
variable 'v
in precondition of the specification, since it is
never actually used in any other predicate.
fn assign (#a:Type) (r:ref a) (v:a)
requires pts_to r 'v
ensures pts_to r v
{
r := v;
}
And indeed, another way to write the specification of assign
, without
the logical variable argument, is shown below.
fn assign #a (x:ref a) (v:a)
requires
exists* w. pts_to x w
ensures
pts_to x v
{
x := v
}
This time, in the precondition, we use an existential quantifier to
say that assign
is callable in a context where x
points to any
value w
.
Usually, however, the postcondition of a function relates the
initial state prior to the call to the state after the call and
existential variables are only in scope as far to the right as
possible of the enclosing vprop
. So, existential quantifiers in
the precondition of a function are not so common.
To illustrate, the following attempted specification of incr
does
not work, since the existentially bound w0
is not in scope for the
postcondition.
[@@expect_failure]
```pulse
fn incr #a (x:ref int)
requires
exists* w0. pts_to x w
ensures
pts_to x (w0 + 1) //w0 is not in scope here
{
let w = !x
x := w + 1
}
```
However, existential quantification often appears in postconditions,
e.g., in order to abstract the behavior of function by underspecifying
it. To illustrate, consider the function make_even
below. It’s
type states that it sets the contents of x
to some even number
w1
, without specifying w1
exactly. It also uses an existential
quantification in its precondition, since its postcondition does not
depend on the initial value of x
.
fn make_even (x:ref int)
requires
exists* w0. pts_to x w0
ensures
exists* w1. pts_to x w1 ** pure (w1 % 2 == 0)
{
let v = !x;
x := v + v;
}
Manipulating existentials
In a previous chapter on handling classical connectives, we saw how F* provides various constructs for introducing and eliminating logical connectives, including the existential quantifier. Pulse also provides constructs for working explicitly with existential quantifiers, though, usually, Pulse automation takes care of introducing and eliminating existentials behind the scenes. However, the explicit operations are sometimes useful, and we show a first example of how they work below
fn make_even_explicit (x:ref int)
requires
exists* w0. pts_to x w0
ensures
exists* w1. pts_to x w1 ** pure (w1 % 2 == 0)
{
with w0. assert (pts_to x w0);
let v = !x; // v:int; _:squash(v==w0); Ctxt=pts_to x v
x := v + v; // ... pts_to x (v + v)
introduce
exists* w1. pts_to x w1 ** pure (w1 % 2 == 0)
with (v + v);
}
Eliminating existentials
The form with w0...wn. assert p; rest
is often used as an
eliminator for an existential. When the context contains exists*
x0...xn. p
, the with
construct binds w0 ... wn
to the
existentially bound variables in the remainder of the scope rest
.
A show_proof_state
immediately after the with w0. assert (pts_to
x w0)
prints the following:
- Current context:
pts_to x (reveal w0) **
emp
- In typing environment:
[w0#2 : erased int,
x#1 : ref int]
That is, we have w0:erased int
in scope, and pts_to x (reveal
w0)
in context.
Here is another example usage of with
, this time with multiple
binders.
fn make_even_explicit_alt (x y:ref int)
requires
exists* wx wy. pts_to x wx ** pts_to y wy ** pure (wx % 2 == wy % 2)
ensures
exists* wx' wy'. pts_to x wx' ** pts_to y wy' ** pure (wx' % 2 == 0)
{
with wx wy. _;
let vx = !x;
let vy = !y;
x := vx + vy;
introduce exists* wx' wy'. pts_to x wx' ** pts_to y wy' ** pure (wx' % 2 == 0)
with (vx + vy) vy;
}
When there is a single existential formula in the context, one can
write with x1..xn. _
to “open” the formula, binding its witnesses
in scope. A show_proof_state
after the first line prints:
- Current context:
pts_to x (reveal wx) **
pts_to y (reveal wy) **
pure (eq2 (op_Modulus (reveal wx) 2) (op_Modulus (reveal wy) 2)) **
emp
- In typing environment:
[_#5 : squash (eq2 (op_Modulus (reveal wx) 2) (op_Modulus (reveal wy) 2)),
wy#4 : erased int,
wx#3 : erased int,
y#2 : ref int,
x#1 : ref int]
Introducing existentials
The Pulse checker will automatically introduce existential formulas by
introduces new unification variables for each existentially bound
variable, and then trying to find solutions for those variables by
matching vprops
in the goal with those in the context.
However, one can also introduce existential formulas explicitly, using
the introduce exists*
syntax, as seen in the two examples
above. In general, one can write
introduce exists* x1 .. xn. p
with w1...wn
explicitly providing witnesses w1..wn
for each of the
existentially bound variables x1..xn
.