Using Default Specifications

scaffold is geared towards incremental verification. It allows users to iteratively increase the scope of the verification project. Running the interactive command scaffold verify presents us with all available specification/verification targets (or, in our case, one target).

Run scaffold verify, select the swap function, and confirm the choice with Enter.

$ scaffold verify
? What would you like to specify?  
>    swap(size_t &, size_t &) @ src/stage1.cpp
  <DONE>

scaffold now asks us to select how we want to specify the function. We have several options:

  1. Writing a specification by hand,
  2. Automatically generating a default, memory safety, specification, or
  3. Direct the tools to reason about it by inlining it.

To keep the example simple, we select Memory safey, which will generate a simple specification based on the function's type.

Verifying swap(size_t &, size_t &)...
void swap(size_t& x, size_t& y) {
    size_t tmp = y;
    y = x;
    x = tmp;
}
? How?  
  By hand
> Memory safety
  Inline it
[Write a specification for the function]

We are brought back to the initial overview where swap is now marked with a checkmark, indicating that scaffold knows what the function's specification is supposed to be. We select <DONE> to save our changes to disk.

? What would you like to specify?  
  ✅ swap(size_t &, size_t &) @ src/stage1.cpp
> <DONE>

The proof/ directory now contains a new directory called stage1_cpp. In it, scaffold collects specifications and proofs of functions from stage1.cpp, i.e. swap.

$ tree proof/stage1_cpp/
proof/stage1_cpp/
├── pred.v
├── proof
│   └── swap.v
├── proof.v
└── spec.v

Two files are of particular interest to us.

  1. proof/stage1_cpp/spec.v contains the specification that scaffold generated for us. Since we selected the default specification, we see only following line:

    cpp.spec "swap(size_t &, size_t &)" as swap_spec default.
  2. proof/stage1_cpp/proof/swap.v contains a simple proof script that is meant to verify the function. The two relevant lines are:

    Lemma swap_ok : verify[source] "swap(size_t &, size_t &)".
    Proof. verify_spec; go. Qed.

Even though the proof ends with Qed, it has not actually been checked yet. We can run dune build proof/ (or dune b proof/ for short) to check the proof is indeed correct. The command should succeed.

$ dune b proof/

What Have We Proven?

To check the specification that scaffold generated for swap, open the file proof/stage1_cpp/spec.v and add Print swap_spec. to the end of the file. To see the result, we can run dune again.

$ dune b proof/
swap_spec =
λ (thread_info : biIndex) (: gFunctors) (Σ : cpp_logic thread_info _Σ) 
  (σ : genv),
  specify
    {|
      info_name := "swap(unsigned long&, unsigned long&)";
      info_type :=
        tFunction "void" ["unsigned long&"%cpp_type; "unsigned long&"%cpp_type]
    |}
    (\arg{(x_addr : ptr) (x : Z)} "x" (Vptr x_addr)
     \pre x_addr |-> ulongR 1$m x 
     \arg{(y_addr : ptr) (y : Z)} "y" (Vptr y_addr)
     \pre y_addr |-> ulongR 1$m y 
     \post    Exists (x0 x1 : Z), y_addr |-> ulongR 1$m x0 ** x_addr |-> ulongR 1$m x1)

In scaffold ,the default specification for a function is formed by specifying the basic ownership of each type with existentially quantified values. For more information on what this specification means, check out the documentation on function specifications.

While the default specification doesn't say that the function actually swaps its arguments, the proof of it does guarantee the absence of undefined behavior as long as the pre-condition holds when calling the function. This includes properties such as

Up Next

In the next chapter of this tutorial, we provide a more precise specification that also encompasses the functional behavior of swap.