POCS.Lab0.Exercises
To start with, we define a module called Exercises that
takes as an argument an implementation of our variables
layer, called vars.
Now that we are inside the module, we have access to the
variables API that we defined in ThreeVariablesAPI.
You can examine the functions provided by the vars
module using the Check command, as follows:
The above Check command should have printed:
vars.read
: var -> proc nat
meaning that vars.read is something that takes a variable
name (of type var) and gives us a procedure that returns
a nat value.
vars.write similarly takes a variable name and a new value
and gives us a procedure with no return value.
To start with, we will define a simple procedure incX
that increments the value of the x variable:
Part A: incrementing and swapping.
To check if we got the implementation right, let's try first
testing it on a specific input. We can do this by writing a
very particular specification: if the starting state was
(1, 2, 3), corresponding to the values of variables x, y, and
z respectively, then the final state should be (2, 2, 3).
The vars.recover at the end of the specification says that
we should run the vars.recover procedure after a crash; this
does not matter so far, because we are ignoring crashes.
The vars.abstr indicates how we are describing states in
this specification. Here, states are described in terms of
the variables layer (i.e., consisting of the three values for
x, y, and z). We will see in the next lab how and why your
specification may want to describe the state at a different
abstraction layer.
Theorem incX_test :
proc_spec (fun (_:unit) state => {|
pre :=
state = mkState 1 2 3;
post := fun r state' =>
state' = mkState 2 2 3;
recovered := fun _ _ => False;
|})
incX
vars.recover vars.abstr.
Proof.
(* Pretty much every proof in our lab infrastructure
starts by expanding out the procedure you're proving.
*)
unfold incX.
(* We will now consider each step of this code in turn,
using the step_proc Ltac tactic. step_proc will
take the first thing our code is doing (here, calling
vars.read VarX) and find a corresponding proven spec
for that code (here, it comes from vars.read_ok,
which states that vars.read satisfies read_spec).
*)
step_proc.
(* We will potentially have to prove several subgoals after
calling step_proc: that our precondition is sufficient to
invoke the proven spec that step_proc found, that the
recovery condition matches up, and that the rest of the
procedure will be correct. Here, most of these were trivial
and were proven automatically, so we are left with just one
goal.
This remaining goal requires us to prove something about
executing the rest of the incX code. To help orient yourself
in this proof ("what is this that I'm having to prove?"), look
at Lexec in your proof context. It's a marker telling you
that the current proof goal is what's left "after" running
(vars.read VarX).
This time, to illustrate what step_proc is doing, we break
it apart into several steps. This first invocation does
everything step_proc does except solve trivial goals. *)
step_proc_basic; simplify; split_all; simplify.
(* We are now left with several sub-goals that we have
to prove. To orient yourself among these subgoals,
look for Marker hypotheses in the context. In this
first subgoal, you should see L as a marker indicating
that we are looking at a subgoal related to the precondition
of vars.write VarX 2. The precondition happens to be
just True, so it would have been automatically solved
if you had invoked the full step_proc tactic.
*)
{
(* To solve this subgoal, we must use curly braces
(or dashes), in order to keep the proof script
clean and readable.
Since the goal is True, we can solve it using the
trivial tactic. eauto would have also worked.
*)
trivial.
}
{
(* The next subgoal left for us by step_proc_basic
relates to the recovery condition of vars.write VarX 2,
which you can determine by looking at the bottom-most
Marker in the current context.
Since we aren't reasoning about crashes in this lab,
every recovered condition is False. You can prove
this using the recovered condition of the vars.read VarX operation, which is H: False.
assumption does this, as would eauto.
*)
assumption.
}
(* Now we are left with the remaining part of the incX code:
the call to Ret tt.
You can see there are two markers in your proof context now,
since these markers simply accumulate. The bottom marker
will always be the most recent one, so here Lexec0 tells
you that you're looking at the proof state just after
calling vars.write VarX (StateX {| ... |} + 1).
What's left for us to prove? The goal is saying something
pretty simple: if the starting state is (2, 2, 3), as
specified by the precondition, then running Ret tt gives
us a state of (2, 2, 3).
Invoking step_proc proves this simple fact. *)
step_proc.
Qed.
Now your job is to state a more general specification for incX. It
should take a precondition of True, as stated in the theorem below. Your
task is to fill in the postcondition below so that it applies to any
starting state, unlike the previous theorem that only worked for mkState
1 2 3. Then, prove your general specification correct.
Replace Admitted. with Qed. when the proof is complete. When we grade,
we basically check that your code compiles and that you didn't use
Admitted.
Take a look at the specifications in ThreeVariablesAPI.v if you're not
sure where to start.
Theorem incX_ok :
proc_spec (fun (_:unit) state => {|
pre := True;
post := fun r state' =>
(* EXERCISE: replace with your postcondition *)
state' = state;
recovered := fun _ _ => False;
|})
incX
vars.recover vars.abstr.
Proof.
(* EXERCISE: Fill in your proof here. *)
Admitted.
You have proven that the code of incX satisfies your specification
in incX_ok, but how do you know if your specification is good enough?
Did your specification omit any important details?
One general approach is to try to use your specification to prove something.
Let's do that now: prove that your specification above is sufficient
to prove that running incX from state (5, 5, 5) gives us state (6, 5, 5).
To use your incX_ok theorem from above, we tell the step_proc tactic
about it by including it in the core hints database. To make sure that
we don't accidentally look inside the code of incX, and thus force ourselves
to prove statements about incX by relying on incX_ok, we mark it opaque
using Opaque incX.
Hint Resolve incX_ok : core.
Opaque incX.
Theorem incX_ok_seems_good :
proc_spec (fun (_:unit) state => {|
pre :=
state = mkState 5 5 5;
post := fun r state' =>
state' = mkState 6 5 5;
recovered := fun _ _ => False;
|})
incX
vars.recover vars.abstr.
Proof.
(* EXERCISE: Fill in your proof here. Most likely step_proc. will be
enough to prove the entire theorem, if your specification is correct. *)
Admitted.
Swapping.
Definition swapXY : proc unit :=
(* EXERCISE: Fill in your code here. *)
Ret tt.
Theorem swapXY_ok :
(* EXERCISE: Fill in your spec here. *)
True.
Proof.
(* EXERCISE: Fill in your proof here. *)
Admitted.
As a sanity check, prove that swapXY from state (2, 3, 4)
gives you state (3, 2, 4).
Hint Resolve swapXY_ok : core.
Opaque swapXY.
Theorem swapXY_ok_seems_good :
proc_spec (fun (_:unit) state => {|
pre := state = mkState 2 3 4;
post := fun r state' => state' = mkState 3 2 4;
recovered := fun _ state' => False;
|})
swapXY
vars.recover
vars.abstr.
Proof.
(* EXERCISE: Fill in your proof here. *)
Admitted.
Part B: recursive procedures.
Definition addX (delta : nat) : proc unit :=
x <- vars.read VarX;
_ <- vars.write VarX (x + delta);
Ret tt.
Theorem addX_ok : forall delta,
proc_spec (fun (_:unit) state => {|
pre := True;
post := fun r state' => state' = mkState (delta + StateX state) (StateY state) (StateZ state);
recovered := fun _ _ => False;
|})
(addX delta)
vars.recover
vars.abstr.
Proof.
intros.
unfold addX.
step_proc.
step_proc.
step_proc.
replace (StateX state + delta) with (delta + StateX state).
- reflexivity.
- lia.
Qed.
Hint Resolve addX_ok : core.
Opaque addX.
Now, we define a procedure addX_list that takes a list of nats,
and calls addX on each of them in turn.
Fixpoint addX_list (l : list nat) : proc unit :=
match l with
| nil =>
Ret tt
| v :: l' =>
_ <- addX v;
_ <- addX_list l';
Ret tt
end.
To specify what we expect addX_list to do, we need to talk about
the sum of values in a list. To do that, we define a Gallina function
(purely in Coq) that computes the sum of values in a list.
Fixpoint list_sum (l : list nat) : nat :=
match l with
| nil => 0
| v :: l' => v + (list_sum l')
end.
Now we can state a specification about addX_list and prove it using
induction.
Note: You get an error "Expected a single focused goal but 2 goals are
focused". For the labs, we turned on an option in Coq that enforces proofs
use bullets (+, -, *) or curly braces { } to "focus" each line of
the proof on a single goal. This makes proof scripts more manageable,
which will help in later labs.
If you get this error, make sure to use bullets or braces, the way you saw
throughout Software Foundations.
Theorem addX_list_ok : forall l,
proc_spec (fun (_:unit) state => {|
pre := True;
post := fun r state' =>
state' = mkState (StateX state + list_sum l) (StateY state) (StateZ state);
recovered := fun _ _ => False;
|})
(addX_list l)
vars.recover
vars.abstr.
Proof.
induction l.
- (* The base case: adding an empty list. *)
simpl.
(* EXERCISE: Finish the proof. *)
admit.
- (* The inductive case: adding one more element. *)
simpl.
(* We first consider the addX v call in the second case of addX_list's match. *)
step_proc.
(* You can see that this is indeed where we are in the proof,
by observing the Lexec marker: it says we're just after
executing addX a. *)
simpl.
(* Now we need to run the rest of addX_list l. This works
out surprisingly easily, because the induction l tactic
gave us an inductive hypothesis, IHl, that gives us a
specification for addX_list l. step_proc will use this
hypothesis if it matches the invocation. *)
step_proc.
(* Finally, we have to prove the state matches the postcondition
after running both addX and addX_list l. *)
(* EXERCISE: Finish the proof. *)
admit.
Admitted.
Recursive procedures on your own.
Search Nat.even.
Search Nat.odd.
(* EXERCISE: Fill in your code, spec, and proof here. *)
Fixpoint add_odds_evens (l : list nat) : proc unit := Ret tt.
To check your solution, prove this simple theorem about
a particular execution of add_odds_evens using your spec.
Add a Hint Resolve statement to let step_proc see your
proven specification about add_odds_evens.
Opaque add_odds_evens.
Theorem add_odds_evens_ok_seems_good :
proc_spec (fun (_:unit) state => {|
pre := state = mkState 0 0 0;
post := fun r state' => state' = mkState 6 7 0;
recovered := fun _ _ => False;
|})
(add_odds_evens (2 :: 3 :: 3 :: 4 :: 1 :: nil))
vars.recover
vars.abstr.
Proof.
(* EXERCISE: Prove this specification. *)
Admitted.
End Exercises.