API Reference

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 fileModule nameDomain sourceLoading path used
maelys_datalog.jsMaelysDatalogCompiled into the module in C. Smallest binary.Inline, static
maelys_datalog_examples.jsMaelysDatalogExamplesBuilt-in example domains (graph, decision). For demos.Inline, static
maelys_datalog_dynamic.jsMaelysDatalogDynamicDefined 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):

PathVariantC API
Manifestfilemaelys_datalog_manifest_load_ex — not used in WASM, no filesystem in the browser.
Manifestbuffermaelys_datalog_manifest_load_from_text → supported, see Manifest path, buffer variant below.
Inlinestaticmaelys_datalog_load_policy_inline_with_static_domain → supported, see Inline path, static variant below.
Inlinedynamicmaelys_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 source

Combine 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()
MethodDescription
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 pg1

Two 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 fact

EDB 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()
MethodDescription
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'])
MethodDescription
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 result

solve() 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 false

querySymbol / 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-QUERY predicate 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 solve

Throws 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 C

See 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 omittedSignatureDescription
domain_begin(name: string) → numberBegin domain declaration.
domain_add_predicate(name: string, arity: number, kindFlags: number) → numberAdd predicate.
domain_commit() → numberCommit domain.
domain_abort() → numberAbort build.
load_ruleset(domain: string, id: string, src: string, srcLen: number) → numberLoad ruleset. Use lengthBytesUTF8(src) for srcLen.
ruleset_ptr() → numberPointer to loaded ruleset.
get_build_limits(outPtr: number) → voidWrite 10 packed uint32 build limits at outPtr. See Build limits above for field order.
last_diag_message() → stringLast diagnostic message.
last_diag_hint() → stringLast diagnostic hint. Empty string if none.
last_diag_code() → numberLast diagnostic code.

EDB, solve, and query

Exportmaelys_datalog_wasm_ prefix omittedReturnsDescription
edb_beginnumberInit EDB from current policy.
edb_intern_runtime_symbolnumberIntern 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_factnumberAdd 1-arity fact by id. Signature (pred: string, id: number) → number.
edb_add_symbol_ids_factnumberAdd 2-arity fact by two ids. Signature (pred: string, left: number, right: number) → number.
edb_add_symbol_id_factsnumberBatch add unary facts by id. Signature (pred: string, idsPtr: number, count: number) → number. Validated and inserted atomically.
edb_add_symbol_ids_factsnumberBatch add binary facts by flat id pairs. Signature (pred: string, pairsPtr: number, pairCount: number) → number.
edb_add_runtime_symbol_factsnumberBatch add unary facts from a packed string buffer. Signature (pred: string, packed: number, byteLen: number, valueCount: number) → number.
edb_add_runtime_symbol_pair_factsnumberBatch add binary facts from a packed string buffer (flat pairs). Signature (pred: string, packed: number, byteLen: number, pairCount: number) → number.
edb_add_symbolnumberAdd 1-arity symbol fact (string sugar; interns then delegates to the id path).
edb_add_symbol2numberAdd 2-arity symbol fact (string sugar over the id path).
solvenumberFinalize EDB and solve.
derived_fact_countnumberTotal IDB facts derived by the last solve. -1 if no solve result is available.
query_symbolnumberQuery 1-arity: 1 found, 0 absent, -1 error.
query_symbol2numberQuery 2-arity.
solve_result_freevoidFree 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