WASM Bindings
██╗ ██╗ █████╗ ███████╗███╗ ███╗ ██║ ██║██╔══██╗██╔════╝████╗ ████║ ██║ █╗ ██║███████║███████╗██╔████╔██║ ██║███╗██║██╔══██║╚════██║██║╚██╔╝██║ ╚███╔███╔╝██║ ██║███████║██║ ╚═╝ ██║ ╚══╝╚══╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
Maelys DL compiles to WebAssembly via Emscripten. This page covers the JavaScript API, loading strategies, and web page integration.
The underlying concepts — EDB facts, predicate kinds, solving, querying, manifests — are the same in WASM and C, documented once at the C level. See Bindings overview for the concept-to-page map and how this binding compares to the native Python binding.
This page covers the JavaScript surface only: targets, loading strategies,
web integration, and the MaelysPlayground API.
Three WASM targets
| Target file | Module name | Domain source | Loading path used |
|---|---|---|---|
maelys_datalog.js | MaelysDatalog | Compiled into the module in C. Smallest binary. | Inline, static |
maelys_datalog_examples.js | MaelysDatalogExamples | Built-in example domains (graph, decision). For demos. | Inline, static |
maelys_datalog_dynamic.js | MaelysDatalogDynamic | Defined at runtime from JavaScript. Includes domain builder. | Inline, dynamic |
Loading path
There are two ways to get a policy loaded: the Manifest path (JSON
manifest, SHA-256 verification, Public Query Whitelist) and the Inline
path (raw .dl text, no manifest, no whitelist). Each path has two
variants. WASM uses three of the four combinations — the fourth,
Manifest/file, does not apply in a browser (no filesystem to read a
manifest or policy file from):
| Path | Variant | C API |
|---|---|---|
| Manifest | file | maelys_datalog_manifest_load_ex — not used in WASM, no filesystem in the browser. |
| Manifest | buffer | maelys_datalog_manifest_load_from_text → supported, see Manifest path, buffer variant below. |
| Inline | static | maelys_datalog_load_policy_inline_with_static_domain → supported, see Inline path, static variant below. |
| Inline | dynamic | maelys_datalog_load_policy_inline → supported, see Inline path, dynamic variant below. |
See Registries — Loading paths for the full model behind all four, and Python bindings — Loading path for how the native Python binding uses only the Inline/dynamic row.
Inline path, dynamic variant — the domain builder
Use the dynamic target when the domain is defined at runtime from JavaScript. This is the right choice for playgrounds, REPLs, or any context where the predicate vocabulary is not known at build time.
MaelysPlayground
MaelysPlayground is a JavaScript class that wraps MaelysDatalogDynamic. It
hides pointer management, struct construction, symbol interning, and solve
result lifecycle from JS callers.
Factory
import MaelysDatalogDynamic from '/wasm/maelys_datalog_dynamic.js'
import { MaelysPlayground, PredKind } from '/wasm/maelys_playground.js'
const wasmUrl = '/wasm/maelys_datalog_dynamic.wasm'
const pg = await MaelysPlayground.create(MaelysDatalogDynamic, wasmUrl)MaelysPlayground.create(factory, wasmUrl?) loads the module and returns a
ready instance. wasmUrl tells Emscripten where to find the .wasm file
when it is not in the same directory as the .js file. Always pass it when
serving from a non-root path.
Build limits
pg.buildLimits()
// {
// maxSymbols, stringPoolBytes, maxPredicates, maxRules, maxArity,
// maxBodyLiterals, maxDepth, maxEdbFacts, maxIdbFacts, maxFactsPerPred
// }Returns the active build profile's observable capacities — compile-time
constants, not runtime occupancy. Does not depend on a loaded domain or a
solved EDB; callable immediately after MaelysPlayground.create().
Predicate kind flags
PredKind.EDB // 1 — runtime fact supplied by the caller
PredKind.IDB // 2 — derived by Datalog rules
PredKind.QUERY // 4 — inspectable through the query API
PredKind.POLICY_FACT // 8 — declared by the policy sourceCombine with |: PredKind.IDB | PredKind.QUERY.
Domain definition
One domain per MaelysPlayground instance. After domainCommit(), the domain
is permanent for the lifetime of the module.
pg.domainBegin('document_access')
pg.domainAddPredicate('owns', 2, PredKind.EDB)
pg.domainAddPredicate('allow', 2, PredKind.IDB | PredKind.QUERY)
pg.domainCommit()| Method | Description |
|---|---|
domainBegin(name) | Start declaring a domain. Throws if already committed. |
domainAddPredicate(name, arity, kindFlags) | Add one predicate. |
domainCommit() | Register the domain. Permanent — cannot be undone. |
domainAbort() | Cancel the current build. No-op if idle. |
One domain per module instance. After domainCommit(), calling domainBegin()
again throws. To use a different domain, create a new MaelysPlayground instance.
Each MaelysPlayground.create() instantiates a new WASM module, with its
own linear memory. The domain declared through
domainBegin/domainAddPredicate/domainCommit lives in that memory,
isolated from every other instance:
const pg1 = await MaelysPlayground.create(MaelysDatalogDynamic, wasmUrl)
pg1.domainBegin('document_access') // ...
pg1.domainCommit()
const pg2 = await MaelysPlayground.create(MaelysDatalogDynamic, wasmUrl)
pg2.domainBegin('document_access') // works fine — pg2 has its OWN
pg2.domainCommit() // memory, independent of pg1Two instances mean two completely separate domain spaces, for free — no
registry to share, no name collision to worry about. This is the opposite of
the native Python binding, where the domain registry is process-wide:
creating a second Engine() does not get you a second isolated domain
space, since both share the same registry. See
Python bindings — Domain definition
for that contrast in detail.
Ruleset loading
pg.loadRuleset('document_access', 'doc.main', `
allow(User, Document) :- owns(User, Document).
`)loadRuleset computes the source's UTF-8 byte length internally — pass the
source string directly, nothing to size by hand. (The lengthBytesUTF8 vs
src.length distinction only matters if you call the raw C export
load_ruleset yourself, where you pass the byte length — see the
raw C exports below.)
The .dl source accepts the full policy source language, including the bounded
or disjunction in rule bodies.
Throws with diagMessage on parse or validation failure.
EDB: adding facts
pg.edbBegin() // reset EDB session
pg.addFact('owns', 'alice') // 1-arity symbol fact
pg.addFact2('owns', 'alice', 'roadmap') // 2-arity symbol factEDB facts use the open atom path — symbol values such as "alice" are
interned at evaluation time and do not need to be pre-declared. See
Runtime EDB — Inline loading and WASM.
edbBegin() resets the session and auto-frees any previous solve result.
Boundary errors — thrown TypeError / RangeError
The batch and intern helpers validate their JavaScript arguments before
marshalling anything into WASM, and throw a built-in exception when the shape
is wrong — a TypeError for a non-array, a non-string element, or an
odd-length flat pair array, and a RangeError for an over-large packed buffer.
They fire before any WASM call, so no engine state changes; a boundary error
signals a bug in the calling code. (The single-fact and query methods do not
pre-validate in JS — Emscripten coerces their string arguments, and a bad value
surfaces as an engine failure below.)
Engine failures — thrown Error + diagCode / diagMessage
Every failing engine call throws — there is no silent failure to poll for,
so the catch always fires. When a well-formed call is rejected, the wrapper
method throws a plain JavaScript Error whose message already carries the
failing call and the maelys_result_t return code (for example
querySymbol2 failed (rc=-2): …). To get the fields individually, read the
last diagnostic off the instance after catching the throw:
pg.diagMessage // string — last diagnostic message
pg.diagHint // string — corrective hint, '' if none
pg.diagCode // number — last DIAGNOSTIC code (maelys_datalog_diag_code_t)Mind the two distinct codes — unlike Python's single exc.code. The thrown
Error's rc is the maelys_result_t (Error codes table;
MAELYS_ERR_INVALID_FIELD is -2), while pg.diagCode is the finer-grained
lexer / parser / manifest diagnostic code
(Diagnostic codes) — a different enum.
See Diagnostics for the accessors.
A querySymbol that returns false is not an error — it is a valid
"fact not present" answer. Only a hard failure (for example querying before a
successful solve()) throws.
Handling both
Both layers surface as thrown exceptions, so one try / catch covers
them; discriminate by type:
try {
pg.addRuntimeSymbolFacts('user', ['alice', 'bob'])
pg.solve()
} catch (err) {
if (err instanceof TypeError || err instanceof RangeError) {
throw err // wrong-shaped call — a bug to fix
}
// engine failure: err.message already summarizes it;
// pg.diag* gives the structured fields for the error just caught
console.error(err.message, pg.diagCode, pg.diagHint)
}This differs from Python, where shape errors (TypeError / ValueError) and
engine errors (MaelysDatalogError) are separate exception hierarchies caught
in separate clauses. In WASM every failure is a thrown Error, told apart by
instanceof and the diag* accessors.
Inserting by pre-interned symbol ID
The string helpers above (addFact, addFact2) are ergonomic sugar. Under
the hood they intern the string and delegate to an ID-based path, which is
also exposed directly. Use the ID path when the same symbol is inserted many
times (dense graphs, repeated nodes): intern once, reuse the integer handle,
and skip re-marshalling the string across the JS/WASM boundary on every fact.
pg.edbBegin()
const alice = pg.internRuntimeSymbol('alice') // → positive int32 handle
const doc = pg.internRuntimeSymbol('doc.pdf')
pg.addSymbolIdFact('safe', alice) // 1-arity, by id
pg.addSymbolIdsFact('owns', alice, doc) // 2-arity, by two ids
pg.solve()| Method | Description |
|---|---|
internRuntimeSymbol(text) | Intern text into the current EDB, returns a 1-based int32 handle (0 is never returned for success). Throws on non-OPEN state or invalid argument. |
addSymbolIdFact(pred, id) | Add a 1-arity fact using a handle from internRuntimeSymbol. |
addSymbolIdsFact(pred, left, right) | Add a 2-arity fact using two handles. |
The string and ID paths are semantically equivalent: addFact('safe', 'alice')
produces the same EDB as addSymbolIdFact('safe', internRuntimeSymbol('alice')).
Interning is idempotent within one EDB session — interning "alice" twice
returns the same handle.
Symbol handles are scoped to the EDB session they were interned in.
edbBegin() resets the session, after which the first intern returns 1
again. Do not carry a handle across an edbBegin(); intern again in the new
session. The boundary validates every handle (rejects 0, negatives, and
out-of-range values) against the session's own symbol table, so a stale handle
from a previous session is rejected rather than silently misresolved.
Batch insertion
The single-fact methods cross the JS/WASM boundary once per fact. The batch methods insert many facts in one validated, atomic core call — one boundary crossing for the whole group. Two families: by symbol id and from runtime strings.
// By symbol id (intern once, reuse handles)
const alice = pg.internRuntimeSymbol('alice')
const bob = pg.internRuntimeSymbol('bob')
pg.addSymbolIdFacts('user', [alice, bob]) // unary
pg.addSymbolIdsFacts('owns', [alice, 1, bob, 2]) // binary: flat pairs
// From runtime strings (interned for you, then batch-inserted)
pg.addRuntimeSymbolFacts('user', ['alice', 'bob', 'carol'])
pg.addRuntimeSymbolPairFacts('owns', ['alice', 'doc.pdf', 'bob', 'img.png'])| Method | Description |
|---|---|
addSymbolIdFacts(pred, ids) | Batch unary facts from an array of symbol-id handles. |
addSymbolIdsFacts(pred, pairs) | Batch binary facts from a flat array of handles ([l0,r0,l1,r1,…], even length). |
addRuntimeSymbolFacts(pred, values) | Batch unary facts from a string[]; interns each then inserts once. |
addRuntimeSymbolPairFacts(pred, flatPairs) | Batch binary facts from a flat string[] ([l0,r0,l1,r1,…], even length). |
The string batch marshals one packed NUL-terminated UTF-8 buffer across the
boundary with a single _malloc/_free. Inputs are validated before any
WASM call: a non-array, a non-string element, or an odd-length flat pair array
throws a TypeError; an over-large buffer throws a RangeError.
EDB insertion is atomic; symbol interning is monotonic and is not rolled back. A capacity failure inserts zero EDB facts, but strings interned before a mid-batch failure may remain in the symbol table. The WASM boundary only validates and marshals — it adds no rollback and no new semantics. See Performance — batch insertion for the full model.
Solve
pg.solve() // finalize the EDB and run the solver
pg.freeResult() // release the solve resultsolve() finalizes the current EDB session and evaluates the ruleset against
it. edbBegin() auto-frees any previous result, so calling freeResult() is
optional when an edbBegin() immediately follows.
Ground queries
pg.querySymbol('safe', 'alice') // → true or false
pg.querySymbol2('allow', 'alice', 'roadmap.pdf') // → true or falsequerySymbol / querySymbol2 answer a single ground yes/no question against
the solved result (a hard failure throws — see
Engine failures).
Enumerating derived IDB facts
After a successful solve(), enumeratePredicateFacts(predicate, arity)
returns every already-derived IDB fact of a QUERY-authorized predicate:
const facts = pg.enumeratePredicateFacts('allow', 2)
for (const [subject, resource] of facts) {
const subjectText =
subject.kind === 'symbol' ? pg.symbolText(subject.symbolId) : null
console.log(subjectText, resource)
}Each fact is an array of discriminated terms:
type MaelysDatalogTerm =
| { kind: 'symbol'; symbolId: number }
| { kind: 'int'; text: string; value: number | null }
| { kind: 'bool'; value: boolean }
| { kind: 'var'; variable: number }For integers, text is always the exact decimal representation. value is
the corresponding JavaScript number only when that conversion is exact;
otherwise it is null. Symbol terms deliberately retain their ruleset-local
handle. Resolve one explicitly with symbolText(symbolId), which returns the
symbol string or null for an invalid id. Enumeration never resolves symbols
automatically.
This API is intentionally narrower than a general relation dump:
- it includes derived IDB facts only — never EDB or
POLICY_FACT; - the predicate must be present, have the requested arity, and be authorized
as
QUERY; - it performs no pattern filtering and provides no pagination;
- result order is unspecified;
- a valid relation with no derived fact returns
[]; an invalid, forbidden, or non-QUERYpredicate throws an engine error.
The Python binding offers both resolved tuples and a raw form. WASM instead exposes one serializable term representation plus explicit symbol lookup.
Diagnostics
pg.diagMessage // string — last diagnostic message
pg.diagHint // string — corrective hint, empty string if none
pg.diagCode // number — last diagnostic code (maelys_datalog_diag_code_t)diagCode/diagMessage/diagHint mirror the fields of
maelys_datalog_diagnostic_t — see Errors — Diagnostic codes.
Note that diagCode is the diagnostic code (maelys_datalog_diag_code_t),
a different enum from the maelys_result_t return code carried in a thrown
Error — see Engine failures.
Every diagnostic call site in the lexer and parser populates both message
and hint; diagHint is never a separate authoring step, only an accessor.
Derived fact count
pg.solve()
pg.derivedFactCount() // number — total IDB facts derived by this solveThrows if called before a successful solve() in the current EDB session.
Not a session constant — read it again after each solve() to reflect the
current result.
Complete example
import MaelysDatalogDynamic from '/wasm/maelys_datalog_dynamic.js'
import { MaelysPlayground, PredKind } from '/wasm/maelys_playground.js'
const pg = await MaelysPlayground.create(
MaelysDatalogDynamic,
'/wasm/maelys_datalog_dynamic.wasm')
// 1. Define domain — once per instance
pg.domainBegin('document_access')
pg.domainAddPredicate('owns', 2, PredKind.EDB)
pg.domainAddPredicate('allow', 2, PredKind.IDB | PredKind.QUERY)
pg.domainCommit()
// 2. Load ruleset — once per instance
pg.loadRuleset('document_access', 'doc.main', `
allow(User, Document) :- owns(User, Document).
`)
// 3. Evaluate — as many times as needed
pg.edbBegin()
pg.addFact2('owns', 'alice', 'roadmap.pdf')
pg.solve()
console.log(pg.querySymbol2('allow', 'alice', 'roadmap.pdf')) // true
console.log(pg.querySymbol2('allow', 'bob', 'roadmap.pdf')) // false
pg.freeResult()
// 4. Next evaluation — same policy, different EDB
pg.edbBegin()
pg.addFact2('owns', 'bob', 'roadmap.pdf')
pg.solve()
console.log(pg.querySymbol2('allow', 'bob', 'roadmap.pdf')) // true
pg.freeResult()Inline path, static variant — production
Use the production target when the domain is fixed at compile time. The domain is registered in C code compiled into the WASM module. The policy source can be fetched from a server or embedded as a string.
This produces the smallest WASM binary — the dynamic builder is not included.
C side: domain and loader function
#include "include/maelys_datalog.h"
#include <emscripten.h>
static maelys_datalog_policy_set_t g_policy;
static maelys_datalog_diagnostic_t g_diag;
static const maelys_datalog_predicate_def_t preds[] = {
{ "owns", 2, MAELYS_DATALOG_PRED_KIND_EDB },
{ "allow", 2, MAELYS_DATALOG_PRED_KIND_IDB |
MAELYS_DATALOG_PRED_KIND_QUERY },
};
EMSCRIPTEN_KEEPALIVE
maelys_result_t load_document_access(const char *src, size_t src_len) {
return maelys_datalog_load_policy_inline_with_static_domain(
preds, 2,
"document_access", "doc.main",
src, src_len,
0, &g_policy, &g_diag);
}
EMSCRIPTEN_KEEPALIVE
const maelys_datalog_ruleset_t *get_ruleset(void) {
return &g_policy.policies[0];
}EMSCRIPTEN_KEEPALIVE ensures the linker does not eliminate the function.
JavaScript side
Wrap the exported loader once with cwrap, then call it through a small
loadPolicy helper rather than scattering raw ccall sites. cwrap returns a
reusable typed function, which reads more cleanly than repeating the
ccall(name, returns, argTypes, args) form at each call site.
const mod = await MaelysDatalog({ locateFile: f => '/wasm/' + f })
const loadDocumentAccess = mod.cwrap(
'load_document_access',
'number',
['string', 'number']
)
function loadPolicy(src) {
const rc = loadDocumentAccess(src, mod.lengthBytesUTF8(src))
if (rc !== 0) throw new Error('Policy load failed: ' + rc)
}
const src = await fetch('/policies/document_access.dl').then(r => r.text())
loadPolicy(src)Pass lengthBytesUTF8(src) (UTF-8 byte length), not src.length (UTF-16 code
units). load_document_access is the C export from the section above; this is
the only Emscripten-level call the application needs for the static-domain
strategy.
See Registries for the full domain registration API and both static-table and installer-callback approaches.
Manifest path, buffer variant — in WASM
Use maelys_datalog_manifest_load_from_text when you need full manifest
features in WASM: SHA-256 integrity verification, loading modes, policy
metadata, or a Public Query Whitelist that restricts which predicates are
publicly observable.
// Manifest JSON + policy bundle passed as C strings
const manifestJson = JSON.stringify({
policy_set_id: 'doc.v1',
policy_set_version: '1',
manifest_version: '1',
default_profile: 'MAELYS-DATALOG-TEXT-v1',
created_for: 'production',
strict_loading: true,
fail_closed: true,
capabilities: [],
policies: [{
policy_id: 'doc.main',
domain: 'document_access',
file: 'ignored.dl',
sha256: computedSha256, // hex SHA-256 of policy source bytes
mode: 'enforce',
enabled: true,
description: 'Document access',
queries: [{ name: 'allow', arity: 2 }]
}]
})
// Use ccall with manifest and policy bundle
// The domain must already be registered in CSee Manifest Loading for the full manifest schema and bundle format.
Web page integration
Files to serve
After building, copy the WASM artifacts to your web server:
# Dynamic target (playground)
cp build/wasm/maelys_datalog_dynamic.js public/wasm/
cp build/wasm/maelys_datalog_dynamic.wasm public/wasm/
cp js/maelys_playground.js public/wasm/
# Production target
cp build/wasm/maelys_datalog.js public/wasm/
cp build/wasm/maelys_datalog.wasm public/wasm/Both the .js and .wasm files must be served. The .wasm file must be
served with Content-Type: application/wasm for browser WASM compilation.
locateFile
Emscripten uses a locateFile hook to find the .wasm file at runtime.
Pass it when the .wasm is not in the same directory as the .js, or when
serving from a path prefix.
// locateFile resolves the .wasm path
const mod = await MaelysDatalogDynamic({
locateFile: (f) => f.endsWith('.wasm')
? '/wasm/maelys_datalog_dynamic.wasm'
: f
})MaelysPlayground.create(factory, wasmUrl) handles this automatically:
const pg = await MaelysPlayground.create(
MaelysDatalogDynamic,
'/wasm/maelys_datalog_dynamic.wasm')HTML script tag
<script src="/wasm/maelys_datalog_dynamic.js"></script>
<script type="module">
import { MaelysPlayground, PredKind }
from '/wasm/maelys_playground.js'
const pg = await MaelysPlayground.create(
MaelysDatalogDynamic, // global from the script tag
'/wasm/maelys_datalog_dynamic.wasm')
// ...
</script>ES module import (bundler / Next.js)
When the .js file is an ES module, import it directly. Pass wasmUrl so
Emscripten can locate the .wasm at the correct runtime path.
import MaelysDatalogDynamic from '/wasm/maelys_datalog_dynamic.js'
import { MaelysPlayground, PredKind } from '/wasm/maelys_playground.js'
export async function createPlayground() {
return MaelysPlayground.create(
MaelysDatalogDynamic,
'/wasm/maelys_datalog_dynamic.wasm')
}In Next.js, place the WASM files in public/wasm/ so they are served from
/wasm/. Use 'use client' when calling WASM from a React component.
Do not import the .wasm file directly with bundler syntax
(import wasmUrl from '...'). Emscripten manages .wasm loading internally
through the locateFile hook. Passing the URL string is sufficient.
CORS and Content-Type
If your .wasm is on a different origin, the server must set:
Content-Type: application/wasm
Access-Control-Allow-Origin: *
Most static file servers (nginx, Vercel, Netlify) serve .wasm with the
correct Content-Type automatically. For development servers, check that
.wasm is not served as application/octet-stream — some browsers refuse
to compile WASM from that type.
Node.js
The dynamic target is compatible with Node.js for server-side evaluation and testing:
import MaelysDatalogDynamic from './build/wasm/maelys_datalog_dynamic.js'
import { MaelysPlayground, PredKind } from './js/maelys_playground.js'
const pg = await MaelysPlayground.create(
MaelysDatalogDynamic,
'./build/wasm/maelys_datalog_dynamic.wasm')Paths are relative to the calling file. The import.meta.url form is useful
for computing absolute paths:
const wasmUrl = new URL(
'../../build/wasm/maelys_datalog_dynamic.wasm',
import.meta.url).href
const pg = await MaelysPlayground.create(MaelysDatalogDynamic, wasmUrl)Raw C exports (dynamic target)
For callers that work directly with ccall instead of MaelysPlayground.
Domain builder
| Exportmaelys_datalog_wasm_ prefix omitted | Signature | Description |
|---|---|---|
| domain_begin | (name: string) → number | Begin domain declaration. |
| domain_add_predicate | (name: string, arity: number, kindFlags: number) → number | Add predicate. |
| domain_commit | () → number | Commit domain. |
| domain_abort | () → number | Abort build. |
| load_ruleset | (domain: string, id: string, src: string, srcLen: number) → number | Load ruleset. Use lengthBytesUTF8(src) for srcLen. |
| ruleset_ptr | () → number | Pointer to loaded ruleset. |
| get_build_limits | (outPtr: number) → void | Write 10 packed uint32 build limits at outPtr. See Build limits above for field order. |
| last_diag_message | () → string | Last diagnostic message. |
| last_diag_hint | () → string | Last diagnostic hint. Empty string if none. |
| last_diag_code | () → number | Last diagnostic code. |
EDB, solve, and query
| Exportmaelys_datalog_wasm_ prefix omitted | Returns | Description |
|---|---|---|
| edb_begin | number | Init EDB from current policy. |
| edb_intern_runtime_symbol | number | Intern a symbol into the open EDB. Signature (text: string, outIdPtr: number) → number: writes a 1-based int32 id at outIdPtr, returns maelys_result_t. Read the id with getValue(outIdPtr, 'i32'). |
| edb_add_symbol_id_fact | number | Add 1-arity fact by id. Signature (pred: string, id: number) → number. |
| edb_add_symbol_ids_fact | number | Add 2-arity fact by two ids. Signature (pred: string, left: number, right: number) → number. |
| edb_add_symbol_id_facts | number | Batch add unary facts by id. Signature (pred: string, idsPtr: number, count: number) → number. Validated and inserted atomically. |
| edb_add_symbol_ids_facts | number | Batch add binary facts by flat id pairs. Signature (pred: string, pairsPtr: number, pairCount: number) → number. |
| edb_add_runtime_symbol_facts | number | Batch add unary facts from a packed string buffer. Signature (pred: string, packed: number, byteLen: number, valueCount: number) → number. |
| edb_add_runtime_symbol_pair_facts | number | Batch add binary facts from a packed string buffer (flat pairs). Signature (pred: string, packed: number, byteLen: number, pairCount: number) → number. |
| edb_add_symbol | number | Add 1-arity symbol fact (string sugar; interns then delegates to the id path). |
| edb_add_symbol2 | number | Add 2-arity symbol fact (string sugar over the id path). |
| solve | number | Finalize EDB and solve. |
| derived_fact_count | number | Total IDB facts derived by the last solve. -1 if no solve result is available. |
| query_symbol | number | Query 1-arity: 1 found, 0 absent, -1 error. |
| query_symbol2 | number | Query 2-arity. |
| solve_result_free | void | Free solve result. |
All maelys_result_t values equal 0 (MAELYS_OK) on success.
The symbol-id parameters are int32, not unsigned. The boundary rejects any
id <= 0 (sentinel 0 and negatives, including a JavaScript -1 that would
otherwise wrap to a large unsigned value) before range-checking it against the
EDB's symbol table. Pass only handles returned by
_maelys_datalog_wasm_edb_intern_runtime_symbol for the current session.
Use lengthBytesUTF8(src) not src.length when passing ruleset source
strings to _maelys_datalog_wasm_load_ruleset. JavaScript counts UTF-16
code units; C expects UTF-8 byte length.
Available runtime methods
ccall, cwrap, UTF8ToString, stringToUTF8, lengthBytesUTF8, getValue
getValue(ptr, 'i32') reads the int32 id written by
_maelys_datalog_wasm_edb_intern_runtime_symbol into a pointer obtained from
_malloc(4) (free it with _free). MaelysPlayground.internRuntimeSymbol
wraps this allocate-call-read-free sequence for you.
Build targets
# Dynamic target (domain builder + EDB/solve/query helpers)
make -f Makefile.wasm maelys_datalog_dynamic.js
# → build/wasm/maelys_datalog_dynamic.js
# → build/wasm/maelys_datalog_dynamic.wasm
# Production target (smallest, no domain builder)
make -f Makefile.wasm maelys_datalog.js
# → build/wasm/maelys_datalog.js
# → build/wasm/maelys_datalog.wasm
# Examples target (built-in graph + decision domains)
make -f Makefile.wasm maelys_datalog_examples.js
# → build/wasm/maelys_datalog_examples.js
# → build/wasm/maelys_datalog_examples.wasm