Constraints
The previous chapter, Logic Programming, ended on a problem. The
STLC type checker, asked about a term that still has a hole in it, has nothing
sensible to do:
because its first argument is an input, no rule head matches a bare variable
and the goal simply fails. With unification in place of matching it does
worse, inventing an app (then a lam inside that, and so on) for the
hole and looping forever. Neither is any use to an elaborator, which works
with such incomplete terms, full of holes still to be filled in, all the
time. The fix is to let the checker suspend on a hole rather than guess.
Controlling instantiation
When a rule’s search meets a hole, one of two things should happen instead of guessing: suspend the computation, recording it as a constraint to be retried later, or synthesize a value for the hole through some dedicated routine richer than plain unification (out of scope for this chapter). A suspended computation can also carry data along, anything from a single flag to a whole typing sequent, and the data that several suspended computations have attached to one hole can later be combined. Suspending and resuming are what follow; combining what several constraints know about the same hole is covered in The constraint store.
Matching, not unifying
A signature (Type declarations) splits a predicate’s
arguments into input, before the ->, and output, after it. When a
rule is selected the two are treated differently: the goal’s input arguments
are matched against the rule head’s patterns, its output arguments are
unified with them (Formal semantics’s select). The difference
shows as soon as an argument is an unassigned variable. Matching it against a
constant pattern would have to assign it, so it simply fails to match, no rule
fires, and the variable is left untouched. Unifying it succeeds and assigns
it:
../code/input-output.elpi:
1% An input argument is matched: passing an unassigned variable matches no
2% rule, since matching a variable against a rule's pattern would have to
3% assign it. An output argument is unified: it does get assigned.
4
5data tm.
6symb z tm.
7symb s tm -> tm.
8
9pred as-input tm.
10as-input z :- print "matched z".
11as-input (s _) :- print "matched s _".
12
13pred as-output -> tm.
14as-output z.
15
16main :-
17 ( as-input _ ; print "as-input: an unassigned variable matched no rule" ),
18 as-output Y, print "as-output assigned Y to" Y.
as-input: an unassigned variable matched no rule
as-output assigned Y to z
An input argument is therefore safe to leave partly unknown: a call with a
hole in an input position does not force a guess, it finds no matching rule.
And that is what lets a rule detect a hole and act on it. The uvar
keyword is a head pattern that matches only an unassigned variable, and
uvar as E binds E to it. The two rules below are equivalent, the
second spelling out what the first means:
of (uvar as E) T :- declare_constraint (of E T) [E].
% equivalent to:
of E T :- var E, declare_constraint (of E T) [E].
The full set of uvar head patterns is in
Unification and variables. The standalone mode
directive, the older and separate way of
marking arguments input or output, is described in Compatibility with Teyjus, Prolog and legacy Elpi.
declare_constraint (Constraint handling rules) is what
turns a goal into a constraint. Its goal argument must itself be a function,
needing a func signature, because a constraint that could be resumed in
more than one way would make the search unpredictable once constraint handling
rules start combining constraints.
Suspending and resuming
The list passed to declare_constraint is the constraint’s trigger: the
variables whose assignment wakes it up. A constraint honours backtracking
just as a plain unification assignment does: undoing the assignment that
resumed a constraint suspends it again, as though nothing had happened.
../code/backtracking-constraints.elpi:
1% The constraint store honours backtracking: an assignment made and then
2% undone leaves a resumed-and-reverted constraint exactly as it was.
3
4func even int.
5even (uvar as X) :- !, declare_constraint (even X) [X].
6even 0 :- !.
7even N :- N > 0, M is N - 2, even M.
8
9main :-
10 even Y,
11 print "before:", print_constraints,
12 (Y = 3, fail ; true), % assign, resume, fail (3 is not even), backtrack
13 print "after backtracking:", print_constraints.
before:
even X0 /* suspended on X0 */
after backtracking:
even X0 /* suspended on X0 */
The STLC checker from Logic Programming, with one rule added, closes the problem that chapter ended on: a hole in argument position now suspends instead of looping, and resumes when the hole is filled.
../code/holes.elpi:
1% The STLC type checker from "The logic programming model", now handling
2% incomplete terms: a hole in argument position suspends instead of looping.
3
4data tm.
5symb app tm -> tm -> tm.
6symb lam (tm -> tm) -> tm.
7
8data ty.
9symb arr ty -> ty -> ty.
10
11func of tm -> ty.
12of (app H A) T :- of H (arr S T), of A S.
13of (lam F) (arr S T) :- pi c\ of c S ==> of (F c) T.
14of (uvar as E) T :- declare_constraint (of E T) [E].
15
16main :-
17 of (app H _A) _T, % H and A are holes: two constraints appear
18 print "before:", print_constraints,
19 H = (lam x\ x), % filling one hole resumes its constraint
20 print "after:", print_constraints.
before:
of X0 X1 /* suspended on X0 */ of X2 (arr X1 X3) /* suspended on X2 */
after:
of X0 X1 /* suspended on X0 */
Incompatible constraints
Suspending and resuming on their own are not quite enough. Asked about an
unknown N, both even N and odd N suspend without complaint. Yet
no number is both, so a goal that suspended the two of them should have
failed.
../code/incompatible.elpi:
1func even int.
2even (uvar as X) :- !, declare_constraint (even X) [X].
3even 0 :- !.
4even N :- N > 0, !, M is N - 2, even M.
5
6func odd int.
7odd (uvar as X) :- !, declare_constraint (odd X) [X].
8odd 1 :- !.
9odd N :- N > 1, !, M is N - 2, odd M.
10
11main :- even N, odd N, print_constraints.
odd X0 /* suspended on X0 */ even X0 /* suspended on X0 */
Both constraints are accepted, quietly, side by side. The remedy is given in the next chapter (The constraint store).