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 contextThe 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
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.
edb_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.
solve_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.
query_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.
solve_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.
| API | Use when |
|---|---|
maelys_datalog_solve_once | Standard path. Diagnostics are not needed on solve failure. |
maelys_datalog_solve_once_ex | The 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
| Parameter | Meaning |
|---|---|
ruleset | Pointer to the first loaded ruleset. Typically &policy_set.policies[0]. |
edb | Finalized EDB containing runtime input facts. |
out_result | Receives the solve result on success. Set to NULL on failure. Must not be NULL itself. |
out_diag | Optional 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_resultis set toNULL.- 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;
}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 code | Meaning |
|---|---|
MAELYS_DATALOG_SOLVE_DIAG_MAX_DEPTH | Recursion depth limit exceeded. |
MAELYS_DATALOG_SOLVE_DIAG_IDB_OVERFLOW | Derived fact capacity exceeded. |
MAELYS_DATALOG_SOLVE_DIAG_COMPARISON_TYPE_ERROR | Runtime 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_EDB | EDB not finalized or structurally invalid. |
MAELYS_DATALOG_DIAG_SOLVE_INTERNAL | Internal 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 code | Meaning |
|---|---|
MAELYS_OK | Solving succeeded. *out_result is valid. |
MAELYS_ERR_INVALID_ARGUMENT | NULL pointer argument. |
MAELYS_ERR_INVALID_STATE | EDB not finalized. |
MAELYS_ERR_PAYLOAD_TOO_LARGE | IDB fact capacity exceeded during solving. |
MAELYS_ERR_INTERNAL | Internal 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 queriesSolving 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.