From 5afe58c4b86a0e4c12a1f1fc93f6b830a22c458f Mon Sep 17 00:00:00 2001
From: Michael Sammler <noreply@sammler.me>
Date: Thu, 2 Dec 2021 14:37:45 +0100
Subject: [PATCH] Add is_closed_term tactic

---
 CHANGELOG.md             |  2 ++
 tests/is_closed_term.ref | 16 +++++++++++++
 tests/is_closed_term.v   | 52 ++++++++++++++++++++++++++++++++++++++++
 theories/tactics.v       | 29 ++++++++++++++++++++++
 4 files changed, 99 insertions(+)
 create mode 100644 tests/is_closed_term.ref
 create mode 100644 tests/is_closed_term.v

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c832122..cd68a8fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ API-breaking change is listed.
 
 Coq 8.10 is no longer supported by this release.
 
+- Add `is_closed_term` tactic for determining whether a term depends on variables bound
+  in the context. (by Michael Sammler)
 - Add `list.zip_with_take_both` and `list.zip_with_take_both'`
 - Specialize `list.zip_with_take_{l,r}`, add generalized lemmas `list.zip_with_take_{l,r}'`
 - Add `bool_to_Z` that converts true to 1 and false to 0. (by Michael Sammler)
diff --git a/tests/is_closed_term.ref b/tests/is_closed_term.ref
new file mode 100644
index 00000000..3bf0af63
--- /dev/null
+++ b/tests/is_closed_term.ref
@@ -0,0 +1,16 @@
+"is_closed_term_test"
+     : string
+The command has indeed failed with message:
+Tactic failure: The term x is not closed.
+The command has indeed failed with message:
+Tactic failure: The term (x + 1) is not closed.
+The command has indeed failed with message:
+Tactic failure: The term (x + y) is not closed.
+The command has indeed failed with message:
+Tactic failure: The term section_variable is not closed.
+The command has indeed failed with message:
+Tactic failure: The term (section_variable + 1) is not closed.
+The command has indeed failed with message:
+Tactic failure: The term P is not closed.
+The command has indeed failed with message:
+Tactic failure: The term (P ∧ True) is not closed.
diff --git a/tests/is_closed_term.v b/tests/is_closed_term.v
new file mode 100644
index 00000000..aa9393fa
--- /dev/null
+++ b/tests/is_closed_term.v
@@ -0,0 +1,52 @@
+From stdpp Require Import tactics strings.
+Unset Mangle Names.
+
+Section test.
+  Context (section_variable : nat).
+
+  Axiom axiom : nat.
+
+  Check "is_closed_term_test".
+  Lemma is_closed_term_test :
+    ∀ x y (P : Prop),
+      let a := 10 in
+      a = a →
+      let b := (a + 11) in
+      x + y = y + x.
+  Proof.
+    intros x y P a H b.
+    (* Constructors are closed. *)
+    is_closed_term 1.
+
+    (* Functions on closed terms are closed. *)
+    is_closed_term (1 + 1).
+
+    (* Variables bound in the context are not closed. *)
+    Fail is_closed_term x.
+    Fail is_closed_term (x + 1).
+    Fail is_closed_term (x + y).
+
+    (* Section variables are not closed. *)
+    Fail is_closed_term section_variable.
+    Fail is_closed_term (section_variable + 1).
+
+    (* Axioms are considered closed. (Arguably this is a bug, but
+    there is nothing we can do about it.) *)
+    is_closed_term axiom.
+    is_closed_term (axiom + 1).
+
+    (* Let-bindings are considered closed. *)
+    is_closed_term a.
+    is_closed_term (a + 1).
+    is_closed_term b.
+    is_closed_term (b + 1).
+
+    (* is_closed_term also works for propositions. *)
+    is_closed_term True.
+    is_closed_term (True ∧ True).
+    Fail is_closed_term P.
+    Fail is_closed_term (P ∧ True).
+
+    lia.
+  Qed.
+End test.
diff --git a/theories/tactics.v b/theories/tactics.v
index ff8306f3..744ec4b7 100644
--- a/theories/tactics.v
+++ b/theories/tactics.v
@@ -599,6 +599,35 @@ Tactic Notation "revert" "select" open_constr(pat) := select pat (fun H => rever
 Tactic Notation "rename" "select" open_constr(pat) "into" ident(name) :=
   select pat (fun H => rename H into name).
 
+(** The tactic [is_closed_term t] succeeds if [t] is a closed term and fails otherwise.
+By closed we mean that [t] does not depend on any variable bound in the context.
+axioms are considered closed terms by this tactic (but Section
+variables are not). A function application is considered closed if the
+function and the argument are closed, without considering the body of
+the function (or whether it is opaque or not). This tactic is useful
+for example to decide whether to call [vm_compute] on [t].
+
+This trick was originally suggested by Jason Gross:
+https://coq.zulipchat.com/#narrow/stream/237977-Coq-users/topic/Check.20that.20a.20term.20is.20closed.20in.20Ltac/near/240885618
+*)
+Ltac is_closed_term t :=
+  first [
+      (** We use the [assert_succeeds] sandbox to be able to freely
+      change the context. *)
+      assert_succeeds (
+          (** Make sure that the goal only contains [t]. (We use
+          [const False t] instead of [let x := t in False] as the
+          let-binding in the latter would be unfolded by the [unfold]
+          later.) *)
+          exfalso; change_no_check (const False t);
+          (** Clear all hypotheses. *)
+          repeat match goal with H : _ |- _ => try unfold H in *; clear H end;
+          (** If there are still hypotheses left, [t] is not closed. *)
+          lazymatch goal with H : _ |- _ => fail | _ => idtac end
+        ) |
+      fail 1 "The term" t "is not closed"
+    ].
+
 (** Coq's [firstorder] tactic fails or loops on rather small goals already. In
 particular, on those generated by the tactic [unfold_elem_ofs] which is used
 to solve propositions on sets. The [naive_solver] tactic implements an
-- 
GitLab