Python

Python

Use the unified Python binding built on the installed opaque C SDK.

Maelys DL exposes a native Python binding for in-process use — no browser or WASM runtime. It targets research pipelines, offline analysis, and long-running services: Python exceptions represent errors, explicit objects represent policy and session lifetimes, and integer terms retain the full native int64 range.

The underlying concepts — EDB facts, predicate kinds, solving and querying — are the same as in C and WASM, documented in the Concepts pages. See Bindings overview for the cross-language map.

These topic pages preserve the progression of the historical Python V1 reference: installation, loading, domains, ownership, term encoding, queries, explanations, and the API reference. They add manifest loading, prepared sessions, Why-false and explicit execution requirements.

The ordinary sequence remains: declare the domain, load a policy, collect facts, solve, and inspect the result. The CFFI extension calls the opaque C facade, never an internal engine structure.

Public Python objects

Python exposes classes and value objects rather than C handles and free functions. The links lead directly to each class card or flag reference. The complete method index below is organized by class. Each topic page lists only the reference cards defined there.

Object / constantRole
CapabilityBitmask of backend capabilities required when preparing a session.
DiagnosticImmutable native detail record, separate from the operation status.
EdbOwns the mutable input facts for one complete request.
EngineOwns Python wrappers and their native children; registers and loads domains.
ExplanationKindBitmask selecting optional reusable explanation storage.
LimitsImmutable snapshot of native build capacities.
MaelysDatalogErrorException carrying a native status and structured diagnostic.
PredicateImmutable declaration of one predicate; construction does not register it.
PRED_EDBInteger predicate-role flag; not a method or class.
PRED_IDBInteger predicate-role flag; not a method or class.
PRED_POLICY_FACTInteger predicate-role flag; not a method or class.
PRED_QUERYInteger predicate-role flag; not a method or class.
RulesetOwns one loaded policy set and creates input buffers and sessions.
ResultTermImmutable raw term tied to its live result.
SessionReuses one selected policy with at most one live result.
SolveResultRetains a solved model for queries, enumeration and explanations.
StatusNamed native operation statuses, used when handling exceptions.

The binding also exports Status, Capability, ExplanationKind and the PRED_* flags. Predicate roles are explained in Registries, capability and explanation masks in Solving and Querying, and native failure statuses in Errors.

Main methods by task

Explore by topic

API reference

This is the complete cross-topic method index, grouped by its Python class. Each link opens the method's single reference card. On the topic pages, the tables are local indexes rather than complete class inventories.

Edb methods

MethodPurpose
Edb.add_fact()Append one fact
Edb.add_facts()Append an atomic batch
Edb.clear()Discard an unsolved batch
Edb.close()Release input storage
Edb.reset()Start a new request in the same buffer

Engine methods

MethodPurpose
Engine.close()Close the Python owner
Engine.load_inline_ruleset()Load a policy from text
Engine.load_manifest()Load verified policy files
Engine.register_domain()Register the complete domain vocabulary before loading policies.

Predicate methods

MethodPurpose
Predicate.edb()Create a runtime-input declaration with PRED_EDB.
Predicate.edb_query()Create a runtime-input declaration with query permission.
Predicate.idb()Create a derived-fact declaration with PRED_IDB.
Predicate.idb_query()Create a derived-fact declaration with query permission.
Predicate.policy_fact()Create a policy-source fact declaration.
Predicate.policy_fact_query()Create a policy-source fact declaration with query permission.

ResultTerm methods

MethodPurpose
ResultTerm.resolve()Resolve this raw term

Ruleset methods

MethodPurpose
Ruleset.close()Release the loaded policy set
Ruleset.edb()Create an input buffer
Ruleset.prepare()Prepare a reusable session
Ruleset.solve()Solve with a private session

Session methods

MethodPurpose
Session.close()Release a prepared session
Session.solve()Evaluate one complete request

SolveResult methods

MethodPurpose
SolveResult.close()Release a solved result
SolveResult.contains_fact()Check one concrete fact
SolveResult.derived_fact_count()Count the derived facts
SolveResult.enumerate_predicate_facts()Copy resolved result rows
SolveResult.enumerate_raw()Read result-owned term views
SolveResult.explain_false()Explain an absent fact
SolveResult.explain_true()Explain a derived fact
SolveResult.resolve_term()Resolve a term through its owner

Constructors

Class construction is separate from domain registration or solving.

ConstructorResultPurpose
Engine()EngineCreate the Python owner; no domain is registered yet.
Predicate(name, arity, flags)PredicateBuild a declaration; registration validates it later.

Properties and protocols

These are read-only attributes or Python protocols, not additional engine calls. Class-card links explain their types and ownership.

Property / protocolTypeMeaning
engine.limitsLimitsImmutable snapshot of build capacities.
ruleset.policy_countintNumber of loaded policies; valid indices run from 0 to count minus 1.
ruleset.fingerprintstrIdentity of the whole loaded policy set, not a hash of request facts.
len(edb)intNumber of stored entries before deduplication; Python length protocol, not an Edb.count() method.
session.fingerprintstrSelected-policy authority.
session.execution_fingerprintstrSelected policy plus backend and execution configuration.
result.fingerprintstrSelected-policy authority.
result.execution_fingerprintstrSelected policy plus execution configuration.
term.kindstrsymbol, integer or boolean.
term.valueint | boolResult-local symbol ID, integer or boolean according to kind.
error.statusintNative operation status; compare with Status members.
error.codeintAlias of error.status; not the diagnostic reason code.
error.messagestrHuman-readable detail.
error.hintstrSuggested corrective action when supplied.
error.diagnosticDiagnosticStructured native detail, including its distinct reason code and presence mask.

Installation

Python 3.10 or later, CFFI, setuptools, a C11 compiler and CMake 3.16+ are required. The binding lives in the engine repository, not on a package registry. Install the native SDK first, then compile the CFFI extension against that same SDK prefix. From the v0.11.1 source checkout:

CODE
python3 -m venv build/python-venv
build/python-venv/bin/python -m pip install cffi setuptools pytest
cmake -S . -B build/cmake-small
cmake --build build/cmake-small --parallel 4
cmake --install build/cmake-small --prefix "$PWD/build/sdk-small"
build/python-venv/bin/python bindings/python/build_cffi.py \
  --sdk-prefix "$PWD/build/sdk-small"
MAELYS_DATALOG_SDK_PREFIX="$PWD/build/sdk-small" \
MAELYS_DATALOG_EXPECT_PROFILE=small PYTHONPATH=bindings/python \
  build/python-venv/bin/python -m pytest -q tests/python

The builder copies the installed shared library beside the Python extension. --build-dir and --engine-dir belonged to the experiment and are removed; --sdk-prefix proves that headers and library come from one installation. For LARGE, configure a separate build with -DMAELYS_DATALOG_PROFILE_LARGE=ON, install it to a separate prefix, rebuild the extension and start a fresh interpreter. Inspect engine.limits and the expected-profile check to verify which library was actually loaded.

Native library discovery

A compiled Python extension is itself a dynamic library. It depends on libmaelys_datalog_shared (the engine), not the historical libmaelys_py_bind shim. At import time, the operating system must find that dependency; PYTHONPATH alone only helps Python find its package.

The CFFI builder copies the selected engine library beside the package. On Linux the resulting layout is:

CODE
bindings/python/maelys_datalog/
├── __init__.py
├── engine.py
├── _maelys_cffi....so           ← compiled extension
└── libmaelys_datalog_shared.so  ← copied from --sdk-prefix

On macOS the native library uses .dylib; the extension still uses Python's platform-specific suffix. Python first finds maelys_datalog through its module path, then the OS resolves the extension's native dependency. $ORIGIN on Linux and @loader_path on macOS record a loader-relative search path: look beside the extension, not in the original build directory. Keep the extension and library together. No LD_LIBRARY_PATH is needed for this layout; if import fails, check their origin and profile before changing environment variables.

CODE
# check_import.py
import maelys_datalog
print("import OK")
CODE
PYTHONPATH=/path/to/maelys-datalog/bindings/python python check_import.py

Native call path

The loading choices above are separate from the library layers below. Both supported choices cross the same public C boundary:

  1. Python API
    maelys_datalog
  2. CFFI extension
    generated bridge
  3. Opaque C facade
    maelys/datalog.h
  4. Native engine
    same inference semantics

There is no handwritten maelys_py_bind layer in this path. CFFI still generates a native extension; that extension links to libmaelys_datalog_shared. Opaque handles hide implementation details, not features: engine.limits and result.derived_fact_count() are exposed through public C getters.

Both inline source and native manifests are supported. load_inline_ruleset() compiles one policy; load_manifest() preserves native SHA verification, query whitelists and multi-policy selection. The generated bridge includes only <maelys/datalog.h>, never backend descriptors or private engine structures.

One consumer header, a separate extension API

datalog.h supplies the opaque handles, capability constants, session configuration and execution fingerprint used by this binding. datalog_backend.h is for C developers implementing an alternative execution backend; Python does not include it.

When you call ruleset.prepare(required_capabilities=..., work_limit=...), the binding creates an opaque native configuration, sets those values, creates the session and frees the temporary configuration. Session creation copies the values, so the session does not depend on that configuration's lifetime. Python code never supplies a backend ABI version or a C structure size.

This changes the native integration, not the Python method signatures. Unsupported requirements still fail explicitly: in particular, the reference backend rejects a nonzero work budget rather than pretending to enforce it.

Session lifecycle

1. Enginewith Engine() as engine

Python owner of policy wrappers and an immutable snapshot of native build limits.

2. Domainengine.register_domain(...)

Declare the vocabulary in the process-wide registry; it is not owned by an Engine.

3. Rulesetengine.load_inline_ruleset(...)

Own an opaque compiled-policy handle, not an exposed C struct or mutable symbol table.

4. EDB bufferedb.add_fact(...) / edb.add_facts(...)

Own copied input values in an opaque native EDB; additions check storage bounds without running the solver.

5. Session and resultresult = ruleset.solve(edb)

Each solve creates a native session, submits one complete batch and retains the result for queries.

new EDB for each evaluation
6. Closeresult.close() or leaving with Engine()

Release each result before its session, then release its policy. Closing Engine closes remaining native children.

Ruleset owns an opaque compiled-policy handle — not Engine, which owns the Python wrappers and the build-limit snapshot. Multiple rulesets can be loaded and closed independently within a process. Unlike those handles, the domain registry remains shared and survives Engine closure.

Each successful ruleset.solve() creates its own opaque session and result. The diagram above describes this convenience path. For repeated evaluation, ruleset.prepare() returns an explicit reusable Session with one live result at a time; see prepared sessions. Several results can coexist. After success, the EDB cannot be mutated, but it can be solved again to produce another independent result. Use a new ruleset.edb() for different inputs.

result.close() frees its result and, for the convenience path, its private session. With an explicit Session, closing the result leaves the session reusable. session.close() closes its live result first. ruleset.close() closes all results, sessions and EDBs before releasing the policy. The with Engine() block closes all remaining children even on exceptions. Objects used after their parent closes raise RuntimeError; query results must be read while their owner remains open.

The Python Edb owns an opaque native input EDB handle. Its idempotent close() frees that storage; it does not invalidate an already computed result. Closing its Ruleset prevents later use. Close buffers you no longer need to release native input memory, especially in long-running processes.

Python API walkthrough

Five owner/value layers structure this walkthrough: Engine owns Python Rulesets and reads build limits; Ruleset owns a compiled policy set; Edb buffers one evaluation's inputs; Session holds prepared native execution state; SolveResult retains one solve's model. The convenience solve() creates its Session for you. The sequence is the same as the original reference, but insertion timing and symbol ownership change.

Threading model

The old binding used a process-wide registry lock plus per-Ruleset symbol locks. Those locks protected particular operations; they did not make every object safe to share. Python makes the ownership rule explicit instead: an Engine and all its handles are confined to the thread that created it. The wrapper checks this before entering native code, including on close().

Do not rely on the Python interpreter lock to serialize access to native objects, and do not move an Engine into a worker after creating it in the main thread. Create the Engine inside that worker. Independent workers can own independent Engines and rulesets, while domain registration and policy loading remain serialized by the wrapper's process-wide registry lock.

That lock covers calls made through this binding. It is not a cross-language synchronization protocol for arbitrary C code mutating the same registry. Creating another Engine also does not create or unregister a domain: the vocabulary remains process-wide, as described under Domain definition.

Within one owner thread, a prepared Session permits one live result. Close that result before solving again. Independent convenience solves create independent sessions and may keep several results open. These are lifecycle rules, not an invitation to use the handles concurrently on other threads.

Use context managers or explicit close(): the prototype has no GC finalizer. A parent closes native children in result → session → EDB → policy order, and later use of a closed child raises RuntimeError. A second close() on a wrapper is harmless on its owner thread; it is not permission to race close with use.

Engine feature coverage

The following coverage is verified against the current code, not inferred from a roadmap. This is the sole source binding shipped with the current engine; it is not a separately published Python package.

CapabilityPython
Standard language, including negation, positive disjunction and wildcardPolicy source
Standard ground filters starts_with, ends_with, containsPolicy source; visible in native explanations
Manifest SHA checks, query whitelist, policy-local vocabularyengine.load_manifest()
Multiple policiesruleset.policy_count, policy_index
Reusable prepared stateruleset.prepare()
Build limits and total derived countengine.limits, result.derived_fact_count()
Resolved and result-owned raw enumerationenumerate_predicate_facts(), enumerate_raw()
Why-true / Why-falseexplain_true(), explain_false()
Policy and execution identityDistinct fingerprint properties
Full native diagnosticserror.diagnostic
Explicit capability requirementsrequired_capabilities; rejection without fallback
Work-budget requestExposed, but rejected by the current reference backend
True delta/incremental inferenceNot implemented by the current engine API
Custom native frontend/backend/planner/filter registrations and IR buildersSeparate extension SDK; no Python callback bridge here

Opaque handles do not preclude extensions. However, registering native callbacks is a different API from using the engine's standard filters: this binding does not manufacture a dynamic loader or Python callback ABI.

Migration from the deprecated Python V1 binding

SurfaceDeprecated 0.9.1 Python V1Current maelys_datalog
Native boundaryPython-specific C shimPublic opaque C facade
add_fact()Native insertionAtomic native opaque-buffer insertion
add_facts()Not availableAtomic native batch append; complete evaluation at solve
Native input errorsUsually at insertionStorage checks at addition; policy-specific checks at solve
Declaring text constants for inline policiesNo Python atoms parameterregister_domain(..., atoms=[...])
Input symbol IDsRuleset-scoped intern_symbolNot accepted; use strings
Raw enumerationAvailableResult-owned, lifetime-checked views
Why-trueexplain_fact_text()explain_true()
Why-falseNot wrappedexplain_false()
Manifest loadingNot wrappedload_manifest()
Prepared-session reuseDifferent legacy lifecycleExplicit prepare()
Native diagnostic detailBinding-specific errorsImmutable complete Diagnostic

The deprecated binding's missing atoms parameter does not mean that its engine cannot handle constants: that Python registration method simply does not expose the vocabulary parameter. Python makes it explicit, as explained under domain definition.

The old binding is not shipped in 0.10.0. Keeping the same import is not a complete migration: review buffering, symbol ownership, method names and lifetimes.

Lifecycle and configuration notes

Engine and domain

Ruleset and EDB buffer

Prepared Session

  • session.solve(edb) — Solve one full batch. Close the previous result first; this is not delta/incremental inference.
  • session.fingerprint — Identity of selected policy authority.
  • session.execution_fingerprint — Identity also binding backend, execution requirements/budget setting and size profile.
  • Capability / required_capabilities — Add explicit required capabilities; unsupported requirements are rejected without fallback.
  • work_limit — A uint64 cooperative-work request, not a deadline. Nonzero values are rejected by the current reference backend.
  • session.close() / with session — Close its live result then the session; no effect on independent sessions.
  • ExplanationKind — Select optional shared explanation storage when preparing a session; this is distinct from capability requirements.

SolveResult and raw terms

Exceptions