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.plmodule (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.seambeatsname.clausal, which beatsname.pl. Before the extension flip.clausalwas the seam's extension; a seam file still named.clausalnow fails to load.
- The seam's clause forms: facts (
head,), rules (head <- body), and test clauses (test("description") <- body, thetest/1predicate). See Testing. - The seam directives documented in Directives:
-module(including-privateand-hide),-import_from,-import_module,-strict_atoms,-implicit_functors,-dynamic,-table,-discontiguous,-meta_predicate,-shallow,-set_prolog_flag, the-constant_value/-constant_number_unitsfamily,-allow_singletons,-specializeand the EDCG directives.-double_quotesis transitional (see 2). - Export lists:
name/arityexports a procedure. A fielded entryedge(A, B)exports a data functor (a term constructor). A fielded entry with no clauses stays data, and calling it raises anexistence_errorthat says so. A barenameexports the atom. - The dynamic database is declare-first:
assertz/1and its kin add clauses only to a predicate declared-dynamic. Asserting into any other predicate raisespermission_error(modify, static_procedure, Name/Arity). The module flagassert_creates_dynamic(below) selects ISO 7.5.2(2) instead: an assert into a procedure that does not exist creates it as dynamic. It isfalsein a.seammodule andtruein a.clausalor imported.plmodule. - Literal semantics:
'x'and barexare the atomx."…"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 andDecimalare exact,/of two exact numbers is rational (7 / 2is7/2), and an integral rational is presented as anint(4 / 2is2). Mixing a float with aDecimalraises. - 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 raisestype_error(evaluable, Name/Arity). - Units and currencies:
Quantityvalues 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/2andcurrent_prolog_flag/2, the directive-set_prolog_flag/2, and the flag names and values documented in Prolog Flags: the ISO flagsbounded,max_integer,min_integer,integer_rounding_function,char_conversion,debug,max_arity,unknown,double_quotes, andassert_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
.seamor.clausalsource or as a goal cell. The Python objectsclausalexports under the same names are not (see 3). - Error terms are Scryer's. An error is the plain cell
error(Formal, Culprit):Formalis the ISO formal term (type_error(atom, 1),existence_error(procedure, foo/1), …) andCulpritis the predicate indicator of the culprit, or an unbound variable when there is no single culprit. The names insideFormalare atoms. Both theFormaland theCulpritare covered. - The explanatory prose that accompanies an error is not part of the
term. It is
LogicException.message(orNone) 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
na predicate I can call throughm":has_predicate(m, n). A thin facade that only importsnanswers True. - "does
mitself definen":defines_predicate(m, n). It is False for a predicate a facade only imports without re-exporting it. - "is
na real attribute ofm(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 atombar. Compare atoms with==, never withis. - A string is the carrier
('$chars', text). Build one withclausal.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 adictor afrozenset; 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 itsstr, a string as its carrier, a compound as its cell. ++passes a value in unconverted. A Pythonstrthat comes in through++is an atom.- To convert, call the converters by name:
to_python(deep, out),to_clausal(deep, in), andterm_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 clausalinstalls the import hook. After that, a plainimportloads a.seamor.clausalfile (and, experimentally, a.plfile) onsys.pathas a Python module.- Each predicate is bound to its handle, and
$moduleholds the logicModule. - Bytecode is cached in
__pycache__/. The cache format is internal (see 3). CLAUSAL_IPYTHON=1enables 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 nowFalsefor every value the engine produces, so a filter built on it goes silently empty.- Unpickling a pickled
atominstance warns once, attributed to thepickle.loadscall site.
3. Internal (may change in a minor release)¶
- Compiler internals (
clausal.logic.compiler,clausal.templating,clausal.pythonic_ast), theDatabase,ClauseandPredRowinternals, 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-fmtandclausal-rewrite. - Everything under
clausal.tools,clausal.reflection, and any name that starts with_. DictTermandSetTerm(clausal.terms); read them throughto_python(1.4).
Experimental in 1.0¶
- Importing
.plfiles (a plainimportof 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-2end_module/1and its setting: therequire_end_moduleflag,CLAUSAL_REQUIRE_END_MODULE, andclausal.end_module. Attribute access on a.plmodule answers an unbound atom-shaped name with its atom (mod.employmentis'employment'; see Importing Prolog), sogetattr/hasattron a.plmodule no longer signals absence; useclausal.has_predicate(mod, name, arity=None)(a predicate you can call through it),clausal.defines_predicate(one it defines itself) orclausal.module_binds(mod, name)(a real attribute) (1.3). Python code can also reach a.plmodule'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):
CompoundandKWTerm(clausal.terms), withcompound_as_cell,list_to_cons,cons_to_listand the builtinextend/3. A compound term is the plain cell(functor, *args).make_predicateandMakePredicateRetiredError. A predicate is a row in its module's database. Write it in a.seamor.clausalmodule, or implement_get_dispatch(1.5).