C Low-level API

Rulesets

Low-level C operations for rulesets; concepts and integration code are documented separately.

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

From ruleset to EDB

Once a ruleset is loaded, the next step is initializing the EDB for each request. The EDB must share the same symbol table and predicate registry as the ruleset — these are the two fields that bridge loading and runtime evaluation.

CODE
/* A ruleset lives inside policy_set.policies[0] */
maelys_datalog_policy_set_t policy_set;
maelys_datalog_manifest_load_ex("manifest.json", 0, &policy_set, &diag);

/* Connect the EDB to the loaded ruleset */
static maelys_datalog_edb_t edb;
static maelys_datalog_fact_t pool[MAELYS_DATALOG_MAX_EDB_FACTS];

maelys_datalog_edb_init(
    &edb, pool, MAELYS_DATALOG_MAX_EDB_FACTS,
    &policy_set.policies[0].symbols,    /* shared: same string IDs as the ruleset */
    &policy_set.policies[0].registry);  /* shared: same predicate vocabulary */

The ruleset is loaded once. The EDB is recreated per request. Both refer to the same symbol table so that string values in facts match string values in rules.

See Runtime EDB — initialization for the full EDB lifecycle.

Example — document access

The low-level loader returns a transparent policy set. The shared inline example contains one policy, documents.main, so its ruleset is the first entry. The stable API selects that same policy through an opaque session instead of exposing the structure.

CODE
/* After a successful document-access load: */
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) {
    /* Do not add facts or solve. */
}

The shared symbol table and predicate registry are exactly the ones used to parse the owns, delegated, blocked, can_read and allow rules. The EDB page continues with the same seven request facts. The direct parser example below illustrates a different, more specialized document-access policy; it is not a prerequisite for this loader path.

API reference

The ruleset pipeline involves four functions called in a fixed order: ruleset_init → domain install + registry freeze → parse_ruleset_ex → ruleset_clear. High-level loading APIs such as maelys_datalog_load_policy_inline and maelys_datalog_manifest_load_from_text perform all steps automatically. The functions below are exposed for callers that manage the pipeline directly.

Complete example

This example loads the document_access policy from the earlier section. All variables are declared and all values are concrete.

CODE
#include "include/maelys_datalog.h"
#include <string.h>

maelys_result_t
load_document_access_policy(void)
{
    maelys_result_t rc;

    /* --- The policy source --- */
    static const char policy_src[] =
        "blocked(\"mallory\").\n"
        "\n"
        "allow(User, Document) :-\n"
        "    owns(User, Document),\n"
        "    not(blocked(User)),\n"
        "    sensitivity_level(User, Level),\n"
        "    Level >= 3.\n"
        "\n"
        "allow(User, Document) :-\n"
        "    shared_with(User, Document),\n"
        "    not(blocked(User)),\n"
        "    sensitivity_level(User, Level),\n"
        "    Level >= 3.\n";
    const size_t src_len = sizeof(policy_src) - 1;   /* byte count, not strlen */

    /* SHA-256 of policy_src — computed once at build or load time */
    char sha256[65];
    maelys_sha256_hex(
        (const unsigned char *)policy_src, src_len, sha256);

    /* --- Ruleset shell --- */
    maelys_datalog_ruleset_t ruleset = {0};

    rc = maelys_datalog_ruleset_init(
        &ruleset,
        "document_access.main",  /* policy_id  */
        "document_access",       /* domain     */
        sha256,                  /* sha256     */
        0);                      /* test_only  */
    if (rc != MAELYS_OK) return rc;

    /* --- Install domain vocabulary --- */
    maelys_datalog_predicate_registry_add_domain(
        &ruleset.registry, "owns",        2, MAELYS_DATALOG_PRED_KIND_EDB);
    maelys_datalog_predicate_registry_add_domain(
        &ruleset.registry, "shared_with", 2, MAELYS_DATALOG_PRED_KIND_EDB);
    maelys_datalog_predicate_registry_add_domain(
        &ruleset.registry, "sensitivity_level", 2, MAELYS_DATALOG_PRED_KIND_EDB);
    maelys_datalog_predicate_registry_add_domain(
        &ruleset.registry, "blocked",     1, MAELYS_DATALOG_PRED_KIND_POLICY_FACT);
    maelys_datalog_predicate_registry_add_domain(
        &ruleset.registry, "allow",       2,
        MAELYS_DATALOG_PRED_KIND_IDB | MAELYS_DATALOG_PRED_KIND_QUERY);

    /* --- Freeze: no predicates may be added after this point --- */
    maelys_datalog_predicate_registry_freeze(&ruleset.registry);

    /* --- Parse --- */
    maelys_datalog_diagnostic_t diag = {0};
    rc = maelys_datalog_parse_ruleset_ex(
        &ruleset,
        policy_src,
        src_len,
        "policies/document_access.dl",  /* file_path: used in diagnostics */
        &diag);

    if (rc != MAELYS_OK) {
        /* diag.message describes the exact parse failure */
        maelys_datalog_ruleset_clear(&ruleset);
        return rc;
    }

    /* ruleset is ready for solve_once() */
    return MAELYS_OK;
}

maelys_datalog_ruleset_init

Initializes the ruleset identity and internal structures. Does not parse policy text.

CODE
maelys_result_t
maelys_datalog_ruleset_init(
    maelys_datalog_ruleset_t *ruleset,
    const char *policy_id,
    const char *domain,
    const char *sha256,
    int test_only);
ParameterMeaning
rulesetRuleset to initialize. Must not already be loaded.
policy_idStable policy identifier, e.g. "document_access.main".
domainDomain name, e.g. "document_access".
sha256Lowercase hex SHA-256 of the policy source, e.g. "a3f7c291...".
test_onlyNon-zero marks this ruleset as test-only.

After a successful call, the predicate registry is open — domain predicates must be installed and the registry frozen before parsing.

Return codeMeaning
MAELYS_OKInitialized.
MAELYS_ERR_INVALID_ARGUMENTA required pointer is NULL. This initializer copies identity strings but does not validate their semantic format. Manifest and inline loaders perform the public identity validations before initialization.
MAELYS_ERR_INVALID_STATERuleset already loaded.
MAELYS_ERR_PAYLOAD_TOO_LARGEpolicy_id, domain, or sha256 exceeds its bounded field.

Between init and parse: install domain then freeze

After ruleset_init, the predicate registry is open — no domain vocabulary is installed yet, and the parser will refuse to run on an open registry. Two steps are required before parsing:

CODE
1. Install domain predicates via maelys_datalog_predicate_registry_add_domain()
2. Freeze via maelys_datalog_predicate_registry_freeze()

See the complete example above for the code. After freezing, parse_ruleset_ex validates every predicate name and arity in the source against the frozen registry — any predicate not installed in step 1 causes a parse failure.


maelys_datalog_parse_ruleset_ex

Parses .dl source text into the ruleset. The registry must be frozen.

CODE
maelys_result_t
maelys_datalog_parse_ruleset_ex(
    maelys_datalog_ruleset_t *ruleset,
    const char *src,
    size_t len,
    const char *file_path,
    maelys_datalog_diagnostic_t *out_diag);
ParameterMeaning
rulesetInitialized ruleset with a frozen predicate registry.
srcPolicy source bytes. Does not need to be NUL-terminated.
lenByte length of src. Authoritative — use sizeof(arr) - 1 for string literals.
file_pathOptional path used in diagnostics, e.g. "policies/document_access.dl". May be NULL.
out_diagOptional structured diagnostic on parse failure. May be NULL.

The simple variant omits file_path and out_diag:

CODE
maelys_result_t
maelys_datalog_parse_ruleset(
    maelys_datalog_ruleset_t *ruleset,
    const char *src,
    size_t len);
Return codeMeaning
MAELYS_OKParsed successfully.
MAELYS_ERR_INVALID_STATERuleset not initialized or registry not frozen.
MAELYS_ERR_INVALID_FIELDPolicy text violates grammar, predicate, typing, or safety rules.
MAELYS_ERR_PAYLOAD_TOO_LARGEA static parser or ruleset bound was exceeded.

maelys_datalog_ruleset_clear

Releases all structures held by the ruleset. Safe to call with NULL and safe to call on a ruleset that was never parsed.

CODE
void
maelys_datalog_ruleset_clear(
    maelys_datalog_ruleset_t *ruleset);

Parser diagnostics

When out_diag is provided, parse failures include structured context.

DiagnosticMeaning
PARSER_UNKNOWN_PREDICATEPredicate name not found in the frozen registry.
PARSER_ARITY_MISMATCHPredicate found but arity does not match the declaration.
PARSER_RULE_HEAD_EDB_FORBIDDENEDB or POLICY_FACT predicate used as a rule head.
PARSER_FACT_USES_NON_BASE_PREDICATEDirect fact used a non-POLICY_FACT predicate.
PARSER_UNSAFE_VARIABLEHead or negated variable not bound by a positive body atom.
PARSER_INVALID_COMPARISONComparison is malformed or type-invalid.
POLICY_NOT_STRATIFIABLENegation cannot be stratified — recursion through negation.

Each diagnostic also includes file path, line, column, offending token, and a human-readable hint when available.