Python V1 · Deprecated

Registries

Registries in the Python V1 binding.

The Python V1 registries reference continues the Python V1 overview.

Factory

CODE
from maelys_datalog import Engine

engine = Engine()

Engine() loads the native library and returns a ready instance. Unlike the WASM binding's MaelysPlayground.create(), there is no factory module or WASM URL to pass — the shared libraries are already resolved through $ORIGIN/@loader_path (see Installation) before Python code runs at all.

More precisely: import maelys_datalog is what loads the compiled cffi extension and its two native dependencies. At import it also runs a one-time ABI self-check: it asks the compiled shim for its real struct sizes, field offsets, and enum values (maelys_py_get_abi_layout / maelys_py_get_abi_constants) and compares them against what the Python-side cffi definitions expect. A shim rebuilt out of sync with the Python package therefore fails loudly at import rather than silently misreading fields later. All of that has already happened by the time Engine() is even called. Engine() itself only creates the native engine handle and reads the build limits into .limits (see Build limits below); it does not repeat any loading or ABI work.

Build limits

CODE
engine = Engine()
engine.limits
# BuildLimits(max_symbols=..., string_pool_bytes=..., max_predicates=...,
#             max_rules=..., max_arity=..., max_body_literals=...,
#             max_depth=..., max_edb_facts=..., max_idb_facts=...,
#             max_facts_per_pred=...)

These ten fields are also available in WASM's buildLimits(); Wasm additionally exposes string and input-text bounds. Python V1 reads its limits once at Engine() construction — the active build profile's observable capacities, compile-time constants, not runtime occupancy. Unlike WASM, there is no separate call to make later: .limits is already populated by the time Engine() returns.

Predicate kind flags

CODE
from maelys_datalog import PRED_EDB, PRED_IDB, PRED_QUERY, PRED_POLICY_FACT

PRED_EDB         # 1 — runtime fact supplied by the caller
PRED_IDB         # 2 — derived by Datalog rules
PRED_QUERY       # 4 — inspectable through the query API
PRED_POLICY_FACT # 8 — declared by the policy source

Same bit values as WASM's PredKind. These constants are read from the engine's ABI constants at import time, not written as literals in the Python source — so if the engine ever changed a flag value, the Python constant would track it automatically instead of silently disagreeing with the native side. Combine with |: PRED_IDB | PRED_QUERY.

Domain definition

Unlike the WASM dynamic builder — which is scoped to one MaelysPlayground instance (see WASM bindings — Domain definition) — the native domain registry is process-wide. Every Engine instance in the same Python process shares it. Creating a second Engine() does not give you a second, isolated domain space — unlike WASM, where a new MaelysPlayground.create() gets its own WASM module and its own linear memory, so the same domain name can be declared again independently:

CODE
from maelys_datalog import Engine, Predicate, PRED_EDB, PRED_IDB, PRED_QUERY

engine1 = Engine()
engine1.register_domain('document_access', [        # writes into the process-wide registry
    Predicate('owns',  2, PRED_EDB),
    Predicate('allow', 2, PRED_IDB | PRED_QUERY),
])

engine2 = Engine()                                  # same process, same registry
engine2.register_domain('document_access', [        # 'document_access' already exists:
    Predicate('owns',  2, PRED_EDB),                # a safe no-op when the schema is
    Predicate('allow', 2, PRED_IDB | PRED_QUERY),   # identical — nothing new is created
])

The current register_domain method declares predicates, not the string atoms used as literals in policy source. A literal such as "confidential" must belong to the domain's declared atom allowlist; merely interning a runtime string does not declare a policy atom.

This Python registration method has no atoms argument. Unless a native domain has declared the literal, loading such a policy fails explicitly with an unknown-atom diagnostic; it does not silently produce a non-matching rule. Runtime strings supplied through edb.add_fact() are a different case and do not need to be listed as policy literals.

For low-cardinality categories, is_confidential(Document) can be a useful alternative model to classification(Document, "confidential"). For explicit Python-side atom declaration, see Python Next's domain vocabulary.

In practice this rarely bites, because the two common cases are already fine:

CODE
# Case 1: re-running the same cell — by far the most common notebook
# workflow. Safe no-op, nothing to worry about.
engine.register_domain('document_access', [
    Predicate('owns', 2, PRED_EDB),
    Predicate('allow', 2, PRED_IDB | PRED_QUERY),
])
engine.register_domain('document_access', [
    Predicate('owns', 2, PRED_EDB),
    Predicate('allow', 2, PRED_IDB | PRED_QUERY),
])
# no error — identical table, safe no-op

# Case 2: changing the schema while iterating — raises, but the fix is
# the same "Restart Kernel" you would already reach for after changing
# imports or clearing corrupted variable state.
engine.register_domain('document_access', [
    Predicate('owns', 2, PRED_EDB),
    Predicate('allow', 3, PRED_IDB | PRED_QUERY),   # arity changed 2 -> 3
])
# DomainAlreadyRegisteredError — restart the kernel, then re-run

The capacity applies to distinct registered domain names. Re-registering an identical schema consumes no slot; re-registering the same name with a different schema raises an error and does not allocate another slot. A service that invents a new domain name for every reload will eventually exhaust the 16 slots. Reuse a stable vocabulary, or restart the process when you intentionally change its definition.