Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Language Guide

patch-prolog implements an ISO subset of Prolog. A program is a set of facts and rules; you pose queries and the engine searches for proofs, binding variables along the way. This guide teaches the language as this engine implements it — including the places where it makes a deliberate, documented choice. For the complete operator table see the Operators reference; for every builtin and stdlib predicate, the Builtin & Stdlib Reference.

One thing to keep in mind throughout: this is a compiler. Your whole program is known at build time, so there is no assert/retract — the knowledge base is immutable. You change a program by editing its source and recompiling (the REPL’s :edit automates that loop).

Terms

Everything is a term. There are only a few kinds:

KindExamples
Atomtom, [], 'a quoted atom', +
Number42, -7, 3.14
VariableX, Dep, _, _Rest
Compounddepends_on(app, auth), point(1, 2), -(3)

A variable starts with an uppercase letter or _; everything else starting lowercase (or quoted) is an atom. This is the single most common beginner trip-up: Dep is a variable, depends_on is a predicate name.

A list is sugar for nested compounds: [a, b, c] is '.'(a, '.'(b, '.'(c, []))), and [] is the empty-list atom. The [H|T] notation splits a list into its head and tail.

Operators are sugar too: 2 + 3 is the compound +(2, 3), H = [X|T] is =(H, '.'(X, T)). Unification and resolution always see the compound form.

Facts, rules, and clauses

A fact asserts something true:

depends_on(app, auth).
depends_on(auth, crypto).

A rule says a head is true if its body is:

needs(X, Y) :- depends_on(X, Y).
needs(X, Y) :- depends_on(X, Z), needs(Z, Y).

Read :- as “if” and , as “and.” A predicate is identified by name and arity (needs/2), and its clauses are tried top to bottom. Multiple clauses are alternatives — here, needs is a direct dependency or a dependency reached through another.

A directive runs at load/build time:

:- dynamic(extra/1).

A query asks the engine to prove a goal:

./deps --query "needs(app, X)"

The engine searches for variable bindings that make the goal true, yielding solutions one at a time. With depends_on/2 and needs/2 above, needs(app, X) yields X = auth then X = crypto. If a goal can be proved more than one way, backtracking explores each alternative; if it can’t be proved at all, the query fails (no solutions).

Unification

=/2 makes two terms equal by binding variables:

?- point(X, 2) = point(1, Y).
% X = 1, Y = 2

No occurs check (by design). X = f(X) succeeds, producing a cyclic term — matching ISO’s default. Use unify_with_occurs_check/2 when you need the safe version that fails instead.

Related: \=/2 (not unifiable), and ==/2 / \==/2, which test structural identity without bindingX == Y is false for two distinct unbound variables, where X = Y would succeed.

Backtracking

When a goal has several solutions, the engine produces them lazily:

likes(sam, pizza).
likes(sam, sushi).
likes(ann, pizza).
?- likes(sam, Food).
Food = pizza ;
Food = sushi .

Composed goals backtrack jointly — likes(P, pizza), likes(P, sushi) searches for a P satisfying both.

Control constructs

FormMeaning
A, BConjunction — prove A, then B.
(A ; B)Disjunction — A, or B on backtracking.
(Cond -> Then ; Else)If-then-else — if Cond succeeds, commit and prove Then, else Else.
\+ GoalNegation as failure — succeeds iff Goal cannot be proved.
once(Goal)The first solution of Goal only.
call(Goal) / call(G, Extra...)Call a goal built at runtime.

The cut ! commits to the choices made so far in the current clause, discarding remaining alternatives. patch-prolog follows ISO: the cut is transparent through ,, ;, and ->. So with m(1). m(2). m(3).:

cut_demo(X) :- (m(X), X > 1, ! ; X = fallback).

?- cut_demo(X). yields exactly X = 2 — the cut prunes both the rest of m/1 and the ; X = fallback branch. (The cut is opaque inside \+, once, the condition of ->, and catch/3.)

Arithmetic

Arithmetic is explicit: is/2 evaluates its right side, and the comparison operators evaluate both sides.

?- X is 2 + 3 * 4.        % X = 14
?- 6 =:= 2 * 3.           % true   (=:= compares values; = would unify terms)

Division has several forms, and the defaults follow ISO — worth memorizing:

OpResultExample
/always a float (ISO 9.1.4)10 / 33.333…
//integer, truncates toward zero-7 // 2-3
divinteger, floors toward −∞-7 div 2-4
modremainder, sign of divisor-7 mod 32
remremainder, sign of dividend-7 rem 3-1

Other evaluable functions include - abs/1 min/2 max/2 sign/1, the bitwise operators (/\ \/ xor << >> \), and powers (** float, ^ integer-or-float). Use =:= =\= < > =< >= to compare.

Arithmetic is safe, not silent. Integer overflow raises evaluation_error(int_overflow) — it never wraps around. A NaN/infinite float result, or division by zero, is likewise a runtime error.

Lists

Lists use [H|T] to split head from tail, and the stdlib (compiled into every binary) provides the staples:

?- append([1,2], [3,4], L).      % L = [1,2,3,4]
?- member(X, [a,b,c]).           % X = a ; X = b ; X = c
?- length([a,b,c], N).           % N = 3

Also available: reverse/2, last/2, nth0/3, nth1/3. To collect every solution of a goal into a list, use findall/3; to enumerate a range of integers, between/3:

?- findall(D, needs(app, D), Ds).        % Ds = [auth, crypto]
?- between(1, 3, X).                      % X = 1 ; X = 2 ; X = 3

Inspecting and building terms

PredicatePurpose
functor(T, Name, Arity)Term’s functor and arity, or build one.
arg(N, T, A)The Nth argument of a compound.
T =.. List“Univ” — decompose/build a term as `[Functor
copy_term(T, Copy)A fresh copy with renamed variables.

Type tests classify a term: var/1, nonvar/1, atom/1, number/1, integer/1, float/1, compound/1, is_list/1.

Standard order of terms

Terms have a total order — Variables < Numbers < Atoms < Compounds (and a float sorts just before an equal-valued integer). Compare with @<, @>, @=<, @>= or compare/3, and sort with sort/2 (dedupes) or msort/2 (keeps duplicates).

Exceptions

throw/1 raises a term; catch(Goal, Catcher, Recovery) runs Recovery if Goal throws something that unifies with Catcher. Errors use the ISO taxonomy — error(Formal, Context) where Formal is type_error/2, existence_error/2, evaluation_error/1, instantiation_error, and so on.

Calling a predicate that is defined nowhere raises existence_error(procedure, Name/Arity) — catchable, per ISO. Because the compiler sees the whole program, it also warns about such calls at build time (see Compiler Usage and --deny-undefined).

What this subset leaves out

These are deliberate omissions, not gaps to be filled:

  • No assert/retract. The knowledge base is immutable — compiled at build time. :- dynamic(F/A) only makes an undefined predicate fail silently instead of raising existence_error; it does not enable runtime clause mutation.
  • No op/3. The operator table is fixed (see Operators).
  • No postfix operators, no module system, no DCG, no bagof/setof.

Safety guarantees

Every compiled program is bounded: a runaway computation hits a step limit and stops with resource_error(steps) — deliberately uncatchable, so a buggy clause can’t trap its own timeout. Combined with checked arithmetic and no runtime file I/O, a compiled binary is safe to run on untrusted input.