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