constant, inductive and constructor are Coq specific
+data types that are opaque to Elpi. Still the gref data type lets you
see what these names point to (a constant, and inductive type or a
constructor).
The coq.env.* family of APIs lets one access the
+}}.
Toplevel input, characters 38-42
+GRnat is linear: name it _GRnat (discard) or GRnat_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
Toplevel input, characters 87-92
+GRplus is linear: name it _GRplus (discard) or GRplus_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
Toplevel input, characters 62-64
+GRs is linear: name it _GRs (discard) or GRs_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
Query assignments:
GRnat = indt «nat»
GRplus = const «Nat.add»
GRs = indc «S»
The coq.env.* family of APIs lets one access the
environment of well typed Coq terms that have a global name.
The fix constructor carries a pretty printing hint,
+ app [global (indc «S»), app [c0, c3, c2]]]
C = «Nat.add»
The fix constructor carries a pretty printing hint,
the number of the recursive argument (starting at 0), the type
of the recursive function and finally the body where the recursive
call is represented via a bound variable
typefixname->int->term->(term->term)->term.
-
A match constructor carries the term being inspected,
+
A match constructor carries the term being inspected,
the return clause
and a list of branches. Each branch is a Coq function expecting in input
the arguments of the corresponding constructor. The order follows the
@@ -311,13 +327,13 @@
The opaque type univ is a universe level variable. Elpi holds a store of
+
The opaque type univ is a universe level variable. Elpi holds a store of
constraints among these variables and provides APIs named coq.univ.* to
impose constraints.
Let's play a bit more with universe constraints using the
-coq.typecheck API:
The diagnostic data type is used by coq.typecheck to
tell if the term is well typed. The constructor ok signals success, while
error carries an error message. In case of success universe constraints
are added to the store.
@@ -541,7 +559,7 @@
Quotations and Antiquotationsfun `b` (global (indt «nat»)) c1 \
app [global (indt «eq»), global (indt «nat»), c0, c1]
Query assignments:
T = fun `ax` (global (indt «nat»)) c0 \
fun `b` (global (indt «nat»)) c1 \
- app [global (indt «eq»), global (indt «nat»), c0, c1]
In some sense Elpi's way of traversing a binder is similar to a Zipper.
The context of Elpi must record the part of the Zipper context that is
relevant for binders.
Note that after the call to coq.typecheck, X0 is assigned the
+
Note that after the call to coq.typecheck, X0 is assigned the
term _\X1, that means that the offending argument has been pruned
(discarded).
Note
-
All APIs taking a term support the @holes! option.
+
All APIs taking a term support the @holes! option.
-
In addition to the @holes! option, there is a class of APIs which can
+
In addition to the @holes! option, there is a class of APIs which can
deal with terms outside the pattern fragment. These APIs take in input a term
-skeleton. A skeleton is not modified in place, as coq.typecheck
+skeleton. A skeleton is not modified in place, as coq.typecheck
does with its first argument, but is rather elaborated to a term related to it.
In some sense APIs taking a skeleton are more powerful, because they can
modify the structure of the term, eg. insert a coercions, but are less
@@ -894,14 +920,20 @@
Here Bo2 is obtained by taking Bo1, considering all
unification variables as holes and all {{Type}} levels as fresh
(the are none in this example), and running Coq's elaborator on it.
Accumulating code via inline text or file is equivalent, the AST of code
is stored in the .vo file (the external file does not need to be installed).
We postpone the description of data bases to a dedicated section.
-
Once all the code is accumulated ElpiTypecheck verifies that the
-code does not contain the most frequent kind of mistakes. This command
-considers some mistakes minor and only warns about them. You can
-pass -w+elpi.typecheck to coqc to turn these warnings into errors.
+
When some code is accumulated Elpi verifies that the
+code does not contain the most frequent kind of mistakes, via some
+type checking and linting. Some mistakes are minor and Elpi only warns about
+them. You can pass -w+elpi.typecheck to coqc to turn these warnings into
+errors.
We can now run our program!
Hello [str world!]
You should see the following output (hover the bubble next to the
code if you are reading this online):
"true" : "bool"
The 3rd term has type "bool" which should be a subtype of "nat".
The command check_arg receives a term T and type checks it, then it
prints the term and its type.
-
The coq.typecheck API has 3 arguments: a term, its type and a
-diagnostic which can either be ok or (error Message).
-The assert-ok! combinator checks if the diagnostic is ok,
+
The coq.typecheck API has 3 arguments: a term, its type and a
+diagnostic which can either be ok or (error Message).
+The assert-ok! combinator checks if the diagnostic is ok,
and if not it prints the error message and bails out.
The first invocation succeeds while the second one fails and prints
the type checking error (given by Coq) following the string passed to
@@ -315,10 +315,10 @@
The command still fails even if we told Coq how to inject booleans values
into the natural numbers. Indeed the Check commands works.
-
The call to coq.typecheck modifies the term in place, it can assign
+
The call to coq.typecheck modifies the term in place, it can assign
implicit arguments (like the type parameter of eq) but it cannot modify the
structure of the term. To do so, one has to use the
-coq.elaborate-skeleton API.
Synthesizing a term typically involves reading an existing declaration
and writing a new one. The relevant APIs are in the coq.env.* namespace
-and are named after the global reference they manipulate, eg coq.env.const
-for reading and coq.env.add-const for writing.
+and are named after the global reference they manipulate, eg coq.env.const
+for reading and coq.env.add-const for writing.
Here we implement a little command that given an inductive type name
generates a term of type nat whose value is the number of constructors
of the given inductive type.
The command starts by locating the first argument and asserting it points to
-an inductive type. This line is idiomatic: coq.locate aborts if
+an inductive type. This line is idiomatic: coq.locate aborts if
the string cannot be located, and if it relates it to a gref which is not
-indt (for example const plus) assert! aborts with the given
+indt (for example const plus) assert! aborts with the given
error message.
-
coq.env.indt lets one access all the details of an inductive type,
+
coq.env.indt lets one access all the details of an inductive type,
here we just use the list of constructors.
-The twin API coq.env.indt-decl lets
+The twin API coq.env.indt-decl lets
one access the declaration of the inductive in HOAS form, which might be
easier to manipulate in other situations, like the next example.
Then the program crafts a natural number and declares a constant for it.
For the sake of introducing copy, the swiss army knife of λProlog, we
+
For the sake of introducing copy, the swiss army knife of λProlog, we
write a command which takes an inductive type declaration and builds a new
one abstracting the former one on a given term. The new inductive has a
parameter in place of the occurrences of that term.
% let's start to craft the new declaration by putting a% parameter A which has the type of P
- NewDecl= parameter "A" explicit PTyDecl',
+ (NewDecl: indt-decl) = parameter "A" explicit PTyDecl',% let's make a copy, capturing all occurrences of P with a% (which stands for the parameter)
- (pia\ copy P a => copy-indt-decl Decl (Decl' a)),
+ (pia\ copy P a ==> copy-indt-decl Decl (Decl' a)),% to avoid name clashes, we rename the type and its constructors% (we don't need to rename the parameters)
@@ -447,8 +447,8 @@
copy (app [global (indt «option»), global (indt «nat»)]) c0
and that rule masks the one for app when the sub-term being copied is
-exactly optionnat. The API copy-indt-decl copies an inductive
+exactly optionnat. The API copy-indt-decl copies an inductive
declaration and calls copy on all the terms it contains (e.g. the
type of the constructors).
-
The copy predicate is very flexible, but sometimes one needs to collect
-some data along the way. The sibling API fold-map lets one do that.
+
The copy predicate is very flexible, but sometimes one needs to collect
+some data along the way. The sibling API fold-map lets one do that.
For example one can use fold-map to collect into a list all the occurrences
+
For example one can use fold-map to collect into a list all the occurrences
of inductive type constructors in a given term, then use the list to postulate
-the right number of binders for them, and finally use copy to capture them.
+the right number of binders for them, and finally use copy to capture them.
and a Db can be later extended via ElpiAccumulate.
+
ElpiDb name.db lp:{{predsome-pred. }}.
and a Db can be later extended via ElpiAccumulate.
As a convention, we like Db names to end in a .db suffix.
A Db is pretty much like a regular program but can be shared among
other programs and is accumulated by name.
Since is a Db is accumulated when a program runs the current
contents of the Db are used.
Moreover the Db can be extended by Elpi programs themselves
-thanks to the API coq.elpi.accumulate, enabling code to save a state
+thanks to the API coq.elpi.accumulate, enabling code to save a state
which is then visible at subsequent runs.
-
The initial contents of a Db, somecode in the example
+
The initial contents of a Db, predsome-pred. in the example
above, is usually just the type declaration for the predicates part of the Db,
and maybe a few default rules.
Let's define a Db.
Extending data bases this way is fine, but requires the user of our command
to be familiar with Elpi's syntax, which is not very nice. Instead,
-we can write a new program that uses the coq.elpi.accumulate API
+we can write a new program that uses the coq.elpi.accumulate API
to extend the Db.
Additions to a Db are a Coq object, a bit like a Notation or a Type Class
instance: these object live inside a Coq module (or a Coq file) and become
active when that module is Imported.
Deciding to which Coq module these
-extra rules belong is important and coq.elpi.accumulate provides
+extra rules belong is important and coq.elpi.accumulate provides
a few options to tune that. Here we passed _, that uses the default
-setting. See the scope and clause data types for more info.
+setting. See the scope and clause data types for more info.
The std.findall predicate gathers in a list all solutions to
-a query, while std.forall iterates a predicate over a list.
-It is important to notice that coq.error is a fatal error which
+
+
bob is24 years old
alice is21 years old
The std.findall predicate gathers in a list all solutions to
+a query, while std.forall iterates a predicate over a list.
+It is important to notice that coq.error is a fatal error which
aborts an Elpi program. Here we shadow the catch all clause with a regular
-failure so that std.findall can complete to list all the results.
+failure so that std.findall can complete to list all the results.
This command does not support these attributes: more,
this. [unsupported-attributes,parsing,default]
[attribute elpi.loc
- (leaf-loc File "(stdin)", line 10, column 31, character 175:),
+ (leaf-loc File "(stdin)", line 10, column 31, characters 175-179:),
attribute elpi.phase (leaf-str interp), attribute this (leaf-str ),
attribute more (node [attribute stuff (leaf-str 33)])]
The first attribute, elpi.loc is always present and corresponds to the
location in the source file of the command. Then we find an attribute for
"this" holding the empty string and an attribute for "more.stuff" holding
the string "33".
Attributes are usually validated (parsed) and turned into regular options
-using coq.parse-attributes and a description of their types using
-the attribute-type data type:
The elpi tactic/command bad failed without giving a
specific error message. Please report this
-inconvenience to the authors of the program.
You should use the coq.error API or the assert! one
-to abort a program. There is a dedicated coq.ltac.fail API to abort
+inconvenience to the authors of the program.
You should use the coq.error API or the assert! one
+to abort a program. There is a dedicated coq.ltac.fail API to abort
tactics.
-
Warnings can be reported using the coq.warning which lets you
+
Warnings can be reported using the coq.warning which lets you
pick a name and category. In turn these can be used to disable or make fatal
your warnings (as any other Coq warning).
The command did perform no (more) actions during the
parsing phase (aka synterp), while during the
execution phase (aka interp) it tried to perform a
@@ -779,8 +779,8 @@
is stored in the .vo file (the external file does not need to be installed).
We invite the reader to look up the description of data bases in the tutorial
about commands.
-
Once all the code is accumulated ElpiTypecheck verifies that the
-code does not contain the most frequent kind of mistakes. This command
-considers some mistakes minor and only warns about them. You can
-pass -w+elpi.typecheck to coqc to turn these warnings into errors.
-
The entry point for tactics is called solve which maps a goal
-into a list of sealed-goal (representing subgoals).
+
When some code is accumulated Elpi verifies that the
+code does not contain the most frequent kind of mistakes, via some
+type checking and linting. Some mistakes are minor and Elpi only warns about
+them. You can pass -w+elpi.typecheck to coqc to turn these warnings into
+errors.
+
The entry point for tactics is called solve which maps a goal
+into a list of sealed-goal (representing subgoals).
Tactics written in Elpi can be invoked by prefixing its name with elpi.
The _Trigger component, which we did not print, is a variable that, when
assigned, triggers the elaboration of its value against the type of the goal
and obtains a value for Proof this way.
-
Keeping in mind that the solve predicate relates one goal to a list of
+
Keeping in mind that the solve predicate relates one goal to a list of
subgoals, we implement our first tactic which blindly tries to solve the goal.
For a simple tactic like blind the list of subgoals is easy to write, since
it is empty, but in general one should collect all the holes in
the value of Proof (the checked proof term) and build goals out of them.
-
There is a family of APIs named after refine, the mother of all
+
There is a family of APIs named after refine, the mother of all
tactics, in
elpi-ltac.elpi
which does this job for you.
Usually a tactic builds a (possibly partial) term and calls
-refine on it.
terms are received in raw format, eg before elaboration
Indeed the type argument to eq is a variable.
-One can use APIs like coq.elaborate-skeleton to infer holes like
+One can use APIs like coq.elaborate-skeleton to infer holes like
X0.
The coq.error aborts the execution of both
Elpi and any enclosing Ltac context. This failure cannot be caught
by Ltac.
-
On the contrary the coq.ltac.fail builtin can be used to
+
On the contrary the coq.ltac.fail builtin can be used to
abort the execution of Elpi code in such a way that Ltac can catch it.
This API takes an integer akin to Ltac's fail depth together with
the error message to be displayed to the user.
-
Library functions of the assert! family call, by default, coq.error.
+
Library functions of the assert! family call, by default, coq.error.
The flag @ltacfail!N can be set to alter this behavior and turn errors into
calls to coq.ltac.failN.
assumption is a very simple tactic: we look up in the proof
context for an hypothesis which unifies with the goal.
-Recall that Ctx is made of decl and def
+Recall that Ctx is made of decl and def
(here, for simplicity, we ignore the latter case).
refine does unify the type of goal with the type of the term,
+Qed.
refine does unify the type of goal with the type of the term,
hence we can simplify the code further. We obtain a
tactic very similar to our initial blind tactic, which picks
candidates from the context rather than from the program itself.
The set tactic takes a term, possibly with holes, and
makes a let-in out of it.
-
It gives us the occasion to explain the copy utility.
+
It gives us the occasion to explain the copy utility.
ElpiTactic find.ElpiAccumulatelp:{{
solve (goal __T_[trm X]) _:-pix\
- copy X x => copy T (Tabs x),
+ (copy X x ==> copy T (Tabs x)),
if (occurs x (Tabs x))
(coq.say "found" {coq.term->string X})
(coq.ltac.fail _"not found").
}}.
-ElpiTypecheck.
+
and that rule masks the one for app when the
sub-term being copied matches (P/\_). The first time this rule
is used X0 is assigned, making the rule represent the term (P/\P).
Now let's refine the tactic to build a let-in, and complain if the
@@ -613,7 +613,7 @@
_T_[str ID, trm X]asG) GL:-pix\
- copy X x => copy T (Tabs x),
+ (copy X x ==> copy T (Tabs x)),
if (occurs x (Tabs x))
(if (coq.ltac.id-free? IDG) true
(coq.warn ID"is already taken, Elpi will make a name up"),
@@ -623,8 +623,8 @@
Crafting by hand the list of subgoal is not easy.
In particular here we did not set up the new trigger for Pa and Pb,
nor seal the goals appropriately (we did not bind proof variables).
refine T (goal _RawEv_Ev_)GS:-RawEv=T, coq.ltac.collect-goals EvGS_.
-
Now that we know the low level plumbing, we can use refine ;-)
+
Now that we know the low level plumbing, we can use refine ;-)
The only detail we still have to explain is what exactly a
-sealed-goal is. A sealed goal wraps into a single object all
+sealed-goal is. A sealed goal wraps into a single object all
the proof variables and the assumptions about them, making this object easy
(or better, sound) to pass around.
if the tactic is a multi-goal one, it will receive all goals
In Elpi you can implement a multi-goal tactic by providing a rule for
-the msolve predicate. Since such a tactic will need to manipulate
+the msolve predicate. Since such a tactic will need to manipulate
multiple goals, potentially living in different proof context, it receives
-a list of sealed-goal, a data type which seals a goal and
+a list of sealed-goal, a data type which seals a goal and
its proof context.
nabla binds all proof variables, then seal
+ (X3 c0 c1) [])]
nabla binds all proof variables, then seal
holds a regular goal, which in turn carries the proof context.
-
In order to operate inside a goal one can use the coq.ltac.open utility,
+
In order to operate inside a goal one can use the coq.ltac.open utility,
which postulates all proof variables using pix\ and loads the proof
context using =>.
Operating on multiple goals at the same time is doable, but not easy.
@@ -833,7 +836,7 @@
the proof term is the same but for the fact that after the tactic the first
+ conj ?Goal0 (conj ?Goal ?Goal0))
the proof term is the same but for the fact that after the tactic the first
and last missing subterm (incomplete proof tree branch) are represented by
the same hole ?Goal0. Indeed by solving one, we can also solve the other.
coq.say is a built-in predicate provided by Coq-Elpi which
+}}.
The age of alice is20
Query assignments:
A = 20
coq.say is a built-in predicate provided by Coq-Elpi which
prints its arguments.
You can look at the output buffer of Coq to see the value for A or hover
or toggle the little bubble after }}. if you are reading the tutorial with a
web browser.
Omega=appDeltaDelta,
whd OmegaHummm, coq.say "not going to happen"
-}}.
elpi run out of steps (1000)
+}}.
Toplevel input, characters 138-142
+Hummm is linear: name it _Hummm (discard) or Hummm_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
The problem with bad is that TMP is fresh each time the rule
-is used, but not every time the anonymous rule passed to map
+}}.
Toplevel input, characters 401-402
+R1 is linear: name it _R1 (discard) or R1_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
Toplevel input, characters 421-422
+R2 is linear: name it _R2 (discard) or R2_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
Toplevel input, characters 441-442
+R3 is linear: name it _R3 (discard) or R3_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
Query assignments:
R1 = X0
R2 = [2, 3, 4]
R3 = [2, 3, 4]
The problem with bad is that TMP is fresh each time the rule
+is used, but not every time the anonymous rule passed to map
is used. Technically TMP is quantified (allocated) where L
and Result are.
There are two ways to quantify TMP correctly, that is inside the
@@ -1002,7 +1028,7 @@
predgood3i:listint, o:listint.
good3 LResult:-piaux\
- (piTMP X R\ aux XR:-TMPisX+1,R=TMP) =>
+ (piTMP X R\ aux XR:-TMPisX+1,R=TMP) ==>
std.map L aux Result.
}}.
@@ -1011,7 +1037,9 @@
Toplevel input, characters 208-208
+R is linear: name it _R (discard) or R_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
Query assignments:
R = [2, 3, 4]
In this case the auxiliary predicate aux
is only visible inside good3.
What is interesting to remark is that the quantifications are explicit
in the hypothetical rule, and they indicate clearly that each and every
@@ -1029,10 +1057,10 @@
Given that programs are not written in a single place, but rather obtained by
-accumulating code, Elpi is able to print a (full) program to an html file
+accumulating code, Elpi is able to print a (full) program to an text file
as follows. The obtained file provides a facility to filter rules by their
-predicate.
Finally, one can bound the number of backchaining steps
+predicate. Note that the first component of the path is a Coq Load Path (i.e.
+coqc options -R and -Q), the text file will be placed in the directory
+bound to it.
+
ElpiPrint stlc "elpi_examples/stlc".
Look at the generated page.
+Finally, one can bound the number of backchaining steps
performed by the interpreter:
In this tutorial we only used :e: ==> but Elpi also provides
+the standard λProlog implication =>. They have the same meaning
+but different precedences w.r.t. ,.
+
The code a, c ==> d, e reads a, (c ==>(d,e)), that means that
+the rule c is available to both d and e.
+
On the contrary the code a, c => d, e reads a, (c ==> d), e,
+making c only available to d.
+
So, => binds stronger than ,, while ==> binds stronger only
+on the left.
+
According to our experience the precedence of => is a common source
+of mistakes for beginners, if only because it is not stable by adding of
+debug prints, that is a => b and a => print "doing b", b have
+very different meaning (a becomes only available to print!).
+
In this tutorial we only used ==> that was introduced in Elpi 2.0, but
+there is code out there using =>. Elpi 2.0 raises a warning if the
+right hand side of => is a conjenction with no parentheses.
+
A concrete example:
The elpi tactic/command stlc failed without giving a
+}}.
Toplevel input, characters 43-58
+The standard λProloginfixoperatorforimplication => has higher precedence
+than conjunction. This means that 'A => B, C' reads '(A => B), C'.
+This is a common mistake since it makes A only available to B (and not to C
+as many newcomers may expect).
+If this is really what you want write '(A => B), C' to silence this warning.
+Otherwise write 'A => (B, C)', or use the alternative implication operator ==>.
+Infix ==> has lower precedence than conjunction, hence
+'A ==> B, C' reads 'A ==> (B, C)' and means the same as 'A => (B, C)'.
+[elpi.implication-precedence,elpi,default]
Toplevel input, characters 48-48
+A is linear: name it _A (discard) or A_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
Toplevel input, characters 58-58
+B is linear: name it _B (discard) or B_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
Toplevel input, characters 66-66
+C is linear: name it _C (discard) or C_ (fresh variable)
+[elpi.linear-variable,elpi.typecheck,elpi,default]
The elpi tactic/command stlc failed without giving a
specific error message. Please report this
inconvenience to the authors of the program.
In the example above once condition holds we start a sequence of
steps which we will not reconsider. Locally, backtracking is still
available, e.g. between generate and test.
-See also the std.spy-do! predicate which prints each and every step,
-and the std.spy one which can be used to spy on a single one.
+See also the std.spy-do! predicate which prints each and every step,
+and the std.spy one which can be used to spy on a single one.