C Low-level API

Manifests

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

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

Loading architectures

Maelys DL has two loading families—manifest and inline—and four variants. The file-manifest variant has both a simple call and an extended diagnostic call, so this low-level API exposes five C functions in total. All four variants below are supported:

PathVariantLow-level C callSupport
Manifestfilemaelys_datalog_manifest_load / maelys_datalog_manifest_load_exSupported
Manifestbuffermaelys_datalog_manifest_load_from_textSupported
Inlinestatic domainmaelys_datalog_load_policy_inline_with_static_domainSupported
Inlinedynamic domainmaelys_datalog_load_policy_inlineSupported
Domain vocabulary
registered first or supplied by static call
Manifest · file
  1. File
    manifest_load / _ex
Manifest · buffer
  1. Buffer
    manifest_load_from_text
Inline · static
  1. Static table
    register + load in one call
Inline · dynamic
  1. Source bytes
    domain registered first

The static inline call registers its compile-time predicate table and loads the policy in one operation. The other three variants use a domain registered beforehand. The stable opaque C API exposes only the file-manifest and dynamic-inline variants; it does not expose this buffer-manifest or one-call static-domain surface.

Capabilities by loading path

Loading styleMetadataSource bytesQuery whitelistUse case
File manifest loadingManifest JSON filePolicy files on diskManifest queries fieldProduction deployments.
Buffer manifest loadingManifest JSON bufferIn-memory source bundleManifest queries fieldEmbedded, tests, streaming — any caller with in-memory sources.
Inline loading (dynamic)Function argumentsIn-memory src/src_lenNone — all QUERY predicates accessibleTests, examples, REPLs, simple embedded callers; the domain is registered separately, beforehand.
Inline loading (static)Function arguments + compile-time predicate tableIn-memory src/src_lenNone — all QUERY predicates accessibleC static tables, WASM production builds, embedded C where the domain is compiled in — registers and loads in one call.

Loading APIs

Maelys DL exposes five public loading APIs.

APILoading styleUse when
maelys_datalog_manifest_loadFile manifestThe manifest and policy files are on disk, and structured diagnostics are not needed.
maelys_datalog_manifest_load_exFile manifestThe manifest and policy files are on disk, and the caller wants structured diagnostics.
maelys_datalog_manifest_load_from_textIn-memory manifestThe caller has manifest JSON and policy source bytes in memory, but still wants manifest metadata, modes, SHA verification, bundles, or multiple policies.
maelys_datalog_load_policy_inlineIn-memory inline sourceThe caller has one .dl policy source in memory and does not need manifest JSON, caller-provided SHA-256, bundles, modes, or multi-policy metadata. The domain is already registered.
maelys_datalog_load_policy_inline_with_static_domainIn-memory inline source, static domainThe caller has a compile-time predicate table and wants to register the domain and load the policy in one call — no separate registration step.

Example — document access

The low-level one-call static-domain loader uses the same document_access_predicates table defined on Registries and the exact policy source shown in the stable loader:

CODE
maelys_datalog_policy_set_t policy_set = {0};
maelys_datalog_diagnostic_t diagnostic = {0};
maelys_result_t rc = maelys_datalog_load_policy_inline_with_static_domain(
    document_access_predicates,
    sizeof(document_access_predicates) / sizeof(document_access_predicates[0]),
    "documents", "documents.main", source, strlen(source),
    0u, &policy_set, &diagnostic);
if (rc != MAELYS_OK) {
    /* Inspect diagnostic; do not use policy_set. */
}

This path registers the seven-predicate domain and loads one policy in one operation. A separately registered document_access_domain_def can instead be selected by maelys_datalog_load_policy_inline("documents", ...). The policy source has no quoted literal, so neither path needs source atoms. The other loading paths below remain available for manifest metadata and multiple policies.

Choosing a loading path

Use file manifest loading when policies are deployed on disk.

Use buffer manifest loading when there is no filesystem, but the caller still wants the manifest model:

  • policy_set_id, policy_set_version
  • multi-policy loading
  • loading modes
  • caller-provided SHA-256 verification
  • Public Query Whitelist

Use inline loading when the caller simply wants to load one .dl source already in memory. Inline loading has no manifest Public Query Whitelist — all predicates declared as QUERY by the selected domain are accessible.

Use the static domain variant specifically when the predicate table is already written as a compile-time C array and a separate registration call would just be extra ceremony — C static tables, WASM production builds, tests that want a one-call setup. Use the plain (dynamic) variant when the domain is registered separately, from JavaScript, from another module, or from a callback — anywhere the predicate table is not a literal C array sitting next to the load call.

File manifest loading

File manifest loading uses a manifest JSON file on disk. The manifest references policy source files by safe relative path, and the loader reads those files during loading.

Simple file variant

CODE
maelys_result_t
maelys_datalog_manifest_load(
    const char *manifest_path,
    unsigned flags,
    maelys_datalog_policy_set_t *out_set);

This is a convenience wrapper. It loads a manifest file without returning structured diagnostics. Equivalent to calling the extended variant with out_diag = NULL.

Extended file variant

CODE
maelys_result_t
maelys_datalog_manifest_load_ex(
    const char *manifest_path,
    unsigned flags,
    maelys_datalog_policy_set_t *out_set,
    maelys_datalog_diagnostic_t *out_diag);

Use _ex when the caller needs to explain a loading failure precisely. See Error codes for the full diagnostic code reference.

File loading example

CODE
#include "include/maelys_datalog.h"

maelys_result_t
load_document_access_from_files(void)
{
    maelys_result_t rc;
    maelys_datalog_policy_set_t policy_set;
    maelys_datalog_diagnostic_t diag;

    rc = document_access_domains_install();
    if (rc != MAELYS_OK) return rc;

    rc = maelys_datalog_manifest_load_ex(
        "policies/manifest.json",
        0,
        &policy_set,
        &diag);

    if (rc != MAELYS_OK) return rc;  /* fail closed */

    /*
     * policy_set is now ready. Pass policy_set.policies[0].symbols and
     * .registry to edb_init() to initialize the runtime EDB.
     * See: /docs/concepts/edb
     */
    return MAELYS_OK;
}

Buffer manifest loading

Buffer manifest loading uses the same manifest model, but avoids filesystem access. The caller provides manifest JSON bytes and source bundle entries.

CODE
maelys_result_t
maelys_datalog_manifest_load_from_text(
    const char *manifest_json,
    size_t manifest_json_len,
    const maelys_datalog_policy_bundle_entry_t *bundle,
    size_t bundle_count,
    unsigned flags,
    maelys_datalog_policy_set_t *out_set,
    maelys_datalog_diagnostic_t *out_diag);

The buffer loader:

  1. Parses the manifest JSON.
  2. For each enabled policy, finds the matching source entry by policy_id.
  3. Verifies the SHA-256 of the source bytes against the manifest.
  4. Installs the selected domain vocabulary.
  5. Validates and loads the policy source.
  6. Stores the queries Public Query Whitelist in the returned policy set.

manifest_json does not need to be NUL-terminated. manifest_json_len is authoritative.

In-memory policy sources

CODE
typedef struct {
    const char *policy_id;   /* matches policy_id in manifest JSON */
    const char *src;         /* .dl source bytes */
    size_t src_len;          /* byte length of src */
} maelys_datalog_policy_bundle_entry_t;

Buffer loading example

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

maelys_result_t
load_document_access_from_buffers(void)
{
    maelys_result_t rc;
    maelys_datalog_policy_set_t policy_set;
    maelys_datalog_diagnostic_t diag;

    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";

    rc = document_access_domains_install();
    if (rc != MAELYS_OK) return rc;

    /* Compute SHA-256 of the source bytes */
    char sha256[65];
    maelys_sha256_hex(
        (const unsigned char *)policy_src,
        sizeof(policy_src) - 1,
        sha256);

    /* Build manifest JSON — check for truncation */
    char manifest[2048];
    int written = snprintf(
        manifest, sizeof(manifest),
        "{"
        "\"policy_set_id\":\"document_access.buffer\","
        "\"policy_set_version\":\"1\","
        "\"manifest_version\":\"1\","
        "\"default_profile\":\"MAELYS-DATALOG-v2\","
        "\"created_for\":\"test\","
        "\"strict_loading\":true,"
        "\"fail_closed\":true,"
        "\"capabilities\":[],"
        "\"policies\":[{"
        "\"policy_id\":\"document_access.main\","
        "\"domain\":\"document_access\","
        "\"file\":\"ignored-for-buffer-loading.dl\","
        "\"sha256\":\"%s\","
        "\"mode\":\"shadow\","
        "\"enabled\":true,"
        "\"description\":\"buffer loading example\","
        "\"queries\":[{\"name\":\"allow\",\"arity\":2}]"
        "}]"
        "}",
        sha256);

    if (written < 0 || (size_t)written >= sizeof(manifest))
        return MAELYS_ERR_PAYLOAD_TOO_LARGE;

    maelys_datalog_policy_bundle_entry_t sources[] = {
        {
            .policy_id = "document_access.main",
            .src       = policy_src,
            .src_len   = sizeof(policy_src) - 1,
        },
    };

    rc = maelys_datalog_manifest_load_from_text(
        manifest, (size_t)written,
        sources, sizeof(sources) / sizeof(sources[0]),
        0, &policy_set, &diag);

    if (rc != MAELYS_OK) return rc;

    /*
     * policy_set now has allow/2 in its Public Query Whitelist.
     * Pass policy_set.policies[0].symbols and .registry to edb_init()
     * to initialize the runtime EDB for evaluation.
     * See: /docs/concepts/edb
     */
    return MAELYS_OK;
}

Inline loading

Inline loading is the low-ceremony path for loading one policy directly from an in-memory .dl source. It does not use a manifest.

CODE
maelys_result_t
maelys_datalog_load_policy_inline(
    const char *domain,
    const char *policy_id,
    const char *src,
    size_t src_len,
    unsigned flags,
    maelys_datalog_policy_set_t *out_set,
    maelys_datalog_diagnostic_t *out_diag);

Inline loading has no manifest Public Query Whitelist. All predicates declared as QUERY by the selected domain are accessible through the query API. There is no observation gate between the solver output and the caller.

Inline domain-closed vocabulary

All predicates used by the inline source must be declared by the selected domain. Inline loading does not support manifest-level overlays or per-call vocabulary extensions.

This is valid if the selected domain declares owns/2, sensitivity_level/2, and allow/2:

DATALOG
allow(User, Document) :-
    owns(User, Document),
    sensitivity_level(User, Level),
    Level >= 3.

This is invalid unless the selected domain also declares helper/2:

DATALOG
helper(User, Document) :- owns(User, Document).
allow(User, Document) :- helper(User, Document).

See Registries for the domain-closed vocabulary model.

Inline loading example

CODE
#include "include/maelys_datalog.h"

maelys_result_t
load_document_access_inline(void)
{
    maelys_result_t rc;
    maelys_datalog_policy_set_t policy_set;
    maelys_datalog_diagnostic_t diag;

    static const char policy_src[] =
        "allow(User, Document) :-\n"
        "    owns(User, Document),\n"
        "    sensitivity_level(User, Level),\n"
        "    Level >= 3.\n";

    rc = document_access_domains_install();
    if (rc != MAELYS_OK) return rc;

    rc = maelys_datalog_load_policy_inline(
        "document_access",
        "document_access.inline",
        policy_src,
        sizeof(policy_src) - 1,
        0,
        &policy_set,
        &diag);

    if (rc != MAELYS_OK) return rc;

    /*
     * All domain QUERY predicates are accessible — no whitelist.
     * Pass policy_set.policies[0].symbols and .registry to edb_init()
     * to initialize the runtime EDB for evaluation.
     * See: /docs/concepts/edb
     */
    return MAELYS_OK;
}

Inline loading with a static domain

maelys_datalog_load_policy_inline_with_static_domain registers a domain from a compile-time predicate table and loads a policy in one call — no separate registration step, no separate domain_registry_register call to write.

CODE
maelys_result_t
maelys_datalog_load_policy_inline_with_static_domain(
    const maelys_datalog_predicate_def_t *predicates,
    size_t predicate_count,
    const char *domain_name,
    const char *policy_id,
    const char *src,
    size_t src_len,
    unsigned flags,
    maelys_datalog_policy_set_t *out_set,
    maelys_datalog_diagnostic_t *out_diag);

Verified against the implementation: this function is a thin wrapper — it calls maelys_datalog_domain_registry_register with predicates/ predicate_count/domain_name, then calls maelys_datalog_load_policy_inline with the remaining arguments unchanged, flags included. Every constraint of the dynamic variant (flags must be 0, domain-closed vocabulary, no Public Query Whitelist) applies identically here.

domain_name and predicates are stored by pointer in the global domain registry (first-registration-wins, see Registries — Registering a domain) and must remain valid for as long as the registry may reference them — a static const array and string literals satisfy this automatically, which is why this API exists specifically for compile-time C tables rather than heap-built or short-lived ones.

Static domain loading example

Reuses the document_access_predicates static table from Registries — Method 1: static predicate table.

CODE
#include "include/maelys_datalog.h"

maelys_result_t
load_document_access_static(void)
{
    maelys_result_t rc;
    maelys_datalog_policy_set_t policy_set;
    maelys_datalog_diagnostic_t diag;

    static const char policy_src[] =
        "allow(User, Document) :-\n"
        "    owns(User, Document),\n"
        "    sensitivity_level(User, Level),\n"
        "    Level >= 3.\n";

    rc = maelys_datalog_load_policy_inline_with_static_domain(
        document_access_predicates,
        sizeof(document_access_predicates) / sizeof(document_access_predicates[0]),
        "document_access",
        "document_access.static",
        policy_src,
        sizeof(policy_src) - 1,
        0,
        &policy_set,
        &diag);

    if (rc != MAELYS_OK) return rc;  /* fail closed */

    /*
     * No separate document_access_domains_install() call needed above —
     * this one call registered the domain and loaded the policy.
     * Pass policy_set.policies[0].symbols and .registry to edb_init()
     * to initialize the runtime EDB.
     * See: /docs/concepts/edb
     */
    return MAELYS_OK;
}

WASM loading

For WASM and embedded environments, use the in-memory loading paths. See WASM bindings for the full build and API reference.

The JavaScript binding provides registerDomain and loadPolicy through the public SDK. It does not export the native manifest loader or the historical static-domain target. In-memory bundles remain separate binding work.

What loading returns

Every loading function writes its result into out_set on success. The policy_set you receive is not just a return value — it is the shared context that every subsequent operation depends on.

CODE
maelys_datalog_policy_set_t policy_set;  /* filled by any loader */

policy_set.policies[0]          /* first loaded policy (one per manifest entry) */
  .symbols                      /* shared string intern table: "alice" → ID 1 */
  .registry                     /* predicate registry: which predicates exist */
  /* ... parsed rules, strata, query whitelist, identity ... */

These two fields are the bridge between loading and runtime evaluation. The EDB must share the same symbol table and predicate registry as the policy it will be solved against. If they used different tables, string IDs would not match between EDB facts and ruleset rules — no fact would ever match a rule.

CODE
/* After loading, initialize the EDB with the policy's shared context */
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,    /* same string table as the ruleset */
    &policy_set.policies[0].registry);  /* same predicate vocabulary */

From this point, loading is complete. The full evaluation lifecycle is:

CODE
1. Load    — manifest loading produces policy_set
2. Init    — edb_init(&edb, pool, N, &policy_set.policies[0].symbols,
                       &policy_set.policies[0].registry)
3. Add     — edb_add_fact(&edb, "owns", terms, 2)  per request
4. Finalize — edb_finalize(&edb)
5. Solve   — solve_once(&policy_set.policies[0], &edb, &result)
6. Query   — query_solved_ground_fact(result, "allow", terms, 2, &present)
7. Free    — solve_result_free(result)
8. Repeat  — edb_clear(&edb), go to step 3 for the next request

The ruleset (policy_set.policies[0]) is loaded once and reused across all requests. Only the EDB changes per request.

See Runtime EDB — initialization for a detailed explanation of the EDB lifecycle and why the shared symbol table is required.

Parameter reference

FunctionWhitelist enforcedDiagnostics
maelys_datalog_manifest_loadYes — manifest queriesNo
maelys_datalog_manifest_load_exYes — manifest queriesOptional out_diag
maelys_datalog_manifest_load_from_textYes — manifest queriesOptional out_diag
maelys_datalog_load_policy_inlineNo — all QUERY accessibleOptional out_diag
maelys_datalog_load_policy_inline_with_static_domainNo — all QUERY accessibleOptional out_diag

Shared output parameters

NameTypeMeaning
out_setmaelys_datalog_policy_set_t *Receives the loaded policy set on success. Cleared on failure. Must not be NULL.
out_diagmaelys_datalog_diagnostic_t *Optional structured diagnostic output. May be NULL for APIs that expose it.

File manifest parameters

NameTypeMeaning
manifest_pathconst char *Filesystem path to the manifest JSON file. Must not be NULL.
flagsunsignedManifest loading flags. Use 0 for strict production loading.
out_setmaelys_datalog_policy_set_t *Receives the loaded policy set. Cleared on failure.
out_diagmaelys_datalog_diagnostic_t *Optional diagnostics. Only on _ex. May be NULL.

Buffer manifest parameters

NameTypeMeaning
manifest_jsonconst char *Manifest JSON bytes. Does not need to be NUL-terminated.
manifest_json_lensize_tAuthoritative byte length of manifest_json.
bundleconst maelys_datalog_policy_bundle_entry_t *In-memory .dl source entries matched by policy_id. May be NULL if bundle_count is zero.
bundle_countsize_tNumber of entries in bundle.
flagsunsignedManifest loading flags. Use 0 for strict production loading.
out_setmaelys_datalog_policy_set_t *Receives the loaded policy set. Cleared on failure.
out_diagmaelys_datalog_diagnostic_t *Optional diagnostics. May be NULL.

Inline (dynamic) parameters

NameTypeMeaning
domainconst char *Registered domain name. Non-NULL, non-empty.
policy_idconst char *Stable identifier for this inline policy. Non-NULL, non-empty.
srcconst char *.dl policy source bytes. Does not need to be NUL-terminated.
src_lensize_tAuthoritative byte length of src. Must be greater than zero.
flagsunsignedReserved. Must be 0.
out_setmaelys_datalog_policy_set_t *Receives the loaded single-policy set. Cleared on failure.
out_diagmaelys_datalog_diagnostic_t *Optional diagnostics. May be NULL.

Inline (static domain) parameters

Same as the dynamic variant, plus the predicate table registered before loading:

NameTypeMeaning
predicatesconst maelys_datalog_predicate_def_t *Compile-time predicate table. Stored by pointer in the domain registry — must remain valid for the registry's lifetime.
predicate_countsize_tNumber of entries in predicates.
domain_nameconst char *Domain name to register. Stored by pointer — same lifetime requirement as predicates.
policy_idconst char *Stable identifier for this inline policy. Non-NULL, non-empty.
srcconst char *.dl policy source bytes. Does not need to be NUL-terminated.
src_lensize_tAuthoritative byte length of src. Must be greater than zero.
flagsunsignedReserved. Must be 0 — passed through unchanged to the same check load_policy_inline performs.
out_setmaelys_datalog_policy_set_t *Receives the loaded single-policy set. Cleared on failure.
out_diagmaelys_datalog_diagnostic_t *Optional diagnostics. May be NULL.

Flags

Loading styleAccepted flagsMeaning
Manifest loading0No optional permissions: enabled test-only policies are rejected and policy string constants must be declared in the domain.
Manifest loadingMAELYS_DATALOG_MANIFEST_ALLOW_TEST_ONLYAllows enabled test_only policies to load and evaluate normally; it does not relax atom declarations. Tests and development only.
Manifest loadingMAELYS_DATALOG_MANIFEST_ALLOW_UNDECLARED_POLICY_ATOMSAllows policy-local source constants absent from the domain's atoms; it does not admit test-only policies.
Manifest loadingBoth permission bits combined with |Grants the two permissions independently; other checks still apply.
Inline loading (dynamic)0Required.
Inline loading (dynamic)Any non-zero valueRejected with MAELYS_ERR_INVALID_ARGUMENT.
Inline loading (static domain)0Required — passed through unchanged to the same check as the dynamic variant.
Inline loading (static domain)Any non-zero valueRejected with MAELYS_ERR_INVALID_ARGUMENT.

These are independent permissions, not strict/permissive modes. atoms belongs to the registered domain, not the manifest. A source fact such as blocked("mallory"). needs the string declared there unless the caller grants the source-constant permission. The same value supplied in a request EDB needs no atom declaration. Neither permission creates predicates or changes the domain globally.

The table above describes the lower-level loading API. The opaque facade uses MAELYS_DATALOG_PUBLIC_ALLOW_TEST_ONLY and MAELYS_DATALOG_PUBLIC_ALLOW_UNDECLARED_POLICY_ATOMS instead. Its manifest loader rejects unknown bits with MAELYS_DATALOG_STATUS_INVALID_ARGUMENT. Its maelys_datalog_policy_load_inline() has no flags parameter, unlike the lower-level inline signatures on this page; it always requires declared policy constants. See public loading permissions.

Diagnostics

The return code tells the caller the broad class of failure. The diagnostic object explains the precise cause.

A manifest SHA mismatch:

CODE
Return code : MAELYS_ERR_INVALID_FIELD
Diagnostic  : code=MAELYS_DATALOG_DIAG_MANIFEST_SHA_MISMATCH
              field=sha256 / hint=update manifest sha256 after policy edit

A rejected test-only policy:

CODE
Return code : MAELYS_ERR_FORBIDDEN
Diagnostic  : code=MAELYS_DATALOG_DIAG_MANIFEST_TEST_ONLY_REJECTED

A Public Query Whitelist validation failure:

CODE
Return code : MAELYS_ERR_INVALID_FIELD
              (unknown predicate, arity mismatch, or missing QUERY flag)

A non-whitelisted query at runtime:

CODE
Return code : MAELYS_ERR_FORBIDDEN
              (predicate exists but is not in the Public Query Whitelist)

See Error codes for the full diagnostic code reference.

Return values

Return codeMeaning
MAELYS_OKLoading succeeded.
MAELYS_ERR_INVALID_ARGUMENTA required argument is invalid. For inline loading, also includes non-zero flags.
MAELYS_ERR_NOT_FOUNDManifest file, referenced policy file, or bundle entry not found.
MAELYS_ERR_INVALID_FIELDInvalid manifest JSON, policy source, or Public Query Whitelist validation failure.
MAELYS_ERR_UNSUPPORTEDUnknown domain.
MAELYS_ERR_FORBIDDENTest-only policy without flag, or non-whitelisted query at runtime.
MAELYS_ERR_IOFile read failure. File manifest loading only.
MAELYS_ERR_INTERNALInternal allocation failure.
MAELYS_ERR_PAYLOAD_TOO_LARGEStatic capacity or bounded field size exceeded.