1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593
|
(*|
Tutorial on the Elpi programming language
*****************************************
:author: Enrico Tassi
.. include:: ../etc/tutorial_style.rst
..
Elpi is an extension language that comes as a library
to be embedded into host applications such as Coq.
Elpi is a variant of λProlog enriched with constraints.
λProlog is a programming language designed to make it easy
to manipulate abstract syntax trees containing binders.
Elpi extends λProlog with modes and constraints in order
to make it easy to manipulate abstract syntax trees
containing metavariables (also called unification variables, or
evars in the Coq jargon).
This software, "coq-elpi", is a Coq plugin embedding Elpi and
exposing to the extension language Coq specific data types (e.g. terms)
and API (e.g. to declare a new inductive type).
In order to get proper syntax highlighting using VSCode please install the
"gares.coq-elpi-lang" extension. In CoqIDE please chose "coq-elpi" in
Edit -> Preferences -> Colors.
This little tutorial does not talk about Coq, but rather focuses on
Elpi as a programming language. It assumes no previous knowledge of
Prolog, λProlog or Elpi. Coq is used as an environment for stepping trough
the tutorial one paragraph at a time. The text between `lp:{{` and `}}` is
Elpi code, while the rest are Coq directives to drive the Elpi interpreter.
.. contents::
|*)
From elpi Require Import elpi. (* .none *)
(*|
=================
Logic programming
=================
Elpi is a dialect of λProlog enriched with constraints. We start by introducing
the first order fragment of λProlog, i.e. the terms will not contain binders.
Later we cover terms with binders and constraints.
Our first program is called `tutorial`.
We begin by declaring the signature of our terms.
Here we declare that :e:`person` is a type, and that
:e:`mallory`, :e:`bob` and :e:`alice` are terms of that type.
|*)
Elpi Program tutorial lp:{{
kind person type.
type mallory, bob, alice person.
}}.
(*|
An Elpi program is made of rules that declare
when predicates hold and that are accumulated one after the
other. Rules are also called clauses in Prolog's slang, so we may use both
terms interchangeably.
The next code snippet accumulates on top
of the current `tutorial` program a predicate declaration for :e:`age`
and three rules representing our knowledge about our terms.
|*)
Elpi Accumulate lp:{{
pred age o:person, o:int.
age mallory 23.
age bob 23.
age alice 20.
}}.
(*|
The predicate :e:`age` has two arguments, the former is a person while
the latter is an integer. The label :e:`o:` (standing for output)
is a mode declaration, which we will explain later (ignore it for now).
.. note:: :stdtype:`int` is the built-in data type of integers
Integers come with usual arithmetic operators, see the :stdlib:`calc` built-in.
In order to run our program we have to write a query,
i.e. a predicate expression containing variables such as:
.. code:: elpi
age alice A
The execution of the program is expected to assign a value to :e:`A`, which
represents the age of :e:`alice`.
.. important::
Syntactic conventions:
* variables are identifiers starting with a capital letter, eg
:e:`A`, :e:`B`, :e:`FooBar`, :e:`Foo_bar`, :e:`X1`
* constants (for individuals or predicates) are identifiers
starting with a lowercase letter, eg
:e:`foo`, :e:`bar`, :e:`this_that`, :e:`camelCase`,
:e:`dash-allowed`, :e:`qmark_too?`, :e:`arrows->and.dots.as<-well`
A query can be composed of many predicate expressions separated by :e:`,`
that stands for conjunction: we want to get an answer to all the
predicate expressions.
|*)
Elpi Query lp:{{
age alice A, coq.say "The age of alice is" A
}}.
(*|
:builtin:`coq.say` is a built-in predicate provided by Coq-Elpi which
prints its arguments.
You can look at the output buffer of Coq to see the value for :e:`A` or hover
or toggle the little bubble after `}}.` if you are reading the tutorial with a
web browser.
.. note:: :stdtype:`string` is a built-in data type
Strings are delimited by double quotes and ``\`` is the escape symbol.
The predicate :e:`age` represents a *relation* (in contrast to a function)
and it computes both ways: we can ask Elpi which person :e:`P` is 23 years old.
|*)
Elpi Query lp:{{
age P 23, coq.say P "is 23 years old"
}}.
(*|
-----------
Unification
-----------
Operationally the query :e:`age P 23` is *unified* with each
and every rule present in the program starting from the first one.
Unification compares two
terms structurally and eventually assigns variables.
For example for the first rule of the program we obtain
the following unification problem:
.. code:: elpi
age P 23 = age mallory 23
This problem can be simplified into smaller unification problems following
the structure of the terms:
.. code:: elpi
age = age
P = mallory
23 = 23
The first and last are trivial, while the second one can be satisfied by
assigning :e:`mallory` to :e:`P`. All equations are solved,
hence unification succeeds.
See also the `Wikipedia page on Unification <https://en.wikipedia.org/wiki/Unification_(computer_science)#Syntactic_unification_of_first-order_terms>`_.
Since the first part of the query is successful the rest of
the query is run: the value of :e:`P` is printed as well as
the :e:`"is 23 years old"` string.
.. note:: :e:`=` is a regular predicate
The query :e:`age P 23` can be also written as follows:
.. code:: elpi
A = 23, age P A, Msg = "is 23 years old", coq.say P Msg
Let's try a query harder to solve!
|*)
Elpi Query lp:{{
age P 20, coq.say P "is 20 years old"
}}.
(*|
This time the unification problem for the first rule
in the program is:
.. code:: elpi
age P 20 = age mallory 23
that is simplified to:
.. code:: elpi
age = age
P = mallory
20 = 23
The second equation can be solved by assigning :e:`mallory` to :e:`P`,
but the third one has no solution, so unification fails.
------------
Backtracking
------------
When failure occurs all assignments are undone (i.e. :e:`P` is unset again)
and the next rule in the program is tried. This operation is called
*backtracking*.
The unification problem for the next rule is:
.. code:: elpi
age P 20 = age bob 23
This one also fails. The unification problem for the last rule is:
.. code:: elpi
age P 20 = age alice 20
This one works, and the assignment :e:`P = alice` is kept as the result
of the first part of the query. Then :e:`P` is printed and the program
ends.
An even harder query is the following one where we ask for two distinct
individuals to have the same age.
|*)
Elpi Query lp:{{
age P A, age Q A, not(P = Q),
coq.say P "and" Q "are" A "years old"
}}.
(*|
This example shows that backtracking is global. The first solution for
:e:`age P A` and :e:`age Q A` picks :e:`P` and :e:`Q` to
be the same individual :e:`mallory`,
but then :e:`not(P = Q)` fails and forces the last choice that was made to be
reconsidered, so :e:`Q` becomes :e:`bob`.
Look at the output of the following code to better understand
how backtracking works.
|*)
Elpi Query lp:{{
age P A, coq.say "I picked P =" P,
age Q A, coq.say "I picked Q =" Q,
not(P = Q),
coq.say "the last choice worked!",
coq.say P "and" Q "are" A "years old"
}}.
(*|
.. note:: :e:`not` is a black hole
The :e:`not(P)` predicate tries to solve the query :e:`P`: it fails if
:e:`P` succeeds, and succeeds if :e:`P` fails. In any case no trace is left
of the computation for :e:`P`. E.g. :e:`not(X = 1, 2 < 1)` succeeds, but
the assignment for :e:`X` is undone. See also the section
about the `foundations`_ of λProlog.
---------------------------
Facts and conditional rules
---------------------------
The rules we have seen so far are *facts*: they always hold.
In general rules can only be applied if some *condition* holds. Conditions are
also called premises, we may use the two terms interchangeably.
Here we add to our program a rule that defines what :e:`older P Q` means
in terms of the :e:`age` of :e:`P` and :e:`Q`.
.. note:: :e:`:-` separates the *head* of a rule from the *premises*
|*)
Elpi Accumulate lp:{{
pred older o:person, o:person.
older P Q :- age P N, age Q M, N > M.
}}.
(*|
The rule reads: :e:`P` is older than :e:`Q` if
:e:`N` is the age of :e:`P`
*and* :e:`M` is the age of :e:`Q`
*and* :e:`N` is greater than :e:`M`.
Let's run a query using older:
|*)
Elpi Query lp:{{
older bob X,
coq.say "bob is older than" X
}}.
(*|
The query :e:`older bob X` is unified with the head of
the program rule :e:`older P Q`
assigning :e:`P = bob` and :e:`X = Q`. Then three new queries are run:
.. code:: elpi
age bob N
age Q M
N > M
The former assigns :e:`N = 23`, the second one first
sets :e:`Q = mallory` and :e:`M = 23`. This makes the last
query to fail, since :e:`23 > 23` is false. Hence the
second query is run again and again until :e:`Q` is
set to :e:`alice` and :e:`M` to :e:`20`.
Variables in the query are said to be existentially
quantified because Elpi will try to find one
possible value for them.
Conversely, the variables used in rules are
universally quantified in the front of the rule.
This means that the same program rule can be used
multiple times, and each time the variables are fresh.
In the following example the variable :e:`P` in :e:`older P Q :- ...`
once takes :e:`bob` and another time takes :e:`mallory`.
|*)
Elpi Query lp:{{
older bob X, older mallory X,
coq.say "both bob and mallory are older than" X
}}.
(*|
==================
Terms with binders
==================
So far the syntax of terms is based on constants
(eg :e:`age` or :e:`mallory`) and variables (eg :e:`X`).
λProlog adds another term constructor:
λ-abstraction (written :e:`x\ ...`).
.. note:: the variable name before the ``\`` can be a capital
Given that it is explicitly bound Elpi needs not to guess if it is a global
symbol or a rule variable (that required the convention of using capitals for
variables in the first place).
-------------
λ-abstraction
-------------
Functions built using λ-abstraction can be applied
to arguments and honor the usual β-reduction rule
(the argument is substituted for the bound variable).
In the following example :e:`F 23` reads, once
the β-reduction is performed, :e:`age alice 23`.
|*)
Elpi Query lp:{{
F = (x\ age alice x),
coq.say "F =" F,
coq.say "F 20 =" (F 20),
coq.say "F 23 =" (F 23)
}}.
(*|
Let's now write the "hello world" of λProlog: an
interpreter and type checker for the simply
typed λ-calculus. We call this program `stlc`.
We start by declaring that :e:`term` is a type and
that :e:`app` and :e:`fun` are constructors of that type.
|*)
Elpi Program stlc lp:{{
kind term type.
type app term -> term -> term.
type fun (term -> term) -> term.
}}.
(*|
The constructor :e:`app` takes two terms
while :e:`fun` only one (of functional type).
Note that
* there is no constructor for variables, we will
use the notion of bound variable of λProlog in order
to represent variables
* :e:`fun` takes a function as subterm, i.e. something
we can build using the λ-abstraction :e:`x\ ...`
As a consequence, the identity function λx.x is written like this:
.. code:: elpi
fun (x\ x)
while the first projection λx.λy.x is written:
.. code:: elpi
fun (x\ fun (y\ x))
Another consequence of this approach is that there is no
such thing as a free variable in our representation of the λ-calculus.
Variables are only available under the λ-abstraction of the
programming language, that gives them a well defined scope and
substitution operation (β-reduction).
This approach is called `HOAS <https://en.wikipedia.org/wiki/Higher-order_abstract_syntax>`_.
We can now implement weak head reduction, that is we stop reducing
when the term is a :e:`fun` or a global constant (potentially applied).
If the term is :e:`app (fun F) A` then we compute the reduct :e:`F A`.
Note that :e:`F` is a λProlog function, so passing an argument to it
implements the substitution of the actual argument for the bound variable.
We first give a type and a mode for our predicate :e:`whd`. It reads
"whd takes a term in input and gives a term in output". We will
explain what input means precisely later, for now just think about it
as a comment.
|*)
Elpi Accumulate lp:{{
pred whd i:term, o:term.
% when the head "Hd" of an "app" (lication) is a
% "fun" we substitute and continue
whd (app Hd Arg) Reduct :- whd Hd (fun F), !,
whd (F Arg) Reduct.
% otherwise a term X is already in normal form.
whd X Reduct :- Reduct = X.
}}.
(*|
Recall that, due to backtracking, all rules are potentially used.
Here whenever the first premise of the first rule succeeds
we want the second rule to be skipped, since we found a redex.
The premises of a rule are run in order and the :e:`!` operator discards all
other rules following the current one. Said otherwise it commits to
the currently chosen rule for the current query (but leaves
all rules available for subsequent queries, they are not erased from the
program). So, as soon as :e:`whd Hd (fun F)` succeeds we discard the second
rule.
|*)
Elpi Query lp:{{
I = (fun x\x),
whd I T, coq.say "λx.x ~>" T,
whd (app I I) T1, coq.say "(λx.x) (λx.x) ~>" T1
}}.
(*|
Another little test using global constants:
|*)
Elpi Accumulate lp:{{
type foo, bar term.
}}.
Elpi Query lp:{{
Fst = fun (x\ fun y\ x),
T = app (app Fst foo) bar,
whd T T1, coq.say "(Fst foo bar) ~>" T1,
S = app foo bar,
whd S S1, coq.say "(foo bar) ~>" S1
}}.
(*|
A last test with a lambda term that has no weak head normal form:
|*)
Elpi Bound Steps 1000. (* Let's be cautious *)
Fail Elpi Query lp:{{
Delta = fun (x\ app x x),
Omega = app Delta Delta,
whd Omega Hummm, coq.say "not going to happen"
}}. (* .fails *)
Elpi Bound Steps 0.
(*|
-----------------------
:e:`pi x\ ` and :e:`=>`
-----------------------
We have seen how to implement substitution using the binders of λProlog.
More often than not we need to move under binders rather than remove them by
substituting some term in place of the bound variable.
In order to move under a binder and inspect the body of a function λProlog
provides the :e:`pi` quantifier and the :e:`=>` connective.
A good showcase for these features is to implement a type checker
for the simply typed lambda calculus.
See also `the Wikipedia page on the simply typed lambda calculus <https://en.wikipedia.org/wiki/Simply_typed_lambda_calculus>`_.
We start by defining the data type of simple types.
We then declare a new predicate :e:`of` (for "type of") and finally
we provide two rules, one for each term constructor.
|*)
Elpi Accumulate lp:{{
kind ty type. % the data type of types
type arr ty -> ty -> ty. % our type constructor
pred of i:term, o:ty. % the type checking algorithm
% for the app node we ensure the head is a function from
% A to B, and that the argument is of type A
of (app Hd Arg) B :-
of Hd (arr A B), of Arg A.
% for lambda, instead of using a context (a list) of bound
% variables we use pi and ==> , explained below
of (fun F) (arr A B) :-
pi x\ of x A ==> of (F x) B.
}}.
(*|
The :e:`pi name\ code` syntax is reserved, as well as
:e:`rule ==> code`.
Operationally :e:`pi x\ code` introduces a fresh
constant :e:`c` for :e:`x` and then runs :e:`code`.
Operationally :e:`rule ==> code` adds :e:`rule` to
the program and runs :e:`code`. Such extra rule is
said to be hypothetical.
Both the constant for :e:`x` and :e:`rule` are
removed once :e:`code` terminates.
.. important:: hypothetical rules are added at the *top* of the program
Hypothetical rules hence take precedence over static rules, since
they are tried first.
Note that in this last example the hypothetical rule is going to be
:e:`of c A` for a fixed :e:`A` and a fresh constant :e:`c`.
The variable :e:`A` is fixed but not assigned yet, meaning
that :e:`c` has a type, and only one, but we may not know it yet.
Now let's assign a type to λx.λy.x:
|*)
Elpi Query lp:{{
of (fun (x\ fun y\ x)) Ty, coq.say "The type of λx.λy.x is:" Ty
}}.
(*|
Let's run this example step by step:
The rule for :e:`fun` is used:
* :e:`arrow A1 B1` is assigned to :e:`Ty` by unification
* the :e:`pi x\ ` quantifier creates a fresh constant :e:`c1` to play
the role of :e:`x`
* the :e:`=>` connective adds the rule :e:`of c1 A1` the program
* the new query :e:`of (fun y\ c1) B1` is run.
Again, the rule for :e:`fun` is used (since its variables are
universally quantified, we use :e:`A2`, :e:`B2`... this time):
* :e:`arrow A2 B2` is assigned to :e:`B1` by unification
* the :e:`pi x\ ` quantifier creates a fresh constant :e:`c2` to play
the role of :e:`x`
* the :e:`=>` connective adds the rule :e:`of c2 A2` the program
* the new query :e:`of c1 B2` is run.
The (hypothetical) rule :e:`of c1 A1` is used:
* unification assigns :e:`A1` to :e:`B2`
The value of :e:`Ty` is hence :e:`arr A1 (arr A2 A1)`, a good type
for λx.λy.x (the first argument and the output have the same type :e:`A1`).
What about the term λx.(x x) ?
|*)
Elpi Query lp:{{
Delta = fun (x\ app x x),
(of Delta Ty ; coq.say "Error:" Delta "has no type")
}}.
(*|
The :e:`;` infix operator stands for disjunction. Since we see the message
:e:`of` failed: the term :e:`fun (x\ app x x)` is not well typed.
First, the rule for elpi:`fun` is selected:
* :e:`arrow A1 B1` is assigned to :e:`Ty` by unification
* the :e:`pi x\ ` quantifier creates a fresh constant :e:`c1` to play the
role of :e:`x`
* the :e:`=>` connective adds the rule :e:`of c1 A1` the program
* the new query :e:`of (app c1 c1) B1` is run.
Then it's the turn of typing the application:
* the query :e:`of c1 (arr A2 B2)` assigns to :e:`A1` the
value :e:`arr A2 B2`. This means that the
hypothetical rule is now :e:`of c1 (arr A2 B2)`.
* the query :e:`of c1 A2` fails because the unification
.. code:: elpi
of c1 A2 = of c1 (arr A2 B2)
has no solution, in particular the sub problem :e:`A2 = (arr A2 B2)`
fails the so called occur check.
.. _foundations:
===================
Logical foundations
===================
This section tries to link, informally, λProlog with logic, assuming the reader
has some familiarity with first order intuitionistic logic and proof theory.
The reader which is not familiar with that can probably skip this section,
although section `functional-style`_ contains some explanations about
the scope of variables which are based on the logical foundations of
the language.
The semantics of a λProlog program is given by interpreting
it in terms of logical formulas and proof search in intuitionistic logic.
A rule
.. code:: elpi
p A B :- q A C, r C B.
has to be understood as a formula
.. math::
∀A~B~C, (\mathrm{q}~A~C ∧ \mathrm{r}~C~B) → \mathrm{p}~A~B
A query is a goal that is proved by backchaining
rules. For example :e:`p 3 X`
is solved by unifying it with the conclusion of
the formula above (that sets :e:`A` to :e:`3`) and
generating two new goals, :e:`q 3 C` and
:e:`r C B`. Note that :e:`C` is an argument to both
:e:`q` and :e:`r` and acts as a link: if solving :e:`q`
fixes :e:`C` then the query for :e:`r` sees that.
Similarly for :e:`B`, that is identified with :e:`X`,
and is hence a link from the solution of :e:`r` to
the solution of :e:`p`.
A rule like:
.. code:: elpi
of (fun F) (arr A B) :-
pi x\ of x A ==> of (F x) B.
reads, as a logical formula:
.. math::
∀F~A~B, (∀x, \mathrm{of}~x~A → \mathrm{of}~(F~x)~B) → \mathrm{of}~(\mathrm{fun}~F)~(\mathrm{arr}~A~B)
where :math:`F` stands for a function.
Alternatively, using the inference rule notation typically used for
type systems:
.. math::
\frac{\Gamma, \mathrm{of}~x~A \vdash \mathrm{of}~(F~x)~B \quad x~\mathrm{fresh}}{\Gamma \vdash \mathrm{of}~(\mathrm{fun}~F)~(\mathrm{arr}~A~B)}
Hence, :e:`x` and :e:`of x A` are available only
temporarily to prove :e:`of (F x) B` and this is
also why :e:`A` cannot change during this sub proof (:e:`A` is
quantified once and forall outside).
Each program execution is a proof (tree) of the query
and is made of the program rules seen as proof rules or axioms.
As we hinted before negation is a black hole, indeed the usual definition of
:math:`\neg A` as :math:`A \to \bot` is the one of a function with no output
(see also the `the Wikipedia page on the Curry-Howard correspondence <https://en.wikipedia.org/wiki/Curry%E2%80%93Howard_correspondence#Natural_deduction_and_lambda_calculus>`_).
=====================
Modes and constraints
=====================
Elpi extends λProlog with *syntactic constraints*
and rules to manipulate the store of constraints.
Syntactic constraints are goals suspended on
a variable which are resumed as soon as that variable
gets instantiated. While suspended they are kept in a store
which can be manipulated by dedicated rules.
A companion facility is the declaration of *modes*.
The argument of a predicate can be marked as input
to avoid instantiating the goal when it is unified
with the head of a rule (an input argument
is matched, rather than unified).
A simple example: Peano's addition:
|*)
Elpi Program peano lp:{{
kind nat type.
type z nat.
type s nat -> nat.
pred add o:nat, o:nat, o:nat.
add (s X) Y (s Z) :- add X Y Z.
add z X X.
}}.
Elpi Query lp:{{
add (s (s z)) (s z) R, coq.say "2 + 1 =" R
}}.
(*|
Unfortunately the relation does not work well
when the first argument is a variable. Depending on the
order of the rules for :e:`add` Elpi can either diverge or pick
:e:`z` as a value for :e:`X` (that may not be what one wants)
|*)
Elpi Bound Steps 100.
Fail Elpi Query lp:{{ add X (s z) Y }}. (* .fails *)
Elpi Bound Steps 0.
(*|
Indeed the first rule for add can be applied forever.
If one exchanges the two rules in the program, then Elpi
terminates picking :e:`z` for :e:`X`.
We can use the mode directive in order to
*match* arguments marked as :e:`i:` against the patterns
in the head of rules, rather than unifying them.
|*)
Elpi Program peano2 lp:{{
kind nat type.
type z nat.
type s nat -> nat.
pred sum i:nat, i:nat, o:nat.
sum (s X) Y (s Z) :- sum X Y Z.
sum z X X.
sum _ _ _ :-
coq.error "nothing matched but for this catch all clause!".
}}.
Fail Elpi Query lp:{{ sum X (s z) Y }}. (* .fails *)
(*|
The query fails because no rule first argument matches :e:`X`.
Instead of failing we can suspend goals and turn them into
syntactic constraints
|*)
Elpi Program peano3 lp:{{
kind nat type.
type z nat.
type s nat -> nat.
pred sum i:nat, i:nat, o:nat.
sum (s X) Y (s Z) :- sum X Y Z.
sum z X X.
sum X Y Z :-
% the head of the rule always unifies with the query, so
% we double check X is a variable (we could also be
% here because the other rules failed)
var X,
% then we declare the constraint and schedule its resumption
% on the assignment of X
declare_constraint (sum X Y Z) [X].
}}.
Elpi Query lp:{{ sum X (s z) Z }}.
(*|
Syntactic constraints are resumed when the variable
they are suspended on is assigned:
|*)
Elpi Query lp:{{
sum X (s z) Z, X = z,
coq.say "The result is:" Z
}}.
(*|
Here a couple more examples. Keep in mind that:
* resumption can cause failure
* recall that :e:`;` stands for disjunction
|*)
Fail Elpi Query lp:{{ sum X (s z) (s (s z)), X = z }}. (* .fails *)
Elpi Query lp:{{ sum X (s z) (s (s z)), (X = z ; X = s z) }}.
(*|
In this example the computation suspends, then makes progress,
then suspends again...
|*)
Elpi Query lp:{{
sum X (s z) Y,
print_constraints, coq.say "Currently Y =" Y,
X = s Z,
print_constraints, coq.say "Currently Y =" Y,
Z = z,
coq.say "Finally Y =" Y
}}.
(*|
Sometimes the set of syntactic constraints becomes unsatisfiable
and we would like to be able to fail early.
|*)
Elpi Accumulate lp:{{
pred even i:nat.
pred odd i:nat.
even z.
even (s X) :- odd X.
odd (s X) :- even X.
odd X :- var X, declare_constraint (odd X) [X].
even X :- var X, declare_constraint (even X) [X].
}}.
Elpi Query lp:{{ even (s X), odd (s X) }}. (* hum, not nice *)
(*|
-------------------------
Constraint Handling Rules
-------------------------
A constraint (handling) rule can see the store of syntactic constraints
as a whole, remove constraints and/or create new goals:
|*)
Elpi Accumulate lp:{{
constraint even odd {
% if two distinct, conflicting, constraints about the same X
% are part of the constraint store
rule (even X) (odd X) <=>
% generate the following goal
(coq.say X "can't be even and odd at the same time", fail).
}
}}.
Fail Elpi Query lp:{{ even (s X), odd (s X) }}. (* .fails *)
(*|
.. note:: :e:`fail` is a predicate with no solution
See also the Wikipedia page on `Constraint Handling Rules <https://en.wikipedia.org/wiki/Constraint_Handling_Rules>`_
for an introduction to the sub language to manipulate constraints.
.. _functional-style:
================
Functional style
================
Elpi is a relational language, not a functional one. Still some features
typical of functional programming are available, with some caveats.
-------------------------------
Spilling (relation composition)
-------------------------------
Chaining "relations" can be painful, especially when
they look like functions. Here we use :stdlib:`std.append`
and :stdlib:`std.rev` to build a palindrome:
|*)
Elpi Program function lp:{{
pred make-palindrome i:list A, o:list A.
make-palindrome L Result :-
std.rev L TMP,
std.append L TMP Result.
}}.
Elpi Query lp:{{
make-palindrome [1,2,3] A
}}.
(*|
.. note:: variables (capital letters) can be used in
types in order to describe ML-like polymorphism.
.. note:: :e:`list A` is a built-in data type
The empty list is written :e:`[]`, while the cons constructor
is written :e:`[Hd | Tail]`. Iterated cons can be written
:e:`[ E1, E2 | Tail ]` and :e:`| Tail` can be omitted if the list
is nil terminated.
The :e:`make-palindrome` predicate has to use a temporary variable
just to pass the output of a function as the input to another function.
Spilling is a syntactic elaboration which does that for you.
Expressions between `{` and `}` are
said to be spilled out and placed just before the predicate
that contains them.
*)
Elpi Accumulate lp:{{
pred make-palindrome2 i:list A, o:list A.
make-palindrome2 L Result :-
std.append L {std.rev L} Result.
}}.
Elpi Query lp:{{
make-palindrome2 [1,2,3] A
}}.
(*|
The two versions of :e:`make-palindrome` are equivalent.
Actually the latter is elaborated into the former.
----------------------
APIs for built-in data
----------------------
Functions about built-in data types are available via the
:stdlib:`calc` predicate or its infix version :e:`is`. Example:
|*)
Elpi Query lp:{{
calc ( "result " ^ "=" ) X,
Y is 3 + 2,
coq.say X Y
}}.
(*|
The :stdlib:`calc` predicate works nicely with spilling:
|*)
Elpi Query lp:{{ coq.say "result =" {calc (2 + 3)} }}.
(*|
-----------------------
Allocation of variables
-----------------------
The language let's one use λ-abstraction also to write anonymous rules
but one has to be wary of where variables are bound (allocated really).
In our example we use the higher order predicate :stdlib:`std.map`:
.. code:: elpi
pred std.map i:list A, i:(A -> B -> prop), o:list B.
.. note:: :e:`prop` is the type of predicates
The actual type of the :e:`std.map` symbol is:
.. code:: elpi
type std.map list A -> (A -> B -> prop) -> list B -> prop.
The :e:`pred` directive complements a type declaration for predicates
(the trailing :e:`-> prop` is implicit) with a mode declaration for
each argument.
The type of the second argument of :e:`std.map`
is the one of a predicate relating :e:`A` with :e:`B`.
Let's try to call :e:`std.map` passing an anonymous rule (as we
would do in a functional language by passing an anonymous function):
|*)
Elpi Accumulate lp:{{
pred bad i:list int, o:list int.
bad L Result :-
std.map L (x\ r\ TMP is x + 1, r = TMP) Result.
pred good i:list int, o:list int.
good L Result :-
std.map L good.aux Result.
good.aux X R :- TMP is X + 1, R = TMP.
pred good2 i:list int, o:list int.
good2 L Result :-
std.map L (x\ r\ sigma TMP\ TMP is x + 1, r = TMP) Result.
}}.
Elpi Query lp:{{
not(bad [1,2,3] R1),
good [1,2,3] R2,
good2 [1,2,3] R3
}}.
(*|
The problem with :e:`bad` is that :e:`TMP` is fresh each time the rule
is used, but not every time the anonymous rule passed to :stdlib:`map`
is used. Technically :e:`TMP` is quantified (allocated) where :e:`L`
and :e:`Result` are.
There are two ways to quantify :e:`TMP` correctly, that is inside the
anonymous predicate. One is to actually name the predicate. Another one is
to use the :e:`sigma x\ ` quantifier to allocate :e:`TMP` at every call.
We recommend to name the auxiliary predicate.
.. tip:: predicates whose name ends in `.aux` don't trigger a missing type
declaration warning
One last way to skin the cat is to use :e:`=>` as follows. It gives us
the occasion to clarify further the scope of variables.
|*)
Elpi Accumulate lp:{{
pred good3 i:list int, o:list int.
good3 L Result :-
pi aux\
(pi TMP X R\ aux X R :- TMP is X + 1, R = TMP) ==>
std.map L aux Result.
}}.
Elpi Query lp:{{
good3 [1,2,3] R
}}.
(*|
In this case the auxiliary predicate :e:`aux`
is only visible inside :e:`good3`.
What is interesting to remark is that the quantifications are explicit
in the hypothetical rule, and they indicate clearly that each and every
time :e:`aux` is used :e:`TMP`, :e:`X` and :e:`R` are fresh.
The :e:`pi x\ ` quantifier is dual to :e:`sigma x\ `: since here it
occurs negatively it has the same meaning. That is, the hypothetical rule
could be written :e:`pi X R\ aux X R :- sigma TMP\ TMP is X + 1, R = TMP`.
.. tip:: :e:`pi x\ ` and :e:`sigma x\ ` can quantify on a bunch of variables
at once
That is, :e:`pi x y\ ...` is equivalent to :e:`pi x\ pi y\ ...` and
:e:`sigma x y\ ...` is equivalent to :e:`sigma x\ sigma y\ ...`.
.. tip:: :e:`=>` can load more than one clause at once
It is sufficient to put a list on the left hand side, eg :e:`[ rule1, rule2 ] ==> code`.
Moreover one can synthesize a rule before loading it, eg:
.. code:: elpi
Rules = [ one-more-rule | ExtraRules ], Rules ==> code
The last remark worth making is that bound variables are intimately related
to universal quantification, while unification variables are related to
existential quantification. It goes without saying that the following
two formulas are not equivalent and while the former is trivial the latter
is in general false:
.. math::
∀x, ∃Y, Y = x\\
∃Y, ∀x, Y = x
Let's run these two corresponding queries:
|*)
Elpi Query lp:{{ pi x\ sigma Y\ Y = x, coq.say "Y =" Y }}.
Fail Elpi Query lp:{{ sigma Y\ pi x\ Y = x, coq.say "Y =" Y }}. (* .fails *)
(*|
Another way to put it: :e:`x` is in the scope of :e:`Y` only in the first
formula since it is quantified before it. Hence :e:`x` can be assigned to
:e:`Y` in that case, but not in the second query, where it is quantified
after.
More in general, λProlog tracks the bound variables that are in scope of each
unification variable. There are only two ways to put a bound variable
in the scope:
* quantify the unification variable under the bound one (first formula)
* pass the bound variable to the unification variable explicitly: in this
case the unification variable needs to have a functional type.
Indeed :math:`∃Y, ∀x, (Y x) = x` has a solution: :e:`Y` can be
the identity function.
.. coq::
Elpi Query lp:{{ sigma Y\ pi x\ Y x = x, coq.say "Y =" Y }}.
If we look again at the rule for type checking
λ-abstraction:
.. code:: elpi
of (fun F) (arr A B) :-
pi x\ of x A ==> of (F x) B.
we can see that the only unification variable that sees the fresh
`x` is :e:`F`, because we pass :e:`x` to :e:`F` explicitly
(recall all unification variables such as :e:`F`, :e:`A`, :e:`B` are
quantified upfront, before the :e:`pi x\ `).
Indeed when we write:
.. math::
\frac{\Gamma, x : A \vdash f : B}{\Gamma \vdash λx.f : A → B}
on paper, the variable denoted by :e:`x` being bound there can only occur in
:math:`f`, not in :math:`\Gamma` or :math:`B` for example (although a
*different* variable could be named the same, hence the usual freshness side
conditions which are not really necessary using HOAS).
Remark that in the premise the variable :math:`x` is still bound, this time
not by a λ-abstraction but by the context :math:`\Gamma, x : A`.
In λProlog the context is the set of hypothetical rules and :e:`pi\ `
-quantified variables and is implicitly handled by the runtime of the
programming language.
A slogan to keep in mind is that:
.. important:: There is no such thing as a free variable!
Indeed the variable bound by the λ-abstraction (of our data) is
replaced by a fresh variable bound by the context (of our program). This is
called binder mobility. See also the paper
`Mechanized metatheory revisited <https://hal.inria.fr/hal-01884210/>`_ by
Dale Miller which is an excellent
introduction to these concepts.
=========
Debugging
=========
The most sophisticated debugging feature can be used via
the Visual Studio Code extension ``gares.elpi-lang`` and its
``Elpi Tracer`` tab.
---------------
Trace browser
---------------
In order to generate a trace one needs to execute the
``Elpi Trace Browser.`` command and then run any Elpi code.
|*)
(* Elpi Trace Browser. *)
Elpi Query stlc lp:{{ % We run the query in the stlc program
of (fun (x\ fun y\ x)) Ty, coq.say Ty
}}.
(*|
The trace file is generated in ``/tmp/traced.tmp.json``.
If it does not load automatically one can do it manually by clicking on
the load icon, in the upper right corner of the Elpi Tracer panel.
.. note:: partial display of goals
At the time of writing one may need to disable syntax highlighting in
the extension settings in order to get a correct display.
The trace browser displays, on the left column, a list of cards corresponding
to a step performed by the interpreter. The right side of the
panel gives more details about the selected step. In the image below one
can see the goal, the rule being applied, the assignments performed by the
unification of the rule's head with the goal, the subgoals generated.
.. image:: tracer.png
:width: 800
One can also look at the trace in text format (if VSCode is not an option,
for example).
|*)
Elpi Trace.
Elpi Query stlc lp:{{ % We run the query in the stlc program
of (fun (x\ fun y\ x)) Ty, coq.say Ty
}}.
Fail Elpi Query stlc lp:{{
of (fun (x\ app x x)) Ty, coq.say Ty
}}. (* .fails *)
(*|
The trace can be limited to a range of steps. Look at the
numbers ``run HERE {{{``.
|*)
Elpi Trace 6 8.
Elpi Query stlc lp:{{
of (fun (x\ fun y\ x)) Ty, coq.say Ty
}}.
(*|
The trace can be limited to a (list of) predicates as follows:
|*)
Elpi Trace "of".
Elpi Query stlc lp:{{
of (fun (x\ fun y\ x)) Ty, coq.say Ty
}}.
(*|
One can combine the range of steps with the predicate:
|*)
Elpi Trace 6 8 "of".
Elpi Query stlc lp:{{
of (fun (x\ fun y\ x)) Ty, coq.say Ty
}}.
(*|
To switch traces off:
|*)
Elpi Trace Off.
(*|
---------------
Good old print
---------------
A common λProlog idiom is to have a debug rule
lying around. The :e:`:if` attribute can be used to
make the rule conditionally interpreted (only if the
given debug variable is set).
|*)
Elpi Debug "DEBUG_MYPRED".
Elpi Program debug lp:{{
pred mypred i:int.
:if "DEBUG_MYPRED"
mypred X :-
coq.say "calling mypred on " X, fail.
mypred 0 :- coq.say "ok".
mypred M :- N is M - 1, mypred N.
}}.
Elpi Query lp:{{ mypred 3 }}.
(*|
------------------------
Printing entire programs
------------------------
Given that programs are not written in a single place, but rather obtained by
accumulating code, Elpi is able to print a (full) program to an text file
as follows. The obtained file provides a facility to filter rules by their
predicate. Note that the first component of the path is a Coq Load Path (i.e.
coqc options -R and -Q), the text file will be placed in the directory
bound to it.
|*)
Elpi Print stlc "elpi_examples/stlc".
(*|
Look at the `generated page <https://lpcic.github.io/coq-elpi/stlc.txt>`_.
Finally, one can bound the number of backchaining steps
performed by the interpreter:
|*)
Elpi Query lp:{{ 0 = 0, 1 = 1 }}.
Elpi Bound Steps 1.
Fail Elpi Query lp:{{ 0 = 0, 1 = 1 }}. (* .fails *) (* it needs 2 steps! *)
Elpi Bound Steps 0. (* Go back to no bound *)
(*|
---------------
Common pitfalls
---------------
Well, no programming language is perfect.
+++++++++++++++++++++++++++++++++++++++++++
Precedence of :e:`,`, :e: `==>` and :e:`=>`
+++++++++++++++++++++++++++++++++++++++++++
In this tutorial we only used :e: `==>` but Elpi also provides
the standard λProlog implication :e:`=>`. They have the same meaning
but different precedences w.r.t. :e:`,`.
The code :e:`a, c ==> d, e` reads :e:`a, (c ==> (d,e))`, that means that
the rule :e:`c` is available to both :e:`d` and :e:`e`.
On the contrary the code :e:`a, c => d, e` reads :e:`a, (c ==> d), e`,
making :e:`c` only available to :e:`d`.
So, :e:`=>` binds stronger than :e:`,`, while :e:`==>` binds stronger only
on the left.
According to our experience the precedence of :e:`=>` is a common source
of mistakes for beginners, if only because it is not stable by adding of
debug prints, that is :e:`a => b` and :e:`a => print "doing b", b` have
very different meaning (:e:`a` becomes only available to `print`!).
In this tutorial we only used :e:`==>` that was introduced in Elpi 2.0, but
there is code out there using :e:`=>`. Elpi 2.0 raises a warning if the
right hand side of :e:`=>` is a conjenction with no parentheses.
A concrete example:
|*)
Fail Elpi Query stlc lp:{{
pi x\
of x A => of x B, of x C
}}. (* .fails *)
Elpi Query stlc lp:{{
pi x\
of x A => (of x B, of x C) % both goals see of x A
}}.
Elpi Query stlc lp:{{
pi x\
of x A ==> of x B, of x C
}}.
(*|
++++++++++++
Backtracking
++++++++++++
Backtracking can lead to weird execution traces. The :stdlib:`std.do!` predicate
should be used to write non-backtracking code.
.. code:: elpi
pred not-a-backtracking-one.
not-a-backtracking-one :- condition, !, std.do! [
step,
(generate, test),
step,
].
In the example above once :e:`condition` holds we start a sequence of
steps which we will not reconsider. Locally, backtracking is still
available, e.g. between :e:`generate` and :e:`test`.
See also the :stdlib:`std.spy-do!` predicate which prints each and every step,
and the :stdlib:`std.spy` one which can be used to spy on a single one.
+++++++++++++++++++++++++++++++++++++++++++++++
Unification variables v.s. Imperative variables
+++++++++++++++++++++++++++++++++++++++++++++++
Unification variables sit in between variables in imperative programming and
functional programming. In imperative programming a variable can hold a value,
and that value can change over time via assignment. In functional languages
variables always hold a value, and that value never changes. In logic programming
a unification variable can either be unset (no value) or set to a value that
never changes. Backtracking goes back in time, it is not visible to the program.
As a result of this, code like
.. code:: elpi
pred bad-example.
bad-example :- X is 1 + 2, X is 4 + 5.
fails, because :e:`X` cannot be at the same time 3 and 9. Initially
:e:`X` is unset, then it is set to 3, and finally the programmer is
asserting that 3 (the value hold by :e:`X`) is equal to 9.
The second call to :e:`is` does not change the value carried by :e:`X`!
Unification, and hence the :e:`=` predicate, plays two roles.
When :e:`X` is unset, :e:`X = v` sets the variable.
When :e:`X` is set to :e:`u`, :e:`X = v` checks if the value
of :e:`X` is equal to :e:`u`: it is equivalent to :e:`u = v`.
===============
Further reading
===============
The `λProlog website <http://www.lix.polytechnique.fr/~dale/lProlog/>`_
contains useful links to λProlog related material.
Papers and other documentation about Elpi can be found at
the `Elpi home on github <https://github.com/LPCIC/elpi/>`_.
Three more tutorials specific to Elpi as an extension language for Coq
can be found in the `examples folder <https://github.com/LPCIC/coq-elpi/blob/master/examples/>`_.
You can continue by reading the one about the
`HOAS for Coq terms <https://lpcic.github.io/coq-elpi/tutorial_coq_elpi_HOAS.html>`_.
|*)
|