Beluga/Harpoon user’s manual¶
Beluga is a language for encoding and reasoning about formal systems specified by inference rules, e.g. lambda calculi and type systems. It uses contextual modal logic as its foundation. Object-language binding constructs are encoded using higher-order abstract syntax. That is, functions are used to encode binders. Terms are paired with the contexts in which they are meaningful to form contextual objects, enabling reasoning about open terms. Proofs in Beluga are represented by recursive programs according to the Curry-Howard correspondence.
Harpoon is an interactive proof development REPL built on top of Beluga. Users develop proofs by successively eliminating subgoals using a small, fixed set of tactics. Behind the scenes, the execution of tactics builds a proof script that can be machine-translated into a traditional Beluga program.
The Beluga project is developed at the Complogic group at McGill university, led by Professor Brigitte Pientka. It is implemented in OCaml.
Getting Started¶
Installation¶
We support only installation on macOS, Linux, and WSL.
Any of the following methods uses opam
2. Please ensure
that you have that version of opam
. You can find the installation
instruction here.
We use recent versions of OCaml, so check which are supported on our continuous integration before creating an opam switch.
Install using opam
¶
The following command will install beluga
under the switch you created.
opam install beluga
Now beluga
and harpoon
binaries are installed in $OPAM_SWITCH_PREFIX/bin
,
and these command work:
beluga --help
harpoon --help
Install from the source¶
First, clone the repository.
git clone https://github.com/Beluga-lang/Beluga
cd Beluga
Now, build the source with the following commands:
make setup-install
make install
This will place beluga
and harpoon
binaries in $OPAM_SWITCH_PREFIX/bin
.
They can be copied wherever you like, or you can add this directory to your PATH
environment variable.
Harpoon Example¶
You can try the following example to check Harpoon works:
LF tp : type =
| i : tp
| arr : tp -> tp -> tp
;
LF eq : tp -> tp -> type =
| eq_i : eq i i
| eq_arr : eq A1 A2 -> eq B1 B2 -> eq (arr A1 B1) (arr A2 B2)
;
First, save this code as a file tp-refl.bel
. Next, run the following command to load the Harpoon session.
harpoon --sig tp-refl.bel
Here, --sig
option represents a signature used for proofs. Now, Harpoon will print a session wizard:
## Type Reconstruction begin: tp-refl.bel ##
## Type Reconstruction done: tp-refl.bel ##
Configuring theorem #1
Name of theorem (:quit or empty to finish):
The session wizard will ask for the name of theorem, the actual statement, and the induction order. After giving tp-refl
, {A : [|- tp]} [|- eq A A]
, and 1
, the session wizard will print this:
## Type Reconstruction begin: stlc.bel ##
## Type Reconstruction done: stlc.bel ##
Configuring theorem #1
Name of theorem (:quit or empty to finish): halts_step
Statement of theorem: [|- step M M'] -> [|- halts M'] -> [|- halts M]
Induction order (empty for none):
Configuring theorem #2
Name of theorem (:quit or empty to finish):
Users can give any numbers of theorems they want. Here, for the purpose of this example, we will finish the session wizard, by typing the enter key. Then, Harpoon will display an interactive session:
Assumptions
Meta-assumptions:
A : ( |- tp)
are automatically introduced for the subgoal of type
{A : ( |- tp)} [ |- eq A A]
Theorem: tp-refl
intros
Meta-context:
A : ( |- tp)
Computational context:
--------------------------------------------------------------------------------
[ |- eq A A]
>
Now we can use interactive tactics to prove the goal (the type under the line). First, by applying split [|- A]
, we split the type into cases.
Theorem: tp-refl
intros
Meta-context:
A : ( |- tp)
Computational context:
--------------------------------------------------------------------------------
[ |- eq A A]
> split [|- A]
This will generate two subgoals, and you will notice that the label (the string on the second line) is changed so that we can see which subgoal we are in.
Theorem: tp-refl
intros <- split [ |- X1] (case arr)
Meta-context:
X : ( |- tp)
X1 : ( |- tp)
Computational context:
--------------------------------------------------------------------------------
[ |- eq (arr X X1) (arr X X1)]
>
To prove this, we need [|- eq X X]
and [|- eq X1 X1]
. We can get these by induction.
Theorem: tp-refl
intros <- split [ |- X1] (case arr)
Meta-context:
X : ( |- tp)
X1 : ( |- tp)
Computational context:
--------------------------------------------------------------------------------
[ |- eq (arr X X1) (arr X X1)]
> by tp-refl [|- X] as EQ_X unboxed
Theorem: tp-refl
intros <- split [ |- X1] (case arr)
Meta-context:
X : ( |- tp)
X1 : ( |- tp)
EQ_X : ( |- eq X X)
Computational context:
--------------------------------------------------------------------------------
[ |- eq (arr X X1) (arr X X1)]
> by tp-refl [|- X1] as EQ_X1 unboxed
With these two, we are able to use eq_arr
.
Theorem: tp-refl
intros <- split [ |- X1] (case arr)
Meta-context:
X : ( |- tp)
X1 : ( |- tp)
EQ_X : ( |- eq X X)
EQ_X1 : ( |- eq X1 X1)
Computational context:
--------------------------------------------------------------------------------
[ |- eq (arr X X1) (arr X X1)]
> solve [|- eq_arr EQ_X EQ_X1]
This will solve the subgoal, and Harpoon will subsequently show the next case, which can be solved directly with eq_i
.
Theorem: tp-refl
intros <- split [ |- FREE MVar 1] (case i)
Meta-context:
Computational context:
--------------------------------------------------------------------------------
[ |- eq i i]
> solve [|- eq_i]
After solving all subgoals, Harpoon will print the proof script as well as its translation as a Beluga program, and save the proof script (You can check it by cat tp-refl.bel
) and type-check the signature file again.
Subproof complete! (No subgoals left.)
Full proof script:
intros
{ A : ( |- tp)
|
; split [ |- A] as
case arr:
{ X : ( |- tp), X1 : ( |- tp)
|
; by tp-refl [ |- X] as EQ_Y unboxed;
by tp-refl [ |- X1] as EQ_X unboxed;
solve [ |- eq_arr EQ_Y EQ_X]
}
case i:
{
|
; solve [ |- eq_i]
}
}
Translation generated program:
mlam A =>
case [ |- A] of
| [ |- arr X X1] =>
let [ |- EQ_Y] = tp-refl [ |- X] in
let [ |- EQ_X] = tp-refl [ |- X1] in [ |- eq_arr EQ_Y EQ_X]
| [ |- i] =>
[ |- eq_i]
No theorems left. Checking translated proofs.
- Translated proofs successfully checked.
Proof complete! (No theorems left.)
## Type Reconstruction begin: t/harpoon/tp-refl.bel ##
## Type Reconstruction done: t/harpoon/tp-refl.bel ##
Once the proof is completed, Harpoon will restart the session wizard, and we can choose whether to prove more theorems or :quit
.
Configuring theorem #1
Name of theorem (:quit or empty to finish): :quit
Harpoon terminated.
That’s it! If you want to know more details including how to write the signature file and what kinds of tactics do we provide, please read the common elements and interactive proving with harpoon section of this page. For additional examples, you can check out the test directory in our github repository.
Common elements¶
The Logical Framework LF¶
Languages are encoded for study in Beluga using the Logical Framework LF [1].
New syntactic categories are defined using the LF
toplevel definition, which
defines a new LF (indexed) type together with its constructors.
Basic example: natural numbers¶
LF nat : type =
| zero : nat
| succ : nat -> nat
;
The first line defines a new simple LF type nat
and the following lines
define its constructors, zero
and succ
. The number three is written
succ (succ (succ zero))
in this encoding.
One can define relations on natural numbers by defining indexed types. For
example, we can encode a less-than-equals relation on nat
as follows.
LF le : nat -> nat -> type =
| le_z : le zero N
| le_s : le N M ->
% ------------------
le (succ N) (succ M)
;
Terms of this type encode proofs. As a concrete example, a term of type
le (succ zero) (succ (succ zero))
would represent a proof that 1 is less
than 2.
First, observe the presence of free variables M
and N
.
Any free variable is automatically Pi-quantified at the very front of the
constructor being defined. Thus, the internal type of le_s
is really
{N : nat} {M : nat} le N M -> le (succ N) (succ M)
.
(Beluga uses curly braces to write Pi-types.)
We call such automatically quantified variables implicit parameters. The
types of implicit parameters are determined by type reconstruction. It is not
possible for the user to explicitly provide an instantiation for an implicit
parameter. Instantiations for implicit parameters are automatically found via
unification, based on the types of provided arguments.
In other words, when one writes a term le_s Q
for some argument proof Q
,
it is via the type of Q
that the instantiations for N
and M
are
found.
Next, the rule le_s
has been suggestively written with a commented line to
illustrate that one would ordinarily write this as an inference rule. In Beluga,
we use LF
declarations to uniformly represent the syntax and the inference
rules of object languages.
(Semantic predicates about encodings are defined using Inductive Types.)
One can state a theorem about such an encoding using Contextual LF and one can prove them by writing a functional program or interactively.
HOAS example: lambda calculus¶
Beluga is a domain-specific language for reasoning about formal systems, and one of the simplest such systems is the lambda calculus. Unlike in the natural numbers, lambda-terms contain binders. The philosophy of LF is to represent binders using higher-order abstract syntax (HOAS). That is, the functions of the metalanguage (LF) are used to represent the binding structure of the object-language (lambda calculus). Here is how we define untyped lambda terms using HOAS.
LF tm : type =
| lam : (tm -> tm) -> tm
| app : tm -> tm -> tm
;
One immense benefit of HOAS is that the object-language inherits the substitution properties of the metalanguage. In practice, this means that one needs not define substitution, but rather simply use LF function application. For example, consider the following encoding of a small-step, call-by-name operational semantics for the lambda calculus.
LF step : tm -> tm -> type =
| e_app : step M M' ->
% -----------------------
step (app M N) (app M' N)
| beta : step (app (lam M) N) (M N)
;
First, observe that step
is not a simple type. It is indexed by two terms,
so we understand it as a binary relation between terms.
Finally, the rule beta
demonstrates HOAS in action. We use LF function
application to implement the beta reduction of the lambda calculus. The type of
the variable M
in this constructor is inferred by type reconstruction as
tm -> tm
, given that it appears as the first argument to the constructor
lam
.
To complete the example encoding of the lambda calculus, we will now turn our attention to a simple type assignment system for this language. First, we will define the syntax of types.
LF tp : type =
| base : tp
| arr : tp -> tp -> tp
;
Second, we define the typing judgment as an indexed type.
In this case, we understand oft
as relating a term tm
to a type tp
.
LF oft : tm -> tp -> type =
| t_app : oft M (arr A B) -> oft N A ->
% ---------------------------
oft (app M N) B
| t_lam : ({x : tm} oft x A -> oft (M x) B) ->
% ----------------------------------
oft (lam M) (arr A B)
;
We will concentrate on the rule t_lam
. Here, the variable M
is
understood as the body of the lambda-abstraction, and it has type tm -> tm
.
The premise of this rule reads “for any term x
, if x
is of type
A
, then M x
is of type B
”. This precisely captures the parametric
reasoning used on paper when proving that a lambda-abstract has an arrow-type.
Here it is necessary to explicitly write a Pi-type for x
as leaving it
implicit would have it incorrect quantified at the level above.
To reason about these definitions, one would formulate a theorem and prove it. Theorems are stated and proven in Beluga’s computation language. Whereas LF is used as a metalanguage for encoding various formal systems, Beluga’s computation language is used as a metalanguage for Contextual LF. To prove a theorem, one either writes a functional program in Beluga or uses Harpoon.
[1] | TODO cite LF paper |
Contextual LF¶
In contrast with languages such as Coq or Agda that feature full dependent types, Beluga has an indexed type system. This is a restricted form of dependent types in which one quantifies only over a specific index domain. The index domain of Beluga is Contextual LF.
At a high level, a Contextual LF object (or type) is an LF term (or type) together with an LF context in which the term (or type) makes sense. For example, consider the following syntax of types for the simply-typed lambda calculus together with a structural equality relation on types.
LF tp : type =
| base : tp
| arr : tp -> tp -> tp
;
LF eq : tp -> tp -> type =
| eq_base : eq base base
| eq_arr : eq A1 A2 -> eq B1 B2 ->
% ------------------------
eq (arr A1 B1) (arr A2 B2)
;
The contextual object that encodes a closed proof that the base
type is
equal to the base
type is |- eq_base
. We write this sometimes with
parentheses to make reading easier, e.g. ( |- eq_base )
.
Open terms can be represented by using a nonempty LF context. For example,
x : eq A B |- eq_arr x eq_base
is a contextual object of type x : eq A B |- eq (arr A base) (arr B base)
.
Computation types¶
Contextual LF objects and types may be boxed into computation types. These types are primarily used for encoding theorems about a formal system. For example, the type of a theorem expressing that equality is reflexive would be
{A : |- tp} [ |- eq A A]
We read this as “for all (closed) types A
, A
is equal to itself.”
This is a computation type. It is composed of two parts in this example: a
PiBox-type and a box-type. The PiBox is used as a quantifier; we will go
into more depth on this in the following section.
The syntax [ |- eq A A ]
is a boxed contextual type: any contextual type may
be surrounded by a box [ ]
to embed it as a base computation type.
Similarly, contextual objects may be boxed to form computation expressions.
Another example theorem trans
, that equality is transitive, can be expressed
as
[ |- eq A B] -> [ |- eq B C] -> [ |- eq A C]
Similar to in a pure LF type (see here), free metavariables appearing in a computation type are automatically abstracted over using PiBox at the front. So really, the above example type is elaborated into
{A : |- tp} {B : |- tp} {C : |- tp}
[ |- eq A B] -> [ |- eq B C] -> [ |- eq A C]
But again as in a pure LF type, these quantifiers are implicit and there is no
way for a programmer to explicitly supply an instantiation for them. The
instantiations are found when the theorem trans
is used.
This example illustrates the simple function space, written with the arrow
->
. In Beluga, the dependent function space (using the PiBox syntax)
is separate from the simple function space (using the arrow syntax).
To recap, the formal grammar of computation types is the following.
where B
denotes an inductive type and U
denotes a
contextual type. The remaining two forms are the simple and dependent function
space, respectively.
Metavariables¶
The syntax {A : |- tp} ...
expresses what’s called a PiBox-type. This
example quantifies over the contextual type |- tp
and binds A
as a
metavariable. Whenever a metavariable is used, as A
is used for example
in eq A A
, it is given a substitution. This substitution mediates between
the LF context of the metavariable and the LF context at the use site. If no
explicit substitution is provided, an identity substitution is assumed. An
identity substitution is sufficient in the example because the context of both
the metavariable and its use site is the empty context.
For example, suppose we have the metavariable X : (x : tm, y : tm |-
tp)
. (Perhaps X
refers to x, y |- arr x y
.) Then, X[base, base] : |-
tp
. Here we use an explicit substitution to instantiate the bound variables
x
and y
.
Context variables and schemas¶
Beluga provides a mechanism for abstracting over and quantifying over contexts. An abstract context is referred to by a context variable. A context variable is a special form of metavariable.
Whereas kinds classify types, contexts are classified by schemas. A
schema essentially lists the possible types of the variables occurring in the
context. The following declaration defines a schema ctx
that can contain
only types.
schema ctx = tp;
Before we can elaborate an example demonstrating context variables, first consider the following syntax of terms for the simply typed lambda calculus as well as a typing judgment for this language.
LF tm : type =
| app : tm -> tm -> tm
| lam : tp -> (tm -> tm) -> tm
;
LF oft : tm -> tp -> type =
| t_app : oft M (arr A B) -> oft N A ->
% ---------------------------
oft (app M N) B
| t_lam : ({x : tm} oft x A -> oft (M x) B) ->
% ----------------------------------
oft (lam A M) (arr A B)
This syntax of terms includes a type annotation on lambda abstractions, and the
typing judgment ensures that it is the type given in the annotation that is used
as the parameter x
’s type in the premise of the t_lam
rule.
This language admits type uniqueness. That is, given two typing derivations for the same term, the types assigned to that term must be equal. We can state this theorem as a computation type in Beluga as follows.
(g : ctx) [g |- oft M A1[]] -> [g |- oft M A2[]] -> [|- eq A1 A2]
First, notice the syntax (g : ctx) ...
. This is called implicit context
quantification.
Unlike for ordinary implicit metavariables such as M
, the
schema of an implicit context variable cannot be inferred by type
reconstruction. Therefore, one must use implicit context quantification to
explicitly specify the schema of the context variable.
Second, notice that the metavariables A1
and A2
, referring to types, are
associated with the substitution []
in the assumptions of the theorem. Type
reconstruction is in some sense a greedy algorithm, so had these substitutions
been left out, the type of A1
, upon appearing in g |- oft M A1
, would be
g |- tp
. But this makes no sense because types ought to be closed in the
simply-typed lambda calculus. To specify that the metavariables A1
and
A2
must be closed, we associate them with a weakening substitution
[]
. This way, type reconstruction will correct infer that the context of
these metavariables is empty.
Confusingly, the reported error had the weakening substitutions been omitted
would be relating to the occurrences of A1
and A2
in |- eq A1
A2
. Here, the implicit identity substitution would be ill-typed. Recall that
the type of A1
, for instance, would have been inferred as g |- tp
and
the identity subsitution would need to send the metavariable’s context g
to
the empty context, which it does not. In general, when dealing with ill-typed
substitution errors, it is worth paying close attention to every occurrence of
any relevant metavariables.
Unboxing¶
When one has a computational variable referring of a boxed contextual type, one
frequently likes to promote this variable to a metavariable. This process is
called unboxing.
For example, suppose we have the assumption x : [|- tp]
.
- In Beluga, one writes
let [_ |- X] = x in ...
in order to unboxx
as the metavariableX
. - In Harpoon, one uses the
unbox
tactic for this:unbox x as X
.
LF Subordination¶
LF declarations in Beluga may or may not refer to earlier declarations. Therefore, it is possible to ascertain that certain derivations cannot depend on certain contextual assumptions, and to eliminate these assumptions. As a concrete example, consider this signature.
LF unit1 : type =
| u1 : unit1
;
LF unit2 : type =
| u2 : unit2
;
schema ctx = block a1 : unit1, a2 : unit2 ;
The schema ctx
consists of blocks (pairs) consisting of a unit1
and a
unit2
. Now suppose we have a derivation of type
g, b : block a1 : unit1, a2 : unit2 |- unit1
where g : ctx
.
This derivation builds a unit1
, and by looking at the definition of that
type, we can see that there is no way any unit2
could be involved in the
construction of that derivation.
Strengthening¶
Beluga recognizes as legitimate any program that would drop such irrelevant
assumptions: this is called strengthening.
In Harpoon this is accomplished by using the strengthen
tactic. In a Beluga program, one uses pattern matching together with
an explicit substitution that witnesses the strengthening. Suppose in the below
example that x : [g, b : block a1 : unit1, a2 : unit2 |- unit1]
. To
strengthen this, one would use a construction as follows.
let [g, b : block a1 : unit1, a2 : unit2 |- X[.., b.1]] = x in ...
The synthesized type for X
would be g, x1 : unit1 |- unit1
. This is
somewhat confusing because the substitution that witnesses the strengthening
here is in fact a substitution that weakens X
.
Beluga’s subordination analysis over the loaded signature accounts for
transitive dependencies between LF declarations. Beluga will consider
strengthening notably during coverage checking as well as when synthesizing the
type of unknowns, written as _
in LF terms.
Inductive Types¶
Attention
This page is a stub.
Interactive Proving with Harpoon¶
Harpoon is an interactive prover for Beluga. Users of Harpoon develop proofs in a REPL using a small set of built-in tactics. These correspond to the main proof strategies one uses in developing metatheory proofs, e.g. inversion or appeal to a lemma or induction hypothesis.
Prover Structure¶
Harpoon is structured in a hierarchical fashion: the prover maintains a number of sessions, each contains a number of theorems, each of which contains a number of subgoals. A theorem with no subgoals remaining is complete, and similarly, a session with no theorems remaining is complete.
Session¶
Harpoon organizes a set of mutually inductive theorems into a session. Often, there is only one session at a time in Harpoon, but more than one simultaneous session is possible. For example, you might want to start a second session if you decide to prove a lemma while in the middle of another proof.
Changing sessions¶
One can switch between theorems by using the select command. Selecting a theorem belonging to another session will cause a session switch, which is a somewhat delicate process.
In order to prevent incomplete theorems in different sessions from referring to each other, all theorems belonging to other sessions are not in scope. When switching sessions, the theorems in the active session are moved out of scope, and the theorems in the destination session are brought into scope. Crucially, this is to prevent undesirable circularities between theorems.
It is important to be aware of this limitation, as it means that lemmas must be proven before they can be used.
Session configuration wizard¶
The series of interactive prompts that appear when Harpoon is started is called the session configuration wizard. This wizard appears when there are no sessions in the prover, and unless there were incomplete proofs in the loaded signature, there will be no sessions when Harpoon starts.
The wizard prompts for three things about each theorem:
- The name of the theorem. This is how the theorem is referred to for induction hypotheses, and how other theorems can refer to this theorem.
- The statement of the theorem. This is a Beluga computational type. (See Contextual LF.)
- The induction order of the theorem. Non-inductive theorems can simply leave this blank. Inductive theorems specify the order numerically, by giving the position of the parameter to induct on. Only explicit parameters are counted, and counting proceeds from left to right.
Note
Implicit context quantifiers are not counted for the numeric induction order. That is, in
(g : ctx) [g |- oft M A] -> [g |- step M M'] -> [g |- oft M' A]
the position of the parameter of type [g |- step M M']
is still 2
.
For example, here is how one might configure a type preservation proof for the simply-typed lambda calculus.
Name of theorem: tps
Statement of theorem: [|- oft M A] -> [|- step M M'] -> [|- oft M' A]
Induction order: 2
Once a theorem is configured, the wizard will repeat so you can configure
additional theorems to be proven mutually. To end the wizard, use an empty
theorem name or the special name :quit
. Ending the wizard without
configuring any theorems will abort the creation of the new session. If there
are no other sessions in Harpoon, then Harpoon will exit.
Loading goals from a file¶
Harpoon proofs are recorded into signature files as a proof script. This is a
structured language whose core constructs close resembles the syntax of
Proof tactics, which the following section in this document will describe.
In a signature file, a proof named foo
is declared as follows.
proof foo : tau = P;
where tau
is a computation type and P
is the body of the proof.
However, when a proof script is saved back using session commands,
this P
contains some subgoals left. When harpoon loads a signature file with
such incomplete proofs, harpoon will load the proofs into a session so that a user doesn’t
need to repeat the session wizard. It is still possible to open the session wizard using
session commands in a case when a user want to use it to start
a new proof.
Subgoal¶
Proofs are developed by applying a tactic to a subgoal. If the tactic is successful, the subgoal it is applied to is solved and removed from its theorem. New subgoals may be introduced by the tactic. These subgoals are understood as children of the subgoal that was eliminated.
Subgoal prompt¶
To apply a tactic, one types the corresponding command into the subgoal prompt. This prompt is the main point of interaction with Harpoon. Consider this example subgoal prompt from the beginning of a type preservation proof for the simply-typed lambda calculus.
intros
Meta-context:
M : ( |- tm) (not in scope)
A : ( |- tp) (not in scope)
M' : ( |- tm) (not in scope)
Computational context:
x : [ |- oft M A]
x1 : [ |- step M M']
-------------------------------
[ |- oft M' A]
>
The subgoal prompt shows the prover state at the subgoal. This state contains three key pieces of information.
- The subgoal label. Every subgoal in Harpoon is identified by a subgoal
label. This label indicates where in the proof this subgoal is located. In
the example, the label is
intros
at the very top, and demonstrates that this subgoal is right after having introduced the assumptions of the theorem. - The contexts. Harpoon uses Beluga’s
indexed type system in which one
distinguishes between metavariables and computational
variables. Metavariables belong to the meta-context and computational
variables belong to the computational context. Notice that the metavariables
in the example are all marked
(not in scope)
. This annotation is presented for implicit parameters: recall that in the statement of the theorem, the parametersM
,A
andM'
appeared free. - The goal. Below the line, the type of the subgoal appears. As tactics are applied and new subgoals are introduced, one can expect the goal type to change. Broadly speaking, one’s objective is to construct a term of this type.
Administrative tactics¶
There are a number of tactics in Harpoon that do not contribute directly to the development of the proof, but are used to manipulate the state of the prover. To distinguish these from the proof tactics, we call these administrative tactics. Despite not contributing to the development of the proof, administrative tactics are nonetheless entered into the subgoal prompt.
See here for the complete list of administrative tactics.
Proof automation¶
Harpoon provides rudimentary automation for basic tasks during proofs.
Each automation type can be controlled via the toggle-automation
tactic.
Automations run at the moment that a new subgoal is created.
Actions performed by automatic tactics can be undone.
auto-intros
¶
This automation introduces assumptions into the context whenever the subgoal has a function type.
auto-solve-trivial
¶
This automation solves subgoals in which the context contains an assumption whose type is convertible with goal type.
It will never solve the last remaining subgoal in a theorem, as this makes
certain theorems impossible to prove using Harpoon. For example, this is
essential for implementing a function such as double : [|- nat] -> [|- nat]
:
auto-intros
will bring x : [|- nat]
into the context and
auto-solve-trivial
would immediately finish the theorem before the user ever
sees a subgoal prompt.
actions capabilities.
- Current limitations:
- no paramater variables
- no substitution variables
- no case splits (>1 case produced)
- no pair type
- no splitting on contexts
auto-invert-solve
¶
This automation attempts to solve the subgoal if no variable splitting (other than inversions) is required to solve
the goal. It performs 2 iterations of depth-first proof-search, once on the computation level, and
once again on the LF level. Use auto-invert-solve INT
to specify the maximum depth
you want your search tree to reach. Depth is incremented when we attempt to
solve a subgoal.
For example, if we want to solve Ev [|- S (S (S (S z)))]
this would require
a depth bound of at least 2::
inductive Ev : [⊢ nat] → ctype =
| ZEv : Ev [⊢ z]
| SEv : Ev [⊢ N] → Ev [⊢ S (S N)]
depth = 0
solve for Ev [|- S (S (S (S z)))] -> focus on SEv --->
depth = 1
solve for Ev [|- S (S z)] -> focus on SEv --->
depth = 2
solve for Ev [|- z] -> focus on zEv
Note: If a goal has more than one subgoal, depth only increments by 1.
For example, if we want to solve ``Less_Than [|- S z] [|- S (S (S z))]`` this
would require depth bound of 3:
inductive Less_Than : [⊢ nat] → [⊢ nat] → ctype =
| ZLT : Less_Than [⊢ z] [⊢ S N]
| LT : Less_Than [⊢ N] [⊢ M] → Less_Than [⊢ S N] [⊢ S M]
| Trans_LT : Less_Than [⊢ N] [⊢ M]
→ Less_Than [⊢ M] [⊢ K] → Less_Than [⊢ N] [⊢ K]
depth = 0
solve for Less_Than [|- S z] [|- S (S (S z))] -> focus on Trans_LT --->
depth = 1
solve for Less_Than [|- M] [|- S (S (S z))] -> focus on LT --->
depth = 2
solve for Less_Than [|- M'] [|- S (S z)] -> focus on LT --->
depth = 3
solve for Less_Than [|- M''] [|- S z] -> focus on ZLT
---> found LT LT ZLT : Less_Than [|- S (S z)] [|- S (S (S z))]
depth = 1
solve for Less_Than [|- S z] [|- S (S z)] -> focus on LT --->
depth = 2
solve for Less_Than [|- z] [|- S z] -> focus on ZLT
---> found LT ZLT : Less_Than [|- S z] [|- S (S z)]
-> found Trans_LT (LT ZLT) (LT LT ZLT) : Less_Than [|- S z] [|- S (S (S z))]
inductive-auto-solve
¶
This automation will perform a case split on the user-specified variable then call auto-invert-solve on each sub case. Use inductive-auto-solve INT
to specify the maximum depth you want your search tree to reach. Depth is incremented as above.
Interactive Command Reference¶
Administrative tactics¶
Administrative tactics are used to navigate the proof, obtain information about functions or constructors, or to prove a lemma in the middle of another proof.
undo
¶
Undoes the effect of a previous proof tactic. See Undo.
theorem list
¶
Lists all theorems in the current session.
theorem defer
¶
Moves the current theorem to the bottom of the theorem stack, selecting the next theorem.
See select for a more flexible way to select a theorem.
theorem show-ihs
¶
Display the induction hypotheses available in the current subgoal.
Note
This is a debugging command, and the output is not particularly human-readable.
theorem dump-proof PATH
¶
Records the current theorem’s partial proof to PATH
.
theorem show-proof
¶
Displays the current theorem’s partial proof.
session list
¶
Lists all active sessions together with all theorems within each session.
session defer
¶
Moves the current session to the bottom of the session stack and selects the next one.
See select for a more flexible way to select a theorem.
session create
¶
Creates a new session. This command will start the Session configuration wizard for setting up the theorems in the new session.
session serialize
¶
Saves the current session as partial proofs to the signature. In other words, any work done interactively will be reflected back into the loaded signature.
subgoal list
¶
Lists all remaining subgoals in the current theorem.
subgoal defer
¶
Moves the current subgoal to the bottom of the subgoal stack and selects the next one.
select
¶
select NAME
selects a theorem by name for proving.
See the session list command.
Note
When selecting a theorem from another session, be aware of the consequences this has on scoping. See Changing sessions.
rename
¶
Note
Renaming is poorly supported at the moment.
The resulting Harpoon proof script that is generated by interactive proving will not contain the renaming, and this could lead to accidental variable capture.
Renames a variable. Use rename meta SRC DST
to rename a metavariable and
rename comp SRC DST
to rename a program variable.
toggle-automation
¶
Use toggle-automation AUTO [STATE]
to change the state of proof automation
features. See Proof automation for available values for AUTO
.
Valid values for STATE
are on
, off
, and toggle
. If unspecified,
STATE
defaults to toggle
.
Proof tactics¶
intros
¶
Use intros [NAME...]
to introduce assumptions into the context.
Restrictions:
- The current goal type is either a simple or dependent function type.
For Pi-types, the name of the assumption matches the name used in the Pi. For arrow-types, names will be taken from the given list of names, in order. If no names are given explicitly, then names are automatically generated.
On success, this tactic will replace the current subgoal with a new subgoal in which the assumptions are in the context.
Note
It is uncommon to use this tactic directly due to automation.
split
¶
Use split EXP
to perform case analysis on the synthesizable expression EXP
.
Restrictions:
- The expression
EXP
and its synthesized type may not contain uninstantiated metavariables.
On success, this tactic removes the current subgoal and introduces a new subgoal
for every possible constructor for EXP
.
msplit
¶
Use msplit MVAR
to perform case analysis on the metavariable MVAR
.
This command is syntactic sugar for split [_ |- MVAR]
.
invert
¶
Use invert EXP
to perform inversion on the synthesizable expression
EXP
.
This is the same as using split EXP
, but invert
will check that a unique
case is produced.
impossible
¶
Use impossible EXP
to eliminate the uninhabited type of the synthesizable
expression EXP
.
This is the same as using split EXP
, but impossible
will check that zero
cases are produced.
by
¶
Use by EXP as VAR [MODIFIER]
to invoke a lemma or induction hypothesis
represented by the synthesizable expression EXP
and bind the result to the
name VAR
.
The optional parameter MODIFIER
specifies at what level the binding occurs.
Valid values for MODIFIER
are
boxed
(default): the binding is made as a computational variable.unboxed
: the binding is made as a metavariable.strengthened
: the binding is made as a metavariable, and its context is strengthened according to LF Subordination.
Restrictions:
- The defined variable
VAR
must not already be in scope. EXP
and its synthesized type may not contain uninstantiated metavariables.- (For
unboxed
andstrengthened
only.) The synthesized type must be a boxed contextual object.
On success, this tactic replaces the current subgoal with a subgoal having one additional entry in the appropriate context.
Tip
LF terms whose contexts contain blocks are not in principle eligible for
strengthening. But such a context is equivalent to a flat context, and
Beluga will automatically flatten any blocks when strengthening.
Therefore, strengthened
has a secondary use for flattening.
strengthen
¶
The command strengthen EXP as X
is syntactic sugar for by EXP as X
strengthened
.
See also by.
solve
¶
Use solve EXP
to complete the proof by providing an explicit checkable
expression EXP
.
Restrictions:
- The expression
EXP
must check against the current subgoal’s type.
On success, this tactic removes the current subgoal, introducing no new subgoals.
suffices
¶
Use suffices by EXP toshow TAU...
to reason backwards via the synthesizable
expression EXP
by constructing proofs for each type annotation TAU
.
This command captures the common situation when a lemma or computational constructor can be used to complete a proof, because its conclusion is (unifiable with) the subgoal’s type. In this case, it suffices to construct the arguments to the lemma or constructor.
The main restriction on suffices
is that the expression EXP
must
synthesize a type of the form
{X1 : U1} ... {Xn : Un} tau_1 -> ... -> tau_k -> tau
Thankfully, this is the most common form of type one sees when working with Beluga.
Restrictions:
- The expression
EXP
must synthesize a compatible type, as above. - Its target type
tau
must unify with the current goal type. - Each type
tau_i
must unify with thei
th type annotation given in the command. - After unification, there must remain no uninstantiated metavariables.
Tip
Sometimes, not all the type annotations are necesary to pin down the
instantiations for the Pi-bound metavariables.
Instead of a type, you can use _
to indicate that this type annotation
should be uniquely inferrable given the goal type and the other specified
annotations. It is not uncommon to use suffices by i toshow _
.
Tip
suffices
eliminates both explicit and implicit leading Pi-types via
unification. It can sometimes be simpler to manually eliminate leading
explicit Pi-types via partial application:
suffices by i [C] ... toshow ...
.
When explicit Pi-types are manually eliminated, the need for a full type
annotation is less common.
On success, one subgoal is generated for each tau_i
, and the current subgoal
is removed.
In principle, this command is redundant with solve
because one could just
write solve EXP
to invoke the lemma directly, but this can be quite
unwieldy if the arguments to the lemma are complicated. Furthermore, the
arguments would need to be written as Beluga terms rather than interactively
constructed.
Note
The user-provided type annotations TAU...
are allowed to refer to
metavariables marked (not in scope)
.
However, it is an error if an out-of-scope metavariable appears in the
instantiation for an explicitly Pi-bound metavariable.
actions capabilities.
- Current limitations:
- no paramater variables
- no substitution variables
- no case splits (>1 case produced)
- no pair type
- no splitting on contexts
auto-invert-solve
¶
This automation attempts to solve the subgoal if no variable splitting (other than inversions) is required to solve
the goal. It performs 2 iterations of depth-first proof-search, once on the computation level, and
once again on the LF level. Use auto-invert-solve INT
to specify the maximum depth
you want your search tree to reach. Depth is incremented when we attempt to
solve a subgoal.
For example, if we want to solve Ev [|- S (S (S (S z)))]
this would require
a depth bound of at least 2::
inductive Ev : [⊢ nat] → ctype =
| ZEv : Ev [⊢ z]
| SEv : Ev [⊢ N] → Ev [⊢ S (S N)]
depth = 0
solve for Ev [|- S (S (S (S z)))] -> focus on SEv --->
depth = 1
solve for Ev [|- S (S z)] -> focus on SEv --->
depth = 2
solve for Ev [|- z] -> focus on zEv
Note: If a goal has more than one subgoal, depth only increments by 1.
For example, if we want to solve ``Less_Than [|- S z] [|- S (S (S z))]`` this
would require depth bound of 3:
inductive Less_Than : [⊢ nat] → [⊢ nat] → ctype =
| ZLT : Less_Than [⊢ z] [⊢ S N]
| LT : Less_Than [⊢ N] [⊢ M] → Less_Than [⊢ S N] [⊢ S M]
| Trans_LT : Less_Than [⊢ N] [⊢ M]
→ Less_Than [⊢ M] [⊢ K] → Less_Than [⊢ N] [⊢ K]
depth = 0
solve for Less_Than [|- S z] [|- S (S (S z))] -> focus on Trans_LT --->
depth = 1
solve for Less_Than [|- M] [|- S (S (S z))] -> focus on LT --->
depth = 2
solve for Less_Than [|- M'] [|- S (S z)] -> focus on LT --->
depth = 3
solve for Less_Than [|- M''] [|- S z] -> focus on ZLT
---> found LT LT ZLT : Less_Than [|- S (S z)] [|- S (S (S z))]
depth = 1
solve for Less_Than [|- S z] [|- S (S z)] -> focus on LT --->
depth = 2
solve for Less_Than [|- z] [|- S z] -> focus on ZLT
---> found LT ZLT : Less_Than [|- S z] [|- S (S z)]
-> found Trans_LT (LT ZLT) (LT LT ZLT) : Less_Than [|- S z] [|- S (S (S z))]
inductive-auto-solve
¶
This automation will perform a case split on the user-specified variable then call auto-invert-solve on each sub case. Use inductive-auto-solve INT
to specify the maximum depth you want your search tree to reach. Depth is incremented as above.
Undo¶
Harpoon includes an undo
command to revert previous actions. Undo history is
stored on a per-theorem basis, so ensure that the correct theorem is selected
when executing undo
.
Only commands that effect a change to the subgoal list can be
undone. Concretely, this means that Administrative tactics cannot be
undone, since they do not introduce nor eliminate subgoals.
Harpoon also includes a command redo
that will undo the effect of the last undo
command. If a change to the subgoal list is effected, the redo history is
purged. In other words, the history is stored linearly and no “undo tree”
is available.
To see the state of the history, use the history
command. This will show the
history of commands that can be undone as well as whether any commands that can
be redone.
Limitations¶
Undo cannot undo things such as creating new sessions. It also cannot undo the
command that completes a proof, as beyond that point there is no more
subgoal prompt through which the user could type undo
.
Serializing the current Harpoon state will also cause the undo history to be lost. This is because serialization will cause Harpoon to reload all state from the signature. From Harpoon’s point of view, the list of subgoals it then has comes simply from the signature, not from a history of tactics.
To work around these limitations, we currently suggest manually undoing the offending actions by editing the proof script.
Funding¶
This research has been funded through: NSERC (Natural Science and Engineering Research Council), FQRNT Recherche d’Equipe, PSR-SIIRI Projets conjoints de recherche et d’innovation and 63e session de la Commission permanente de coopération franco-québécoise by Ministère du Développement économique, de l’Innovation et de l’Exportation au Quebec.