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.
/* 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.
/* 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.
#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.
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);| Parameter | Meaning |
|---|---|
ruleset | Ruleset to initialize. Must not already be loaded. |
policy_id | Stable policy identifier, e.g. "document_access.main". |
domain | Domain name, e.g. "document_access". |
sha256 | Lowercase hex SHA-256 of the policy source, e.g. "a3f7c291...". |
test_only | Non-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 code | Meaning |
|---|---|
MAELYS_OK | Initialized. |
MAELYS_ERR_INVALID_ARGUMENT | A 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_STATE | Ruleset already loaded. |
MAELYS_ERR_PAYLOAD_TOO_LARGE | policy_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:
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.
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);| Parameter | Meaning |
|---|---|
ruleset | Initialized ruleset with a frozen predicate registry. |
src | Policy source bytes. Does not need to be NUL-terminated. |
len | Byte length of src. Authoritative — use sizeof(arr) - 1 for string literals. |
file_path | Optional path used in diagnostics, e.g. "policies/document_access.dl". May be NULL. |
out_diag | Optional structured diagnostic on parse failure. May be NULL. |
The simple variant omits file_path and out_diag:
maelys_result_t
maelys_datalog_parse_ruleset(
maelys_datalog_ruleset_t *ruleset,
const char *src,
size_t len);| Return code | Meaning |
|---|---|
MAELYS_OK | Parsed successfully. |
MAELYS_ERR_INVALID_STATE | Ruleset not initialized or registry not frozen. |
MAELYS_ERR_INVALID_FIELD | Policy text violates grammar, predicate, typing, or safety rules. |
MAELYS_ERR_PAYLOAD_TOO_LARGE | A 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.
void
maelys_datalog_ruleset_clear(
maelys_datalog_ruleset_t *ruleset);Parser diagnostics
When out_diag is provided, parse failures include structured context.
| Diagnostic | Meaning |
|---|---|
PARSER_UNKNOWN_PREDICATE | Predicate name not found in the frozen registry. |
PARSER_ARITY_MISMATCH | Predicate found but arity does not match the declaration. |
PARSER_RULE_HEAD_EDB_FORBIDDEN | EDB or POLICY_FACT predicate used as a rule head. |
PARSER_FACT_USES_NON_BASE_PREDICATE | Direct fact used a non-POLICY_FACT predicate. |
PARSER_UNSAFE_VARIABLE | Head or negated variable not bound by a positive body atom. |
PARSER_INVALID_COMPARISON | Comparison is malformed or type-invalid. |
POLICY_NOT_STRATIFIABLE | Negation cannot be stratified — recursion through negation. |
Each diagnostic also includes file path, line, column, offending token, and a human-readable hint when available.