Proof language
The REPL and tactics
This page is a draft. Rainbow's authors are reviewing it, and details may change.
Proof modules write proof terms by hand. From a REPL you can build them in steps. rainbow.repl (source) sits over rainbow.proof (proof states), rainbow.tactic (steps) and rainbow.search (automatic search). Tactics are untrusted. They propose pieces of a term, and when the last goal closes, the kernel checks the whole term.
A session
Run lein repl in the repository. Outputs are from v1.4.1.
(require '[rainbow.repl :as repl]
'[rainbow.tactic :as tactic]
'[rainbow.environment :as environment])
(def env (environment/empty-environment))
(def started
(repl/surface-goal env '[:forall [[A [:sort [:param u]]] [x A]] A]
{:universes ['u]}))
(:status started)
;=> :ready
(repl/goals (repl/proof-state started))
;=> [#:goal{:id "g-0",
; :context [],
; :target [:pi [:sort [:param u]] [:pi [:bound 0] [:bound 1] :explicit] :explicit],
; :path []}]
The goal asks for an A, given any type A in universe u and any x : A. :universes declares u.
(def s1 (repl/try! (repl/proof-state started) (tactic/intro 'A)))
(def s2 (repl/try! (repl/proof-state s1) (tactic/intro 'x)))
(repl/goals (repl/proof-state s2))
;=> [#:goal{:id "g-2",
; :context [#:goal{:name x, :type [:bound 0], :mode :explicit}
; #:goal{:name A, :type [:sort [:param u]], :mode :explicit}],
; :target [:bound 1],
; :path [0 0]}]
Locals are listed innermost first: x is [:bound 0], and A is [:bound 1], the target.
A wrong step fails and leaves the state unchanged:
(def bad (repl/try! (repl/proof-state s2) (tactic/exact [:bound 1])))
(:status bad)
;=> :failure
(repl/why bad)
;=> #:failure{:category :tactic/type-mismatch,
; :tactic :exact,
; :detail {:term [:bound 1],
; :kernel {:status :rejected,
; :error {:expected [:bound 1], :actual [:sort [:param u]], ...},
; :message "Term does not inhabit expected type"}}}
(= (:state bad) (repl/proof-state s2))
;=> true
[:bound 1] is the type A itself, and the goal wants an element of A. The local x fits:
(def done (repl/try! (repl/proof-state s2) (tactic/assumption)))
(:status done)
;=> :proved
(:proof done)
;=> {:rainbow/kind :checked-proof,
; :proof/term [:lam [:sort [:param u]] [:lam [:bound 0] [:bound 0] :explicit] :explicit],
; :proof/type [:pi [:sort [:param u]] [:pi [:bound 0] [:bound 1] :explicit] :explicit],
; :proof/environment-id "sha256:...",
; :proof/axioms #{},
; :proof/history [#:event{:tactic :intro, :name A}
; #:event{:tactic :intro, :name x}
; #:event{:tactic :exact, :term [:bound 0]}]}
assumption closed the goal with exact. With no goals left, rainbow.proof/progress assembled the term and called kernel/check, and the result also holds :kernel {:status :checked ...}.
Functions
| function | what it does |
|---|---|
(goal env target [options]) | starts a proof of a core term. Options: :universes, :context, :names. |
(surface-goal env surface [options]) | elaborates with type-expression, then starts a proof |
(proof-state result) | the state in a result, or the argument if it is a state |
(goals state) | open goals, focused first |
(context state) | the focused goal’s local context |
(try! state tactic) | runs one tactic |
(search! state budget) | runs the baseline proof search |
(why result) | the :failure map of a failed tactic |
(checkpoint state label), (restore checkpoint) | saves a state under a label and gets it back |
(diff-attempts left right) | compares the attempted sets of two states |
(run-program started steps) | runs steps written as data |
(declare-theorem env alias options surface proof-form) | elaborates, proves and adds a theorem |
A state is a map with :proof/goals, :proof/partial-term (the proof so far, [:hole "g-n"] for open goals), :proof/history, :proof/root-goal and :proof/environment. Keep a state in a var to go back to it.
A tactic result has :status :progress (new state in :state, open goals in :goals, the step in :event), :proved (the kernel accepted the term, results in :proof and :kernel) or :failure (:state is the unchanged input, :failure says why). repeat and all-goals can also return :no-progress.
Tactics
Tactics in rainbow.tactic (source) map a state to a result. They work on the focused goal and take core terms in its context.
| tactic | effect |
|---|---|
(exact term) | closes the goal if term checks against the target |
(intro), (intro name) | for a Pi target, or one that normalises to Pi. Adds a local (default name h plus a number) and leaves the body as the goal. |
(intro-from pi-target name) | intro from a Pi type you supply, definitionally equal to the target |
(assumption) | closes the goal with the innermost local whose type fits |
(reflexivity) | closes equality/Eq A l r with equality/reflexive when l and r are definitionally equal |
(rewrite proof), (rewrite proof {:direction :backward}) | with proof : equality/Eq A l r, replaces r with l in the target, or l with r backward (via equality/symmetric) |
(induction term) | for term of an inductive type. Builds the motive from the target, one goal per constructor. |
(apply term) | when the type of term ends in the target after non-dependent premises. One goal per premise. |
(refine template targets) | fills the goal with a partial term. [:hole 0], [:hole 1] … become goals with the types in targets. |
(calc proofs) | chains a = b, b = c, … with equality/transitive and closes the goal |
(simplify equalities) | repeats reflexivity or a forward rewrite with one of the equalities, up to 100 times |
(then t1 t2) | t1, then t2 if t1 made progress |
(choice t1 t2 ...), (first-success [t1 t2 ...]) | the first tactic that makes progress or finishes |
(repeat t), (repeat t {:max n}) | t until it fails, at most n times (default 100) |
(focus selector t) | t on the goal picked by id, index or predicate |
(all-goals t) | t on every open goal |
reflexivity, rewrite, calc and simplify need equality/Eq, equality/reflexive, equality/symmetric and equality/transitive in the environment.
Induction
rainbow.history.nat-induction (source) proves (n : Nat) -> n = n. It loads artifacts/day7/environment.edn, so run it from the repository root.
(defn prove-reflexive-induction [environment terms]
(let [{:keys [nat]} terms
target (term/pi-term nat (equality-type terms (term/bound 0) (term/bound 0)))
introduced (tactic/run (:state (proof/start environment target)) (tactic/intro 'n))
induced (tactic/run (:state introduced) (tactic/induction [:bound 0]))
base (tactic/run (:state induced) (tactic/reflexivity))
predecessor (tactic/run (:state base) (tactic/intro 'predecessor))
hypothesis (tactic/run (:state predecessor) (tactic/intro 'induction-hypothesis))]
(tactic/run (:state hypothesis) (tactic/reflexivity))))
(tactic/induction [:bound 0]) leaves two goals:
(:event induced)
;=> #:event{:tactic :induction, :expression [:bound 0], :cases 2}
(mapv :goal/path (:goals induced))
;=> [[0 0] [0 1]]
Goal g-2 is the motive at data/zero. Goal g-3 says the motive at predecessor implies the motive at succ predecessor. The motive (fn [n] (Eq Nat n n)) comes from abstracting [:bound 0] out of the target and stays unreduced in the case targets. reflexivity reduces the head of the target before it looks for equality/Eq, so it closes both cases. The proof is axiom-free. induction passes the goal’s universe level to the recursor as the motive universe (see Inductive types).
Search
search! runs a deterministic, bounded, breadth-first search over proof states. The default budget is 1,000 states, depth 32 and 10 seconds, and your map is merged over it. It proves transitivity of implication by itself:
(def s (repl/surface-goal env '[:forall [[A Prop] [B Prop] [C Prop]]
[:-> [:-> A B] [:-> B C] A C]]))
(def r (repl/search! (repl/proof-state s) {:states 100}))
(:status r)
;=> :proved
(get-in r [:proof :proof/history])
;=> [#:event{:tactic :intro, :name h1} ... #:event{:tactic :intro, :name h6}
; #:event{:tactic :apply, :term [:bound 1], :subgoals 1}
; #:event{:tactic :apply, :term [:bound 2], :subgoals 1}
; #:event{:tactic :exact, :term [:bound 0]}]
A search that cannot finish reports where it stopped:
(repl/search! (repl/proof-state (repl/surface-goal env '[:forall [[A Prop] [B Prop]] [:-> A B]]))
{:states 50})
;=> {:status :blocked,
; :reason :frontier-exhausted,
; :remaining [{:goal-hash "sha256:...", :target [:bound 1], :context [...]}],
; :dominant-failures [{:class :tactic/conclusion-mismatch, :count 6}
; {:class :tactic/no-matching-assumption, :count 4}],
; :telemetry {...}}
Tactic programs
(repl/run-program started '[[:intro A] [:intro x] [:assumption]]) runs a vector of steps on a started proof. Steps are [:intro name], [:exact term], [:apply term], [:assumption], [:refine template targets], [:rewrite proof options], [:simplify equalities options], [:calc proofs], [:induction term] and [:search budget]. The program stops at the first result that is neither :ready nor :progress and returns it. An unknown step throws with :repl/unsupported-step.
declare-theorem elaborates the statement, runs the proof and adds the theorem through kernel/add-declaration:
(repl/declare-theorem env 'example/identity {:universes ['u]}
'[:forall [[A [:sort [:param u]]] [x A]] A]
'[:by [[:intro A] [:intro x] [:assumption]]])
;=> {:status :accepted,
; :declaration #:rainbow{:kind :theorem, :aliases #{example/identity}, :universes [u],
; :id "sha256:e8cf6058...", ...},
; :environment {...}}
The proof form is [:by steps] or [:core term]. Steps are data, and no Clojure code in them runs. declare-theorem elaborates with type-expression and records no dependencies, so it only takes statements and proofs without constants. For anything that uses the library, write a spec in a module, as in Declarations and modules.