Skip to content

Python Integration

Clausal and Python work together seamlessly. From .clausal files, use ++() to call any Python expression. From Python code, use query() to run logic programs and collect results.


++() — Python Escape

The ++() operator evaluates an arbitrary Python expression at search time with logic variables automatically dereferenced.

As a Value

Use ++expr on the right side of ==, :=, or is to compute a Python value:

list_len(L, N) <- (N is ++len(L))
to_upper(S, R) <- (R is ++S.upper())
inc(X, R) <- (R is ++(X + 1))
first(L, R) <- (R is ++L[0])
get_key(D, K, R) <- (R is ++D[K])
join_words(W, R) <- (R is ++", ".join(W))
double_all(L, R) <- (R is ++[x*2 for x in L])

Any valid Python expression works: function calls, method calls, subscripts, comprehensions, arithmetic.

As a Goal

Use ++expr as a standalone goal for side effects:

show(X) <- ++print(X)

when used as a goal, ++() always succeeds once.

Multiple Variables

All logic variables in the expression are dereferenced before evaluation:

add_len(A, B, R) <- (R is ++(len(A) + len(B)))

Per-Solution Evaluation

PyThunk values are evaluated fresh for each solution during backtracking:

Item(1), Item(2), Item(3),
Doubled(R) <- (Item(X), R is ++(X * 2))
Test("doubled") <- Doubled(4)

Querying from Python

Direct iteration — the simplest way

Predicate term instances are directly iterable. Each iteration yields the Trail after a solution — read bindings via Var.value, int(), float(), or str():

from clausal import Var
from fibonacci import Fib

for trail in Fib(10, F := Var()):
    print(F.value)  # 55

The module is inferred automatically from the predicate class. No solve or deref import needed.

Var objects support Python coercion:

Access Behaviour
X.value Dereferenced value (Var if still unbound)
str(X) String of dereferenced value (_N if unbound)
int(X) Integer coercion (raises UnboundVarCoercionError if unbound)
float(X) Float coercion (raises UnboundVarCoercionError if unbound)
bool(X) Bool coercion (raises UnboundVarCoercionError if unbound)
f"{X}" F-string auto-deref

solve — iterate with explicit module

Use solve when you need to pass the module explicitly or when working with non-predicate goal terms:

from clausal import Var, solve
from fibonacci import Fib

for trail in solve(Fib(10, F := Var())):
    print(F.value)  # 55

You can pass the module explicitly (as an imported Python module or a Module object):

import fibonacci
for trail in solve(Fib(10, F := Var()), fibonacci):
    print(F.value)

once — first solution only

from clausal import Var, once
from fibonacci import Fib

trail = once(Fib(10, F := Var()))
if trail is not None:
    print(F.value)  # 55

Returns the Trail for the first solution, or None if the goal fails.

call — drive a named predicate by string

Lowest-overhead path — dispatches directly to the compiled function:

from clausal import Var, call
import fibonacci

for trail in call("Fib", 7, N := Var(), module=fibonacci):
    print(N.value)

query — collect binding dicts (deprecated)

Deprecated

query() is deprecated. Iterate the goal directly and use Var.value: for trail in pred(X := Var()): print(X.value)

from clausal import Var, query
from fibonacci import Fib

F = Var()
for bindings in query(Fib(10, F), {"F": F}):
    print(bindings)  # {"F": 55}

Python Objects as Terms

Any Python object works as a ground term. The C unify function (see Architecture) handles non-Var objects via Python's ==:

import datetime as dt
from clausal.logic.variables import Var, Trail, unify, deref

trail = Trail()
v = Var()
unify(v, dt.date(2026, 3, 16), trail)
deref(v)  # → datetime.date(2026, 3, 16)

This means datetime, Decimal, pathlib.Path, and any other Python type with __eq__ works as a logic term without wrapping. Call methods via ++():

-import_from(date_time, [Date])

IsoDate(Y, M, D, S) <- (
    Date(Y, M, D, DT),
    S is ++DT.isoformat()
)

Using Module Directly

For tests or programmatic use without the import hook:

from clausal.logic.database import Module, Clause
from clausal.logic.predicate import make_predicate
from clausal.logic.compiler import compile_predicate
from clausal.logic.variables import Var

fib = make_predicate("fib", ["n", "result"])
fib._assertz(Clause(head=fib(n=Var(), result=Var()), body=[True]))
compile_predicate("fib", 2, fib._clauses, pred_cls=fib)

mod = Module("test", module_dict={"fib": fib})

Low-Level API Details

The Trail

The Trail (from clausal.logic.variables, a C extension) records variable bindings for backtracking:

from clausal.logic.variables import Trail, Var, unify, deref

trail = Trail()
mark = trail.mark()
v = Var()
unify(v, 42, trail)   # binds v → 42
deref(v)              # → 42
trail.undo(mark)      # undoes the binding
deref(v)              # → v  (unbound again)

Custom Undo Callbacks

trail.record(callable) pushes a no-arg callable for backtrackable mutations:

d = {}
_ABSENT = object()

def trailed_put(key, value, trail):
    old = d.get(key, _ABSENT)
    def undo():
        if old is _ABSENT:
            d.pop(key, None)
        else:
            d[key] = old
    trail.record(undo)
    d[key] = value

Dispatch Lookup Order

call(functor, *args, module) resolves predicates in this order:

  1. module.module_dict[functor]._get_dispatch() — PredicateMeta class from module globals
  2. get_builtin_predicate(functor, arity, db)._get_dispatch() — builtin predicates
  3. module.db.get_dispatch(functor, arity) — Database fallback

structural_unify

clausal.logic.builtins.structural_unify(t1, t2, trail) is a Python-level recursive unifier for Compound, KWTerm, PredicateMeta instances, and @dataclass instances. The C unify handles Var binding, tuples, lists, and atomic equality.

Term Dereferencing

deref(var) unwraps one level. For full recursive dereferencing:

from clausal.logic.solve import _deref_walk

_deref_walk(term)  # recursively dereferences Compound, lists, etc.

Builtin Predicate Classes

Every builtin has a constructable PredicateMeta class:

from clausal.logic.builtins import get_builtin_class

append = get_builtin_class("append")
t = append([1, 2], [3], Var())   # → append(l1=[1, 2], l2=[3], l3=Var())

Multi-arity builtins (maplist, phrase) use MultiArityBuiltin.

assertz/retract from Python

when called from a .clausal module, these builtins:

  1. Check that the target predicate is not locked (see Directives for -dynamic)
  2. assertz/retract the clause on the Database
  3. Look up the PredicateMeta class from db.module_dict
  4. Sync pred_cls._clauses with the database
  5. Recompile with module globals

++() Implementation

The ++ syntax is detected by visit_UnaryOp in term_rewriting.py as UAdd(UAdd(expr)). The compiler emits a _pyt_<id>(deref(...)) call wrapping the expression in a PyThunk lambda.


See also: I/O — write, writeln, f-strings for formatted output. See also: Predicates — defining predicates in .clausal files.