C Advanced API
Event windows
Use the native last-N and multi-fact window adapters with the public C facade.When a window is useful
Suppose an application must decide whether three failed attempts occurred among the most recent ten observations. Ordinary solves receive a complete fact snapshot: the application would otherwise have to retain the last ten events, remove the oldest, rebuild the request EDB and solve it again. A window adapter manages that bounded snapshot and calls the same solver for each accepted push. The policy and its meaning do not change.
Choose the adapter by what one push represents:
- Last-N single-fact window: each push is one occurrence. The adapter gives it an integer occurrence ID, so two identical observations can still be distinct events. Use it when the rule is about the most recent N individual events.
- Multi-fact group window: each push is a group of zero or more complete facts, such as all observations from one request. The oldest group expires together; identical facts are deduplicated in the union sent to the solver. Use it when observations must be retained and expired as a unit.
These are specialized native C adapters in separate installed headers, not functions of <maelys/datalog_advanced.h>. They use the same opaque policy, session and result handles as the C Stable API, but belong in the Advanced navigation because they add lifecycle, storage and concurrency obligations. They are not Datalog syntax or incremental maintenance. Read the window concept for the event-versus-fact distinction.
| Release | Include | Event submitted | Main call |
|---|---|---|---|
| 0.8.0 | <maelys/datalog_window.h> | Predicate and zero to three values; the adapter prepends an integer occurrence ID. | maelys_datalog_window_push() |
| 0.9.0 | <maelys/datalog_group_window.h> | An array of zero or more complete typed facts. | maelys_datalog_group_window_push() |
Both headers are installed with the native SDK and use the opaque policy, session and result types from <maelys/datalog.h>. Python and JavaScript/WASM do not expose these adapters.
One push, one published snapshot
For the failed-attempt example, a successful push produces a new answer for the current retained window. If an input or capacity check rejects the push, the previous input and result remain usable: no half-updated decision is published. The adapter reruns ordinary solving over the retained snapshot; it does not maintain Datalog derivations incrementally. Capacity counts stored events or contributions, not elapsed time, so this is not a clock-based sliding window.
Example — detect repeated document denials
Use the document-access policy and six-request trace from the concept page. We retain five requests and query whether Mallory has at least three denials in that history. This additional example deliberately keeps ownership, delegation and blocked status in policy facts; it is not an adapter for updating current permissions.
The five C fragments below form one program, in order. Save them together as window.c. Every handle is declared before it is used. Setup reserves memory once; the loop then submits requests and borrows the corresponding answers.
Step 1 — Prepare the policy and two sessions
The window needs two sessions because it must retain the old answer while evaluating a proposed replacement. Both are created from policy index 0 of the same loaded policy. These are not two independent policies.
The quoted strings below belong to the policy source, so the domain declares them in atoms. Request strings supplied later at runtime do not need that source declaration.
#include <maelys/datalog_window.h>
#include <maelys/datalog_builders.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
int exit_code = 1;
maelys_datalog_status_t rc = MAELYS_DATALOG_STATUS_OK;
maelys_datalog_diagnostic_t diagnostic =
maelys_datalog_diagnostic_initializer();
maelys_datalog_policy_t *policy = NULL;
maelys_datalog_session_t *session_a = NULL;
maelys_datalog_session_t *session_b = NULL;
maelys_datalog_window_t *window = NULL;
void *storage = NULL;
const maelys_datalog_predicate_t predicates[] = {
MAELYS_DATALOG_POLICY_FACT("owns", 2),
MAELYS_DATALOG_POLICY_FACT("delegated", 2),
MAELYS_DATALOG_POLICY_FACT("blocked", 1),
MAELYS_DATALOG_EDB("request", 3),
MAELYS_DATALOG_IDB("can_read", 2),
MAELYS_DATALOG_IDB("requested", 2),
MAELYS_DATALOG_IDB("denied", 3),
MAELYS_DATALOG_IDB_QUERY("denials", 3),
MAELYS_DATALOG_IDB_QUERY("alert", 2),
};
const char *atoms[] = {"alice", "bob", "mallory", "roadmap.pdf"};
const maelys_datalog_domain_t domain =
MAELYS_DATALOG_DOMAIN_WITH_ATOMS("window_documents", predicates, atoms);
const char *source =
"owns(\"alice\", \"roadmap.pdf\").\n"
"owns(\"mallory\", \"roadmap.pdf\").\n"
"delegated(\"bob\", \"roadmap.pdf\").\n"
"blocked(\"mallory\").\n"
"can_read(User, Doc) :- owns(User, Doc) or delegated(User, Doc),"
" not(blocked(User)).\n"
"requested(User, Doc) :- request(_, User, Doc).\n"
"denied(Id, User, Doc) :- request(Id, User, Doc),"
" not(can_read(User, Doc)).\n"
"denials(User, Doc, N) :- requested(User, Doc),"
" count(Id, denied(Id, User, Doc), N).\n"
"alert(User, Doc) :- denials(User, Doc, N), N >= 3.\n";
rc = maelys_datalog_domain_register(&domain);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
rc = maelys_datalog_policy_load_inline(
domain.name, "document_history", source, strlen(source),
&policy, &diagnostic);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
rc = maelys_datalog_session_create(policy, 0u, &session_a);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
rc = maelys_datalog_session_create(policy, 0u, &session_b);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;The declarations and SYMBOL initializers used here are the optional builders from <maelys/datalog_builders.h>. Window operations themselves remain ordinary typed C functions. If you want explanation workspaces, configure each session independently before creating it; the two sessions cannot share one explanation arena.
Step 2 — Reserve the window's input storage
Ask the SDK for the storage size rather than guessing a buffer budget. Five events bounds the retained history; 128 text bytes per bank is a separate budget for interned predicate names and symbol strings. A bank is one complete input buffer: one holds the committed history, the other holds a candidate.
size_t bytes = 0, alignment = 0;
rc = maelys_datalog_window_storage_requirements(
5u, 128u, &bytes, &alignment);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
storage = aligned_alloc(alignment, bytes);
if (storage == NULL) {
fputs("Cannot reserve window storage\n", stderr);
goto cleanup;
}This program makes one application allocation for the window during setup. aligned_alloc supplies the requested alignment; the size returned by this adapter is aligned as well. The adapter does not allocate that memory for you and will not grow it later. An embedded application may instead supply a suitably aligned region it already owns, with at least the queried number of bytes. Policy/session/result and optional explanation storage are additional, not included in bytes.
Step 3 — Initialize the empty history
0u below is the first occurrence ID, not a policy index. It starts the sequence at zero. The adapter immediately solves an empty runtime history using the policy's fixed facts, so initialization must also have its status checked.
rc = maelys_datalog_window_init(
storage, bytes, 5u, 128u, 0u,
session_a, session_b, &window, &diagnostic);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;From this point until closing the window, do not independently solve, free or share either session. The adapter manages their alternating use.
Step 4 — Push each request, then query its snapshot
Supply two values, the user and document. The adapter prepends its ID, producing a three-term request(Id, User, Doc) fact. It performs the solve inside push: there is no extra session_solve to call.
Only borrow and use the new result after a successful push. An absent alert is a successful negative answer; a failed push or query is a processing error, not that answer.
const char *users[] = {"mallory", "mallory", "alice", "mallory", "bob", "bob"};
for (size_t i = 0; i < sizeof(users) / sizeof(users[0]); ++i) {
const maelys_datalog_value_t values[] = {
MAELYS_DATALOG_SYMBOL(users[i]),
MAELYS_DATALOG_SYMBOL("roadmap.pdf"),
};
uint32_t occurrence = 0;
rc = maelys_datalog_window_push(
window, "request", values, 2u, &occurrence, &diagnostic);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
maelys_datalog_result_t *result = NULL;
rc = maelys_datalog_window_result(window, &result);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
const maelys_datalog_value_t query[] = {
MAELYS_DATALOG_SYMBOL("mallory"),
MAELYS_DATALOG_SYMBOL("roadmap.pdf"),
};
int present = 0;
rc = maelys_datalog_result_query(result, "alert", query, 2u, &present);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
printf("request=%u user=%s mallory-alert=%s\n",
occurrence, users[i], present ? "yes" : "no");
/* The window owns result; never free it independently. */
}Expected output:
request=0 user=mallory mallory-alert=no
request=1 user=mallory mallory-alert=no
request=2 user=alice mallory-alert=no
request=3 user=mallory mallory-alert=yes
request=4 user=bob mallory-alert=yes
request=5 user=bob mallory-alert=noThe last no is not a permission change. The first denial has expired, leaving two Mallory denials among the five retained requests. If a push fails, the earlier result still describes the earlier history. Never use it as though the rejected new request had been evaluated.
Step 5 — Close before releasing the sessions and storage
Closing the window releases its result and returns exclusive use of the two sessions to the application. A prepared explanation can prevent that close; release it first. The example creates no prepared explanation handles.
exit_code = 0;
cleanup:
if (rc != MAELYS_DATALOG_STATUS_OK) {
fprintf(stderr, "Window example failed: %s\n",
maelys_datalog_status_name(rc));
}
if (window != NULL) {
maelys_datalog_status_t close_rc = maelys_datalog_window_free(window);
if (close_rc != MAELYS_DATALOG_STATUS_OK) {
fprintf(stderr, "Window close failed: %s\n",
maelys_datalog_status_name(close_rc));
return 1; /* Do not free borrowed sessions or storage after a failed close. */
}
}
if (session_b != NULL) maelys_datalog_session_free(session_b);
if (session_a != NULL) maelys_datalog_session_free(session_a);
if (policy != NULL) maelys_datalog_policy_free(policy);
free(storage);
return exit_code;
}After installing the published native SDK, compile with its include/library directories:
cc -std=c11 -Wall -Wextra -I "$MAELYS_SDK/include" \
window.c "$MAELYS_SDK/lib/libmaelys_datalog.a" -o window
./windowSet MAELYS_SDK to your SDK installation directory. These adapters have no Python or JavaScript/WASM binding in the version served by this site. Those applications can retain a history themselves and submit ordinary snapshots, but cannot call the native adapters through a documented binding.
Alternative — submit a group of complete facts
Keep the same domain, policy and two-session preparation from Step 1. Replace its include with <maelys/datalog_group_window.h> and its handle declaration with maelys_datalog_group_window_t *window = NULL;. Replace Steps 2–5 with the following fragment. Save this alternative as a separate program, group-window.c; it is not extra code to run inside the already active single-fact window.
Here a group contains several requests. The application supplies occurrence IDs 100, 101, and 102 explicitly. All three belong to Mallory. A second group repeats only request 100, showing why contribution counts and distinct facts are different.
The capacity choices follow that trace: two retained groups; four contributions for A's three facts plus B's one repeated fact; three distinct union facts. The text budget remains independent. A duplicate needs a contribution slot even when it adds no distinct fact.
const maelys_datalog_group_window_capacities_t capacities = {
.groups = 2u,
.contributions = 4u,
.unique_facts = 3u,
.text_bytes = 128u,
};
size_t bytes = 0, alignment = 0;
rc = maelys_datalog_group_window_storage_requirements(
&capacities, &bytes, &alignment);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
storage = aligned_alloc(alignment, bytes);
if (storage == NULL) {
fputs("Cannot reserve group-window storage\n", stderr);
goto cleanup;
}
rc = maelys_datalog_group_window_init(
storage, bytes, &capacities, 0u,
session_a, session_b, &window, &diagnostic);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
maelys_datalog_fact_t facts[3] = {
{.predicate = "request", .arity = 3u},
{.predicate = "request", .arity = 3u},
{.predicate = "request", .arity = 3u},
};
for (size_t i = 0; i < 3u; ++i) {
const maelys_datalog_value_t terms[] = {
{.kind = MAELYS_DATALOG_VALUE_INTEGER, .as.integer = 100 + (int64_t)i},
MAELYS_DATALOG_SYMBOL("mallory"),
MAELYS_DATALOG_SYMBOL("roadmap.pdf"),
};
memcpy(facts[i].terms, terms, sizeof(terms));
}
for (size_t step = 0; step < 4u; ++step) {
/* A: three facts; B: the first fact again; C and D: empty. */
size_t fact_count = step == 0u ? 3u : step == 1u ? 1u : 0u;
uint32_t group_id = 0;
rc = maelys_datalog_group_window_push(
window, fact_count ? facts : NULL, fact_count, &group_id, &diagnostic);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
maelys_datalog_result_t *result = NULL;
rc = maelys_datalog_group_window_result(window, &result);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
const maelys_datalog_value_t query[] = {
MAELYS_DATALOG_SYMBOL("mallory"),
MAELYS_DATALOG_SYMBOL("roadmap.pdf"),
};
int present = 0;
rc = maelys_datalog_result_query(result, "alert", query, 2u, &present);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
maelys_datalog_group_window_usage_t usage;
rc = maelys_datalog_group_window_state(window, &usage);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
printf("group=%u contributions=%zu facts=%zu mallory-alert=%s\n",
group_id, usage.contributions, usage.unique_facts,
present ? "yes" : "no");
}
exit_code = 0;
cleanup:
if (rc != MAELYS_DATALOG_STATUS_OK) {
fprintf(stderr, "Group example failed: %s\n", maelys_datalog_status_name(rc));
}
if (window != NULL) {
maelys_datalog_status_t close_rc = maelys_datalog_group_window_free(window);
if (close_rc != MAELYS_DATALOG_STATUS_OK) {
fprintf(stderr, "Group close failed: %s\n",
maelys_datalog_status_name(close_rc));
return 1;
}
}
if (session_b != NULL) maelys_datalog_session_free(session_b);
if (session_a != NULL) maelys_datalog_session_free(session_a);
if (policy != NULL) maelys_datalog_policy_free(policy);
free(storage);
return exit_code;
}Expected output:
group=0 contributions=3 facts=3 mallory-alert=yes
group=1 contributions=4 facts=3 mallory-alert=yes
group=2 contributions=1 facts=1 mallory-alert=no
group=3 contributions=0 facts=0 mallory-alert=noAfter group B, request 100 has two supplying groups but remains one fact. Empty C expires A; request 100 remains through B, while 101 and 102 disappear. Empty D then expires B. At that point there is no Mallory/document pair in requested, so there is no denials fact for that pair—not a newly invented denials(..., 0).
The same API can group a request and its contextual facts instead. Include a shared explicit request ID in those facts, as shown in the concept page. Group retention alone does not isolate rule evaluation from other retained groups.
Shared lifecycle
The adapters borrow two distinct, idle sessions with matching execution fingerprints. One holds the current result while the other evaluates the candidate. A result returned by a window is borrowed: do not free it independently. Finish any prepared explanation before pushing again or closing the window. Configure and release the two sessions outside the window lifetime.
- Register the domain and load the policy as usual. Create two distinct, idle sessions for the same selected policy, backend, options and profile. Their execution fingerprints must match. Configure separate explanation storage for each session before giving them to a window.
- Ask the chosen
*_storage_requirements()function for exact bytes and alignment. Reserve an exclusive, immovable, suitably aligned arena. This covers adapter storage; session, result, provenance and optional explanation storage are additional. - Initialize the adapter with that arena and both sessions. Initialization
evaluates the empty runtime EDB and can fail. On success the adapter borrows
exclusive use of both sessions until its
*_free()succeeds. - Push an event or group. A successful call publishes the new complete input, result and identifier together. A failed call leaves the committed input, result and identifier untouched; the candidate scratch may have changed.
- Borrow the current result through
*_result(). Query or explain it normally, but never callmaelys_datalog_result_free()on this borrowed result. A successful push or close invalidates it; a failed push does not. Release any prepared explanation before another push or close. - Close the window, then free both sessions and release the caller's arena. The window never frees or erases that arena itself.
All access, including queries against the borrowed result, must be serialized. Neither adapter allocates or frees memory in its own init/push/read/close path; that statement does not cover session creation, policy compilation, legacy explanation rendering, custom backends or application code.
The adapters use caller-owned, fixed storage and do not allocate during their own initialize/push/read/close operations. Session creation, policy loading, explanation text or application code can still allocate. Each candidate is a full solve, so these APIs do not promise incremental performance.
Last-N single-fact window (0.8.0)
maelys_datalog_window_storage_requirements(event_capacity, text_capacity, &bytes, &alignment) sizes storage for two event buffers. The text capacity
applies per buffer. maelys_datalog_window_init(...) receives the two
sessions and an initial occurrence ID from 0 through INT32_MAX.
maelys_datalog_window_push(window, predicate, values, value_count, &occurrence, &diagnostic) accepts zero to three supplied values. The adapter
precedes them with its generated integer ID, so the registered predicate's
arity must be one greater than value_count. A rejected push does not consume
an ID. IDs never wrap or recycle; once INT32_MAX has been accepted, further
pushes fail with PAYLOAD_TOO_LARGE.
Use maelys_datalog_window_events() for ordered committed input,
maelys_datalog_window_state() for count and next ID, and
maelys_datalog_window_text_usage() for interned predicate/symbol bytes in
the committed buffer. That text measurement excludes scratch and session
memory; free text does not prove that a future solve meets every other limit.
The engine's last-N design and validation describe this adapter's transaction and storage contract in detail.
Multi-fact group window (0.9.0)
maelys_datalog_group_window_capacities_t declares four independent bounds:
| Field | Meaning |
|---|---|
groups | Maximum retained groups; an empty group still occupies one slot. |
contributions | Maximum retained raw facts, including duplicates. At most the loaded MAX_EDB_FACTS. |
unique_facts | Maximum distinct facts in the union submitted to the solver. |
text_bytes | Interned predicate and symbol bytes per input buffer, including NULs. |
Query maelys_datalog_group_window_storage_requirements() before reserving
the arena, then initialize with maelys_datalog_group_window_init(). A push
passes an array of complete maelys_datalog_fact_t values:
maelys_datalog_status_t rc = maelys_datalog_group_window_push(
window, facts, fact_count, &group_id, &diagnostic);facts == NULL is valid only when fact_count == 0: the resulting empty
group still advances the cursor and can expire an older group. Group IDs
are metadata, not Datalog terms. Equal facts within or across groups appear
once in the runtime union and survive until their last contributing group
expires. Put an occurrence term in supplied facts yourself if two otherwise
equal observations need to remain distinct to Datalog.
maelys_datalog_group_window_state() reports committed groups,
contributions, distinct union facts, text bytes and next ID. The *_groups(),
*_contributions() and *_facts() functions return borrowed views. They are
invalidated by a successful push or close, and preserved by a rejected push.
For a complete, compiled C program using this API, see the installed-SDK example. The engine's multi-fact design and validation explain contribution accounting and atomic publication.
Boundaries worth remembering
- Each accepted push recomputes a complete input snapshot. Neither adapter is an incremental-maintenance engine or a timestamped/sliding-time window.
- A full window may expire its oldest event as it accepts a replacement; admission is checked against the final retained suffix plus the new event.
- A prepared explanation can block result replacement with
INVALID_STATE. Release it and retry the same input. Do not treat this as a partial commit. - A multi-fact group's raw contribution limit can be exhausted even when deduplication leaves a small union. Storage never grows implicitly.
Verify these contracts
The published engine has single-fact window tests and group-window tests for expiry, rejection, independent snapshot comparison and borrowed-result lifetimes. Separate single-fact and group allocation tests exercise the runtime with engine allocators disabled after session creation. These tests establish correctness and allocation properties, not a throughput guarantee.
Working model
What you have learned- Prepare once, push repeatedly
Load the policy, create two matching idle sessions, reserve adapter storage and initialize one window.
- Let push perform the solve
Check its status before borrowing the new result. There is no separate solve call to make on the borrowed sessions.
- Respect the borrowed result
Query it without freeing it. A successful push or close invalidates it; a rejected push preserves the earlier snapshot.
- Close in ownership order
Release prepared explanations, close the window, then release its two sessions and the application storage.