Theory

Algorithms & formal foundations

Maelys DL is an embedded Datalog micro-kernel for security policy decisions. Its design follows the classical database-theory view of Datalog: a policy is a finite logical program, inputs are finite relations, and positive rules reach a least fixpoint. Negation and aggregates read completed lower layers.

Maelys is designed and developed by David Bromberg. The foundations below are established work; the Maelys contribution is their concrete integration into a bounded, auditable C runtime and the combined contracts described here, not a claim of priority for those algorithms. This page follows engine 0.11.1; its source and test links are pinned to v0.11.1. See Related work for the comparison with Soufflé, PUG, Binder and Biscuit.

FOUNDATION

Datalog as a least-fixpoint model

Foundations: Ullman, 1989 · Abiteboul, Hull & Vianu, 1995Complexity: Finite fixpoint over bounded facts
Maelys design

For positive Datalog, evaluation starts from base facts and applies rules until no new fact is produced: the least fixpoint. With stratified negation and aggregates, the engine completes each lower layer before evaluating the next. Maelys implements fail-closed loading and solving: an error is not a successful negative answer, and an incomplete solve is not published as a result. The embedding application must authorize only after successful solving and a successful positive query.

Evidence (v0.11.1): implementation · regression tests

Invariants
  • ▸EDB facts are runtime input facts supplied by the caller
  • ▸POLICY_FACT facts are trusted facts embedded in the policy
  • ▸IDB facts are derived by rule evaluation
  • ▸Positive rules are monotone and can only add facts
  • ▸Negation is handled through stratification, not as negative facts
  • ▸Each layer reaches its positive fixpoint before a higher layer reads it
  • ▸No new derived fact after an iteration means the fixpoint is reached
pseudocode
# Positive rules within one evaluation layer.
facts = EDB ∪ POLICY_FACTS

repeat:
  new_facts = {}

  for each rule H :- B1, ..., Bn:
    for each substitution θ satisfying B1θ, ..., Bnθ:
      new_facts += Hθ

  delta = new_facts - facts
  facts = facts ∪ delta

until delta = ∅

return facts
EVALUATOR

Semi-naive evaluation

Foundations: Bancilhon & Ramakrishnan, 1986Complexity: Reduces redundant derivations; deduplication still applies
Maelys design

Naive bottom-up evaluation repeats work on old facts. Semi-naive evaluation requires each iterative rule variant to read at least one newly derived IDB fact. The pseudocode below separates old facts and delta conceptually. In Maelys, delta is a range inside the accepted current prefix of one bounded pool; merge is the appended candidate range. These are not three independently allocated copies.

Evidence (v0.11.1): implementation · regression tests

Invariants
  • ▸The accepted current prefix includes the previous-round delta range
  • ▸idb_delta contains facts derived in the previous round
  • ▸idb_merge collects facts newly derived in the current round
  • ▸Each variant chooses one delta-eligible IDB body literal
  • ▸EDB and POLICY_FACT literals never scan delta
  • ▸Fixpoint is reached when idb_delta becomes empty
pseudocode
idb_current = {}
idb_delta   = base_derivation(rules, EDB ∪ POLICY_FACTS)

while idb_delta ≠ ∅:
  idb_merge = {}

  for each rule r:
    for each delta-eligible IDB literal i:
      derive r with:
        body[i]     read from idb_delta
        other IDBs  read from idb_current ∪ idb_delta
        base atoms  read from EDB ∪ POLICY_FACTS

      append only genuinely new facts to idb_merge

  idb_current = idb_current ∪ idb_delta
  idb_delta   = idb_merge
PLANNER

Static join-order planning

Maelys design

Rule evaluation is a sequence of joins. A bad order can multiply intermediate combinations. Maelys uses a deterministic greedy heuristic with bounded masks: it favors bound terms and constants, imposes a delta pivot for semi-naive variants, and selects only safe literals. This is a Maelys implementation choice, not a new general join-optimization algorithm or an optimality guarantee. The complexity below describes the built-in planner, not arbitrary extension callbacks or the joins themselves.

Evidence (v0.11.1): implementation · regression tests

Invariants
  • ▸The delta literal is forced to join_order[0] for semi-naive variants
  • ▸Positive relational atoms bind variables
  • ▸A ready aggregate also binds its result variable
  • ▸Comparisons and negated atoms are constraints, not binders
  • ▸A literal is safe only when all required variables are already bound
  • ▸Orphan variables fail closed at plan time
  • ▸Tie-breaking uses original body index for deterministic output
pseudocode
planned = {}
bound_vars = {}
order = []

if semi_naive_variant:
  delta = selected_delta_literal
  require delta is a positive IDB atom
  order.push(delta)
  planned += delta
  bound_vars += vars(delta)

while order.size < body.size:
  candidates = safe_literals(body - planned, bound_vars)

  if candidates = ∅:
    FAIL CLOSED

  best = max_score(candidates)
  order.push(best)
  planned += best

  bound_vars += variables_bound_by(best)

return order
NEGATION

Stratified negation

Foundations: Apt, Blair & Walker, 1988 · Abiteboul, Hull & Vianu, 1995Complexity: Bounded iterative stratum assignment
Maelys design

Negation is not monotone: adding a fact can invalidate an earlier absence test. Stratification requires a negated relation to be complete before it is read. Maelys also gives aggregate dependencies a strict downward edge: a counted or summed relation must not depend on the result of that aggregate. Positive recursion is allowed within the lower layer; cycles through negation or aggregation are rejected.

Evidence (v0.11.1): implementation · regression tests

Invariants
  • ▸Positive dependency: stratum(head) ≥ stratum(body)
  • ▸Negative dependency: stratum(head) > stratum(body)
  • ▸Aggregate dependency: stratum(head) > stratum(source)
  • ▸EDB and POLICY_FACT predicates are treated as stratum 0 inputs
  • ▸Recursion through negation is rejected
  • ▸Negative cycles are rejected before evaluation
  • ▸A non-stratifiable policy fails closed
pseudocode
for each predicate p:
  stratum[p] = 0

repeat up to MAX_PREDICATES:
  changed = false

  for each rule r:
    required = stratum[r.head]

    for each body literal L:
      s = stratum[L.predicate]

      if L is negative or an aggregate:
        s = s + 1

      required = max(required, s)

    if required > stratum[r.head]:
      stratum[r.head] = required
      changed = true

  if !changed:
    return strata

FAIL CLOSED
NEGATION

Frozen stratum windows

Maelys design

A negative literal reads a complete lower relation, as required by stratification. The Maelys representation keeps derived facts in one bounded flat pool and records the boundaries of each completed stratum. Sorted slices support membership checks without duplicating the pool. These internal stratum windows are not the last-N event windows described below.

Evidence (v0.11.1): implementation · regression tests

Invariants
  • ▸A stratum is evaluated only after all lower strata are frozen
  • ▸Negative lookup never reads an actively mutating window
  • ▸Each frozen window is sorted and deduplicated
  • ▸The IDB pool is partitioned by boundaries, not duplicated
  • ▸For IDB, not(p(t)) checks absence in its frozen lower window; base predicates read their completed input relation
pseudocode
start[0] = 0

for s in 0..max_stratum:
  evaluate all rules whose head is in stratum s
  sort facts derived in stratum s
  deduplicate facts derived in stratum s
  end[s] = current_idb_end
  frozen[s] = true
  start[s + 1] = end[s]

negative_lookup(p, tuple):
  if p is EDB or POLICY_FACT:
    return !base_contains(p, tuple)
  s = stratum[p]
  require frozen[s]
  return !binary_search(idb_facts[start[s]..end[s]], p(tuple))
STORAGE

Sorted-array fact store

Maelys design

Maelys stores bounded fact sets in arrays, with a canonical comparator and adjacent deduplication after sorting. Its typed sort is an introsort: median-of-three partitioning, insertion sort for small segments, and a heapsort fallback when the depth budget is exhausted. Only the smaller partition recurses. The adaptation avoids allocator calls and variable-length arrays; introsort itself is an established algorithm. Why-false additionally uses a separate context-aware heapsort.

Evidence (v0.11.1): implementation · regression tests

Invariants
  • ▸Facts have a canonical lexicographic order
  • ▸The comparison key is predicate id, arity, then terms
  • ▸Lookup uses binary search once a window is sorted
  • ▸Deduplication is a linear pass after sorting
  • ▸Windows are tracked by index ranges rather than heap containers
  • ▸Prepared reference-session append/solve/query/release avoids engine allocator calls; construction and other paths have separate contracts
pseudocode
fact_key(f) = (
  f.predicate_id,
  f.arity,
  f.terms[0],
  f.terms[1],
  ...
)

append_unsorted(window, fact)

freeze(window):
  sort(window, by=fact_key)
  deduplicate_adjacent_equal_facts(window)

contains(window, query):
  require window.sorted
  return binary_search(window, query)
PROOF

Bounded Why-true witness graphs

Foundations: Datalog provenance · Zhao, Subotić & Scholz, 2020Complexity: Bounded proof nodes, truncation non-fatal
Maelys design

Why-true explains one retained derivation of a present IDB fact. Each step records its rule and lexical premises, including positive facts, negative absence, comparisons, filters and aggregates. IDB premises link to earlier steps, and shared parents are emitted once: the result is a bounded directed acyclic witness graph, not simply a single-parent chain. This is not an enumeration of every derivation or a guarantee of the smallest explanation.

Evidence (v0.11.1): implementation · regression tests

Invariants
  • ▸Proof nodes are recorded in lockstep with derived IDB facts
  • ▸Each derived fact can carry a proof index
  • ▸Parent links describe the derivation dependencies
  • ▸Sorting and deduplication preserve fact/proof pairing
  • ▸Extraction emits ancestors before descendants and deduplicates shared parents
  • ▸Truncation is non-fatal and does not change decision semantics
  • ▸The canonical document exposes one bounded witness graph, not all possible proofs
pseudocode
derive fact F by rule R using parents P:
  proof_index = proof_add(
    derived_fact = F,
    rule_id      = R.id,
    parents      = P
  )

  idb_facts.append(F)
  idb_proof_index.append(proof_index)

extract_proof_for_fact(F):
  root = proof_index_for(F)
  graph = ancestors_first_dag(root, deduplicate_shared_parents=true)
  remap parent indexes locally
  return graph
EXPLANATION

Bounded Why-false exploration

Foundations: Why-not provenance · Lee, Ludäscher & Glavic, 2018Complexity: Explicit bounds on exploration and output
Maelys design

Why-false starts from an absent query and explores candidate rules against the completed result. It reports obstacles such as a missing positive fact, a present negated fact, a failed comparison, filter or aggregate condition. PUG is related work on why/why-not provenance, not a claim that Maelys implements the PUG algorithm. Maelys keeps this diagnostic exploration separate from solving and bounds its candidates, branches, depth and output.

Evidence (v0.11.1): implementation · regression tests

Invariants
  • ▸Membership is checked against an already completed result
  • ▸Explaining absence never inserts facts or changes the decision
  • ▸Canonical ordering makes the retained diagnostic reproducible
  • ▸Hitting an explanation bound produces an explicitly truncated document
  • ▸Complete describes this exploration under its contract, not a proof of impossibility under every possible input
  • ▸Why-true and Why-false share the MAELYS-DATALOG-v2 envelope but explain different things
pseudocode
require successful membership query says absent
frontier = candidate_rules(query)

while frontier is not empty and explanation budgets remain:
  candidate = next_in_canonical_order(frontier)
  inspect candidate against the completed result
  retain bounded obstacles and nested causes

return document(obstacles, complete_or_truncated)
# No solve and no mutation of the result.
AGGREGATES

Aggregates over completed relations

Foundations: Stratified aggregation in deductive databases · Ullman, 1989Complexity: Bounded source scans and typed comparison
Maelys design

The language supplies count, min, max and sum over a completed lower relation. Count deduplicates the projected typed values. Sum instead adds one integer contribution per distinct source fact: two different facts with the same projected amount both contribute. Signed overflow and invalid aggregate input fail the solve; they are not silently clamped or published as partial answers. These are language and engineering contracts, not newly invented aggregate operators.

Evidence (v0.11.1): implementation · regression tests · min/max/sum and failure tests

Invariants
  • ▸Grouping variables are bound before the aggregate is evaluated
  • ▸The source relation is complete before its aggregate is read
  • ▸A dependency cycle through the aggregate is rejected
  • ▸Typed identity distinguishes an integer, a boolean and a symbol
  • ▸No partial result is published after an aggregate domain or overflow error
pseudocode
source = completed_relation_for_bound_group()
count = number_of_distinct_typed_projected_values(source)

sum = 0
for each distinct source fact:
  require projected value is an integer
  sum = checked_int64_add(sum, projected value)

on domain error or overflow:
  reject the solve
RUNTIME

Reusable preparation and explicit memory

Foundations: Prepared execution and caller-owned bounded storageComplexity: Reuse setup; each solve still evaluates a complete snapshot
Maelys design

A reference session keeps its preparation and bounded working storage for successive requests. Only one result may be alive per session. Explanation workspace reuse is optional: prepare one explanation, measure it, then format it without repeating the exploration while the cached query is unchanged. Allocation-free native hot paths are tested, but this does not make construction, output text allocation or Python objects allocation-free. In 0.11.1, sessions also share engine-owned immutable policies; caller-owned policy storage still receives an independent session snapshot.

Evidence (v0.11.1): implementation · regression tests · allocation guard

Invariants
  • ▸A live result retains its session; a prepared explanation retains its result
  • ▸Release explanations before their result and release that result before the next ordinary solve
  • ▸Resetting input facts does not release a result
  • ▸The explanation cache holds one query and is replaced when its key changes
  • ▸Supplying memory changes ownership, not the explanation limits
  • ▸Sharing engine-owned policy storage does not introduce incremental solving
pseudocode
session = prepare(policy, optional_workspace)
for each request:
  result = solve(session, complete_input_snapshot(request))
  query(result)
  optionally explain(result)
  release(result)
release(session)
IDENTITY

Policy identity

Maelys design

Maelys separates the manifest file digest from compiled-policy and execution identities. Canonical policy material includes the vocabulary, rules and applicable extension identities; the execution identity additionally records the selected backend and execution configuration. Applications can record these with a decision. SHA-256 identifies material: it does not authenticate its author, replace a signature, or guarantee confidentiality.

Evidence (v0.11.1): implementation · regression tests

Invariants
  • ▸Manifest-loaded policies verify file bytes against the manifest hash
  • ▸Embedded policies are hashed from source or structural material
  • ▸Applications read the ruleset hash rather than inventing a value
  • ▸Hash mismatch fails closed at load time
  • ▸The hash identifies policy material, not merely a policy name
  • ▸Policy and execution fingerprints distinguish policy authority from backend/configuration identity
pseudocode
load_policy(path, manifest_hash):
  actual = sha256(file_bytes(path))

  if actual != manifest_hash:
    FAIL CLOSED

  ruleset.sha256 = actual

decide(request):
  result = solve_once(ruleset, request.edb)
  decision = query(result, allow(request.id))
  return { decision, source_digest, policy_fingerprint, execution_fingerprint }
Maelys design

Guarantees combined in one runtime

Truncation does not change the decision

A successful solve has already established membership. Why-true and Why-false may reach their diagnostic limits and report truncation without changing that answer. In contrast, a solve capacity error means no successful result. These are different failure boundaries, not two kinds of approximate decision.

Atomic window publication

The native event-window adapters stage a candidate snapshot before publishing it. If a push is rejected, committed inputs, cursor and the previous result remain available. With backend ABI 5, commit occurs only after host acceptance. This guarantee belongs to those adapters: an ordinary session does not keep two live results or permit another solve before release.

These are demonstrable engineering properties of this implementation, not a claim that Maelys invented provenance, transactions or bounded evaluation. The evidence is executable: proof truncation, read-only Why-false, single-fact windows, group windows and backend acceptance.

Memory, not a general speedup claim

The 0.11.1 release reports 515,416 bytes instead of 1,178,352 for a reference SMALL session sharing an engine-owned policy (56.3% less, still three engine allocations). Policy creation and optional workspaces are excluded. The release also reports unresolved prepared-session latency regressions; this memory reduction is not a promise of faster solving. See the release measurements and their limits.

Key references

[1986]
François Bancilhon, Raghu Ramakrishnan. An Amateur's Introduction to Recursive Query Processing Strategies. SIGMOD.
[1988]
Krzysztof Apt, Howard Blair, Adrian Walker. Towards a Theory of Declarative Knowledge. Foundations of Deductive Databases and Logic Programming.
[1989]
Jeffrey D. Ullman. Principles of Database and Knowledge-Base Systems, Volume II. Computer Science Press.
[1995]
Serge Abiteboul, Richard Hull, Victor Vianu. Foundations of Databases. Addison-Wesley.
[2016]
Herbert Jordan, Bernhard Scholz, Pavle Subotić. Soufflé: On Synthesis of Program Analyzers. CAV.
[2016]
Bernhard Scholz, Herbert Jordan, Pavle Subotić, Till Westmann. On Fast Large-Scale Program Analysis in Datalog. CC.
[2018]
Pavle Subotić, Herbert Jordan, Lijun Chang, Alan Fekete, Bernhard Scholz. Automatic Index Selection for Large-Scale Datalog Computation. PVLDB.
[2020]
David Zhao, Pavle Subotić, Bernhard Scholz. Debugging Large-scale Datalog: A Scalable Provenance Evaluation Strategy. TOPLAS.
[2022]
Samuel Arch, Xiaowen Hu, David Zhao, Pavle Subotić, Bernhard Scholz. Building a Join Optimizer for Soufflé. LOPSTR.
[1997]
David R. Musser. Introspective Sorting and Selection Algorithms. Software—Practice and Experience 27(8), 983–993.
[2018]
Seokki Lee, Bertram Ludäscher, Boris Glavic. PUG: A Framework and Practical Implementation for Why & Why-Not Provenance. Extended version, arXiv:1808.05752.
[2015]
NIST. FIPS 180-4: Secure Hash Standard. Federal Information Processing Standards.