Logic Programming
Elpi computes by reducing a query to smaller goals. What follows is when a rule applies to a goal, and what a rule means on its own. The surface syntax it refers to is the Syntax part of the manual, linked as it comes up. Elpi’s model is Prolog’s, rules and backtracking search, with λProlog’s treatment of syntax that contains binders layered on top.
Elpi vs Prolog
Elpi keeps Prolog’s core. Data is built from constructors, which in Elpi are declared and typed (Type declarations) rather than used freely as in Prolog. A program is a set of rules, each one a self-contained unit of meaning. And the search is backtracking: rules are tried in order, and when a goal fails the search retreats to the most recent still-open choice and takes its next alternative.
Where Elpi departs is in how a rule’s head meets the goal. Prolog unifies the two. Elpi unifies only a predicate’s output arguments and one-way matches the input ones: the head may inspect the goal but not instantiate a variable the goal left unbound. Which arguments are which is fixed by the signature (Type declarations); what the split is for is Constraints. This is the matching clauses idea of B-Prolog, marked per argument rather than per clause.
A head need not be linear: a variable may occur in it more than once, which forces the two positions to hold the same term:
pred symmetric tree.
symmetric leaf.
symmetric (node _ T T).
symmetric succeeds on a tree that is a leaf, or a node whose two
children are the very same term; the repeated T in the second head is
what ties them together. (It does not recurse, so it does not check that the
children are themselves symmetric.)
Several rules may apply to one goal. Elpi tries them top to bottom and
backtracks into the next on failure. The only control over this search is the
cut, ! (Inference rules and queries), and Elpi’s is a
hard cut: besides discarding the current rule’s untried alternatives, it
also discards any alternative solutions still available for the premises
already solved earlier in the same rule.
A rule is meaningful in isolation: adding one to a program, or taking one away, is a well-defined change, unlike an assignment or a function definition, which only mean something in a larger context. It is closer to adding or removing an axiom in a proof system. This is why the order in which rules are accumulated matters, why their relative order matters too once cut is in play, and why File structure and attributes gives a rule a name and a place among its siblings.
Elpi vs λProlog
λProlog adds binders. An object-language variable is represented directly by a bound variable of the programming language, a technique called Higher-Order Abstract Syntax, or λ-tree syntax. A simply-typed λ-calculus term is declared as
data tm.
symb app tm -> tm -> tm.
symb lam (tm -> tm) -> tm.
lam takes an Elpi function of type tm -> tm: the identity λx.x is
written lam x\ x, and applying that function to a term substitutes the
term for the bound x, with no substitution code to write. A bound
variable like x is not a unification variable: it can never be assigned,
only substituted for, and it is fresh, distinct from every other name in
scope.
Type-checking this calculus needs one ingredient a Prolog rule cannot express
on its own: a rule that holds only for the lifetime of one bound variable’s
scope. pi introduces a fresh constant for the duration of a goal, and
==> adds a rule for the duration of a goal; together they give a bound
variable exactly the context it needs. Types are declared like terms, with a
constructor arr for the function space:
data ty.
symb arr ty -> ty -> ty.
pred of tm -> ty.
of (app H A) T :- of H (arr S T), of A S.
of (lam F) (arr S T) :- pi c\ of c S ==> of (F c) T.
To type-check lam F, the second rule introduces a fresh constant c for
the bound variable, adds of c S for the rest of that goal only, and checks
the body F c. The application rule is an ordinary Horn clause; the
abstraction rule needs the ∀ and nested ⇒ of a Hereditary Harrop
formula, the richer logic that λProlog, and so Elpi, is built on. A rule
added by ==> is hypothetical: unlike Prolog’s assert / retract
its scope is exactly the goal that added it, never the rest of the program
(Inference rules and queries). The full checker, with
weak-head reduction, is worked through in Simply-typed λ-calculus.
Run on a closed term, the checker infers a type without committing to what the bound variables stand for:
goal> Fst = (lam x\ lam y\ x), of Fst Ty.
Success:
Fst = lam c0 \ lam c1 \ c0
Ty = arr X0 (arr X1 X0)
This is as far as λProlog alone goes. Elpi is meant to type-check incomplete
terms as well, the terms an interactive prover’s elaborator manipulates, still
full of holes to be filled in. Ask the checker above for of X T with X
an unknown hole and nothing useful happens: because the first argument of
of is an input, no rule head matches a bare variable, and the goal simply
fails. Made an output instead, the way plain λProlog with no modes would have
it, the goal does worse: the first rule matches X against app H1 A1
for fresh H1, A1, then against app H2 A2 inside that, and so on
without end. Neither outcome is what an elaborator needs. The way out is to
suspend the goal on the hole until its shape is known instead of guessing,
which is covered in Constraints.