Semantics¶
This page states the operational rules the engine follows. For the machinery behind them, see Architecture; for the exported symbols, see the API Reference.
Core rules¶
- Clause order — clauses are tried in definition order. Facts and rules share one ordered sequence per predicate; a fact (a clause with an empty body) is not tried ahead of a rule defined before it.
- Cut (
!) — prunes the running clause's remaining choice points and the predicate's remaining rule clauses. It is local to the predicate invocation. - Optional depth bound — rule resolution is unbounded by default. Set
:max-depthto a non-negative integer to bound user-rule resolution; exhaustion signalsprolog-depth-limit-exceededrather than masquerading as logical failure. - Occurs check — on during clause resolution, so the engine never
introduces a cyclic term behind your back; the
occurs_checkflag (defaulttrue) governs=/2and\=/2specifically, see below. Host-provided cyclic cons structures are handled safely either way: unification compares them coinductively, substitution preserves cycles and sharing, and variable collection terminates.
Atoms¶
An atom is its text. Two atoms are the same atom exactly when they print
the same, and =/2, ==/2, compare/3, and sort/2 all agree on that.
Quoting is therefore invisible, as ISO 13211-1 6.4.2 requires — hello ==
'hello' — while case is significant: 'FooBar', fooBar, and foobar are
three distinct atoms, each of which writeq/1 renders so that reading the
output back yields it again.
[] is an atom (ISO 6.3.5) of text "[]", represented as Common Lisp NIL,
so [] == '[]' holds and atom_length([], 2) succeeds.
Atoms in Lisp-authored rulebases¶
An atom is an ordinary Common Lisp symbol, which is what lets a Lisp-authored
rulebase and a Prolog-authored one denote the same predicate: (parent ?x ?y)
read by the Common Lisp reader and parent(X, Y). read by this engine's parser
both arrive as CL-PROLOG::PARENT. The consequence is that a symbol's name
is not its atom's text: the text of CL-PROLOG::PARENT is "parent".
Write a bare symbol for any atom whose text is lower case. For anything else,
call prolog-atom rather than writing a symbol literal, and prolog-atom-text
to go back:
(prolog-atom "FooBar") ; the atom Prolog source spells 'FooBar'
(prolog-atom-text (prolog-atom "FooBar"))
;; => "FooBar"
cl-prolog::|FooBar| is not that atom — its text is "foobar", because the
bar syntax only escapes the Common Lisp reader, which is a different question
from what the atom is called. Inside quoted data, where prolog-atom cannot be
called, #.(cl-prolog:prolog-atom "FooBar") reads it at read time.
Proof-search semantics¶
- Registered builtins and foreign predicates are authoritative for their predicate indicator; otherwise the predicate's clauses are resolved in definition order, with facts and rules interleaved as written.
- Rule variables (and variables inside facts) are freshly renamed per use, so bindings never leak between two uses of the same clause.
- An explicit depth bound decrements per user-rule resolution, so left recursion terminates with the solutions found within the bound.
What :max-depth counts¶
Depth decreases when proof enters a user rule, not for every builtin or unification step:
0— facts and builtins only; no rule expansion.1— one level of rule expansion.- larger values — deeper derived proofs.
nil means unbounded. See Querying for the caller's
view and Conditions and Errors for the exhaustion condition.
Cut in detail¶
Cut uses dynamically scoped catch/throw tags:
- Each user-predicate invocation establishes a fresh cut barrier.
!emits the current state and throws to that invocation's tag, abandoning the invocation's remaining alternatives.- Opaque meta-calls establish their own barrier; transparent control constructs deliberately reuse the caller's barrier.
This keeps a rule-body cut local to the predicate invocation while preserving the transparency of control constructs. See Architecture.
Modules¶
Unqualified calls first resolve predicates in the current module and then its
imports. current_predicate/1 follows the same visible-predicate view,
including imported predicates. In a qualified call Module:Goal, Module is
resolved through the current bindings and must become an atom naming an existing
module; violations raise catchable ISO instantiation, type, or existence errors.
Tabling¶
A predicate declared with a :- table name/arity directive resolves through a
per-query memo table instead of plain clause resolution. The engine also detects
left recursion automatically and routes it through the same table, so a
left-recursive definition terminates with the answers reachable within the
query.
- Answers are keyed up to variance, so calls that differ only in variable naming share memoized results.
- The table lives for one public query and is discarded afterward.
- Depth-limited searches and active finite-domain constraints bypass the table where memoization would be unsound.
Tabling has no exported Common Lisp symbols — it is engaged only through the source directive. See Architecture.
Parser resource limits¶
Reading Prolog text is bounded by configurable resource limits (source length,
nesting depth, token count, lexeme sizes, and interned-symbol count). Exceeding
one signals prolog-parser-resource-error from the direct reader APIs, or a
catchable ISO resource_error/1 term when the limit is hit while a
consult/load_files goal runs. See Parser Resource Limits
for the full list of specials and their defaults.
Library-predicate divergences from SWI¶
The library(lists) and library(apply) predicates aim for SWI compatibility
but differ deliberately in a few edge modes:
subtract/3,intersection/3,union/3test membership by unifiability but discard the successful unification's bindings, whereas SWI'smemberchk-based versions propagate them. On ground lists the results are identical; the divergence only shows when a list element is an unbound variable, where these predicates never bind the caller's variable.permutation/2requires at least one argument to be a proper list and raisesinstantiation_errorwhen neither is; SWI's clause-based version can instead loop or partially unify. Results and solution order match SWI for the commonpermutation(+List, -Perm)mode.format/2~eand~gare rendered through the host and then normalized to the C/Prolog exponent convention (3.14e+0); exact digit and exponent-width formatting may differ slightly from SWI.char_type/2andcode_type/2are input-only on the character/code argument (it must be instantiated) — they test or extract, but do not generate a character from a type.char_type/2'sto_lower/to_upper/upper/loweryield a one-character atom (SWI yields a code even here);code_type/2yields codes.graphicis the Prolog symbol-char class (distinct fromgraph), andspace/whiteandend_of_line/newlineare treated as synonyms.aggregate_all/3evaluates thesum/max/mintemplate as an arithmetic expression (sosum(X*2)works), but does not support themax(Expr, Witness)/min(Expr, Witness)witness forms.read_term_from_atom/3validates but ignores its options list (variable_namesetc. are not honored).predsort/3fails (rather than raising) when its comparison predicate has no proof, matching SWI.
ISO conformance¶
t/iso-conformance-test.lisp states the ISO 13211-1 requirements this engine is
checked against as goals in Prolog source text, each citing its subclause, so a
regression names the requirement it broke. The knowingly divergent cases are
asserted there too, so a change of behavior surfaces as a failure rather than
drifting:
- A non-empty
{T}is not'{}'(T)(ISO 6.3.6). It is the engine's internalbraceterm, which DCG bodies are built on. A bare{}is the atom of that name. - A functor is not required to be followed immediately by
((ISO 6.3.3), so a bare+(1,2)reads as the prefix operator+applied to(1,2). The writer compensates by quoting a compound's functor unless it is a plain atom name, which keepswrite_canonical/1output re-readable. ,/2,;/2 and\+/1 are represented internally asand,orandnot, sowrite_canonical((a;b))emitsor(a,b). That reads back as the same term here, but is not portable to another system.occurs_checkdefaults totrue, soX = f(X)fails where ISO would let it build a cyclic term — see below.- The character set is the host's, so Unicode. ISO 6.5 leaves it
implementation-defined; a code past 255 is a character here rather than a
representation_error, which is what lets an atom hold text in any script. The 1999 conformance corpus assumes a 256-character set and expects the error on two of its cases; keeping Unicode is a deliberate choice, asserted int/iso-conformance-test.lisp.
occurs_check and cyclic terms¶
Unification during clause resolution always performs the occurs check, so the
engine never builds a cyclic term behind your back. The occurs_check flag
(default true) controls =/2 and \=/2 specifically: false lets X = f(X)
succeed with a cyclic binding, and error raises when a cycle would form.
Substitution, the standard-order comparison, and the term writer all handle
cyclic structures safely — the writer marks each cons on the current path and
prints ... (compound argument) or |...] (cyclic list tail) on revisiting one,
so a cyclic term prints in bounded time. That ... output is not re-readable,
so a cyclic term written with writeq/write_canonical cannot be read back —
building cyclic terms (only possible under occurs_check=false) trades away
re-readability.
unify_with_occurs_check/2 always checks regardless of the flag. This differs
from SWI, where the flag is global and defaults to false; here the default is
true and the flag is scoped to =/2/\=/2, keeping the core cycle-free by
default (the writer being cycle-safe means the default could be flipped to
false in a future release without introducing a non-terminating print).
Strings¶
A string is a distinct atomic term type. The double_quotes flag
(codes default, chars, atom, string) selects how a "..." literal is
read; it is honored by read_term/2,3, consult/load_files,
read_term_from_atom/3, term_to_atom/2, and term_string/2. In the standard
order of terms strings sort between atoms and compound terms; ==/compare/3
compare string content. atom_string/2 and friends coerce freely between text
kinds. The direct Lisp reader APIs default to codes regardless of any
rulebase flag.
library(assoc) representation¶
library(assoc) associations are keyed by the standard order of terms, but the
representation diverges from SWI: an assoc here is the ordinary, inspectable,
unifiable compound assoc(SortedPairList) (the empty assoc is assoc([])),
not SWI's opaque AVL t/… term. Operations are therefore O(n) rather than
O(log n), and a program can (but should not) inspect or forge the structure —
a malformed assoc(...) is rejected with a catchable type_error. The
implemented subset is empty_assoc/1, get_assoc/3, put_assoc/4,
del_assoc/4, list_to_assoc/2 (which rejects duplicate keys), assoc_to_list/2,
assoc_to_keys/2, assoc_to_values/2; get_assoc/3 is a semidet lookup (it
does not enumerate keys on backtracking).
Builtin output resource limit¶
Builtins that materialize a character run or list from a small input — format
fill/repeat/newline runs (~t, ~|, ~Nc, ~Nn), tab/1, and numlist/3
ranges — are bounded by *max-prolog-builtin-output-length*. Exceeding it
raises a catchable ISO resource_error/1, preventing attacker-controlled
allocation from a tiny query.
See also¶
- Architecture — how cut, guards, and tabling are implemented in the CPS engine.
- Conditions and Errors — the conditions these rules raise.