Built-in predicates
A builtin is a predicate whose body is OCaml rather than Elpi, reached
through the foreign function interface (Embedding and extending Elpi). Elpi’s own library
ships close to two hundred of them, from = to the garbage-collector
controls; a host application registers more the same way.
The FFI fixes the shape a builtin can have. Each argument is declared input
or output and carries a conversion between an Elpi term and an OCaml
value, so the OCaml code receives its inputs already converted and hands back
its outputs to be converted on the way out. A builtin succeeds once or fails
(by raising No_clause); it does not backtrack and does not offer a second
solution. A term it
has no conversion for it can still carry through untouched, as any.
elpi -document-builtins prints the exhaustive, generated reference: one
signature and doc comment per predicate, the same text checked in as
src/builtin.elpi. Throughout this manual a name written like
print links to that predicate’s declaration line in
src/builtin.elpi on GitHub. What follows is a tour by category, saying what
each group is for and which chapter covers it in depth where one does.
Logic, control and inspection
(=) unifies, with the occur check; unsound_unif does the same without
it and so can build a cyclic term (Unification and variables,
where it is defined with :nooc). same_term, infix ==, tests
plain syntactic equality, assigning nothing. pattern_match T P matches
T against the pattern P, assigning only P’s variables
(Terms).
declare_constraint and print_constraints are covered in
Constraint handling rules.
The cut !, not, if / if2, halt / stop, std.once
and std.do! are covered in Control and cut, and
pi / sigma in Inference rules and queries.
ground_term T checks that T has no unification variables left;
closed_term yields a fresh variable barred from naming any eigenvariable
(Binders and HOAS); cmp_term orders two terms
structurally, and only works when both are ground.
name / names list the eigenvariables in scope, var recognises and
takes apart a unification variable (Unification and variables
for its uvar Hd Args form), constant a global constant, and
occurs A T checks whether the atom A appears in T
(Unification and variables).
new_int hands out a strictly increasing integer and new_safe hands
out a store
that survives backtracking; both step outside Elpi’s usual scoping, so use
them sparingly.
Arithmetic
X is Expr evaluates Expr and unifies the result with X; calc
is the same as a function, for use with spilling (Spilling and functional syntax):
f {calc (N + 1)}. The precedences of every operator below are in
Lexical conventions.
Evaluated inside is / calc:
binary
+-*(intorfloat),/(float),divmod(int),^(stringconcatenation);unary
~(negation),abs, and, forfloat,sqrtexplnsincostanarcsinarccosarctan;two-argument functions
minmax, and, forfloat,pow(pow X Yis X to the power Y) andarctan2(arctan2 Y Xis the angle of the point (X, Y), asatan2in C);conversions
int_to_realtruncatefloorceil(int↔float),int_to_stringstring_to_intreal_to_stringstring_to_realsubstringsize(string),chrrhc(int↔ one-characterstring);type-suffixed variants that fix the operand type instead of inferring it:
i+ i- i* i~ iabsforint,r+ r- r* r~ rabsforfloat.
Comparisons are goals, not expressions: X < Y succeeds or fails, it is
not written under is. < > =< >= work on int,
float or string, with i< r< s< … fixing the type.
This set is extensible from the host application: API.Calc.register
adds an operation (a symbol, its argument types, and an OCaml function) to a
calc_descriptor passed to API.Setup.init ~calc (Embedding and extending Elpi).
Standard data types
These are declared in the builtin library, ready to use without an
accumulate:
data bool.
symb tt bool.
symb ff bool.
data pair A B.
symb pr A -> B -> pair A B. % + func fst, func snd
data option A.
symb none option A.
symb some A -> option A.
data cmp.
symb eq cmp.
symb lt cmp.
symb gt cmp.
data diagnostic.
symb ok diagnostic.
symb error string -> diagnostic.
data triple A B C.
symb triple A -> B -> C -> triple A B C. % + triple_1..3
data quadruple A B C D.
symb quadruple A -> B -> C -> D -> quadruple A B C D. % + quadruple_1..4
bool uses tt / ff because true / false are goals; pair’s
constructor is pr because , is conjunction; cmp is the result of a
three-way comparison: cmp_term, or a comparator a caller supplies, as
std.map and std.set require; diagnostic is
returned by builtins that report a reason for failing rather than just
failing (ok / error "message"). list (:: / []) is built
in too (Terms).
A short tour of calc, a pair, term_to_string, rex.split and a
reseeded generator:
code/builtins-tour.elpi:
1% A cross-section of the builtin library: arithmetic, standard data types,
2% string/term conversion, regular expressions, seeded randomness.
3
4main :-
5 X is 2 + 3 * 4, print "calc:" X,
6 P = pr 1 "one", term_to_string P S, print "term_to_string:" S,
7 rex.split "," "a,b,c" Parts, print "rex.split:" Parts,
8 random.init 42, random.int 100 R1,
9 random.init 42, random.int 100 R2, % same seed, same draw
10 print "seeded random repeats:" R1 R2.
calc: 14
term_to_string: pr 1 one
rex.split: [a, b, c]
seeded random repeats: 14 14
Regular expressions and randomness
rex.match, rex.replace and rex.split (OCaml’s Str syntax, not
PCRE) cover the common text-processing needs. random.int N draws a
uniform integer in \([0, N)\); random.init Seed reseeds the
generator, making a sequence reproducible: the same seed always draws the
same numbers.
Input, output and the file system
print and dprint write their arguments to standard output (dprint
shows raw terms); term_to_string renders a term to a string instead of
printing it. Beyond that Elpi has the stream I/O of OCaml:
open_in/open_out/open_appendopen a file;open_stringturns a string into a readable stream;std_in/std_out/std_errare the standard streams;input InStream Bytes Sreads a fixed number of bytes,input_linereads up to the newline,lookaheadpeeks one byte,eoftests for end of input;output OutStream Swrites,flushforces pending output out,close_in/close_outclose.
sys.* reaches the file system and the process environment:
sys.file_exists, sys.is_directory, sys.mkdir / sys.rmdir,
sys.remove / sys.rename, sys.readdir, sys.chdir /
sys.getcwd, plus getenv, gettimeofday and system (run a shell
command). The calls that can fail for an external reason return a
diagnostic (ok or error "…") rather than just failing.
unix.process.open / unix.process.close spawn a subprocess and reap
it, handing back its three standard streams.
code/builtins-io.elpi:
1% Stream input (without touching the file system) and a typed finite map.
2
3main :-
4 open_string "alpha\nbeta\ngamma\n" S,
5 input_line S L1,
6 input_line S L2,
7 close_in S,
8 print "first two lines:" L1 L2,
9
10 std.string.map.empty M0,
11 std.string.map.add "one" 1 M0 M1,
12 std.string.map.add "two" 2 M1 M2,
13 std.string.map.find "two" M2 V,
14 print "string map, two ->" V.
first two lines: alpha beta
string map, two -> 2
Pretty printing
Output that has to fit a given width, such as code or nested data, is
easier to produce by describing its layout than by computing line breaks
by hand. Elpi delegates this to OCaml’s Format module: a program builds
a term of type std.fmt that describes boxes and break hints, and a
builtin hands that term to Format, which decides where lines are split.
The document is ordinary data, so it can be built, inspected and discarded
on backtracking like any other term; only the final rendering has an
effect.
The constructors mirror the calls of Format one to one:
std.fmt.txt Sis a piece of text, never split;std.fmt.h L,std.fmt.v I L,std.fmt.hv I L,std.fmt.hov I Landstd.fmt.box I Lgroup the documents in the listLin a box, respectively horizontal (breaks never split the line), vertical (every break splits it), horizontal-or-vertical (all breaks split it, or none does), packing (a break splits the line only when the next item does not fit) and structural (like packing, but also splitting when that reduces the indentation).Iis the indentation, relative to where the box starts, of the lines a split creates;std.fmt.brk N Ois a break hint:Nspaces when the line is not split, otherwise a newline indented byOmore than the box;std.fmt.spcandstd.fmt.cutabbreviatestd.fmt.brk 1 0andstd.fmt.brk 0 0;std.fmt.nlalways splits the line;std.fmt.tbox Lis a tabulation box, which aligns its content in columns:std.fmt.settabsets a tab stop at the current column,std.fmt.tabmoves to the next tab stop, andstd.fmt.tbrk N Omoves to the next tab stop and thenNspaces further, or to a new line shifted byOwhen no tab stop is left on the line.
std.fmt->string Margin Doc S lays Doc out in the string
S, splitting lines longer than Margin columns;
std.fmt.output OutStream Margin Doc writes the same layout to a
stream. Passing _ as Margin selects the default of 80 columns; a
Margin smaller than 2 is rejected.
The constructor names are long when written in full, so a program that builds many documents usually shortens them. The example below lays out the same block at two margins: at the default 80 columns the packing box keeps all the items on one line, at 16 it splits only the breaks it has to.
code/builtins-format.elpi:
1% A document built from std.fmt constructors, laid out at two margins.
2
3shorten std.fmt.{ txt, v, hov, brk, cut, spc }.
4
5func items list string -> list std.fmt.
6items [] [].
7items [X] [txt X] :- !.
8items [X|XS] [txt X, txt ";", spc | R] :- items XS R.
9
10func block string, list string -> std.fmt.
11block Name Xs (v 2 [txt Name, txt " {", cut, hov 0 {items Xs}, brk 0 -2, txt "}"]).
12
13main :-
14 block "record" ["alpha", "beta", "gamma", "delta"] B,
15 std.fmt->string _ B Wide,
16 print Wide,
17 std.fmt->string 16 B Narrow,
18 print Narrow.
record {
alpha; beta; gamma; delta
}
record {
alpha; beta;
gamma; delta
}
Tab stops do not adapt to the cells: a column is as wide as the distance
between the two tab stops that delimit it, and a cell longer than that
pushes the rest of its row past the following tab stops. A table therefore
sets its tab stops in a first row padded to the widest cell of each
column, typically a header, as in the example below; a std.fmt.tab
then starts each cell of the following rows.
code/builtins-format-table.elpi:
1% A table: the padded header row sets one tab stop per column.
2
3shorten std.fmt.{ txt, tbox, settab, tab, nl }.
4
5func row pair string (pair string string) -> list std.fmt.
6row (pr Name (pr Arity Kind)) [nl, tab, txt Name, tab, txt Arity, tab, txt Kind].
7
8main :-
9 Header = [settab, txt "name ", settab, txt "arity ", settab, txt "kind"],
10 std.map [pr "map" (pr "3" "func"), pr "append" (pr "3" "pred")] row Rows,
11 std.flatten [Header|Rows] Cells,
12 std.fmt->string _ (tbox Cells) S,
13 print S.
name arity kind
map 3 func
append 3 pred
Typed finite maps
std.string.map, std.int.map and std.loc.map are FFI-backed
persistent maps over one fixed key type (std.string.set and
std.int.set are the matching sets). Each map has .empty, .mem,
.add, .remove, .find and .bindings, plus .filter /
.map / .fold taking an Elpi func; the value type has to be a
closed term. The general, any-key structures std.map and std.set,
written in Elpi rather than OCaml, are covered in The standard library.
Garbage collector and runtime
gc.get / gc.set read and write the OCaml garbage-collector
parameters, gc.stat / gc.quick-stat report live statistics, and
gc.minor / gc.major / gc.full / gc.compact force a
collection. trace.counter reads a named trace point
(Debugging and tracing). These matter only when profiling or trimming
the footprint of a long-running embedding.