Python

Solving

Solving in the unified Python binding.

The Python solving reference continues the Python overview.

Classes and enumerations

ClassKindRole
SessionResource-owning classReuses one selected policy with at most one live result.
CapabilityEnumerationBitmask of backend capabilities required when preparing a session.
ExplanationKindEnumerationBitmask selecting optional reusable explanation storage.

Methods

MethodPurpose
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 / protocolTypeMeaning
session.fingerprintstrSelected-policy authority.
session.execution_fingerprintstrSelected 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

Python · class
Session
Creation
Ruleset.prepare(
    policy_index: int = 0,
    *,
    required_capabilities: int = 0,
    work_limit: int = 0,
    explanations: ExplanationKind | int = 0,
) -> Session
Reusable selected-policy owner returned by ruleset.prepare(); do not construct it from native handles. fingerprint and execution_fingerprint are read-only str properties, accessed without parentheses. Only one result may remain live at a time.
Methods
Example
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.

OperationPurpose
Ruleset.prepare()Prepare a reusable session

Session attributes

Read these attributes without parentheses; they are not callable methods.

Attribute / protocolTypeMeaning
session.fingerprintstrSelected-policy authority.
session.execution_fingerprintstrSelected policy plus backend and execution configuration.

Capability reference

Python · enum
Capability
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)
IntFlag values combined with | to require backend features at session preparation. Operators used by a compiled policy are detected automatically. AGGREGATES means count only; MIN, MAX and SUM are independent. WORK_LIMIT is unsupported by the reference backend.
Example
from maelys_datalog import Capability

required = Capability.NEGATION | Capability.EXPLAIN_FALSE
session = ruleset.prepare(required_capabilities=required)

ExplanationKind reference

Python · enum
ExplanationKind
class ExplanationKind(IntFlag):
    TRUE = int(lib.MAELYS_DATALOG_EXPLAIN_TRUE)
    FALSE = int(lib.MAELYS_DATALOG_EXPLAIN_FALSE)
IntFlag selecting optional reusable session explanation storage. TRUE | FALSE uses one shared workspace, not two. Zero reserves none; when a workspace is selected, requesting an omitted kind is rejected without allocating a fallback.
Example
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.

Python · method
Ruleset.prepare
Ruleset.prepare(
policy_index: int = 0,
*,
required_capabilities: int = 0,
work_limit: int = 0,
explanations: ExplanationKind | int = 0
) -> Session
Prepares one selected policy for reuse across requests, with the chosen session options. Preparation does not yet supply facts or compute an answer.
Arguments
policy_indexint
Zero-based index in this loaded policy set. Default: 0. Out-of-range indices raise IndexError.
required_capabilitiesint
Capability bitmask required of the backend, not a switch that enables language operators. Default: 0.
work_limitint
Unsigned 64-bit budget setting. Default: 0. The current reference backend rejects nonzero budgets with UNSUPPORTED.
explanationsExplanationKind | int
ExplanationKind mask for a reusable native workspace. Default: 0, no reserved explanation workspace. TRUE | FALSE shares one workspace.
Return value
Session

Returns 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.

Example
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.

Python · method
Ruleset.solve
Ruleset.solve(
edb: Edb,
*,
policy_index: int = 0,
required_capabilities: int = 0,
work_limit: int = 0,
explanations: ExplanationKind | int = 0
) -> SolveResult
Evaluates one complete input batch using a private session created for this call. Use prepare() when the same session should be reused for multiple requests.
Arguments
edbEdb
Open input Edb belonging to this same Ruleset; the entire stored batch is evaluated.
policy_indexint
Zero-based index in this loaded policy set. Default: 0. Out-of-range indices raise IndexError.
required_capabilitiesint
Capability bitmask required of the backend, not a switch that enables language operators. Default: 0.
work_limitint
Unsigned 64-bit budget setting. Default: 0. The current reference backend rejects nonzero budgets with UNSUPPORTED.
explanationsExplanationKind | int
ExplanationKind mask for a reusable native workspace. Default: 0, no reserved explanation workspace. TRUE | FALSE shares one workspace.
Return value
SolveResult

Returns 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().

Example
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.

Python · method
Session.solve
Session.solve(
edb: Edb
) -> SolveResult
Evaluates a complete input batch using this prepared session. Release its previous result before solving another request; facts are not implicitly inherited.
Arguments
edbEdb
Open input Edb belonging to this same Ruleset; the entire stored batch is evaluated.
Return value
SolveResult

Returns 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.

Example
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.

Python · method
Session.close
Session.close() -> None
Closes the prepared session and any active Python result it owns.
Arguments
Return value
None

Returns None; any active result is closed first, then native session storage is released. Repeated closure is harmless.

Example
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.

CODE
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"]))
CODE
True
True
False
True
True
True

Repeated 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.

CODE
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')]
CODE
[('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

CODE
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

PropertyIdentity
ruleset.fingerprintLoaded policy set
session.fingerprint, result.fingerprintSelected policy authority
session.execution_fingerprint, result.execution_fingerprintAdditionally 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:

CODE
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:

CODE
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:

  • min and max require at least one matching integer; an empty group derives no output tuple;
  • an empty sum binds zero;
  • sum includes 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 MaelysDatalogError with INVALID_FIELD, and no partial result is returned;
  • explain_true() may contain kind=min, kind=max, or kind=sum, while explain_false() may report the matching *-mismatch, min-empty, or max-empty obstacle.

Diagnostics

CODE
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.