This book is licensed under Creative Commons Attribution 4.0 International. You may copy, share, and adapt it for any purpose, including commercially; please give appropriate credit, link to the licence, and indicate changes.
EyeProlog turns facts and rules into answers and inspectable proofs. This book is an original introduction to the habits of logic programming: describe a world, state the relationships that hold in it, and let unification and search connect the two.
This book is also the reference for the EyeProlog implementation. EyeProlog is a standards-based reasoning system: programs use the documented and tested ISO Prolog profile. Chapters 38–40 define the supported ISO Prolog profile, built-ins, and execution interface, describe every supported built-in predicate, and document the command-line interface. The explanatory chapters give the reasoning and operational context needed to use those details correctly.
Its subject is not syntax alone. A logic program has two inseparable aspects: the relation described by its clauses and the procedure induced when goals are selected and clauses are tried. The first tells us what answers are justified; the second tells us whether and how the machine will find them. Learning to program with EyeProlog means learning to move comfortably between these views.
EyeProlog implements a broad ISO Prolog profile with facts, clauses, terms, lists, control, arithmetic, dynamic predicates, operators, streams, and standard built-ins. Automatic tabling, explicit integrity checks, and proof output are implementation capabilities around that standards-based foundation. EyeProlog does not attempt to claim formal certification of every ISO processor edge case.
Standards are crucial because knowledge and rules often outlive the software that first processes them. Using ISO Prolog keeps programs teachable, inspectable, and portable across processors. EyeProlog aims to provide a compact implementation of that standard with explanations and practical host integration, not another proprietary rule language.
This places EyeProlog in a tradition that joins automated deduction, database querying, and programming. Jacques Herbrand’s doctoral work made ground terms and ground instances central to proof theory; Robinson’s later resolution principle turned unification and refutation into a general proof procedure; early Prolog showed that Horn clauses could also be executable programs; deductive databases emphasized finite relations and fixed points. EyeProlog borrows from all three traditions without pretending that they are identical. Its clauses are logical statements, its query execution is an ordered computation, and its proof terms make the connection between the two available for inspection.
That history explains a recurring theme of the book. Logic programming is not the claim that control disappears. It is the discipline of stating the relation clearly enough that control can be studied and improved separately. Robert Kowalski’s phrase “algorithm = logic + control” names this separation; EyeProlog’s focused surface makes it unusually easy to see in running examples.
Complete EyeProlog code displays from the book are also available as files under
examples/book/, grouped by chapter. From a source checkout
with Node.js 18 or newer, run the CLI directly:
node bin/eyeprolog.js examples/socrates.pl
The EyeProlog command should print:
type(socrates, mortal).
holds_result(test, true).
Then ask for the derivations:
node bin/eyeprolog.js --proof examples/socrates.pl
To use the published package, first verify that node --version reports Node.js
18 or newer. Upgrade an older runtime through a Node version manager or the
official Node.js download. A current Linux
distribution can still expose an older Node.js package.
The package can be launched without a global installation:
npx --yes eyeprolog
For a persistent command without administrator access, install into a user-owned prefix:
npm install --global --prefix "$HOME/.local" eyeprolog
export PATH="$HOME/.local/bin:$PATH"
Persist the PATH export in the appropriate shell startup file. Do not use
sudo npm install; npm’s
EACCES guidance
recommends a Node version manager or a user-owned npm prefix.
Readers who do not want to install anything can begin in the
browser playground. Paste
the source of examples/socrates.pl into the editor and run it. The playground
and local CLI accept the same in-memory Prolog source and resolve the same
standard modules. A program imports relations such as append/3 and member/2
with use_module(library(lists)). The page starts src/playground-worker.js as
a dedicated ES-module worker for each run. Serve a
local checkout over HTTP(S), rather than opening the page as a file: URL.
Filesystem predicates and include/1 are Node-only; URL and embedding examples
require their documented host environment.
The best way to read is beside a running interpreter. Before each run, predict
the answer; after it, change one fact or query and explain the difference.
Use npm run generate after editing the book to refresh the extracted
examples/book/ files.
Code displays serve three different purposes:
eyeprolog block is Prolog source accepted by EyeProlog; complete blocks are extracted under
examples/book/, although a short block may rely on facts introduced in the
surrounding chapter;text block shows output, a trace, a data shape, or pseudocode and is not
necessarily accepted as EyeProlog input;sh or js block is a host command or embedding example.Top-level programs under examples/ are the complete runnable
cases. Their exact outputs live under examples/output/; selected proof
outputs live under examples/proof/. Use the chapter extractions for copying a
particular display and the top-level corpus for end-to-end experiments.
This book treats logic programming as a craft, not a collection of clever tricks. By the end, a reader should be able to:
That is the stake in the ground: a focused implementation of standard Prolog
is enough to teach the large ideas when semantics, execution, and evidence
remain visible together.
The implementation is therefore part of the argument. The examples are
programs, the reference chapters are the reference for the running system, and
npm test checks the complete code displays, local references, and
built-in index against the source tree.
Approach each example through the same six moves:
This rhythm deliberately joins declarative reading, operational reading, and program construction. Readers new to logic programming can follow Parts I–III in order. Experienced Prolog programmers can begin with Chapters 3, 13, and 17 to see where EyeProlog’s hybrid execution and proof-oriented design differ. Chapter 41 gives further routes through the material.
Do not change several clauses at once. Use this recovery loop:
--proof if an unexpected answer succeeds;--stats or hand-trace the first branch if an expected answer is
missing or slow;No output can mean a legitimate absence, suppression of a queried source fact, an unready built-in, or an unfinished search. Chapter 1 introduces source-fact suppression, Chapters 7 and 11 distinguish failure from printed output, and Chapter 32 develops the full debugging method.
The book supports several paths; reading every chapter in order is not a test of seriousness.
| Reader | Suggested route | What to postpone |
|---|---|---|
| New to programming | Chapters 1–10, 11–12, 18–20, then Laboratories 1–4 | The formal parts of Chapter 3, embedding, and Parts V–VI |
| Programmer new to logic | Parts I–II, Chapters 11–13 and 17–25, then Part VII | Detailed history and mathematical foundations on the first pass |
| Experienced Prolog programmer | Chapters 3, 7, 11–13, 16–17, and 31–33 | Introductory syntax and list material |
| Knowledge engineer | Chapters 7, 11–16, 25, 31–33, then Laboratories 9–12 | Symbolic mathematics unless it serves the domain |
| Mathematics reader | Chapters 1–5, 19, and 26–30 | Embedding until an application needs it |
| Instructor or study group | Parts I–III, one route through Part V or VI, then selected laboratories | Reference chapters until reference work begins |
On a first pass, treat sections marked Deeper foundations as optional. They make the semantics precise but are not prerequisites for writing and running the next program.
The sequence follows the teaching architecture associated with The Art of Prolog: begin with the meaning of a relation, make the relation executable, study the control it induces, and then return to the same ideas at a larger scale through transformation, search, interpreters, and applications. Each part therefore ends by asking the reader to construct, test, or improve a program rather than merely recognize syntax. Reference material follows practice, laboratories turn the methods into work, and checkpoint notes close the loop with retrieval and diagnosis.
Balance here does not mean that every chapter has the same length or the same number of pictures. A language catalog should be searchable; a construction chapter should be argumentative; a laboratory should leave an artifact. The recurring balance is instead between four readings of a program:
| Reading | Question carried through the book | Typical evidence |
|---|---|---|
| Meaning | What does each ground relation claim? | a domain sentence and examples |
| Computation | How are answers actually found? | a trace, finite bound, or termination measure |
| Construction | Why is the program shaped this way? | a worked refinement and rejected alternative |
| Judgment | What has been established, and what remains assumed? | tests, proofs, counterexamples, and a trust boundary |
Diagrams follow the same rule. Scenes introduce an intuition; structural diagrams expose a term, proof, or dependency; process diagrams guide a piece of work; maps help navigate reference and review. A diagram earns its place by making a relationship visible that prose alone would make easy to miss.
Chapters are numbered continuously across the twelve parts, from Chapter 1 to Chapter 46.
Chapters 1–5
Chapters 6–10
Chapters 11–16
Chapters 17–20
Chapters 21–25
Chapters 26–30
Chapters 31–33
Chapters 34–37
Chapters 38–43
Chapter 44
Chapter 45
Chapter 46
We begin with connection rather than calculation. Facts place points in a relational world; variables draw threads between them; rules make one pattern follow from another.
Logic programming begins with a change of emphasis. Instead of listing the steps that calculate an answer, write sentences that are true in the problem domain.
parent(ada, byron).
parent(byron, clara).
parent(clara, diego).
Each line is a fact. parent/2 is a relation: the name is parent and the
arity is two. Arity matters. parent/2 and parent/3 are different predicates.
A host-supplied query selects the relation whose ground answers EyeProlog prints:
child(Child, Parent) :- parent(Parent, Child).
eyeprolog --goal 'child(X, Y)' program.pl
The answers are:
child(byron, ada).
child(clara, byron).
child(diego, clara).
EyeProlog distinguishes solutions found by the solver from answers printed by the
CLI. A query such as eyeprolog --goal 'parent(X, Y)' program.pl can find the three source facts
internally, but the normal CLI output suppresses answers that merely repeat
source facts. Derived child/2 answers are printed. Chapter 11 explains this
output policy; it does not change what calls inside rules can prove.
The program did not copy values through named slots. It found substitutions
for Child and Parent that made the rule body true, then applied those same
substitutions to the head.
Before writing a relation, ask:
For parent(Parent, Child), a ground fact reads naturally from left to right.
Calling it with a parent enumerates children; calling it with a child enumerates
parents; calling it open enumerates the finite database. A good relation has a
clear sentence and useful modes.
Facts are data, not commands. Clause order can affect search order, but a fact does not mean “do this now.”
The shift from functions to relations takes practice. A function is normally introduced with a direction: put an input in one side and receive an output from the other. A relation begins with a set of tuples. Direction enters only when somebody asks a question.
Take parent/2. The program does not store a procedure named “find children.”
It stores pairs for which the relation holds. From that single relation, one
may ask for a child’s parents, a parent’s children, whether two named people
stand in the relation, or every known pair. The source text stays fixed while
the binding pattern changes.
This is why the wording of a predicate matters. Before adding a rule, read a ground instance aloud:
parent(ada, byron)means that Ada is a parent of Byron.
Now replace one name at a time with a question:
For which
Childis Ada a parent?Who is a
Parentof Byron?Which
Parent–Childpairs are known?
If those questions feel like natural uses of one statement, the relation is probably well shaped. If each reading requires a different interpretation of an argument, split the concept before the ambiguity spreads into later rules.
Exercise. Add grandparent/2 using two calls to parent/2. Query all
grandparents, then only the grandparents of diego.
Checkpoint. Before continuing, make sure you can (1) read
parent(ada, byron) as a sentence, (2) explain what the two variables in
eyeprolog --goal 'child(X, Y)' program.pl ask for, and (3) predict which output changes after adding
parent(diego, elena).
Prolog programs accepted by EyeProlog are built from terms:
ada, accepted, 'atom with spaces';"sensor too hot" (the default shorthand for
[s,e,n,s,o,r,' ',t,o,o,' ',h,o,t]);42, -7, 3.14159, 1.2e3;X, Person, _temporary;point(3, 4), reading(temp, 91);[], [red, green, blue], [Head | Tail].Plain atom constants begin with a lowercase ASCII letter. Variables begin with
an uppercase letter or underscore. The bare _ is anonymous and every
occurrence is fresh. _Name is a named variable; repeated occurrences refer to
the same variable within its clause. Variables are local to a clause.
Unification asks whether two terms can be made identical by binding variables.
reading(Sensor, 91)
reading(temp, Value)
They unify with Sensor = temp and Value = 91. Structure must agree
recursively. point(X, X) unifies with point(2, 2) but not point(2, 3).
Functor and arity must agree.
The picture is worth lingering over. Unification does not assign values in a one-way parameter list. It aligns two structures. A variable on either side may receive a binding; a nested pair of compounds causes the same comparison to continue recursively. The result shown is the most general substitution: it commits to exactly what structural agreement requires and nothing more.
EyeProlog exposes unification as =/2:
same_shape(Pair) :- (Pair = pair(X, X)).
eyeprolog --goal 'same_shape(pair(red, red))' program.pl
eyeprolog --goal 'same_shape(pair(red, blue))' program.pl
Only the first query succeeds. \=/2 succeeds when two resolved terms are not
structurally equal.
Compound terms retain domain structure:
:- use_module(library(lists)).
measurement(battery_1, sample(17, volts(28.4), amps(12.1))).
route(a, d, path([a, b, d], cost(9))).
As a fact head, measurement(...) is an atomic formula. Nested terms are data.
The same surface form serves both roles; context decides which.
ready is an atom constant and "ready" is, by default, a proper list of
one-character atoms. Keep symbolic vocabulary as atoms and use character lists
when text must be inspected relationally. Quoted atoms remain atoms:
label(sensor_1, "Cabin temperature").
web_name(sensor_1, '<https://example.org/sensor/1>').
Exercise. Write diagonal/1, which succeeds for point(X, X). Then write
same_ends/1 for a three-element list whose first and last values agree.
Checkpoint. Without running EyeProlog, decide whether each pair unifies:
point(X, X) with point(red, red), point(X, X) with
point(red, blue), and [Head | Tail] with [a, b, c]. Then run a small
=/2 query to check each prediction.
The executable-clause idea emerged from work on automated theorem proving. Robinson’s resolution principle supplied a general proof rule, while the development of Prolog specialized proof search around clauses that could be read as procedures. EyeProlog begins further downstream: it offers a compact definite-clause language rather than a general first-order theorem prover. The restriction buys a direct correspondence between a rule body and the subquestions used to establish its head.
A rule has a head and a comma-separated body:
:- use_module(library(lists)).
eligible(Person) :-
age(Person, Years),
(Years >= 18),
registered(Person).
Read it declaratively: a person is eligible if the person has an age of at least 18 and is registered. Read it operationally: to solve the head, solve the body goals in their written dependency order, carrying bindings into later goals. EyeProlog normally selects from left to right. As a safe optimization, it may run a ready deterministic built-in filter early; such a filter cannot add alternative answers and already has the inputs its registered mode requires.
Both readings matter. The declarative reading checks the model. The operational reading helps make search finite and selective. Put a generator before a built-in that needs its input:
The two readings are not rivals. The logical reading prevents an efficient program from quietly answering the wrong question. The operational reading prevents a beautiful specification from wandering forever without producing an answer. Much of the craft in this book consists of keeping one reading steady while improving the other.
:- use_module(library(lists)).
adult(Person) :-
age(Person, Years),
(Years >= 18).
Multiple clauses express alternatives:
can_enter(Person) :- staff(Person).
can_enter(Person) :- visitor(Person), escorted(Person).
Helper predicates reveal the model and improve explanations:
high_score(Case) :-
score(Case, Score),
threshold(Threshold),
(Score >= Threshold).
status(Case, accepted) :- high_score(Case).
reason(Case, "score meets threshold") :- high_score(Case).
This section through “Meaning is not the search strategy” supplies the formal model behind the earlier examples. On a first practical reading, it is safe to continue at Chapter 4 and return here after writing a recursive relation.
The terminology in the next section honors a remarkably early source. Jacques Herbrand developed the relevant ideas in his 1930 doctoral thesis, Recherches sur la théorie de la démonstration (“Investigations in proof theory”). His fundamental theorem connected first-order derivability with propositional reasoning over suitably chosen ground instances. In broad terms, quantified proof obligations could be studied through formulas obtained by substituting constructed terms for variables.
That move supplied more than names. It made syntax usable as a mathematical universe: constants and function symbols generate ground terms, and atomic formulas over those terms provide a concrete space in which proofs can be analyzed. This viewpoint became foundational for automated theorem proving. Unification can be understood as finding substitutions that bring symbolic formulas together, while later proof procedures can search among clause instances without first assigning terms to an unrelated external domain.
The historical distinctions matter. Herbrand did not invent Robinson’s 1965 resolution calculus, nor did his thesis state the later least-model semantics of logic programs in its modern form. Rather, his proof theory laid essential groundwork. Resolution supplied a powerful subsequent inference mechanism, and van Emden and Kowalski later gave definite logic programs their fixed-point and least-Herbrand-model account. EyeProlog sits downstream of this sequence:
Herbrand: ground terms and instances as a proof-theoretic foundation
-> Robinson: resolution and unification as a proof procedure
-> logic programming: executable clauses and least-model semantics
-> EyeProlog: a focused Prolog implementation with inspectable derivations
Herbrand completed this work while still in his early twenties and died in
The declarative reading needs a precise answer to a deceptively simple question: what can a term denote? EyeProlog uses Herbrand semantics. Its universe contains exactly the ground terms that can be constructed from the program’s atom constants, numbers, list constructors, and compound functors. There are no unnamed elements hiding behind the notation. A ground term denotes itself.
This separates the Herbrand universe, whose members are terms such as
pat, 3, [red, blue], and ticket(alice), from the Herbrand base,
whose members are ground atomic formulas such as person(pat) and
owns(alice, ticket(17)). A term is not true or false merely by existing:
pat is a possible argument, whereas person(pat) is a proposition that an
interpretation may make true.
This three-level distinction answers several recurring questions. A newly constructed term does not automatically assert anything. A formula that can be written is not automatically true. And the model is not an arbitrary collection of convenient formulas: it is the smallest collection forced by the program. Keeping those levels separate makes symbolic data safe to inspect without confusing mention with assertion.
A Herbrand interpretation is a set of ground atomic formulas regarded as true. A source fact contributes one such formula:
parent(pat, jan).
A rule stands for all of its ground instances. Thus:
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).
says that for every substitution of X, Y, and Z by Herbrand terms, truth
of both body formulas entails truth of the head formula. Variables in rules
are implicitly universally quantified.
The declarative meaning of a pure Prolog program is its least Herbrand model: the smallest interpretation containing every fact and closed under every rule. One mathematical way to obtain it is the immediate-consequence operation. Begin with the facts; add each ground rule head whose ground body is already true; repeat until reaching the least fixed point. This construction defines meaning. It does not prescribe that the implementation enumerate the model from the bottom up.
Herbrand semantics is a particular form of ordinary model theory, chosen because logic programs inspect and construct symbolic terms. Consider:
different(alice, bob) :- (alice \= bob).
different(ticket(alice), ticket(bob)) :-
(ticket(alice) \= ticket(bob)).
In an unrestricted first-order interpretation, alice and bob could denote
the same object unless a unique-name axiom forbids it. Even if they denote
different objects, the interpretation of ticket need not be injective.
Additional axioms would be required to show that ticket(alice) and
ticket(bob) differ.
In the Herbrand universe those terms differ by construction. Different atom
constants are different terms; compound terms are free constructors and are
identical only when functor, arity, and corresponding arguments are identical.
Lists follow the same rule through [] and the internal ./2 constructor.
Unification, read-back, witness construction, and proof explanations therefore
share one predictable notion of identity.
This is a property of the representation, not a claim that two names can never
refer to one real-world entity. If robert and bob name the same person, say
so with same_as(robert, bob) or normalize them to one canonical term. The
Herbrand layer keeps names unambiguous; domain rules express equivalence.
The runnable
examples/herbrand-semantics.pl example and
its normal and proof outputs make this distinction concrete.
Variables range over Herbrand terms, not external records, pointers, or host-language objects. Variables in a selected goal are existential in the logic-programming sense: EyeProlog searches for substitutions that make the goal follow from the program.
EyeProlog has no blank nodes or existential variables in rule heads. When a rule needs to name a consequent object, construct an explicit witness:
has_parent(Child, parent_of(Child)) :-
person(Child).
registration(Student, Course, registration_of(Student, Course)) :-
takes(Student, Course).
The same inputs construct the same witness term; different inputs construct different terms. The witness is printable, queryable, and visible in a proof, rather than being an anonymous object created behind the program’s back.
Equality in the pure Herbrand reading is syntactic identity after substitution. Operationally, unification discovers a substitution that makes terms identical. EyeProlog performs an occurs check whenever unification would bind a variable. It therefore uses finite-tree unification and rejects a binding when the variable occurs anywhere in the proposed value. For example, this call fails rather than constructing a cyclic term:
(X = wrapper(X)).
ISO classifies unifications whose outcome depends on an occurs check as
subject-to-occurs-check (STO). EyeProlog’s default remains the sound finite-tree
behavior above. For diagnosis, EyeProlog additionally provides the
implementation-specific flag occurs_check; setting it to error turns a
normal unification that would otherwise fail because of the occurs check into
a representation error:
:- set_prolog_flag(occurs_check, error).
sto_example :- X = wrapper(X).
% error(representation_error(term), [])
The ISO error mechanism wraps an error term together with an
implementation-defined context term. For this implementation-specific STO
diagnostic EyeProlog uses representation_error(term) and currently uses the
empty list [] as that context. This reports that the cyclic result of a
succeeding STO unification cannot be represented by EyeProlog’s finite-tree
term model, without exposing a non-standard occurs_check/2 error term.
The supported values are true (the default finite-tree behavior) and error
(STO detection). EyeProlog deliberately does not provide occurs_check=false,
because its term model does not construct cyclic terms. The ISO predicate
unify_with_occurs_check/2 is independent of the diagnostic flag: it continues
to perform finite-tree unification and fails on unify_with_occurs_check(X,
wrapper(X)) even when occurs_check is error.
EyeProlog’s evaluator is goal-directed. It resolves selected goals against facts, rules, and built-ins using ordered conjunction, clause selection, indexing, tabling, and deterministic host operations. Written order defines the normal dataflow; a mode-ready deterministic built-in may be selected early as a pure filter. For the pure Horn-clause fragment, the answers it finds are intended to belong to the least Herbrand model. The evaluator is not, however, a complete bottom-up enumerator. Infinite generation or nonterminating recursion can prevent it from reaching a true answer.
Built-ins extend the pure core. Relational built-ins such as =/2,
append/3, and member/2 are readily understood over Herbrand terms.
Arithmetic, date handling, regular expressions, aggregation, once/1, and
negation have additional operational definitions. They still consume and
produce Prolog terms: X is 2 + 3 binds X to the Herbrand number term 5,
not to an invisible host value.
\+ Goal succeeds when the current finite search finds no solution for
Goal; it does not insert a negative formula into the Herbrand model.
User-defined negative dependencies should be stratified. In a stratified
program, positive dependencies may remain in the same or a lower layer, while
every negative dependency points strictly downward:
closed(X) :- blocked(X).
open(X) :- candidate(X), \+ closed(X).
A cycle containing a negative edge is not stratified:
p(X) :- q(X).
q(X) :- \+ p(X).
The CLI reports such portability problems with --warnings. JavaScript
embedders can inspect stratifiedNegation, negationStratificationErrors,
negationDependencies, and per-group negationStratum; request eager analysis
with analyzeNegation, reject it with strictNegation, or call
program.assertStratifiedNegation().
Checkpoint. Read one rule twice: first as a sentence about all its ground instances, then as a left-to-right sequence of subquestions. Identify which body goal first binds each variable. If you took the practical route, defer Herbrand bases and interpretations without guilt; recursion is next.
Recursive rules define an unbounded family of finite proofs. An ancestor is a parent, or a parent of an ancestor:
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).
eyeprolog --goal 'ancestor(X, Y)' program.pl
The first clause is the base case. The second reduces an ancestor question to a subquestion one edge farther through the graph. To design recursion, draw one proof, find the repeated subquestion, and ensure some path reaches a base case.
A recursive program should expose the same argument that would justify its
result on paper. For ancestor/2, that argument has four parts:
| Design obligation | ancestor/2 answer |
|---|---|
| Smallest supported case | one known parent/2 edge |
| Repeated question | whether the intermediate parent is an ancestor |
| Progress | advance from X to the next vertex Y |
| Finite reason | a finite graph gives finitely many endpoint pairs to table |
The progress column is deliberately not “the term gets smaller.” Structural recursion over a list usually consumes a tail; graph recursion moves through a finite relation; arithmetic recursion may decrease a number. State the actual well-founded argument for the intended mode instead of borrowing the language of a different recursion pattern.
Clause order then expresses a control preference. Trying the direct edge first finds short proofs early, but it does not change which ancestor pairs the two clauses mean. Reversing the recursive clause’s body is different: it asks an open recursive question before selecting an edge and may destroy the useful mode. Meaning and control must be reviewed separately.
Real graphs contain cycles. Naive depth-first recursion can revisit a call
forever. EyeProlog analyzes predicate dependencies and automatically tables
suitable positive recursive groups. A table records answers for a recursive
call, iterates cyclic calls to a fixed point, and reuses results. Authors
describe path/2; the engine chooses the recursive strategy.
Tabling does not make every open relation finite. A rule that constructs ever-larger terms can still produce infinitely many distinct calls or answers. Keep the selected query and its generators finite.
A relation can construct a witness:
:- use_module(library(lists)).
path(X, Y, [X, Y]) :- edge(X, Y).
path(X, Z, [X | Rest]) :-
edge(X, Y),
path(Y, Z, Rest).
On cyclic graphs, track visited vertices and use ISO negation as
\+ member(Next, Visited) to obtain finite simple paths rather than arbitrary walks.
Notice that ancestor/2 and path/3 make different promises. Endpoint
reachability has at most one logical pair for each pair of vertices, whereas
path construction may have many witnesses for the same endpoints. Table the
finite relation you need; bound or simplify the richer witness relation. This
distinction reappears in grammars, planning, proof search, and program analysis.
Checkpoint. In the three-edge family from Chapter 1, predict the direct and
indirect ancestor/2 answers. Point to the base clause and recursive clause,
then say what becomes smaller or moves closer to a known fact in one successful
derivation.
[a, b, c] abbreviates nested cons cells. [Head | Tail] exposes one cell;
[] is empty.
first([Head | _], Head).
contains_item(X, [X | _]).
contains_item(X, [_ | Rest]) :- contains_item(X, Rest).
joins([], Ys, Ys).
joins([X | Xs], Ys, [X | Zs]) :- joins(Xs, Ys, Zs).
Different modes give joins/3 different uses. It can construct a concatenated
list, enumerate every prefix/suffix split, or find a missing part. This is the
practical meaning of a relational definition.
Some algorithms carry explicit state through an accumulator:
reverse_acc(List, Reversed) :- reverse_go(List, [], Reversed).
reverse_go([], Acc, Acc).
reverse_go([X | Xs], Acc, Reversed) :-
reverse_go(Xs, [X | Acc], Reversed).
No mutation occurs; every call receives a new term. EyeProlog also includes
member/2, append/3, select/3, nth0/3, reverse/2, length/2,
ISO sort/2, slicing helpers, and numeric summaries. Improper lists such as
[a | Tail] are valid terms, but operations requiring a proper finite list
fail unless the tail is [].
Checkpoint. Trace joins([a], [b, c], Whole) by hand. Then reverse the
question: bind Whole to [a, b, c] and predict all prefix/suffix splits.
Finally explain why [a | Tail] is not yet known to be a proper finite list.
Part I established the relational eye:
You should now be able to read a program aloud, predict a unifier, write base and recursive clauses, and explain why a list relation may construct as well as inspect its arguments. Carry forward one habit: begin with a meaningful ground instance, then ask which variables may safely replace which parts.
The ingredients of Part I were assembled across several traditions. First-order logic supplied variables, substitution, and quantified formulas. Herbrand made ground terms and ground instances central to proof theory. Robinson’s 1965 resolution principle gave automated deduction a uniform, machine-oriented inference rule whose practical force depended on unification.
Prolog emerged when these ideas met a natural-language project in Marseille in the early 1970s. Colmerauer and Roussel stress that the project did not begin as an abstract attempt to invent a programming language: the need to analyze French drove the development of executable clauses and their control. Lists then became more than containers. They naturally represented sentences, syntax, proof states, and sequences of goals. The familiar two-clause list program condenses a much older mathematical pattern—definition by constructors and structural induction—into executable form.
A theory may justify many conclusions, but an evaluator must still find them. This Part studies the finite domains, constraints, failure, and choice that turn a field of possibilities into a productive computation.
Arithmetic uses the standard is/2 predicate, conventionally written with
infix operator syntax:
:- use_module(library(lists)).
next(X, Y) :- (Y is X + 1).
area_rectangle(W, H, Area) :- (Area is W * H).
hypotenuse(A, B, C) :-
(A2 is A * A),
(B2 is B * B),
(C2 is A2 + B2),
(C is sqrt(C2)).
Inputs must be bound to suitable numbers before a numeric function runs. Comparisons filter generated solutions:
safe_reading(Sensor, Value) :-
reading(Sensor, Value),
(Value >= 0),
(Value =< 80).
between(Low, High, Value) enumerates an inclusive integer range or checks an
already-bound value:
:- use_module(library(lists)).
square(N, Square) :-
between(1, 10, N),
(Square is N * N).
Finite generators turn loops into searches. Recurrences need intended modes:
factorial(0, 1).
factorial(N, F) :-
(N > 0),
(Previous is N - 1),
factorial(Previous, PF),
(F is N * PF).
The intended call direction belongs in the predicate’s tests and surrounding documentation; it does not require executable metadata.
Checkpoint. For every arithmetic goal above, mark which arguments must be
numbers before the goal can run. Explain why between/3 is a generator in
square/2 but merely a check when its third argument is already bound.
A goal fails when no clause or built-in proves it under current bindings. Failure prunes that branch and search tries another choice.
Failure is an operational event, not automatically a statement about the
world. Turning failure into \+ Goal is justified only relative to the
program and the current bindings. This is the closed-world move familiar
from databases: for some bounded relation, what cannot be derived is treated
as absent. It differs from the open-world stance common on the Web, where a
missing claim may simply be unknown. Neither stance is universally right; the
modeler must say which knowledge boundary is complete.
\+ Goal succeeds when Goal has no solution:
allowed(User) :-
user(User),
\+ blocked(User).
This means “blocked cannot be proved from this program,” not classical
negation. Bind variables before negating. Putting \+ blocked(User) before
user(User) asks whether there is no blocked user at all, not whether this
particular user is unblocked.
For ordinary \+/1, negative dependencies should normally be stratified:
compute a lower relation, then negate it from a higher layer. Use --warnings
to report negative recursion:
eyeprolog --warnings program.pl
Some finite rule systems intentionally contain recursion through negation. In
normal mode, EyeProlog provides explicit tnot/1 for that case. When the
reachable component is finite, function-free, and range-restricted Datalog,
cycles through tnot/1 are evaluated with the well-founded semantics (WFS)
rather than ordinary negation-as-failure. WFS has three truth states: true,
false, and undefined. A negative cycle may therefore produce a conditional
answer instead of forcing an arbitrary true/false choice.
move(a, b).
move(b, a).
win(X) :- move(X, Y), tnot(win(Y)).
Here neither win(a) nor win(b) is unconditionally established; both belong
to the undefined part of the well-founded model. EyeProlog exposes undefined
WFS answers as conditional successes, including inside finite collectors.
Direct calls to tnot/1 must be ground. In WFS rules, variables occurring in
the head or a negated literal must be range-restricted by positive body
literals. Ordinary \+/1 is unchanged, and strict ISO mode does not provide
tnot/1.
Universal checking needs no extension predicate: define the counterexample and negate it.
all_tests_pass(Suite) :-
\+ failing_test(Suite).
failing_test(Suite) :-
test_in(Suite, Test),
\+ passed(Test).
Use negation where the knowledge boundary is closed: a complete roster,
configuration, or finite result set. In open-world data, model explicit states
such as confirmed_absent instead of deriving absence from silence.
Checkpoint. Compare user(User), \+ blocked(User) with
\+ blocked(User), user(User). State the question each ordering asks and the
completeness assumption needed before calling either result “allowed.”
Finite aggregation asks about a solution set:
:- use_module(library(aggregate)).
:- use_module(library(lists)).
:- use_module(library(iso_ext)).
findall(Template, Goal, List).
countall(Goal, Count).
sumall(Value, Goal, Sum).
:- use_module(library(aggregate)).
:- use_module(library(lists)).
outgoing_costs(Node, Costs) :-
findall(Cost, edge(Node, _, Cost), Costs).
total_outgoing(Node, Total) :-
sumall(Cost, edge(Node, _, Cost), Total).
findall/3 returns [] for no answers; counts and sums return zero.
Choose a collector from the question, not from convenience:
| Question | Result shape | Empty search |
|---|---|---|
| Which witnesses were found? | findall/3 returns a list |
[] |
| How many derivations succeeded? | countall/2 returns an integer |
0 |
| What is their numeric total? | sumall/3 returns a number |
0 |
| Which candidate has the least or greatest key? | aggregate_min/5 or aggregate_max/5 returns one candidate |
failure |
Counting solutions is not necessarily counting distinct domain objects: two
proofs may resolve the visible value in the same way. When identity matters,
collect the identifying template and deliberately canonicalize it with ISO
sort/2; when derivation multiplicity matters, retain the duplicates. Making
that decision explicit prevents a database-style summary from silently
changing the question.
Optimization can retain only a best solution:
:- use_module(library(aggregate)).
:- use_module(library(lists)).
best_route(From, To, Route, Cost) :-
aggregate_min(
[CandidateCost, CandidateRoute],
CandidateRoute,
route(From, To, CandidateRoute, CandidateCost),
[Cost, Route],
Route
).
The key [Cost, Route] supplies deterministic tie-breaking through term order.
aggregate_min/5 and aggregate_max/5 fail when their goal has no answers.
An aggregate opens a smaller query scope inside the surrounding proof, and its
inner search must be finite.
Keep candidate generation separate from choice. A relation such as
route/4 should explain which routes exist and how their costs arise;
best_route/4 states a policy over that finite relation. This separation lets
the same candidates be inspected, counted, tested, or optimized without
burying their meaning in a single committed search. It also makes an empty
candidate set visible: “there is no route” is different from inventing a
sentinel route with an artificial cost.
Checkpoint. For an empty route relation, predict the behavior of
findall/3, countall/2, sumall/3, and aggregate_min/5. Then identify the
finite generator that bounds each aggregate in a program of your own.
Term predicates decompose or construct general terms:
functor(Term, Name, Arity).
arg(Index, Term, Value).
(Term =.. [Name | Arguments]).
arg/3 uses one-based indexes. Prefer direct pattern matching when the shape
is known; use inspection for generic transformations.
Text is best normalized at the model boundary. The library(strings) module
uses ISO-friendly atoms or proper lists of one-character atoms for its text
arguments; newly produced text is an atom:
:- use_module(library(strings)).
:- use_module(library(lists)).
normalized(Input, Words) :-
trim(Input, Trimmed),
lowercase(Trimmed, Lower),
split(Lower, ' ', Words).
Conversions include number_string/2, atom_string/2, and term_string/2.
Pattern operations include contains/2, matches/2, and named-capture
matches/3. Turn text into structured terms early and keep central rules
relational. Double-quoted source notation follows the ISO double_quotes flag;
it does not create a separate Prolog string type.
Parenthesized comma terms can serve as context data:
:- use_module(library(lists)).
message(event_17, (severity(high), source(sensor_3), reading(temp, 91))).
context_member((Left, _right), Member) :- context_member(Left, Member).
context_member((_left, Right), Member) :- context_member(Right, Member).
context_member(Member, Member) :- Member \= (_left, _right).
hot_event(Id) :-
message(Id, Context),
context_member(Context, severity(high)),
context_member(Context, reading(temp, Value)),
(Value > 80).
context_member/2 is an ordinary program relation: it walks a comma-context
from left to right. When the member’s shape is not known in advance, decompose
it with (Member =.. [Name | Arguments]). Context members remain quoted data;
inspecting them does not assert them as ambient facts.
Checkpoint. Distinguish the atomic formula message(...) from the nested
data term (severity(high), source(sensor_3), reading(temp, 91)). Explain why
context_member/2 can inspect the latter without asserting severity(high)
globally.
A robust finite search has three layers: generate candidates, constrain them, and present a concise answer.
color(red).
color(green).
color(blue).
coloring(A, B, C) :-
color(A),
color(B),
(A \= B),
color(C),
(B \= C),
(A \= C).
answer(colors(A, B, C)) :- coloring(A, B, C).
eyeprolog --goal 'answer(X)' program.pl
Place cheap, selective constraints as soon as their inputs are bound. For state-transition problems, represent state and moves explicitly:
:- use_module(library(lists)).
plan(State, State, _, []).
plan(State, Goal, Seen, [Move | Moves]) :-
transition(State, Move, Next),
\+ member(Next, Seen),
plan(Next, Goal, [Next | Seen], Moves).
The visited list makes a finite state space explicit. EyeProlog is strongest when the result is a logical consequence with a compact witness: a path, matching, classification, schedule, proof, or bounded model. Mutable arrays and large numerical kernels generally belong in a host, with EyeProlog as the decision layer.
For the coloring program, the six printed answers are the permutations of
red, green, and blue:
answer(colors(red, green, blue)).
answer(colors(red, blue, green)).
answer(colors(green, red, blue)).
answer(colors(green, blue, red)).
answer(colors(blue, red, green)).
answer(colors(blue, green, red)).
Checkpoint. Label the generator, each constraint, and the final witness in
the coloring program. Before changing it, predict how many answers remain if
A \= C is removed; then run the program and account for every additional
answer.
Part II turned relations into finite computations:
\+/1 makes finite failure a closed-world
test;once/1 makes search order observable;You should now be able to justify a query’s finiteness, order goals by binding dependency, distinguish negation as failure from classical negation, and explain why optimization is search plus an ordering.
Early Prolog made a decisive engineering choice: clauses would be tried in an order and subgoals would normally be selected left to right. That choice made logic executable, but also made control visible. A logically symmetric conjunction could behave asymmetrically when one order supplied a value and another asked arithmetic to run too soon.
The meeting of logic programming and database research in the 1970s sharpened questions about finite relations, closed-world reasoning, and query evaluation. Keith Clark’s 1978 account did not identify failure with unrestricted logical negation; it related negation as failure to a completed database reading. Later work on stratification disciplined negative dependencies. Aggregation continued the database lineage: a set of solutions could itself become data, provided the nested search was finite.
These distinctions explain EyeProlog’s conservative treatment. Negation and aggregation are powerful because they expose a bounded subcomputation. Their safety comes not from punctuation but from a mathematical argument about scope and termination.
An answer becomes useful when its grounds remain visible. Here reasoning is treated as an accountable structure: queries define the question, proofs retain support, integrity checks expose invalid states, and knowledge boundaries stay explicit.
EyeProlog goals are supplied by the host, for example
eyeprolog --goal 'child(X, Y)' program.pl. EyeProlog prints ground answers, removes
duplicates, and suppresses answers that merely repeat source facts. Answers
are not inserted back into the running program.
An answer and a derivation serve different audiences. An answer records what the theory supports; a derivation records how this run supported it. In mathematics that distinction resembles theorem versus proof. In data systems it resembles result versus provenance. The proof is not a substitute for valid source data or sound domain rules, but it makes both reviewable: a user can trace a decision to clauses, facts, bindings, and built-in operations instead of trusting an opaque status code.
Use --proof or -p to add a machine-readable why/2 fact after every answer:
eyeprolog --proof examples/socrates.pl
:- use_module(library(lists)).
why(
type(socrates, mortal),
proof(
goal(type(socrates, mortal)),
by(rule("socrates.pl", clause(4))),
bindings([binding("X", socrates)]),
uses([
proof(
goal(type(socrates, man)),
by(fact("socrates.pl", clause(3)))
)
])
)
).
Proof output is valid EyeProlog input:
eyeprolog --proof examples/socrates.pl > socrates.why.pl
A normal answer is one resolved ground term followed by a period. Strings,
quoted atoms, lists, and compounds are rendered in supported source syntax so
the output can be read back. Enabling --proof, --warnings, or --stats
must not change which answers are found.
The second argument of why/2 is an abstract proof term of the general shape
proof(goal(G), by(Method), bindings(Bindings), uses(Proofs)). User clauses
are identified as fact(Filename, clause(N)) or
rule(Filename, clause(N)), with one-based source clause numbers. Built-ins
are identified as builtin(Name, Arity). Explanation data is outside the
logical semantics of the input program: it describes the derivation but does
not participate in finding it.
A second program can query why/2. Read a proof as an argument. If it contains
irrelevant detours, improve the helpers. If a key premise is hidden inside an
opaque value, model it as a fact. Designing for a good explanation often
produces a better theory.
Checkpoint. Run examples/socrates.pl once normally and once with
--proof. Confirm that the ground answers are unchanged. In one proof,
identify the queried goal, the rule that derived it, the source fact used, and
the binding carried between them.
Integrity conditions are ordinary relations that describe invalid input states:
invalid_probability(Disease, Probability) :-
probability(Disease, Probability),
(Probability > 1).
A host that requires validated input queries the integrity relation explicitly before it asks for domain decisions. This keeps the policy visible: the host may reject the input, report every defect, or continue in a diagnostic mode.
false/0 keeps its ISO meaning: it is a built-in goal that always fails. It is
a protected static procedure, so false. and clauses of the form
false :- Body. are rejected with
permission_error(modify, static_procedure) rather than acquiring special
pre-query behavior.
invalid_assignment(Person, Role, Other) :-
assigned(Person, Role),
incompatible_roles(Role, Other),
assigned(Person, Other).
The logical reading is that the program can derive witnesses for an inadmissible combination. The operational response is outside the relation itself and remains an explicit host decision.
Start with a sentence that must never be accepted, then translate its witnesses into positive, finite goals. “No person has two incompatible roles” becomes the relation above. A useful integrity check is:
Four outcomes that can look like “failure” at a shell prompt have different meanings:
| Outcome | Interpretation | Appropriate response |
|---|---|---|
| query has no answer | this theory did not derive the selected goal | inspect data, rules, and closed-world assumptions |
| integrity query has an answer | the supplied input contains a forbidden combination | repair, reject, or report the input |
| resource ceiling is reached | the computation exceeded an operational budget | bound or redesign the search |
| parser or type error | the program or call violates the language contract | correct the source or interface |
Do not treat every undesirable business result as invalid input. A declined application, unavailable route, or negative test may be a perfectly valid answer of the theory. Reserve integrity relations for states whose witnesses must be handled before trusted downstream decisions.
To see the explicit validation path, run:
node bin/eyeprolog.js examples/integrity-check.pl
It prints the invalid-state witness and the resulting diagnostic status. Nothing runs implicitly before the supplied goals.
Checkpoint. Explain the difference between an ordinary query with no answer and an integrity query that returns a defect. Write one invalid-state relation and one ordinary negative result that should remain query failure.
Declarative clarity and operational care reinforce each other. Bind selective arguments early, keep generators finite, and make decreasing structure visible.
Naive depth-first search can revisit the same recursive question indefinitely. Tabling changes the unit of work: a call pattern becomes a shared subproblem, its answers are remembered, and consumers reuse answers rather than expanding the same call again. This idea connects logic programming to memoization and dynamic programming, but tabling also has a semantic role: over a finite positive recursive domain, repeated rounds can compute the least fixed point. It is therefore especially natural for reachability, grammars, dependency analysis, and other recursive relations with overlapping subproblems.
Ordinary goals use indexed depth-first resolution. Positive recursive groups
are tabled automatically. Bound recursive calls reuse answers and cyclic calls
iterate toward a fixed point. For sufficiently large finite, function-free
Datalog dependency cones, EyeProlog may share one most-general relation table
across call variants. This turns an open closure such as tc(X, Y) into one
finite relation computation rather than many overlapping bound subcomputations;
bound consumers can then use indexes over the stored answers. The exact
admission policy is an engine optimization, not part of the language contract.
Fully open or structurally unbounded recursive programs may retain ordinary
resolution.
Recursive components with negative dependencies are not positive least-fixed-
point problems. When such a component uses explicit tnot/1 and satisfies the
finite, range-restricted, function-free Datalog restrictions, EyeProlog instead
computes the alternating fixed point of the well-founded semantics. Existing
\+/1 code remains ordinary negation-as-failure.
This section explains why an optimization does not change clause meaning. Readers focused on modeling may skip to the statistics command and return when performance or implementation portability becomes relevant.
Every predicate group keeps compact indexes for scalar values in each argument
position. Index keys include the scalar type, so 7, '7', and "7" remain
distinct even though their printed payload is the same. A clause whose indexed
head argument is a variable or structured term
stays in a fallback set, and the selected candidates are merged back into
source order before unification. An index narrows where to look; it never
decides whether a clause matches.
For groups of at least ten clauses, a call with several bound scalar arguments may cause a wider combined index to be built on demand. The admission policy rejects indexes with too many variable fallbacks or too little expected speedup, and requires a combined index to improve substantially over the best single-argument index. These choices are performance details: removing every index should change running time, not answers or clause order.
Authors choose query modes, finite domains, visited-state representations, negation strata, and witness size. They normally do not choose the engine’s search strategy.
Inspect counters without changing answer output:
eyeprolog --stats examples/observability-log-correlation.pl
The reported counters include completed goal lists, calls to the goal solver
and single-goal solver, unification attempts, maximum depth and goal-list size,
deterministic built-in successes and failures, and table fixed-point rounds.
WFS execution additionally reports wfs_fixpoint_rounds and
wfs_undefined_answers. The latter counts undefined-answer observations made
while producing query results; it is an execution statistic, not a declaration
that those atoms are true. All statistics describe work performed, not logical
truth. Compare counters only across equivalent queries and the same
implementation version.
Common sources of nontermination are recursive calls made before constraints, ever-growing terms, infinite open mathematical queries, negative cycles, and path enumeration without a visited set. Repair the model by strengthening the query, adding a finite domain, tracking states, or exposing a decreasing argument.
Checkpoint. Classify three recursive calls: one justified by a decreasing list, one by finitely many tabled graph answers, and one that constructs terms without bound. State why the first two may terminate and why tabling does not repair the third.
A maintainable theory separates:
status/2, action/2, risk/2, and reason/2;Prefer positive domain concepts. Use negation only across a closed boundary. Represent confidence, alternative worlds, and provenance explicitly rather than hiding them in rule order.
An evidence-backed diagnosis can separate physics from policy:
heating(Battery, Watts) :-
current(Battery, Amps),
resistance(Battery, Ohms),
(I2 is Amps * Amps),
(Watts is I2 * Ohms).
thermal_warning(Battery) :-
heating(Battery, Watts),
heating_limit(Limit),
(Watts > Limit),
temperature(Battery, Celsius),
temperature_limit(TLimit),
(Celsius > TLimit).
action(Battery, isolate_and_cool) :- thermal_warning(Battery).
Physics, limits, redundant sensing, and policy become distinct proof steps. See
examples/spacecraft-battery-diagnosis.pl for a complete case.
Test theories with successful derivations, expected failures, boundary values, duplicate paths, contradictory inputs, and proof premises. The repository’s conformance cases, example goldens, and proof goldens demonstrate these levels.
Checkpoint. Draw four columns for the battery example: source, physical concept, decision, and integrity. Place each predicate in a column, then list the measurements and policy thresholds that a proof cannot authenticate by itself.
EyeProlog deliberately keeps external integration outside the reasoning core. An embedder validates input, converts it to ordinary Prolog terms and clauses, and then asks the solver a focused goal. This keeps parsing a business format, authenticating a source, and deriving a conclusion as three separate jobs.
A boundary should make four decisions visible:
Suppose a host receives one JSON temperature record. The host, not the logic program, owns the JSON syntax and the decision to trust that record. A narrow adapter can validate the record, map its values into a deliberately small Prolog vocabulary, construct the theory, and ask one bounded question:
import { run } from 'eyeprolog';
const inputText = '{"sensor":"sensor_1","celsius":91}';
const allowedSensors = new Set(['sensor_1', 'sensor_2']);
function reasoningSource(record) {
if (!record || typeof record !== 'object') throw new TypeError('record');
if (!allowedSensors.has(record.sensor)) throw new TypeError('sensor');
if (!Number.isFinite(record.celsius)) throw new TypeError('celsius');
if (record.celsius < -100 || record.celsius > 200) {
throw new RangeError('celsius');
}
return `
reading(${record.sensor}, ${record.celsius}).
thermal_alert(Sensor) :-
reading(Sensor, Celsius),
(Celsius >= 80).
`;
}
const record = JSON.parse(inputText);
const result = run(reasoningSource(record), {
goal: `thermal_alert(${record.sensor})`,
proof: true,
maxDepth: 10_000,
maxInferences: 100_000,
maxMemoryBytes: 256 * 1024 * 1024,
solutionLimit: 10
});
console.log(result.stdout);
The allow-list makes interpolation safe here: the external sensor identifier
can become only one of two known Prolog atoms, and the temperature must be a
finite number in an accepted range. General text must be encoded with a
well-tested term constructor or serializer rather than inserted into source.
The generated program defines only reading/2 and the fixed domain rule; the
host supplies the goal and ceilings explicitly.
This small example exposes four different claims:
| Stage | Claim and owner |
|---|---|
| Parse | the bytes are valid JSON — host parser |
| Validate | the record has an accepted sensor and temperature — adapter |
| Convert | the accepted values denote these exact Prolog terms — adapter |
| Derive | the supplied reading satisfies thermal_alert/1 — EyeProlog proof |
The proof procedure can explain how supplied clauses support an answer. It cannot prove that a file, database, sensor, or remote service was trustworthy. That responsibility stays with the host application.
Checkpoint. Choose one external record used by an application. State what the host validates, the Prolog term it constructs, the goal it asks, and the resource limit that prevents an untrusted input from consuming unbounded work.
The JavaScript API exposes a convenience runner and lower-level types:
import { run, Program, Solver, parseGoalText } from 'eyeprolog';
const result = run(`
answer(ok) :- ok = ok.
`, { goal: 'answer(X)' });
console.log(result.stdout);
console.log(result.stats);
The first console.log prints answer(ok). followed by a newline. The second
prints numeric work counters; those counters describe this run rather than an
additional logical answer.
run/2 accepts source text or an already parsed Program. Its options include
proof (with why and explain as aliases), maxDepth, maxInferences,
maxMemoryBytes, solutionLimit, a custom registry, and strictNegation or
analyzeNegation. It returns stdout, the solver’s numeric stats, and a
nullable haltCode; it does not write to the process streams.
When run receives an already parsed Program, canonical interop imports
needed only by its host-supplied goals are added to that Program before solving,
just as they are while source text is parsed. Pass autoload: false when the
Program must retain only explicitly imported predicates.
For applications that inspect or prepare a theory before running it, use
Program directly:
const source = `
edge(a, b).
edge(b, c).
path(X, Y) :- edge(X, Y).
path(X, Z) :- edge(X, Y), path(Y, Z).
`;
const program = Program.parse(source, { analyzeNegation: true });
const goal = parseGoalText('path(a, X)');
const path = program.findGroup('path', 2);
console.log(goal);
console.log(program.stratifiedNegation);
console.log(path?.recursive, path?.tabled, path?.tableInputPositions);
const solver = new Solver(program, {
maxDepth: 50_000,
maxInferences: 1_000_000,
maxMemoryBytes: 256 * 1024 * 1024,
solutionLimit: 100_000
});
The limits are safety ceilings, not logical declarations. Reaching the depth,
inference, or solution ceiling may truncate search; it does not prove that no
further answer exists. Reaching maxMemoryBytes instead raises
resource_error(memory), because continuing until the JavaScript engine’s hard
heap limit would let the host abort before Prolog could report an exception. At
the Solver API boundary, solutionLimit is opt-in: if it is omitted, ordinary
solving and child searches that inherit the solver limit do not stop after a
fixed number of solutions. This matters for re-executable goals such as
repeat/0 and for library relations such as call_nth/2; an implementation
safety threshold must not turn a still re-executable search into logical
failure. Embedders that need a finite answer budget should pass solutionLimit
explicitly.
Variable term order is deliberately scoped rather than stored as a permanent
property of a variable. ISO 13211-1 section 7.2.1 leaves the order of two
distinct variables implementation dependent and requires constancy only while
a sorted list is being created. EyeProlog therefore chooses a local variable
ranking for an ordinary term comparison, while sort/2, keysort/2, and the
sorting step of setof/3 share one ranking for the duration of that single
sorted-list operation. No process-global variable registry or creation ordinal
is retained or exposed through later comparisons.
EyeProlog periodically checks detectable JavaScript heap use and keeps a quarter
of the applicable host heap ceiling in reserve so the solver can unwind and report
resource_error(memory) before a fatal host out-of-memory abort. When Node is
started with --max-old-space-size, the guard compares that old-generation
ceiling with V8’s non-young heap spaces; short-lived new-generation allocations
therefore do not cause a false resource error. Embedders may
replace that automatically derived soft ceiling with maxMemoryBytes; setting
it to Infinity disables the proactive check. Environments that do not expose
heap use cannot provide the proactive check. Host capacity failures that V8
reports as Map maximum size exceeded or Set maximum size exceeded are also
normalized at the solver boundary instead of leaking a JavaScript RangeError.
ISO 13211-1 leaves the resource atom implementation dependent. EyeProlog uses
memory for a finite host allocation/capacity ceiling and reserves the
finite_memory spelling for the distinct convention where no finite amount of
memory could complete the computation. After a recoverable memory error, the
solver keeps a bounded recovery window while the failed search unwinds so the
host can collect released query terms. The same solver can then run later
queries; this recovery does not resume the query that exhausted its limit.
The iterative solver keeps active-call frames only where they are semantically
needed for cut scope or recursive variant guards. Bundled-library helpers whose
callable dependency region is cut-free and which need no recursive variant
guard therefore do not copy a growing active-call sequence at every step.
Under the normal EyeProlog registry, the bundled Prologue length/2 also has a
scoped iterative execution path: named lists are counted or constructed without
recursive interpreter frames, and an anonymous list is not materialized because
its binding cannot be observed. A newly constructed fixed-length suffix starts
as a lazy compact skeleton and expands one ordinary ./2 cell at a time when
unification, another list predicate, or answer readback inspects it. This is a
storage optimization, not a distinct Prolog term or list semantics. Embedders
that inspect the JavaScript term model can recognize this representation with
CompactListTerm, isCompactList, and compactListLength, or construct one
with compactVariableList. For open-ended length(List, N) generation, each generated spine is known not
to contain the caller’s dereferenced tail variable. The bundled path passes that
proof through the normal unifier, sharing the same proven-nonoccurrence mechanism
as first-use clause variables instead of maintaining a predicate-specific raw
binding shortcut. It still reserves recovery headroom proportional to the
retained spine, so a finite heap limit is raised inside the length/2 search as
a catchable resource_error(memory) rather than allowing an outer solver frame
to encounter the limit first.
The same proven-nonoccurrence mechanism has a conservative source-level form for freshly renamed
clauses. A singleton variable in the clause head, or a variable that has not
appeared in the head or any earlier body goal and occurs exactly once in a
direct =/2 goal, cannot already be a subterm of the value it is about to
receive. EyeProlog marks only that binding as locally fresh and skips its occurs
traversal. A repeated variable such as the X in X = f(X), a variable already
seen earlier in the clause, and unify_with_occurs_check/2 all keep the normal
finite-tree check. The solver also treats such a first-use equality as a
source-order barrier for its deterministic-goal scheduling, so the freshness
proof cannot be invalidated by moving a later goal ahead of it. This recovers
much of the classic WAM-family “local variable” optimization for DCG tail
variables without introducing a WAM local/global stack distinction into the
JavaScript term model.
For grammar execution, phrase/2 passes its fixed final remainder [] directly
into the expanded grammar. Besides matching the two-argument contract, this
avoids repeatedly trying an empty production against a temporary output
variable. phrase/3 still uses a private final-output variable and delays its
last unification, preserving the existing steadfast treatment of its explicit
third argument. The ordinary length/2 clauses remain the authoritative module
definition and are used unchanged by the ISO-only registry and whenever delays
or finite-domain constraints require their normal wake-up points.
The source layout mirrors the language boundary while keeping the JavaScript
runtime flat under src/. src/iso.js remains the stable ISO facade and
built-in registry; arithmetic evaluation lives in src/iso-arithmetic.js, and
processor control/error classes live in src/errors.js. src/dcg.js
implements the shared Part 3-oriented grammar-rule and dynamic-body expansion
without depending back on the ISO registry. This keeps the low-level syntax and
error layers acyclic while preserving the existing src/iso.js exports.
src/cleanup.js is an execution-layer sibling of the solver. It installs
lifecycle-aware closing of protected builtin iterators from the supported API
and CLI entry paths and registers call_cleanup/2 and
setup_call_cleanup/3 for the normal EyeProlog profile. The standard-library
layer does not import the solver back through this module, preserving the
acyclic source graph.
Program preparation follows the same pattern. src/program.js remains the
Program facade and source/module loader. Static recursion, Datalog, WFS, and
negation-stratification analysis is isolated in src/program-analysis.js,
while compact-clause representation and conservative candidate indexes live in
src/program-indexing.js. The solver consumes those same indexes directly;
large execution fast paths deliberately remain in src/solver.js rather than
being split through extra strategy objects or callbacks. Architectural cleanup
is required to preserve benchmark performance as well as semantics.
Focused files under src/lib/ contain the portable extensions, with
src/lib/lists.pl supplying common list relations. They are ordinary Prolog modules using EyeProlog’s documented module compatibility
surface, organized like Trealla’s library/ and registered for
library(Name) by src/standard-library.js in Node and the browser. The
browser entry point src/playground-worker.js uses that same program and
module-loading path in a dedicated worker. src/ARCHITECTURE.md records the
layering and dependency rules, and the architecture regression rejects
JavaScript import cycles.
Normal CLI, JavaScript, Solver, proof replay, and the browser playground use
the same module loader. A library is added to a Program only when its source
uses use_module/1 or use_module/2; exported predicates are imported into the
calling module and private predicates remain module-local. Advanced embedders
and conformance tests can select getStrictIsoRegistry() together with
isoStrict: true for the Part 1 + Corrigenda strict surface. All paths share
the parser, term representation, solver, streams, and proof machinery.
An embedder can start from the default EyeProlog registry and add a host relation. A handler is a generator over environments. It should clone before binding and yield only environments in which its result unifies:
import {
atom,
createEyePrologRegistry,
run,
unify
} from 'eyeprolog';
const registry = createEyePrologRegistry();
registry.add(
'host_status',
2,
function* ({ goal, env }) {
const next = env.clone();
if (
unify(goal.args[0], atom('service'), next) &&
unify(goal.args[1], atom('ready'), next)
) {
yield next;
}
},
{ deterministic: true }
);
const result = run(`
answer(X) :- host_status(service, X).
`, { registry, goal: 'answer(X)' });
Only mark a built-in deterministic when it can produce at most one environment
for a call. An unmarked suspended iterator is conservatively an untried
continuation: the solver never resumes it merely to discover whether a later
answer will succeed. An iterator that knows its remaining search positions may
provide hasPendingAlternatives(), updated before each yield, to remove its
resume frame exactly when no position remains. This method reports pending
search, not the existence of a future successful answer. A mode-sensitive
extension can additionally provide ready,
fallbackWhenNotReady, and shouldUse metadata. This metadata affects
dispatch and safe early filtering, so it belongs to the extension’s contract.
The ISO false/0 built-in always fails, and source clauses that attempt to
define it raise permission_error(modify, static_procedure). Programs expose stratification diagnostics through
stratifiedNegation, negationStratificationErrors, and
assertStratifiedNegation().
Treat remote source as executable logic. Although EyeProlog has no arbitrary host call primitive, search can consume CPU and memory. Embedders should impose appropriate depth, solution, input-size, and time limits.
Those ceilings are operational safeguards. If one is reached, report an incomplete computation rather than turning truncation into a negative domain conclusion.
Part III moved from obtaining answers to trusting them:
tnot/1 gives eligible finite Datalog components well-founded,
three-valued negation without changing ordinary \+/1;You should now be able to distinguish proof trees from search trees, state what an integrity query establishes, explain the finite-answer argument behind tabling, and name which trust duties remain outside the solver.
The least-model semantics developed by van Emden and Kowalski in 1976 connected
definite programs to a mathematical fixed point: repeatedly add supported
ground consequences until nothing new appears. Tabled logic programming later
turned fixed-point ideas into a goal-directed technique that shares recursive
calls and accumulates answers. EyeProlog’s automatic positive tabling is smaller
than the general systems in that literature, but inherits their central
insight: remembering a recursive question can change termination without
changing what the relation says. For finite Datalog with recursion through
explicit tnot/1, EyeProlog also uses the alternating-fixed-point account of
the well-founded semantics so a negative cycle may remain undefined instead of
being collapsed into ordinary negation-as-failure.
In parallel, deductive databases asked where facts come from and how derived claims retain provenance. EyeProlog adopts the expectation that conclusions should be inspectable while implementing a focused ISO Prolog profile.
The historical lesson is architectural. A proof procedure can attest that a conclusion follows from supplied clauses. It cannot authenticate a database, calibrate a sensor, or authorize a request. Systems became more trustworthy when those boundaries became named rather than implicit.
This Part turns from implementation features to habits of construction. A good program rarely arrives whole; it is discovered through examples, corrected by invariants, and refined without losing sight of the relation it means.
The central pleasure—and central difficulty—of logic programming is that a short definition plays two roles. Consider:
This distinction is one of logic programming’s oldest and most durable design ideas. The logical component describes admissible answers; the control component determines which consequences are explored, in what order, and with what resource cost. A change in indexing, goal order, or tabling policy should ideally preserve the first while improving the second. In practice, modeful built-ins and incomplete searches mean that programmers must reason about both.
:- use_module(library(lists)).
path(X, Y) :- edge(X, Y).
path(X, Z) :- edge(X, Y), path(Y, Z).
As logic, the clauses say that every edge is a path and that an edge followed by a path is a path. As control, they tell the solver to try a direct edge first, then choose an outgoing edge and continue from its endpoint.
It is useful to write the relation first as a sentence:
path(X, Y)holds when there is a finite sequence of edges fromXtoY.
That sentence is independent of clause order. It is the specification against which examples and counterexamples can be judged. Only then ask procedural questions: which argument will normally be known, which goal generates a finite set, and which recursive call is smaller or already tabled?
Conjunction is logically commutative, but its textual order guides search. These two rules have the same intended ground consequences:
:- use_module(library(lists)).
adult(Person) :- person(Person), age(Person, Age), (Age >= 18).
adult(Person) :- (Age >= 18), age(Person, Age), person(Person).
The first is executable in the natural open mode because person/1 and
age/2 bind values before >=/2 inspects them. The second asks a comparison
to operate on unbound variables and fails. Logical equivalence therefore does
not imply equivalent behavior for a goal-directed interpreter with modeful
built-ins.
Clause order also gives a search order. Put simple and common proofs where they can be found cheaply, provided doing so does not starve a necessary base case. A recursive clause that calls itself before consuming input is a warning:
:- use_module(library(lists)).
% Poor control: recursion starts before one list cell is exposed.
bad_member(X, List) :- bad_member(X, Rest), (List = [_ | Rest]).
The usual definition exposes the decreasing structure first:
item(X, [X | _]).
item(X, [_ | Rest]) :- item(X, Rest).
A predicate has one logical meaning but may support several useful calling
patterns. append(Prefix, Suffix, Whole) can:
Whole when the first two arguments are known;It is not a useful generator when all three arguments are free: there are infinitely many lists. Before accepting a predicate design, make a small mode table:
| Call | Intended use | Finite? |
|---|---|---|
append(+,+,-) |
concatenate | yes |
append(-,-,+) |
enumerate splits | yes |
append(-,-,-) |
generate all triples | no |
The + and - marks are documentation, not supported Prolog syntax.
A mode is a promise about calls, not a replacement for the relation’s meaning. When a rule calls a helper outside its promised mode, the program may remain logically plausible while becoming operationally useless.
A proof tree contains only the successful choices supporting one answer. A search tree also contains failed alternatives and repeated attempts. Proof output shows the former; performance counters give clues about the latter. Confusing the two leads to a common surprise: a tiny proof may have required a large search.
The distinction also explains why explanations are not performance profiles.
Removing a failed branch can make a program dramatically faster without
changing the final why/2 term. Conversely, introducing a well-named helper
may make a proof longer on paper while making it far clearer to a reader.
When a program is slow, sketch the first few levels of its search tree. Mark:
This exercise often reveals that the model is sound but a generator is too broad, a constraint is too late, or a witness carries needless alternatives.
Checkpoint. Take one clause and write two notes beside it: its ground meaning and its intended mode. Reorder two body goals, predict whether the answer set, termination, first answer, or proof shape changes, and only then run the variant.
A good logic program is rarely discovered by typing clauses from top to bottom. It is constructed by moving between examples, relations, and invariants.
Suppose packages must be routed through compatible hubs. Start with sentences that contain no variables:
:- use_module(library(lists)).
routeable(parcel_7, hub_north).
Decide exactly what that sentence claims. Does it mean the parcel can enter the hub, can leave it, or can complete an entire route through it? Ambiguity in a ground sentence becomes ambiguity in every rule built on it.
Now name the evidence:
:- use_module(library(lists)).
routeable(Parcel, Hub) :-
destination_zone(Parcel, Zone),
serves(Hub, Zone),
package_class(Parcel, Class),
accepts(Hub, Class).
The variables express the joins already present in the English explanation. No variable should appear merely because “a value might be needed later.” Every repeated variable asserts identity; every distinct variable permits difference.
For a recursive relation, write the smallest positive example, the next larger positive example, and a near miss. For list prefixes:
prefix([], [a,b]) true
prefix([a], [a,b]) true
prefix([b], [a,b]) false
The empty example suggests the base clause. Comparing the second example with a smaller one suggests removing a matching head from both lists:
prefix([], _).
prefix([X | Xs], [X | Ys]) :- prefix(Xs, Ys).
This is a general construction method: find a measure that becomes smaller, preserve the invariant while reducing it, and state directly the case where no reduction is needed.
Finite combinatorial programs become easier to read when their jobs are separate:
candidate_pair(A, B) :-
person(A),
person(B).
compatible_pair(A, B) :-
candidate_pair(A, B),
(A \= B),
\+ conflict(A, B).
answer(pair(A, B)) :- compatible_pair(A, B).
candidate_pair/2 states the domain. compatible_pair/2 states the
constraints. answer/1 controls presentation. The split is not bureaucratic:
it makes the closed domain visible, gives negation bound arguments, and makes
proofs say whether a step generated or rejected a choice.
For performance, tests may be interleaved as soon as their inputs are ready:
compatible_pair(A, B) :-
person(A),
person(B),
(A \= B),
\+ conflict(A, B).
The conceptual separation remains even when the final clause is compact.
The same domain can be represented in many ways. A graph may be edge facts, a list of edge terms, or a context. Ask which questions dominate:
edge/2 facts suit indexed relational lookup and proof provenance.Do not encode structure into strings and then recover it throughout the
theory. Parse once at the boundary. A term such as
address(City, PostalCode) can be unified, inspected, and explained; a string
containing the same data needs repeated procedural parsing.
Large rule sets benefit from a dependency direction:
source facts → normalized facts → domain concepts → decisions → answers
Negation should normally point in the same direction, from a higher layer to a complete lower layer. Cycles among positive domain concepts may be tabled; cycles through negation usually signal that the concepts have not been given a stable meaning.
At every layer, add one representative query. Do not wait for the final decision predicate to discover that normalization silently failed. Small queries are the logic-programming counterpart of inspecting intermediate values, but they retain the declarative vocabulary of the model.
Checkpoint. Before writing rules for a small domain of your own, record three positive ground examples, one near miss, the intended query mode, and a candidate finite generator. If the ground sentences are ambiguous, revise the predicate names before introducing variables.
Testing examples is necessary, but a reusable relation deserves a stronger argument. Two questions should be asked separately:
For prefix/2, partial correctness follows by the clauses. The base clause
returns only the empty prefix. The recursive clause adds the same head to a
smaller valid prefix, so the result remains a prefix. Completeness follows in
the opposite direction: every nonempty prefix shares its first element with
the whole list, and removing that element yields a smaller prefix problem
covered by the recursive clause.
This informal induction is often enough. State the property, justify each base clause, assume recursive calls satisfy it, and show that each recursive clause preserves it.
A correct relation may still fail to return. For ordinary structural recursion, identify a well-founded measure:
The measure must decrease before the recursive call in the intended mode. For
factorial, N decreases while remaining a nonnegative integer:
factorial(0, 1).
factorial(N, F) :-
(N > 0),
(Previous is N - 1),
factorial(Previous, PF),
(F is N * PF).
Reordering the subtraction after the recursive call preserves a mathematical equation but destroys the termination argument.
Tabling changes the argument for graph recursion. A cyclic path/2 call can
terminate when the program has only finitely many distinct tabled calls and
answers. The measure is then not necessarily smaller at each edge; finiteness
comes from exhausting a finite answer space. Tabling cannot rescue a rule that
constructs s(s(s(...))) without bound.
\+ Goal and aggregates ask the engine to settle a nested search. Their
meaning is usable only when that search can finish. Before
writing:
\+ disqualified(Person)
check that Person is bound and that disqualified/1 has a finite search for
that value. Before collecting routes, decide whether only simple routes, only
routes below a cost, or some other finite family is intended.
Ordinary failure says that one attempted proof did not work. An explicit integrity relation can instead return the evidence for an invalid state:
invalid_limits(Name, Low, High) :-
lower_limit(Name, Low),
upper_limit(Name, High),
(Low > High).
This distinction matters operationally and socially. A failed eligibility
query may be a legitimate negative result. A successful invalid_limits/3
query identifies contradictory limits; the host can then stop decisions until
the input is repaired.
Checkpoint. For one recursive relation, state three claims separately: partial correctness, completeness in one intended mode, and termination in that mode. Give the invariant supporting the first two and the decreasing measure or finite table supporting the third.
Program improvement begins with observation, not cleverness. Preserve a set of representative answers and proofs, collect solver statistics, and change one structural choice at a time.
The most effective improvement is often a better question. Prefer
route(brussels, Destination) to a completely open enumeration if the
application already knows its origin. Put selective, indexed relations early
enough to bind arguments for later work. Avoid constructing a large witness
when the caller needs only existence.
Compare:
connected(X, Y) :- path_with_nodes(X, Y, _).
with a direct reachability relation that tables pairs. The first may enumerate many distinct paths to establish one fact; the second records the fact itself. Keep the witness-producing relation for callers that truly need a path.
Inlining every condition creates wide clauses with repeated work. A helper can name a stable concept:
within_thermal_limits(Battery) :-
temperature(Battery, T),
temperature_limit(Max),
(T =< Max).
The gain is not just reuse. Proofs now contain a domain statement, and later
changes to the limit policy have one home. Choose helpers that add vocabulary;
avoid names such as step2/3 that merely expose an implementation sequence.
If a recursive call repeatedly computes a value that does not change, compute it once and pass the result:
search(Request, Answer) :-
normalized_request(Request, Normalized),
search_normalized(Normalized, initial_state, Answer).
This resembles loop-invariant code motion in procedural programming, but the relational formulation is explicit: the helper’s arguments show exactly which values vary from step to step.
Reordering goals, adding a helper, or specializing a predicate should preserve the intended ground answers. Verify that with:
An optimization that changes which proof is found first may affect once/1,
tie-breaking aggregates, and explanation shape even when the answer set is
unchanged. Treat those observable choices as part of the calling contract
whenever users depend on them.
Not every relation should be made maximally general. A three-mode predicate can be harder to terminate, explain, and index than two simple predicates with clear contracts. Generalize when a real second use appears. The art lies in keeping the logical idea visible while giving it enough control to run well.
Checkpoint. Save representative answers, one proof, and solver statistics for a program. Make exactly one control change, rerun all three views, and classify every difference as intended, harmless but observable, or a regression.
Part IV treated logic programming as a discipline of construction:
You should now be able to construct a theory from examples, state a termination measure, refactor a helper without losing meaning, and recognize when greater relational generality has no practical use.
Kowalski’s 1979 formulation “algorithm = logic + control” gave a durable name to the dual reading developed here. The logic component specifies knowledge; control determines how it is used. The slogan did not claim that control was unimportant. It argued that control can often be improved while meaning stays steady, and that programs become easier to reason about when the two are distinguished.
The craft tradition of Prolog grew around this tension. Goal ordering, accumulators, generate-and-test, and representation change were never merely interpreter tricks. At their best they were transformations justified by invariants and modes. Sterling and Shapiro made construction and improvement central to The Art of Prolog, showing that declarative clarity and procedural competence mature together.
EyeProlog removes several classic Prolog control devices, especially cut. The smaller surface changes the techniques but not the problem: authors must still turn a true relation into a productive computation and say what was preserved.
The earlier parts introduced the supported Prolog profile and the habits needed to use it safely. This part stays longer with whole computations. It asks how to inspect a search tree, represent languages and evaluators as relations, transform a correct program without losing its meaning, and organize a decision system whose conclusions remain auditable.
EyeProlog supplies the Part 1 control, dynamic-database, operator, and I/O
facilities together with its normal-profile module forms module/2,
use_module/1, use_module/2, and Module:Goal. These forms are treated as a
module compatibility surface, not as a claim of complete ISO/IEC 13211-2:2000
conformance. Definite-clause grammar notation remains
outside this profile. The examples still prefer explicit domain
relations, state, and syntax trees where that makes assumptions easier to
inspect.
A query is not solved in one leap. It is reduced to goals, each goal is matched against candidate clauses, and each successful match contributes bindings and new subgoals. The computation has two kinds of branching:
This and–or structure is the operational counterpart of the program’s logical structure. A conjunction asks for several supporting claims; multiple clauses offer alternative justifications.
parent(ada, byron).
parent(byron, clara).
parent(clara, diego).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).
eyeprolog --goal 'ancestor(ada, Who)' program.pl
For ancestor(ada, Who), the first clause asks
parent(ada, Who) and produces Who = byron. The second clause asks two
questions in sequence. parent(ada, Y) first binds Y = byron; the remaining
call is therefore ancestor(byron, Who). That call repeats the choice between
a direct-parent proof and a longer proof.
The three answers occupy increasing depths of one proof family:
ancestor(ada, byron)
parent(ada, byron)
ancestor(ada, clara)
parent(ada, byron)
ancestor(byron, clara)
parent(byron, clara)
ancestor(ada, diego)
parent(ada, byron)
ancestor(byron, diego)
parent(byron, clara)
ancestor(clara, diego)
parent(clara, diego)
Drawing even a partial tree exposes errors that are hard to see in source alone: a variable that should have been shared, a recursive call that did not consume input, or a generator placed after the test that needs its output.
A substitution is a set of bindings carried through the remaining goals. Bindings are not local return values. If the first goal binds a variable, every later occurrence of that variable sees the same term:
grandparent(X, Z) :-
parent(X, Y),
parent(Y, Z).
Solving grandparent(ada, Z) begins with parent(ada, Y). Once that goal
binds Y to byron, the second goal is the selective
parent(byron, Z).
Repeated variables impose equality through unification:
:- use_module(library(lists)).
loop_edge(Node) :- edge(Node, Node).
This does not find an arbitrary edge and compare its endpoints later. The shared variable makes equal endpoints part of the pattern being matched.
Suppose a later goal fails:
:- use_module(library(lists)).
eligible(Person) :-
applicant(Person),
age(Person, Age),
(Age >= 18),
verified(Person).
Failure of verified(Person) rejects the current combination of bindings.
Search may return to another age/2 fact or another applicant clause. It does
not retract source facts or erase answers already printed. Backtracking is
better understood as exploring alternatives than as undoing the world.
Recursive graph search can encounter the same logical subquestion through
different paths. Calls that differ only in variable names are variants:
path(a, X) and path(a, Y) pose the same pattern. Positive recursive groups
can table such patterns, share their answers, and stop a cycle from expanding
the same question forever.
The table does not prove termination for every recursive program. If each call constructs a larger pattern, then the calls are not variants:
grows(X) :- grows(wrapper(X)).
grows(A), grows(wrapper(A)), and
grows(wrapper(wrapper(A))) are distinct calls. Remembering them does not
make their number finite.
When a query surprises you, write down:
Compare that hand trace with --proof for a successful answer and --stats
for the amount of search. Proofs explain one successful derivation; statistics
summarize work across successful and failed branches. Neither is a complete
trace, but together they usually locate the modeling issue.
Exercises.
ancestor(byron, Who).clara and identify where the tree branches.edge/2 graph and compare reachability answers with the
table rounds reported by --stats.Checkpoint. Hand-trace one successful answer and one failed branch using
the six-item tracing discipline. Compare the successful trace with --proof
and the total work with --stats; state one fact that each view omits.
Compound terms are finite trees. A functor labels an internal node and its arguments are the children. Lists are one familiar tree encoding, but syntax, plans, types, circuits, formulas, and organizational structures can all be represented directly.
tree(
oak,
tree(birch, empty, empty),
tree(pine, empty, empty)
).
A structural relation follows the representation:
:- use_module(library(lists)).
tree_member(X, tree(X, _, _)).
tree_member(X, tree(_, Left, _)) :- tree_member(X, Left).
tree_member(X, tree(_, _, Right)) :- tree_member(X, Right).
The clauses say where a member may occur. They also define a search order:
root, then left subtree, then right subtree. If only membership matters, that
order is an implementation choice. If a caller uses once/1, it becomes
observable.
mirror(empty, empty).
mirror(
tree(Value, Left, Right),
tree(Value, MirroredRight, MirroredLeft)
) :-
mirror(Left, MirroredLeft),
mirror(Right, MirroredRight).
Read forward, mirror/2 constructs a mirror. With both trees ground, it
verifies the relationship. In some partially bound modes it can fill missing
structure. The rule does not mutate a tree; it relates two persistent terms.
The representation exposes an invariant: mirroring preserves every node value and exchanges left and right at every level. It also suggests a structural induction. The empty tree is its own mirror; if both recursive calls are correct, the constructed parent is correct.
ISO/IEC TS 13211-3 definite clause grammar rules describe a sequence while the processor supplies the pair of difference-list arguments used for execution:
sentence --> noun_phrase, verb_phrase.
noun_phrase --> [the], noun.
noun_phrase --> [a], noun.
noun --> [robot].
noun --> [scientist].
verb_phrase --> verb, noun_phrase.
verb --> [helps].
verb --> [observes].
complete_sentence(Words) :- phrase(sentence, Words).
eyeprolog --goal 'complete_sentence([the, robot, helps, a, scientist])' program.pl
The expansion of sentence//0 is an ordinary sentence/2 relation: its first
extra argument is the sequence before parsing and its second is the suffix.
Composition shares the suffix from noun_phrase//0 with the input to
verb_phrase//0. phrase/2 requires complete consumption; phrase/3 exposes
the remaining sequence.
The same relation can be a bounded generator when vocabulary and output length
are constrained by surrounding relations. An unconstrained
complete_sentence(Words) call can generate sentences of unbounded length if
the grammar is recursive. A grammar is also a search program, so its intended
modes need the same termination analysis as other recursive relations.
Syntax trees separate the expression from the act of evaluating it:
evaluate(number(N), N).
evaluate(add(Left, Right), Value) :-
evaluate(Left, L),
evaluate(Right, R),
(Value is L + R).
evaluate(multiply(Left, Right), Value) :-
evaluate(Left, L),
evaluate(Right, R),
(Value is L * R).
eyeprolog --goal 'evaluate(
add(number(2), multiply(number(3), number(4))),
Value
)' program.pl
The value is 14. More importantly, the proof follows the syntax tree: two
literal evaluations support one multiplication and one addition.
An extension can add variables and an explicit environment:
:- use_module(library(lists)).
lookup(Name, [binding(Name, Value) | _], Value).
lookup(Name, [_ | Rest], Value) :- lookup(Name, Rest, Value).
evaluate(variable(Name), Environment, Value) :-
lookup(Name, Environment, Value).
Passing the environment as data avoids hidden global state. Shadowing is
determined by list order and should be documented as part of lookup/3.
Evaluation collapses syntax to a value. Rewriting preserves syntax while replacing one form with an equivalent or preferred form:
simplify(add(number(0), X), X).
simplify(add(X, number(0)), X).
simplify(multiply(number(1), X), X).
simplify(multiply(X, number(1)), X).
simplify(add(A, B), add(SA, SB)) :-
simplify(A, SA),
simplify(B, SB).
Overlapping rules can produce several answers. That may be desirable when
exploring equivalent forms, but a normalizer needs a strategy and termination
measure. A rule that expands X to add(X, number(0)) reverses the first
simplification and permits unbounded rewriting.
Exercises.
tree_size/2 and tree_height/2.add(number(A), number(B)).Checkpoint. For one compound term, label the object-language syntax, the EyeProlog relation that inspects it, and the environment or state used to interpret it. Then identify a rewrite pair that would create a cycle if both directions were enabled.
Program transformation changes clauses while attempting to preserve an intended relation. The useful question is not merely “does the new version run?” but “for which calls does it preserve answers, termination, answer order, and explanations?”
Four transformations recur in logic programs:
Each can improve control or reveal structure. Each can also change modes, duplicate work, or alter proof shape.
Start with:
:- use_module(library(lists)).
adult(Person) :-
recorded_age(Person, Age),
adult_age(Age).
adult_age(Age) :- (Age >= 18).
Unfolding adult_age/1 gives:
:- use_module(library(lists)).
adult(Person) :-
recorded_age(Person, Age),
(Age >= 18).
For this deterministic helper, the ground answers are unchanged. The shorter
proof loses the named concept adult_age/1, however. That loss may be
undesirable in an auditable policy even if execution becomes slightly cheaper.
If a helper has several clauses, unfolding produces one caller clause for each
alternative. If it is recursive, unrestricted unfolding may never finish.
Folding moves in the other direction. Suppose decisions repeat:
:- use_module(library(lists)).
can_board(Person) :-
registered(Person),
identity_checked(Person),
\+ suspended(Person),
has_ticket(Person).
can_enter_lounge(Person) :-
registered(Person),
identity_checked(Person),
\+ suspended(Person),
lounge_pass(Person).
Name the shared concept:
:- use_module(library(lists)).
traveler_in_good_standing(Person) :-
registered(Person),
identity_checked(Person),
\+ suspended(Person).
can_board(Person) :-
traveler_in_good_standing(Person),
has_ticket(Person).
can_enter_lounge(Person) :-
traveler_in_good_standing(Person),
lounge_pass(Person).
The helper is valuable because it has a stable meaning, not merely because
three lines became one. It creates one place to state and test the closed-world
assumption behind \+ suspended(Person).
A general transport database may use:
connection(Mode, From, To, Cost).
An application that only plans rail journeys can define:
rail_connection(From, To, Cost) :-
connection(rail, From, To, Cost).
This wrapper establishes a stronger contract and gives indexing a bound first argument. Deeper specialization can precompute invariant classifications or remove irrelevant branches. Keep the general relation as the specification against which specialized answers are compared.
A direct list sum performs work after recursion:
sum_numbers([], 0).
sum_numbers([X | Xs], Sum) :-
sum_numbers(Xs, Rest),
(Sum is X + Rest).
An accumulator makes the partial sum explicit:
sum_numbers_acc(List, Sum) :- sum_from(List, 0, Sum).
sum_from([], Accumulator, Accumulator).
sum_from([X | Xs], Accumulator, Sum) :-
(Next is Accumulator + X),
sum_from(Xs, Next, Sum).
For a ground numeric list, both versions return the same sum. They do not have identical relational behavior in every mode. The accumulator version requires each intermediate addition to be ready on the way down. State the intended mode instead of claiming unconditional equivalence.
Before replacing one definition with another, record:
Then compare both versions. --stats can show fewer calls or unifications, but
performance evidence comes after semantic evidence. A faster program that
silently drops a mode is a different program.
Exercises.
once/1.Checkpoint. Choose one original and transformed relation. Compare their answer sets in both directions over a finite domain, then separately compare termination, answer order, duplicates, proof shape, and solver statistics.
Nondeterminism is not randomness. A nondeterministic relation defines several legitimate continuations, and search systematically explores them. The design problem is to make useful alternatives complete while keeping their number finite and their order productive.
A clear search program often has three layers:
worker(ada).
worker(byron).
worker(clara).
task(inspect).
task(repair).
qualified(ada, inspect).
qualified(byron, repair).
qualified(clara, inspect).
qualified(clara, repair).
assignment(Worker, Task) :-
worker(Worker),
task(Task),
qualified(Worker, Task).
eyeprolog --goal 'assignment(Worker, Task)' program.pl
worker/1 and task/1 make the search space explicit. qualified/2 is both a
constraint and a selective relation. If the application knows the task,
calling assignment(Worker, repair) avoids generating irrelevant task values.
A state-space problem needs a state term, a finite move relation, a goal test, a policy for repeated states, and a witness representation. A simple graph path carries visited nodes:
:- use_module(library(lists)).
simple_path(From, To, Path) :-
walk(From, To, [From], Reversed),
reverse(Reversed, Path).
walk(To, To, Visited, Visited).
walk(From, To, Visited, Path) :-
edge(From, Next),
\+ member(Next, Visited),
walk(Next, To, [Next | Visited], Path).
The visited list makes the witness finite on a finite graph. It also changes the question from arbitrary walks to simple paths. That is a modeling choice, not merely an optimization. A caller asking for repeated stops needs another bound, such as maximum steps or cost.
These questions have very different costs:
:- use_module(library(lists)).
reachable(From, To).
once(simple_path(From, To, Path)).
findall(Path, simple_path(From, To, Path), Paths).
Reachability needs only a pair and is a good candidate for tabling. One path may stop after the first witness. All simple paths may be exponentially numerous even though the graph is finite. Choose the weakest result that meets the caller’s need.
An optimal answer requires a finite candidate relation and a comparison key:
:- use_module(library(aggregate)).
:- use_module(library(lists)).
best_plan(Request, Plan, Cost) :-
aggregate_min(
[CandidateCost, CandidatePlan],
CandidatePlan,
candidate_plan(Request, CandidatePlan, CandidateCost),
[Cost, Plan],
Plan
).
The structured key makes ties deterministic. It does not reduce the candidate
space: aggregate_min/5 must settle the nested search before knowing the
minimum. For a large problem, strengthen candidate_plan/3 or use a
domain-specific dynamic program instead of assuming aggregation performs
branch-and-bound.
Depth-first clause search can become trapped in an infinite branch before reaching a later finite proof. Base cases should be reachable before recursive expansion, and recursive steps should consume a finite resource or enter a finite table. When neither is possible, the query is outside the practical contract of the relation.
Multiple clauses normally mean that any or all may yield legitimate answers.
once/1 turns the first success into a don’t-care choice: later alternatives
are intentionally discarded. Use it only when selection order is an accepted
part of the specification.
Exercises.
simple_path/3 to return accumulated cost.aggregate_min/5 still performs
an impractically large search.Checkpoint. Write down the size of a candidate space before running its search. Name the generator, the earliest ready constraint, the witness, and the ordering used for optimization. If the size cannot be bounded, the design is not yet ready for aggregation.
This case study develops a small access decision from prose to an executable, explainable theory. The purpose is the sequence of design decisions that turns informal requirements into maintainable relations.
A research facility says:
Before coding, identify ambiguities. Is the badge registry complete? Is missing training evidence a denial or unknown? Can a person have several active badges? Which clock determines “current”? A rule engine cannot remove these choices; it can only make the chosen answers precise.
Represent observations without embedding decisions:
:- use_module(library(lists)).
person(ada).
badge(b17, ada).
badge_status(b17, active).
badge_clearance(b17, laboratory).
zone_requires(clean_room, laboratory).
training_valid(ada, clean_room).
The badge identifier remains explicit. Collapsing it into
active_badge(ada) would hide the record used as evidence and make conflicting
records harder to detect.
Build vocabulary that reads like the policy:
:- use_module(library(lists)).
active_badge(Person, Badge) :-
badge(Badge, Person),
badge_status(Badge, active).
cleared_for(Badge, Zone) :-
badge_clearance(Badge, Clearance),
zone_requires(Zone, Clearance).
prepared_for(Person, Zone) :-
training_valid(Person, Zone).
Each helper has one responsibility. A proof of cleared_for/2 names both the
badge clearance and zone requirement rather than burying their join in a wide
decision clause.
If the suspension list is authoritative and complete, absence can be used:
in_good_standing(Person) :-
person(Person),
\+ suspended(Person).
If it is incomplete, this rule is unsound as policy. Replace it with a positive
source claim such as standing(Person, good). The difference is an agreement
about the knowledge boundary, not a matter of syntax.
:- use_module(library(lists)).
permit(Person, Zone) :-
active_badge(Person, Badge),
cleared_for(Badge, Zone),
prepared_for(Person, Zone),
in_good_standing(Person).
reason(Person, Zone, badge_and_training_verified) :-
permit(Person, Zone).
eyeprolog --goal 'permit(Person, Zone)' program.pl
eyeprolog --goal 'reason(Person, Zone, Reason)' program.pl
reason/3 supplies a stable user-facing summary. With --proof, the same
answer carries its detailed derivation. These are complementary: the reason is
domain vocabulary, while the proof records actual clauses and bindings.
Contradictory badge states are exposed by an explicit validation relation:
incompatible_status(active, revoked).
incompatible_status(revoked, active).
invalid_badge_status(Badge, Status, Other) :-
badge_status(Badge, Status),
incompatible_status(Status, Other),
badge_status(Badge, Other).
This result does not say that one permit failed. It identifies input that is
unfit for a trusted decision. The host can query invalid_badge_status/3 before
permit goals, alongside checks for a badge assigned to two people or a zone
with incompatible clearance definitions.
A useful test set includes an ordinary permit, missing training, suspension, insufficient clearance, duplicate derivations, contradictory status, and a proof showing the exact badge and training facts.
Boundary examples reveal requirements. If a person has two valid badges,
should there be one permit with two derivations or two permit terms containing
the badge? permit(Person, Zone) chooses one ground decision with potentially
several proofs. If badge identity belongs in the answer, define
permit(Person, Zone, Badge).
Authenticate source systems in the host, convert records to EyeProlog facts, run the theory, and store the answer with its proof and input version. The solver can explain logical support; it cannot attest that a badge database was current or a training provider trustworthy.
authenticated source snapshot
-> normalized EyeProlog facts
-> checked theory
-> permit and reason
-> proof referencing clauses and facts
When policy changes, preserve old inputs, theory versions, and proofs so a past decision can be reconstructed under the rules that actually governed it.
Exercises.
difference/3.denial/3 without assuming every failed permit has the same reason.--proof and decide which helpers improve the explanation.Checkpoint. Reconstruct one permit decision from a preserved source snapshot, theory version, answer, and proof. Mark which step authenticates the source, which checks integrity, which derives the decision, and which merely stores evidence for later audit.
Part V followed whole computations rather than isolated features:
You should now be able to trace substitutions through several goals, represent an object language without confusing it with the surrounding Prolog syntax, justify a bounded program transformation, and design a reconstructable decision theory.
Logic programming became a laboratory for symbolic programming because its principal data—terms, clauses, substitutions, and proof trees—could be represented with the same structures used for ordinary domains. Meta-interpreters made resolution itself a program topic; grammar rules made language recognition relational; partial evaluation showed how a general relation could be specialized when part of its input was known.
Futamura’s work in the 1970s gave partial evaluation a striking interpretation: specializing an interpreter with respect to a source program can produce a compiled form. Logic-program transformation developed related practices of unfolding, folding, and specialization. The inheritance for EyeProlog is not a promise that every classic transformation is built in. It is the demand that a transformation name its invariant and preserve a stated answer contract.
The Art of Prolog joined computation, construction, nondeterminism, grammars, interpreters, transformation, and applications into a sustained account of craft. Part V pays tribute to that breadth through EyeProlog’s explicit subset: syntax is data, state is an argument, and audit evidence remains visible.
This Part is a route, not a prerequisite for the reasoning laboratory. For a short practical path, read Chapters 26, 27, and 29, then continue at Chapter
Mathematics appears throughout this book as subject matter: arithmetic, combinatorics, graphs, geometry, algebra, statistics, and physical models. But its deeper presence is structural. A logic program is possible because parts of mathematical reasoning can be represented as finite symbols, transformed by explicit rules, and checked step by step.
In that qualified sense, the history of logic programming belongs inside the history of mathematics. It inherits the mathematician’s old practices of definition, proof, construction, abstraction, and counterexample. It also inherits the twentieth century’s harder questions. What counts as a formal proof? What is an effective procedure? Which truths follow from a finite set of axioms? Which questions cannot be decided by any uniform mechanical method?
EyeProlog inherits a small, practical fragment of that tradition. It is not a foundation for all mathematics, a computer algebra system, or an interactive theorem prover. Its definite clauses cover only a disciplined fragment of logic. Precisely because the fragment is small, however, one can see the ancient mathematical acts inside the running machine:
| Mathematical act | EyeProlog form | Operational consequence |
|---|---|---|
| Define a class | facts and clauses | enumerate its instances |
| Introduce an unknown | a variable | seek a substitution |
| Use a lemma | call a helper relation | open a subgoal |
| Split into cases | multiple clauses | create alternatives |
| Perform induction | base and recursive clauses | reduce to smaller calls |
| Construct a witness | bind an output term | return evidence, not only truth |
| Refute a universal guess | search for a counterexample | one answer is enough |
| Check consistency | an explicit integrity query | let the host reject or report invalid input |
| Explain a conclusion | a proof term | expose the successful derivation |
The table is a correspondence, not an identity. A mathematical proof and an EyeProlog execution answer different questions unless the encoding between them is itself justified. This Part develops both the power and the limit of the correspondence.
For most of mathematical history, an algorithm and a proof could live close together without being regarded as the same kind of object. Euclid’s algorithm computes a greatest common divisor, while Euclid’s propositions justify why the procedure works. A geometrical construction produces an object, while an argument establishes that it has the required properties. The distinction remains useful, but modern logic revealed increasingly exact connections among a proposition, its proof, and the construction carried by that proof.
Logic programming enters through one particular connection. A definite clause
mortal(X) :- human(X).
is at once an implication-like statement and an instruction for reducing the
question mortal(socrates) to the subquestion human(socrates). A successful
derivation does not merely return true; it records a sequence of justified
reductions and the substitutions that made them fit.
The route was neither straight nor inevitable. A compact historical spine is:
Each step narrowed one ambiguity while uncovering another. Formal syntax made proofs mechanically inspectable, but Gödel marked the boundary of formal completeness. Models of computation made “algorithm” exact, but Church and Turing marked the boundary of decidability. Resolution made inference uniform, but a proof procedure still needed control: selection order, clause order, termination discipline, and eventually tabling.
Logic programming is therefore not the historical triumph of machinery over mathematics. It is one result of mathematics becoming reflective about its own methods.
Consider a relation for a Pythagorean triple:
:- use_module(library(between), [between/3]).
:- use_module(library(lists)).
triple(A, B, C) :-
between(1, 20, A),
between(A, 20, B),
between(B, 20, C),
(AA is A * A),
(BB is B * B),
(Sum is AA + BB),
(Sum is C * C).
eyeprolog --goal 'triple(A, B, C)' program.pl
The open query asks an existential question over a finite domain: find values for which the equation holds. Each printed ground answer is a witness. The substitution is not an incidental side effect; it is the computational content of the existential claim.
A verifier and a generator are logically close but operationally different.
If A, B, and C are already known, the arithmetic goals check a candidate.
If they are unknown, the bounded between/3 calls create candidates first.
The equation alone does not tell a mode-sensitive evaluator where numbers
should come from.
This is an important distinction between mathematical existence and executable witness production. A classical proof may establish that something exists without furnishing an efficient construction. An EyeProlog query produces a witness only when its clauses and control actually reach one.
The normal answer
:- use_module(library(lists)).
triple(3, 4, 5).
states the result. Proof output adds the successful chain of facts, rule uses, built-ins, and bindings. That evidence supports three different activities:
These activities must not be conflated. A derivation can be mechanically valid but pedagogically obscure. It can be clear but depend on an untrustworthy source fact. It can be valid in the implemented arithmetic but fail to express the intended physical quantity. Proof output makes scrutiny possible; it does not perform all scrutiny on the reader’s behalf.
For a definite program, begin with its ground facts. Repeatedly add every ground rule head whose ground body is already satisfied. The least fixed point of this operation is the least Herbrand model.
For
:- use_module(library(lists)).
edge(a, b).
edge(b, c).
path(X, Y) :- edge(X, Y).
path(X, Z) :- edge(X, Y), path(Y, Z).
the closure first contains the two edges, then the corresponding direct paths,
then path(a, c). No unsupported path is added. Bottom-up closure and
goal-directed proof search approach the same declarative meaning from opposite
directions: one asks what follows globally, the other asks what is needed for
this goal.
Automatic tabling makes the connection visible. A table for a recursive component grows monotonically with newly discovered answers until no rule adds another. The implementation is performing a local, demand-driven fixed-point calculation.
There is an important lifetime distinction between a table that is needed to
finish one fixed point and a cache of tables retained for possible later reuse.
For recursive grammars invoked through phrase/2-3, EyeProlog uses a separate
invocation-keyed table scope rather than the caller’s general table map. Static
analysis also recognizes the normalized DCG form of strict list-tail recursion
(for example ... --> [_], ...). On a new input, such a grammar runs directly
without automatic tabling because every recursive call consumes the input and
there is no fixed-point cycle to close. This prevents a stream of distinct
sequences from constructing a useless suffix-table family for every input.
The optimization is deliberately asymmetric. If the same phrase/2-3
invocation repeats, normal tabling is allowed again so the compact completed
table from that invocation can serve subsequent repetitions. That is useful for
workloads that probe one grammar/input many thousands of times. Only one
invocation signature is retained in the phrase scope, completed entries are
still subject to a bound, and active fixed points are never evicted. Thus the
policy limits cross-call retention without turning each distinct input into an
O(cache-size) eviction pass or changing the declarative answers.
Exercises.
examples/fundamental-theorem-arithmetic.pl. Separate the witness
it constructs from the property it verifies.Checkpoint. For one printed mathematical answer, state the existential claim witnessed by its bindings, the finite domain that made search effective, and the separate argument—if any—that justifies a universal theorem.
Recursion and mathematical induction are not identical, but they are natural partners. Induction justifies a statement for every object generated by a finite construction. Recursion defines a result by following that same construction toward smaller objects.
For natural numbers represented as z, s(z), s(s(z)), and so on:
natural(z).
natural(s(N)) :- natural(N).
plus(z, Y, Y).
plus(s(X), Y, s(Z)) :- plus(X, Y, Z).
The clauses for plus/3 say:
Y produces Y;X to Y produces the successor of the result of
adding X to Y.Operationally, the first argument decreases by one constructor until it reaches
z. Mathematically, the clauses mirror a recursive definition. To prove a
property of plus/3 for all Peano naturals, induction on that first argument is
the obvious proof shape.
A recursive mathematical program invites three separate arguments:
They are logically independent. A program can terminate and return the wrong answer. It can return only correct answers while missing some. It can describe the correct relation and still diverge before producing it.
For plus(+,+,-), a termination measure is the number of s/1 constructors in
the first argument. Every recursive call strictly decreases that natural
number. The measure is well-founded: there is no infinite descending sequence
of natural numbers.
That last sentence is the mathematical heart of a termination proof. “It seems to get smaller” is not enough. Name a set with no infinite descent, give a measure into that set, and show strict decrease on every recursive branch.
Lists carry their induction principle in their syntax:
[];[Head | Tail].A relation following that structure is easy to reason about:
:- use_module(library(lists)).
list_length([], 0).
list_length([_ | Tail], N) :-
list_length(Tail, M),
(N is M + 1).
To prove that list_length(List, N) returns the number of cells in a finite
proper list, prove the empty case, then assume the claim for Tail and prove
it for [Head | Tail]. The recursive program and inductive proof share a
skeleton because both respect the same constructors.
Representation can either expose or obscure this skeleton. A syntax tree built
from number/1, plus/2, and times/2 supports structural recursion directly.
A flat token string requires parsing before the same argument becomes visible.
Good representations do not merely save code; they make invariants and proof
principles available.
An accumulator often improves control but makes the induction hypothesis more subtle:
reverse_acc(List, Reversed) :-
reverse_go(List, [], Reversed).
reverse_go([], Acc, Acc).
reverse_go([X | Xs], Acc, Reversed) :-
reverse_go(Xs, [X | Acc], Reversed).
The useful invariant is not merely “Reversed is the reverse of Xs.” It is:
reverse_go(Xs, Acc, Reversed)holds whenReversedis the reverse ofXsplaced beforeAcc.
Strengthening the statement makes the recursive step provable. This is a classic mathematical move: a theorem that is too weak to support induction is generalized until the induction hypothesis contains what the next step needs. Program transformation and proof discovery meet at the invariant.
Ordinary structural recursion terminates by decreasing a term. Graph reachability on a cyclic finite graph has no such simple decrease: following an edge can return to an earlier vertex. Tabling supplies a different well-founded argument.
If the graph has finitely many vertices, then there are finitely many possible
ground path/2 answers. A table only grows; each productive iteration adds a
previously unseen answer; therefore only finitely many productive iterations
are possible. Termination follows from finiteness of the answer space rather
than structural descent of each call.
The proof also states its boundary. If rules construct terms of unbounded depth, the set of possible calls or answers may be infinite, and tabling no longer supplies a finite bound.
Exercises.
plus(+,+,-) separately.reverse_go/3 invariant on paper.examples/peano-calculus.pl and identify where the data constructors
determine the available induction.Checkpoint. Align one recursive program with an inductive argument: base clause with base case, recursive call with induction hypothesis, and rule head with the preserved conclusion. Then give the independent termination measure.
Algebra studies operations by the laws they satisfy rather than by the material of the objects being operated on. Logic programming has a similar appetite for structure. Unification ignores the private identity of a variable name and asks whether two terms have a common instance. A relational program often works over lists, trees, graphs, substitutions, or group elements because the clauses depend only on their constructors and laws.
The goal
(pair(X, f(Y)) = pair(g(a), f(b))).
decomposes into structural equations. The outer functors and arities agree,
so corresponding arguments must agree; the resulting substitution is
X = g(a) and Y = b.
This resembles algebraic equation solving, but unification is more specific.
It operates in the free term algebra: different constructors are distinct,
and two constructed terms agree only when their outer symbols and corresponding
arguments agree. It does not know, unless clauses or built-ins say so, that
X is 2 + 3 and X is 3 + 2 express a commutative operation.
The distinction prevents a common conceptual error:
For polynomials, matrices, groups, or sets, choosing a canonical representation can turn some domain equalities into syntactic equalities. But the normalization algorithm then carries a proof obligation: equivalent objects must normalize alike, and normalization must not identify inequivalent ones.
Suppose a triangle is represented by three side lengths. Searching all permutations repeats the same geometric object six times. Ordering the sides removes the symmetry:
:- use_module(library(between), [between/3]).
:- use_module(library(lists)).
triangle(A, B, C) :-
between(1, 20, A),
between(A, 20, B),
between(B, 20, C),
(Sum is A + B),
(Sum > C).
The constraints A =< B =< C select one representative from each permutation
class. This is more than a performance trick. It is a quotient-like move:
identify descriptions related by a symmetry, then search canonical
representatives.
Mathematics repeatedly advances by finding the right equivalence relation. Fractions are identified when cross-products agree; graphs may be identified up to renaming; group presentations may denote isomorphic structures; logical formulas may be identified up to variable renaming. Logic programs must decide which distinctions belong to the problem and which are artifacts of notation.
A function privileges one direction. An equation or relation contains several:
:- use_module(library(lists)).
rectangle(W, H, Area) :- (Area is W * H).
In a supported arithmetic mode, this relation may verify an area or calculate it from width and height. With a finite generator it can also search for factorizations:
:- use_module(library(between), [between/3]).
:- use_module(library(lists)).
integer_rectangle(Area, W, H) :-
between(1, Area, W),
between(W, Area, H),
(Area is W * H).
eyeprolog --goal 'integer_rectangle(24, W, H)' program.pl
The relational view makes inverse questions conceptually ordinary, even when the implementation still needs an explicit finite direction. Mathematics has long moved between direct and inverse problems: multiply versus factor, evaluate versus interpolate, evolve a system versus infer its initial state. A relational vocabulary lets both questions share a specification where their common structure genuinely permits it.
Well-designed relations compose because variables carry outputs from one statement into another. Mathematical structure tells us what composition should preserve.
If a mapping is claimed to preserve an operation, write the preservation law
as a testable relation. For a symbolic mapping image/2 and operation
combine/3:
:- use_module(library(lists)).
preserves_combine(X, Y) :-
combine(X, Y, XY),
image(X, IX),
image(Y, IY),
image(XY, IXY),
combine(IX, IY, CombinedImages),
(IXY = CombinedImages).
Over a finite carrier, define a relation for a violating pair and use ISO
\+/1 to ask whether that counterexample relation has any answer. Over an
infinite carrier, finite testing is evidence, not proof. The algebraic law must
instead follow from definitions or a stronger proof system.
The examples d3-group.pl, matrix-noncommutativity.pl,
group-inverse-uniqueness.pl, and
composition-of-injective-functions-is-injective.pl show different roles:
computing a finite operation table, finding a counterexample to commutativity,
proving uniqueness from axioms, and composing preserved properties.
Representing a rational number as fraction(N, D) raises immediate questions:
may D be zero, must signs be normalized, and are fraction(1, 2) and
fraction(2, 4) identical or merely equivalent? These are not serialization
details. They determine the equality relation, the search space, and the
meaning of every later proof.
Before selecting a representation, state:
That checklist joins abstract algebra, data modeling, and program design.
Exercises.
examples/d3-group.pl to test identity, inverses, and associativity.
Which checks are exhaustive, and why?Checkpoint. Pick a domain value with two possible representations. State whether EyeProlog regards them as structurally equal, whether the domain regards them as equivalent, and which normalization or explicit relation connects the two notions.
Mathematicians do not prove only by moving forward from axioms. They calculate small cases, draw figures, search for patterns, try extreme examples, and hunt for counterexamples. Computation greatly enlarges this experimental practice. Logic programming contributes a particularly transparent form: generate a finite mathematical world, state the property relationally, and ask for witnesses or failures.
The first values of a sequence can suggest a recurrence. Exhaustive search up to a bound can destroy a false conjecture. Neither establishes a universal theorem over an unbounded domain.
This boundary can be written directly:
:- use_module(library(between), [between/3]).
:- use_module(library(lists)).
counterexample_to_odd_square(N) :-
between(1, 100, N),
(1 is N mod 2),
(Square is N * N),
(Remainder is Square mod 2),
(Remainder \= 1).
eyeprolog --goal 'counterexample_to_odd_square(N)' program.pl
No answer means only that no counterexample was found in the generated range
under the implemented arithmetic. The theorem that every odd integer has an
odd square needs an algebraic argument valid for an arbitrary integer:
(2k+1)^2 = 2(2k^2+2k)+1.
By contrast, if the claim concerns exactly the integers from 1 through 10,000, the finite exhaustive search can be a proof—provided the generator is complete, the predicate expresses the property correctly, and the arithmetic implementation is trusted.
A universal statement falls to one valid counterexample. This makes finite search especially valuable for criticism. Testing associativity over random inputs offers evidence; finding one triple where associativity fails settles the negative question.
noncommuting_pair(A, B) :-
matrix(A),
matrix(B),
matrix_multiply(A, B, AB),
matrix_multiply(B, A, BA),
(AB \= BA).
The example need not explain every failure of commutativity. Its existence is enough to refute the universal claim. This asymmetry between confirmation and refutation is one reason constraint solving, model finding, and property-based testing are so productive.
A finite structure consists of a finite carrier and interpretations of its operations and relations. EyeProlog can enumerate candidates, apply axioms as filters, and return models or countermodels. The method is mathematically serious because the scope is explicit.
For a carrier of three named elements, a binary operation table has nine entries. Searching all possible tables is finite but large. Algebraic laws can prune partial or complete candidates:
The order of these constraints is operational mathematics. A strong law applied early may collapse the search space; the same law applied after full generation merely rejects enormous numbers of candidates.
Search complexity is often a counting problem before it is a programming
problem. If a choice has n alternatives at each of k positions, naive
generation contains n^k leaves. If order does not matter, permutations may
be redundant. If partial choices already violate a constraint, pruning saves
an entire subtree.
This is why combinatorial examples are not toys. clpz-n-queens.pl exposes
the classic eight-queens search through finite-domain constraints. Together with send-more-money.pl,
integer-partitions.pl, stirling-bell-numbers.pl,
and weighted-interval-scheduling.pl, they show different geometries of
choice: permutations, digit assignments, recursive decompositions, set
partitions, and ordered optimization.
For each search program, ask a mathematical question before a performance question:
What objects are being counted, and when do two execution branches denote the same mathematical object?
Only after answering that should one add indexing, reorder goals, or introduce an accumulator. Otherwise the program may optimize accidental multiplicity.
The scientific examples combine logical rules with floating-point
calculations. beam-deflection.pl, orbital-transfer-design.pl,
competitive-enzyme-kinetics.pl, and least-squares-regression.pl encode
mathematical models of physical or statistical relationships.
A correct derivation inside such a model establishes a conditional:
given these measurements, equations, units, approximations, and thresholds, this conclusion follows under the implementation’s numeric semantics.
It does not establish that the sensor was calibrated, the model applies in this regime, omitted variables are negligible, or a floating-point result is an exact real number. The proof boundary should name these conditions rather than conceal them.
Exercises.
send-more-money.pl, then identify each
constraint that removes branches.examples/stirling-bell-numbers.pl to connect a recurrence with the
combinatorial objects it counts.Checkpoint. Label a computation as one of: witness construction, counterexample, exhaustive finite-model check, bounded evidence, or numerical model evaluation. Write one sentence stating exactly what its success proves and what its failure leaves open.
Mathematics earns unusual trust because it makes its conditions inspectable. Once definitions, axioms, and inference rules are fixed, a valid proof does not negotiate with status, rhetoric, fashion, or desire. The conclusion either follows by the accepted rules or it does not.
That is perhaps the precise sense in which mathematics does not cheat us. It does not promise that our premises describe the world. It promises that we can ask whether the conclusion follows from them.
Every theorem is conditional, even when the conditions have become culturally invisible:
axioms + definitions + inference rules
-> theorem
Every trustworthy EyeProlog conclusion has the same broad shape:
source facts + clauses + built-in semantics + execution assumptions
-> ground answer + proof
The arrows are where rigor lives. A proof disciplines the transition from premises to conclusion. It cannot authenticate the premises merely by using them.
This yields four layers of trust:
Explicit integrity relations expose contradictions and invalid states inside the supplied theory. Conformance tests address the implementation. Proof output addresses the derivation. Provenance, signatures, calibration, peer review, and domain validation address other layers. No single mechanism replaces the rest.
Mathematics corrects itself through definitions and counterexamples. A false conjecture is not rescued by the beauty of its statement. One legitimate counterexample has standing against a thousand confirming cases.
Logic programming should preserve this culture. Write negative tests before the theory becomes emotionally expensive. Search boundary cases. Ask for forbidden states. Turn domain invariants into queryable integrity relations. Keep the failed model that forced a redesign.
A knowledge system becomes trustworthy not when it never changes, but when it can say:
This is mathematical honesty translated into engineering practice.
Gödel, Church, and Turing did not diminish mathematics by proving limits. They made informal hopes precise enough to refute. There is no complete effective method that settles every sufficiently expressive mathematical question. No amount of faster hardware turns an undecidable general problem into a decidable one.
EyeProlog has smaller, immediate limits:
Naming these limits is not an apology. A trustworthy formal tool states the edge of its guarantee.
The deepest lesson is methodological. Mathematics asks us to separate:
Those separations are exactly what good logic programming requires. A predicate must have a sentence. A recursive clause must have an invariant and a termination argument. A finite search must declare its domain. An aggregate must have a bounded subsearch. A decision must retain its premises. A proof must remain attached to the theory version that licensed it.
The result is not certainty about everything. It is something more useful: certainty whose boundary is visible.
Before trusting an EyeProlog conclusion, ask:
That ritual is the book in miniature. State a small theory. Ask a precise question. Let the machine search. Inspect the witness. Challenge the premises. Preserve the proof.
Exercises.
Checkpoint. Take one strong conclusion and prefix it with every condition on which it depends: source authenticity, model scope, built-in semantics, finite search, theory version, and derivation validity. If the qualified claim still matters, the model has earned its confidence honestly.
Part VI placed logic programming inside the longer history of mathematics:
You should now be able to distinguish computation from proof, bounded evidence from a universal theorem, syntactic equality from mathematical equivalence, and valid derivation from trustworthy premises.
This arc begins before electronic computing. Hilbert’s program made formal proof and consistency mathematical objects. Gödel established limits for sufficiently expressive effective axiomatic systems. Church and Turing made effective calculability precise enough to prove that some general decision problems have no algorithm. Herbrand and Robinson supplied ideas that became central to automated first-order deduction.
Logic programming belongs to this history because it operationalizes a restricted proof discipline. It does not erase the limit results or turn every existence proof into an efficient witness generator. It gives a small region where propositions, substitutions, proof steps, and computations can be inspected together.
The deeper inheritance is a style of honesty. Mathematics advanced by proving not only more statements but also where methods fail, separating truth, provability, decidability, and computation. EyeProlog’s finite bounds, mode restrictions, search risks, and trust boundaries belong inside its account for the same reason: limits are part of the result, not fine print.
The final craft is experimental without being careless. A logic programmer works like a mathematician at a blackboard and an engineer at a test bench: state a claim precisely, derive consequences, seek counterexamples, measure the computation, and preserve enough evidence for another person to repeat the work.
This Part turns the book’s ideas into a daily discipline. It does not add a new language feature. It shows how to make theories survive change.
A conventional unit test often presents an input to a function and compares one returned value with an expected value. A relational program needs a wider test vocabulary. One call may have several answers, no answer, duplicate proofs, or different useful modes. Correctness includes the answer set, the absence of forbidden answers, the shape of witnesses, and the finiteness of the intended search.
Before writing test code, make a table in domain language:
| Case | Given | Question | Expected | Why this case matters |
|---|---|---|---|---|
| direct | edge(a,b) |
path from a to b? |
yes | base clause |
| composed | a→b→c |
path from a to c? |
yes | recursive clause |
| absent | disconnected d |
path from a to d? |
no | false positive |
| cycle | c→a |
all destinations from a? |
finite set | tabling or visited state |
| reflexive | no explicit loop | path from a to a? |
design choice | relation boundary |
The last row is especially valuable. Many bugs are not implementation mistakes but unresolved meanings. Does a path require at least one edge, or may it be empty? No test framework can choose the definition for you.
Queries naturally record positive expectations:
:- use_module(library(lists)).
edge(a, b).
edge(b, c).
path(X, Y) :- edge(X, Y).
path(X, Z) :- edge(X, Y), path(Y, Z).
eyeprolog --goal 'path(a, b)' program.pl
eyeprolog --goal 'path(a, c)' program.pl
To make an expected absence visible, define a finite observer:
unexpected_path :-
path(a, d).
expected_absence :-
\+ unexpected_path.
eyeprolog --goal 'expected_absence' program.pl
This is a test over a ground, terminating goal. It does not turn negation as failure into classical negation; it records that this finite theory derives no such path.
For a reusable package, prefer a dedicated test program that loads or repeats the relevant theory and declares only test queries. For a small example, the golden answer file is an executable specification of the expected answer set.
Suppose append/3 is intended both to concatenate and to split:
eyeprolog --goal 'append([a, b], [c], Whole)' program.pl
eyeprolog --goal 'append(Prefix, Suffix, [a, b])' program.pl
The first call should construct one list. The second should enumerate three splits. Testing only the first mode would miss a regression in relational generality; testing the completely open mode would request an infinite relation and prove little beyond the absence of a useful bound.
For every public predicate, record:
Keep this design close to the clauses in comments or tests, and exercise each supported call pattern directly.
Examples test selected points. A finite generated property tests every point in a declared scope:
:- use_module(library(between), [between/3]).
:- use_module(library(lists)).
double(N, D) :- (D is N + N).
double_is_even(N) :-
double(N, D),
(0 is D mod 2).
bounded_double_law :-
\+ bounded_double_counterexample.
bounded_double_counterexample :-
between(-100, 100, N),
\+ double_is_even(N).
eyeprolog --goal 'bounded_double_law' program.pl
This is exhaustive for the 201 generated integers, not for all integers.
Naming the predicate bounded_double_law/0 keeps the scope honest.
Useful finite properties include:
Sometimes the correct answer is hard to list, but a controlled change has a predictable effect. These are metamorphic tests.
If an isolated graph vertex is added, existing reachability answers should not change. If every edge cost is multiplied by a positive constant, the cheapest route should retain the same vertices. If the order of source facts changes, the set of logical answers should remain unchanged even if their discovery order changes.
A metamorphic test states a relation between runs. It is particularly useful for optimizations because it checks a preserved invariant rather than one frozen implementation trace.
An answer golden asks, “Did the public conclusions change?” A proof golden asks, “Did their supporting derivations change?”
Proof changes may be desirable after introducing a clearer helper. They may also reveal that a decision now depends on an unintended fact. Treat proof goldens as reviewed evidence, not snapshots updated automatically whenever a test fails.
Use answer regression broadly. Use proof regression selectively where provenance, explanation, or policy accountability is part of the product.
Three outcomes carry different meanings:
--warnings reports unstratified negation: execution may proceed, but the
program crosses a portability and semantic boundary.A mature suite covers all three. Include malformed source in parser tests, inconsistent source in integrity-query tests, and semantically dubious dependency cycles in warning tests.
Before releasing a theory or embedded service, cover:
| Dimension | Minimum evidence |
|---|---|
| Meaning | one positive, one absent, and one boundary case per public relation |
| Modes | every documented mode; explicit rejection or warning for unsafe uses |
| Recursion | base case, multi-step case, cycle, and termination argument |
| Search | smallest witness, competing witnesses, ties, and empty domain |
| Negation | ground success, ground failure, and stratification check |
| Aggregation | empty, singleton, duplicates, and deterministic tie handling |
| Integrity | each invalid state is detected and valid input is not misclassified |
| Proof | representative derivation with source premises visible |
| Scale | a case large enough to expose indexing or table behavior |
| Reproducibility | fixed time, source version, stable fixtures, and clean output |
Exercises.
ancestor/2, including a cycle and a
disputed reflexive case.Checkpoint. Assemble a minimum release matrix for one public relation: positive, absent, boundary, alternate mode, recursive or cyclic, integrity, proof, and scale cases. State which expected outputs should be exact goldens.
Debugging a logic program is difficult when every symptom is described as “the query failed.” Failure can mean the fact is absent, a variable was bound too early, a built-in ran outside its mode, a negative goal saw an unintended answer, recursion did not reach its base case, or the original relation was misstated.
Use four views in a fixed order:
Do not begin with an open query that prints hundreds of answers. Name one conclusion that is missing or surprising:
eyeprolog --goal 'eligible(alex)' program.pl
Then expand only the clause intended to prove it. Replace broad generators with the relevant ground facts. A small ground question removes accidental branching and makes every failed subgoal discussable.
If the ground question is itself ambiguous, stop debugging the implementation. Rewrite the domain sentence first.
Consider:
:- use_module(library(lists)).
eligible(Person) :-
(Age >= 18),
age(Person, Age).
The intended mathematics is easy to recognize, but >=/2 sees an unbound
Age. Write a binding ledger:
| Before goal | Goal | Bindings produced |
|---|---|---|
| none | Age >= 18 |
none; not ready |
| — | age(Person,Age) |
never productively reached |
Reordering the goals repairs the operational mode:
:- use_module(library(lists)).
eligible(Person) :-
age(Person, Age),
(Age >= 18).
For a clause with five goals, the ledger is often more revealing than staring at the source. Record structures as well as scalar bindings: a variable may be bound to an improper list or a compound term whose inner variables remain open.
No answers
Too many answers
Right answers, wrong order
once/1 or aggregate tie-breaking that makes order observable.Nontermination or explosive search
--stats and compare one controlled revision at a time.A surprising proof
Temporary helpers can expose intermediate concepts:
:- use_module(library(lists)).
candidate_debug(Person, Age) :-
age(Person, Age).
adult_debug(Person, Age) :-
candidate_debug(Person, Age),
(Age >= 18).
eyeprolog --goal 'candidate_debug(Person, Age)' program.pl
eyeprolog --goal 'adult_debug(Person, Age)' program.pl
Once the fault is understood, either remove the helper or rename it as a
permanent domain concept. Do not leave debug2/3 archaeology in a theory whose
proofs people must read.
For a bounded domain, write a deliberately simple reference relation and compare it with the optimized one. The reference may be slow; its purpose is clarity.
:- use_module(library(between), [between/3]).
:- use_module(library(lists)).
reference_square(N, S) :-
between(0, 20, N),
(S is N * N).
optimized_square(N, S) :-
between(0, 20, N),
(S is N * N).
disagreement(N, S) :-
reference_square(N, S),
\+ optimized_square(N, S).
eyeprolog --goal 'disagreement(N, S)' program.pl
A complete equivalence check needs both directions and must account for duplicates if proof multiplicity matters. Within a finite domain, differential testing is a powerful guard during program transformation.
--stats reports work, not meaning. A high solution count may be necessary or
may indicate a generator that should be constrained. Many table hits may show
effective reuse; many distinct table entries may reveal an argument that
prevents calls from sharing. On the Node CLI it also reports current heap use,
non-young/old-generation use, the amount currently compared with the memory
guard, resident-set size, and the soft and hard memory ceilings in bytes. These
memory figures are printed even when execution ends by raising a Prolog error.
--stats is an end-of-run summary. For a deliberately non-terminating or very
long computation, call the EyeProlog extension statistics/0 at the points
where a live snapshot is useful. For example, a long-running loop can include
statistics as one of its goals, as in loop :- work, statistics, loop.
statistics/0 writes the current solver counters and memory figures
immediately to the current output stream. statistics/2 makes an individual
value available to the program, for example
statistics(memory_guard_used_bytes, Used). With an unbound first argument it
enumerates the available statistic keys and values. An atom that is not an
available key raises domain_error(statistics_key, Key) rather than silently
failing. These predicates are EyeProlog observability extensions and are not
available under --iso-strict.
Compare statistics only between runs with the same query, data, and observable answer contract. A faster program that silently loses answers is not an optimization.
Every repaired defect should leave behind one of:
Otherwise the repository remembers the repair but forgets the reason.
Exercises.
Checkpoint. Preserve one defect as a regression. Record the smallest disputed ground question, expected answer, first incorrect binding or search choice, repaired invariant, and test that would fail if the defect returned.
A pattern is not a copied code fragment. It is a recurring arrangement of meaning, representation, and control that solves a named design problem. The following patterns summarize the strongest constructions in this book.
Problem: a predicate’s argument order and meaning drift while rules are being written.
Form: write one representative ground fact and read it aloud before adding variables.
:- use_module(library(lists)).
assigned_badge(alex, badge_17).
Consequence: argument roles become reviewable; modes and indexes can be discussed against a stable sentence.
Problem: spelling, aliases, units, or source-specific terms leak into every domain rule.
Form: retain source facts, derive one canonical vocabulary, and make core rules depend only on the normalized layer.
:- use_module(library(strings)).
:- use_module(library(lists)).
source_role(person_7, 'Doctor').
canonical_role(Person, clinician) :-
source_role(Person, Text),
lowercase(Text, doctor).
Consequence: adapters change independently from policy; proofs still trace back to source data.
Problem: a search relation mixes candidate production, pruning, and explanation until none can be reasoned about separately.
Form: generate a finite candidate, apply the cheapest selective constraints in dependency order, then construct a witness or reason.
:- use_module(library(between), [between/3]).
:- use_module(library(lists)).
chosen_pair(pair(X, Y), reason(sum_is_ten)) :-
between(0, 10, X),
between(X, 10, Y),
(10 is X + Y).
Consequence: the search domain and each pruning step are visible.
Problem: a Boolean-like conclusion proves existence but loses the object needed for explanation or later computation.
Form: add a structured output containing the path, assignment, schedule, or evidence summary.
:- use_module(library(lists)).
path(X, Y, [X, Y]) :- edge(X, Y).
path(X, Z, [X | Rest]) :-
edge(X, Y),
path(Y, Z, Rest).
Consequence: answers become constructive; witness size and duplicate paths become explicit design concerns.
Problem: the domain needs a negative conclusion, but absence is meaningful only after a complete finite search.
Form: bind the subject and finite scope before \+/1; isolate the
closed-world step behind a clearly named predicate.
unregistered(Person) :-
person(Person),
\+ registered(Person).
Consequence: the closed-world assumption has one reviewable home. It must not be mistaken for an explicit fact that the person is not registered.
Problem: planning or interpretation appears to require mutable state.
Form: represent the old and new states as terms related by an action.
step(state(Room, outside), enter(Room), state(Room, inside)).
Consequence: histories are ordinary lists, transitions can be queried, and the state representation exposes invariants.
Problem: reachability, inheritance, or dataflow revisits the same finite subquestions.
Form: state the positive recursive relation directly and let eligible components be tabled.
depends(X, Y) :- direct_dependency(X, Y).
depends(X, Z) :- direct_dependency(X, Y), depends(Y, Z).
Consequence: termination rests on a finite call and answer space, not on pretending the graph is acyclic.
Problem: low-level helper clauses produce technically correct but unreadable explanations.
Form: introduce stable domain concepts and a small public decision relation whose premises are meaningful reasons.
within_limit(Device) :-
reading(Device, Value),
maximum(Max),
(Value =< Max).
status(Device, safe) :-
within_limit(Device).
Consequence: internal calculations remain available, while the successful proof reads in domain vocabulary.
Problem: contradictory or impossible input would make ordinary conclusions misleading.
Form: encode forbidden combinations as ordinary relations with diagnostic arguments.
:- use_module(library(lists)).
invalid_badge_assignment(Badge, PersonA, PersonB) :-
assigned_badge(PersonA, Badge),
assigned_badge(PersonB, Badge),
(PersonA \= PersonB).
Consequence: callers can collect every defect, and a host that requires validated input can query this relation before it requests trusted decisions. The rejection policy remains explicit rather than being hidden in clause-head syntax.
Problem: an answer can be reproduced only if its facts, rules, and external semantics are known.
Form: retain source snapshot, theory version, adapter version, and relevant clock or numeric assumptions beside the proof.
theory_version("2026-07-24").
source_snapshot("telemetry-0042").
numeric_model(ieee_754_double).
Consequence: an old decision can be reconstructed under the system that actually made it rather than silently rerun under today’s theory.
The unbounded open generator. A relation is queried with every argument free even though its mathematical extension is infinite.
The premature test. A mode-sensitive built-in or negative goal appears before the goals that bind its inputs.
The accidental Cartesian product. Two goals use different variables for what should be the same entity.
The opaque mega-clause. One rule performs normalization, search, policy, and explanation with no named intermediate concepts.
The Boolean witness eraser. A relation returns only yes after doing the
work needed to construct a useful path or reason.
The silent closed world. Failure to derive a fact is used as its opposite without documenting finite scope and completeness assumptions.
The proof-hostile helper. Names such as step3/2 or tmp/4 expose an
implementation sequence instead of a domain idea.
The optimization by answer loss. once/1, early aggregation, or reordered
search makes a benchmark faster by changing the public answer contract.
The floating theorem. A numerical result is described as mathematically exact without naming units, approximation, or host floating-point behavior.
The timeless decision. Sources and rules change, but conclusions retain no snapshot or theory version.
Patterns compose. A robust decision service often uses:
normalize at the boundary
-> generate, constrain, describe
-> carry the witness
-> proof façade
-> integrity before inference
-> version the evidence boundary
Do not apply every pattern mechanically. A three-fact teaching example does not need six architectural layers. Introduce a pattern when its named problem is present, and keep the smallest theory that makes meaning and control clear.
Exercises.
Checkpoint. Select the smallest set of patterns that solves a real problem in one theory. For every selected pattern, name the pressure that justifies it; remove any layer that exists only because the catalog made it available.
Part VII made theory development repeatable:
You should now be able to design a release-quality test matrix, reduce a surprising result to one ground question, compare a reference relation with an optimized relation, and recognize productive patterns and anti-patterns.
Logic programs have long stood between specification and implementation. That made testing both easier and subtler: a ground clause could serve as an example, yet a relation might have several modes and an answer set rather than one returned value. Testing practice absorbed ideas from theorem proving, database validation, software regression, and property-oriented testing.
The repository form of this practice is historically significant in its quiet way. A theory, exact answer file, proof file, conformance corpus, and version tag preserve not only a program but expectations about its meaning. Regression tests make old decisions reviewable; property tests seek counterexamples; metamorphic tests state what remains invariant across controlled change.
Patterns complete the cycle by naming recurring design knowledge. Sterling and Shapiro’s craft-oriented presentation helped establish that expertise lives in constructions and transformations, not syntax alone. The reasoning laboratory extends that attitude into maintenance: prediction, execution, evidence, and revision form one method, and the failure that taught a lesson becomes executable memory.
The supported ISO Prolog profile includes processor-facing facilities that
become important in reusable libraries, language tools, long-running
applications, and file boundaries. Earlier chapters use its relational core;
this part makes control, reflection, state, operators, and streams explicit.
For Part 1 portability work, EyeProlog also provides a strict core mode:
--iso-strict on the CLI or isoStrict: true in the JavaScript API restricts
the language/runtime surface to ISO/IEC 13211-1:1995 plus Technical Corrigenda
1–3. The processor character set is an implementation-defined choice shared by
normal and strict profiles: EyeProlog uses Unicode scalar values U+0000..U+10FFFF
excluding surrogates, with the scalar value as the collating-sequence integer.
Strict mode restricts implementation-specific language facilities, but it does
not narrow this processor-defined character repertoire. Isolated mode and error cases live in test/conformance/cases/iso/.
The examples here compose those operations into programs worth changing and
rerunning.
These facilities do not all have the same declarative character. Term inspection and atomic conversion are relations. Cut commits to an operational choice. Dynamic updates and stream operations change solver-owned state. Use the pure relation when it expresses the problem; introduce control or effects at a named boundary.
The control predicates accept goals as arguments. call/1 invokes a callable
term, and call/2-8 appends arguments to a callable closure. The expanded goal
runs in the current search continuation, so a direct goal and its meta-called
form expose the same remaining alternatives; the meta-call still establishes
its own cut boundary. once/1 keeps its first solution, and !/0 commits
within the clause that contains it. If-then-else commits to the first successful
condition:
travel_status(From, To, Status) :-
(route(From, To) -> Status = connected ; Status = disconnected).
once(Goal) is a local request for one solution. Cut is lower level: it
discards alternatives created since entry into the current predicate call.
The two can produce the same first answer without expressing the same control
boundary. Keep cut close to the choice it documents and test the complete
answer set before and after introducing it. A cut executed inside a predicate
called by one disjunction branch remains local to that predicate: if the branch
later fails, Left ; Right must still try Right. This remains true for
cut-bearing validation helpers used by generators such as between/3.
Exceptions separate an exceptional call from ordinary logical failure:
require_route(From, To) :-
(route(From, To) -> true ; throw(no_route(From, To))).
checked_route(From, To, Result) :-
catch(
(require_route(From, To), Result = accepted),
no_route(From, To),
Result = rejected
).
The catcher is unified with the thrown term. A matching recovery goal runs in
the environment at the catch/3 boundary; unrelated exceptions continue
outward. Prefer failure for an expected negative answer, such as a route that
does not exist. Throw when a caller cannot safely interpret the computation,
for example malformed input or an unavailable required resource. ISO
instantiation, type, domain, permission, representation, and evaluation errors
follow this same exception path.
Normal EyeProlog also provides call_cleanup(Goal, Cleanup) and
setup_call_cleanup(Setup, Goal, Cleanup). Cleanup is run exactly once when the
protected search completes deterministically, is exhausted, is cut or otherwise
pruned, top-level answer enumeration is abandoned, or an exception unwinds the
search. setup_call_cleanup/3 runs Setup once and installs Cleanup only after
Setup succeeds. On cut or ordinary pruning Cleanup sees the current Goal
bindings; on exception unwind the Goal bindings have been removed and Cleanup
sees the Setup environment. When a deterministic protected goal runs Cleanup
before yielding, substitutions produced by a successful Cleanup are included in
that answer. Cleanup failure is ignored, and an exception already being
propagated takes precedence over a cleanup exception. Nested cleanups run
inside-out. These two controls are EyeProlog extensions and are absent from
--iso-strict.
Collection also makes search boundaries explicit. findall/3 returns one list
and existentially closes variables that occur only in its goal. bagof/3
instead creates a group for each binding of a free variable and fails when
there are no solutions. setof/3 has the same grouping rule, then sorts and
deduplicates each group. The ^/2 notation marks a goal variable existential:
:- use_module(library(lists)).
regional_total(Region, Total) :-
bagof(Amount, Seller^sale(Region, Seller, Amount), Amounts),
sum_amounts(Amounts, Total).
Here Region deliberately remains free and produces one answer per region;
Seller is hidden from grouping. This distinction matters whenever a
collection unexpectedly arrives as several answers.
Integer arithmetic has similarly precise choices. // truncates the quotient
toward zero, while Corrigendum 2’s div takes the mathematical floor. With a
positive divisor, mod returns a nonnegative modulo while rem keeps the
dividend’s sign. For -7 and 3, // is -2, div is -3, and the two
remainders are 2 and -1. Bitwise conjunction, disjunction, exclusive-or,
complement, and shifts require integers.
Run the focused examples:
iso-control-and-errors.pl
covers call/1, once/1, cut, if-then-else, and recovery;iso-grouped-solutions.pl
contrasts the three collectors and inspects a source clause; andiso-integer-arithmetic.pl
makes division and bit-operation results visible.Checkpoint. Explain why bagof(Amount, sale(Region, Seller, Amount), X)
groups on both Region and Seller, then write the existential qualification
that groups only on Region. Name one expected absence that should fail and
one broken precondition that should throw.
Ordinary pattern matching should remain the first choice when term shape is known. Reflective predicates are valuable when the shape itself is input: generic walkers, schema checkers, interpreters, and source transformations.
functor/3 relates a term to its name and arity. arg/3 selects a one-based
argument. =../2—traditionally called univ—relates a term to a list whose
head is the functor and whose tail contains the arguments:
term_shape(Term, shape(Name, Arity, Arguments)) :-
functor(Term, Name, Arity),
(Term =.. [Name | Arguments]).
In a construction mode, functor/3 creates a term with fresh arguments and
=../2 rebuilds a term from a proper list. Their ISO errors are useful
guardrails: an unknown functor name, negative arity, partial univ list, or
uninstantiated required argument is not silently treated as failure.
copy_term/2 preserves sharing inside a term while replacing its variables
with fresh ones. term_variables/2 returns each distinct variable in
first-occurrence order. Identity predicates make the distinction observable:
==/2 tests whether two resolved terms are identical without binding them;
\==/2 is its negation. =/2 still performs unification, while
unify_with_occurs_check/2 explicitly rejects cyclic bindings.
The standard term-order family—compare/3, @</2, @=</2, @>/2, and
@>=/2—compares terms without evaluating arithmetic. Do not replace
3 + 4 < 8 with 3 + 4 @< 8: the former evaluates numbers and the latter
orders syntax.
Atomic conversion predicates expose reversible representations:
atom_concat/3 joins an atom or solves a sufficiently instantiated split;sub_atom/5 relates a source to before, length, after, and fragment;atom_chars/2 and atom_codes/2 use character atoms or Unicode codes;char_code/2 converts one character; andnumber_chars/2 and number_codes/2 parse or render ISO numbers.These are atom relations, distinct from the EyeProlog library predicates whose
historical names contain string. Quoted atoms such as 'λ' remain atoms;
with the default flag, "λ" denotes the character list ['λ'].
iso-reflective-terms.pl
walks through shape, rebuilding, fresh copying, variables, and order.
iso-atomic-conversion.pl
demonstrates both conversion directions and every three-character sub-atom of
eyeprolog.
Checkpoint. Given pair(X, X), predict the variable list before and after
copy_term/2. Then explain why atom_codes/2 belongs at a text boundary
rather than throughout a domain theory.
A dynamic predicate is a mutable clause store owned by one solver run. Declare it before updates:
:- dynamic(task/2).
prepare_queue :-
asserta(task(check_power, urgent)),
assertz(task(check_network, normal)).
asserta/1 inserts at the beginning and assertz/1 at the end. retract/1
removes the first unifying clause and can be retried for later matches.
abolish/1 removes a dynamic procedure. clause/2 inspects accessible
clauses, while current_predicate/1 enumerates or tests predicate indicators.
Static and private built-in procedures are protected by permission errors.
Updates are ordered effects, not pure logical conclusions, and they are not
undone by ordinary backtracking: later goals observe a changed database. Each
update invalidates cached tabled and ground-chain answers, and rule changes
refresh recursion and negation analysis before later goals continue. Keep them
in a narrow lifecycle layer.
The queue example performs setup in initialization/1, so query order does not
determine its state:
:- initialization(prepare_queue).
Initialization runs after preparation and before host queries. include/1
expands a source file in place; ensure_loaded/1 loads the same designation at
most once. multifile/1 and discontiguous/1 document permitted clause
layout. Prolog flags and character conversions are also solver-scoped and
should be set deliberately near the boundary that relies on them.
Operators offer readable syntax without adding a new data model:
:- op(600, xfx, reports).
sensor_7 reports temperature.
The fact is exactly reports(sensor_7, temperature). Priority determines
binding strength, and fx, fy, xf, yf, xfx, xfy, and yfx
determine position and associativity. current_op/3 inspects the table;
op(0, Specifier, Name) removes a definition. Because declarations affect
parsing of subsequent text, place them before their first use. ISO argument
syntax also permits an atom that is currently an operator to appear directly
as a functional argument or list element, so forms such as
current_op(Priority, Specifier, :-) and [:-,-] are valid without quoting
or parenthesizing those operator atoms. A current operator atom may likewise
be the complete content of parentheses or curly brackets: (+) denotes the
atom +, and {*} denotes the curly term {}(*). Term output observes the
same context rules: with quoted(true), an operator atom is not quoted merely
because it occurs as a functional argument, list element, or sole curly-bracket
content. Thus writeq({*}) emits {*}, writeq([:-,-]) emits [:-,-], and
writeq(f(;,'|',';;')) emits f(;,'|',';;'); the bar stays quoted because
ISO treats the unquoted | token as a list separator rather than an atom. The ISO initial operator table also
contains ?- at priority 1200 with specifier fx, so
current_op(1200, fx, ?-) succeeds. EyeProlog’s embedded quad syntax permits
an optional label before the query marker (Label ?- Query.), so while quad
syntax is supported it additionally exposes ?- at priority 1200 with
specifier xfx as an implementation-specific operator. Consequently
current_op(Priority, Specifier, ?-) enumerates both definitions. At top level in the normal EyeProlog profile, the quad marker is recognized
from the parsed ?-/1 or ?-/2 term rather than from one privileged surface
spelling. Thus Label ?- Query., ?-(Label, Query)., mixed forms such as
?-((Label), Query)., quoted-functor notation, and a parenthesized whole
(?-(Label, Query)). denote the same quad when followed by indented answer
descriptions. Label itself is parsed with the ordinary Prolog term grammar:
there is no quad-specific comma or metadata syntax. The runner requires the
resulting first argument to be ground; if it is not, that quad is reported as
BAD_ID and later quads are still processed. In --iso-strict mode this quad
interpretation is disabled, and ?-/2 remains ordinary Prolog term syntax.
Run iso-dynamic-database.pl
for an explicitly stateful queue and
iso-operators.pl
to see custom notation decomposed back into an ordinary term.
Checkpoint. State the final clause order after one asserta/1 and two
assertz/1 calls. Then rewrite one custom-operator fact in canonical
functor notation and verify it with =../2.
Streams are handles to ordered input or output. open/4 adds options to the
basic open/3: text or binary type, alias, repositioning, and end-of-file
action. Always close a nonstandard stream, including exceptional paths in
application code.
write_event(Path, Event) :-
setup_call_cleanup(
open(Path, write, Stream, [type(text)]),
( write_canonical(Stream, Event),
put_char(Stream, '.'),
nl(Stream)
),
close(Stream)
).
In normal mode, setup_call_cleanup/3 is the preferred lifecycle boundary for
resources such as streams: close(Stream) still runs if the protected work
fails, throws, is cut, or its remaining alternatives are abandoned. Strict ISO
mode does not provide this EyeProlog extension.
The period is essential when another Prolog processor will read the result as
a term. write/1-2 uses readable conventional syntax, writeq/1-2 quotes
where needed, and write_canonical/1-2 exposes canonical structure. Dotted
graphic atoms do not need quotes merely because they contain a period:
writeq(./*), writeq(.*), and writeq(...*) output ./*, .*, and ...*
respectively. ISO term output uses only the separator characters needed by the
syntax, so functional arguments, list elements, and operator applications are
emitted compactly when no lexical ambiguity would arise. For example,
writeq([a,b]) outputs [a,b] and writeq(1+2) outputs 1+2; a separator is
still retained where adjacent graphic tokens would otherwise merge, as in
a+ -b.
write_term/2-3 supports quoted/1, ignore_ops/1, numbervars/1, and
variable_names/1. Normal mode additionally accepts the EyeProlog extension
double_quotes(true|false): true lets eligible character/code lists use the
current double_quotes representation, while strict ISO mode rejects this
implementation-specific write option. Normal mode also accepts the
implementation-specific boolean spacing(true|false) option: false emits
only separators required to avoid lexical ambiguity, while true adds
conventional layout around operators. For example,
write_term(1+1,[spacing(false)]) emits 1+1 and
write_term(1+1,[spacing(true)]) emits 1 + 1. The REPL always follows
the minimal-separator rule, so X=1*1 is displayed as X = 1*1, while a
separator is retained in X = a+ -b because the adjacent graphic tokens would
otherwise merge. Strict ISO mode rejects both extension options.
Character operations are get_char, peek_char, put_char, get_code,
peek_code, and put_code; byte streams use the corresponding byte
predicates. Peeking does not advance the position. Mixing byte operations with
a text stream, or text operations with a binary stream, raises a permission
error rather than guessing an encoding.
read/1-2 reads the next term. read_term/2-3 can also return all variables,
source variable names, and singletons. The metadata contains variables, so a
program normally validates or transforms it before placing it in a ground
query answer. Every read operation creates a fresh variable set: a source name
such as X in two separately read terms does not alias either the caller’s X
or the variable named X by the other read. Within one read term, repeated
occurrences and the variables returned through its metadata still share as
written. stream_property/2 exposes mode, type, alias, position, and
end state. current_input/1, current_output/1, set_input/1, and
set_output/1 manage defaults shared by nested goals.
End of file is a state transition, not merely a character. With
eof_action(eof_code), term input yields end_of_file, character input yields
end_of_file, and code or byte input yields -1; at_end_of_stream/1
tests the position. Repeated input after the end follows the selected
eof_action.
iso-term-io.pl
writes a temporary fixture, reads its terms in order, checks variable metadata,
and observes end of stream. The file lives under /tmp; running the example
does not modify the checkout.
Checkpoint. Write a term round trip and name where quoting, the terminating period, stream type, and close operation matter. Explain why a stream side effect belongs outside the central relation that decides what the term means.
The supported ISO facilities make EyeProlog suitable for more than closed rule files:
Chapters 38–40 state the supported profile, list every registered predicate, and document the command line; the conformance corpus fixes success, failure, mode, and error behavior. Use this part for working practice and those chapters for exact reference.
Reference is useful only when the route into it is clear. Begin with the task in hand: Chapter 38 answers what source means, Chapter 39 helps select a predicate, and Chapter 40 turns a file into observable evidence. Chapters 41–43 then support study design, boundary decisions, and precise vocabulary. The long catalogs are meant to be entered locally, not memorized linearly.
The normative strict-core baseline is ISO/IEC 13211-1:1995, as corrected by
Technical Corrigenda 1:2007, 2:2012, and 3:2017. The post-N289 WG17/STC
working draft is used as defect-discovery input, not as an unpublished fourth
Corrigendum. The 2026-08-23 draft through items #73-#76 is tracked by
test/conformance/STC-DRAFT-STATUS.md; where a proposal changes published
semantics, such as #75’s conditional power-underflow proposal, strict mode keeps
the licensed baseline until the change is standardized or explicitly adopted as
a compatibility extension. Normal EyeProlog additionally provides a practical
module interface aligned with later WG17 module amendment work and a
definite-clause-grammar profile following ISO/IEC TS 13211-3. Those
normal-mode profiles are documented and tested compatibility surfaces; they are
not currently claimed as complete clause-by-clause certifications of Part 2 or
Part 3.
Normal-mode Prolog source accepted by EyeProlog is UTF-8. % starts a line
comment and /* ... */ delimits a block comment. Plain atoms begin with a
lowercase ASCII letter. Variables begin with uppercase or underscore. The bare
_ is fresh each time. Single quotes delimit quoted atoms; double quotes use
ISO double-quoted-list notation. Integers, decimals, scientific notation,
binary/octal/hexadecimal integers, and character-code constants are accepted.
The processor character set is shared by normal and --iso-strict modes because
Part 1 makes it implementation defined rather than an extension boundary.
EyeProlog’s PCS is the Unicode scalar repertoire. Printable ASCII keeps the Part
1 lexical classes; Unicode letters extend alphanumeric name syntax, Unicode
white-space characters are layout, and remaining non-ASCII symbols/punctuation
are extended graphic characters. Character-code and collation values are the
corresponding Unicode scalar integers. Quoting remains available for any atom
spelling that should not depend on an extended lexical class:
:- use_module(library(lists)).
city('München').
message("café").
Inside a quoted atom, a single quote is doubled: 'don''t'. EyeProlog follows
the ISO quoted-character grammar rather than accepting arbitrary backslash
escapes. The symbolic control escapes are \a, \b, \r, \f, \t,
\n, and \v; the meta characters backslash, single quote, double quote, and
back quote may be escaped after a backslash; and numeric octal or hexadecimal
escapes are terminated by a backslash. For example, '\7\' and '\x7\'
both denote the alert character. Forms such as \c, \d, \e, \u, \.
and \ are not ISO quoted-character escapes and are syntax errors.
A literal layout character other than ordinary space is not a quoted
character. In particular, a literal tab or newline inside quotes is a syntax
error. A quoted token can cross a line boundary only through a continuation
escape: a backslash immediately followed by the newline, which contributes no
character to the atom. The NUL character is written readably as '\0\';
digits 8 and 9 are not octal digits, so forms such as '\8\' are syntax
errors. writeq/1 uses octal escapes for other non-symbolic control characters,
for example ESC is written as '\33\'. Double-quoted lists use the same
quoted-character rules. Whitespace is insignificant between tokens, and a %
comment continues to the end of its line. Doubling the active delimiter is
also accepted inside either quoted form, so "" inside double-quoted notation
denotes one literal double quote character.
Graphic tokens use the characters #$&*+-./<=>?@^~\; ! and ; are solo
atoms. A colon is the Part 2 module qualification operator in Module:Goal;
quote an atom whose name itself contains a colon. Unquoted angle-bracket IRIs
are not syntax.
A /* sequence opens a block comment only when it begins a token; inside a
maximal graphic token the slash and star remain atom characters. Graphic tokens
are formed maximally before a period can be recognized as the terminating full
stop. Consequently, interactive input *. or ./*. is not yet a complete term:
the period is part of the graphic atom and the reader waits for a separate
terminating full stop. Thus ./*. . reads the atom ./*. and consumes the
second period as the terminator.
In the grammar below, { x } means zero or more repetitions of x, [ x ]
means that x is optional, and parentheses group alternatives. These marks
describe the grammar; they are not characters written in EyeProlog source.
program ::= { clause }
clause ::= head "."
| head ":-" goal-list "."
head ::= term
goal-list ::= term { "," term }
term ::= variable | atom-constant | double-quoted-list | number
| compound | list | curly-term | parenthesized-term
compound ::= atom-constant "(" term { "," term } ")"
list ::= "[" "]"
| "[" term { "," term } [ "|" term ] "]"
double-quoted-list ::= '"' { quoted-character } '"'
curly-term ::= "{}" | "{" term "}"
parenthesized-term ::= "(" term [ "," term { "," term } ] ")"
variable ::= "_"
| variable-start { name-continue }
atom-constant ::= plain-atom | quoted-atom | graphic-atom
plain-atom ::= lowercase-letter { name-continue }
number ::= [ "-" ] digits [ "." digits ] [ exponent ]
exponent ::= ( "e" | "E" ) [ "+" | "-" ] digits
variable-start ::= uppercase-letter | "_"
name-continue ::= uppercase-letter | lowercase-letter | digit | "_"
Zero-arity compounds such as ready() are unsupported; use ready. Every
clause ends in a period. The grammar above gives the canonical term shapes.
The initial operator table contains the following ISO-style operators, all
lowered to ordinary compound terms:
?-, \+, unary +, unary -, and \;,, ;, and ->;?- is also a priority-1200 xfx operator so a
label may precede a quad query;--> and the Part 3 alternative |;=, \=, ==, \==, @<, @=<, @>,
@>=, is, =:=, =\=, <, =<, >, and >=;+, -, *, /, //, div, mod, rem, /\, \/,
<<, >>, **, and ^.op/3 directives and runtime calls define or remove prefix, infix, and postfix
operators using the ISO fx, fy, xf, yf, xfx, xfy, and yfx
specifier classes. Variables cannot occur in functor or predicate position.
Parentheses around one term
denote that term; parentheses around two or more comma-separated terms
construct a right-associated ','/2 term. In goal position it is conjunction;
in data position it remains inspectable data.
The pure definite-clause fragment has a Herbrand reading: ground terms denote themselves, predicates denote sets of ground atomic formulas, variables have clause scope, and unification is structural. The implementation performs first-order finite-tree unification with an occurs check. An attempt to bind a variable to a term containing that same variable fails.
An atom constant such as pat is a term. An atomic formula such as
parent(pat, jan) is a proposition that may be a fact, rule head, or goal.
The surface form pair(pat, jan) may also be compound data when nested inside
another term; its role comes from context. Predicate identity includes arity,
so edge/2 and edge/3 are different predicates.
Execution is goal-directed rather than complete bottom-up saturation. Goals in
a body normally run from left to right; the solver may select a ready
deterministic built-in early as a pure filter. Ordinary user-defined calls use
depth-first resolution, while eligible positive recursive groups are tabled
automatically. \+/1 is stratified negation as failure, not classical
negation.
EyeProlog supports cut, operator declarations, dynamic database updates, grouped
solutions, exceptions, flags, initialization and inclusion directives, and
standard stream and term I/O. Normal mode additionally provides lifecycle-aware
call_cleanup/2 and setup_call_cleanup/3; these cleanup controls are
EyeProlog extensions and are excluded by --iso-strict. Normal mode also
adds the documented module compatibility surface and a Part 3-oriented DCG
profile.
A module gives predicate identity one more component: module name, predicate name, and arity. The first directive in a module source names the module and lists its public predicates:
% colors.pl
:- module(colors, [tone/1]).
tone(blue).
hidden(module_private).
Another source can import all exports or select particular indicators. An unqualified call first uses a predicate local to the calling module and then an import; a local definition therefore stays distinct from a same-named private predicate elsewhere.
:- use_module('colors.pl', [tone/1]).
hidden(user_local).
answer(Tone, Hidden) :- tone(Tone), hidden(Hidden).
qualified(ok) :- colors:tone(blue).
Imports such as use_module(library(lists)) and
use_module(library(strings)) resolve the bundled modules in Node and the
browser. Atom source designations such as
'colors.pl' resolve relative to the importing file in Node. use_module/1
imports every export; use_module/2 imports only its indicator list, including
an empty list when only qualified calls are wanted. Module:Goal selects a
module explicitly. Repeated module loads are idempotent, while conflicting
imports and requests for predicates that a module does not export are errors.
A grammar rule Head --> Body. is prepared as an ordinary predicate with two
additional difference-list arguments. Parameterized nonterminals retain their
written arguments, so token(Type)//1 is implemented by token/3.
Nonterminal indicators can be exported and imported through modules:
:- module(vocabulary, [word//1]).
word(noun) --> [robot] | [scientist].
The supported grammar constructs include terminal lists, [], sequencing with
comma, alternatives with ; or |, if-then-else, embedded goals {Goal},
call//1, phrase//1, and !//0. Semicontexts provide look-ahead by restoring
terminals to the remaining sequence:
look_ahead(X), [X] --> [X].
phrase(+Body,?Sequence) accepts or generates a complete sequence.
phrase(+Body,?Sequence,?Rest) leaves Rest unconsumed and is steadfast in
that argument. A variable body raises instantiation_error; a non-callable
body raises type_error(callable). EyeProlog performs terminal-sequence checks
and reports the portable ISO type_error(list) error term.
DCGs become more useful when the grammar produces a structured term rather than
merely accepting a token list. The checked
dcg-expression-language.pl
example implements a small arithmetic language in both directions. Its parser
respects precedence and left associativity while constructing an abstract syntax
tree:
expression(AST) -->
term(First),
additive_tail(First, AST).
additive_tail(Left, AST) -->
['+'], term(Right),
{ Next = add(Left, Right) },
additive_tail(Next, AST).
additive_tail(AST, AST) --> [].
The accumulator removes left recursion without moving parsing into JavaScript.
A second DCG walks the AST in the other direction and emits only the parentheses
needed to preserve its structure. The example therefore exercises parsing,
semantic actions, nonterminal-to-nonterminal state hand-off, generation,
backtracking, phrase/3 remainder handling, and AST-to-token-to-AST
round-tripping. The checked answers are in
examples/output/dcg-expression-language.pl.
library(iso_ext) provides the common ... //0 helper, which describes an
arbitrary number of input elements. It is not part of ISO Part 3, but it is a
useful interoperability and stress-test relation. A compact hand-off test is:
a --> ..., epsilon.
epsilon --> [].
Here the remaining sequence is repeatedly passed from ... //0 to another
nonterminal. For a finite compact list, EyeProlog can scan the arbitrary
sequence iteratively instead of consuming one ordinary solver depth level per
list cell. If the continuation is structurally proven to be a zero-width
identity grammar such as epsilon//0, the hand-off can be continued without
constructing a fresh general clause-resolution frame at every suffix. The list
spine is still traversed; this is a control/allocation optimization rather than
an O(1) semantic shortcut.
The optimization is deliberately narrow. phrase(..., Sequence, Rest) still
enumerates the valid remainders, open or non-compact inputs retain ordinary
relational behavior, and grammars that can consume or constrain the remainder
are not treated as identity continuations. time/1 can be used in normal mode
to measure such runs; its inference counter records solver-level inferences and
does not count every internal step of an optimized scanner.
Part 3 leaves \+//1 and standalone ->//2 implementation dependent.
EyeProlog uses non-consuming negation (\+ Body tests from the current state)
and threads the state produced by the condition into the then-grammar.
false/0 is the ISO always-failing built-in. It is protected as a static
procedure, so source clauses headed by false are rejected instead of being
interpreted as directives or integrity constraints.
Standard directives include dynamic/1, multifile/1, discontiguous/1,
op/3, char_conversion/2, initialization/1, include/1,
ensure_loaded/1, module/2, use_module/1, use_module/2,
meta_predicate/1, and set_prolog_flag/2. Initialization goals run once
after program preparation and before host queries. Included text is expanded
in place; repeated ensure_loaded/1 designations are loaded once.
Normal output contains only ground query answers, one term and period at a time. Source facts are not echoed as new conclusions, and duplicate answers are suppressed. Answers are not asserted back into the running program. Supported output syntax is designed to be readable as Prolog input accepted by EyeProlog.
The program loader detects predicate-dependency cycles, including dependencies
inside conjunction, \+/1, once/1, and aggregate goals.
Positive recursive components—including directly queried recursive
relations—are tabled to an answer fixed point before answers are replayed.
Components with a negative dependency retain guarded ordinary resolution,
because positive least-fixed-point tabling does not define unstratified
negation. Nonrecursive groups use indexed, depth-first resolution.
For calls with ground structural input, tabled answers can be reused within a solver run. The engine infers common structurally decreasing inputs from recursive heads. Fully open calls and calls whose inferred structural input is not ground may remain under ordinary resolution rather than forcing a possibly infinite relation into a table. This changes control, not declarative meaning.
The host-supplied goal must be callable and may contain constants or variables.
An unbound goal raises instantiation_error; a non-callable goal raises
type_error(callable). A program without a supplied goal prints no normal
answers. The host:
why/2 explanation.Goal selection affects host execution rather than the program’s logical meaning. One goal’s answers are not asserted for later goals, although internal tables may be reused during the solver run. For stable output, queries for known predicates are grouped by the source order in which their predicate groups first appear; goals within one group retain their supplied order. Queries for predicates with no group follow the known groups.
EyeProlog’s default registry contains the built-ins in its ISO compatibility
profile. Where a predicate is defined by ISO/IEC 13211-1:1995, EyeProlog uses its
standard predicate indicator; the registry also includes a few later or common
compatibility predicates identified below. Arithmetic is expressed through
is/2 rather than output arguments on arithmetic predicates. The registry
contains 129 name/arity entries across 100 names.
| Role | Registered predicate indicators |
|---|---|
| Control and exceptions | true/0, fail/0, false/0, !/0, call/1, call/2, call/3, call/4, call/5, call/6, call/7, call/8, \+/1, once/1, repeat/0, ;/2, ->/2, catch/3, throw/1, halt/0, halt/1 |
| Unification and identity | =/2, unify_with_occurs_check/2, \=/2, subsumes_term/2, ==/2, \==/2 |
| Type tests | var/1, nonvar/1, atom/1, integer/1, float/1, number/1, atomic/1, compound/1, callable/1, ground/1, acyclic_term/1 |
| Term order | compare/3, @</2, @=</2, @>/2, @>=/2, sort/2, keysort/2 |
| Term inspection | functor/3, arg/3, =../2, copy_term/2, term_variables/2 |
| Collection | findall/3, bagof/3, setof/3 |
| Grammar processing | phrase/2, phrase/3 |
| Database and information | clause/2, asserta/1, assertz/1, retract/1, retractall/1, abolish/1, current_predicate/1 |
| Operators, conversion, and flags | op/3, current_op/3, char_conversion/2, current_char_conversion/2, current_prolog_flag/2, set_prolog_flag/2 |
| Atomic terms | atom_length/2, atom_concat/3, sub_atom/5, atom_chars/2, atom_codes/2, char_code/2, number_chars/2, number_codes/2 |
| Stream control | open/3, open/4, close/1, close/2, current_input/1, current_output/1, set_input/1, set_output/1, flush_output/0, flush_output/1, stream_property/2, set_stream_position/2, at_end_of_stream/0, at_end_of_stream/1 |
| Character input | get_char/1, get_char/2, peek_char/1, peek_char/2, get_code/1, get_code/2, peek_code/1, peek_code/2 |
| Character output | put_char/1, put_char/2, put_code/1, put_code/2, nl/0, nl/1 |
| Byte input/output | get_byte/1, get_byte/2, peek_byte/1, peek_byte/2, put_byte/1, put_byte/2 |
| Term input | read/1, read/2, read_term/2, read_term/3 |
| Term output | write/1, write/2, writeq/1, writeq/2, write_canonical/1, write_canonical/2, write_term/2, write_term/3 |
| Arithmetic | is/2, =:=/2, =\=/2, </2, =</2, >/2, >=/2 |
The call patterns below use + for an argument that must be sufficiently
instantiated, - for a result normally produced by the call, and ? for an
argument that may be supplied or returned. These are principal operational
modes, not a separate mode system enforced by the parser. A call described as
semidet succeeds at most once; a nondet call may yield further answers on
backtracking. Unless stated otherwise, checking a result that does not unify
simply fails.
Built-in dispatch is authoritative and built-in procedures cannot be modified
through the dynamic database predicates. Source clauses with the same indicator
do not replace a built-in implementation. clause/2 and op/3 are the two
dispatch exceptions: when a program defines a source predicate with that same
indicator, EyeProlog uses the source clauses. false/0 is stricter still and is
rejected as a source-clause head. Portable programs should avoid every such
collision because other Prolog systems commonly reject it while loading.
| Predicate and principal call | Behavior |
|---|---|
true |
Succeeds once without binding variables. |
fail, false |
Always fail. false/0 is provided as a compatibility alias and is also forbidden as a source-clause head. |
! |
Commits to choices made since entry into the current predicate invocation. It does not erase alternatives belonging to an enclosing caller. |
call(+Goal) |
Calls an atom or compound goal. An unbound argument raises instantiation_error; another non-callable term raises type_error(callable). |
call(+Closure,?Arg,...) |
call/2 through call/8 append their extra arguments to an atom or compound closure, as specified by Corrigendum 2. |
\+(+Goal) |
Negation as finite failure. It succeeds once when Goal has no solution and never exports bindings made while testing Goal. Bind variables needed by the test first. |
once(+Goal) |
Returns only the first solution of Goal, or fails when there is none. |
repeat |
Produces an unbounded sequence of successes; normally paired with a test, cut, exception, or halt/0. |
Left ; Right |
Enumerates Left, then Right, restoring the incoming environment between branches. A cut inside a called predicate cannot discard the other branch. |
If -> Then |
Commits to the first solution of If and runs Then; it does not provide an else branch by itself. |
(If -> Then ; Else) |
Runs Then from the first solution of If, otherwise runs Else. Alternatives of If are discarded. |
catch(+Goal,?Catcher,+Recovery) |
Runs Goal; on a matching thrown ball or PrologError, unifies it with Catcher and calls Recovery. Runtime errors are exposed as error(Formal,eyeprolog). |
throw(+Ball) |
Throws a copied nonvariable term. An unbound ball raises instantiation_error. |
halt, halt(+Status) |
Stops the processor with status 0 or the supplied integer. The JavaScript API reports the status without terminating its host process. |
;/2 recognizes an ->/2 term on its left and implements the ISO
if-then-else commitment described above. Cuts and committed conditions are
operational controls; use ordinary relations when all alternatives should
remain observable.
| Predicate and principal call | Behavior |
|---|---|
phrase(+Body,?Sequence) |
Parses or generates Sequence with a Part 3 grammar body and requires complete consumption. |
phrase(+Body,?Sequence,?Rest) |
Parses or generates a prefix described by Body and unifies Rest with the unconsumed terminal sequence. The final unification is delayed so the third argument is steadfast. |
Grammar rules are expanded during program preparation and therefore appear to
the solver as ordinary predicates with two extra arguments. Dynamic grammar
bodies passed to phrase/2-3 use the same expansion rules. This includes
module qualification and the caller module used by embedded or meta-called
nonterminals.
| Predicate and principal call | Behavior |
|---|---|
?Left = ?Right |
Unifies two terms and returns the resulting bindings. EyeProlog rejects direct and indirect cyclic bindings. |
unify_with_occurs_check(?Left,?Right) |
Performs occurs-check-safe unification. Because ordinary EyeProlog unification is already cycle-safe, it has the same successful bindings as =/2. |
?Left \= ?Right |
Succeeds only when the terms cannot unify at call time. It is a test, not a delayed disequality constraint. |
subsumes_term(+General,+Specific) |
Tests one-sided syntactic unification without binding either argument. Variables in Specific remain unchanged. |
?Left == ?Right, ?Left \== ?Right |
Test term identity or non-identity without binding variables. Two distinct unbound variables are not identical. |
var(?Term), nonvar(?Term) |
Test whether the dereferenced term is or is not an unbound variable. |
atom(?Term), integer(?Term), float(?Term), number(?Term) |
Test the corresponding scalar category. Integer values retain arbitrary precision; finite noninteger numeric values are floats. |
atomic(?Term), compound(?Term), callable(?Term), ground(?Term), acyclic_term(?Term) |
Test for an ISO atomic term, a compound, a callable atom/compound, a term containing no unbound variables, or a finite acyclic term. A default double-quoted value is a list and is therefore compound unless it is empty. |
compare(?Order,+Left,+Right) |
Unifies Order with <, =, or > according to standard term order. A supplied order must be one of those atoms. |
Left @< Right, Left @=< Right, Left @> Right, Left @>= Right |
Compare terms without arithmetic evaluation or bindings. These calls are semidet. |
sort(+List,?Sorted) |
Sorts by standard term order and removes identical duplicates. |
keysort(+Pairs,?Sorted) |
Stably sorts Key-Value pairs by key without removing duplicates. |
For ISO terms, the standard term order is variables, numbers, atoms, then compounds;
compound terms compare by arity, functor, and arguments. Within the numeric
category, floats precede integers; floats compare by finite numeric value and
integers compare exactly. ISO leaves the ordering of two distinct variables
implementation dependent, subject to stability while a sorted list is being
created. EyeProlog assigns a creation ordinal to each logical variable and
carries that ordinal on the variable term itself; repeated occurrences share it
within parsing or clause-freshening scope. It therefore does not keep a
process-global table of every fresh variable name ever created, so long-running
generators can create and discard fresh variables without growing an unrelated
host Map. Double-quoted Prolog source
follows the double_quotes flag and never creates an extra host-only scalar
category.
| Predicate and principal call | Behavior |
|---|---|
functor(+Term,?Name,?Arity) |
Decomposes a term. Scalars have arity zero. |
functor(-Term,+Name,+Arity) |
Constructs a scalar when Arity is zero or a compound with fresh arguments otherwise. Arity must be a nonnegative representable integer and a positive-arity name must be an atom. |
arg(+Index,+Term,?Argument) |
Selects the one-based argument of a compound. Index zero or an index beyond the arity fails; a negative index is a domain error. |
?Term =.. ?List |
Converts a term to [Functor\|Arguments] or constructs a term from a nonempty proper list. A one-item list constructs its atomic item. |
copy_term(+Term,-Copy) |
Copies the dereferenced term while replacing every distinct unbound variable with a fresh variable and preserving variable sharing. |
term_variables(+Term,?Variables) |
Returns distinct variables in first-occurrence traversal order. A supplied result may be a proper or partial list. |
Construction calls raise instantiation_error when neither side supplies the
required shape. =../2 distinguishes an incomplete list
(instantiation_error) from an improper list (type_error(list)).
| Predicate and principal call | Behavior |
|---|---|
findall(+Template,+Goal,?Bag) |
Collects a fresh copy of Template for every solution of Goal, preserving solution order. It succeeds with [] when there are no solutions and treats all free variables existentially. |
bagof(+Template,+Goal,?Bag) |
Groups answers by free variables not present in Template. It yields one nonempty bag per witness group and fails when no group exists. Prefix variables with ^ in Goal to quantify them existentially. |
setof(+Template,+Goal,?Set) |
Has the grouping behavior of bagof/3, then sorts each group by profile term order and removes identical duplicates. |
Each collector runs its goal in an isolated inner search while sharing the current logical program and stream state. Collected terms are copied, so local variables do not escape accidentally. The bag argument must be a proper or partial list.
| Predicate and principal call | Behavior |
|---|---|
clause(+Head,?Body) |
Enumerates fresh copies of source clauses matching the callable Head; facts have body true. Access to built-ins raises permission_error(access,private_procedure). |
asserta(+Clause), assertz(+Clause) |
Insert a copied fact or rule at the beginning or end of a predicate declared dynamic/1. Static and built-in procedures cannot be modified. |
retract(+Clause) |
Removes matching dynamic clauses one at a time on backtracking. A call sees the logical update view captured when it began. A fact pattern matches facts only. |
retractall(+Head) |
Removes every matching clause from a dynamic procedure, succeeds when none match, and keeps the empty dynamic procedure known. |
abolish(+Name/+Arity) |
Removes a dynamic procedure and its clauses. The indicator must contain an atom and a nonnegative representable integer. |
current_predicate(?Name/?Arity) |
Enumerates predicate groups present in the loaded program, including empty dynamic groups. It does not enumerate registry-only built-ins. |
Declare mutable predicates explicitly, including empty ones:
:- dynamic(cache/2).
remember(Key, Value) :- retract(cache(Key, _)), !, assertz(cache(Key, Value)).
remember(Key, Value) :- assertz(cache(Key, Value)).
Assertions and retractions invalidate affected reasoning tables. Mutating a predicate that was not declared dynamic raises a permission error rather than silently changing a static program.
| Predicate and principal call | Behavior |
|---|---|
op(+Priority,+Specifier,+NameOrNames) |
Defines or removes operators in the current program. Priority is 0..1200; specifiers are fx, fy, xf, yf, xfx, xfy, or yfx; names may be one atom or a proper list. Priority zero removes the definition. , and \| cannot be modified. |
current_op(?Priority,?Specifier,?Name) |
Enumerates active operator definitions and filters supplied arguments. |
char_conversion(+Input,+Output) |
Installs a one-character conversion. In prepared Prolog text, later unquoted characters are converted when char_conversion=on; quoted characters are unchanged. The same mapping initializes execution-time term input. Mapping a character to itself removes its custom mapping. |
current_char_conversion(?Input,?Output) |
Enumerates installed nonidentity conversions. |
current_prolog_flag(?Flag,?Value) |
Enumerates flags or retrieves one named flag. An unknown bound flag raises domain_error(prolog_flag). |
set_prolog_flag(+Flag,+Value) |
Changes a supported mutable flag after validating its allowed atom value. Read-only flags raise a permission error. |
| Flag | Default in normal EyeProlog | Allowed values | Mutable |
|---|---|---|---|
bounded |
false |
false |
no |
integer_rounding_function |
toward_zero |
toward_zero |
no |
char_conversion |
on |
on, off |
yes |
debug |
off |
on, off |
yes |
max_integer |
no current value because bounded=false |
not applicable | no |
min_integer |
no current value because bounded=false |
not applicable | no |
max_arity |
unbounded |
unbounded |
no |
unknown |
error |
error, fail, warning |
yes |
double_quotes |
chars |
chars, codes, atom |
yes |
occurs_check |
true |
true, error |
yes |
Because bounded=false, current_prolog_flag(max_integer, _) and
current_prolog_flag(min_integer, _) fail as required by ISO 7.11.1.1;
EyeProlog does not expose an unbounded sentinel as either flag value.
Preparation-time char_conversion/2 mappings affect later unquoted source text
and also initialize the execution-time conversion mapping; setting the
char_conversion flag to off disables conversion for following source text.
Quoted source characters are not converted.
Both normal EyeProlog and strict ISO core mode use the ISO unknown=error
default. Interactive set_prolog_flag/2 changes are retained when the REPL
consults another file or imports a module instead of being reset by the host
rebuild of the program. Programs that intentionally treat an undefined
predicate as failure must opt in with set_prolog_flag(unknown, fail); bundled
examples and non-ISO corpus cases that depend on that policy do so explicitly.
The occurs_check flag is an EyeProlog diagnostic
extension rather than an ISO-defined core flag: it is absent in strict mode,
while normal mode keeps its true default and optional error diagnostic for
STO attempts. Operator and flag directives are processed per program rather
than changing global JavaScript state. The double_quotes setting affects
subsequent source text, included files, command-line and API goal text, and
terms read by read_term/*:
% Default: a list of one-character atoms.
chars("ab"). % chars([a,b])
:- set_prolog_flag(double_quotes, codes).
codes("ab"). % codes([97,98])
:- set_prolog_flag(double_quotes, atom).
quoted_atom("ab"). % quoted_atom(ab)
| Predicate and principal call | Behavior |
|---|---|
atom_length(+Atom,?Length) |
Counts Unicode code points, not UTF-16 code units. A supplied length must be a nonnegative integer. |
atom_concat(?Prefix,?Suffix,?Whole) |
Concatenates two atoms, removes a supplied prefix or suffix, or enumerates every split when only Whole is bound. At least Whole, or both parts, must determine the operation. |
sub_atom(+Atom,?Before,?Length,?After,?SubAtom) |
Enumerates substrings and their Unicode-code-point offsets. Supplied counts must be nonnegative integers. |
atom_chars(?Atom,?Chars), atom_codes(?Atom,?Codes) |
Convert between an atom and a proper list of one-character atoms or character codes. Both profiles use EyeProlog’s Unicode scalar PCS/codes; surrogates and values above U+10FFFF are rejected. At least one side must be instantiated. |
char_code(?Character,?Code) |
Converts one character atom and its collating/code value. Both profiles accept Unicode scalar codes and reject surrogates/out-of-range values. |
number_chars(?Number,?Chars), number_codes(?Number,?Codes) |
Convert finite numbers to canonical text or parse a proper character/code list using ISO number and negative-number syntax, including radix integers, character-code constants, and leading layout. The input is not parsed as a general term: grouping such as (0) is a syntax error. At least one side must be instantiated; malformed numeric input raises syntax_error(number). |
Conversions accept partial output lists when the atomic input is known, but
constructing an atom or number requires a complete proper list with no unbound
elements. Numeric parsing accepts ISO layout before tokens, including layout between a
minus token and the following numeric token. A single-line %... comment may
therefore follow - directly because % cannot continue a graphic token; an
adjacent bracketed comment in -/**/1 remains a syntax error under the eager
token-consumer rule. Decimal fractions and decimal exponents are supported;
the apostrophe character code is written with a doubled apostrophe as 0'''
and has value 39, while a literal space character code is 0' and has value
number_chars/2 comparison, including the Cor.2 error-precedence cases;
number_codes/2 shares the same numeric parser and has mirrored coverage for
the recent numeric-syntax regressions.Stream arguments accept an alias atom or the opaque handle returned by
open/3 or open/4. Omitting a stream argument selects the current standard
input or output.
| Predicate and principal call | Behavior |
|---|---|
open(+Source,+Mode,-Stream), open(+Source,+Mode,-Stream,+Options) |
Opens an atom path in read, write, or append mode. Options are type(text or binary), alias(Atom), reposition(true or false), and eof_action(error, eof_code, or reset). |
close(+Stream), close(+Stream,+Options) |
Closes a nonstandard stream. The only close option is force(true or false); standard streams remain available. |
current_input(?Stream), current_output(?Stream) |
Return or test the current input or output handle. |
set_input(+Stream), set_output(+Stream) |
Select an existing stream with the required direction. |
flush_output, flush_output(+Stream) |
Completes successfully for the current output or validates and flushes the selected output stream. EyeProlog writes synchronously. |
stream_property(?Stream,?Property) |
Enumerates streams and their properties: mode/1, type/1, reposition/1, eof_action/1, position/1, input, output, end_of_stream/1, and optional alias/1 and file_name/1. |
set_stream_position(+Stream,+Position) |
Repositions a stream opened with reposition(true). Position is a nonnegative integer or position(Integer) within the stream content. |
at_end_of_stream, at_end_of_stream(+Stream) |
Succeeds when the current or selected input position is at or beyond its content. |
get_char(?Character), get_char(+Stream,?Character) |
Reads one text character; end of input is end_of_file. |
peek_char(?Character), peek_char(+Stream,?Character) |
Observes the next text character without advancing. |
get_code(?Code), get_code(+Stream,?Code) |
Reads a character code; end of input is -1. Both profiles return codes from EyeProlog’s Unicode scalar PCS. |
peek_code(?Code), peek_code(+Stream,?Code) |
Observes the next Unicode scalar character code without advancing. |
get_byte(?Byte), get_byte(+Stream,?Byte) |
Reads one unit from a binary stream; end of input is -1. |
peek_byte(?Byte), peek_byte(+Stream,?Byte) |
Observes the next binary unit without advancing. |
put_char(+Character), put_char(+Stream,+Character) |
Writes one character atom to a text stream. |
put_code(+Code), put_code(+Stream,+Code) |
Writes one character code to a text stream. Both profiles accept Unicode scalar codes. |
put_byte(+Byte), put_byte(+Stream,+Byte) |
Writes an integer in 0..255 to a binary stream. |
nl, nl(+Stream) |
Writes a newline to a text stream. |
Text operations on binary streams and byte operations on text streams raise
permission errors. After EOF, eof_action(error) rejects another consuming
read, eof_code continues returning the EOF value, and reset resumes from
the beginning. A peek does not mark the stream as past-end.
| Predicate and principal call | Behavior |
|---|---|
read(?Term), read(+Stream,?Term) |
Reads one full-stop-terminated Prolog term from a text stream. Returns end_of_file when no term remains. |
read_term(?Term,+Options), read_term(+Stream,?Term,+Options) |
Reads a term and supports variables(List), variable_names(Pairs), and singletons(Pairs). Unknown options raise domain_error(read_option). |
write(+Term), write(+Stream,+Term) |
Writes readable operator notation without quoting atoms merely because quoting would be required for reparsing. Number variables are enabled. |
writeq(+Term), writeq(+Stream,+Term) |
Like write, but quotes atoms when required for unambiguous input syntax. |
write_canonical(+Term), write_canonical(+Stream,+Term) |
Writes quoted canonical functor notation while ignoring operators and without interpreting $VAR/1. |
write_term(+Term,+Options), write_term(+Stream,+Term,+Options) |
Writes with quoted(true or false), ignore_ops(true or false), numbervars(true or false), and variable_names([Name=Variable,…]). Normal mode also supports double_quotes(true or false) and spacing(true or false). |
Term input uses the program’s current operator table and the same ISO quoted-character
syntax as source text, including backslash-terminated octal and hexadecimal
escapes such as '\7\' and '\x7\'. This applies equally to read/1-2 and
read_term/2-3. Installed character conversions are applied outside quoted text
when the char_conversion flag is on. variable_names/1 and singletons/1 omit anonymous variables. Output
predicates do not append a period or newline; call write/1, then write('.')
and nl/0 when emitting a complete source term manually.
A standalone numeric term read by read/1-2 or read_term/2-3 uses the same
bounded numeric scanner and canonical value conversion as number_chars/2.
Consequently every numeric character sequence accepted by number_chars/2 has
the same value when read as a full-stop-terminated term, without requiring EOF
after that term.
| Predicate and principal call | Behavior |
|---|---|
is(?Result,+Expression) |
Evaluates Expression once and unifies its numeric value with Result. It is evaluation plus unification, not mutable assignment. |
Left =:= Right, Left =\= Right |
Evaluate both sides and test numeric equality or inequality. Integer/float representation differences do not by themselves make values unequal. |
Left < Right, Left =< Right, Left > Right, Left >= Right |
Evaluate both sides and perform the indicated numeric comparison. |
Every variable in an arithmetic expression must already be bound to a number or evaluable expression. Integers use arbitrary-precision arithmetic while an operation remains in the integer domain; operations requiring floating point convert their operands to finite JavaScript numbers.
| Expression family | Supported forms and result rules |
|---|---|
| Literals and constants | Integer and finite floating-point literals; pi and e produce floating-point constants. |
| Unary arithmetic | Unary +, unary -, and integer bitwise complement \. |
| Basic binary arithmetic | +, -, and * preserve integers when both operands are integers. / produces a float and rejects a zero divisor. |
| Exponentiation | Base ^ Exponent remains an integer for nonnegative integer operands. Corrigendum 3 requires a float base for most negative integer exponents; ** is floating-point exponentiation. |
| Integer division | // and div require integers and reject zero divisors. // rounds toward zero as reported by integer_rounding_function; div rounds down. |
| Integer remainder | rem is the truncating remainder; mod normalizes the result with the divisor’s sign. Both require integers and a nonzero divisor. |
| Bit operations | Integer /\, \/, xor, <<, and >>, plus unary \. |
| Numeric normalization | abs, sign, and float. Integer abs and sign preserve integer results; float produces a float. |
| Rounding | truncate, round, ceiling, and floor produce integers. |
| Float decomposition | float_integer_part and float_fractional_part require a float and return floats. |
| Min/max and transcendental functions | min, max, sin, cos, atan, asin, acos, atan2, tan, exp, log, and sqrt; pi is the Corrigendum 2 constant. |
Arithmetic comparisons evaluate both operands. Standard term-order predicates
(@<, @=<, @>, @>=) compare terms without arithmetic evaluation.
EyeProlog’s documented profile order distinguishes floats from integers rather
than applying arithmetic equality across representations. Double-quoted text
does not introduce another ISO term-order category: it becomes the list or atom
selected by double_quotes before comparison.
ISO built-ins distinguish logical failure from exceptional calls. Insufficient
instantiation raises instantiation_error; wrong argument categories raise
type_error; invalid values raise domain_error; and arithmetic faults raise
evaluation_error. JavaScript embedders receive these as PrologError
instances whose message contains the corresponding Prolog error term.
| Error class | Typical cause |
|---|---|
| instantiation_error | A required callable, stream, number, list, option value, or construction input is still unbound. |
| type_error(Expected,Culprit) | A bound value has the wrong term category, such as a noninteger index or non-callable goal. |
| domain_error(Domain,Culprit) | The type is correct but the value is outside the supported domain, such as a bad stream option or operator priority. |
| representation_error(Flag) | A value cannot be represented by the profile, such as an invalid Unicode scalar code. |
| evaluation_error(zero_divisor) | Integer or floating-point division was attempted with a zero divisor. |
| evaluation_error(undefined) | Floating-point evaluation produced a non-finite or undefined result. |
| permission_error(Operation,Permission,Culprit) | A static procedure was modified, a stream was used in the wrong mode, or a protected resource was accessed. |
| existence_error(Object,Culprit) | A stream, source sink, or required procedure does not exist. |
| syntax_error(number), syntax_error(read_term) | Lexical number conversion or streamed term parsing failed. |
catch/3 converts a PrologError into a catchable
error(Formal,eyeprolog) term. throw/1 copies its ball before unwinding: bound
parts are preserved, repeated variables remain shared within the copied ball,
and unbound variables are fresh with respect to the protected goal and catcher.
Catchable error terms follow the same variable-freshening rule. The interactive
top level uses the same ISO error envelope for uncaught processor errors, so an
ordinary runtime error is displayed as error(Formal,eyeprolog) rather than
dropping the implementation-defined second argument. Variables that occur in an
uncaught ISO error are rendered with fresh answer names such as _A rather than
reusing query-variable spellings such as X or Xx; this keeps the displayed
error consistent with the copied exception term. An unmatched ball or error
continues outward.
Streams belong to one solver run and are shared by nested calls, exceptions,
and solution collectors. user_input and user_output are always present.
open/4 supports type/1, alias/1, reposition/1, and eof_action/1;
read_term/3 supports variables/1, variable_names/1, and singletons/1.
The JavaScript ioOptions.input and ioOptions.write hooks connect standard
streams to an embedder. File-backed streams use synchronous lifecycle semantics
so side effects occur in Prolog execution order.
call_cleanup/2 and setup_call_cleanup/3 are normal EyeProlog runtime extensions rather than members of the isolated ISO builtin registry. They protect a goal across deterministic completion, exhaustion, cut, top-level abandonment, and exception unwinding, running Cleanup exactly once. setup_call_cleanup/3 runs Setup once and installs Cleanup only after Setup succeeds. A successful Cleanup run before a deterministic answer contributes its substitutions to that answer. Nested cleanups run inside-out, and strict ISO mode does not provide either predicate.
EyeProlog exposes 299 distinct non-ISO library and normal-extension predicate
indicators in addition to the 129 indicators in its isolated ISO profile.
241 are defined entirely as ordinary Prolog clauses in focused modules under
src/lib/; 58 use host support for control, attributed variables,
constraints, or observability. The ISO and library catalogs therefore cover
428 distinct predicate indicators. Normal-mode controls and observability
relations that do not belong to a library, such as call_cleanup/2,
setup_call_cleanup/3, statistics/0,2, and tnot/1, are documented in their
own sections rather than counted as library predicates. time/1 is additionally
registered directly in the normal runtime so Trealla-style timing works at the
interactive top level without an import; it is absent from the strict ISO
registry.
The sources are src/lib/aggregate.pl, src/lib/arithmetic.pl,
src/lib/assoc.pl, src/lib/atts.pl, src/lib/between.pl,
src/lib/charsio.pl, src/lib/clpb.pl, src/lib/clpz.pl,
src/lib/comparison.pl, src/lib/dates.pl, src/lib/dcgs.pl,
src/lib/debug.pl, src/lib/dif.pl, src/lib/error.pl,
src/lib/format.pl, src/lib/freeze.pl, src/lib/gensym.pl,
src/lib/iso_ext.pl, src/lib/lambda.pl, src/lib/lists.pl,
src/lib/ordsets.pl, src/lib/pairs.pl, src/lib/pio.pl,
src/lib/primes.pl, src/lib/prologue.pl, src/lib/random.pl,
src/lib/reif.pl, src/lib/si.pl, src/lib/strings.pl,
src/lib/tabling.pl, src/lib/terms.pl, src/lib/time.pl,
src/lib/ugraphs.pl, src/lib/uuid.pl, and src/lib/when.pl. Each declares a same-named module
with module/2; there is no catch-all library(eyeprolog). A program imports
only the modules it needs, and
use_module/2 can select an even smaller indicator list. The Prologue
module exposes p.p.1 through p.p.11 of the
working-draft Prologue,
as a compatibility facade over the canonical lists, between, iso_ext,
and freeze modules. The published Prologue, call_nth/2, and length/2
quads are retained as offline regressions. src/standard-library.js registers the module sources and private control and
constraint adapters for Node and browser resolution. Explicit use_module/1-2
loads remain supported; outside strict ISO mode, the conservative interop
autoloader may also load a module for one uniquely mapped common predicate.
The core registry remains available through createDefaultRegistry() and
getDefaultRegistry() for low-level embedding. The stricter Part 1 +
Corrigenda registry is exposed as createStrictIsoRegistry() and
getStrictIsoRegistry() and is paired with isoStrict: true when a complete
strict-language boundary is required. Module-local predicate identity keeps
private helpers and same-named predicates in different modules separate.
freeze(?Term,:Goal) runs Goal immediately when Term is already nonvariable;
otherwise it delays the goal until Term becomes nonvariable. Suspensions are
kept in the logical environment, so bindings and backtracking remain isolated
between solution branches. When a suspension wakes, its goal is meta-invoked
with its own cut scope: a ! inside the delayed goal may commit choices made by
that invocation, but it cannot prune alternatives that were created before the
freeze/2 call. Multiple suspensions on the same variable are stored internally
as a binary join tree, making each merge constant-time instead of repeatedly
appending an ever-growing list. Wakeup and residual projection traverse that tree
left-to-right and emit the original suspensions separately. This keeps a call/1-style cut boundary for every delayed goal without
collapsing the user-visible residuals into one conjunction. Thus
call(((Y=1;Y=2),freeze(X,!),X=c));Y=3 retains the three answers Y=1, Y=2,
and Y=3.
dif(?Left,?Right) posts a delayed finite-tree disequality when its arguments
can still unify. Its residual store is normalized by logical implication:
symmetric or equivalent constraints share one residual, and a stronger
constraint removes weaker ones regardless of insertion order. Independent
disequalities remain separate. Residual projection is re-evaluated in each
solution environment, so a compound disequality is kept intact until later
bindings make one aligned subterm pair sufficient. For example:
?- dif(f(X,A),f(Y,B)), ( true ; A = B ).
dif(f(X, A), f(Y, B))
; A = B, dif(X, Y).
| Module | Exported predicate indicators | Primary role |
|---|---|---|
library(aggregate) |
sumall/3, aggregate_min/5, aggregate_max/5 |
Aggregation |
library(arithmetic) |
lsb/2, msb/2, popcount/2 |
CLP(Z) arithmetic support |
library(assoc) |
empty_assoc/1, assoc_to_keys/2, assoc_to_list/2, assoc_to_values/2, del_assoc/4, del_max_assoc/4, del_min_assoc/4, gen_assoc/3, get_assoc/3, get_assoc/5, is_assoc/1, list_to_assoc/2, map_assoc/2, map_assoc/3, max_assoc/3, min_assoc/3, ord_list_to_assoc/2, put_assoc/4 |
AVL association trees; reused from the shared portable source |
library(atts) |
put_atts/2, get_atts/2, put_attr/3, get_attr/3, del_attr/2, term_attributed_variables/2, call_residue_vars/2 |
Attributed variables |
library(between) |
between/3, gen_int/1, gen_nat/1, numlist/2, numlist/3, repeat/1 |
Integer generation |
library(charsio) |
char_type/2, get_line_to_chars/3, get_n_chars/3, get_single_char/1 |
Character classification and stream reads |
library(clpb) |
labeling/1, random_labeling/2, sat/1, sat_count/2, taut/2, weighted_maximum/3 |
Boolean constraints and BDD reasoning; upstream Prolog source with small state adapters |
library(clpz) |
#>/2, #</2, #>=/2, #=</2, #=/2, #\=/2, #\/1, #<==>/2, #==>/2, #<==/2, #\//2, #\/2, #/\/2, in/2, ins/2, all_different/1, all_distinct/1, nvalue/2, sum/3, scalar_product/4, tuples_in/2, labeling/2, label/1, indomain/1, lex_chain/1, serialized/2, global_cardinality/2, global_cardinality/3, circuit/1, cumulative/1, cumulative/2, disjoint2/1, element/3, automaton/3, automaton/8, zcompare/3, chain/2, fd_var/1, fd_inf/2, fd_sup/2, fd_size/2, fd_dom/2, clpz_t/2, #=/3, #</3 |
Constraint logic programming over integers; the final three indicators support reified compatibility libraries |
library(comparison) |
lt/2, gt/2, le/2, ge/2 |
Generic comparison |
library(dates) |
difference/3 |
ISO duration differences |
library(dcgs) |
phrase/2, phrase/3, seq/3, seqq/3 |
DCG compatibility; seq/3 and seqq/3 are declared as seq //1 and seqq //1 |
library(debug) |
*/1, $/1, $-/1, debug/1, debug/3, nodebug/1, bb_get/2, bb_put/2, bb_b_put/2, bb_global_get/2 |
Declarative debug operators and constraint-library blackboards |
library(dif) |
dif/2 |
Common module facade over native delayed disequality |
library(error) |
must_be/2, can_be/2, instantiation_error/0, instantiation_error/1, domain_error/2, domain_error/3, type_error/2, type_error/3, representation_error/1, resource_error/1, call_with_error_context/2 |
Error checking and construction |
library(format) |
format_/4, format/2, format/3, listing/1, portray_clause_/3, portray_clause/1, portray_clause/2 |
Formatted DCG text and output; format_/4 and portray_clause_/3 are the expanded nonterminals |
library(freeze) |
freeze/2 |
Delayed goals |
library(gensym) |
gensym/2, reset_gensym/1 |
Process-local generated atoms |
library(iso_ext) |
call_nth/2, countall/2, forall/2, succ/2, cfor/3, findall/4, variant/2, time/1, .../2 |
Common ISO extensions |
library(lambda) |
^/3, ^/4, ^/5, ^/6, ^/7, ^/8, ^/9, ^/10, \/1, \/2, \/3, \/4, \/5, \/6, \/7, \/8, +\/2, +\/3, +\/4, +\/5, +\/6, +\/7, +\/8, +\/9 |
Higher-order lambda notation |
library(lists) |
member/2, memberchk/2, select/3, append/2, append/3, last/2, same_length/2, nth0/3, nth0/4, nth1/3, nth1/4, reverse/2, length/2, maplist/2, maplist/3, maplist/4, maplist/5, maplist/6, maplist/7, maplist/8, foldl/4, foldl/5, foldl/6, sum_list/2, min_list/2, max_list/2, list_to_set/2, list_max/2, list_min/2, permutation/2, transpose/2, set_nth0/4, take/3, drop/3, slice/4 |
List relations and shared matrix/permutation helpers |
library(ordsets) |
is_ordset/1, list_to_ord_set/2, ord_add_element/3, ord_del_element/3, ord_disjoint/2, ord_empty/1, ord_intersect/2, ord_intersect/3, ord_intersection/2, ord_intersection/3, ord_intersection/4, ord_memberchk/2, ord_selectchk/3, ord_seteq/2, ord_subset/2, ord_subtract/3, ord_symdiff/3, ord_union/2, ord_union/3, ord_union/4 |
Ordered-set relations; reused upstream source |
library(pairs) |
pairs_keys_values/3, pairs_keys/2, pairs_values/2, group_pairs_by_key/2, map_list_to_pairs/3 |
Key-value pair support |
library(pio) |
phrase_from_file/2, phrase_from_file/3, phrase_to_file/2, phrase_to_file/3, phrase_to_stream/2 |
Eager portable DCG file and stream I/O |
library(primes) |
smallest_divisor_from/3 |
Prime factor support |
library(prologue) |
member/2, append/3, length/2, between/3, select/3, succ/2, maplist/2, maplist/3, maplist/4, maplist/5, maplist/6, maplist/7, maplist/8, nth0/3, nth0/4, nth1/3, nth1/4, call_nth/2, freeze/2, foldl/4, foldl/5, foldl/6, countall/2 |
Legacy facade over canonical focused modules |
library(random) |
maybe/0, random/1, random/3, random_integer/3, set_random/1 |
Common mutable-seed interface plus EyeProlog’s pure state-threaded generator |
library(reif) |
,/3, ;/3, =/3, cond_t/3, dif/3, if_/3, memberd_t/3, tfilter/3, tmember/2, tmember_t/3, tpartition/4 |
Reified conditions and list filtering; reused upstream source |
library(si) |
atom_si/1, integer_si/1, atomic_si/1, list_si/1, character_si/1, term_si/1, chars_si/1, dif_si/2, not_si/1, when_si/2 |
Sufficient-instantiation checks used by CLP(Z) |
library(strings) |
matches/3, split/3, replace/4, lowercase/2, uppercase/2, trim/2, number_string/2, atom_string/2, term_string/2, string_concat/3, contains/2, matches/2, join/3, substring/4 |
Text relations |
library(tabling) |
abolish_all_tables/0, start_tabling/2 |
Common explicit syntax over automatic recursive tabling |
library(terms) |
numbervars/3, copy_term_nat/2 |
Term operations used by bundled libraries |
library(time) |
current_time/1, format_time/4 |
Clock timestamps and the expanded format_time//2 nonterminal |
library(ugraphs) |
add_edges/3, add_vertices/3, complement/2, compose/3, connect_ugraph/3, del_edges/3, del_vertices/3, edges/2, neighbors/3, neighbours/3, reachable/3, top_sort/2, top_sort/3, transitive_closure/2, transpose_ugraph/2, ugraph_union/3, vertices/2, vertices_edges_to_ugraph/3 |
Directed graph relations; reused upstream source |
library(uuid) |
uuid/3, uuid_string/2, uuidv4/1, uuidv4_string/1 |
Common UUID byte/string conversion and generation plus pure state threading |
library(when) |
when/2 |
Shared delayed-condition interface over attributed variables |
EyeProlog keeps four related concepts separate:
| Layer | Meaning |
|---|---|
| ISO core | The documented ISO predicate profile built into the processor. No EyeProlog library import is involved. |
| EyeProlog library surface | Every module and exported predicate listed in the catalog above. Programs normally access these with use_module/1-2. |
| Interoperability profile | A deliberately smaller set of library names and predicate interfaces that EyeProlog intends to keep source-compatible with Trealla and Scryer where practical. |
| Autoload map | An even smaller convenience table that gives selected unqualified predicates one canonical EyeProlog provider. |
These layers answer different questions. A predicate may be implemented entirely as ordinary Prolog and still be outside the cross-processor interoperability profile; conversely, an interoperable predicate may be backed by a private host adapter. In this section, portable refers to source portability between Prolog systems, not merely to the language in which a predicate happens to be implemented.
The shared module-name profile is the overlap of the Scryer src/lib and
Trealla library layouts that EyeProlog can implement without pretending an
engine-specific service exists. It includes arithmetic, assoc, atts,
charsio, clpb, clpz, debug, dif, format, freeze, gensym,
iso_ext, lambda, lists, ordsets, pio, random, reif, tabling,
time, ugraphs, uuid, and when. The predicate profile is narrower than
the union of their exports: for example, EyeProlog’s explicit-state random/3
and uuid/3 remain useful extensions rather than shared interfaces.
Source reuse is preferred over translation. clpb.pl, ordsets.pl,
reif.pl, and ugraphs.pl retain the upstream Prolog algorithms and license
headers; assoc.pl uses the complete upstream AVL implementation. gensym.pl
and when.pl keep the same algorithms with small blackboard and parser-safe
closure adaptations. dif.pl, tabling.pl, and the clock portion of
time.pl are thin facades over EyeProlog runtime facilities. charsio.pl and
pio.pl provide the common character and eager DCG I/O surfaces. The
portable library overlap example
composes Boolean constraints, ordered sets, graphs, reification, delayed goals,
generated names, character conversion, transposition, and explicit table
syntax in one checked program.
Two matching upstream basenames are intentionally not exposed as portable
libraries. The two builtins.pl files are private engine facades and have no
shared public predicate interface. sockets.pl requires a native networking
event loop and host socket objects; EyeProlog does not provide a source-only
facade that would accept calls and then fail at runtime. Rational-only
library(arithmetic) predicates are likewise omitted until rational numbers
are processor values.
For library(lists), the current interop predicate set is member/2,
memberchk/2, select/3, append/2-3, last/2, same_length/2,
nth0/3-4, nth1/3-4, reverse/2, length/2, maplist/2-8,
foldl/4-6, sum_list/2, list_to_set/2, list_max/2, list_min/2,
permutation/2, and transpose/2. Other exports from the same
module, such as min_list/2, max_list/2, set_nth0/4, take/3, drop/3,
and slice/4, remain available to EyeProlog programs but lie outside this
conservative cross-engine subset.
length/2 remains fully relational. With both arguments variable,
length(Xs, N) enumerates Xs = [], N = 0, then one-element lists with
N = 1, and so on. Open-ended generation uses the normal memory guard with
recovery headroom so finite-heap exhaustion remains a catchable
resource_error(memory). A supplied nonnegative length selects at most one
answer, and a supplied closed list has exactly one length; these modes do not
retain an exhausted choicepoint.
library(iso_ext) is a common interop module name, but only part of its
EyeProlog API belongs to the shared profile. call_nth/2, time/1, and
.../2 are mapped there. time/1 measures each solution of a meta-call and
prints elapsed time, EyeProlog inference count, and MLips in Trealla-style form,
for example % Time elapsed 0.832s, 65551 Inferences, 0.079 MLips; ... //0
describes an arbitrary number of input elements. Together they let the
Trealla/Scryer DCG hand-off benchmark run in EyeProlog without source changes
(assuming the usual list library is already imported in an interactive
session). The focused modules have one canonical implementation owner per
predicate. library(prologue) re-exports those same owners, so legacy code can
combine the facade with library(lists), library(iso_ext), and
library(freeze) in either import order without an accidental collision.
library(lambda) follows Scryer’s higher-order notation, adapted from Ulrich
Neumerkel’s permissively licensed implementation. Its public syntax is:
\X1^X2^...^XN^Goal
Free+\X1^X2^...^XN^Goal
The first form has no explicitly shared free variables. Before each invocation,
EyeProlog copies the closure term so local variables are fresh on successive
maplist/2-8, foldl/4-6, or direct call/N uses. In the second form, the
variables contained in Free remain shared with the surrounding goal.
Importing the library installs +\ as a priority-201 xfx operator; \ and
^ use their existing ISO operator definitions. Parenthesize lower-priority
goal operators after ^, for example \X^(X > 3).
A continuation lambda may leave arguments for a later call:
f(x, y).
answer(A, B) :- call(\X^f(X), A, B).
This is equivalent to supplying both arguments directly. A lambda called with
too few parameters raises existence_error(lambda_parameter, ...). EyeProlog
uses its ISO copy_term/2 implementation for the fresh-copy step and does not
require a separate copy_term_nat/2 predicate.
Autoloading is a convenience layered on top of the interoperability profile; it
is not a general search through all EyeProlog libraries. When a source program
or an explicit CLI/API goal is built, an otherwise undefined unqualified
predicate may be autoloaded only when the interop table assigns it one canonical
provider. The interactive top level keeps ordinary library imports explicit;
time/1 is available there because it is also a normal EyeProlog runtime
extension. For example, the canonical build-time providers are:
| Predicate | Canonical autoload provider |
|---|---|
member/2 |
library(lists) |
call_nth/2 |
library(iso_ext) |
time/1 |
library(iso_ext) |
.../2 |
library(iso_ext) |
between/3 |
library(between) |
Predicates outside that table require an explicit import even when EyeProlog provides them. Explicit imports therefore remain the clearest way to state library dependencies:
:- use_module(library(lists)).
:- use_module(library(iso_ext), [call_nth/2]).
Use --no-autoload, or the JavaScript option autoload: false, when every
library dependency should be explicit. --iso-strict always disables EyeProlog
library autoloading.
-w / --warnings reports explicit dependencies on non-profile libraries and
calls to non-profile predicates from otherwise common modules. --portable
turns those diagnostics into a failing run, making the conservative profile
suitable for continuous integration. npm run test:interop executes the same
portable Towers of Hanoi source under EyeProlog, Trealla, and Scryer when those commands
are installed. Because those are external runtimes, the default npm test
instead validates all four generated OpenRuleBench source trees and their
engine-specific tabling and WFS adaptations without requiring them. The fast
structural check is also available directly as npm run test:openrulebench.
The catalog above is authoritative for the complete EyeProlog library surface. The following notes describe useful parts of that surface without extending the cross-engine claims made above.
library(atts) is the Prolog-facing attributed-variable layer over the persistent
annotated-variable machinery in src/term.js, with its small host bridge in
src/atts.js. It provides the attribute operations used by Scryer libraries,
accepts :- attribute ... declarations, invokes module-local
verify_attributes/3 before an attributed binding is committed, and schedules
the returned goals immediately after the binding. Attribute maps are copied only
when changed and therefore backtrack with Env branches; the interactive top
level projects module attribute_goals//1 hooks as residual goals. The checked
examples/attributed-variables.pl
demonstrates binding verification and attribute transfer across aliases.
library(clpz) is Markus Triska’s MIT-licensed Scryer Prolog implementation of
constraint logic programming over integers, bundled as src/lib/clpz.pl.
EyeProlog executes the Prolog propagator implementation through the generic
attributed-variable machinery in src/term.js and src/atts.js. The library
provides relational arithmetic and reification, finite and union domains,
labeling, all-different/all-distinct constraints, sums and scalar products,
extensional tuple tables, lexicographic chains, serialized and cumulative
scheduling, global cardinality with costs, Hamiltonian circuits, disjoint2/1,
automaton/3,8, value counting, comparison, and domain reflection.
The compiler services used by the library are ordinary EyeProlog facilities:
module-local and user term_expansion/2 and goal_expansion/2, clause-list
expansion, and expand_term/2 for processor DCG lowering. The same runtime also
provides a generic copy-on-write backtrackable blackboard and the supporting
Prolog modules assoc, pairs, between, dcgs, terms, error, si,
freeze, arithmetic, debug, and format. The bundled CLP(Z) source is
synchronized with Scryer commit e3df91e25f8a09ee942c04e8baef553bba5c6110,
Git blob 806445c11e14c8b2515f3de7f309e0ac04d9ad04.
Alongside its interop entries call_nth/2, time/1, and .../2,
library(iso_ext) also exports EyeProlog’s extension relations countall/2,
forall/2, succ/2, cfor/3, findall/4, and variant/2. forall/2 checks an action for every solution of a
condition; cfor/3 enumerates an inclusive evaluated integer range; succ/2
relates adjacent nonnegative integers; findall/4 collects into a difference
list; and variant/2 recognizes terms equal up to variable renaming.
countall/2 counts solutions without exposing a template. These exports do not
all belong to the conservative interop subset merely because they share the
iso_ext module.
library(format) accepts the common format/[2,3], format_//2,
portray_clause/[1,2], portray_clause_//1, and listing/1 interfaces. Its
portable formatter currently implements literal text, ~~, ~n, ~w, ~q,
~a, and ~d; controls for field widths and floating-point presentation are
not yet part of EyeProlog’s compatibility subset. library(pio) eagerly reads
or materializes a DCG character list around ISO streams. It preserves the
declarative grammar interface, but unlike Scryer’s lazy list implementation it
does not defer file reads.
library(tabling) accepts :- table Name/Arity. and the common
start_tabling/2 and abolish_all_tables/0 predicates. Recursive user
predicates are already tabled by EyeProlog’s program analysis, so the directive
is a compatible declaration rather than a second tabling engine.
library(time) returns a timestamp association list from the local clock;
format_time//2 supports the documented year, month, day, time, month-name,
weekday-name, and day-of-year specifiers.
random/1 keeps the common mutable-seed interface but uses a private native
state step for hot-loop performance; its public sequence remains the same
Park-Miller sequence used by the portable explicit-state random/3. The helper
is internal to the normal EyeProlog registry and is not part of the ISO or
public library predicate catalogs.
uuidv4/1, uuidv4_string/1, and uuid_string/2 provide the common UUID byte
list and character-list interface. uuid(+Seed0,-UUID,-Seed) remains an
EyeProlog extension that creates a version 4 UUID atom using pure random/3.
Passing the returned seed to the next call produces the next UUID; restarting
with the same integer seed reproduces the same sequence. set_random/1 controls
the common stateful generator used by uuidv4/1.
On the command line, a program can state its library dependencies explicitly:
printf '%s\n' ':- use_module(library(lists)).' 'answer(X) :- member(X, [ready]).' > program.pl
eyeprolog --goal 'answer(X)' program.pl
eyeprolog -p program.pl # add proof output
JavaScript uses the same normal EyeProlog library registry by default:
import { run } from 'eyeprolog';
const source = `
:- use_module(library(lists)).
answer(Whole) :- append([red, green], [blue], Whole).
`;
const result = run(source, { goal: 'answer(X)' });
console.log(result.stdout);
The mode notation used in the reference tables below is descriptive:
+ means the argument must already have the required input shape;- means the predicate produces that argument;? means a bound value can be checked or an unbound value generated.Most EyeProlog library predicates are projections or filters. When an input is unbound, malformed, outside its domain, or incompatible with the requested output, they normally fail rather than raising the ISO errors described in the errors section above. They do not invent open-ended domains. Bind arithmetic operands, source text, proper lists, indexes, dates, and aggregate generators before calling the corresponding predicate.
| Predicates and principal modes | Behavior |
|---|---|
lt(+A,+B), le(+A,+B), gt(+A,+B), ge(+A,+B) |
Compare integers exactly, finite numeric text numerically, PnYnMnD duration text component-wise, and other lexical values by string order. These differ from ISO arithmetic comparison and standard term order. |
random(-Value) |
Stateful Park-Miller step using the library seed set by set_random/1; implemented through a private native fast path while retaining the same sequence and numeric conversion as random/3. |
random(+Seed0,-Value,-Seed) |
Portable Park-Miller generator with explicit state. Value is in [0,1); pass the returned integer Seed to the next call. The same initial integer seed always reproduces the same sequence. |
difference(+End,+Start,-Duration) |
Portable Prolog. Computes a nonnegative calendar difference between ISO date atoms/character lists and returns atom 'PnYnMnD'. Invalid dates or an end before the start fail. |
:- use_module(library(dates)).
:- use_module(library(between), [between/3]).
:- use_module(library(random)).
:- use_module(library(lists)).
answer(square, S) :- (S is 12 * 12).
answer(day_count, N) :- between(3, 5, N).
answer(age, D) :- difference('2026-07-28', '2020-05-20', D).
answer(random_pair, [A,B]) :- random(42, A, S), random(S, B, _).
eyeprolog --goal 'answer(Kind, Value)' program.pl
The library deliberately does not register named arithmetic wrappers such as
add/3, mul/3, abs/2, or sqrt/2, because ISO arithmetic already
expresses them: for example, R is A + B, R is abs(A), and
R is sqrt(A). The same applies to subtraction, multiplication, division,
modulo, powers, sine, cosine, exponential, logarithm, and the ISO rounding
functions.
The bundled library layer defines between/3 in library(between) and
smallest_divisor_from/3 in library(primes) as ordinary Prolog clauses.
Choose the smaller or
larger of two arithmetic values directly with ISO control, for example
(A =< B -> Min = A ; Min = B) or (A >= B -> Max = A ; Max = B).
These relations are the actual Prolog implementations in
src/lib/lists.pl. Every list-consuming relation below expects a
proper list unless explicitly stated otherwise. Indexes and counts are
zero-based, nonnegative safe integers.
| Predicate and principal mode | Behavior |
|---|---|
append(+Prefix,+Suffix,-Whole) |
Appends a proper prefix to any suffix, including an improper tail. |
append(-Prefix,-Suffix,+Whole) |
Enumerates every split of a proper Whole, from empty prefix to empty suffix. |
member(?Item,+List) |
Produces one answer per matching position, so duplicates remain observable. |
select(?Item,+List,-Rest) |
Removes one occurrence at a time and preserves the order of all other elements. Duplicate occurrences may produce duplicate answers. |
\+ member(+Item,+List) |
Succeeds only when Item does not unify with any member. Use it after binding the item and list. |
nth0(?Index,+List,?Item) |
Checks a bound zero-based index or enumerates indexes and their items. |
nth1(?Index,+List,?Item) |
Checks a bound one-based index or enumerates one-based indexes and items. |
maplist(+Closure,+List1,?List2) |
Applies a two-argument closure pairwise through ISO call/3; partially applied compound closures are supported. |
[Head|Tail] = List |
Decomposes a nonempty list directly with ISO unification; no library wrapper is needed. |
set_nth0(+Index,+List,+Item,-NewList) |
Replaces one existing position without mutating the input list. |
last(+List,?Last) |
Returns the final element of a nonempty proper list. |
take(+Count,+List,-Prefix), drop(+Count,+List,-Suffix) |
Select the first Count elements or remove them. Counts beyond the list length fail. |
slice(+Start,+Count,+List,-Slice) |
Selects exactly Count elements beginning at Start; an out-of-range slice fails. |
reverse(+List,-Reversed) |
Reverses a proper list. |
length(?List,?Length) |
Reports or checks the length of a proper list, or generates a list skeleton when Length is a bound nonnegative integer. |
sum_list(+List,-Sum) |
Sums numeric elements with ISO is/2. The empty sum is 0; invalid arithmetic raises the corresponding ISO error. |
min_list(+List,-Min), max_list(+List,-Max) |
Select by EyeProlog term order, not numeric coercion. Empty lists fail. |
list_to_set(+List,-Set) |
Removes later structural duplicates while preserving first-occurrence order. |
:- use_module(library(lists)).
answer(split, pair(Prefix, Suffix)) :-
append(Prefix, Suffix, [a, b]).
answer(second, Item) :-
nth0(1, [a, b, c], Item).
eyeprolog --goal 'answer(Kind, Value)' program.pl
The portable text API uses ISO atoms or proper lists of one-character atoms.
A generated text result defaults to an atom. Double-quoted source text uses the
ISO representation selected by double_quotes; with the default chars, it is
already a proper character list accepted by this API. The 56-predicate portable
library itself has no STRING or JavaScript dependency.
| Predicate and principal mode | Behavior |
|---|---|
string_concat(?Left,?Right,?Text) |
Concatenates or splits atom/character-list text. At least two arguments must determine the operation; generated text is an atom. |
contains(+Text,+Needle) |
Tests literal containment. |
matches(+Text,+Pattern) |
Tests |-separated literal alternatives. |
matches(+Text,+Pattern,-Context) |
Portable named-capture matcher. Supports literals, ^/$, named groups (?<name>...), optional named groups, \w+, [A-Za-z]+, [0-9]+, and literal group bodies. Captures are atoms in comma-context data such as (year('2026'), month('07')). |
split(+Text,+Separator,-Parts) |
Literal split into a proper list of atoms. |
join(+Parts,+Separator,-Text) |
Joins atom/number/character-list lexical values. The empty list produces the empty atom ''. |
substring(+Text,+Start,+Count,-Part) |
Extracts characters using zero-based nonnegative integer indexes. |
replace(+Text,+Search,+Replacement,-Result) |
Replaces every literal occurrence. An empty search leaves the text unchanged. |
lowercase(+Text,-Lower), uppercase(+Text,-Upper) |
Portable ASCII case mapping. Non-ASCII characters are preserved unchanged rather than delegated to host Unicode case conversion. |
trim(+Text,-Trimmed) |
Removes the ISO-portable ASCII whitespace set at both ends. |
number_string(?Number,?Text) |
Historical predicate name retained for compatibility; converts a number to atom/character-list text or parses such text. |
atom_string(?Atom,?Text) |
Historical predicate name retained for compatibility; relates an atom to atom/character-list text. |
term_string(+Term,-Text) |
Renders a nonvariable term into atom/character-list text using the portable library serializer. It does not parse text back into a term. |
The named-capture matcher deliberately implements a small, auditable Prolog subset rather than JavaScript regular-expression semantics. Use a host predicate when an application genuinely requires a full host regex engine.
:- use_module(library(strings)).
:- use_module(library(lists)).
answer(words, Words) :-
trim(' Logic Made Visible ', Clean),
lowercase(Clean, Lower),
split(Lower, ' ', Words).
answer(captures, Context) :-
matches('Ada Lovelace',
'^(?<first>[A-Za-z]+) (?<last>[A-Za-z]+)$',
Context).
eyeprolog --goal 'answer(Kind, Value)' program.pl
These Prolog relations follow the documented collection, arithmetic, term-order, and scoping contracts. The caller is responsible for making that search finite. Bind outer variables before the nested goal when they are intended to restrict its domain.
| Predicate and principal mode | Behavior |
|---|---|
countall(+Goal,-Count) |
Counts all solutions, including solutions that produce the same visible template. The empty count is 0. |
sumall(+Template,+Goal,-Sum) |
Sums the numeric value of Template in every solution. The empty sum is 0; invalid arithmetic raises the corresponding ISO error. |
aggregate_min(+KeyTemplate,+ValueTemplate,+Goal,-BestKey,-BestValue) |
Retains the solution with the smallest resolved key under standard term order. |
aggregate_max(+KeyTemplate,+ValueTemplate,+Goal,-BestKey,-BestValue) |
Retains the solution with the largest resolved key. Both best-value predicates fail on an empty solution set and retain the first solution on an equal key. |
ISO findall/3 is present in both registries. The EyeProlog library aggregates follow
the same scoping principle: variables created inside the nested search do not
leak except through the declared templates and outputs.
There is no not/1 alias; use ISO \+/1. forall/2 is available from
library(iso_ext), and once/1 is supplied directly by the ISO registry.
:- use_module(library(aggregate)).
:- use_module(library(lists)).
:- use_module(library(iso_ext)).
cost(a, 8).
cost(b, 3).
cost(c, 3).
answer(count, N) :- countall(cost(_, _), N).
answer(best(Name), Cost) :-
aggregate_min(CandidateCost, CandidateName,
cost(CandidateName, CandidateCost),
Cost, Name).
eyeprolog --goal 'answer(Kind, Value)' program.pl
A comma-context needs no special native predicate. A small program relation can
walk its members, and ISO =../2 can expose any member’s name and argument list.
:- use_module(library(lists)).
message(event_17,
(severity(high), source(sensor_3), reading(temp, 91))).
context_member((Left, _right), Member) :- context_member(Left, Member).
context_member((_left, Right), Member) :- context_member(Right, Member).
context_member(Member, Member) :- Member \= (_left, _right).
context_parts(Context, Name, Args) :-
context_member(Context, Member),
(Member =.. [Name | Args]),
atom(Name).
answer(field(Name, Args)) :-
message(event_17, Context),
context_parts(Context, Name, Args).
eyeprolog --goal 'answer(X)' program.pl
The ISO profile includes functor/3, arg/3, and =../2. Use =../2 for whole-argument-list
decomposition and construction, =/2 for unification, and \=/2 for
non-unifiability; redundant aliases are not registered.
Import library(iso_ext) when a program needs portable solution counting,
universal checks, inclusive integer generation, difference-list collection,
or variant comparison:
:- use_module(library(iso_ext)).
task(parse).
task(check).
task(report).
extension_answer(all_tasks_are_atoms, true) :-
forall(task(Task), atom(Task)).
extension_answer(numbered, Pairs) :-
findall(N-S, (cfor(1, 3, N), succ(N, S)), Pairs).
extension_answer(with_tail, Tasks) :-
findall(Task, task(Task), Tasks, [done]).
extension_answer(same_shape, true) :-
variant(node(X, X), node(Y, Y)).
The command line is an observation boundary around a theory. Keep the program fixed while selecting the evidence you need: ordinary output for answers, proof output for support, warnings for portability risks, and statistics for search behavior.
eyeprolog
eyeprolog [options] [file-or-url.pl|- ...]
Run eyeprolog without arguments to enter the interactive top level. Queries
may span lines and end with a full stop, as in Scryer Prolog:
?- use_module(library(lists)).
true.
?- member(X, [prolog, logic]).
X = prolog
; X = logic.
?- halt.
When another answer exists in an interactive terminal, press ;, Space, or
n to ask for it immediately; no Return is needed. Return or . stops
enumeration, a enumerates all remaining answers, and f advances to the
next five-answer boundary (5, 10, 15, … displayed leaf answers), regardless
of how many answers were stepped through individually beforehand. h displays
the answer-control help. Enumeration of goal continuations is demand-driven:
after an answer is found, the top level does not execute a later program branch
or side effect merely to discover whether the current answer is the last one.
The solver’s uniform choicepoint protocol uses explicit clause and control
frames, permits a one-answer buffer only for effect-free host relations, and
requires stateful or meta-control iterators to report their pending state
directly. Search that can perform an effect starts only after an answer-control
command asks to continue. Stopping enumeration closes any active
call_cleanup/2 or
setup_call_cleanup/3 protection exactly once; detecting that a choicepoint
remains does not execute that next branch. If an unresolved alternative
ultimately has no solution, asking for it may therefore finish with false.. In scripted non-TTY input, a new
query line implicitly stops the preceding answer enumeration without consuming
the new query; explicit ;, n, Space, a, or f still requests more
answers. Once the top-level reader has accepted a complete query, the following
line begins with two spaces to mark active execution; a
third space appears when its result is ready for formatting. The answer prompt
is ; with no trailing space while it waits for input; after an advance
command, one space marks active search and a second marks an answer ready for
formatting. While a query is actively computing, EyeProlog releases readline’s
terminal signal handling: Ctrl-C
therefore terminates the current EyeProlog process immediately, and on POSIX
terminals Ctrl-Z suspends it in the usual shell-managed way. This remains a
host top-level convention rather than an ISO/IEC 13211-1 language feature. A
period-terminated query with no solutions prints false.; a solution without
visible variable bindings prints true.. Answer substitutions are rendered as
valid Prolog syntax under the current operator table: when a bound value would
not be a valid right operand of the displayed =/2, EyeProlog adds parentheses,
for example T = (a = b). rather than the invalid T = a = b.. When an answer
ends in a graphic token, the top level inserts layout before its terminating
full stop so the two tokens cannot merge; for example ?- X = .* . displays
X = .* ., not X = .*.. Use [file]., ['file.pl']., or
consult(file). to consult local source; reconsult(file). is accepted as a
compatibility alias. Use halt. or halt(Status). to leave the top level.
For an extensionless designation such as [file]. or consult(file)., the
top level tries file.pl before the unsuffixed file. Both the shorthand and
consult/1 have modern reconsult semantics: consulting the same resolved file
again replaces its previous source, so clauses removed from the file do not
remain active.
When read/1-2 or read_term/2-3 actually reaches interactive
user_input, the top level requests the next full-stop-terminated Prolog term
with a |: input prompt instead of treating the terminal stream as already
exhausted. The request is made at execution time, so multiple reads in one goal
and reads reached through user predicates work independently. For example:
?- read(X), read(Y).
|: hello.
|: world.
X = hello, Y = world.
Typing Ctrl-D at an empty |: prompt makes that Prolog read return
end_of_file; it does not close the surrounding EyeProlog top-level loop, so a
new ?- query can still be entered afterwards. The top-level prompts and this
terminal EOF convention are host-interface behavior rather than part of
ISO/IEC 13211-1; terms supplied to the reads are parsed by the same ISO
term-input machinery as read/1-2 and read_term/2-3 on other text streams.
Up and Down recall queries from the current session. Explicit eyeprolog -h
displays command-line help.
A Prolog source file states facts, rules, and ISO directives; the command line
selects what to solve. Supply -g or --goal followed by a callable Prolog goal:
eyeprolog --goal 'ancestor(ada, Who)' examples/ancestor.pl
Repeat -g or --goal to request several result relations in one run. EyeProlog prints
their ground answers in the order the goals were supplied.
For a self-running example, place the host goal in an ordinary comment:
%% goal: ancestor(ada, Who)
When no -g or --goal option is present, the CLI reads these comments from all
input sources and runs them in source order. An explicit goal option overrides them.
Because %% goal: is a comment rather than a Prolog directive, another ISO
processor may ignore it and the program remains portable Prolog text. External
goals are still preferable when a script, shell history, or API call should
make the observed question explicit.
| Option | Meaning |
|---|---|
-h, --help |
Show usage |
-p, --proof |
Print why/2 explanations |
-q, --quads |
Run embedded quad tests and fail if any do not hold |
--iso-strict |
Restrict parsing and execution to ISO/IEC 13211-1:1995 + Corrigenda 1–3; reject EyeProlog language extensions, disable interop autoloading, and disable automatic tabling |
--portable |
Enforce the conservative EyeProlog/Trealla/Scryer interoperability profile |
--no-autoload |
Disable conservative interop predicate autoloading |
-s, --stats |
Print final solver and memory statistics to stderr after execution |
-v, --version |
Print the package version |
-w, --warnings |
Print non-fatal portability warnings |
-g, --goal Goal |
Solve a callable goal; may be repeated; overrides %% goal: comments |
-- |
Treat following arguments as inputs |
Short flags may be combined, so -pqw is equivalent to -p -q -w.
--iso-strict cannot be combined with --quads, because quads and their
predefined infix (?-)/2 form are an EyeProlog testing extension. Strict mode
retains the Part 1 prefix (?-)/1 operator and treats -->/2 as ordinary Part
1 operator syntax; it does not perform Part 3 grammar-rule expansion or expose
phrase/2-3. Part 2 module directives and EyeProlog libraries are rejected,
the implementation-specific occurs_check flag is absent, and automatic
tabling/recursion guards are disabled. Normal mode continues to support Parts
2–3 and EyeProlog extensions, plus conservative interop autoloading for the
profile documented above.
Inputs may be local files, HTTP(S) URLs, or one - for stdin. The bare command
eyeprolog starts the normal REPL; eyeprolog --iso-strict starts the strict
core REPL. When options are present but no input is named, stdin is normally
used, but the strict-mode-only invocation is reserved for the REPL, so write
eyeprolog --iso-strict - explicitly when strict source should come from
stdin. Multiple sources are parsed as one program, so facts, rules, and
directives can be separated across files. A relative include/1 inside a local
file resolves from that file’s directory.
For example:
eyeprolog --iso-strict --goal 'p(X)' program.pl
eyeprolog --iso-strict
printf 'p(a).\n' | eyeprolog --iso-strict --goal 'p(X)' -
Work in a fixed sequence:
--proof when the support for an answer is the question;--warnings when portability or negative dependencies are the
question; use --portable when non-profile dependencies must fail CI;--stats only when comparing two executions of the same semantic case.For example:
eyeprolog --goal 'ancestor(X, Y)' examples/ancestor.pl
eyeprolog --proof --goal 'type(X, Y)' examples/socrates.pl
eyeprolog --warnings --goal 'answer(X)' test/conformance/warnings/negation/unstratified_mutual.pl
eyeprolog --portable --goal 'sudoku9_solution(S)' examples/clpz-sudoku-9x9.pl
eyeprolog --stats --goal 'path(a, X)' examples/path-discovery.pl > answers.pl 2> run.stats
Normal answers and why/2 terms go to stdout, which makes them suitable for a
golden file or another EyeProlog input. Warnings and statistics go to stderr so
they do not corrupt that logical stream. A successful run normally exits with
status zero; loading, syntax, option, and other uncaught errors use status 1. halt/0-1 can deliberately choose the
process status from inside a program.
A quad places a query directly before a description of its expected top-level answer. The answer is ordinary Prolog syntax rather than quoted text or a comment, so a small test reads like the interaction it checks:
color(red).
color(green).
colors ?- color(X).
X = red
; X = green.
?- color(blue).
false.
Run all quads in a file with eyeprolog --quads file.pl or eyeprolog -q
file.pl. A label such as colors is optional. A label is not a separate
mini-language: it is the ordinary first argument of (?-)/2, and therefore may
be any Prolog term admitted there by the normal term grammar. Quad execution
requires that argument to be ground; a non-ground label is reported as a quad
failure rather than aborting source parsing. Loading the file normally only
records its quads; it does not execute them or add their queries and answers as
program clauses. A quad run prints a summary and exits with status 1 when any
description fails. If no description fails but a bounded search cannot decide
an exact answer sequence, the case is reported separately as UNDECIDED and
the CLI exits with status 2. Quad mode imports library(prologue) as a
compatibility prelude because the ISO Prolog working-example files use those
predicates as system predicates without an explicit module directive.
Unless the source explicitly selects another unknown flag, quad execution
uses unknown=error, so an undefined predicate is reported rather than being
accepted as a negative answer.
Answer descriptions support ordered answers separated by ;, acceptable
alternatives separated by |, true, false, standard error descriptions,
and the unexpected annotation for an answer that must not occur (inattendue
is its synonym). In an ordered answer sequence, unexpected is a negative
assertion about the answer at that position: once the next observed answer does
not match the annotated leaf, that description succeeds and does not require the
query to have no further answers. Variables named in the query keep their
identity inside answer descriptions; variables introduced only by a description
are fresh. For example,
a query throw(g(X)) is described by throw(g(_X)), while
throw(g(X)), unexpected verifies that ISO throw/1 did not retain the query
variable in the renamed exception term. ... and ad_infinitum accept further answers. The maybe annotation describes
a successful answer that still has at least one pending residual constraint. It
does not stand for an arbitrary answer and it does not weaken substitution
matching: X = a, maybe still requires the X = a substitution. Conversely, a
successful answer description without maybe requires that no residual
constraint remain. For example, a pending dif/2 constraint can be checked as:
?- dif(X,Y), X = a.
true, unexpected.
X = a, unexpected.
X = a, maybe.
maybe, unexpected.
EyeProlog treats native variable constraints, attributed-variable residue, and
delayed goals as pending residue for this purpose. Multiple
indented descriptions after one query are independent checks: each re-runs the
query, each is counted in the quads: summary, and a failing description does
not suppress later descriptions for that query. inputs/1 supplies and checks
exactly the characters consumed by the query. peeks/1 may add one character
that is available for look-ahead but must remain unconsumed. The runner puts an
invalid-character sentinel immediately after the declared input boundary, so a
reader cannot accidentally use an artificial end-of-file to decide that a full
stop terminates the term. For example:
?- read(X).
inputs("1."), X = 1, unexpected.
inputs("1."), peeks(" "), X = 1.
inputs("1. "), peeks(" "), X = 1, unexpected.
outputs/1 checks characters emitted while reaching the described answer or
error, including output produced before a later exception. Its argument may be
an exact character list/string or a DCG body: terminal sequences,
conjunction/disjunction, .../ad_infinitum sequence wildcards, and
user-defined DCG nonterminals are matched against the captured characters.
The waits and unordered other_answer_sequence annotations are not executed
by the current runner.
Following Trealla’s quad convention, sto declares that a query is subject to
occurs-check. EyeProlog checks this conservatively rather than attempting a
complete STO/NSTO decision procedure. During the query’s ordinary execution,
the finite-tree unifier records a concrete occurs-check event as positive STO
evidence; the query is not run a second time merely to probe STO-ness. A finite
execution that completes naturally without such an event disproves sto, while
a search or resource boundary leaves the declaration conservatively unchecked.
The answer portion of an sto-annotated leaf remains implementation-dependent
and is therefore not compared.
For example, the cyclic binding in the first query provides positive STO evidence, whereas the second query is finite and cannot be STO:
?- X = s(X).
X = ..., unexpected.
false, unexpected.
sto, false
| sto, true.
?- true.
sto. % fails: no STO evidence
When a quad declares STO and the execution observes an occurs-check event, an
unannotated unexpected leaf does not reject the implementation-dependent
finite-tree outcome. This is partial STO detection only: EyeProlog makes a
definite statement where execution provides definite evidence and otherwise
does not guess.
loops is kept distinct from merely exhausting the quad runner’s resources.
EyeProlog accepts structural nontermination evidence such as an active-variant
recursion cycle, with the loop depth/inference bounds as a bounded fallback. A
structural cycle is also strong enough to refute a finite false description;
it is not reported as undecided merely because the same query was checked by a
different answer description:
inf :- inf, inf.
?- inf.
loops.
Ordinary answer descriptions also have a finite inference budget (100000 by
default). Exhausting that budget does not establish loops and does not
turn an unfinished search into false; instead the description is reported as
UNDECIDED, for example:
quads: UNDECIDED expensive_case, program.pl:12
undecided: inference limit reached.
Thus quad execution has three useful outcomes: passed, failed, and undecided.
When there are no failures but at least one undecided description, the CLI exits
with status 2. The JavaScript API may override the ordinary search budget with
quadMaxInferences; loopMaxDepth and loopMaxInferences control the explicit
loops probe.
The JavaScript API exposes the same operation without process I/O:
import { Program, runQuads } from 'eyeprolog';
const program = Program.parse(source);
const report = runQuads(program);
console.log(report.passed, report.failed, report.undecided, report.stdout);
The syntax follows the “queries using answer descriptions” convention used by
Trealla and the ISO Prolog working examples. Because answer descriptions are
layout-sensitive, indent every description while keeping ordinary clause heads
and the next quad query at the left margin. A quad label may contain any number
of comma-separated metadata fields, including across layout before ?-; for
example 9, "case", passes ?- Goal. is one labelled query. Quad recognition
is structural after ordinary term parsing: functional, mixed, quoted-functor,
and parenthesized spellings of the same ?-/1 or ?-/2 term are semantically
equivalent. For example ?-(Label, Query). and (?-(Label, Query))., followed
by the same indented answer descriptions, create the same labelled quad as
Label ?- Query..
Statistics are comparative evidence, not a score in isolation. Preserve the program, input, runtime version, selected query, answers, and counters together. An optimization is acceptable only when the intended answers remain unchanged and the chosen resource measure improves on the relevant scale case.
The files under examples/ pair readable programs with checked output under
examples/output/. The conformance cases under test/conformance/ focus on
language behavior, including success, failure, errors, warnings, and file
loading. Use an example to learn a modeling pattern and a conformance case to
settle an exact processor question. npm test checks both along with the book’s
extracted programs; npm run generate refreshes those extracted examples after
changing executable book blocks.
Checkpoint. Run one example with --proof --stats. Identify which bytes
belong to the reusable logical result, which describe this execution, and which
process status an automated caller observes. Then change one fact and predict
all three channels before rerunning it.
For a first week, run socrates.pl and ancestor.pl, rewrite them from memory,
inspect their proofs, learn member/2, append/3, and select/3, solve one
finite puzzle, and add one explicit integrity query.
These schedules name a spine rather than a reading quota. Every meeting should include prediction, execution, one changed input, and a short explanation.
| Meeting | Six-meeting introduction | Ten-meeting course | Fourteen-meeting course |
|---|---|---|---|
| 1 | Chapters 1–2; Socrates and family facts | Chapters 1–2; Laboratory 1 begins | Chapters 1–2; predicates, terms, and unification |
| 2 | Chapters 3–5; recursion and lists | Chapters 3–5; Laboratories 1–2 | Chapters 3–4; rules, semantics, and recursion |
| 3 | Chapters 6–10; one finite puzzle | Chapters 6–8; finite generation and absence | Chapters 5–6; lists and arithmetic |
| 4 | Chapters 11–14 and 17–20; proof, integrity, construction | Chapters 9–12; contexts, models, proofs, and integrity checks | Chapters 7–8; negation and aggregation |
| 5 | Choose Chapters 21–25 or 26–30 | Chapters 13–16; performance and boundaries | Chapters 9–10; structured data and finite models |
| 6 | Chapters 31–33; release matrix and reflection | Chapters 17–20; construction and improvement | Chapters 11–12; answers, proofs, and integrity |
| 7 | — | Chapters 21–25; one advanced case | Chapters 13–14; termination and knowledge engineering |
| 8 | — | Choose Chapters 26–30 | Choose Chapters 15–16 or an alternate domain route |
| 9 | — | Chapters 31–32; test and debug | Chapters 17–20; construction, correctness, improvement |
| 10 | — | Chapter 33; project review | Chapters 21–25; advanced relational design |
| 11 | — | — | Chapters 26–27; witnesses and induction |
| 12 | — | — | Chapters 28–30; representation, experiment, limits |
| 13 | — | — | Chapters 31–33; test, debug, patterns |
| 14 | — | — | Laboratory demonstrations and rubric review |
For a classroom, use checkpoints as exit questions and laboratories as multi-meeting projects. A six-meeting introduction should prefer one small, finished theory over hurried coverage of every feature.
Modelers should study access-control-policy.pl,
clinical-trial-screening.pl, gdpr-compliance.pl, and
trust-flow-provenance-threshold.pl. Identify facts, derived concepts,
decisions, closed-world assumptions, and proof premises.
Algorithm students should study graph-reachability.pl,
dijkstra-risk-path.pl, stable-marriage.pl, sat-solver-dpll.pl, and
type-inference.pl. For each, identify the finite domain, branching relation,
pruning goals, witness, and termination argument.
Mathematics students should read Chapters 3, 19, and 26–30 together, then study
peano-calculus.pl, fundamental-theorem-arithmetic.pl,
stirling-bell-numbers.pl, d3-group.pl, and
matrix-noncommutativity.pl. For each program, distinguish definition from
theorem, computation from justification, finite evidence from universal proof,
and syntactic equality from the domain’s mathematical equality.
Review questions:
\+/1?invalid/1 relation before domain decisions?The examples directory is the book’s executable companion. The top-level directory contains 224 self-contained runnable programs. Every source program has an exact answer file under examples/output, and 61 selected programs have a checked explanation under examples/proof. The thematic tables below link every top-level program and open the program itself rather than merely naming it.
The generated examples/book/ tree serves a different
purpose: it mirrors the complete inline EyeProlog displays chapter by chapter.
Those files are checked for syntax, and displays containing queries are
executed, but some teaching fragments deliberately depend on neighboring
facts or helpers. Use the top-level catalog below when you want a self-contained
program with a golden answer; use examples/book/ when you want the exact
display being discussed on a page.
For any named example, the three useful views are:
examples/output/ counterpart;--proof output in
the corresponding examples/proof/ file.Run one program directly:
node bin/eyeprolog.js examples/ancestor.pl
node bin/eyeprolog.js --proof examples/ancestor.pl
Then compare the result with its linked golden file. A productive reading sequence is:
These examples compose ISO facilities that isolated conformance cases test one mode at a time.
| Program | Standard facility | Checked answer |
|---|---|---|
| CLP(B) Boolean circuit | A NOT/AND/OR XOR circuit is enumerated with labeling/1, then taut/2 verifies equivalence to the XOR (#) specification. |
answers |
| CLP(B) cardinality | A two-of-four review quorum combines card/2, implication, exclusive-or, labeling, and model counting. |
answers |
| CLP(B) feature model | Deployment-feature dependencies are expressed as Boolean constraints, enumerated with labeling/1, and counted directly with sat_count/2. |
answers |
| CLP(B) weighted planning | A bounded release plan uses implications and cardinality constraints, then weighted_maximum/3 selects the highest-value admissible feature set. |
answers |
| CLP(Z) factorial | Declarative predecessor and product constraints propagate a factorial without mode-sensitive is/2. |
answers |
| CLP(Z) global constraints | Compatibility tables, lexicographic and serialized schedules, global cardinality with costs, circuits, value counting, and integer comparison. | answers |
| CLP(Z) N-queens | A checked eight-queens witness using finite domains, delayed diagonal constraints, all_distinct/1, and first-fail labeling, plus a four-queens multi-solution search. |
answers |
| CLP(Z) resource allocation | Resource assignment using element/3, sum/3, scalar_product/4, reification, labeling options, and domain reflection. |
answers |
| CLP(Z) Sudoku 9×9 | The AI Escargot 9×9 model with finite domains, 27 all-distinct constraints, and first-fail labeling; the default golden verifies its known solution while sudoku9/1 remains the search relation. |
answers |
| Combinatorics Findall Sort | Eyelet-inspired combinations example using findall/3 and ISO sort/2. |
answers |
| Floating Point | Floating-point arithmetic and comparisons. | answers · proof |
| Atomic conversion | Atom splitting, character atoms, Unicode codes, and numeric parsing. | answers |
| Control and errors | call/1, once/1, cut, if-then-else, throw/1, and catch/3. |
answers |
| DCG command parser | A Part 3 grammar parses token lists into application terms, generates tokens, preserves a remainder, and rejects malformed input. | answers |
| DCG expression language | A precedence-aware bidirectional grammar builds arithmetic ASTs, evaluates variable expressions, regenerates minimally parenthesized tokens, round-trips syntax, and preserves a remainder. | answers |
| Dynamic database | Initialization and ordered updates to a declared dynamic procedure. | answers · proof |
| Grouped solutions | findall/3, bagof/3, setof/3, existential qualification, and clause/2. |
answers |
| Integer arithmetic | Integer quotient/remainder choices plus bit operations. | answers |
| ISO extension pipeline audit | A bounded pipeline audit composing every library(iso_ext) relation: nested counting, universal validation, successor generation, difference-list collection, and schema comparison modulo variable names. |
answers |
| ISO extensions | Common control, collection, integer, and term-variant extensions from library(iso_ext). |
answers |
| Operators | Custom syntax, standard term order, and operator-table inspection. | answers · proof |
| Portable library overlap | Shared Scryer/Trealla interfaces for CLP(B), ordered sets, graphs, reification, delayed goals, generated names, character conversion, matrix transposition, and explicit table syntax. | answers |
| Reflective terms | Term shape, construction, copying, variables, identity, and standard order. | answers |
| Term I/O | Text-stream lifecycle, canonical writing, reading, metadata, and end state. | answers |
| Term Tools | Term-tool builtins for inspecting, constructing, rendering, and validating structured terms. | answers · proof |
Read these beside Part VIII. Then use the ISO conformance cases when a program depends on the exact failure or error behavior of a particular mode.
These programs isolate one idea at a time. Read them before the larger case studies.
| Program | What to notice | Checked companions |
|---|---|---|
| Age | Arithmetic comparison acts as a filter after a fact supplies the age. | answers · proof |
| Ancestor | The canonical base-plus-recursive definition computes a transitive family relation. | answers · proof |
| Animal | Several clauses form a small classification theory with inspectable reasons. | answers · proof |
| Annotation | Terms attach descriptive data while the logical relation remains ordinary. | answers · proof |
| Backward | A tiny derived fact justified by a numeric comparison in an ordinary Horn rule. | answers · proof |
| Cat Koko | Named Skolem-style witnesses standing in for the existential witnesses of the original N3 example. | answers · proof |
| Derived rule | A conclusion depends on another derived predicate rather than directly on a source fact. | answers · proof |
| Dog | A compact inheritance chain shows how intermediate concepts appear in a proof. | answers · proof |
| Existential rule | Structured Herbrand terms carry explicit generated witnesses. | answers · proof |
| Good cobbler | Multiple premises combine into a conclusion without hidden mutation or control state. | answers · proof |
| Herbrand Semantics | Herbrand terms denote themselves: distinct names and constructor applications remain distinct without extra unique-name or free-constructor axioms. | answers · proof |
| Herbrand witnesses | Functional witness terms make existential structure and syntactic identity visible in both answers and derivations. | answers · proof |
| Reusable built-ins | Arithmetic, strings, lists, and term inspection compose through ordinary variables. | answers · proof |
| Skolem Functions | Skolem functional terms in rule heads. | answers |
| SNAF | Negation as failure establishes that Alice does not hate Bob before deriving that she hates Nobody. | answers · proof |
| Socrates | A fact and one rule turn the classical syllogism into a ground derivation. | answers · proof |
| UUID | uuid/3 reproducibly creates one version 4 UUID atom from explicit random state; the example validates its canonical shape. |
answers |
| Witch | Burn the witch, adapted from Eyeling’s examples/witch.n3. | answers · proof |
Suggested path: Socrates → Age → Ancestor → Derived rule → Reusable built-ins. At each step, say aloud what one ground instance of every predicate means.
These examples make termination arguments visible. Compare structural descent, visited-state search, and fixed-point tabling rather than treating all recursion as one technique.
| Program | What to notice | Checked companions |
|---|---|---|
| Chart parser | A finite chart represents shared parsing subproblems and recursive grammatical structure. | answers · proof |
| Cyclic path | A deliberately cyclic graph exposes repeated calls and the need for disciplined recursion. | answers |
| Deep taxonomy: 10 | A small generated hierarchy is readable by hand and establishes the benchmark shape. | answers |
| Deep Taxonomy 100 | A 100-step taxonomy chain that exercises deep recursive closure and side-label derivation. | answers |
| Deep taxonomy: 1,000 | The same logical theory tests indexing and recursive closure at a realistic depth. | answers |
| Deep Taxonomy 10000 | A 10,000-step taxonomy chain used as a large-depth tabling and closure stress test. | answers |
| Deep taxonomy: 100,000 | A stress case separates semantic simplicity from implementation scale. | answers |
| Family cousins | Several relational joins derive kinship beyond a simple transitive closure. | answers |
| Graph reachability | A visited list bounds cyclic traversal and makes explicit negative test cases finite. | answers · proof |
| Graph | Productive right-recursive transitive closure over a directed map, contrasted with an under-generating left-recursive formulation. | answers |
| List collection | findall/3, list construction, and aggregation turn a solution stream into data. |
answers · proof |
| Path discovery | Witness paths, not only endpoint pairs, are constructed during a larger graph search. | answers |
| Service Impact | Practical cyclic recursion: incident impact analysis for a service dependency graph. | answers |
Read the three taxonomy programs as one experiment: the mathematical relation does not change as the data scale changes. Any difference in runtime belongs to control, indexing, memory, and table management.
The central question for every program in this group is: what exactly is the finite search space, and which constraint removes which branches?
| Program | Search design | Checked answer |
|---|---|---|
| Dijkstra Findall Sort | Eyelet-inspired Dijkstra example using findall/3 and ISO sort/2. |
answers |
| Dijkstra | Weighted path enumeration adapted from Eyeling dijkstra.n3. | answers |
| DONALD + GERALD = ROBERT | All ten decimal digits are assigned to ten distinct letters. Right-to-left carry propagation cuts a naive 10! search space to one solution. | answers |
| Enigma1225 | New Scientist Enigma 1225, retaining the best board in one pass with aggregate_max/5. |
answers |
| Eulerian path | The state tracks remaining edges rather than merely visited vertices. | answers |
| Four-color map | A finite color assignment is filtered by adjacency constraints. | answers |
| Hamiltonian path | A witness must visit every vertex exactly once; path construction and global coverage meet. | answers |
| Job-shop scheduling | Resource and precedence constraints interact in a larger finite schedule space. | answers |
| Knapsack optimization | Candidate subsets become feasible solutions, then aggregation selects a best value. | answers |
| Map Four Color Search | Four-colour search for the European Union neighbour graph. | answers |
| Markov Logic Network | Markov Logic Network style scoring over a tiny finite domain. | answers |
| Matrix Chain Order | Matrix-chain multiplication order by automatically tabled interval dynamic programming. | answers |
| Register allocation | Interference constraints turn compiler allocation into graph coloring. | answers |
| SEND + MORE = MONEY | Digit assignments are generated under distinctness, leading-zero, and column constraints. | answers |
| Stable marriage | Preference data, matching generation, and the absence of blocking pairs define stability. | answers |
| Weighted interval scheduling | Compatibility constraints and an ordered objective select a maximum-value schedule. | answers · proof |
| Zebra puzzle | House records, adjacency relations, and clue constraints jointly determine the famous solution. | answers |
A useful comparative exercise is to draw the first three levels of the search tree for N-Queens, SEND + MORE = MONEY, DONALD + GERALD = ROBERT, and Knapsack. Mark whether each branching decision chooses a permutation element, assigns a digit, derives a carry-constrained digit, or includes an item. The syntax is similar; the combinatorial objects and pruning strength are different.
Planning programs represent a world state as a term, define legal transitions, and search for a sequence whose final state satisfies a goal.
| Program | State-space idea | Checked answer |
|---|---|---|
| Allen Interval Calculus | Allen interval relations over integer time offsets, with interval records kept as scoped data. | answers |
| Blocks world | Symbolic actions transform a compact arrangement of blocks. | answers |
| Critical-path schedule | Dependency closure and duration arithmetic derive project timing. | answers |
| Dijkstra Risk Path | Risk-adjusted route selection that combines delivery cost, accumulated risk, path length, and a trust gate. | answers |
| Dining Philosophers | Chandy-Misra dining philosophers trace adapted from Eyeling dining-philosophers.n3. | answers |
| Drone corridor planner | Route feasibility combines graph structure with domain restrictions. | answers |
| GPS | Route planning over scoped map data, accumulating actions, duration, cost, belief, and comfort. | answers |
| Gray Code Counter | Gray-code counter adapted from Eyeling gray-code-counter.n3. | answers |
| Hanoi | A recursive plan mirrors the inductive structure of moving a tower. | answers · proof |
| Lee routing | Breadth-first wave expansion reaches a destination on a grid, then reconstructs a path around rectangular obstacles using the standard list relations. | answers |
| Microgrid dispatch | Candidate operating decisions are checked against supply, demand, and engineering limits. | answers |
| Missionaries and cannibals | Numeric state constraints must hold on both banks after every crossing. | answers |
| Monkey and bananas | Actions change location, support, and possession facts until the goal becomes true. | answers |
| Route planning | Weighted edges construct candidate routes and expose the chosen path as a witness. | answers |
| Wolf, goat, and cabbage | Safety invariants reject river-bank states before they enter a valid plan. | answers |
Compare the witness shape: Hanoi returns an inductively constructed move list; route planning returns a graph path; Lee routing reconstructs a path from breadth-first wave layers; Blocks world and the river puzzles expose a sequence of whole states. Representation determines which plan properties are easy to check.
These examples accompany Part VI. They range from executable definitions to finite counterexample searches. Do not call every computed result a theorem: state which domain was exhausted and which general property was proved only by the clauses.
| Program | Mathematical content | Checked answer |
|---|---|---|
| Ackermann | Ackermann-style fast-growing recursion benchmark adapted from Eyeling ackermann.n3. |
answers |
| Binomial Vandermonde | Two finite sums compute the sides of Vandermonde’s identity. | answers |
| Catalan convolution | A classic convolution identity is evaluated over a bounded range. | answers |
| Collatz 1000 | Collatz conjecture suite translated from Eyeling’s examples/collatz-1000.n3. | answers |
| Complex | Complex numbers, adapted from Eyeling complex.n3. | answers |
| Composition Of Injective Functions Is Injective | Composition of injective functions is injective, adapted from Eyeling’s examples/composition-of-injective-functions-is-injective.n3. | answers · proof |
| Continued Fraction Sqrt2 | Convergents of sqrt(2) by automatically tabled recurrence. | answers |
| D3 group | A finite Cayley table, inverses, and subgroup closure make group laws executable. | answers · proof |
| Diamond Property | Diamond property, adapted from Eyelet’s input/diamond-property.pl. | answers · proof |
| Easter Computus | Gregorian Easter computus adapted from Eyeling’s easter.n3. | answers |
| Equivalence Classes Overlap Implies Same Class | Equivalence-class overlap example adapted from Eyeling. | answers · proof |
| Fast exponentiation | Algebraic decomposition by parity changes a linear recurrence into logarithmic-depth recursion. | answers |
| Fibonacci | A recurrence becomes an executable relation with a visibly decreasing argument. | answers |
| Fundamental theorem of arithmetic | Two factorization strategies construct normalized prime-factor witnesses and check reconstruction. | answers |
| Goldbach | Bounded search checks Goldbach decompositions for powers of two using the portable Prolog primality relation. | answers |
| Greatest lower bound uniqueness | Order-theoretic definitions support a uniqueness argument. | answers · proof |
| Group inverse uniqueness | A short derivation exposes the algebraic premises needed for uniqueness. | answers · proof |
| Heron Theorem | Heron’s theorem: area = sqrt(s(s-a)(s-b)(s-c)). | answers |
| Integer partitions | Recursive generation constructs unordered additive decompositions without permutation duplicates. | answers |
| Law Of Cosines | Law of cosines: c^2 = a^2 + b^2 - 2ab cos(C). | answers |
| Matrix noncommutativity | Two concrete products provide a counterexample to universal commutativity. | answers |
| Modular exponentiation | Intermediate reduction preserves the residue while controlling numeric growth. | answers |
| Newton Raphson | Newton-Raphson root finding, adapted from Eyelet input/newton-raphson.pl. | answers |
| Peano arithmetic | Explicit natural-number terms support arithmetic relations and structural recursion. | answers |
| Peano calculus | Addition, multiplication, and factorial follow the constructors z and s/1. |
answers |
| Peasant | Peasant multiplication and exponentiation cases, adapted from Eyelet. | answers |
| Pell equation | Bounded generation searches for integer witnesses to a Diophantine equation. | answers |
| Pi | The Nilakantha series is a deterministic numeric recurrence; EyeProlog recognizes its accumulator shape and executes 10,000 terms without tabling or heap growth. | answers |
| Prime range | Bounded integer generation and divisor tests enumerate primes over an explicit finite interval. | answers · proof |
| Quadratic Formula | Quadratic formula over sample equations. | answers |
| Riemann Hypothesis | A deliberately finite audit of catalogued non-trivial zeros, illustrating the boundary between evidence and universal proof. | answers |
| Shoelace Polygon Area | Polygon area by the shoelace formula. | answers |
| Sieve | List filtering presents a different operational route to finite prime generation. | answers |
| Stirling and Bell numbers | Inclusion–exclusion and recurrence count set partitions in two related ways. | answers |
| Takeuchi | The Takeuchi function as a demanding nested-recursion benchmark. | answers |
| Totient summatory function | Divisibility, coprimality, counting, and summation compose over finite domains. | answers |
For a focused seminar, read Peano calculus, Fast exponentiation, D3 group, Matrix noncommutativity, and Fundamental theorem of arithmetic. They exhibit, respectively, structural induction, program improvement by algebra, finite model checking, refutation by one witness, and witness-producing number theory.
Here terms denote syntax, formulas, expressions, or programs. The crucial discipline is to keep object language and EyeProlog metalanguage distinct.
| Program | What the terms represent | Checked answer |
|---|---|---|
| SAT solver: CDCL | The example extends the SAT vocabulary toward conflicts and learned information. | answers |
| Chart parser | Shared chart items prevent grammatical subproblems from being rediscovered independently. | answers · proof |
| Context Schema Audit | Schema auditing for heterogeneous context terms by decomposing members with =../2 and checking predicate arity. |
answers |
| Derived Backward Rule | Derived backward rule example adapted from Eyeling derived-backward-rule.n3. | answers · proof |
| Equality saturation | Repeated rewrite closure explores equivalent symbolic forms to a fixed point. | answers |
| Expression evaluator | Arithmetic expression trees are interpreted under an explicit environment. | answers · proof |
| Fast Fourier Transform | Recursive evaluation builds a shared expression tree and treats graphic operators such as + and * as data atoms. |
answers |
| Intuitionistic Logic Kripke | Intuitionistic logic emulation with a finite Kripke model. | answers · proof |
| Knuth–Bendix completion | Oriented equations and critical interactions seek a more canonical rewrite system. | answers |
| Language | A small grammar recognizes a finite relational language. | answers |
| Linear Logic Resources | Linear logic emulation with explicit consumable resources. | answers · proof |
| Modal Logic Kripke | Modal logic emulation with a finite Kripke frame. | answers · proof |
| Partial evaluator | Known inputs specialize an expression or program while unknown parts remain symbolic. | answers · proof |
| Polynomial | Structured coefficients and powers support symbolic polynomial operations. | answers |
| Proof Contrapositive | Proof by contrapositive example adapted from Eyelet input/proof-by-contrapositive.pl. | answers · proof |
| Quine–McCluskey | Boolean minimization with the Quine–McCluskey method, including essential implicants and deterministic cover selection. | answers |
| SAT solver: DPLL | Formula representation, assignment, simplification, and branching form a compact solver. | answers |
| Symbolic derivative | Differentiation rules transform expression trees without evaluating them numerically; the proof golden exposes the recursive construction. | answers · proof |
| Turing machine | Machine configuration terms and transition rules expose a classical computation model. | answers |
Inspect the outermost functor of every data term. In the derivative example it names an expression constructor; in the SAT examples it names logical syntax; in the Turing example it helps describe a machine configuration. None of those nested terms is automatically asserted as an EyeProlog goal.
These programs make programs or system configurations the subject of reasoning.
| Program | Analysis idea | Checked answer |
|---|---|---|
| Abstract interpretation | A finite sign domain conservatively approximates many concrete executions. | answers |
| Cache performance | Configuration and workload facts derive performance classifications and reasons. | answers · proof |
| Canary release | Observations and thresholds support a deployment decision with auditable evidence. | answers · proof |
| Network SLA | Technology example: network path SLA check. | answers |
| Observability log correlation | Structured log events join across identifiers and time-related facts. | answers |
| Pointer analysis | Allocation and assignment constraints derive a points-to relation by closure. | answers |
| Register allocation | Liveness interference becomes a finite coloring problem. | answers |
| Relational Cube Lookup | Performance example: repeated multi-key relational lookups. | answers |
| Security incident correlation | Distributed observations combine into incident conclusions. | answers · proof |
| Truth-maintenance system | Justifications remain explicit when conclusions depend on defeasible information. | answers |
| Type inference | Structural unification solves type constraints for a tiny expression language. | answers |
| Vulnerability Impact | Vulnerability impact analysis over a transitive dependency graph. | answers |
Abstract interpretation deserves special care: an abstract warning is not the claim that every concrete execution fails. It says the abstraction cannot rule the failure out. The direction of approximation is part of the theorem.
These examples are best read in layers: source facts, normalized concepts, decisions, reasons, integrity conditions, and proof.
| Program | Decision domain | Checked companions |
|---|---|---|
| Access control policy | Attribute and policy facts derive permit status and reasons. | answers · proof |
| Clinical-trial screening | Inclusion and exclusion criteria produce an evidence-backed eligibility result. | answers · proof |
| Data negotiation | Offered and required data conditions derive an agreement or mismatch. | answers · proof |
| Deontic Logic | Deontic logic: obligations, prohibitions, compensations, and violations. | answers · proof |
| GDPR compliance | Purpose, basis, and processing facts support compliance conclusions. | answers · proof |
| Illegitimate Reasoning | Illegitimate reasoning detector. | answers |
| Integrity check | An explicit invalid-state relation reports contradictory input and a diagnostic status. | answers · proof |
| Nixon Diamond | Nixon diamond: two independent defaults support incompatible conclusions. | answers · proof |
| Trust-flow provenance threshold | Provenance and trust values remain premises of the derived threshold decision, including its arithmetic and comparison steps. | answers · proof |
| Workplace compliance | Training, role, and workplace conditions feed a compact compliance theory. | answers |
When studying a policy proof, circle every premise imported from outside the theory. The derivation validates the transition from those premises to the decision; it does not authenticate the source by itself.
This explicit separation between premises, rules, questions, alternatives, and proofs is also why Prolog is a natural human-facing layer. It is not a model of the whole brain, but its computational vocabulary is unusually close to the way people communicate deliberate reasoning: assert something, state a generalization, ask a question, consider another answer, reject an alternative, and explain why. The symbiotic KG example uses that closeness as an interface between machine proposals and human judgment rather than treating the model’s latent state as the shared source of truth.
These programs combine generated rdf/4 source facts with ISO Prolog rules
adapted from the rdf-prolog-roundtrip example corpus. Each query materializes
ground RDF-shaped results that can be serialized back to RDF.
The Symbiotic Knowledge Graphs example extends the same boundary into a human/AI feedback loop. Named graphs preserve source and governance context, RDF 1.2 triple terms carry AI-proposed statements without asserting them, EyeProlog decides which claims become operational knowledge, and result_rdf/4 materializes accepted knowledge and derived decisions for conversion back to RDF.
| Program | Roundtrip idea | Checked answer |
|---|---|---|
| Cross-organization data sharing | Combine ODRL/DPV policy, recipient properties, safeguards, jurisdiction, and retention into permit, deny, or review decisions with obligations. | answers · deck |
| Explainable EV-depot configuration | Select a compatible charger while deriving blockers and reversible required changes from the same relational rules. | answers · deck |
| DPV–ODRL purpose mapping | Verify six correspondences between a DPV process and an ODRL policy. | answers |
| ODRL–DPV–FPV trust flow | Combine policy rules and trust scores into permit, review, and deny decisions. | answers |
| ODRL–DPV healthcare risk ranking | Detect and rank healthcare-policy risks with clauses and mitigations. | answers |
| ODRL–DPV consumer risk ranking | Score consumer-policy conflicts and return a deterministic risk ranking. | answers |
| ODRL policy | Read one purpose-constrained permission from an ODRL policy graph. | answers |
| Advanced ODRL policy | Evaluate permission, duty, constraint failure, and prohibition outcomes. | answers |
| ODRL policy reasoning | Query action relationships, rule and enforcement outcomes, conflict strategies and kinds, action/rule/policy subsumption, and three-valued WFS defaults. | answers |
| RDF 1.2 annotated claims | Rank conflicting annotated claims by confidence and source trust. | answers |
| RDF 1.2 annotation | Recover an asserted triple together with its reifier and annotations. | answers |
| RDF 1.2 directional language | Preserve language and base-direction metadata in derived labels. | answers |
| RDF 1.2 nested triple term | Match nested triple terms and derive the innermost relationship. | answers |
| RDF 1.2 TriG graph join | Join default-graph metadata with measurements from a named graph. | answers |
| RDF 1.2 TriG named graph | Derive ancestor relationships inside the source named graph. | answers |
| RDF 1.2 TriG triple term | Project a triple term while retaining its named-graph context. | answers |
| RDF 1.2 triple term | Project a triple term into an ordinary asserted relationship. | answers |
| Operational incident response | Correlate symptoms and telemetry through a service dependency graph to derive root cause, transitive impact, evidence, and a guarded failover action. | answers · deck |
| SBOM vulnerability response | Traverse transitive dependencies, apply severity and waiver policy, expose the exact vulnerable path, and derive an upgrade action. | answers · deck |
| Scientific evidence graph | Use RDF 1.2 triple terms plus study metadata to distinguish supported claims, lower-quality counterevidence, and genuinely contested conclusions. | answers · deck |
| Symbiotic Knowledge Graphs | Roundtrip a city heatwave KG through RDF 1.2 triple-term proposals, human review, explicit Prolog governance, and materialized decisions. | answers · deck |
These examples make mathematical assumptions operational. Their values are illustrative models, not professional engineering or medical advice.
| Program | Model | Checked companions |
|---|---|---|
| Bayes Diagnosis | Bayesian diagnosis adapted from Eyeling bayes-diagnosis.n3. | answers · proof |
| Bayes Therapy | Memoize shared inference layers: the score vector, disease likelihood tails, and expected therapy success are reused by several report relations. | answers |
| Beam deflection | A mechanics equation combines load, geometry, and material parameters. | answers · proof |
| BMI | Metric and US-unit normalization, BMI classification, healthy-weight bands, and audit checks. | answers |
| Braking Safety Worlds | EYE reasoning-inspired example: braking safety in alternative worlds. | answers |
| Buck converter design | Electrical design candidates are checked against component and performance constraints. | answers |
| Competitive enzyme kinetics | A biochemical rate law becomes a numeric relational model. | answers |
| Control system | System parameters derive stability- and response-related quantities. | answers |
| Dairy energy balance | Intake and expenditure quantities are combined in an agricultural model. | answers |
| Electrical RC filter | Component values derive circuit behavior under an explicit formula. | answers · proof |
| Epidemic policy | Observations and thresholds connect a simple epidemic model to policy conclusions. | answers · proof |
| EV Range Worlds | EYE-inspired electric-vehicle range worlds. | answers |
| Exoplanet Validation Worlds | EYE reasoning-inspired example: exoplanet candidate validation worlds. | answers |
| FFT-8 Numeric | An eight-point radix-2 FFT over explicit complex pairs, showing butterflies, twiddle factors, and selected bins. | answers |
| Field nitrogen balance | Inputs, removal, and losses form a conservation-style accounting relation. | answers |
| GD Step Certified | A proof-friendly certified gradient-descent step with memoized interval bounds and explicit acceptance evidence. | answers |
| Hamming Code | Technology example: Hamming(7,4) single-bit error correction. | answers |
| Heat Loss | Engineering example: one-dimensional conductive heat loss through a wall. | answers · proof |
| Ideal Gas Law | Science example: ideal gas law. | answers · proof |
| Least-squares regression | Finite observations are summarized into a fitted linear model. | answers |
| Orbital transfer design | Candidate orbital parameters are evaluated against transfer equations. | answers |
| Pendulum Period | Science example: simple pendulum period. | answers |
| Radioactive Decay | Science example: radioactive decay. | answers |
| Spacecraft battery diagnosis | Telemetry, P = I²R, limits, and redundant sensing support diagnosis and action. |
answers · proof |
| Statistics summary | Aggregates compute descriptive statistics over a finite list. | answers |
| Superdense Coding | Superdense coding using discrete quantum computing, adapted from Eyelet’s input/superdense-coding.pl. | answers |
| Vector Similarity | Vector dot product, Euclidean norm, and cosine similarity. | answers |
For each scientific example, write a five-column audit: quantity, unit, source, equation, and approximation. A machine-checked derivation is only as interpretable as that modeling boundary.
After the focused examples, these programs are useful for whole-program reading. Begin by drawing their predicate dependency layers.
| Program | Why it is a capstone | Checked answer |
|---|---|---|
| AuroraCare | A large healthcare-oriented knowledge theory combines many domain concepts and decisions. | answers |
| Basic monadic | A large generated symbolic theory stresses parsing, terms, and relational execution. | answers |
| Delfour | Delfour insight-economy case adapted from Eyeling delfour.n3. | answers |
| Flandor | A broad rule set provides practice navigating a less tutorial-shaped theory. | answers |
| Knowledge-engineering alignment flow | Source concepts, mappings, validation, and derived alignment are kept in explicit layers. | answers |
| LLDM | A larger logical model demonstrates layered derivation over substantial source data. | answers |
| Manufacturing quality control | Measurements, limits, classifications, and actions form an auditable industrial decision. | answers |
Do not read a capstone from the first line to the last as if it were prose. Start at the supplied goal, find its predicate heads, follow their dependencies downward, and only then inspect the source facts. This is backward slicing by hand.
Run all 210 normal answer goldens and the 61 selected proof goldens with:
npm run test:examples
Run the complete conformance, regression, example, and proof corpus with:
npm test
When adding an example:
examples/output/;examples/proof/ when explanation is central;The full set of runnable source programs is checked against exact output, and
the tables in this chapter link every top-level program under examples/. The
thematic tables remain the recommended reading routes; the final alphabetical
table completes the index. Apply the same reading discipline to every example—
sentence, mode, finite domain, answer, proof, and revision.
This book is the single reference for the EyeProlog implementation. Chapters 38–40 describe its supported ISO Prolog syntax, directives, execution model, built-in predicates, and command-line interface. The earlier chapters explain the reasoner, automatic tabling, proof terms, warnings, answer formatting, embedding, and explicit host data boundaries.
The executable corpus under test/conformance/ tests the JavaScript
implementation. Positive programs and exact output cover arithmetic, text relations,
lists, terms, atoms, variables, negation, queries, rules, and
syntax. Separate corpora cover expected errors, warnings, and proofs:
npm run test:conformance
npm run test:iso-strict
npm run test:wg17
# Refresh the vendored TU Wien WG17 inventory when upstream changes:
npm run wg17:upgrade
node test/run-conformance-report.mjs
test/conformance/ISO-COMPLIANCE.md is the processor-requirement ledger for the
Part 1 conformance audit. It records explicit dispositions for the tracked processor, syntax, semantic, built-in, and arithmetic requirements. test/conformance/ISO-COMPLIANCE.md
maps language families to representative executable cases.
test/conformance/ISO-IMPLEMENTATION-DEFINED.md is the ISO 5.4 decision
index: it enumerates the Part 1 implementation-defined decisions and the
implementation-specific extension families without turning draft WG17/STC
proposals into the licensed baseline. ISO-TERM-SEMANTICS-MATRIX.md closes the 7.1-7.3 type/order/unification
rows, ISO-PROLOG-TEXT-EXECUTION-MATRIX.md closes 7.4-7.8 preparation,
database, conversion, execution, and control, and
ISO-EVALUABLE-FUNCTOR-MATRIX.md closes 7.9/Clause 9 expression and arithmetic
rows. The exit checklist in ISO-COMPLIANCE.md records the closure criteria and their evidence. test/conformance/WG17-SYNTAX-STATUS.md separately traces the vendored active
upstream syntax cases. Reviewed cases can pin exact strict-reader outcomes, while
newly upgraded cases execute directly against the upstream Codex expectation.
The syntax audit also cross-checks extension safety: each vendored WG17 case accepted by the strict Part 1 reader is executed through the normal profile and must preserve the same observable outcome. Additional normal-mode syntax may accept texts outside the strict grammar, but it may not reinterpret an accepted standard case.
The complete suite must pass before release. The file-based conformance corpus
contains 802 cases, including 386 focused ISO
cases derived from the success, failure, mode, and error behavior in
ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
Separate exact-output suites check 210 normal
examples and 61 proof examples; all extracted book programs are parsed and
their declared goals are executed. The eight-case
playground contract suite imports the production worker, sends real reasoning
requests through its message protocol, and crawls the served module graph for
missing assets, bad MIME types, and static Node-only imports. The generated
conformance-report.md is the authoritative source for the current executable
WG17 syntax result and file-based conformance category totals.
Repository artifacts have distinct roles:
conformance-report.md executes the vendored WG17 syntax gate and also inventories the file-based conformance corpus;examples/book/ is extracted from executable code blocks in this book and
should be rebuilt with npm run generate rather than edited directly;examples/output/ and examples/proof/ contain reviewed exact-output
goldens that make behavior changes visible in version control.Release preparation runs the complete suite and refreshes the conformance report. Keeping these details here allows the README to remain a short project landing page while this book remains the implementation reference.
Run the browser contract independently with:
npm run test:playground
EyeProlog executes a documented and tested ISO-oriented Prolog profile. Its strict-core target is ISO/IEC 13211-1:1995 with Technical Corrigenda 1-3. Normal mode additionally provides the documented module compatibility surface and a Part 3-oriented definite-clause-grammar implementation. The exact supported predicate indicators are listed in Chapter 39. The normal profile includes control and exceptions, term operations, arithmetic, grouped solutions, dynamic clauses, operators, atomic-term processing, flags, character conversion, streams, character/byte and term I/O, initialization, source inclusion, module compatibility forms, definite-clause grammar rules, and EyeProlog extensions.
For a Part 1 conformance boundary, --iso-strict (or API option
isoStrict: true) limits the processor to ISO/IEC 13211-1:1995 plus Technical
Corrigenda 1–3. Corrigendum 2 additions—including subsumes_term/2,
acyclic_term/1, sort/2, keysort/2, term_variables/2, retractall/1,
and call/2-8—remain part of that strict baseline. Part 2 modules, Part 3 DCG
expansion/phrase/2-3, quads, EyeProlog libraries, the occurs_check flag,
automatic tabling, call_cleanup/2, and setup_call_cleanup/3 are outside
that Part 1 strict surface.
The strict-core audit has explicit dispositions for the Clause 5 processor
obligations, Clause 6 syntax and rejection families, Clause 7 term/execution/I/O
and error semantics, the 8.2-8.17 built-in families, and Clause 9 evaluable
functors. The complete vendored WG17 syntax matrix is a release gate, and each
strict-success WG17 observation is also checked through normal mode so syntax
extensions cannot reinterpret accepted standard text. Implementation-defined
choices—including the Unicode-scalar processor character set, stream details,
flag defaults, floating behavior, and signed bitwise/shift semantics—are indexed
in test/conformance/ISO-IMPLEMENTATION-DEFINED.md. The release-facing closure
ledger is test/conformance/ISO-COMPLIANCE.md.
Notable implementation boundaries are:
ready() is represented by the atom
ready;double_quotes exactly; the default chars value
matches Trealla and Scryer and may be changed to codes or atom;write_term/2-3 implements the Part 1 plus Corrigendum 3 quoted/1,
ignore_ops/1, numbervars/1, and variable_names/1 option surface,
including option validation and traversal rules; normal mode also offers
double_quotes(true|false) and spacing(true|false) as explicitly
implementation-specific extensions, which strict mode rejects;Write terms explicitly, keep variables uppercase or underscore-prefixed, and quote atom names that are neither lowercase plain names nor graphic tokens. The conformance ledger and release gates verify this documented strict-core boundary. Their closure is implementation evidence, not independent ISO certification.
EyeProlog has no general host-call primitive, yet an untrusted theory is still executable input. It can request enormous finite searches or construct unbounded terms. URL inputs also cross a network and trust boundary. Applications should restrict accepted sources and impose suitable input-size, time, depth, memory, and solution limits. Proof output can be larger than answer output and needs its own budget.
The book is self-contained as an EyeProlog guide. These sources provide historical and technical background for the ideas that EyeProlog adapts. They describe larger languages and theories, so they should not be read as additional EyeProlog specifications.
ISO/IEC, ISO/IEC 13211-1:1995 — Programming languages — Prolog — Part 1: General core, with Technical Corrigendum 1:2007, Technical Corrigendum 2:2012, and Technical Corrigendum 3:2017. Chapter 38 defines the precise EyeProlog compatibility profile against this standards baseline; Chapter 39 lists the implemented predicate indicators.
Michael Genesereth, Introduction to Logic, Stanford University. This free online text provides a broader introduction to logical syntax and semantics, proof systems, and resolution, complementing the focused treatment of executable Horn clauses in this book.
David Hilbert, “Mathematical Problems”, address to the International Congress of Mathematicians, Paris, 1900; English translation published in 1902. The address exemplifies the axiomatic, problem-directed mathematical culture from which the later formal study of proof grew. Part VI places logic programming within that longer development without reducing the history of mathematics to formalism.
Kurt Gödel, “Über formal unentscheidbare Sätze der Principia Mathematica und verwandter Systeme I”, Monatshefte für Mathematik und Physik 38, 1931, pp. 173–198. The incompleteness theorems establish intrinsic limits for sufficiently expressive effectively axiomatized formal systems. Chapter 30 treats such limits as part of mathematical rigor, not as a failure of it.
Alonzo Church, “An Unsolvable Problem of Elementary Number Theory”, American Journal of Mathematics 58(2), 1936, pp. 345–363. Church’s lambda-definability account of effective calculability and his negative solution concerning general decision procedures helped make the boundary of algorithmic method mathematically exact.
Alan M. Turing, “On Computable Numbers, with an Application to the Entscheidungsproblem”, Proceedings of the London Mathematical Society 42, 1936–1937, pp. 230–265. Turing’s machine model gave an independent analysis of effective computation and another route to the undecidability of the general decision problem. It supplies historical context for the distinction in Part VI between a mathematical relation and a procedure guaranteed to decide it.
Jacques Herbrand, Recherches sur la théorie de la démonstration, doctoral thesis, University of Paris, 1930. Herbrand’s fundamental theorem and treatment of ground instances form a major proof-theoretic foundation for automated deduction. Chapter 3 explains how the later Herbrand universe, base, interpretations, and least-model vocabulary connect that foundation to logic programming.
J. A. Robinson, “A Machine-Oriented Logic Based on the Resolution Principle”, Journal of the ACM 12(1), 1965, pp. 23–41. The foundational account of resolution and machine-oriented unification behind later logic-programming proof procedures.
Alain Colmerauer and Philippe Roussel, “The Birth of Prolog”, in History of Programming Languages II, 1996, pp. 331–367. A first-person history of how theorem proving, natural-language processing, and programming-language design converged in early Prolog.
Maarten H. van Emden and Robert A. Kowalski, “The Semantics of Predicate Logic as a Programming Language”, Journal of the ACM 23(4), 1976, pp. 733–742. The classic fixed-point and model-theoretic account behind the least-Herbrand-model discussion in Chapter 3.
Robert A. Kowalski, “Algorithm = Logic + Control”, Communications of the ACM 22(7), 1979, pp. 424–436. The source of the distinction developed throughout Chapters 3 and 17–20.
Keith L. Clark, “Negation as Failure”, in Logic and Data Bases, 1978, pp. 293–322. Clark relates finite failure in a logic database to a completed-database reading. The historical note after Part II uses this work to distinguish operational negation from unrestricted classical negation.
Yoshihiko Futamura, “Partial Evaluation of Computation Process—An Approach to a Compiler-Compiler”, originally published in 1971 and republished in English translation. Futamura showed how specializing an interpreter with respect to a source program connects partial evaluation with compilation. Part V invokes this as historical context for specialization, not as an EyeProlog implementation claim.
Krzysztof R. Apt, Howard A. Blair, and Adrian Walker, “Towards a Theory of Declarative Knowledge”, in Foundations of Deductive Databases and Logic Programming, 1988, pp. 89–148. Background for stratified negation and for treating negative dependencies as layers rather than unrestricted cycles.
Weidong Chen and David S. Warren, “Tabled Evaluation with Delaying for General Logic Programs”, Journal of the ACM 43(1), 1996, pp. 20–74. A foundational treatment of tabled logic-program evaluation. EyeProlog’s automatic positive tabling is smaller in scope, but the shared-call and fixed-point intuitions are closely related.
Dörthe Arndt and Stephan Mennicke, “Notation3 as an Existential Rule Language”, 2023. Context for Notation3 and for the relationship between Semantic Web rule languages and existential-rule reasoning. EyeProlog deliberately implements a different, compact Horn-clause language.
Leon Sterling and Ehud Shapiro, The Art of Prolog, second edition, MIT Press, 1994. Its sustained treatment of computation, program construction, nondeterminism, transformation, interpreters, grammars, search, and applications is an important pedagogical benchmark for Part V. EyeProlog differs substantially from full Prolog, so the material here develops those themes only through EyeProlog’s explicit, supported relations.
The aim of EyeProlog is not to make every difficult problem easy. It is to keep the theory visible while the machine searches it: facts you can inspect, rules you can discuss, answers you can test, and proofs you can carry forward as data.
This glossary fixes the book’s vocabulary. Definitions describe EyeProlog unless a broader mathematical meaning is explicitly stated.
Aggregate. A relation that evaluates a finite nested solution space and
combines its solutions, as findall/3, countall/2, sumall/3,
aggregate_min/5, or aggregate_max/5 does.
Answer. A ground instance of a declared query goal produced by successful search. EyeProlog suppresses duplicate printed answers and source facts already identical to queried conclusions.
Answer set. The distinct ground answers for a query, considered without their discovery order or number of proofs.
Arity. The number of arguments of a predicate or compound term. Predicate
identity includes arity: edge/2 and edge/3 are different.
Atom constant. A symbolic scalar such as alice, ready, or
'a quoted atom'. An atom constant is data; an atomic formula uses a predicate
name, possibly with arguments, as a proposition.
Atomic formula. A callable proposition such as ready or
parent(ada, byron).
Base case. A nonrecursive clause that gives recursion a directly solvable case.
Binding. An association between a variable and a term accumulated during unification and search.
Binding pattern. Which arguments of a call are known, unknown, or partly structured at call time. See also mode.
Body. The comma-separated goals to the right of :- in a rule. Every body
goal must succeed for that rule use to succeed.
Built-in. A predicate whose relation is supplied by the host implementation rather than by source clauses. Built-ins may have restricted operational modes.
Call. A goal selected for solving, together with its current bindings.
Canonical form. A chosen representative for all values considered equivalent in a domain. Canonicalization can make some domain equality decidable by structural equality.
Clause. A fact or rule terminated by a period.
Choicepoint. A remaining search alternative that may produce another answer if the caller asks the solver to continue. Every resumable engine iterator follows the same pending-alternative protocol. A suspended iterator is conservatively a choicepoint unless it reports that no search position remains; the engine never executes an unrequested effect or program branch to look for a later successful answer.
Cleanup. A protected finalization goal installed by normal-mode
call_cleanup/2 or setup_call_cleanup/3. It runs exactly once when the
protected search ends, is pruned or abandoned, or unwinds through an exception.
Closed-world assumption. The decision to treat failure to derive a
sufficiently scoped claim as evidence for its absence. EyeProlog’s \+/1 performs
negation as failure; the modeler is responsible for justifying the scope.
Compound term. Structured data with a functor and one or more arguments,
such as point(3, 4) or reason(limit, exceeded).
Conformance corpus. The executable cases defining the supported ISO Prolog
profile and implementation extensions under test/conformance/.
Conjunction. Several goals joined by commas. Operationally they normally run left to right while carrying bindings forward.
Constraint. In this book, a goal that rejects candidates not satisfying a property. EyeProlog does not provide a general persistent constraint store.
Declarative reading. What ground instances of clauses mean independently of the particular order in which a solver searches.
Definite clause. A clause with exactly one positive head and a conjunction of positive body goals. The pure definite fragment has a least-Herbrand-model semantics.
Dependency graph. A graph whose vertices are predicate indicators and whose edges record calls between predicates. Recursive components are cycles in this graph.
Environment. The current collection of variable bindings during a branch of search.
Fact. A clause with no body, such as parent(ada, byron).
Failure. The absence of a solution for the selected goal along the current branch. Failure causes search to reconsider alternatives; it is not an exception and not automatically an explicit negative fact.
Finite domain. An explicitly bounded set of candidates a search can exhaust. Finiteness is a property of a call and its generators, not merely of a predicate name.
Fixed point. A stage of repeated consequence generation at which no new answers are added.
Functor. The name at the root of a compound term. In point(3,4), the
functor is point and the arity is two.
Generator. A goal that produces candidate bindings, usually from facts, finite lists, or bounded numeric ranges.
Goal. An atomic formula the solver is asked to establish.
Golden file. Checked expected output stored in the repository. Normal example goldens record answers; proof goldens record explanations.
Ground. Containing no variables. EyeProlog prints only ground query answers.
Head. The atomic formula to the left of :-, or the entire formula in a
fact. A successful rule use derives an instance of its head.
Herbrand base. The set of all ground atomic formulas constructible from a language’s predicate symbols and Herbrand universe.
Herbrand interpretation. A selection of ground atomic formulas treated as true over the Herbrand universe.
Herbrand universe. The set of ground terms constructible from the constants and function symbols of a program.
Indexing. Implementation machinery that narrows candidate clauses using bound arguments without changing the intended answer set.
Integrity check. An ordinary predicate whose answers identify invalid input. The host decides whether to reject, report, or inspect those answers.
Least Herbrand model. The smallest Herbrand interpretation satisfying a definite program; equivalently, the fixed point obtained by repeatedly adding supported ground consequences.
List. Either [] or a cons cell written [Head | Tail]. A proper list
eventually ends in [].
Mode. An intended direction of use described by which arguments are supplied and which are produced.
Negation as failure. The operational meaning of \+ Goal: succeed when a
terminating nested search finds no solution for Goal.
Operational reading. How a clause directs computation: which subgoal is selected, which bindings it needs and produces, and which alternatives it creates.
Occurs check. A unification check that prevents binding a variable to a
term containing that variable. EyeProlog performs it consistently for ordinary
unification as well as unify_with_occurs_check/2.
Predicate indicator. A predicate name paired with its arity, conventionally
written name/arity.
Proof. A successful derivation showing which clauses, facts, and built-ins support a ground answer. A proof records success, not every failed search branch.
Proof tree. The tree of successful subgoals supporting one derivation. Unlike a search tree, it omits failed alternatives.
Proper list. A finite list whose final tail is [].
Host goal. A callable Prolog goal supplied by the CLI or embedding API to select the relation whose answers are observed.
Readiness. The binding condition under which a mode-sensitive built-in can run safely and productively.
Recursion. A predicate depending on itself directly or through other predicates.
Relation. A set of tuples described by the ground instances for which a predicate holds.
Resolution. The proof-search step that matches a goal with a clause head and replaces it with the instantiated clause body.
Rule. A clause with a head and body, written Head :- Body.
Search branch. One sequence of clause and solution choices considered by the solver.
Search tree. The tree of successful, failed, and repeated alternatives explored while seeking answers.
Source fact. A fact explicitly present in loaded input, as opposed to a derived conclusion.
Stratified negation. Negative dependencies arranged in layers so no predicate depends negatively on itself through a dependency cycle.
Substitution. A mapping from variables to terms. Applying a substitution replaces those variables consistently throughout a term or clause.
Tabling. Evaluation that shares recursive calls and accumulates their answers toward a fixed point.
Term. An atom constant, number, variable, compound term, list, or
parenthesized comma term. Double-quoted notation denotes a list or atom as
selected by the ISO double_quotes flag.
Termination measure. A value in a well-founded order that strictly decreases along every recursive branch in a stated mode.
Theory. The collection of source facts and rules loaded together and interpreted as claims about a domain.
Unification. Structural equation solving that finds a substitution making two terms identical, when one exists.
Variable. A clause-local placeholder beginning with uppercase or
underscore. Bare _ is fresh at every occurrence.
Variant call. A call identical to another up to consistent renaming of variables. Variant recognition is important for tabling and cycle analysis.
Witness. A constructed ground term demonstrating an existential result, such as a path, assignment, factorization, schedule, or proof-relevant object.
These laboratories turn the book into a course. Each has a deliverable, an acceptance test, and a reflection question. Complete them in order or choose a route suited to a study group.
The estimates below assume familiarity with the listed chapters and include design, implementation, tests, and reflection. They are planning ranges, not deadlines.
| Laboratories | Preparation | Typical scope |
|---|---|---|
| Laboratories 1–2 | Chapters 1–5 | 2–4 hours each |
| Laboratories 3–4 | Chapters 6–10 and 13 | 4–8 hours each |
| Laboratories 5–7 | Chapters 19 and 26–29 | 4–8 hours each |
| Laboratories 8–10 | Chapters 14, 25, and 31–33 | 6–12 hours each |
| Laboratory 11 | Chapters 15–16 | 4–8 hours |
| Laboratory 12 | Chapters 16, 25, and 31–33 | multi-session capstone |
Build: facts for at least six people and relations for parent, sibling, grandparent, and cousin.
Requirements:
Acceptance: normal output contains the predicted ground relations; one proof for a cousin conclusion passes through named intermediate concepts.
Reflect: which conclusions depend on absence, and are those closed-world assumptions justified?
Build: user-defined relations for membership, concatenation, reversal, and prefix.
Requirements:
Acceptance: bounded differential queries find no disagreement in either direction.
Reflect: which logically meaningful modes are operationally infinite?
Build: a network with at least ten stations, cycles, weighted edges, and two disconnected components.
Requirements:
--stats before and after one justified control improvement.Acceptance: every returned witness begins and ends at the queried stations, uses known edges, and contains no repeated station.
Reflect: why can endpoint reachability table finitely while the set of arbitrary walks is infinite?
Build: encode a small Latin square, scheduling puzzle, or house puzzle.
Requirements:
Acceptance: the solver returns every intended solution and no permutation duplicate representing the same mathematical object.
Reflect: which source line contributes the greatest pruning power?
Build: Peano addition and multiplication, then one relation of your choice: exponentiation, comparison, division with remainder, or factorial.
Requirements:
Acceptance: the proof and program share a clearly identified base and recursive structure.
Reflect: did the representation make the induction easier or merely the computation slower?
Build: finite operation tables over carriers of two or three elements.
Requirements:
Acceptance: one deliberately nonassociative table is rejected with a specific triple; one valid group table passes every finite law.
Reflect: why does one counterexample settle the negative question while a thousand random confirmations do not settle the positive one?
Build: a small expression language with literals, variables, addition, conditionals, and local bindings.
Requirements:
Acceptance: the transformation is idempotent over the chosen corpus and does not change evaluated results.
Reflect: where is the boundary between Prolog syntax and the object language represented by Prolog terms?
Build: a sign, nullness, taint, or permission analysis for a tiny statement language.
Requirements:
Acceptance: every tested concrete behavior is covered by its abstract result; the analyzer may overapproximate but must not miss the chosen unsafe case.
Reflect: why is a warning not necessarily evidence that a concrete failure occurs?
Build: an access, consent, eligibility, or compliance theory.
Requirements:
Acceptance: changing one source fact changes exactly the predicted decision and its supporting proof.
Reflect: which trust claims are established by derivation, and which require authentication outside the theory?
Build: encode a compact model from mechanics, circuits, chemistry, epidemiology, or statistics.
Requirements:
Acceptance: a proof for the final classification includes measurements, equations represented by built-ins, and thresholds in an intelligible order.
Reflect: what has been proved conditionally, and what empirical claim remains outside formal logic?
Build: start with a small external record, validate it in JavaScript, convert it to ordinary Prolog facts, and derive one new relation.
Requirements:
Acceptance: invalid records are rejected before solving, valid records map to explicit finite terms, and the checked answer has an inspectable proof.
Reflect: which claims belong to host validation and which are established by the Prolog derivation?
Build: combine the preceding techniques into a small embedded service.
Requirements:
Acceptance: another person can clone the repository, run one command, and reproduce answers and proofs from the preserved inputs without oral instructions.
Reflect: if the service gives a wrong real-world decision, which of the four trust layers—source, model, engine, or derivation—would reveal the fault?
Evaluate each project on five independent axes:
| Axis | Excellent work demonstrates |
|---|---|
| Meaning | every public ground relation has one stable domain sentence |
| Logic | clauses derive the intended answers and reject counterexamples |
| Control | supported modes terminate for a stated mathematical reason |
| Evidence | tests, witnesses, and proofs expose why results hold |
| Boundary | sources, assumptions, versions, limits, and host duties are named |
A beautiful program is not merely short. It makes the reason for its correctness, the shape of its search, and the boundary of its trust available to the next reader.
Checkpoints are for retrieval and diagnosis, not grading by hidden wording. Attempt one before reading these notes. When a checkpoint asks about a program of your own, compare the structure of your argument rather than expecting one canonical implementation.
Chapter 1. parent(ada, byron) says that Ada is a parent of Byron.
eyeprolog --goal 'child(X, Y)' program.pl asks for every ground child–parent pair derivable by the
program. Adding parent(diego, elena). adds child(elena, diego).; it does not
change the earlier three child answers.
Chapter 2. point(X, X) unifies with point(red, red) by binding X to
red. It does not unify with point(red, blue) because one variable cannot
be both distinct atoms. [Head | Tail] unifies with [a, b, c] using
Head = a and Tail = [b, c].
Chapter 3. In
adult(Person) :- age(Person, Years), Years >= 18., a ground reading is:
every person with a recorded age of at least 18 is an adult. Operationally,
age/2 supplies Person and Years before >=/2 checks the numeric bound.
Reversing those goals asks >=/2 to inspect unbound terms.
Chapter 4. With ada → byron → clara → diego, direct ancestor answers are
the three edges. Recursive answers additionally include
ancestor(ada, clara), ancestor(byron, diego), and
ancestor(ada, diego). A successful derivation advances along a known parent
edge until a direct parent clause closes the proof.
Chapter 5. joins([a], [b, c], Whole) yields [a, b, c]. With the whole
list bound, the prefix/suffix splits are:
[] and [a, b, c]
[a] and [b, c]
[a, b] and [c]
[a, b, c] and []
[a | Tail] is not yet known to be proper because Tail might never resolve
to a finite chain ending in [].
Chapter 6. is/2, numeric comparisons, and the recursive
arithmetic steps require their documented numeric inputs. In
between(1, 10, N), an unbound N is generated from a finite interval; a
bound N is checked for membership in that interval.
Chapter 7. user(User), \+ blocked(User) first selects each known user,
then asks a ground absence question for that user.
\+ blocked(User), user(User) first asks whether the database contains no
blocked user at all. Calling either result “allowed” requires a justified,
complete user and blocked-status boundary.
Chapter 8. Over an empty nested search, findall/3 produces [],
countall/2 produces 0, and sumall/3 produces numeric zero.
aggregate_min/5 and aggregate_max/5 fail because no candidate can supply a
best key. The goal passed into the aggregate, not the aggregate’s punctuation,
must establish finiteness.
Chapter 9. message/2 is asserted as an atomic formula. Its context
argument is structured data. The program-defined context_member/2 relation
examines members inside that term; it does not add those members as globally
callable source facts.
Chapter 10. The complete coloring has six answers. Removing A \= C
leaves the requirements A ≠ B and B ≠ C, producing twelve answers. The six
new answers are those with equal first and third colors:
red–green–red, red–blue–red, green–red–green,
green–blue–green, blue–red–blue, and blue–green–blue.
Use this table to check that the checkpoint response separates concepts that are often collapsed:
| Chapter | A sound response distinguishes |
|---|---|
| 11 | ground answer, successful proof, failed search branches, and source trust |
| 12 | ordinary absence, invalid theory, process exit, and resource failure |
| 13 | structural descent, finite table growth, and unbounded term construction |
| 14 | source evidence, derived concepts, policy decisions, and integrity |
| 15 | host validation, explicit term conversion, and logical derivation |
| 16 | host validation, solver derivation, proof retention, and operational ceilings |
| 17 | ground meaning, intended mode, answer set, first answer, and proof shape |
| 18 | examples, near misses, finite generators, invariants, and presentation |
| 19 | partial correctness, completeness in a mode, and termination in that mode |
| 20 | semantic regression, observable control change, and measured improvement |
A response that says only “the program works” is incomplete. It should name the claim, the mode, the evidence inspected, and the boundary that remains outside that evidence.
Later checkpoints often admit several good programs. Evaluate them with five questions:
For mathematical checkpoints, add a sixth question: does the conclusion claim only what the computation warrants? One witness proves existence; one counterexample refutes a universal claim; an exhausted finite carrier proves a property only for that model; repeated bounded confirmations do not become an unbounded theorem.
For laboratory checkpoints, leave an artifact. A useful completion is not
merely a paragraph: it is a small source file, predicted output, actual output,
and one sentence explaining any difference. The extracted chapter examples,
top-level goldens, and npm test demonstrate that rhythm at repository scale.
GPT-5.6 was used during the development of this book to assist with chapter reorganisation, refinement of explanations, and review of examples and diagrams.
All suggestions were evaluated and directed by the author, who remains responsible for the book’s claims, choices, and any remaining errors.