Skip to content

Public API (1.0)

From 1.0.0, Clausal Prolog follows semantic versioning. A change that breaks anything in the covered surface below needs a major release (2.0). Anything internal may change in any minor release.

"Stable" here means two things together: the covered API is frozen, and downstream code built on it keeps working. A release candidate has to go through a 7-day stability window, with no behaviour changes, before it is tagged 1.0.0.

Deprecated forms keep working, with a warning, for the whole 1.x series. They are removed in 2.0.


1. Covered

1.1 The surface language

  • Three source surfaces, told apart by extension (see Clausal Prolog):
    • .clausal: Clausal Prolog, cut-free ISO-like Prolog, always read by the native ISO front end. !, -> and *-> are refused, a module file must end with :- end_module(Name)., and it may not import a .pl module (permission_error(access, prolog_module, M)).
    • .seam: the seam, the Python-syntax surface (the clause forms and directives below). See Syntax.
    • .pl: regular ISO Prolog. Its in-process import is experimental (see Experimental in 1.0).
    • In one directory name.seam beats name.clausal, which beats name.pl. Before the extension flip .clausal was the seam's extension; a seam file still named .clausal now fails to load.
  • The seam's clause forms: facts (head,), rules (head <- body), and test clauses (test("description") <- body, the test/1 predicate). See Testing.
  • The seam directives documented in Directives: -module (including -private and -hide), -import_from, -import_module, -strict_atoms, -implicit_functors, -dynamic, -table, -discontiguous, -meta_predicate, -shallow, -set_prolog_flag, the -constant_value / -constant_number_units family, -allow_singletons, -specialize and the EDCG directives. -double_quotes is transitional (see 2).
  • Export lists: name/arity exports a procedure. A fielded entry edge(A, B) exports a data functor (a term constructor). A fielded entry with no clauses stays data, and calling it raises an existence_error that says so. A bare name exports the atom.
  • The dynamic database is declare-first: assertz/1 and its kin add clauses only to a predicate declared -dynamic. Asserting into any other predicate raises permission_error(modify, static_procedure, Name/Arity). The module flag assert_creates_dynamic (below) selects ISO 7.5.2(2) instead: an assert into a procedure that does not exist creates it as dynamic. It is false in a .seam module and true in a .clausal or imported .pl module.
  • Literal semantics:
    • 'x' and bare x are the atom x.
    • "…" is a string, which is a char list, by default (as in Scryer and Trealla).
    • b"…" is a list of byte codes.
    • Lists accept both [H, *T] and ISO [H|T], which are the same term.
    • Arithmetic on exact numbers stays exact: + - * on integers, rationals and Decimal are exact, / of two exact numbers is rational (7 / 2 is 7/2), and an integral rational is presented as an int (4 / 2 is 2). Mixing a float with a Decimal raises.
    • Arithmetic written as a plain cell, such as ('+', 1, 2), evaluates wherever an arithmetic expression does (eval_/2, the arithmetic comparisons, between/3, #=, ==/!= constraints, CLP(Q) and CLP(R)). The evaluable functors form one closed, static table; any other compound raises type_error(evaluable, Name/Arity).
    • Units and currencies: Quantity values with dimension checking. See Units and Currency.
  • Name classes: a name with a capital initial, or one that starts with _, is a logic variable. A TitleCase name used as a functor is a load-time error.
  • The Python seams in hosted code: ++expr (a Python escape) and --term (a term, or a goal in goal position). Their boundary rules are in 1.4.

1.2 Builtins and error terms

  • The ISO builtins and the error terms they throw, as documented in Builtins and the ISO compatibility report. The quoted ISO names ('is', '=', '==', '=:=', '@<', compare/3, '=..', and the rest) are part of this.
  • Prolog flags: set_prolog_flag/2 and current_prolog_flag/2, the directive -set_prolog_flag/2, and the flag names and values documented in Prolog Flags: the ISO flags bounded, max_integer, min_integer, integer_rounding_function, char_conversion, debug, max_arity, unknown, double_quotes, and assert_creates_dynamic. Which flags are module-scoped is covered too. The set of values a flag can be SET to may grow (unknown = fail, say) in a minor release.
  • The builtin predicates in the Clausal Prolog standard library, by name and arity, as documented under Builtins and the library pages. They are covered as predicates, called from .seam or .clausal source or as a goal cell. The Python objects clausal exports under the same names are not (see 3).
  • Error terms are Scryer's. An error is the plain cell error(Formal, Culprit): Formal is the ISO formal term (type_error(atom, 1), existence_error(procedure, foo/1), …) and Culprit is the predicate indicator of the culprit, or an unbound variable when there is no single culprit. The names inside Formal are atoms. Both the Formal and the Culprit are covered.
  • The explanatory prose that accompanies an error is not part of the term. It is LogicException.message (or None) and is printed after the term. Its text is not covered: messages can improve in a minor release. After a Prolog catch-and-rethrow (catch(G, E, throw(E))) the prose is recovered on a best-effort basis only.

1.3 Python entry points

All of these import from clausal:

from clausal import (
    solve, call, once, query,            # the query API
    query_wfs, Solutions,
    declared_atoms, imported_atoms,      # module introspection
    module_signatures, has_predicate, defines_predicate, module_binds,
    Var, Trail, Module,                  # runtime objects
    deref, unify,                        # term access
    cell_functor, cell_args, make_cell,  # reading and building a cell
    to_python, to_clausal, term_key,     # the converters
    LogicException, UnboundVarCoercionError,
    Quantity, UnitsMismatch,
)
Name Signature Notes
solve solve(goal, module=None, trail=None) -> Iterator[Trail] goal is a cell; module= is required for an unqualified cell. Read bindings inside the loop.
call call(functor, *args, module=None, trail=None) -> Iterator[Trail] Drives one predicate directly. module= and trail= are keyword-only.
once once(goal, module=None, trail=None) -> Trail \| None The first solution, or None.
query query(goal, variables, module=None, trail=None) -> Iterator[dict] Deprecated (DeprecationWarning). Covered through 1.x, removed in 2.0.
query_wfs query_wfs(goal, variables, module=None, trail=None) -> list[dict] Well-founded semantics: each answer dict carries "_truth" (True or Undefined) and "_delays". See WFS.
Solutions Solutions(goal, module=...) The interactive (REPL/notebook) solution iterator and display.
declared_atoms declared_atoms(module_or_package) -> frozenset[str] The atoms the module's own files declare in their -module/-private lists; for a package, also its loaded submodules: the package's atom vocabulary, NOT the attributes bound on the package root (an atom declared only in a submodule is listed although the root does not bind it, so getattr(pkg, name) can be missing; read the root module's own names for that). Excludes -import_fromed atoms and does not depend on import order. Takes a module, a Module or a dotted name (lookup-only, like module=). See Python integration.
imported_atoms imported_atoms(module_or_package) -> dict[str, str] {atom: exporter module name} for the atoms the module's own files (for a package, also its loaded submodules) bring in via -import_from without re-declaring them. Counts a name only when the exporter's own file declares it as an atom (one level, as the import edge resolves it; for a package exporter, its __init__ only), so imported predicates are excluded (a name/N entry always names a predicate and is never counted). Disjoint from declared_atoms. On a clash the later directive in a file that names the atom wins (an alias(x, y) names x), and the package's __init__ wins over its submodules. Same argument forms and errors as declared_atoms. See Python integration.
module_signatures module_signatures(module) -> dict[str, frozenset[int]] {name: arities} for the predicates a module offers to -import_from, keys sorted. For a predicate module (.seam, .clausal, .pl): the predicates its own database holds (clauses, -dynamic, a bare name/N export entry, which is how an imported predicate is re-exported), not what it merely imports, not a fielded data declaration. For a Python-backed module (py.datetime, currency, units, ...): each public predicate adapter with at least one registered arity; a unit or currency constant and a plain function (such as date/3) are not predicates. An adapter whose arity cannot be read (its dispatch takes *args) is listed with an empty frozenset. Takes a module, a Module, or a name spelled as -import_from spells it (py.datetime, date_time, currency), resolved the same way and imported if not loaded (unlike declared_atoms). A name that resolves to no module raises LogicException(existence_error(module, Name)). A package answers for its __init__ only.
has_predicate has_predicate(module, name, arity=None) -> bool True iff calling name/arity through the module resolves to a predicate (any arity when arity is None), whether the module defines it or imports it (from .pl, .clausal, .seam, or by a Python-level import in a facade __init__). False for a data atom and for a name only the .pl attribute fallback answers. The replacement for getattr(mod, name, None) is not None as a predicate probe.
defines_predicate defines_predicate(module, name, arity=None) -> bool True iff the module defines, or imports and exports, the predicate name/arity (any arity when arity is None): the population of module_signatures, with its argument forms. An imported predicate counts only when a name/N entry in the module's export list re-exports it. Works for .seam, .clausal, .pl (both front ends) and package roots. It answers "does the module itself define it"; to ask whether a predicate can be called through the module, use has_predicate.
module_binds module_binds(module, name) -> bool True iff name is a real attribute of the module: in its __dict__, where a definition, an import, a declaration or a native .pl auto-declaration binds it. Takes a module, a Module or a dotted name (lookup-only: a module that is not loaded binds nothing).
cell_functor, cell_args, make_cell cell_functor(c), cell_args(c) -> tuple, make_cell(functor, *args) -> tuple Read and build a compound term (a cell).
to_python to_python(val) Deep conversion out.
to_clausal to_clausal(value) -> Any Deep conversion in. Raises TypeError for an unregistered class.
term_key term_key(term) -> tuple The standard order of terms, as a sort key.
Var Var() .value, and int() / float() / bool() / str() / f-string coercion.
Trail Trail() Pass one explicitly to keep a residual constraint store.
Module Module(name) A logic module. module= also accepts an imported predicate module (.seam, .clausal, .pl) or a dotted name.
Module.declare_dynamic m.declare_dynamic(name, arity) -> None Declares name/arity dynamic, as -dynamic(name/arity) does. Idempotent; ISO dynamic/1 errors. See Database Operations.
deref, unify deref(term), unify(a, b, trail) -> bool
LogicException .term is the thrown term; .message is the prose or None Every throw/1 and ISO error that reaches Python.
UnboundVarCoercionError subclass of TypeError
Quantity, UnitsMismatch The units value and its error.

Three existence questions, three functions (getattr answers none of them on a .pl module, where an unbound atom-shaped name reads as its atom):

  • "is n a predicate I can call through m": has_predicate(m, n). A thin facade that only imports n answers True.
  • "does m itself define n": defines_predicate(m, n). It is False for a predicate a facade only imports without re-exporting it.
  • "is n a real attribute of m (predicate or data)": module_binds(m, n). It is True for a data atom too, so it is no predicate probe.

module= accepts a Module, an imported predicate module, or the module's dotted name as a string. An unqualified cell with no module raises ISO existence_error(module, …).

A module attribute for a predicate (fibonacci.fib) is the predicate's handle, a str. Compare handles with == and never parse them; the spelling is opaque (see 3). A handle is not callable. Build the cell and pass the module.

Also covered:

Import Names
clausal.logic.atoms mint, is_atom, spelling, char_atom (the Python atom API)
clausal.logic.cells chars, is_chars, chars_text (the string carrier)
clausal.logic.seam UndefinedAnswer, ResidualConstraints
clausal.logic.solve query_wfs (the same function as clausal.query_wfs)
clausal.testing load_clausal_module
clausal.import_hook enable_ipython
clausal.lint_warnings the warning classes in 1.6

1.4 The term representation and the Python boundary

  • An atom is a Python str. 'bar' is the atom bar. Compare atoms with ==, never with is.
  • A string is the carrier ('$chars', text). Build one with clausal.logic.cells.chars(text).
  • A compound is a plain cell: a tuple whose first element is the functor name and whose remaining elements are the arguments. For example, ('edge', 'a', 'b'). There is no compound class.
  • The 1-tuple ('x',) is reserved. It is not an atom and must not be built.
  • A list is a Python list.
  • A dict term or a set term reaches Python as an internal object. The covered way to read one is to_python, which gives a dict or a frozenset; the classes themselves (DictTerm, SetTerm) are not covered.
  • A goal from Python is a cell run against a module: solve(('fib', 10, F := Var()), module=m).

The boundary does no conversion unless you ask for it (the "dumb seam"):

  • A goal-position -- seam (if --g(X):, for X in --g(X):, while --g(X):) hands back the engine's raw term: an atom as its str, a string as its carrier, a compound as its cell.
  • ++ passes a value in unconverted. A Python str that comes in through ++ is an atom.
  • To convert, call the converters by name: to_python (deep, out), to_clausal (deep, in), and term_key (the sort key).

See Python integration for the full rules.

1.5 The _get_dispatch protocol

Any Python object with a _get_dispatch() method can stand as a predicate (for call/N, as a goal object, or as a py.* module predicate). This protocol is duck-typed and has out-of-tree implementors, so its signature is frozen:

class MyPredicate:
    def _get_dispatch(self):          # no parameters, ever
        return self._dispatch

    def _dispatch(self, this_generator, proceed, fail, catcher, *args):
        trail = args[-1]              # the arguments, then the trail
        ...
        yield (proceed, None)         # once per solution
        yield (fail, DONE)            # when exhausted

DONE is clausal.logic.trampoline.DONE. The engine never passes an extra argument to _get_dispatch(). When it needs more information, it routes around the method instead.

1.6 Lint warnings

The warning classes and their hierarchy are covered, so a warnings.filterwarnings(…, category=…) keeps working. The message text is not covered. All the classes live in clausal.lint_warnings, and every one is a UserWarning through ClausalLintWarning, so they show by default:

ClausalLintWarning, ClausalSingletonWarning, ClausalCrossModeLiteralWarning, ClausalDeprecatedSpellingWarning, ClausalTitleCaseIdentifierWarning, ClausalKeywordArgumentWarning, ClausalCurrencyLiteralWarning, ClausalScaleInNameWarning, ClausalShadowedVariableWarning, ClausalBooleanSeamWarning, ClausalAtomExportDefinedAsPredicateWarning, ClausalExportArityMismatchWarning, ClausalRetiredQuasiQuoteWarning, ClausalAtomClassDeprecationWarning, ClausalSeamTextCompareWarning, ClausalStringInCatchPatternWarning.

The lint rule for 1.x: a minor release may add a new lint, but a new lint may only warn. Turning a lint into a load-time error, so that code which loaded before is refused, needs a major release.

1.7 The import hook

  • import clausal installs the import hook. After that, a plain import loads a .seam or .clausal file (and, experimentally, a .pl file) on sys.path as a Python module.
  • Each predicate is bound to its handle, and $module holds the logic Module.
  • Bytecode is cached in __pycache__/. The cache format is internal (see 3).
  • CLAUSAL_IPYTHON=1 enables the IPython integration eagerly.

2. Deprecated: works through 1.x, removed in 2.0

Form Replacement Warning
clausal.logic.atoms.atom('x') (the boundary class) 'x'; test with type(v) is str or is_atom(v) ClausalAtomClassDeprecationWarning, once per call site
-double_quotes(atom) / -double_quotes(chars) Drop it: 'x' for a symbol, "x" for text Per-module ratchet. The directive is deleted once no module needs it.
TitleCase unit names (Metre) and the old physical-constant spellings The lowercase / snake_case spelling (metre) ClausalDeprecatedSpellingWarning
query(goal, variables, module) solve(...), reading Var.value DeprecationWarning

Two known gaps in the atom-class deprecation:

  • isinstance(v, atom) does not warn. It is now False for every value the engine produces, so a filter built on it goes silently empty.
  • Unpickling a pickled atom instance warns once, attributed to the pickle.loads call site.

3. Internal (may change in a minor release)

  • Compiler internals (clausal.logic.compiler, clausal.templating, clausal.pythonic_ast), the Database, Clause and PredRow internals, and the trampoline and drive-loop internals, apart from the protocol in 1.5.
  • The spelling of a mangled predicate handle. It is opaque.
  • The C extension ABI.
  • Cache formats, including the predicate-module bytecode cache.
  • The Prolog exporter (clausal.tools.clausal_to_prolog, clausal.tools.prolog_dialect), clausal-fmt and clausal-rewrite.
  • Everything under clausal.tools, clausal.reflection, and any name that starts with _.
  • DictTerm and SetTerm (clausal.terms); read them through to_python (1.4).

Experimental in 1.0

  • Importing .pl files (a plain import of a Prolog source file, see Importing Prolog). It goes through a translator that is being replaced, and what it accepts and how it names things may change in a minor release. This includes ISO 13211-2 end_module/1 and its setting: the require_end_module flag, CLAUSAL_REQUIRE_END_MODULE, and clausal.end_module. Attribute access on a .pl module answers an unbound atom-shaped name with its atom (mod.employment is 'employment'; see Importing Prolog), so getattr/hasattr on a .pl module no longer signals absence; use clausal.has_predicate(mod, name, arity=None) (a predicate you can call through it), clausal.defines_predicate (one it defines itself) or clausal.module_binds(mod, name) (a real attribute) (1.3). Python code can also reach a .pl module's unexported predicates (getattr, mod.name, from mod import name), as with any Python module attribute. That is possible but not supported long-term and not advisable: it may stop working in a future release, so use the module's exported predicates (see Importing Prolog).

Exported by clausal but not covered

The builtin objects with a Python-identifier name (append, between, length, …) are in clausal.__all__ for convenience. Each builds the goal cell for its builtin (between(1, 3, X) is ('between', 1, 3, X)). The predicates are covered by 1.2; the Python objects are internal and may change in a minor release.

Attributes of clausal that are internal

These are attributes of the clausal module but are not in clausal.__all__ and are not covered:

Name What it is Use instead
Database the clause store Module
Clause a stored clause record
structural_unify the unifier behind =, taking a trail unify
get_builtin_class looks up a builtin's object by functor

A builtin whose name is not a Python identifier ('#=', '=..', '@<', …) and the dotted solver predicates (z3.*, ortools.*, pysat.*, clpq.*, clpr.*) are not in clausal.__all__. They are still attributes of the clausal module (getattr(clausal, '#=')) and are callable from .seam and .clausal source as before.

The packages under packages/

The packages under packages/ are versioned independently. Some of them use internal helpers (clausal.modules.py.ModulePredicate, simple_to_trampoline, clausal.logic.builtins._helpers, the exporter), so at the 1.0 release each one pins an exact minor release of Clausal Prolog: a package built for 1.0 requires clausal>=1.0,<1.1, and is re-released for each Clausal Prolog minor.

Not part of 1.0

These were removed before 1.0 (see CHANGELOG.md in the repository):

  • Compound and KWTerm (clausal.terms), with compound_as_cell, list_to_cons, cons_to_list and the builtin extend/3. A compound term is the plain cell (functor, *args).
  • make_predicate and MakePredicateRetiredError. A predicate is a row in its module's database. Write it in a .seam or .clausal module, or implement _get_dispatch (1.5).