Spilling

A rule that threads a value from one call straight into the next has to name that value, and a name like Tmp or Aux1 carries no information: it is there only to plumb the two calls together. Spilling removes the name. { E } (Terms) lifts the goal E out to just before the smallest enclosing predicate call and puts a fresh variable in its place, one for each argument E is not given (its output arguments, so one in the common case):

f X R :- foo X Y, bar Y R.      % written out, with the plumbing variable Y
f X R :- bar {foo X} R.         % the same, spilled

If E has several missing arguments, { E } becomes that many variables at once:

g X R :- h X Y Z, k Y Z R.      % h has two outputs
g X R :- k {h X} R.             % {h X} stands for the two of them, Y and Z

For this to work Elpi has to know the spilled predicate’s arity, from its pred / func signature (Type declarations); an undeclared predicate cannot be spilled.

The spilled call is inserted just before the closest call recognised as a predicate, which today means a monomorphic, first-order signature; a polymorphic or higher-order argument position (as in an anonymous predicate passed to std.map) may place it one level up if the callee’s own type isn’t precise enough to pin it down. elpi -print FILE prints a program after spilling (and other compilation passes), so any doubt about where a particular { } ended up can be checked directly.

Here {std.rev L} becomes a fresh variable produced by an std.rev goal inserted right before std.append:

../code/palindrome.elpi:

1% Example used in the "Spilling" chapter.
2% The spilled call {std.rev L} is rewritten to a fresh variable bound by an
3% std.rev goal inserted just before std.append.
4
5pred make-palindrome list A -> list A.
6make-palindrome L Result :- std.append L {std.rev L} Result.
7
8main :- make-palindrome [1, 2, 3] R, print R.
[1, 2, 3, 3, 2, 1]

Spilling into a binder

Under pi and in the conclusion of ==>, the spilled goal is placed inside the binder, so its fresh variable can still mention the names and hypotheses in scope there. A pretty-printer that recurses under lam spills {pp (F x)} right where the fresh x and its pp x "x" hypothesis are visible:

../code/spilling-under-binder.elpi:

 1% Spilling reaches into a binder: in the `lam` rule, `{pp (F x)}` is lifted
 2% to a `pp (F x) R` goal placed *inside* the `pi x\ ... ==>`, so it can use the
 3% fresh `x` and the `pp x "x"` hypothesis. The `app` rule spills twice in one
 4% `is`, both lifted before the enclosing call.
 5
 6data tm.
 7symb app tm -> tm -> tm.
 8symb lam (tm -> tm) -> tm.
 9
10func pp tm -> string.
11pp (app F X) S :- S is "(" ^ {pp F} ^ " " ^ {pp X} ^ ")".
12pp (lam F) S :- pi x\ pp x "x" ==> S is "\\x." ^ {pp (F x)}.
13
14main :- pp (lam f\ app f f) S, print S.
\x.(x x)

elpi -print FILE shows where the spilled goal landed: in the lam rule it sits after the ==>, inside the pi and under its own sigma, equivalent to writing

pp (lam F) S :- pi x\ pp x "x" ==> sigma R\ (pp (F x) R, S is "\x." ^ R).

Only the conclusion of an implication may spill. A { } in the hypothesis is rejected at compile time (Spilling in negative position is forbidden), since the lifted goal would have to run before the hypothesis it depends on is in scope.

Spilling out of a term-level lambda

A { } inside a lambda that is passed as data, not the body of a pi or an implication, goes the other way: the goal is lifted out past the lambda and the fresh variable is η-expanded so it can still depend on the bound name. apply (x\ {f x}) becomes

main :- apply (x\ {f x}).
% becomes
main :- pi c\ f c (R c), apply (x\ R x).

The spilled f c runs once, before apply, with c a fresh constant. This is only meaningful when f does not actually need c’s value; a { } under a term-level lambda that genuinely depends on the bound variable is usually a mistake.

Spilling a conjunction

When { } wraps a conjunction, only its last conjunct is spilled, and that conjunct’s missing output is what becomes the fresh variable. The earlier conjuncts run in place, typically to bind a variable the last one then reads:

foo :- pi x\ f {g x Y, h Y}.
% becomes
foo :- pi x\ sigma S\ (g x Y, h Y S), f S.

g x Y runs first and binds Y; h Y is the spilled conjunct, so h Y S computes the value S that takes the place of the whole { }.