C Low-level API
Registries
Low-level C operations for registries; concepts and integration code are documented separately.This page documents the transparent C functions behind the registries concepts. New application integrations should start with the stable C API; this reference is for consumers of the advanced, structure-level API.
Domain registry
The low-level maelys_datalog_domain_def_t and the stable
maelys_datalog_public_domain_t describe the same logical vocabulary,
but they are different C structs and registration functions. The low-level
struct additionally permits an installer callback and a description; the
stable struct accepts only a predicate array. Do not pass one type to the
other API.
The low-level declaration is:
typedef struct {
const char *domain_name;
const maelys_datalog_predicate_def_t *predicates;
size_t predicate_count;
const char *const *atoms;
size_t atom_count;
const char *description;
maelys_result_t (*install_predicates)(
maelys_datalog_predicate_registry_t *registry);
} maelys_datalog_domain_def_t;| Field | Meaning |
|---|---|
domain_name | Name selected by manifest or inline loading; documents in the common example. |
predicates, predicate_count | Static predicate table and its length. Use NULL, 0 for a callback-based domain. |
atoms, atom_count | Optional source-string vocabulary and its length. Use NULL, 0 when the policy has no quoted literal. |
description | Optional human-readable description. |
install_predicates | Alternative callback that installs predicates; NULL for a static-table domain. |
A domain provides its predicate vocabulary in exactly one of two ways:
| Method | Use when |
|---|---|
| Static predicate table | Vocabulary is fixed at compile time. |
| Installer callback | Installation needs custom logic or multiple modules. |
atoms is independent of that choice. It lists string literals appearing
in policy source, not runtime EDB values. In the common quickstart policy,
blocked(User) contains a variable, while blocked("mallory") arrives
later as an EDB fact, so atoms = NULL and atom_count = 0 are correct.
A policy-source fact such as blocked("mallory"). would instead require
"mallory" in this array. The registry copies names, predicate declarations,
and atom strings into bounded internal storage at registration; a callback
is retained as a function pointer.
Method 1: static predicate table
Example — document access
Use the same seven predicates as the quickstart.
Here blocked/1 is EDB, not a policy fact. The low-level declaration uses
maelys_datalog_predicate_def_t and MAELYS_DATALOG_PRED_KIND_*; the
stable declaration uses maelys_datalog_public_predicate_t and public
builder macros, but both define the same predicate roles.
#include <maelys_datalog.h>
static const maelys_datalog_predicate_def_t document_access_predicates[] = {
{"user", 1u, MAELYS_DATALOG_PRED_KIND_EDB},
{"owns", 2u, MAELYS_DATALOG_PRED_KIND_EDB},
{"delegated", 2u, MAELYS_DATALOG_PRED_KIND_EDB},
{"blocked", 1u, MAELYS_DATALOG_PRED_KIND_EDB},
{"can_read", 2u, MAELYS_DATALOG_PRED_KIND_IDB},
{"has_any_document", 1u,
MAELYS_DATALOG_PRED_KIND_IDB | MAELYS_DATALOG_PRED_KIND_QUERY},
{"allow", 2u,
MAELYS_DATALOG_PRED_KIND_IDB | MAELYS_DATALOG_PRED_KIND_QUERY},
};
static const maelys_datalog_domain_def_t document_access_domain_def = {
.domain_name = "documents",
.predicates = document_access_predicates,
.predicate_count = sizeof(document_access_predicates) /
sizeof(document_access_predicates[0]),
.atoms = NULL,
.atom_count = 0u,
.description = "Document access-control policy domain",
.install_predicates = NULL,
};The zero-atom choice does not omit a user or document: those symbols are
supplied later as request facts. The domain name documents matches the
stable example.
Method 2: installer callback
The same vocabulary can be installed through a callback instead of the
static-table fields. This is an alternative declaration, not a second
registration to perform under documents in the same process. The callback
may reuse the already defined seven-entry table:
static maelys_result_t install_document_access_predicates(
maelys_datalog_predicate_registry_t *registry)
{
for (size_t i = 0u; i < sizeof(document_access_predicates) /
sizeof(document_access_predicates[0]); ++i) {
const maelys_datalog_predicate_def_t *predicate =
&document_access_predicates[i];
maelys_result_t rc = maelys_datalog_predicate_registry_add_domain(
registry, predicate->name, predicate->arity,
predicate->kind_flags);
if (rc != MAELYS_OK) return rc;
}
return MAELYS_OK;
}
static const maelys_datalog_domain_def_t callback_domain_def = {
.domain_name = "documents",
.predicates = NULL,
.predicate_count = 0u,
.atoms = NULL,
.atom_count = 0u,
.description = "Document access-control policy domain",
.install_predicates = install_document_access_predicates,
};The callback installs predicates only; source atoms remain independent fields of the domain definition. The common policy needs none.
Registering a domain
Example — document access
Register one of the two declarations above before loading the policy. The static-table path uses:
maelys_result_t rc =
maelys_datalog_domain_registry_register(&document_access_domain_def);
if (rc != MAELYS_OK) return rc;Alternatively, pass &callback_domain_def in the same call. Registration is
global and first-registration-wins: a second declaration under documents
is ignored and does not replace the first one. Unlike the stable
maelys_datalog_domain_register(), this low-level call accepts
maelys_datalog_domain_def_t and returns maelys_result_t.
The registered name is used for a later inline load or manifest:
{"domain": "documents"}For a complete, executable one-call alternative,
maelys_datalog_load_policy_inline_with_static_domain() registers the
same seven-entry table and loads the same policy in one operation. See the
Full program
and inline loading.
Predicate registry
The predicate registry contains the predicates available to one loaded policy. It is populated from the selected domain definition, then frozen before the policy source is parsed. It does not authorize a different vocabulary: the domain remains the source of the names, arities and flags. The stable C loader performs these steps internally; this low-level API exposes them. A predicate declaration defines:
| Field | Meaning |
|---|---|
name | Predicate name, for example owns, shared_with, or allow. |
arity | Number of terms accepted by the predicate. |
kind_flags | Predicate role: runtime fact, derived fact, query-capable fact, or policy fact. |
Example:
maelys_datalog_predicate_registry_add_domain(
registry,
"allow",
2,
MAELYS_DATALOG_PRED_KIND_IDB |
MAELYS_DATALOG_PRED_KIND_QUERY);This declaration means:
allow/2 exists
allow/2 is derived by Datalog rules
allow/2 is capable of being queriedIt does not mean allow/2 is publicly observable for every manifest-loaded
deployment. Manifest-loaded policies still require allow/2 to be listed in
the Public Query Whitelist.
Predicate identity: one name, one arity
A predicate name expresses a unique relation with a fixed arity. The registry
enforces this as an identity contract: once owns is declared with arity 2,
no other declaration of owns with a different arity is accepted.
owns/2 EDB ✓ — the relation "user owns document"
edge/1 EDB ✓ — the relation "node exists"
edge/2 EDB ✗ — rejected: "edge" already has arity 1If the semantic relationship changes arity, the predicate must change name:
edge/1 node-exists relation
link/2 two-node relation — different name, different conceptThe registry returns MAELYS_ERR_INVALID_FIELD if the same name is registered
with two different arities. This is by design: the name of a predicate is
the contract of the relation it represents. Allowing the same name to mean two
different structural relations — one with one argument, one with two — would
make policy rules ambiguous and solver results unpredictable.
Registry lifecycle: open, then frozen
The predicate registry has exactly two states. When a ruleset is initialized the
registry is open: predicates may be added through
maelys_datalog_predicate_registry_add_domain (or by installing a domain). Once
the vocabulary is complete it is frozen, and the policy source is parsed and
validated against that frozen vocabulary. Freezing is the point of no return: no
predicate may be added after it.
ruleset_initRight after ruleset_init the registry is open and empty. It is ready to accept predicate declarations.
add_domainDeclare a predicate — its name, arity, and kind flags — into the open registry. Called once per predicate (or via a domain installer that batches these calls).
registry_freezeLock the vocabulary. After this point the set of predicates, their arities, and their kind flags are fixed. This is what makes “manifests never create predicates” true.
INVALID_STATEparse_ruleset_exThe parser validates every predicate name and arity in the policy source against the frozen registry. Any predicate not declared before freeze causes a parse failure.
The red branch is the security property in visual form: once the registry is frozen, any further attempt to add a predicate is refused. The vocabulary a policy may use is sealed before a single line of policy source is parsed.
void
maelys_datalog_predicate_registry_freeze(
maelys_datalog_predicate_registry_t *registry);In the manual ruleset pipeline, freeze sits between domain installation and
parsing:
/* Open registry: install the 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, "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: validated against the frozen vocabulary */
maelys_datalog_parse_ruleset_ex(&ruleset, src, src_len, file_path, &diag);Predicate kinds
| Kind | Meaning |
|---|---|
MAELYS_DATALOG_PRED_KIND_EDB | Runtime fact supplied by the caller. |
MAELYS_DATALOG_PRED_KIND_IDB | Derived fact produced by Datalog rules. |
MAELYS_DATALOG_PRED_KIND_QUERY | Observation capability combined with EDB, POLICY_FACT, or IDB to make that predicate queryable. |
MAELYS_DATALOG_PRED_KIND_POLICY_FACT | Fact declared by the policy source, not supplied by runtime callers. |
A predicate may combine flags:
MAELYS_DATALOG_PRED_KIND_IDB |
MAELYS_DATALOG_PRED_KIND_QUERYThis means the predicate is derived by the solver and has query capability. For manifest-loaded policies, public visibility is still controlled by the manifest Public Query Whitelist.
Loading paths
Once a domain is registered, it is available to both loading paths. The path
chosen determines whether the QUERY predicates declared by the domain are
subject to a Public Query Whitelist.
- Manifestqueries whitelist
- Policy setrestricted QUERY surface
- load_policy_inlineno whitelist
- Policy setall QUERY accessible
Manifest path — the manifest selects the registered domain, verifies policy
source integrity, and enforces a Public Query Whitelist through its queries
field. Only predicates explicitly listed there are publicly observable after
solving. See Manifest loading for the full model.
| API | Public Query Whitelist | Use when |
|---|---|---|
maelys_datalog_manifest_load_ex | Manifest queries field | Production, multi-policy, SHA, versioned deployments |
maelys_datalog_manifest_load_from_text | Manifest queries field | WASM, embedded, manifest model without filesystem |
Inline path — the domain name is passed directly to
maelys_datalog_load_policy_inline or
maelys_datalog_load_policy_inline_with_static_domain. There is no manifest
and no Public Query Whitelist: all predicates declared as QUERY by the
selected domain are accessible. See Manifest loading — Inline
loading for the full call signature.
| API | Register step | Use when |
|---|---|---|
maelys_datalog_load_policy_inline | Explicit, before load | Tests, examples, REPLs, embedded simple callers |
maelys_datalog_load_policy_inline_with_static_domain | Combined, idempotent | Static C tables, WASM, one-call convenience |
Loading API comparison
Manifest APIs — require explicit domain registration before load, enforce a Public Query Whitelist.
| API | Public Query Whitelist | Use when |
|---|---|---|
maelys_datalog_manifest_load_ex | Manifest queries field | Production, multi-policy, SHA, versioned deployments |
maelys_datalog_manifest_load_from_text | Manifest queries field | WASM, embedded, manifest model without filesystem |
Inline APIs — no manifest, no whitelist. All domain QUERY predicates accessible.
| API | Register step | Use when |
|---|---|---|
maelys_datalog_load_policy_inline | Explicit, before load | Tests, examples, REPLs, embedded simple callers |
maelys_datalog_load_policy_inline_with_static_domain | Combined, idempotent | Static C tables, WASM, one-call convenience |
Validation behavior
| Error case | Behavior |
|---|---|
| Unknown domain | Policy loading fails. |
| Policy uses an undeclared predicate | Policy loading or parsing fails. |
| Policy uses a predicate with the wrong arity | Policy loading or parsing fails. |
| Both methods provided in domain definition | MAELYS_ERR_INVALID_ARGUMENT. |
| Neither method provided in domain definition | MAELYS_ERR_INVALID_ARGUMENT. |
Manifest queries entry unknown to domain | Loading fails closed. |
Manifest queries entry arity mismatch | Loading fails closed. |
Manifest queries entry lacks QUERY flag | Loading fails closed. |
Manifest queries: [] or field absent | Nothing publicly queryable. |
| Runtime caller supplies an undeclared EDB predicate | Runtime fact insertion fails. |
| Caller queries a non-whitelisted predicate from a manifest-loaded policy | Query rejected. |
Caller queries a QUERY predicate from an inline-loaded policy | Accepted — no whitelist. |
Additional example — sensitivity-level policy
The following earlier variant of document access is kept for readers whose
policy writes blocked("mallory"). in source and uses a sensitivity level.
It is not the shared quickstart policy: blocked/1 is a policy fact here,
not a runtime EDB predicate. When loading this variant with the default atom
guard, the snippets below declare "mallory" in atoms with
atom_count = 1u. Its distinct domain name prevents accidental reuse of the
documents registration.
Static-table variant
A fixed domain can declare its predicates as a static table.
static const char *const sensitive_atoms[] = {"mallory"};
static const maelys_datalog_predicate_def_t sensitive_document_predicates[] = {
{
.name = "owns",
.arity = 2,
.kind_flags = MAELYS_DATALOG_PRED_KIND_EDB,
},
{
.name = "shared_with",
.arity = 2,
.kind_flags = MAELYS_DATALOG_PRED_KIND_EDB,
},
{
.name = "sensitivity_level",
.arity = 2,
.kind_flags = MAELYS_DATALOG_PRED_KIND_EDB,
},
{
.name = "blocked",
.arity = 1,
.kind_flags = MAELYS_DATALOG_PRED_KIND_POLICY_FACT,
},
{
.name = "allow",
.arity = 2,
.kind_flags = MAELYS_DATALOG_PRED_KIND_IDB |
MAELYS_DATALOG_PRED_KIND_QUERY,
},
};
static const maelys_datalog_domain_def_t sensitive_document_domain_def = {
.domain_name = "sensitive_document_access",
.predicates = sensitive_document_predicates,
.predicate_count = sizeof(sensitive_document_predicates) /
sizeof(sensitive_document_predicates[0]),
.atoms = sensitive_atoms,
.atom_count = 1u,
.description = "Document access-control policy domain",
};This form is convenient when the whole vocabulary is known at compile time.
Callback variant
A domain can also install its predicates through a callback.
static maelys_result_t
install_sensitive_document_predicates(
maelys_datalog_predicate_registry_t *registry)
{
maelys_result_t rc;
rc = maelys_datalog_predicate_registry_add_domain(
registry,
"owns",
2,
MAELYS_DATALOG_PRED_KIND_EDB);
if (rc != MAELYS_OK) {
return rc;
}
rc = maelys_datalog_predicate_registry_add_domain(
registry,
"shared_with",
2,
MAELYS_DATALOG_PRED_KIND_EDB);
if (rc != MAELYS_OK) {
return rc;
}
rc = maelys_datalog_predicate_registry_add_domain(
registry,
"sensitivity_level",
2,
MAELYS_DATALOG_PRED_KIND_EDB);
if (rc != MAELYS_OK) {
return rc;
}
rc = maelys_datalog_predicate_registry_add_domain(
registry,
"blocked",
1,
MAELYS_DATALOG_PRED_KIND_POLICY_FACT);
if (rc != MAELYS_OK) {
return rc;
}
return maelys_datalog_predicate_registry_add_domain(
registry,
"allow",
2,
MAELYS_DATALOG_PRED_KIND_IDB |
MAELYS_DATALOG_PRED_KIND_QUERY);
}
static const maelys_datalog_domain_def_t sensitive_document_domain_def = {
.domain_name = "sensitive_document_access",
.predicates = NULL,
.predicate_count = 0,
.atoms = sensitive_atoms,
.atom_count = 1u,
.description = "Document access-control policy domain",
.install_predicates = install_sensitive_document_predicates,
};predicates = NULL and predicate_count = 0 indicate the callback path.
This form is useful when predicates come from multiple C modules, require
conditional installation, or need shared setup logic.