From e5f526c3ade36a2b53c459965617dc12ca85b00f Mon Sep 17 00:00:00 2001
From: Tej Chajed <tchajed@mit.edu>
Date: Thu, 18 Mar 2021 08:25:24 +0000
Subject: [PATCH] Add a verified interpreter for HeapLang

---
 _CoqProject                          |    3 +
 coq-iris-staging.opam                |    1 +
 iris_heap_lang/locations.v           |    8 +-
 iris_heap_lang/pretty.v              |   65 ++
 iris_staging/heap_lang/interpreter.v | 1004 ++++++++++++++++++++++++++
 tests/heap_lang_interpreter.ref      |   44 ++
 tests/heap_lang_interpreter.v        |   66 ++
 7 files changed, 1188 insertions(+), 3 deletions(-)
 create mode 100644 iris_heap_lang/pretty.v
 create mode 100644 iris_staging/heap_lang/interpreter.v
 create mode 100644 tests/heap_lang_interpreter.ref
 create mode 100644 tests/heap_lang_interpreter.v

diff --git a/_CoqProject b/_CoqProject
index 1278ab0df..9904cd695 100644
--- a/_CoqProject
+++ b/_CoqProject
@@ -142,6 +142,7 @@ iris/proofmode/modality_instances.v
 iris_heap_lang/locations.v
 iris_heap_lang/lang.v
 iris_heap_lang/class_instances.v
+iris_heap_lang/pretty.v
 iris_heap_lang/metatheory.v
 iris_heap_lang/tactics.v
 iris_heap_lang/primitive_laws.v
@@ -174,3 +175,5 @@ iris_deprecated/base_logic/auth.v
 iris_deprecated/base_logic/sts.v
 iris_deprecated/base_logic/viewshifts.v
 iris_deprecated/program_logic/hoare.v
+
+iris_staging/heap_lang/interpreter.v
diff --git a/coq-iris-staging.opam b/coq-iris-staging.opam
index 49bff7b51..75cf7add4 100644
--- a/coq-iris-staging.opam
+++ b/coq-iris-staging.opam
@@ -14,6 +14,7 @@ work is needed before they are ready for this.
 
 depends: [
   "coq-iris" {= version}
+  "coq-iris-heap-lang" {= version}
 ]
 
 build: ["./make-package" "iris_staging" "-j%{jobs}%"]
diff --git a/iris_heap_lang/locations.v b/iris_heap_lang/locations.v
index 26c483e65..aae2090d1 100644
--- a/iris_heap_lang/locations.v
+++ b/iris_heap_lang/locations.v
@@ -2,15 +2,17 @@ From stdpp Require Import countable numbers gmap.
 From iris.prelude Require Export prelude.
 From iris.prelude Require Import options.
 
-Record loc := { loc_car : Z }.
+Record loc := Loc { loc_car : Z }.
+
+Add Printing Constructor loc.
 
 Global Instance loc_eq_decision : EqDecision loc.
-Proof. solve_decision. Qed.
+Proof. solve_decision. Defined.
 
 Global Instance loc_inhabited : Inhabited loc := populate {|loc_car := 0 |}.
 
 Global Instance loc_countable : Countable loc.
-Proof. by apply (inj_countable' loc_car (λ i, {| loc_car := i |})); intros []. Qed.
+Proof. by apply (inj_countable' loc_car Loc); intros []. Defined.
 
 Program Instance loc_infinite : Infinite loc :=
   inj_infinite (λ p, {| loc_car := p |}) (λ l, Some (loc_car l)) _.
diff --git a/iris_heap_lang/pretty.v b/iris_heap_lang/pretty.v
new file mode 100644
index 000000000..42e1fcd23
--- /dev/null
+++ b/iris_heap_lang/pretty.v
@@ -0,0 +1,65 @@
+From stdpp Require Export pretty.
+
+From iris.heap_lang Require Import lang.
+From iris.prelude Require Import options.
+
+(** * Pretty printing for HeapLang values *)
+
+Global Instance pretty_loc : Pretty loc :=
+  λ l, pretty l.(loc_car).
+
+Global Instance pretty_base_lit : Pretty base_lit :=
+  λ l, match l with
+       | LitInt z => pretty z
+       | LitBool b => if b then "true" else "false"
+       | LitUnit => "()"
+       | LitPoison => "<poison>"
+       | LitLoc l => "(loc " +:+ pretty l +:+ ")"
+       | LitProphecy i => "(prophecy " +:+ pretty i +:+ ")"
+       end.
+
+Global Instance pretty_binder : Pretty binder :=
+  λ b, match b with
+       | BNamed x => x
+       | BAnon => "<>"
+       end.
+
+(** Note that this instance does not print function bodies and is thus not
+injective (unlike most `pretty` instances). *)
+Global Instance pretty_val : Pretty val :=
+  fix go v :=
+    match v with
+    | LitV l => "#" +:+ pretty l
+    | RecV f x e =>
+      match f with
+      | BNamed f => "rec: " +:+ f +:+ " " +:+ pretty x +:+ " := <body>"
+      | BAnon => "λ: " +:+ pretty x +:+ ", <body>"
+      end
+    | PairV v1 v2 => "(" +:+ go v1 +:+ ", " +:+ go v2 +:+ ")"
+    | InjLV v => "inl (" +:+ go v +:+ ")"
+    | InjRV v => "inr (" +:+ go v +:+ ")"
+    end.
+
+Global Instance pretty_un_op : Pretty un_op :=
+  λ op, match op with
+        | NegOp => "~"
+        | MinusUnOp => "-"
+        end.
+
+Global Instance pretty_bin_op : Pretty bin_op :=
+  λ op, match op with
+        | PlusOp => "+"
+        | MinusOp => "-"
+        | MultOp => "*"
+        | QuotOp => "`quot`"
+        | RemOp => "`rem`"
+        | AndOp => "&"
+        | OrOp => "|"
+        | XorOp => "`xor`"
+        | ShiftLOp => "<<"
+        | ShiftROp => ">>"
+        | LeOp => "≤"
+        | LtOp => "<"
+        | EqOp => "="
+        | OffsetOp => "+â‚—"
+        end .
diff --git a/iris_staging/heap_lang/interpreter.v b/iris_staging/heap_lang/interpreter.v
new file mode 100644
index 000000000..87c30ea58
--- /dev/null
+++ b/iris_staging/heap_lang/interpreter.v
@@ -0,0 +1,1004 @@
+(* This file is still experimental. See its tracking issue
+https://gitlab.mpi-sws.org/iris/iris/-/issues/405 for details on remaining
+issues before stabilization. *)
+
+(** A verified interpreter for HeapLang.
+
+    This file defines a function [exec (fuel:nat) (e:expr) : val + Error] which
+    runs a HeapLang expression to [inl v] if [e] terminates in a value [v], or
+    returns [inr msg] with a structured error message [msg] if [e] gets stuck at
+    some point. Use [pretty msg] to turn the message into a readable string.
+
+    The point of this interpreter is to allow you to test your code or small
+    snippets of HeapLang code and see what the semantics does. We prove it
+    correct so that you can trust that the interpreter actually reflects the
+    semantics, particularly when it says the program is stuck. The interpreter
+    also goes through some pain to report specific error messages on failure,
+    although these explanations are of course not verified.
+
+    We prove a correctness theorem [exec_spec] about [exec] summarizing its
+    guarantees. It distinguishes two cases:
+    1. If [exec] returns [inl v], then [e] can execute to [v] according to [rtc
+    erased_step] (following the semantics of HeapLang).
+    2. If [exec] returns [inr (Stuck msg)], then [e] can execute to some [e']
+    that is stuck according to the HeapLang semantics, so [e] really does "go
+    wrong". [msg] is a human-readable string describing how [e] got stuck.
+    3. Finally, [exec] can also fail due to running out of fuel or encountering
+    an unsupported prophecy variable operation, in which case it returns a
+    distinct error case and the correctness theorem provides no guarantees.
+
+    The interpreter is _sequential_ and _deterministic_, which means it has some
+    limitations. It will ignore forked threads and continue to execute the main
+    thread, which may cause some programs to live-lock that would otherwise make
+    progress under a fair scheduler.
+
+    Determinism creates a subtle difference between the interpreter and the
+    semantics. The interpreter only guarantees properties of one execution while
+    the semantics and any safety property proven using Iris conceptually regard
+    all executions. Concretely, consider this program: [let: "x" := ref #0 in
+    !(LitLoc 1)]. There is one execution where this program terminates in [#0],
+    and many where the allocation results in some other location and it is
+    stuck. The interpeter happens to allocate starting at [LitLoc 1] and will
+    say it produces [#0]. This is technically correct but not useful - there is
+    a stuck execution the interpreter didn't find. The only non-determinism in
+    sequential HeapLang is allocation, so we believe only strange programs like
+    this that correctly "guess" the interpreter's allocations are affected.
+
+    The interpreter is heavily based on Sydney Gibson's MEng thesis:
+    https://pdos.csail.mit.edu/papers/gibsons-meng.pdf. That thesis includes an
+    interpreter for sequential GooseLang, a fork of HeapLang.
+
+*)
+
+From iris.heap_lang Require Export lang.
+From iris.heap_lang Require Import tactics pretty.
+From iris.prelude Require Import options.
+
+Local Ltac invc H := inversion H; subst; clear H.
+
+(** Errors are tagged to give [exec] a stronger specification. [Stuck s] is
+    distinguished from the other cases because it comes with a proof that the
+    expression is eventually stuck. *)
+Inductive Error :=
+| Stuck (s:string)
+| Unsupported (s:string)
+| OutOfFuel.
+
+Global Instance error_pretty : Pretty Error :=
+  λ err, match err with
+         | Stuck s => "stuck: " +:+ s
+         | Unsupported s => "unsupported operation: " +:+ s
+         | OutOfFuel => "out of fuel"
+         end.
+
+Module interp_monad.
+  Record interp_state :=
+    InterpState { lang_state : state; next_loc : Z; forked_threads : list expr; }.
+
+  Add Printing Constructor interp_state.
+
+  Definition modify_lang_state (f: state → state): interp_state → interp_state :=
+    λ s, InterpState (f s.(lang_state)) s.(next_loc) (s.(forked_threads)).
+  Definition add_forked_thread (e: expr) : interp_state → interp_state :=
+    λ s, InterpState s.(lang_state) s.(next_loc) (s.(forked_threads) ++ [e]).
+  Definition interp_state_alloc (n: Z) : interp_state → interp_state :=
+    λ s, InterpState s.(lang_state) (n + s.(next_loc)) s.(forked_threads).
+
+  Inductive state_wf (s: interp_state): Prop :=
+    { state_wf_holds (l: loc) : (s.(next_loc) ≤ l.(loc_car))%Z →
+                                s.(lang_state).(heap) !! l = None; }.
+
+  Definition InterpretM (A:Type) : Type :=
+    interp_state → (A+Error) * interp_state.
+
+  Definition init_state : state := {| heap := ∅; used_proph_id := ∅ |}.
+  Definition init_interp_state : interp_state :=
+    InterpState init_state 1 [].
+
+  (** [run] runs an interpreter monad value starting from an empty initial
+      state. *)
+  Local Definition run {A} (f: InterpretM A) : A + Error :=
+    (f init_interp_state).1.
+
+  Lemma init_interp_state_wf : state_wf init_interp_state.
+  Proof. constructor; rewrite /init_interp_state //=. Qed.
+
+  (* basic monad *)
+  Global Instance interp_ret : MRet InterpretM := λ A (x:A), λ s, (inl x, s).
+  Global Instance interp_bind : MBind InterpretM :=
+    λ A B (f: A → InterpretM B) (x: InterpretM A),
+    λ s, let (r, s') := x s in
+        match r with
+        | inl x' => f x' s'
+        | inr e => (inr e, s')
+        end.
+  Global Instance interp_fmap : FMap InterpretM :=
+    λ A B (f: A → B) (x: InterpretM A),
+    λ s, let (r, s') := x s in
+        match r with
+        | inl x' => (inl (f x'), s')
+        | inr e => (inr e, s')
+        end.
+
+  (* state+error-specific monadic constants *)
+  Definition interp_modify (f: interp_state → interp_state): InterpretM () :=
+    λ s, (inl (), f s).
+  Definition interp_modify_state (f: state → state): InterpretM () :=
+    interp_modify (modify_lang_state f).
+  Definition interp_read {A} (f: state → A): InterpretM A :=
+    λ s, (inl (f s.(lang_state)), s).
+  Definition interp_error {A} (msg: string) : InterpretM A := λ s, (inr (Stuck msg), s).
+  Definition interp_alloc (n:Z): InterpretM loc :=
+    λ s, (inl {| loc_car := s.(next_loc)|}, interp_state_alloc n s).
+
+  Definition read_loc (method: string) (vl: val) : InterpretM (loc*val) :=
+      match vl with
+      | LitV (LitLoc l) =>
+        mv ← interp_read (λ σ, σ.(heap) !! l);
+        match mv with
+        | Some (Some v) => mret (l, v)
+        | Some None =>
+          interp_error $ method +:+ ": use after free at location: " +:+ pretty l
+        | None =>
+          interp_error $ method +:+ ": unallocated location: " +:+ pretty l
+        end
+      | _ => interp_error $ method +:+ ": applied to non-loc " +:+ pretty vl
+      end.
+
+  Lemma error_not_inl {A} {msg s} {v: A} {s'} :
+    interp_error msg s = (inl v, s') → False.
+  Proof. by inversion 1. Qed.
+
+  Lemma mret_inv {A} (v: A) s v' s' :
+    mret v s = (inl v', s') → v = v' ∧ s = s'.
+  Proof. by inversion 1. Qed.
+
+  Lemma interp_bind_inv A B (x: InterpretM A) (f: A → InterpretM B) r s s' :
+    (x ≫= f) s = (r, s') →
+    (∃ e, x s = (inr e, s') ∧ r = inr e) ∨
+    (∃ s0 x', x s = (inl x', s0) ∧
+              f x' s0 = (r, s')).
+  Proof.
+    rewrite /mbind /interp_bind.
+    repeat case_match; inversion 1; subst; eauto.
+  Qed.
+
+  Lemma interp_bind_inl_inv A B (x: InterpretM A) (f: A → InterpretM B) (r: B) s s' :
+    (x ≫= f) s = (inl r, s') →
+    ∃ s0 x', x s = (inl x', s0) ∧
+             f x' s0 = (inl r, s').
+  Proof.
+    intros [(e & ? & ?) | (s0 & x' & H1 & H2)]%interp_bind_inv.
+    - congruence.
+    - rewrite H1.
+      eexists _, _; eauto.
+  Qed.
+
+  Lemma interp_fmap_inv {A B} (f: A → B) x s v s' :
+    (f <$> x) s = (inl v, s') →
+    ∃ v0, v = f v0 ∧ x s = (inl v0, s').
+  Proof.
+    rewrite /fmap /interp_fmap.
+    repeat case_match; inversion 1; subst; eauto.
+  Qed.
+
+  Lemma read_loc_inv method vl s l v s' :
+    read_loc method vl s = (inl (l, v), s') →
+    vl = LitV (LitLoc l) ∧
+    s' = s ∧
+    s.(lang_state).(heap) !! l = Some (Some v).
+  Proof.
+    rewrite /read_loc.
+    destruct vl as [l' | | | | ]; try by inversion 1.
+    destruct l' as [| | | | l' |]; intro H; try by inversion H.
+    apply interp_bind_inl_inv in H as (s0 & mv & Heq1 & Heq2).
+    destruct mv as [mv|]; try by inversion Heq2.
+    destruct mv; inversion Heq2; subst; clear Heq2.
+    inversion Heq1; subst; clear Heq1.
+    eauto.
+  Qed.
+
+  Ltac errored :=
+    lazymatch goal with
+    | H: interp_error _ _ = (inl _, _) |- _ => solve [ exfalso; apply (error_not_inl H) ]
+    | H: (inr _, _) = (inl _, _) |- _ => solve [ exfalso; inversion H ]
+    end.
+
+  Ltac success :=
+    repeat
+      lazymatch goal with
+      | H: mret _ _ = (inl _, _) |- _ =>
+        let Heqv := fresh "Heqv" in
+        let Heqs := fresh "Heqs" in
+        apply mret_inv in H as [Heqv Heqs]; subst
+      | H: (_ ≫= (λ x, _)) _ = (inl _, _) |- _ =>
+        let s := fresh "s" in
+        let x := fresh x in
+        let Heq1 := fresh "Heq" in
+        let Heq2 := fresh "Heq" in
+        apply interp_bind_inl_inv in H as (s & x & Heq1 & Heq2); subst
+      | H: (_ <$> _) _ = (inl ?v, _) |- _ =>
+        let s := fresh "s" in
+        let v_tmp := fresh "v" in
+        rename v into v_tmp;
+        apply interp_fmap_inv in H as (v & -> & H)
+      | H: interp_modify _ _ = (inl _, _) |- _ => invc H
+      | H: interp_modify_state _ _ = (inl _, _) |- _ => invc H
+      | H: interp_read _ _ = (inl _, _) |- _ => invc H
+      | H: read_loc _ _ _ = (inl _, _) |- _ =>
+        apply read_loc_inv in H as (-> & -> & H)
+      end; subst.
+
+  Lemma interp_bind_inr_inv {A B} (x: InterpretM A) (f: A → InterpretM B) r s s' :
+    (x ≫= f) s = (inr r, s') →
+    (x s = (inr r, s')) ∨
+    (∃ s0 x', x s = (inl x', s0) ∧ f x' s0 = (inr r, s')).
+  Proof.
+    rewrite /mbind /interp_bind.
+    repeat case_match; intros; simplify_eq/=; eauto.
+  Qed.
+
+  Lemma interp_fmap_inr_inv {A B} (f: A → B) (x: InterpretM A) s e s' :
+    (f <$> x) s = (inr e, s') →
+    x s = (inr e, s').
+  Proof.
+    rewrite /fmap /interp_fmap.
+    repeat case_match; intros; simplify_eq/=; auto.
+  Qed.
+
+  Lemma read_loc_inr_inv method vl s err s' :
+    read_loc method vl s = (inr err, s') →
+    s = s' ∧ match vl with
+             | LitV (LitLoc l) => ∀ v, s.(lang_state).(heap) !! l ≠ Some (Some v)
+             | _ => True
+             end.
+  Proof.
+    rewrite /read_loc.
+    repeat case_match; subst; try solve [ inversion 1; subst; auto ].
+    intros H.
+    apply interp_bind_inr_inv in H as [H|(s0&x& Hexec1 & Hexec2)]; success.
+    - invc H.
+    - repeat case_match; invc Hexec2.
+      + intuition congruence.
+      + intuition congruence.
+  Qed.
+
+  Ltac failure :=
+    repeat
+      match goal with
+      | H: (_ ≫= _) _ = (inr _, _) |- _ =>
+        let s := fresh "s" in
+        let x := fresh "x" in
+        let Heq := fresh "Heq" in
+        apply interp_bind_inr_inv in H as [H | (s & x & Heq & H)]
+      | H: interp_error _ _ = (inr _, _) |- _ => invc H
+      | H: mret _ _ = (inr _, _) |- _ => solve [ inversion H ]
+      | H: interp_modify _ _ = (inr _, _) |- _ => solve [ inversion H ]
+      | H: interp_modify_state _ _ = (inr _, _) |- _ => solve [ inversion H ]
+      | H: (_ <$> _) _ = (inr _, _) |- _ =>
+        apply interp_fmap_inr_inv in H
+      | H: read_loc _ _ _ = (inr _, _) |- _ =>
+        apply read_loc_inr_inv in H as [-> H]
+      end; subst.
+
+End interp_monad.
+
+Import interp_monad.
+
+Section interpreter.
+
+  (* to make the below definition work with strings as well we add an
+  instance of [Pretty string] *)
+  Local Instance pretty_string : Pretty string := λ s, s.
+  Local Definition pretty_app (s: string) {A} `{Pretty A} (x:A) : string :=
+    s +:+ pretty x.
+
+  Infix "+" := pretty_app.
+
+  (* We explain errors which in the semantics are represented by a pure function
+     returning None; to sanity-check these definitions, we prove they cover
+     exactly the cases where the underlying operation returns None. *)
+
+  Definition option_opposites {A B} (m1 : option A) (m2 : option B) :=
+    is_Some m1 ↔ m2 = None.
+
+  Lemma option_opposites_alt {A B} (m1 : option A) (m2 : option B) :
+    option_opposites m1 m2 ↔ match m1, m2 with
+                             | Some _, None   => True
+                             | None  , Some _ => True
+                             | _     , _      => False
+                             end.
+  Proof.
+    rewrite /option_opposites is_Some_alt.
+    repeat case_match; intuition congruence.
+  Qed.
+
+  (** produce an error message for [un_op_eval] *)
+  Definition explain_un_op_fail op v : option string :=
+    match op with
+    | NegOp => match v with
+               | LitV (LitInt _) => None
+               | LitV (LitBool _) => None
+               | _ => Some $ "~ (NegOp) can only be applied to integers and booleans, got " + v
+               end
+    | MinusUnOp =>
+      match v with
+      | LitV (LitInt _) => None
+      | _ => Some $ "unary - (MinusUnOp) can only be applied to integers, got " + v
+      end
+    end.
+
+  Lemma explain_un_op_fail_wf op v :
+    option_opposites (explain_un_op_fail op v) (un_op_eval op v).
+  Proof.
+    apply option_opposites_alt.
+    rewrite /explain_un_op_fail /un_op_eval.
+    repeat case_match; simplify_eq/=; auto.
+  Qed.
+
+  Definition explain_unboxed v : option string :=
+    match v with
+    | LitV l | InjLV (LitV l) | InjRV (LitV l) =>
+      match l with
+      | LitPoison => Some "poison values (from erasing prophecies) cannot be compared"
+      | LitProphecy _ => Some "prophecies cannot be compared"
+      | _ => None
+      end
+    | InjLV _ | InjRV _ => Some "sum values can only be compared if they contain literals"
+    | PairV _ _ => Some "pairs are large and considered boxed, must compare by field"
+    | RecV _ _ _ => Some "closures are large and cannot be compared"
+    end.
+
+  Lemma explain_unboxed_wf v :
+    match explain_unboxed v with
+    | Some _ => ~val_is_unboxed v
+    | None   => val_is_unboxed v
+    end.
+  Proof.
+    rewrite /explain_unboxed /val_is_unboxed /lit_is_unboxed.
+    repeat case_match; intuition congruence.
+  Qed.
+
+  Definition explain_vals_compare_safe_fail v1 v2 : option string :=
+    match explain_unboxed v1, explain_unboxed v2 with
+    | Some msg1, Some msg2 =>
+      Some $ "one of " + v1 + " and " + v2 + " must be unboxed to compare: "
+      + v1 + ": " + msg1 + ", "
+      + v2 + ": " + msg2
+    | _, _ => None
+    end.
+
+  (** [explain_vals_compare_safe_fail] gives an explanation when
+  [vals_compare_safe] would be false (that is, when v1 and v2 cannot be
+  compared) *)
+  Lemma explain_vals_compare_safe_fail_wf v1 v2 :
+    is_Some (explain_vals_compare_safe_fail v1 v2) ↔ ~vals_compare_safe v1 v2.
+  Proof.
+    cut (explain_vals_compare_safe_fail v1 v2 = None ↔ vals_compare_safe v1 v2).
+    { rewrite is_Some_alt.
+      destruct (explain_vals_compare_safe_fail _ _); intuition congruence. }
+    rewrite /explain_vals_compare_safe_fail /vals_compare_safe.
+    pose proof (explain_unboxed_wf v1).
+    pose proof (explain_unboxed_wf v2).
+    destruct (explain_unboxed v1), (explain_unboxed v2); intuition congruence.
+  Qed.
+
+  (** produce an error message for [bin_op_eval] *)
+  Definition explain_bin_op_fail op v1 v2 : option string :=
+    if decide (op = EqOp)
+    then (explain_vals_compare_safe_fail v1 v2)
+    else match v1, v2 with
+         | LitV (LitInt _), LitV (LitInt _) =>
+           match op with
+           | OffsetOp => Some $ "cannot add to integer " + v1 + " with +â‚— (only locations)"
+           | _ => None
+           end
+         | LitV (LitBool b1), LitV (LitBool b2) =>
+           match bin_op_eval_bool op b1 b2 with
+           | Some _ => None
+           | None => Some $ "non-boolean operator applied to booleans " + op
+           end
+         | LitV (LitLoc _), _ =>
+           match op, v2 with
+           | OffsetOp, LitV (LitInt _) => None
+           | OffsetOp, _ => Some $ "can only call +â‚— on integers, got " + v2
+           | _, _ => Some $ "the only supported operation on locations is +â‚— #i, got "
+                            + op + " " + v2
+           end
+         | _, _ => Some $ "mismatched types of values " + v1 + " and " + v2
+         end.
+
+  Lemma explain_bin_op_fail_wf op v1 v2 :
+    option_opposites (explain_bin_op_fail op v1 v2) (bin_op_eval op v1 v2).
+  Proof.
+    apply option_opposites_alt.
+    rewrite /explain_bin_op_fail /bin_op_eval
+            /bin_op_eval_int /bin_op_eval_bool /bin_op_eval_loc.
+    repeat (case_match; simplify_eq/=; auto).
+    - pose proof (explain_vals_compare_safe_fail_wf v1 v2).
+      intuition eauto.
+    - pose proof (explain_vals_compare_safe_fail_wf v1 v2) as H.
+      replace (explain_vals_compare_safe_fail v1 v2) in H.
+      rewrite -> is_Some_alt in H.
+      intuition eauto.
+  Qed.
+
+  (* define a shorthand for readability below *)
+  Local Notation error := interp_error.
+
+  Fixpoint interpret (fuel:nat) (e: expr) {struct fuel} : InterpretM val :=
+    match fuel with
+    | 0 => λ s, (inr OutOfFuel, s)
+    | S fuel' => let interp := interpret fuel' in
+    match e with
+
+      (* lambda calculus *)
+    | Val v => mret v
+    | Var x => error $ "free var: " + x
+    | Rec f x e => mret (RecV f x e)
+    | App f e =>
+      v2 ← interp e;
+      f ← interp f;
+      match f with
+      | RecV f x e1 =>
+        interp (subst' x v2 (subst' f (RecV f x e1) e1))
+      | _ => error $ "attempt to call non-function " +:+ pretty f
+      end
+
+    (* mostly boring pure operations (sums, products, unary/binary ops) *)
+    | Pair e1 e2 =>
+      v2 ← interp e2;
+      v1 ← interp e1;
+      mret (PairV v1 v2)
+    | InjL e => InjLV <$> interp e
+    | InjR e => InjRV <$> interp e
+    | UnOp op e =>
+      v ← interp e;
+      match un_op_eval op v with
+      | Some v => mret v
+      | None => error $ "un-op failed: " + match explain_un_op_fail op v with
+                                           | Some msg => msg
+                                           | None => "" (* impossible *)
+                                           end
+      end
+    | BinOp op e1 e2 =>
+      v2 ← interp e2;
+      v1 ← interp e1;
+      match bin_op_eval op v1 v2 with
+      | Some v => mret v
+      | None => error $ "bin-op failed: " + match explain_bin_op_fail op v1 v2 with
+                                            | Some msg => msg
+                                            | None => "" (* impossible *)
+                                            end
+      end
+    | If e e1 e2 =>
+      cond ← interp e;
+      match cond with
+      | LitV (LitBool b) => interp (if b then e1 else e2)
+      | _ => error $ "if: non-bool condition " + cond
+      end
+    | Fst e =>
+      v ← interp e;
+      match v with
+      | PairV v1 _ => mret v1
+      | _ => error $ "fst: called on non-pair " + v
+      end
+    | Snd e =>
+      v ← interp e;
+      match v with
+      | PairV _ v2 => mret v2
+      | _ => error $ "snd: called on non-pair " + v
+      end
+    | Case e e1 e2 =>
+      v ← interp e;
+      match v with
+      | InjLV v => interp (App e1 (Val v))
+      | InjRV v => interp (App e2 (Val v))
+      | _ => error $ "case: called on non-sum " + v
+      end
+
+    | Fork e =>
+      _ ← interp_modify (add_forked_thread e);
+      mret (LitV LitUnit)
+
+    (* heap manipulation *)
+    | AllocN ne e =>
+      v ← interp e;
+      nv ← interp ne;
+      match nv with
+      | LitV (LitInt n) =>
+        if decide (0 < n)%Z then
+          l ← interp_alloc n;
+          _ ← interp_modify_state (state_init_heap l n v);
+          mret (LitV (LitLoc l))
+        else (error $ if decide (n = 0)
+                      then "alloc: cannot allocate 0 elements"
+                      else "alloc: negative number of elements (first argument) " + n)
+      | _ => error $ "alloc: number of elements (first argument) " + nv
+      end
+    | Load e =>
+      vl ← interp e;
+      l_v0 ← read_loc "load" vl;
+      let '(_, v0) := l_v0 in
+      mret v0
+    | Free e =>
+      vl ← interp e;
+      l_v0 ← read_loc "free" vl;
+      let '(l, _) := l_v0 in
+      _ ← interp_modify_state (state_upd_heap <[l:=None]>);
+      mret (LitV LitUnit)
+    | Store el e =>
+      w ← interp e;
+      vl ← interp el;
+      l_v0 ← read_loc "store" vl;
+      let '(l, _) := l_v0 in
+      _ ← interp_modify_state (state_upd_heap <[l:=Some w]>);
+      mret (LitV LitUnit)
+    | CmpXchg e e1 e2 =>
+      v2 ← interp e2;
+      v1 ← interp e1;
+      vl ← interp e;
+      l_v0 ← read_loc "cmpxchg" vl;
+      let '(l, vl) := l_v0 in
+      let b := bool_decide (vl = v1) in
+      if decide (vals_compare_safe vl v1) then
+        _ ← interp_modify_state (λ σ, if b
+                                      then state_upd_heap <[l:=Some v2]> σ
+                                      else σ);
+        mret (PairV vl (LitV (LitBool b)))
+      else
+        error $ "cmpxchg: failed comparison: " + default "" (explain_vals_compare_safe_fail vl v1)
+    | FAA el e =>
+      v ← interp e;
+      vl ← interp el;
+      l_v0 ← read_loc "faa" vl;
+      let '(l, v0) := l_v0 in
+      match v0, v with
+      | LitV (LitInt i1), LitV (LitInt i2) =>
+        _ ← interp_modify_state (state_upd_heap <[l:=Some (LitV (LitInt (i1 + i2)))]>);
+        mret (LitV (LitInt i1))
+      (* check constant passed to FAA only if heap value is an integer *)
+      | LitV (LitInt _), _ => error $ "faa: increment " + v + " is not an integer"
+      | _, _ => error $ "faa: called on non-integer heap value: " + v0
+      end
+
+    (* unsupported prophecy variable operations *)
+    | NewProph => λ s, (inr (Unsupported "NewProph"), s)
+    | Resolve _ _ _ => λ s, (inr (Unsupported "Resolve"), s)
+    end
+    end.
+End interpreter.
+
+(** * Theory for proving steps are sound. *)
+
+Lemma atomic_step e σ e' σ' :
+  head_step e σ [] e' σ' [] →
+  ∀ tp, rtc erased_step (e :: tp, σ) (e' :: tp, σ').
+Proof.
+  intros ? tp.
+  apply rtc_once.
+  exists [].
+  eapply (step_atomic e σ e' σ' [] [] tp); simpl; auto.
+  - rewrite app_nil_r //.
+  - eapply (Ectx_step []); eauto.
+Qed.
+
+Lemma step_inv ts1 σ1 κ ts2 σ2 :
+  step (Λ:=heap_lang) (ts1, σ1) κ (ts2, σ2) →
+  ∃ (t1 : list expr) (e1 : expr) (t2 : list expr) (e2 : expr) (efs: list expr),
+    ts1 = t1 ++ e1 :: t2 ∧
+    ts2 = t1 ++ e2 :: t2 ++ efs ∧
+    prim_step e1 σ1 κ e2 σ2 efs.
+Proof.
+  inversion 1.
+  simplify_eq.
+  eauto 10.
+Qed.
+
+Lemma fill_step K (e: expr) σ tp κ e' σ' tp' :
+  step (e :: tp, σ) κ (e' :: tp', σ') →
+  step (fill K e :: tp, σ) κ (fill K e' :: tp', σ').
+Proof.
+  intros Hstep.
+  apply step_inv in Hstep as (t1 & e1 & t2 & e2 & efs & Heq1 & Heq2 & Hprim_step).
+  pose proof Hprim_step as H.
+  apply (fill_prim_step K) in H.
+  simpl in H.
+  destruct t1 as [|e0 t1].
+  - simplify_eq/=.
+    eapply (step_atomic _ _ _ _ efs [] t2); eauto.
+  - simplify_eq/=.
+    eapply (step_atomic _ _ _ _ efs (fill K e0 :: t1) t2); eauto.
+Qed.
+
+Lemma fill_erased_step (e: expr) σ tp e' σ' tp' K :
+  erased_step (e :: tp, σ) (e' :: tp', σ') →
+  erased_step (fill K e :: tp, σ) (fill K e' :: tp', σ').
+Proof.
+  rewrite /erased_step.
+  intros [κ Hstep].
+  exists κ.
+  apply fill_step; auto.
+Qed.
+
+Lemma step_cons_no_destroy (e: expr) tp σ κ ρ2 :
+  step (e :: tp, σ) κ ρ2 →
+  ∃ e' tp' σ', ρ2 = (e' :: tp', σ').
+Proof.
+  destruct ρ2 as [ts' σ'].
+  intros (t1&?&?&?&?&?&?&?)%step_inv; subst.
+  destruct t1; simplify_eq/=; eauto.
+Qed.
+
+Lemma language_nsteps_inv_r (Λ: language) n ρ1 κs ρ2 :
+  language.nsteps (Λ:=Λ) (S n) ρ1 κs ρ2 →
+  ∃ ρ' κ κs', step ρ1 κ ρ' ∧ κs = κ ++ κs' ∧
+              language.nsteps n ρ' κs' ρ2.
+Proof. inversion 1; subst; eauto 10. Qed.
+
+Lemma fill_erased_steps (e: expr) σ tp e' σ' tp' K :
+  rtc erased_step (e :: tp, σ) (e' :: tp', σ') →
+  rtc erased_step (fill K e :: tp, σ) (fill K e' :: tp', σ').
+Proof.
+  rewrite !erased_steps_nsteps.
+  destruct 1 as (n & κs & Hsteps).
+  generalize dependent e.
+  generalize dependent tp.
+  generalize dependent σ.
+  generalize dependent e'.
+  generalize dependent tp'.
+  generalize dependent σ'.
+  generalize dependent κs.
+  induction n as [|n IHn]; intros ??????? Hsteps.
+  - invc Hsteps.
+    exists 0, [].
+    constructor.
+  - apply language_nsteps_inv_r in Hsteps as (ρ2 & κ & κs' & (Hstep & -> & Hsteps)).
+    (* here is the crucial step: to apply [fill_step] need to know that [ρ2] has
+    the right structure, which comes from threads not getting destroyed *)
+    edestruct step_cons_no_destroy as (e''&tp''&σ''&Heq); eauto; subst.
+    apply (fill_step K) in Hstep.
+    apply IHn in Hsteps as (n'&κs&?).
+    exists (S n'), (κ ++ κs).
+    econstructor; eauto.
+Qed.
+
+Section interpret_ok.
+  Tactic Notation "step" "by" tactic1(t) :=
+    etrans; [ solve [ t ] | simpl ].
+
+  Ltac change_to_fill e :=
+    reshape_expr e ltac:(fun K e' =>
+                           (* find a non-trivial context *)
+                           lazymatch K with
+                           | [_] => change e with (fill K e')
+                           end).
+
+  Ltac step_ctx :=
+    lazymatch goal with
+    | |- rtc erased_step (?e :: _, _) _ =>
+      change_to_fill e;
+      step by (apply fill_erased_steps; eauto)
+    end.
+
+  Ltac step_atomic :=
+    step by (apply atomic_step; eauto using head_step);
+    try reflexivity.
+
+  Lemma state_wf_init_alloc (v0 : val) (s : interp_state) (n : Z) :
+    (0 ≤ n)%Z →
+    state_wf s →
+    state_wf
+      (modify_lang_state
+          (λ σ : state, state_init_heap {| loc_car := next_loc s |} n v0 σ)
+          (interp_state_alloc n s)).
+  Proof.
+    intros Hn.
+    constructor; rewrite /modify_lang_state /interp_state_alloc /=; intros l ?.
+    apply fin_maps.lookup_union_None; split.
+    - destruct (heap_array _ _ !! l) eqn:Hlookup; auto.
+      apply heap_array_lookup in Hlookup as (j & w & Hle & ? & ? & Hlookup); subst.
+      apply lookup_replicate in Hlookup as [? ?]; subst.
+      simpl in *.
+      lia.
+    - apply state_wf_holds; auto.
+      lia.
+  Qed.
+
+  Lemma state_wf_same_dom s f :
+    (dom (gmap.gset _) (f s.(lang_state)).(heap) = dom _ s.(lang_state).(heap)) →
+    state_wf s →
+    state_wf (modify_lang_state f s).
+  Proof.
+    intros Hdom_eq Hwf.
+    constructor; rewrite /modify_lang_state /= => l ?.
+    apply fin_map_dom.not_elem_of_dom.
+    rewrite Hdom_eq.
+    apply fin_map_dom.not_elem_of_dom.
+    apply state_wf_holds; auto.
+  Qed.
+
+  Lemma state_wf_upd s l mv0 v' :
+      state_wf s →
+      heap (lang_state s) !! l = Some mv0 →
+      state_wf (modify_lang_state (λ σ : state, state_upd_heap <[l:=v']> σ) s).
+  Proof.
+    intros Hwf Heq.
+    apply state_wf_same_dom; auto.
+    rewrite fin_map_dom.dom_insert_L.
+    apply fin_map_dom.elem_of_dom_2 in Heq.
+    set_solver.
+  Qed.
+
+  Lemma interpret_wf fuel : ∀ (e: expr) s v s',
+      state_wf s →
+      interpret fuel e s = (inl v, s') →
+      state_wf s'.
+  Proof.
+    induction fuel as [|fuel]; simpl; intros e s v s' **; [ errored | ].
+    destruct e; try errored; success; eauto;
+      (repeat case_match; subst; try errored;
+       success;
+       eauto using state_wf_upd).
+    - constructor; intros.
+      simpl.
+      apply state_wf_holds; auto.
+    - match goal with
+      | H: interp_alloc _ _ = (_, _) |- _ => invc H
+      end.
+      apply state_wf_init_alloc; eauto.
+      lia.
+    - apply state_wf_same_dom; eauto.
+  Qed.
+
+  Local Hint Resolve interpret_wf : core.
+
+  Lemma interpret_sound fuel : ∀ e s v s',
+      state_wf s →
+      interpret fuel e s = (inl v, s') →
+      rtc erased_step (e :: s.(forked_threads), s.(lang_state)) (Val v :: s'.(forked_threads), s'.(lang_state)).
+  Proof.
+    induction fuel as [|fuel]; simpl; intros e s v s' **; [ errored | ].
+    destruct e; try errored; success; cbn [forked_threads lang_state];
+      repeat match goal with
+             | H: (match ?x with
+                   | _ => _
+                   end _ = (inl _, _)) |- _ =>
+               let Heqn := fresh "Heqn" in
+               destruct x eqn:Heqn; try errored; [idtac]
+             | _ => progress success
+             | _ => step_ctx
+             | _ => step_atomic
+             end.
+    - (* Val *)
+      reflexivity.
+    - (* App *)
+      eauto.
+    - (* If *)
+      lazymatch goal with
+      | |- context[LitBool ?b] => destruct b; step_atomic; eauto
+      end.
+    - (* Case *)
+      lazymatch goal with
+      | |- context[Case (Val ?v)] => destruct v; try errored; step_atomic; eauto
+      end.
+    - (* Fork *)
+      eapply rtc_once. exists [].
+      lazymatch goal with
+      | |- context[Fork ?e] => eapply (step_atomic _ _ _ _ [e] []); simpl; eauto
+      end.
+      apply head_prim_step; simpl.
+      eauto using head_step.
+    - (* AllocN *)
+      lazymatch goal with
+      | H: interp_alloc _ _ = _ |- _ => invc H
+      end.
+      eapply atomic_step. constructor; auto; intros.
+      simpl. apply state_wf_holds; eauto.
+      simpl; lia.
+  Qed.
+
+  (** * Theory for expressions that are stuck after some execution steps. *)
+
+  Definition eventually_stuck (e: expr) tp σ tp' σ' :=
+    ∃ e'', rtc erased_step (e :: tp, σ) (e'':: tp', σ') ∧ stuck e'' σ'.
+
+  (** a stuck expression is eventually stuck *)
+  Lemma eventually_stuck_now (e: expr) tp σ :
+    stuck e σ →
+    eventually_stuck e tp σ tp σ.
+  Proof.
+    intros.
+    exists e.
+    split; [ reflexivity | auto ].
+  Qed.
+
+  (** we can "peel off" some number of execution steps before proving that an
+  expression is stuck *)
+  Lemma eventually_stuck_steps e tp σ tp0 σ0 e' tp' σ' :
+    rtc erased_step (e :: tp, σ) (e' :: tp0, σ0) →
+    eventually_stuck e' tp0 σ0 tp' σ' →
+    eventually_stuck e tp σ tp' σ'.
+  Proof.
+    intros Hsteps (e'' & Hsteps' & Hstuck).
+    eexists. split; [ etrans; eauto | eauto ].
+  Qed.
+
+  (** [eventually_stuck] respects evaluation contexts *)
+  Lemma eventually_stuck_fill K e tp σ tp' σ' :
+    eventually_stuck e tp σ tp' σ' →
+    eventually_stuck (fill K e) tp σ tp' σ'.
+  Proof.
+    intros (e' & Hsteps & Hstuck).
+    eexists (fill K e'). split.
+    - apply fill_erased_steps; auto.
+    - apply stuck_fill; auto.
+  Qed.
+
+  Local Hint Resolve interpret_sound : core.
+
+  (* peel off execution steps and use above automation to prove the [rtc
+  erased_steps] premise *)
+  Ltac stuck_steps :=
+    eapply eventually_stuck_steps;
+    [ repeat step_ctx; (step_atomic || reflexivity)
+    |].
+
+  (* automate using hypotheses about stuckness inside an evaluation context *)
+  Ltac stuck_fill :=
+    lazymatch goal with
+    | |- eventually_stuck ?e _ _ _ _ =>
+      change_to_fill e; apply eventually_stuck_fill; solve [ eauto ]
+    end.
+
+  (** We need more complicated theory to handle expressions that are stuck now,
+  because there is no [head_step] they can take. *)
+
+  (* [terminal_expr e] holds when e cannot be the result of taking a context
+  step. Slightly more formally, e doesn't have the shape [fill K e'] where e' is
+  reducible. *)
+  Definition terminal_expr e :=
+    ∀ K e', to_val e' = None →
+            e = fill K e' →
+            K = [] ∧ e' = e.
+
+  Lemma stuck_not_val e σ :
+    to_val e = None →
+    (∀ (κs: list observation) (e': expr) (σ': state) (efs: list expr),
+        prim_step e σ κs e' σ' efs → False) →
+    stuck e σ.
+  Proof.
+    rewrite /stuck /irreducible.
+    intuition.
+  Qed.
+
+  Local Hint Resolve val_head_stuck : core.
+
+  (* This theorem expresses the point of [terminal_expr e]: a terminal_expr is
+  stuck if it can't take a head step, because there's *)
+  Lemma terminal_expr_stuck e σ :
+    to_val e = None →
+    terminal_expr e →
+    (∀ κ e' σ' efs, head_step e σ κ e' σ' efs → False) →
+    stuck e σ.
+  Proof.
+    intros Hnot_val Hterminal Hno_head_step.
+    apply stuck_not_val; first done; intros * Hstep.
+    invc Hstep; simpl in *.
+    lazymatch type of Hnot_val with
+    | to_val (fill ?K ?e1') = None => edestruct (Hterminal K e1') as [-> ?]; eauto
+    end.
+  Qed.
+
+  Lemma fill_not_val' K e v :
+    to_val e = None →
+    Val v = fill K e →
+    False.
+  Proof.
+    intros H Hfill.
+    apply (fill_not_val K) in H; simpl in *.
+    rewrite -Hfill /= in H.
+    congruence.
+  Qed.
+
+  (* to prove [terminal_expr], we work by contradiction in the case where [K] is
+  non-empty; to deal with [fill], which is a fold left, we express a non-empty
+  list as [l ++ [x]] rather than the usual [x::l]. *)
+  Lemma list_rev_case {A} (l: list A) :
+    l = [] ∨ ∃ x l', l = l' ++ [x].
+  Proof.
+    induction l using rev_ind; eauto.
+  Qed.
+
+  Ltac ctx_case K Ki :=
+    let K' := fresh "K" in
+    destruct (list_rev_case K) as [->| (Ki & K' & ->)]; [by auto|];
+    rewrite ?fill_app /=.
+
+  Ltac prove_terminal :=
+    lazymatch goal with
+    | |- terminal_expr _ =>
+      let K := fresh "K" in
+      let e := fresh "e" in
+      let Ki := fresh "Ki" in
+      intros K e; ctx_case K Ki;
+      destruct Ki;
+      let H := fresh in
+      intros ? H; invc H;
+      solve [ exfalso; eauto using fill_not_val' ]
+    | _ => fail "not a terminal_expr goal"
+    end.
+
+  (* demo the [prove_terminal] tactic *)
+  Lemma fill_app_inv v1 v2 :
+    terminal_expr (App (Val v1) (Val v2)).
+  Proof. prove_terminal. Qed.
+
+  Ltac stuck_now :=
+    apply eventually_stuck_now, terminal_expr_stuck;
+    [done
+    |prove_terminal
+    |let H := fresh "Hstep" in
+     intros * H;
+     try (inversion H; congruence) ].
+
+  Lemma interpret_complete fuel : ∀ e s msg s',
+    ∀ (Hwf: state_wf s),
+      interpret fuel e s = (inr (Stuck msg), s') →
+      eventually_stuck e s.(forked_threads) s.(lang_state) s'.(forked_threads) s'.(lang_state).
+  Proof.
+    induction fuel as [|fuel]; simpl; intros e s msg s' **; [congruence|].
+    destruct e; failure; stuck_steps; try stuck_fill;
+      try (repeat case_match; failure; try stuck_now;
+           let n := numgoals in guard n <= 1);
+      simplify_eq.
+    - (* App *)
+      stuck_steps.
+      eauto.
+    - (* If *)
+      repeat case_match; failure; try stuck_now.
+      + stuck_steps; eauto.
+      + stuck_steps; eauto.
+    - (* Case *)
+      repeat case_match; failure; try stuck_now.
+      + stuck_steps; eauto.
+      + stuck_steps; eauto.
+    - (* CmpXchg *)
+      success. stuck_now.
+    - (* FAA *)
+      lazymatch goal with
+      | H: read_loc _ _ _ = (inl ?p, _) |- _ => destruct p as [l v]
+      end.
+      success.
+      repeat case_match; failure; stuck_now.
+  Qed.
+
+End interpret_ok.
+
+Definition exec (fuel:nat) (e: expr) : val + Error :=
+  interp_monad.run (interpret fuel e).
+
+Theorem exec_spec fuel e :
+  match exec fuel e with
+  | inl v =>
+    (* if the interpreter runs to completion, it produces a valid execution of
+    [e] *)
+    ∃ tp' σ', rtc erased_step ([e], init_state) ([Val v] ++ tp', σ')
+  | inr (Stuck _) =>
+    (* if the interpreter produces a "stuck" error message, [e] can get stuck *)
+    ∃ e' tp' σ', rtc erased_step ([e], init_state) ([e'] ++ tp', σ') ∧ stuck e' σ'
+  | inr _ =>
+    (* If the interpreter fails otherwise (due to running out of fuel or an
+    unsupported prophecy variable operation), then it provides no guarantees. *)
+    True
+  end.
+Proof.
+  rewrite /exec /interp_monad.run.
+  destruct (interpret fuel e init_interp_state) as [r s] eqn:Hinterpret.
+  destruct r as [v | [msg|msg|] ]; simpl; auto.
+  - apply interpret_sound in Hinterpret;
+      eauto using init_interp_state_wf.
+  - apply interpret_complete in Hinterpret;
+      auto using init_interp_state_wf.
+    destruct Hinterpret as (e' & Hexec & Hstuck); eauto.
+Qed.
diff --git a/tests/heap_lang_interpreter.ref b/tests/heap_lang_interpreter.ref
new file mode 100644
index 000000000..e64c78549
--- /dev/null
+++ b/tests/heap_lang_interpreter.ref
@@ -0,0 +1,44 @@
+"ex1"
+     : string
+     = inl #()
+     : val + Error
+"ex3"
+     : string
+     = inl #2
+     : val + Error
+"ex4"
+     : string
+     = inl #(Loc 2)
+     : val + Error
+"ex5"
+     : string
+     = inl #false
+     : val + Error
+"ex6"
+     : string
+     = inl #2
+     : val + Error
+"fail app non-function"
+     : string
+     = inr (Stuck "attempt to call non-function #2")
+     : val + Error
+"fail loc order"
+     : string
+     = inr
+         (Stuck
+            "bin-op failed: the only supported operation on locations is +â‚— #i, got < #(loc 2)")
+     : val + Error
+"fail compare pairs"
+     : string
+     = inr
+         (Stuck
+            "bin-op failed: one of (#0, #1) and (#0, #1) must be unboxed to compare: (#0, #1): pairs are large and considered boxed, must compare by field, (#0, #1): pairs are large and considered boxed, must compare by field")
+     : val + Error
+"fail free var"
+     : string
+     = inr (Stuck "free var: x")
+     : val + Error
+"fail out of fuel"
+     : string
+     = inl (rec: "foo" <> := "foo" #())%V
+     : val + Error
diff --git a/tests/heap_lang_interpreter.v b/tests/heap_lang_interpreter.v
new file mode 100644
index 000000000..de12d0ae2
--- /dev/null
+++ b/tests/heap_lang_interpreter.v
@@ -0,0 +1,66 @@
+From iris.heap_lang Require Import notation.
+From iris.staging.heap_lang Require Import interpreter.
+
+Example test_1 :
+  exec 1000 ((λ: "x", "x" + #1) #2) = inl #3.
+Proof. reflexivity. Qed.
+
+Check "ex1".
+Eval vm_compute in
+    exec 1000 (let: "x" := ref #() in
+               let: "y" := ref #() in
+               !"y").
+
+Check "ex3".
+(** eval order *)
+Eval vm_compute in
+    exec 1000 (let: "x" := ref #1 in
+               let: "y" := ref #2 in
+               ("y" <- !"x",
+                (* this runs first, so the result is 2 *)
+                "x" <- !"y");;
+               !"x").
+
+(* print a location *)
+Check "ex4".
+Eval vm_compute in
+    exec 1000 (ref #();; ref #()).
+
+Check "ex5".
+Eval vm_compute in
+    exec 1000 (let: "x" := ref #() in
+               let: "y" := ref #() in
+               "x" = "y").
+
+(* a bad case where the interpreter runs a program which is actually stuck,
+because this program guesses an allocation that happens to be correct in the
+interpreter *)
+Check "ex6".
+Eval vm_compute in
+    exec 1000 (let: "x" := ref #1 in
+              #(LitLoc {|loc_car := 1|}) <- #2;;
+              !"x").
+
+(** * Failing executions *)
+
+Check "fail app non-function".
+Eval vm_compute in
+    exec 1000 (#2 #4).
+
+(* locations are not ordered *)
+Check "fail loc order".
+Eval vm_compute in
+    exec 1000 (let: "x" := ref #() in
+               let: "y" := ref #() in
+               "x" < "y").
+
+Check "fail compare pairs".
+Eval vm_compute in
+    exec 1000 ((#0, #1) = (#0, #1)).
+
+Check "fail free var".
+Eval vm_compute in exec 100 "x".
+
+Check "fail out of fuel".
+(** infinite loop *)
+Eval vm_compute in exec 100 (rec: "foo" <> := "foo" #()).
-- 
GitLab