API Reference

Solving

Solving is how a policy becomes a decision. You bring together a loaded ruleset — what the policy says — and a finalized EDB — what the caller knows at request time. The solver evaluates the two together and derives all the conclusions that follow from the policy's rules.

The derived conclusions are collected in an opaque solve result. You query that result for the specific facts you care about, then free it. The ruleset and EDB are read-only inputs: the same policy can be solved again immediately with a different EDB.

Why solving exists

Solving is the step that turns a loaded policy and runtime facts into a conclusion. Without it, a ruleset and an EDB are two inert data structures. The solver is the computation that derives authorization decisions.

Ruleset          =  what the policy says (facts, rules, logic)
EDB              =  what the caller knows (ownership, roles, context)
solve_once()     =  what the policy concludes given that context

The separation matters: the ruleset is parsed once and reused across many requests. Only the EDB changes per request. The solver bridges the two.

Where solving fits

Rulesetloaded policyEDBfinalized factsSolvesolve_onceResultopaque bundle

See Runtime EDB for the EDB lifecycle. See Querying for how to extract facts from the solve result.

Solve lifecycle

Each evaluation follows a fixed four-step sequence.

1. Finalize EDBedb_finalize()

Sort, deduplicate, and freeze the runtime fact store. After this call the EDB is immutable — no more facts can be added. This step is required before solving.

2. Solvesolve_once()

Evaluate the policy: the solver reads the frozen EDB and the loaded ruleset, then derives all IDB facts that follow from the rules. The result is an opaque bundle that contains the complete derived state.

3. Queryquery_solved_ground_fact()

Ask whether specific ground facts are present in the derived state. Each call is a single fact lookup — repeat for every fact you want to inspect, or use the enumeration accessor to read every derived fact of a predicate at once. The result is not modified by querying.

4. Freesolve_result_free()

Release the solve result. The result is heap-allocated and must always be freed after all queries are done. Passing NULL is safe. The ruleset and EDB are untouched.

In code:

maelys_datalog_edb_finalize(&edb);                 /* 1 — freeze */
 
maelys_datalog_solve_result_t *result = NULL;
maelys_datalog_solve_once(                         /* 2 — derive */
    &policy_set.policies[0], &edb, &result);
 
maelys_datalog_query_solved_ground_fact(           /* 3 — inspect, repeat */
    result, "allow", terms, arity, &present);
 
maelys_datalog_solve_result_free(result);          /* 4 — release */

To evaluate a new request: clear the EDB, add new facts, finalize again, then call solve_once again. The ruleset is never reloaded. See Runtime EDB for the EDB reset cycle.

Solving APIs

Two variants are available.

APIUse when
maelys_datalog_solve_onceStandard path. Diagnostics are not needed on solve failure.
maelys_datalog_solve_once_exThe caller wants structured diagnostics on solve failure.

Standard variant

maelys_result_t
maelys_datalog_solve_once(
    const maelys_datalog_ruleset_t *ruleset,
    const maelys_datalog_edb_t *edb,
    maelys_datalog_solve_result_t **out_result);

Extended variant

maelys_result_t
maelys_datalog_solve_once_ex(
    const maelys_datalog_ruleset_t *ruleset,
    const maelys_datalog_edb_t *edb,
    maelys_datalog_solve_result_t **out_result,
    maelys_datalog_solve_diagnostic_t *out_diag);

out_diag may be NULL. When provided, structured diagnostics for solver failures are written there. Diagnostics do not change the return code or the fail-closed behavior.

Parameters

ParameterMeaning
rulesetPointer to the first loaded ruleset. Typically &policy_set.policies[0].
edbFinalized EDB containing runtime input facts.
out_resultReceives the solve result on success. Set to NULL on failure. Must not be NULL itself.
out_diagOptional structured diagnostics. solve_once_ex only. May be NULL.
!

The EDB must be finalized before calling solve_once. Passing an unfinalized EDB is undefined behavior.

Evaluation model

Maelys DL uses semi-naive positive Datalog evaluation with stratified negation support.

Iteration 0 — base pass:
  Derive facts from:
    policy facts (declared in the .dl source)
    EDB facts (supplied by the caller)
 
Iteration 1..N — delta-driven passes:
  For each rule that references a newly derived IDB predicate:
    Derive new facts using the delta (new facts from previous iteration)
 
Convergence:
  Evaluation terminates when no new facts are derived.
  The fixpoint is the least fixpoint of the Datalog program.

For policies without recursion, evaluation terminates in two passes (base + one delta pass). Recursive rules require additional passes.

Stratified negation

Negated body literals are evaluated in stratum order. Each stratum is fully solved before its successors are evaluated. Recursive negation (negation through a cycle) is rejected at load time.

/* Stratum 0 */
blocked("mallory").
 
/* Stratum 1 — depends on stratum 0 */
allow(User, Doc) :-
    owns(User, Doc),
    not blocked(User).

Fail-closed behavior

Solve failures produce no partial results:

  • out_result is set to NULL.
  • Any partial solve result that may have been allocated is freed before returning.
  • The caller receives only the return code.
maelys_datalog_solve_result_t *result = NULL;
maelys_result_t rc = maelys_datalog_solve_once(
    &policy_set.policies[0],
    &edb,
    &result);
 
if (rc != MAELYS_OK) {
    /* result is NULL — nothing to free */
    return DENY;
}
i

Never check result != NULL as a proxy for success. Always check the return code. On failure, result is guaranteed to be NULL.

Example

#include "include/maelys_datalog.h"
 
maelys_result_t
solve_and_query(
    const maelys_datalog_policy_set_t *policy_set,
    maelys_datalog_edb_t *edb)
{
    /* edb has been finalized before this call */
 
    maelys_datalog_solve_result_t *result = NULL;
    maelys_result_t rc = maelys_datalog_solve_once(
        &policy_set->policies[0],
        edb,
        &result);
 
    if (rc != MAELYS_OK) {
        return rc;  /* fail closed — result is NULL, nothing to free */
    }
 
    /* Query results */
    maelys_datalog_term_t term;
    maelys_datalog_symbol_id_t sid;
    maelys_datalog_symbol_intern(
        &policy_set->policies[0].symbols,
        "alice", 5, &sid);
    term.kind       = MAELYS_DATALOG_TERM_SYMBOL;
    term.as.symbol  = sid;
 
    bool present = false;
    rc = maelys_datalog_query_solved_ground_fact(
        result, "allow", &term, 1, &present);
 
    /* Always free the result */
    maelys_datalog_solve_result_free(result);
 
    if (rc != MAELYS_OK) return rc;
    return present ? MAELYS_OK : MAELYS_ERR_FORBIDDEN;
}

Freeing the result

void
maelys_datalog_solve_result_free(
    maelys_datalog_solve_result_t *result);

The solve result is heap-allocated. It must be freed after all queries are done. Passing NULL is safe — solve_result_free(NULL) is a no-op.

!

Do not free the solve result while queries are still in progress. Do not use the result after freeing it.

Solve diagnostics

When using solve_once_ex, structured diagnostics explain solve failures.

Diagnostic codeMeaning
MAELYS_DATALOG_SOLVE_DIAG_MAX_DEPTHRecursion depth limit exceeded.
MAELYS_DATALOG_SOLVE_DIAG_IDB_OVERFLOWDerived fact capacity exceeded.
MAELYS_DATALOG_SOLVE_DIAG_COMPARISON_TYPE_ERRORRuntime cross-type comparison: a variable resolved to a type incompatible with the operator. solve_once_ex() returns MAELYS_ERR_INVALID_FIELD, result is NULL. Diagnostic fields: lhs_kind, rhs_kind, comparison_op. Denial reason: MAELYS_DATALOG_DENY_COMPARISON_TYPE_ERROR. See Comparison operators.
MAELYS_DATALOG_DIAG_SOLVE_INVALID_EDBEDB not finalized or structurally invalid.
MAELYS_DATALOG_DIAG_SOLVE_INTERNALInternal solver error.

Ruleset pointer for WASM

For WASM and embedded callers that use solve_once directly, the ruleset pointer is:

const maelys_datalog_ruleset_t *ruleset =
    &policy_set.policies[0];

The WASM dynamic builder exposes this as wasm_ruleset_ptr(). See WASM bindings for details.

Return values

Return codeMeaning
MAELYS_OKSolving succeeded. *out_result is valid.
MAELYS_ERR_INVALID_ARGUMENTNULL pointer argument.
MAELYS_ERR_INVALID_STATEEDB not finalized.
MAELYS_ERR_PAYLOAD_TOO_LARGEIDB fact capacity exceeded during solving.
MAELYS_ERR_INTERNALInternal solver error.

On any failure, *out_result is NULL and no memory needs to be freed.

Summary

solve_once()       →  evaluate policy; produce opaque result
solve_once_ex()    →  same, with optional solve diagnostics
solve_result_free()→  free the result after all queries

Solving is explicit: each call to solve_once produces an independent result. The ruleset and EDB are read-only inputs. The solve result is a snapshot — it is not updated if the EDB or ruleset changes. To evaluate a new EDB, call solve_once again.