exercises.lang
From iris.heap_lang Require Import lang notation spawn par.
(* ################################ *)
(* ################################ *)
(* ================================================================= *)
Introduction
(* ================================================================= *)
The HeapLang Interpreter (Optional)
opam install coq-iris-unstable
(* From iris.unstable.heap_lang Require Import interpreter. *)
To return to a release version of Iris known to be compatible with the
rest of the tutorial, run:
This also uninstalls the interpreter.
opam install . --deps-only
(* ================================================================= *)
HeapLang has native support for integers and booleans. With these, we
can do basic arithmetic and control flow.
Note that values in HeapLang are prefixed by a #.
If the interpreter was installed, the expression can now be executed
using (exec 10 arith), where 10 is the amount of fuel. To evaluate
the execution inside Coq, we can use the Compute command. Uncomment
the command below to see this in action.
(* Compute (exec 10 arith). *)
Evaluates to inl #7
Example booleans : expr :=
(arith = #7) && #true || (#true = #false).
(* Compute (exec 10 booleans). *)
Evaluates to inl #true
Example if_then_else : expr :=
if: booleans then #() else #false.
(* Compute (exec 10 if_then_else). *)
Evaluates to inl #()
Heaplang supports let expressions. Technically, let expressions are
not native to HeapLang. We get let expressions from the notation
package, which defines them in terms of lambda abstractions.
Note that variables in HeapLang are strings.
Evaluates to inl #6
HeapLang has native support for pairs, with tuples being notation for
nested pairs.
Example pairs : expr :=
let: "p" := (#40, #1 + #1) in
Fst "p" + Snd "p".
(* Compute (exec 10 pairs). *)
Evaluates to inl #42
Example tuples : expr :=
let: "t1" := (#1, #2, #3, #4) in
let: "t2" := (((#1, #2), #3), #4) in
(Snd (Fst (Fst "t1")) = Snd (Fst (Fst "t2"))).
(* Compute (exec 10 tuples). *)
Evaluates to inl #true
We can also do pattern matching using sums. A common use case of sums
is the `option' construction. The notation package has us covered
here as well.
Example sums : expr :=
let: "r" := InjR #1 in
match: "r" with
InjL "_" => #0
| InjR "n" => "n" + #1
end.
(* Compute (exec 10 sums). *)
Evaluates to inl #2
Example option : expr :=
let: "r" := SOME #1 in
match: "r" with
NONE => #0
| SOME "n" => "n" + #1
end.
(* Compute (exec 10 option). *)
Evaluates to inl #2
Finally, we have lambda abstractions and recursive functions. As with
let expressions, lambda abstractions are also a derived construct –
they are recursive functions that do not recurse. In HeapLang,
functions are first-class citizens, which gives support for
higher-order functions.
Example lambda : expr :=
let: "add5" := (λ: "x", "x" + #5) in
let: "double" := (λ: "x", "x" * #2) in
let: "compose" := (λ: "f" "g", (λ: "x", "g" ("f" "x"))) in
("compose" "add5" "double") #5.
(* Compute (exec 10 lambda). *)
Evaluates to inl #20
Example recursion : expr :=
let: "fac" :=
rec: "f" "n" := if: "n" = #0 then #1 else "n" * "f" ("n" - #1)
in
("fac" #4, "fac" #5).
(* Compute (exec 25 recursion). *)
Evaluates to inl (#24, #120)
(* ================================================================= *)
References
Example alloc : expr :=
let: "l1" := ref (#0) in
let: "l2" := ref (#0) in
("l1", "l2").
(* Compute (exec 10 alloc). *)
Evaluates to inl (#(Loc 1), #(Loc 2))
After allocation, we can read and update the value at the returned
location l with !l and l <- v, respectively.
Evaluates to inl #5
Evaluates to inl #6
To allow for synchronisation between threads, HeapLang provides a
single primitive called compare-and-exchange, written
CmpXchg l v1 v2. This instruction atomically reads the contents of
location l, checks if it is equal to v1, and, in case of equality,
updates l to contain v2. The instruction returns a pair (v, b),
with v being the original value stored at l, and b a boolean
indicating whether the location was updated.
Example cmpxchg_fail : expr :=
let: "l" := ref #5 in
CmpXchg "l" #6 #7.
(* Compute (exec 10 cmpxchg_fail). *)
Example cmpxchg_suc : expr :=
let: "l" := ref #5 in
CmpXchg "l" #5 #7.
(* Compute (exec 10 cmpxchg_suc). *)
Evaluates to inl (#5, #true)
The notation package defines a variant of CmpXchg called
compare-and-set, written CAS l v1 v2. The only difference is that
CAS only returns the boolean b.
Example cas : expr :=
let: "l" := ref #5 in
if: CAS "l" #6 #7 then
#()
else
let: "a" := !"l" in
if: CAS "l" #5 #7 then
let: "b" := !"l" in
("a", "b")
else
#().
(* Compute (exec 10 cas). *)
Evaluates to inl (#5, #7)
(* ================================================================= *)
Concurrency
Unfortunately, in its current state, the HeapLang interpreter does not
support concurrency; the forked thread never executes its expression.
Hence, the above program will always return 5. Of course, this is
only a limitation of this specific interpreter – HeapLang is still a
concurrent programming language, and we still have to reason about
forked threads inside the Iris logic.
(* Compute (exec 10 fork). *)
Evaluates to inl #5
From the Fork primitive, we can implement several other
constructions for concurrency. HeapLang ships with two such
constructions, spawn and par, which we will implement and prove
correct later.
spawn takes a thunked expression and creates a new thread which
executes said expression. Additionally, spawn returns a handle,
which we can use in conjunction with join to wait for the result of
the computation.
Example spawn : expr :=
let: "l" := ref #5 in
let: "handle" := spawn (λ: "_", "l" <- #6;; #2) in
let: "res" := spawn.join "handle" in
let: "v" := !"l" in
("res", "v").
(* Evaluates to (2, 6). *)
let: "l" := ref #5 in
let: "handle" := spawn (λ: "_", "l" <- #6;; #2) in
let: "res" := spawn.join "handle" in
let: "v" := !"l" in
("res", "v").
(* Evaluates to (2, 6). *)