C Low-level API

Runtime EDB

Low-level C operations for edb; concepts and integration code are documented separately.
CODE
Caller
  → maelys_datalog_edb_add_fact()   (repeat for each input fact)
  → maelys_datalog_edb_finalize()
  → maelys_datalog_solve_once()
  → maelys_datalog_query_solved_ground_fact()  (read results)
  → maelys_datalog_solve_result_free()

This page documents the transparent C functions behind the edb concepts. New application integrations should start with the stable C API; this reference is for consumers of the advanced, structure-level API.

Initialization

The policy set connection

Before you can initialize an EDB, you need a loaded policy. Any loading function — manifest_load_ex, manifest_load_from_text, load_policy_inline — returns a maelys_datalog_policy_set_t:

CODE
maelys_datalog_policy_set_t policy_set;
maelys_datalog_manifest_load_ex("manifest.json", 0, &policy_set, &diag);
/* or: maelys_datalog_load_policy_inline_with_static_domain(..., &policy_set, ...) */

A policy_set contains an array of loaded policies. In the common single-policy case you access the first one:

CODE
policy_set.policies[0]   /* maelys_datalog_ruleset_t */
  .symbols               /* symbol table — string ↔ integer ID mapping  */
  .registry              /* predicate registry — which predicates exist  */
  /* ... plus parsed rules, facts, strata, identity ... */

The EDB must share these two fields with the ruleset it will be solved against. The reason is concrete:

CODE
Ruleset  :  "alice" was interned at load time  →  symbol ID 1
EDB call :  edb_add_fact("alice", ...) interns "alice" in the SAME table  →  also ID 1
Solve    :  rule says owns(User, Doc) — User binds to ID 1 — matches!

If separate tables are used:
  Ruleset table : "alice" → ID 1
  EDB table     : "bob"   → ID 1

The solver compares symbol IDs, not raw strings. If the ruleset and EDB use
different symbol tables, the same numeric ID may refer to different strings.
Evaluation becomes invalid: facts may fail to match, or worse, match the
wrong value.

The registry serves a different role: it validates that each predicate you insert into the EDB is actually declared as EDB-kind in the domain. Passing the ruleset's registry prevents you from accidentally inserting a fact for a predicate that the policy does not expect from runtime callers.

CODE
maelys_result_t
maelys_datalog_edb_init(
    maelys_datalog_edb_t *edb,
    maelys_datalog_fact_t *fact_pool,
    size_t fact_capacity,
    maelys_datalog_symbol_table_t *symbols,
    const maelys_datalog_predicate_registry_t *registry);

The EDB does not allocate memory. The caller provides a static or stack-allocated fact pool:

CODE
static maelys_datalog_edb_t edb;
static maelys_datalog_fact_t fact_pool[MAELYS_DATALOG_MAX_EDB_FACTS];

maelys_datalog_edb_init(
    &edb,
    fact_pool,
    MAELYS_DATALOG_MAX_EDB_FACTS,
    &policy_set.policies[0].symbols,    /* shared string table  */
    &policy_set.policies[0].registry);  /* predicate validation */

edb vs fact_pool — two roles, one init call

The two static declarations serve different purposes.

maelys_datalog_edb_t edb is the logical manager: a small control structure that holds the fact count, the capacity, a pointer to the backing memory, and the state flags that track whether the EDB is mutable or finalized.

maelys_datalog_fact_t fact_pool[...] is the backing memory: the actual array where fact data lives. The EDB does not allocate this array — you provide it, and edb_init attaches the manager to it.

CODE
edb       →  index / manager  (knows where facts are, how many, what state)
fact_pool →  the pages        (holds the actual fact data)

In database terms: edb is the table descriptor with its metadata; fact_pool is the row storage.

Separating them avoids dynamic allocation in the EDB storage path: the EDB manager never allocates its fact pool. Every byte this EDB instance can use for facts is reserved by the caller. Other engine operations have their own contracts — for example, solving allocates one result object and prepared sessions allocate at creation time.

static on both declarations means the objects have program lifetime and file-local visibility — they persist across calls without living on the stack, and they are not exported to other translation units. This is the standard pattern for a single EDB object reused across many requests inside one C module.

ParameterMeaning
edbThe EDB to initialize. Must not be NULL.
fact_poolCaller-owned backing array for facts. Must not be NULL.
fact_capacityNumber of fact slots in fact_pool. Must be greater than zero.
symbolsSymbol table from the loaded policy set. Shared with the ruleset.
registryPredicate registry from the loaded policy set.

Adding facts

Building and adding a fact

The edb_add_fact function is the core fact insertion API. You fill in a maelys_datalog_term_t array and pass it alongside the predicate name:

CODE
maelys_result_t
maelys_datalog_edb_add_fact(
    maelys_datalog_edb_t *edb,
    const char *predicate,
    const maelys_datalog_term_t *terms,
    size_t arity);

A complete example adding owns("alice", "roadmap.pdf") and clearance("alice", 4):

CODE
/* Intern the symbol strings we need */
maelys_datalog_symbol_id_t alice_id, doc_id;
rc = maelys_datalog_symbol_intern(&ps.policies[0].symbols, "alice",       5, &alice_id);
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_symbol_intern(&ps.policies[0].symbols, "roadmap.pdf", 11, &doc_id);
if (rc != MAELYS_OK) return rc;

/* owns("alice", "roadmap.pdf")  — two symbol terms
   owns/2 is multi-arity, so we build terms manually.
   For unary runtime symbol facts, prefer edb_add_runtime_symbol_fact(). */
maelys_datalog_term_t owns_terms[2];
owns_terms[0].kind      = MAELYS_DATALOG_TERM_SYMBOL;
owns_terms[0].as.symbol = alice_id;
owns_terms[1].kind      = MAELYS_DATALOG_TERM_SYMBOL;
owns_terms[1].as.symbol = doc_id;
maelys_datalog_edb_add_fact(&edb, "owns", owns_terms, 2);

/* clearance("alice", 4)  — symbol + integer */
maelys_datalog_term_t cl_terms[2];
cl_terms[0].kind        = MAELYS_DATALOG_TERM_SYMBOL;
cl_terms[0].as.symbol   = alice_id;
cl_terms[1].kind        = MAELYS_DATALOG_TERM_INT;
cl_terms[1].as.integer  = 4;
maelys_datalog_edb_add_fact(&edb, "clearance", cl_terms, 2);

/* active(true)  — single boolean term */
maelys_datalog_term_t active_term;
active_term.kind        = MAELYS_DATALOG_TERM_BOOL;
active_term.as.boolean  = 1;
maelys_datalog_edb_add_fact(&edb, "active", &active_term, 1);

Helpers for common cases

For the common case of a 1-arity symbol fact, two helpers are available depending on whether the value is a runtime observation or a pre-declared atom:

CODE
/* Runtime open path — for user names, paths, IDs, any value unknown at load time */
maelys_result_t maelys_datalog_edb_add_runtime_symbol_fact(
    maelys_datalog_edb_t *edb,
    const char *predicate,
    const char *value);

/* Closed atom path — for values pre-declared in the atom vocabulary */
maelys_result_t maelys_datalog_edb_add_atom_fact(
    maelys_datalog_edb_t *edb,
    const char *predicate,
    const char *atom);
CODE
/* Runtime observations: no atom vocabulary precondition */
maelys_datalog_edb_add_runtime_symbol_fact(&edb, "user",       "alice");
maelys_datalog_edb_add_runtime_symbol_fact(&edb, "target_ref", "feature/foo");
maelys_datalog_edb_add_runtime_symbol_fact(&edb, "commit_msg", "fix: typo");

/* Pre-declared atoms: checked against vocabulary */
maelys_datalog_edb_add_atom_fact(&edb, "mode", "enforce");  /* "enforce" must be in atom vocabulary */

Batch insertion

The helpers above insert one fact per call. When you load many facts for the same predicate in one evaluation, the batch APIs insert a whole group in a single validated, atomic call — cheaper than N per-fact calls. Two families exist: insert by already-interned symbol id, and insert from runtime strings.

By symbol id

Intern once, reuse the integer handles, insert in bulk. Best when the same symbols recur (dense graphs, repeated nodes).

CODE
/* Unary batch: one fact per id */
maelys_result_t
maelys_datalog_edb_add_symbol_id_facts(
    maelys_datalog_edb_t *edb,
    const char *predicate,
    const maelys_datalog_symbol_id_t *values,
    size_t value_count);

/* Binary batch: flat pairs (left0,right0,left1,right1,...); pair_count = #facts */
maelys_result_t
maelys_datalog_edb_add_symbol_ids_facts(
    maelys_datalog_edb_t *edb,
    const char *predicate,
    const maelys_datalog_symbol_id_t *pairs,
    size_t pair_count);

The whole batch is validated up front — global capacity, per-predicate capacity, and every symbol id — before any fact is written. If validation fails, no fact is inserted (MAELYS_ERR_PAYLOAD_TOO_LARGE or an INVALID_* code).

From runtime strings

Insert many runtime values in one call. Each string is interned (through the symbol-table index) and the resulting ids are inserted through the symbol-id batch above.

CODE
/* Unary string batch */
maelys_result_t
maelys_datalog_edb_add_runtime_symbol_facts(
    maelys_datalog_edb_t *edb,
    const char *predicate,
    const char *const *values,
    size_t value_count);

/* Binary string batch: flat pairs of strings; pair_count = #facts */
maelys_result_t
maelys_datalog_edb_add_runtime_symbol_pair_facts(
    maelys_datalog_edb_t *edb,
    const char *predicate,
    const char *const *flat_pairs,
    size_t pair_count);
CODE
const char *users[] = { "alice", "bob", "carol" };
maelys_datalog_edb_add_runtime_symbol_facts(&edb, "user", users, 3);
/* → user("alice"). user("bob"). user("carol"). */

const char *owns[] = { "alice", "doc.pdf",  "bob", "img.png" };
maelys_datalog_edb_add_runtime_symbol_pair_facts(&edb, "owns", owns, 2);
/* → owns("alice","doc.pdf"). owns("bob","img.png"). */

Example — document access

The same seven request facts as the stable API must be added to the transparent EDB. Unlike the stable batch helper, this path interns runtime strings in the ruleset's symbol table and then adds facts by symbol ID:

CODE
maelys_datalog_ruleset_t *policy = &policy_set.policies[0];
static maelys_datalog_edb_t edb;
static maelys_datalog_fact_t fact_pool[64];
maelys_result_t rc = maelys_datalog_edb_init(
    &edb, fact_pool, 64u, &policy->symbols, &policy->registry);
if (rc != MAELYS_OK) return rc;

maelys_datalog_symbol_id_t alice, bob, mallory, document;
rc = maelys_datalog_edb_intern_runtime_symbol(&edb, "alice", &alice);
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_edb_intern_runtime_symbol(&edb, "bob", &bob);
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_edb_intern_runtime_symbol(&edb, "mallory", &mallory);
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_edb_intern_runtime_symbol(&edb, "roadmap.pdf", &document);
if (rc != MAELYS_OK) return rc;

rc = maelys_datalog_edb_add_symbol_id_fact(&edb, "user", alice);
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_edb_add_symbol_id_fact(&edb, "user", bob);
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_edb_add_symbol_id_fact(&edb, "user", mallory);
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_edb_add_symbol_ids_fact(&edb, "owns", alice, document);
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_edb_add_symbol_ids_fact(&edb, "delegated", bob, document);
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_edb_add_symbol_ids_fact(&edb, "owns", mallory, document);
if (rc != MAELYS_OK) return rc;
rc = maelys_datalog_edb_add_symbol_id_fact(&edb, "blocked", mallory);
if (rc != MAELYS_OK) return rc;

rc = maelys_datalog_edb_finalize(&edb);

The seven facts describe one complete request snapshot. In production code perform cleanup on every failure; the complete compiled example shows that path. blocked("mallory") requires no source atom declaration because this insertion is runtime data.

Clearing the EDB

To reuse the EDB for a new evaluation without re-initializing:

CODE
void maelys_datalog_edb_clear(maelys_datalog_edb_t *edb);

clear resets the fact count and marks the EDB as mutable again. The backing fact_pool is not zeroed — it is simply considered empty. The symbol table and registry remain attached.

Finalization

CODE
maelys_result_t
maelys_datalog_edb_finalize(maelys_datalog_edb_t *edb);

Finalization sorts and deduplicates the fact pool. After finalization the EDB is immutable — no more facts can be added. The EDB is then ready to be passed to solve_once.

CODE
maelys_result_t rc = maelys_datalog_edb_finalize(&edb);
if (rc != MAELYS_OK) return rc;

maelys_datalog_solve_result_t *result;
rc = maelys_datalog_solve_once(
    &policy_set.policies[0],
    &edb,
    &result);

Checking for a fact

CODE
int
maelys_datalog_edb_contains(
    const maelys_datalog_edb_t *edb,
    const maelys_datalog_fact_t *fact);

Returns 1 if the EDB already contains the given fact, 0 otherwise. The EDB automatically deduplicates facts at finalization, but edb_contains may be called before finalization if the EDB has been sorted.

Full example

CODE
#include "include/maelys_datalog.h"

maelys_result_t
evaluate_access(
    const maelys_datalog_policy_set_t *policy_set,
    const char *user,
    const char *document)
{
    maelys_result_t rc;

    /* --- Static EDB storage --- */
    static maelys_datalog_edb_t edb;
    static maelys_datalog_fact_t fact_pool[MAELYS_DATALOG_MAX_EDB_FACTS];

    /* --- Initialize EDB --- */
    rc = maelys_datalog_edb_init(
        &edb,
        fact_pool,
        MAELYS_DATALOG_MAX_EDB_FACTS,
        &policy_set->policies[0].symbols,
        &policy_set->policies[0].registry);
    if (rc != MAELYS_OK) return rc;

    /* --- Intern symbol strings --- */
    maelys_datalog_symbol_id_t user_sid, doc_sid;
    rc = maelys_datalog_symbol_intern(
        &policy_set->policies[0].symbols,
        user, strlen(user), &user_sid);
    if (rc != MAELYS_OK) return rc;

    rc = maelys_datalog_symbol_intern(
        &policy_set->policies[0].symbols,
        document, strlen(document), &doc_sid);
    if (rc != MAELYS_OK) return rc;

    /* --- Build owns(user, document) fact --- */
    maelys_datalog_term_t terms[2];
    terms[0].kind = MAELYS_DATALOG_TERM_SYMBOL;
    terms[0].as.symbol = user_sid;
    terms[1].kind = MAELYS_DATALOG_TERM_SYMBOL;
    terms[1].as.symbol = doc_sid;

    rc = maelys_datalog_edb_add_fact(&edb, "owns", terms, 2);
    if (rc != MAELYS_OK) return rc;

    /* --- Finalize and solve --- */
    rc = maelys_datalog_edb_finalize(&edb);
    if (rc != MAELYS_OK) return rc;

    maelys_datalog_solve_result_t *result = NULL;
    rc = maelys_datalog_solve_once(
        &policy_set->policies[0], &edb, &result);
    if (rc != MAELYS_OK) return rc;

    /* --- Query allow(user, document) --- */
    bool present = false;
    rc = maelys_datalog_query_solved_ground_fact(
        result, "allow", terms, 2, &present);

    maelys_datalog_solve_result_free(result);

    if (rc != MAELYS_OK) return rc;
    return present ? MAELYS_OK : MAELYS_ERR_FORBIDDEN;
}

Inline loading and WASM

When using inline loading or the WASM dynamic playground, the policy source typically contains rules with variables only (no string constants in the .dl source). Runtime string values — user names, document paths, request IDs — are injected into the EDB after loading and are unknown at parse time.

For unary runtime symbol facts, use edb_add_runtime_symbol_fact. It interns the value and inserts the fact without requiring the value to be pre-declared in the atom vocabulary.

For multi-arity or mixed-type facts, build the term array manually and call edb_add_fact.

CODE
/* Unary runtime symbol fact — no atom precondition */
maelys_datalog_edb_add_runtime_symbol_fact(&edb, "user", "alice");

/* Multi-arity fact — build terms manually */
maelys_datalog_term_t terms[2];
terms[0] = ...;  /* symbol term */
terms[1] = ...;  /* int or bool term */
maelys_datalog_edb_add_fact(&edb, "clearance", terms, 2);

Manifest-based deployments may use string constants in the .dl source (operation names, deny codes, protected branch names). Those constants must belong to the atom vocabulary declared by the domain registry, but runtime EDB values always go through edb_add_runtime_symbol_fact regardless.

The WASM edb_add_symbol helper follows this pattern internally: it interns the value and delegates to a symbol-id insertion path, which WASM callers can also reach directly (intern once, insert by id) for repeated symbols. See WASM bindings — inserting by pre-interned symbol ID for the JavaScript API.

Return values

Return codeMeaning
MAELYS_OKSuccess.
MAELYS_ERR_INVALID_ARGUMENTNULL pointer, zero capacity at initialization, missing terms for a positive arity, or arity above the build limit. A generic zero-arity fact is valid when the registry declares a zero-arity EDB predicate.
MAELYS_ERR_INVALID_STATEEDB is immutable (already finalized).
MAELYS_ERR_INVALID_FIELDUnknown predicate name or arity mismatch.
MAELYS_ERR_FORBIDDENPredicate is not accepted for runtime EDB insertion, or edb_add_atom_fact received an atom that is not present in the registered atom vocabulary.
MAELYS_ERR_PAYLOAD_TOO_LARGEEDB capacity exceeded.

Summary

CODE
edb_init()             →  attach fact pool, symbol table, registry
edb_add_fact()         →  add a typed ground fact
edb_add_runtime_symbol_fact()  →  1-arity runtime symbol helper, open values
edb_add_atom_fact()            →  1-arity closed atom helper, vocabulary checked
edb_add_symbol_id_facts()      →  batch insert N unary facts by symbol id (atomic)
edb_add_symbol_ids_facts()     →  batch insert N binary facts by flat id pairs (atomic)
edb_add_runtime_symbol_facts() →  batch insert N unary facts from strings (intern + insert)
edb_add_runtime_symbol_pair_facts()  →  batch insert N binary facts from flat string pairs
symbol_intern()        →  intern a string to get a stable symbol ID
edb_clear()            →  reset for reuse without reinitializing
edb_finalize()         →  sort and dedup; EDB becomes immutable
solve_once()           →  evaluate policy against finalized EDB

Additional C example 1

CODE
/* Call symbol_intern once per string to get its ID */
maelys_datalog_symbol_id_t alice_id;
maelys_result_t rc = maelys_datalog_symbol_intern(
    &policy_set.policies[0].symbols,
    "alice",
    5,        /* byte length, not including NUL */
    &alice_id);
if (rc != MAELYS_OK) return rc;  /* e.g. MAELYS_ERR_PAYLOAD_TOO_LARGE if table full */

/* Symbol IDs are 1-based. 0 is not a valid ID. */