Arithmetic¶
Clausal Prolog supports Python's arithmetic operators directly in clause bodies, plus relational arithmetic predicates that work in multiple directions.
Quick Example¶
factorial(0, 1),
factorial(N, F) <- (
N > 0,
N1 == N - 1,
factorial(N1, F1),
F == N * F1
)
test("fact 5") <- factorial(5, 120)
Evaluation & Comparison¶
The == operator¶
== posts an arithmetic constraint (CLP(ℤ) or CLP(ℝ)) that works in all directions:
Supported operators: +, -, *, /, // (integer division), % (modulo),
** (power). The other evaluable functors (abs(), min(), max(),
round(), sqrt(), ... below) are evaluated when their operands are ground;
the constraint does not propagate through them, so X == max(Y, 3) with Y
unbound raises domain_error(clpz_expression, max(_, 3)) (Scryer's clpz posts
abs, min and max as constraints; this engine does not yet).
A bare operator keeps Python's meaning in today's syntax (-7 // 2 is -4,
2 ** 3 is 8); its quoted spelling follows Scryer Prolog ('//'(-7, 2) is
-3, '**'(2, 3) is 8.0). Operators has the whole table.
eval_/2 — eager evaluation¶
eval_(EXPR, RESULT) evaluates EXPR immediately and unifies the value with
RESULT (Prolog's is/2):
Prefer == for ordinary relational arithmetic — it works in all directions.
EXPR is evaluated the way ISO is/2 evaluates:
- A variable is evaluated at runtime, whatever it is bound to — a number,
or an arithmetic term such as
+(1, 2)built byunpack/2(=..) orfunctor/3.unpack(T, ['+', 1, 2]), eval_(T, X)givesX = 3. - An unbound variable raises
instantiation_error. - An atom, a string or a compound that is not evaluable raises
type_error(evaluable, Name/Arity)—eval_(foo(1), X)raisestype_error(evaluable, foo/1). Before 2026-09-27 these were handed back unevaluated (X = foo(1)), a silent wrong answer. - A Python value — a
Quantity, adate, a list, the result of a++escape or of a qualified Python call such asmath.sqrt(X)— keeps Python's own operators, as before:eval_(L + [3], X)concatenates andeval_(++("%d") % 5, X)formats. - A Python
strthat reacheseval_through a variable or a qualified name (os.sep) is an atom — an atom is its str — and raises like any atom:F is ++("%d items"), eval_(F % N, X)raisestype_error(evaluable, '%d items'/0). Do string work in the escape:X is ++(F % N).
Reach for eval_/2 when you specifically need eager evaluation:
- Unit-carrying values —
eval_(20(metre), D),eval_(D / T, V); CLP constraints don't operate onQuantityobjects. - Catchable exceptions —
catch(eval_(X // Y, R), E, ...)seeserror(evaluation_error(zero_divisor), (//)/2)whenYis 0. A constraint never raises it: over a zero divisor it simply fails. - Accumulator recursion — an eagerly ground argument keeps tail-recursive predicates eligible for tail-call optimisation.
Choosing an arithmetic idiom:
| Goal | Use |
|---|---|
| Relational arithmetic, any direction | X == EXPR |
Eager arithmetic (Prolog is/2) |
eval_(EXPR, X) |
| Structural unification (no evaluation) | X is TERM |
| Arbitrary Python expression | X is ++(PYEXPR) |
X is TERM builds TERM without evaluating it, but a qualified Python
call in it is made (X is math.sqrt(16) binds 4.0, M is
torch.nn.Linear(10, 5) a module). A qualified predicate adapter is not a
Python call: X is torch.tensor([1, 2]), where torch.tensor is the
adapter's tensor/2 predicate, raises type_error(evaluable,
'torch.tensor'/1) -- the term eval_ gives for it -- instead of binding the
compound 'torch.tensor'([1, 2]). The same holds for an adapter call in an
ARITHMETIC position of a side -- an operand of + - * / // % ** or unary
-, or an argument of an evaluable function such as abs/1 or max/2,
at any depth: X is 1 + torch.tensor_sum(L) raises too. Anywhere else it
still builds its compound: in a list (X is [torch.tensor(L)]), a tuple,
or a data or goal term (G is f(m.p(A))). The comparisons ==, !=,
<, <=, >, >= check their sides the same way and name themselves in
the error (1 < 1 + torch.tensor_sum(L) raises type_error(evaluable,
'torch.tensor_sum'/1) with context (<)/2); so does the test of if_/3,
and an is or comparison built as a goal term (G is (X < m.p(A)))
raises where it is built. eval_/2, the quoted ISO forms ('is', '<'
...) and {...} raised already. Call the predicate as a goal
(tensor([1, 2], X)). The bare imported spelling, G is match(P, S) after
-import_from(py.re, [match]), still builds a goal for call/1.
Arithmetic terms built at runtime — the evaluable table¶
Arithmetic written in source (1 + 2) and arithmetic built as a term at
runtime (unpack(T, ['+', 1, 2])) are both evaluable. eval_/2, the ISO
'is'/'=:='/'<'… builtins, ==/!=/</<= (CLP(ℤ)),
clpq.rational/1, clpr.real/1 and between/3 all accept both spellings. A
cell evaluates through one table, keyed by name and arity; the table is
closed — there is no way to register a new evaluable functor. Its functors
are builtins: in scope in every module without a declaration
('is'(X, rdiv(7, 2)), 'is'(X, '//'(-7, 2))), and ordinary terms as data.
Operator rulings of 2026-09-28: the table is the Scryer meaning of each
functor, which a quoted spelling and a runtime-built cell get. A bare operator
written in source keeps Python's meaning, and the two differ for // and **
(see Operators):
| Term | Meaning (Scryer) | Bare source operator (Python) |
|---|---|---|
+(A, B), -(A, B), *(A, B) |
exact (a Decimal keeps its scale) |
A + B, A - B, A * B: the same |
/(A, B) |
division as a float: '/'(7, 2) is 3.5, '/'(6, 2) is 3.0 |
A / B: Python true division, 7 / 2 is 3.5, 6 / 2 is 3.0 |
rdiv(A, B) |
exact rational division: rdiv(7, 2) is 7/2, rdiv(6, 2) is 3 |
— |
//(A, B) |
integer division truncating toward zero: '//'(-7, 2) is -3 |
A // B floors: -7 // 2 is -4 |
div(A, B) |
integer division rounded toward negative infinity: -4 | — |
mod(A, B) |
integer modulo, sign of the divisor | A % B: Python modulo (the same on integers) |
**(A, B) |
power as a float: '**'(2, 3) is 8.0 |
A ** B: Python power, 2 ** 3 is the integer 8 |
^(A, B) |
integer power: '^'(2, 3) is 8 |
— (A ^ B is Python's XOR, for CLP(B)) |
-(A) |
negation | -A: the same |
abs(A) |
absolute value, in the operand's own kind: abs(-3) is 3, abs(-3.5) is 3.5 |
— |
min(A, B), max(A, B) |
the smaller / larger operand, in its own kind (max(2, 5) is 5); beside a float the operands compare as floats and a tie answers the float (max(1, 1.0) is 1.0) |
— |
+(A) |
the number itself | +A: not evaluable (a Python unary plus) |
sign(A) |
-1, 0 or 1 in the operand's kind: sign(-2.5) is -1.0; an exact rational's sign is an integer |
— |
rem(A, B) |
the remainder of '//', the sign of the dividend: rem(-7, 2) is -1; integers only |
— |
truncate(A), round(A), ceiling(A), floor(A) |
an integer from any number: truncate(-3.7) is -3; round halves away from zero (round(-2.5) is -3), exactly |
— |
float(A) |
the operand as a float: float(rdiv(7, 2)) is 3.5 |
— |
float_integer_part(A), float_fractional_part(A) |
the integer part and the rest, as floats: -3.0 and -0.75 for -3.75 | — |
gcd(A, B) |
greatest common divisor, non-negative; integers only | — |
sqrt, sin, cos, tan, asin, acos, atan, exp, log (arity 1) |
a float; an exact operand is taken at its value. sqrt(-1), log(0), log(-1), asin(2) are evaluation_error(undefined); exp(1000) is evaluation_error(float_overflow) |
— |
atan2(Y, X), atan(Y, X) |
the angle of the point (X, Y), a float; atan2(0, 0) is evaluation_error(undefined) |
— |
pi, e |
the constants, as floats ('is'(X, pi)); no declaration needed in arithmetic position, ordinary atoms as data (see Operators) |
— |
'>>'(A, N), '<<'(A, N) |
arithmetic shifts; a negative count shifts the other way ('<<'(1, -1) is 0); integers only |
A >> N, A << N: Python shifts, not evaluable |
'/\\'(A, B), '\\/'(A, B), '\\'(A), xor(A, B) |
bitwise and, or, complement, exclusive or (two's complement: '\\'(5) is -6); integers only |
&, \|, ~, ^: Python's, for CLP(B); not evaluable |
//, div and mod take integers only (type_error(integer, 7.0) for
'//'(7.0, 2)), as in Scryer. ^ follows Scryer's rules for a negative
exponent: '^'(2, -1) is type_error(float, 2), '^'(1, -1) is 1, and
'^'(0, -1) is evaluation_error(undefined); an exact rational base stays
exact ('^'(rdiv(1, 2), 2) is 1/4, where Scryer answers 0.25). rem, gcd
and the bitwise functors take integers only, too (type_error(integer, 1.0)
for xor(12, 1.0)), and an integral rational counts as an integer
(gcd(rdiv(12, 1), 18) is 6; Scryer keeps 12 rdiv 1 a rational and
refuses it). The table is ISO's evaluables (ISO 13211-1 9.1.7, 9.3, 9.4 and
Cor.2) with Scryer's kinds and errors, and two ISO readings where Scryer
differs: log(0) is evaluation_error(undefined) (Scryer:
float_overflow), and atan/2 is evaluable, as atan2/2 (Scryer has only
atan2/2). integer/1 and log/2 are not in the table (nor in ISO or
Scryer). The exact-number term decimal(M, S) evaluates as the number it
denotes.
test("rounding") <- ('is'(A, round(-2.5)), A == -3, 'is'(B, truncate(3.7)), B == 3)
test("rem and gcd") <- ('is'(R, rem(-7, 2)), R == -1, 'is'(G, gcd(12, 18)), G == 6)
test("bitwise") <- ('is'(X, '/\\'(12, 10)), X == 8, 'is'(Y, '>>'(-7, 1)), Y == -4)
test("float functions") <- ('is'(S, sqrt(4)), S == 2.0, 'is'(P, pi), P > 3.14)
Division (ruling Q15, 2026-09-28). In evaluation — eval_/2, 'is',
the ISO comparisons — / is a float division for two integers: bare it is
Python's (7 / 2 is 3.5, 6 / 2 is 3.0), quoted it is Scryer's (the same
floats). A bare / keeps exact kinds exact where Python does: a Fraction
over an int is a Fraction, a Decimal over an int is Python's Decimal
quotient (rounded at 28 digits). A Decimal beside a Fraction divides
exactly; a float beside either raises type_error(exact_number, F). The
quoted '/' always answers a float. rdiv is the exact spelling:
rdiv(7, 2) is 7/2 and rdiv(1, 3) + 1 is 4/3, and it never rounds.
Inside a constraint (==, !=, <, ...) / is rational, as before:
X == 7 / 2 gives 7/2 and X == 6 / 2 gives 3.
A zero divisor in plain arithmetic raises
evaluation_error(zero_divisor) naming the operator, on every spelling, bare or
quoted: eval_(1 / 0, X), 'is'(X, 1 // 0) and '=:='(1, 1 / 0) all raise it.
A constraint is a relation (ruling Q14, 2026-09-28): over an expression
with no value — a zero divisor, or '^'(2, -1) — it has no solutions and
fails, as Scryer's clpz does, in every goal order. X == 1 // 0,
(X == 1 // Y, Y is 0) and X != 1 // 0 all fail; in a search
(X == 10 // Y, in_domain(Y, 0, 2), label([Y])) the branch Y = 0 fails and
the search goes on to Y = 1 and Y = 2; reified, as in
if_(X == 1 // 0, ...), the test is false.
In a CLP(ℤ) constraint (==, !=, <, ...), a term that is not arithmetic
raises Scryer's domain_error(clpz_expression, T): X == foo(1) raises
error(domain_error(clpz_expression, foo(1)), (==)/2). So does the float
power '**', which is not a clpz expression, over a CLP(ℤ) variable
(X == '**'(Y, 2)) or nested in a CLP(ℤ) expression (X == Y + '**'(2, 3));
use '^' there. A ground '**'(2, 3) as a whole side of the comparison is
simply the float 8.0.
Comparison operators¶
Both sides must be ground (bound to numbers) at evaluation time.
| Operator | Meaning |
|---|---|
< |
less than |
> |
greater than |
<= |
less than or equal |
>= |
greater than or equal |
== |
equal |
!= |
not equal |
Relational Arithmetic Predicates¶
These work in multiple argument modes — give any two arguments and the third is computed.
plus/3¶
plus(X, Y, Z) — Z = X + Y. Any two arguments determine the third.
test("plus forward") <- plus(3, 4, 7)
test("plus subtract") <- (plus(3, Y, 7), Y == 4)
test("plus other") <- (plus(X, 4, 7), X == 3)
succ/2¶
succ(X, Y) — Y = X + 1 for non-negative integers. Bidirectional.
Range Generation¶
between/3¶
between(Low, High, X) — generate or test integers in a range (inclusive).
between is nondeterministic in generate mode — it yields each integer on
backtracking.
Numeric Functions¶
abs_/2¶
abs_(X, Y) — Y is the absolute value of X.
sign/2¶
sign(X, S) — S is -1, 0, or 1 depending on the sign of X.
test("sign negative") <- sign(-42, -1)
test("sign zero") <- sign(0, 0)
test("sign positive") <- sign(99, 1)
max_/3, min_/3¶
max_(X, Y, Z) / min_(X, Y, Z) — Z is the maximum/minimum of X and Y.
gcd/3¶
gcd(X, Y, G) — G is the greatest common divisor of X and Y.
divmod_/4¶
divmod_(X, Y, Quotient, Remainder) — integer division and modulo in one step.
Patterns & Recipes¶
Fibonacci with accumulator¶
fib(N, F) <- fib_acc(N, 0, 1, F)
fib_acc(0, A, _, A),
fib_acc(N, A, B, F) <- (
N > 0,
N1 == N - 1,
C == A + B,
fib_acc(N1, B, C, F)
)
test("fib 10") <- fib(10, 55)
Collatz sequence length¶
collatz(1, 0),
collatz(N, STEPS) <- (
N > 1,
MOD == N % 2,
MOD == 0,
HALF == N // 2,
collatz(HALF, S),
STEPS == S + 1
)
collatz(N, STEPS) <- (
N > 1,
MOD == N % 2,
MOD == 1,
NEXT == 3 * N + 1,
collatz(NEXT, S),
STEPS == S + 1
)
test("collatz 6") <- collatz(6, 8)
sum_ of digits¶
digit_sum(0, 0),
digit_sum(N, SUM) <- (
N > 0,
divmod_(N, 10, REST, DIGIT),
digit_sum(REST, S),
SUM == S + DIGIT
)
test("digit sum") <- digit_sum(123, 6)
Gotchas¶
==posts a constraint —X == 3 + 4constrains X to 7 and works even when X is unbound. Useeval_/2only when you need eager evaluation (units, catchable exceptions, accumulator recursion), and++for arbitrary Python such as string operations.- Both sides of comparisons must be ground —
X > 3fails ifXis unbound. Use CLP(ℤ) for constraints over unbound variables. plus/3requires at least two bound arguments — it cannot enumerate all solutions toplus(X, Y, 10).- Integer division —
//floors like Python's (-7 // 2is -4); the quoted'//'(A, B)truncates like Prolog's (-3)./is a float in evaluation (eval_(7 / 2, X)is 3.5) and rational inside a constraint;rdivis exact everywhere.
See also: CLP(ℤ) — arithmetic constraints over integers, Type Checking — integer, float_, number.