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:
when used as a goal, ++() always succeeds once.
Multiple Variables¶
All logic variables in the expression are dereferenced before evaluation:
Per-Solution Evaluation¶
PyThunk values are evaluated fresh for each solution during backtracking:
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):
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 ++():
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:
module.module_dict[functor]._get_dispatch()— PredicateMeta class from module globalsget_builtin_predicate(functor, arity, db)._get_dispatch()— builtin predicatesmodule.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:
- Check that the target predicate is not locked (see Directives for
-dynamic) - assertz/retract the clause on the Database
- Look up the PredicateMeta class from
db.module_dict - Sync
pred_cls._clauseswith the database - 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.