File structure and attributes

A program is one or more files, each a sequence of rules, signatures (Type declarations) and directives. What follows are the directives that stitch files together and organise names, and how they combine into a multi-file program. The attributes that decorate individual rules (:name, :if, :untyped and the rest) are described in Rule attributes.

Accumulating files

accumulate loads another file:

accumulate stdlib.            % loads stdlib.elpi
accumulate parser, printer.   % several at once
accumulate "lib/json".        % a quoted path, for names with . or /

The .elpi extension is added automatically. A plain name is resolved first relative to the file that accumulates it, and then along every directory passed with elpi -I PATH (repeatable); this is how a program accumulates an installed library by name without knowing where it sits on disk. A file is loaded once even if it is accumulated from several places, so accumulation forms a graph, not a tree.

import and accum_sig load Teyjus .mod / .sig files and are covered in Compatibility with Teyjus, Prolog and legacy Elpi.

Namespaces

namespace n { } prefixes every name defined inside the block with n.:

namespace json {
  func parse string -> term.
  parse S T :- tokenize S Ts, parse-tokens Ts T.
}

main :- json.parse Text T.

A name only used inside the block, not defined there, stays global.

namespace blocks nest, and so do their prefixes: a predicate declared in namespace outer { namespace inner { } } is reached from outside as outer.inner.name.

A leading . on a name escapes back to the global scope, which is how code inside a namespace reaches a name it has itself shadowed (see Lexical conventions):

../code/namespaces.elpi:

 1% A namespace can redefine a name; a leading `.` on a name reaches the
 2% global one from inside the namespace that shadows it.
 3
 4func emph string -> string.
 5emph S R :- R is "*" ^ S ^ "*".
 6
 7namespace loud {
 8  func emph string -> string.
 9  emph S R :- .emph S E, R is E ^ "!".   % .emph is the *global* emph
10}
11
12main :- emph "hi" Plain, loud.emph "hi" Loud, print Plain Loud.
*hi* *hi*!

Shortening names

shorten introduces a local alias for a qualified name:

shorten std.{ map, rev }.          % write `map` for `std.map`
shorten std.{ list.map, string.{ concat, escape } }.   % a trie of names

The part before { is dropped, what is inside is kept: the second line makes list.map, string.concat and string.escape stand for the corresponding std.… names. A shorten is in effect until the end of the file or of the enclosing { } block.

A library’s public surface

Namespacing and shorten together let a file expose a small public API and keep its helpers out of the way: nest the helpers under an internal namespace, define the public predicates alongside it (they reach internal.… unqualified, being in the same enclosing namespace), and shorten only the public names for whoever accumulates the file.

../code/library-api.elpi:

 1% A library hides its helpers in a nested namespace and exposes only the
 2% public entry point through `shorten`.
 3
 4namespace mylib {
 5  namespace internal {
 6    func helper int -> int.
 7    helper X Y :- Y is X * 2.
 8  }
 9  func public-api int -> int.
10  public-api X Y :- internal.helper X Y0, Y is Y0 + 1.
11}
12
13shorten mylib.{ public-api }.
14
15main :- public-api 10 R, print "result:" R.
result: 21

Macros

macro @name Args :- Body. defines a macro, expanded at compile time. Macro names start with @ (Lexical conventions). A macro is not a predicate: it disappears before the program runs, and it does not cross a file or block boundary, being visible only in the file (or { } block) that defines it.

macro @newline :- "\n".
macro @of X N T :- (of X T, pp X N).

Expansion is hygienic: a macro’s own variables, those in Body not among Args, get a fresh instance at every expansion, distinct from anything at the use site, even a variable spelled the same way. Writing a macro is therefore as safe as writing a predicate with its own local variables, with no need to pick unlikely names to avoid a clash.

../code/macro-hygiene.elpi:

1% A macro's own variables are fresh at every expansion site, even when they
2% happen to share a name with a variable already in scope where the macro
3% is used.
4
5macro @check X :- (Tmp = X, print "macro's Tmp =" Tmp).
6
7main :- Tmp = 999, @check 5, print "caller's Tmp is still" Tmp.
macro's Tmp = 5
caller's Tmp is still 999

A macro is a natural way to name a recurring combination of goals. @of above pairs the “typed” and “pretty-printed” facts that a hypothetical rule (Inference rules and queries) must add together at every bound variable of a term with binders; each @of x Name A expands to of x A, pp x Name in place, so the two cannot drift apart as the surrounding code changes:

of (lambda Name F) (arr A B) :-
  pi x\ @of x Name A ==> of (F x) B.

A multi-file program

A library file, its predicate under a table namespace:

% A small library, accumulated by file-structure.elpi.

namespace table {
  func lookup string -> int.
  lookup "one" 1.
  lookup "two" 2.
}

and a program that accumulates it, shortens the namespaced name, defines a macro, and grafts a rule before a named fallback:

../code/file-structure.elpi:

 1% Pulls in table-lib.elpi, shortens a namespaced name, defines a macro, and
 2% grafts a rule before a named fallback.
 3
 4accumulate table-lib.
 5
 6shorten table.{ lookup }.        % `lookup` now means `table.lookup`
 7
 8macro @missing :- 0.
 9
10pred value string -> int.
11
12:name "default"
13value _ M :- M = @missing.
14
15:before "default"
16value K V :- lookup K V, !.
17
18main :-
19  value "two" A, value "nope" B,
20  print "two =" A "/ nope =" B.
two = 2 / nope = 0