Skip to content

IPython / Jupyter REPL

Clausal has first-class IPython integration that turns an IPython session into a Prolog-style REPL. Queries use a *(goals) syntax, uppercase names are automatically treated as logic variables, and solutions are presented interactively one at a time — separated by or, just as Clausal's disjunction operator reads. For notebook-specific rendering, see Jupyter Notebooks.


Quick start

After setting up the IPython startup hook (one-time, per profile):

# Cell 1 — load a module
import clausal.examples.sudoku as sudoku

# Cell 2 — query
*(sudoku.Problem(1, ROWS), sudoku.Sudoku(ROWS))

ROWS is declared automatically as a fresh logic variable — no Var() needed.


Set up the IPython startup hook

IPython parses each cell before executing any code in it, so the AST transformer that rewrites -import_from(...) and *(...) queries has to be registered before your first cell runs. The cleanest way is an IPython startup file that imports Clausal once at session start.

clausal-install-ipython

This drops a single file at ~/.ipython/profile_default/startup/00-clausal.py. Every IPython session in that profile picks it up automatically; you can delete the file to opt back out. Useful flags:

clausal-install-ipython --profile myprofile   # different IPython profile
clausal-install-ipython --force               # overwrite existing file
clausal-install-ipython --uninstall           # remove the hook
clausal-install-ipython --print               # print the script to stdout

Manual — write the file yourself

If you'd rather not run the install command, the hook is a five-line file you can copy in by hand:

# ~/.ipython/profile_default/startup/00-clausal.py
try:
    from clausal.import_hook import enable_ipython as _enable_clausal
    _enable_clausal(get_ipython().user_ns)
except ImportError:
    pass

Per-session — call enable_ipython from cell 1

Without a startup file, every session needs an explicit setup cell:

from clausal.import_hook import enable_ipython
enable_ipython(globals())

After that, subsequent cells can use *(...) and -import_from(...).

Environment-variable opt-in (advanced)

Setting CLAUSAL_IPYTHON=1 makes import clausal itself register the AST transformer — handy if your workflow already imports clausal at the top of every session, but on its own it doesn't help: until something imports clausal, IPython has no way to know about the integration. Use this in combination with a startup file or a --ext IPython extension that imports clausal during startup:

# ~/.zshrc or ~/.bashrc
export CLAUSAL_IPYTHON=1

Query syntax

There are two ways to query in IPython:

Plain Python iteration

Since predicate terms are directly iterable, you can use a standard for loop:

from clausal import Var
from hello import greeting

for trail in greeting(X := Var()):
    print(X.value)

This works identically in plain Python scripts and IPython.

*(goals) — IPython-specific shorthand

The *(goals) form is an IPython-specific shorthand that auto-declares uppercase variables and displays solutions interactively. It is rewritten at the text level into a valid Python call before compilation (for compatibility with Python 3.13+).

The entire expression inside *(...) is treated as a clause body, not ordinary Python. Operators that have special meaning in Python are rewritten into Clausal goals:

Inside *(...) Goal constructed
X is Y Unify(left=X, right=Y)
X == Y ArithEq(left=X, right=Y)
X != Y ArithNeq(left=X, right=Y)
X < Y Lt(left=X, right=Y)
A and B And(left=A, right=B)
A or B Or(left=A, right=B)

Single goal

*(Solve(ROWS))

Conjunction — comma-separated

*(Problem(1, ROWS), Sudoku(ROWS))

Multiple comma-separated goals are folded into a left-associative And chain and share a single solver context for correct backtracking.

Unification goal

*(X is ["asdfa", 1, Y, some_term])

is inside *(...) is unification, not Python identity.


Variable auto-declaration

Any uppercase name inside a *(...) query is automatically allocated as a fresh Var() via a walrus assignment embedded in the goal expression itself:

*(Member(X, [1, 2, 3]), X > 1)
# X is allocated as Var() automatically

This mirrors Prolog's convention that uppercase identifiers are variables. If you need to share a variable across multiple cells, declare it explicitly with X = Var() before the query.


Interactive solution browsing

Solutions are presented one at a time with an or separator between them, matching Clausal's disjunction syntax:

ROWS = [[1, 5, 6, ...], ...]
   [SPACE/n: next  |  ENTER/.: stop  |  ESC/q: abort  |  a: all]

Key bindings follow standard Prolog REPL conventions:

Key Action
SPACE, n next solution
ENTER, . stop (commit to current solution)
ESC, q abort (no output)
a show all remaining solutions

Terminal colours

in_ a terminal IPython session (ipython command, not Jupyter), ANSI colours are enabled automatically. Unbound variables, atoms, numbers, strings, and brackets each get a distinct colour with rainbow bracket-depth cycling.

Control the style from any cell using the injected helpers:

# Disable colours
set_style(TermStyle())

# Re-enable default colours
set_style(TermStyle(colors=ANSI_COLORS))

# Change the anonymous-variable symbol (default: '_')
set_style(TermStyle(anon_var='?'))

# Custom colour scheme
set_style(TermStyle(colors={
    'number':   '\033[33m',
    'string':   '\033[32m',
    'atom':     '\033[36m',
    'var':      '\033[35m',
    'brackets': ['\033[91m', '\033[93m', '\033[92m', '\033[96m', '\033[94m', '\033[95m'],
    'reset':    '\033[0m',
}))

set_style, TermStyle, and ANSI_COLORS are automatically injected into the IPython namespace by enable_ipython. Colours are not enabled in Jupyter kernels, which render output as HTML rather than a terminal.


Using Solutions directly

For programmatic use, wrap any iterator of binding dicts:

from clausal import Var, call, Solutions
from clausal.logic.variables import walk, Trail

ROWS = Var()
trail = Trail()

def gen():
    for _ in call(sudoku.Problem, 1, ROWS, trail=trail):
        for _ in call(sudoku.Solve, ROWS, trail=trail):
            yield {'ROWS': walk(ROWS)}

Solutions(gen())

Solutions also accepts a predicate instance directly:

ROWS = Var()
Solutions(sudoku.Sudoku(ROWS))

call with predicate classes

call() accepts a predicate class directly — no module= argument needed:

from clausal import call

for _ in call(sudoku.Solve, ROWS):
    print(walk(ROWS))

String functor names still work when a module is provided:

for _ in call("Solve", ROWS, module=mod):
    ...