Python
Solving
Solving in the unified Python binding.The Python solving reference continues the Python overview.
Classes and enumerations
| Class | Kind | Role |
|---|---|---|
Session | Resource-owning class | Reuses one selected policy with at most one live result. |
Capability | Enumeration | Bitmask of backend capabilities required when preparing a session. |
ExplanationKind | Enumeration | Bitmask selecting optional reusable explanation storage. |
Methods
| Method | Purpose |
|---|---|
Ruleset.prepare() | Prepare a reusable session |
Ruleset.solve() | Solve with a private session |
Session.solve() | Evaluate one complete request |
Session.close() | Release a prepared session |
Properties and attributes
Read properties without parentheses. Their links lead to the class card defined on this page.
| Property / protocol | Type | Meaning |
|---|---|---|
session.fingerprint | str | Selected-policy authority. |
session.execution_fingerprint | str | Selected policy plus backend and execution configuration. |
Detailed API reference
Every class and method indexed at the start of this page has its detailed card here. A class’s method list also links to related operations documented on other topic pages; those links do not duplicate their cards.
Value-class declarations and public creation signatures below come from the published binding inventory. Resource-owning classes list their methods as individual links with a short explanation. Each link opens the complete signature, arguments, return behavior and example; the creation signatures are a reference, not executable class source. The examples are fragments: engine, ruleset, edb, session and result refer to the live objects explained on this page and in the complete examples. Import the named classes before using them.
Session reference
Ruleset.prepare(
policy_index: int = 0,
*,
required_capabilities: int = 0,
work_limit: int = 0,
explanations: ExplanationKind | int = 0,
) -> Session- Session.close()Release a prepared session
- Session.solve()Evaluate one complete request
with ruleset.prepare() as session:
print(session.fingerprint)
print(session.execution_fingerprint)
with session.solve(edb) as result:
print(result.derived_fact_count())Creating a Session
Obtain this object through the following method. Do not call its internal handle-taking constructor.
| Operation | Purpose |
|---|---|
Ruleset.prepare() | Prepare a reusable session |
Session attributes
Read these attributes without parentheses; they are not callable methods.
| Attribute / protocol | Type | Meaning |
|---|---|---|
session.fingerprint | str | Selected-policy authority. |
session.execution_fingerprint | str | Selected policy plus backend and execution configuration. |
Capability reference
class Capability(IntFlag):
POSITIVE = int(lib.MAELYS_DATALOG_CAP_POSITIVE)
NEGATION = int(lib.MAELYS_DATALOG_CAP_NEGATION)
COMPARISONS = int(lib.MAELYS_DATALOG_CAP_COMPARISONS)
ARITHMETIC = int(lib.MAELYS_DATALOG_CAP_ARITHMETIC)
FILTERS = int(lib.MAELYS_DATALOG_CAP_FILTERS)
EXPLAIN_TRUE = int(lib.MAELYS_DATALOG_CAP_EXPLAIN_TRUE)
WORK_LIMIT = int(lib.MAELYS_DATALOG_CAP_WORK_LIMIT)
EXPLAIN_FALSE = int(lib.MAELYS_DATALOG_CAP_EXPLAIN_FALSE)
AGGREGATES = int(lib.MAELYS_DATALOG_CAP_AGGREGATES)
MIN = int(lib.MAELYS_DATALOG_CAP_MIN)
MAX = int(lib.MAELYS_DATALOG_CAP_MAX)
SUM = int(lib.MAELYS_DATALOG_CAP_SUM)from maelys_datalog import Capability
required = Capability.NEGATION | Capability.EXPLAIN_FALSE
session = ruleset.prepare(required_capabilities=required)ExplanationKind reference
class ExplanationKind(IntFlag):
TRUE = int(lib.MAELYS_DATALOG_EXPLAIN_TRUE)
FALSE = int(lib.MAELYS_DATALOG_EXPLAIN_FALSE)from maelys_datalog import ExplanationKind
session = ruleset.prepare(
explanations=ExplanationKind.TRUE | ExplanationKind.FALSE,
)Prepare a reusable session
Prepare one selected policy once, then evaluate successive complete requests. Optional explanation storage is reserved here, not during policy loading.
Ruleset.prepare(
policy_index: int = 0,
*,
required_capabilities: int = 0,
work_limit: int = 0,
explanations: ExplanationKind | int = 0
) -> SessionSessionReturns a new Session without solving any facts. Invalid options raise Python exceptions; unsupported backend requirements raise MaelysDatalogError. One session may retain only one live result.
from maelys_datalog import ExplanationKind
with ruleset.prepare(
explanations=ExplanationKind.TRUE | ExplanationKind.FALSE,
) as session:
with session.solve(edb) as result:
print(result.contains_fact("allow", ["alice", "roadmap.pdf"]))Solve with a private session
Choose this convenience method for an independent evaluation. For successive requests that share prepared memory, use Ruleset.prepare() instead.
Ruleset.solve(
edb: Edb,
*,
policy_index: int = 0,
required_capabilities: int = 0,
work_limit: int = 0,
explanations: ExplanationKind | int = 0
) -> SolveResultSolveResultReturns a SolveResult owning its private session. Closing the result closes that session. A failed solve raises and publishes no partial result; successful solving freezes the Edb until reset().
with ruleset.solve(edb, policy_index=0) as result:
print(result.contains_fact("allow", ["alice", "roadmap.pdf"]))Evaluate one complete request
Reuse the prepared policy, not the previous request facts. Close the current result before solving again on this session.
Session.solve(
edb: Edb
) -> SolveResultSolveResultReturns a new SolveResult. A live previous result or an Edb from another or closed Ruleset raises RuntimeError. Native solving failures raise MaelysDatalogError without publishing a result.
with session.solve(edb) as result:
allowed = result.contains_fact("allow", ["alice", "roadmap.pdf"])
# The result is now closed; this session can solve the next request.Release a prepared session
Close the session once the prepared policy and its optional explanation workspace are no longer needed.
Session.close() -> NoneNoneReturns None; any active result is closed first, then native session storage is released. Repeated closure is harmless.
session.close()Usage and examples
Evaluate one request first, then reuse a prepared session for repeated requests. The later sections cover counts, execution identity, aggregate requirements and diagnostics.
The reference above gives the exact declarations; the sections below explain their use.
Solve
ruleset.solve(edb, policy_index=0) sends one complete fact batch to the native
facade and returns a SolveResult with its own private session. There is no
partial result on rejection. Several such results may remain open together.
After success the EDB is immutable. Re-solving it is allowed; use a fresh
ruleset.edb() for different facts.
Prepared sessions
ruleset.prepare(policy_index=0) returns a reusable Session. Each
session.solve(edb) starts from the prepared policy and a fresh runtime fact
set. This is not incremental inference: previous derived facts are not
updated by deltas. add_fact() and add_facts() still feed one complete batch.
Close the current result before the next solve on that Session. A failed solve publishes no result and allows a retry with corrected input. Engine, Ruleset, Session and SolveResult support context managers.
from maelys_datalog import (
Capability, Engine, Predicate, PRED_EDB, PRED_IDB, PRED_QUERY,
)
with Engine() as engine:
engine.register_domain("prepared_docs", [
Predicate("user", 1, PRED_EDB),
Predicate("blocked", 1, PRED_EDB),
Predicate("allow", 1, PRED_IDB | PRED_QUERY),
])
ruleset = engine.load_inline_ruleset(
"prepared_docs", "access",
"allow(U) :- user(U), not(blocked(U)).",
)
with ruleset.prepare(required_capabilities=Capability.EXPLAIN_FALSE) as session:
for blocked in (False, True, False):
edb = ruleset.edb()
edb.add_fact("user", ["alice"])
if blocked:
edb.add_fact("blocked", ["alice"])
with session.solve(edb) as result:
print(result.contains_fact("allow", ["alice"]))
if blocked:
explanation = result.explain_false("allow", ["alice"])
print("negative-contradicted" in explanation)
else:
print("status=complete" in result.explain_true("allow", ["alice"]))True
True
False
True
True
TrueRepeated evaluations — complete example
The original reference's minimal example also works with plain-value inputs
in Python. Each ruleset.edb() creates a new independent buffer;
nothing needs to be reset between Alice's and Bob's requests.
from maelys_datalog import Engine, Predicate, PRED_EDB, PRED_IDB, PRED_QUERY
with Engine() as engine:
engine.register_domain('document_access', [
Predicate('owns', 2, PRED_EDB),
Predicate('allow', 2, PRED_IDB | PRED_QUERY),
])
ruleset = engine.load_inline_ruleset(
'document_access', 'doc.main',
'allow(User, Document) :- owns(User, Document).\n')
edb = ruleset.edb()
edb.add_fact('owns', ['alice', 'roadmap.pdf'])
result = ruleset.solve(edb)
print(result.enumerate_predicate_facts('allow', 2))
# [('alice', 'roadmap.pdf')]
# Next evaluation — same ruleset, fresh EDB
edb2 = ruleset.edb()
edb2.add_fact('owns', ['bob', 'roadmap.pdf'])
result2 = ruleset.solve(edb2)
print(result2.enumerate_predicate_facts('allow', 2))
# [('bob', 'roadmap.pdf')][('alice', 'roadmap.pdf')]
[('bob', 'roadmap.pdf')]Both results may coexist: the convenience method creates a private session
for each. After a successful solve the corresponding buffer is closed for
mutation, but may be submitted again unchanged. Use a new buffer for different
facts. The with Engine() block closes all remaining native children on exit.
Derived fact count
result = ruleset.solve(edb)
result.derived_fact_count() # Total IDB facts from this solve.As with WASM's derivedFactCount(),
this is a property of one solve, not a session capacity. A SolveResult
corresponds to one successful solve, so call it on the result you care about,
not on Engine or a mutable “current evaluation”.
result.derived_fact_count() calls the public
maelys_datalog_result_derived_fact_count() getter without solving again.
It counts distinct derived IDB facts across all predicates, including
non-queryable intermediates, but excludes runtime EDB and policy facts.
It can therefore exceed the number of rows enumerated for allow.
Policy and execution fingerprints
| Property | Identity |
|---|---|
ruleset.fingerprint | Loaded policy set |
session.fingerprint, result.fingerprint | Selected policy authority |
session.execution_fingerprint, result.execution_fingerprint | Additionally binds backend identity, required capabilities, effective work-budget setting and size profile |
These native identities describe the policy and its execution configuration. They neither hash the runtime facts nor cryptographically sign an execution.
prepare() and convenience solve() accept required_capabilities
(a Capability bitmask) and work_limit (uint64, default zero).
The selected backend is the built-in reference, with no fallback.
The current reference backend does not support WORK_LIMIT: a nonzero
budget or a WORK_LIMIT requirement fails with UNSUPPORTED.
Zero selects the native default setting; it is not a promised wall-clock
deadline or an enforced reference-solver budget.
Stratified count (0.6.0)
The same count syntax works through load_inline_ruleset. A compiled count program already requires aggregate support, and ruleset.prepare() checks that requirement against the selected backend before preparing a session. Passing required_capabilities=Capability.AGGREGATES makes the requirement explicit even for a program without count; it is not necessary to enable count in the reference backend. The usual typed queries, enumeration and explanation methods read the result. PreparedSession.solve() still evaluates a complete input snapshot. No delta transaction or event window is introduced, and Python/CFFI conversions and output objects still allocate.
Integer minimum, maximum, and sum (0.7.0)
Python accepts the same min, max, and sum syntax as the native parser:
source = """
smallest(User, N) :- user(User), min(Size, document_size(User, _, Size), N).
largest(User, N) :- user(User), max(Size, document_size(User, _, Size), N).
total(User, N) :- user(User), sum(Size, document_size(User, _, Size), N).
"""
ruleset = engine.load_inline_ruleset("documents", "sizes", source)The compiled program records its required operators automatically. Explicit capability requests remain independent:
required = Capability.MIN | Capability.MAX | Capability.SUM
with ruleset.prepare(required_capabilities=required) as session:
with session.solve(edb) as result:
print(result.enumerate_predicate_facts("total", 2))Capability.AGGREGATES still means count only; it does not enable or imply
Capability.MIN, .MAX, or .SUM. The reference backend supports all four,
but a custom backend must advertise each implemented operator.
The result and error contracts are the native ones:
minandmaxrequire at least one matching integer; an empty group derives no output tuple;- an empty
sumbinds zero; sumincludes the projected integer from every distinct complete source fact, so two different events carrying the same integer both contribute;- a matching non-integer or an overflowing sum raises
MaelysDatalogErrorwithINVALID_FIELD, and no partial result is returned; explain_true()may containkind=min,kind=max, orkind=sum, whileexplain_false()may report the matching*-mismatch,min-empty, ormax-emptyobstacle.
Diagnostics
from maelys_datalog import MaelysDatalogError
try:
ruleset = engine.load_inline_ruleset("document_access", "doc.main", bad_source)
except MaelysDatalogError as exc:
print(exc.status) # Operation return status.
print(exc.message, exc.hint) # Human-readable diagnosis.
print(exc.diagnostic.phase) # For example, a parser/input phase.
print(exc.diagnostic.line, exc.diagnostic.column)
print(exc.diagnostic.code) # Diagnostic reason, NOT exc.status.Unlike diagnostics read afterwards from mutable properties on an engine
instance, Python raises the diagnosis with the failing operation. There
is no “last error” left over from a previous call to accidentally reuse.
The original page's .message / .hint explanation still applies, but Next
also preserves source, location and phase in the immutable Diagnostic record.
For classification, use the native facade status/diagnostic contracts rather than matching English message text. A native call with no diagnostic output does not manufacture parser coordinates. An input error may mention the zero-based fact index in its message; that is helpful context, not a stable message grammar to parse.
MaelysDatalogError.status carries the operation's native return status.
error.code remains an alias of that status; message and hint remain
available for existing callers.
error.diagnostic is an immutable Diagnostic containing all native fields:
source, code, phase, line, column, message, hint.
The diagnostic code is distinct from the operation status.
Calls with no diagnostic output leave those fields empty/zero, not invented.
Use coordinates for source errors and the structured fields for classification. Input-batch messages can identify zero-based fact/term indices; their prose is not a stable machine-readable grammar. The binding uses one native-error exception type, not the older binding's specialized registration subclasses.
A successful solve returns a SolveResult. Its membership, enumeration and explanation operations are documented in Querying.