Holarch Language Engine
Language Engine API reference
The public interface of the Holarch Language Engine: one JSON request in, one JSON answer out. This page lists every operation, its arguments and its answer, with one small example. It describes engine version 1.0.
Jump to: Example · Entry points · Requests and answers · Diagnostics · Errors · Element records · Operations · Change sets · Limits
The evaluation package holds the engine, the standard library, the full developer guide with a real request and answer for every operation, the list of diagnostic codes, and runnable examples. The engine itself is not on this page.
Example: check a model as you type
The smallest complete integration: a page with a text box and a worker that hosts the engine. It runs as it is once the evaluation package is unpacked: put both files in examples/00-quick-start/, serve the package folder on the loopback address, and open the page.
cd holarch-language-engine-1.0.0
python3 -m http.server 8000 --bind 127.0.0.1
# open http://127.0.0.1:8000/examples/00-quick-start/
The page: index.html
It sends one analyze request per change. Each request carries a revision number rev; the engine copies it into the answer, so the page drops answers to requests it has already replaced.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Quick start</title>
</head>
<body>
<textarea id="src" rows="8" cols="60">package Demo {
part def Pump;
part p : Pmp;
}</textarea>
<pre id="out">Starting…</pre>
<script>
const worker = new Worker('worker.js');
const out = document.getElementById('out');
let rev = 0;
worker.onmessage = (ev) => {
const a = ev.data;
if (a.error) { out.textContent = a.message || a.error; return; }
if (a.rev !== rev) return; // an older answer: a newer request is on its way
out.textContent = a.diagnostics.length
? a.diagnostics.map((d) => `${d.range.from}-${d.range.to} ${d.severity} ${d.code} ${JSON.stringify(d.args)}`).join('\n')
: 'No problems.';
};
const check = () => worker.postMessage({ op: 'analyze', doc: 'demo', rev: ++rev, text: document.getElementById('src').value });
document.getElementById('src').addEventListener('input', check);
check();
</script>
</body>
</html>
The worker: worker.js
It loads the engine and answers each message with one call. Loopback addresses need no license token.
// Loads the core and answers one JSON request per message. Requests that arrive before the core is ready wait.
importScripts('../../core/holarch_sysml.js'); // defines the global wasm_bindgen
const ready = (async () => {
const bytes = await (await fetch('../../core/holarch_sysml_bg.wasm')).arrayBuffer();
await wasm_bindgen({ module_or_path: bytes });
// Pass your license token here; loopback (127.0.0.1, localhost) needs none.
if (wasm_bindgen.unlock) wasm_bindgen.unlock('');
})();
onmessage = async (ev) => {
await ready;
try {
postMessage(JSON.parse(wasm_bindgen.call(JSON.stringify(ev.data))));
} catch (e) {
postMessage({ error: 'core-fault' }); // a trap: start a new worker to continue
}
};
The request and the answer
For the sample text, the page sends:
{ "op": "analyze", "doc": "demo", "rev": 1,
"text": "package Demo {\n part def Pump;\n part p : Pmp;\n}" }
The engine answers (tokens and reference updates shortened):
{
"rev": 1,
"diagnostics": [
{
"code": "name.unresolved",
"severity": "error",
"range": { "from": 43, "to": 46 },
"args": { "name": "Pmp", "segment": "Pmp" },
"elementId": "…",
"field": "typedBy",
"doc": "demo"
}
],
"tokens": [["keyword", 0, 7], ["ws", 7, 8], ["def", 8, 12], "…"],
"refs": ["…"]
}
rangeis in UTF-16 code units, the same as JavaScript string indices:text.slice(43, 46)is"Pmp".codeis stable across releases; the evaluation package has English messages for every code (Pmp is not defined here. Check the spelling, or import its package.).tokensare[kind, from, to]triples for highlighting.
Entry points
In the browser and in Node.js, the engine is a WebAssembly module with a small glue script. The glue defines one global, wasm_bindgen: an async init function with these exports.
| Export | Signature | Notes |
|---|---|---|
wasm_bindgen(init) | ({module_or_path}) → Promise | Instantiates the module from the .wasm bytes, a WebAssembly.Module, a URL or a Response. Call once per worker. |
call(request) | string → string | Runs one JSON request and returns one JSON answer. Never throws for any input string. An internal fault throws a RuntimeError; start a new worker after it. |
version() | () → string | {"core": "1.0.0", "abi": 1}. Works before unlock. |
notice() | () → string | The copyright notice. Works before unlock. |
unlock(token) | string → string | Applies a license token for the worker’s origin and answers {ok, …}, or {ok: false, reason}. Loopback origins need no token. |
The server build for Python, available with an Enterprise license, exposes call(request: str) -> str and version() -> str with the same JSON.
Requests and answers
request: {"op": "<name>", "rev"?: number, …arguments}
answer: {"rev"?: number, …result}
or {"rev"?: number, "error": "<code>", "message"?: string}
opselects the operation. An unknown op answers{"error": "unknown-op"}.revis optional: any number you send comes back unchanged, also with an error. Use it to match answers to the state they were computed from.- Answers are JSON objects. Key order carries no meaning; ignore keys you do not know.
- Malformed JSON, a request that is not an object, or a missing required argument answers
{"error": "bad-request"}. - The engine never reads files or the network. Everything it needs is in the request, in the library loaded with
library.load, or in a document session.
Offsets
Every position (offset, range.from, range.to, and from/to of edits and source maps) counts in the caller’s units: UTF-16 code units in the WebAssembly build, matching JavaScript strings and editor offsets, and Unicode code points in the Python build. Ranges are half-open; a range with from equal to to marks a position, such as where a ; is missing.
Diagnostics
{
"code": "name.unresolved",
"severity": "error",
"range": { "from": 21, "to": 22 },
"args": { "name": "A", "segment": "A" },
"elementId": "8ff5ac4e-a81a-5828-b557-a0b392e84714",
"field": "typedBy",
"doc": "d1"
}
| Field | Present | Meaning |
|---|---|---|
code | always | Stable code, family.rule. The evaluation package lists every code with its meaning and fix. |
severity | always, except in json_import | "error" or "warning". |
range | text operations | Where in the text. Model operations such as check have no text and no range. |
args | always | String values for the message (name, expected, found, …). May be empty. |
elementId | semantic diagnostics | The element whose check raised it. |
field | often | The record field concerned (typedBy, value, body.source, …). |
doc | analyze semantic diagnostics | The request’s doc value, or null. |
Lists are sorted by position. After a syntax error, further syntax diagnostics wait until the parser reads a good token again, so one mistake does not produce a cascade.
Error codes
| Error | Operations | Meaning |
|---|---|---|
bad-request | all | Malformed JSON, not an object, or a required argument missing or of the wrong type. |
unknown-op | all | No such operation in this version. |
limit.input-size | text and record operations | A text over 8 MiB (UTF-8), or library files over 64 MiB in total. |
limit.nesting | record operations, json_import | A record owner chain deeper than 512. |
mirror-missing | analyze | mirrorKey names records this worker does not hold; send records again. |
not-found | shortest_name | target is not in the records or the library. |
apply.<reason> | apply | A change does not fit the records, such as apply.id-used or apply.create. |
rename.not-a-name, rename.library, rename.invalid-name | rename | The offset is not on a name, the name belongs to the library, or the new name is not valid. |
not-licensed | all | No accepted license for this origin. Comes with message. |
limit.time, busy | Python build | The call’s time budget ran out, or too many calls run at once. |
Element records
Text becomes element records: a JSON object that maps each element id to a flat record. The model operations take and return this format.
{
"type": "PartUsage",
"owner": "<id of the owning element, or null>",
"order": "a0",
"name": "wheels",
"typedBy": [{ "ref": "<target id or null>", "text": "Wheel" }],
"multiplicity": { "text": "4" },
"fmt": { "…": "printer hints" }
}
| Field | Shape | Meaning |
|---|---|---|
type | string | Metaclass, such as Package, PartDefinition, PartUsage, ConnectionUsage or TransitionUsage (the SysML v2 metaclass names). Required. |
owner | id or null | Owning element; null is top level. Required. |
order | string | Position among the owner’s members. Compare as plain strings, then by id. Required. |
role | string | Membership kind when not an ordinary member: subject, actor, require, verify, return, entry, do, exit, then, and others. |
name, shortName | string | As declared, without quotes. |
visibility | string | public, private or protected when written. |
flags | object | abstract, variation, ref, readonly, derived, end, ordered, nonunique, parallel and others (only true is stored); direction: in, out or inout. |
typedBy, specializes, subsets, redefines, references, conjugates, disjoins | array of references | Relationships. |
multiplicity | {text} | Bounds as written ("0..*"). |
value | {text, kind, default?} | kind is "=" or ":="; text is the expression as written. |
body | object | Per metaclass: connector ends, transition triggers and effects, import targets and filters, comment text, constraint expressions, and so on. |
doc | string | The text of the first doc comment. |
xid, mid, docId | string | Ids kept from imported JSON. |
fmt | object | Printer hints. Keep it unchanged, or delete it when your change makes the stored spelling wrong. |
References are {ref, text, chain?, conj?}: ref is the target id (null while unresolved), text the name as written (a qualified name A::B or a feature chain a.b.c), chain the resolved id of each chain segment, and conj: true marks a conjugated type (~P).
Ids are lower-case UUIDs. lower derives them from the doc argument and each element’s path, so the same text and doc give the same ids. sync and diff keep existing ids and give new elements ids from your seed. Library elements have the same ids in every version (lib_id).
Operations
Arguments in brackets are optional. Every request may carry rev.
| Operation | Arguments | Answer |
|---|---|---|
version | — | {core, abi}. |
library.load | files: [{path, text}], the .sysml and .kerml library files in any order (at most 64 MiB) | {files, elements, skipped, packages}. Replaces the library and clears every document session. |
library.status | — | {files, elements} of the loaded library. |
lib_id | qname, such as ISQ::MassValue | {id}, the library element id for that name. |
lex | text | {tokens, diagnostics}: [kind, from, to] tokens and lexical diagnostics. Fast; for a first paint. |
roundtrip | text | {text, same, diagnostics}: the text rebuilt from its tokens; a lexer self-test. |
parse | text | {errors, diagnostics}: syntax only, no name resolution. |
analyze | text; [doc], [session], [root], [records], [mirrorKey], [tokensBase], [refs] | {tokens or tokenDelta, diagnostics, refs}. The editor’s main operation: tokens with semantic kinds (def, ref, unresolved), syntax and semantic diagnostics, and reference updates. With a doc key the engine keeps a session and re-checks only what an edit can reach; records resolve names against the rest of the model. |
lower | text; [doc] | {records, roots, skipped, diagnostics}: element records with unresolved references, the top-level ids in text order, and members skipped because of syntax errors. |
check | records | {refs, diagnostics}: the resolved target of each reference whose stored ref differs, and diagnostics with elementId and field. |
evaluate | records; [ids] | {values}: per element id a value {kind, text, …}, where kind is number, quantity (with unit and dimension), boolean, string, unit, sequence, element or unknown. Show text. |
analyze_case | records; [ids] | {cases, requirements}: each satisfy with its status (satisfied, violated, not-applicable, unknown) and constraint values, and each analysis and verification case with its result. |
hover | text, offset; [doc], [root], [records] | {elementId, type, name, qualifiedName, range, doc?, value?, …}, or {elementId: null} off a name. |
definition | as hover | {elementId, qualifiedName, range?, doc?, library?}. |
complete | as hover | {from, to, items}: replace from..to with an item’s insert or label. |
rename | as hover, plus name | {elementId, changes}: [{from, to, insert}] for every occurrence in the text. |
shortest_name | records, target; [context] | {text}: the shortest name that resolves to target from context. |
print_records | records; [roots], [dialect] | {text}. Unchanged elements print byte for byte. |
print | records; [root], [text] | {text, map, edits?}: the text, a source map [{id, from, to}], and the edits that turn the given text into the new one. |
sync | text, records; [root], [seed] | {changes, notApplied, diagnostics}: the change set that makes the records match the text, keeping existing ids. |
rebase | base, text, records; [root] | {text, clean, conflicts, theirs}: a text draft merged with model changes made meanwhile. |
diff | old, new; [base], [seed] | {changes, ids}: the change set from old to new, and the id each element of new has in the result. |
apply | records, changes | {records} with the changes applied, or {error: "apply.<reason>"}. |
edits | old, new (strings) | {edits}: minimal line-aware replacements. |
json_export | records; [ext] | {elements}: SysML v2 JSON elements (@id, @type), including relationship elements. With ext (the default) each element carries what json_import needs to restore the records exactly. |
json_import | elements: an array, {elements: […]}, or API data versions | {records, diagnostics}. Ids that are not UUIDs get UUIDs, with the original kept in xid. Element kinds the engine does not support are reported with json.unsupported-type. |
Change sets
sync and diff answer, and apply takes, an ordered array of changes. Creates and moves come in document order, owners before members; removes come last.
| Change | Meaning |
|---|---|
{op: "create", id, fields, after?, before?} | A new element, placed after or before a sibling, or last. |
{op: "update", id, patch} | Field changes; null deletes a field. Never changes owner or order. |
{op: "move", id, owner, after?, before?} | A new owner or position. |
{op: "remove", ids} | Removes the elements and everything they own. |
Limits
Over a limit the engine answers with a diagnostic or an error code, never a hang or a crash. Every operation takes time in proportion to its input.
| Limit | Value | Over the limit |
|---|---|---|
| Text size | 8 MiB (UTF-8) | limit.input-size |
| Line length | 64 KiB (UTF-8) | limit.line-length for each long line |
| Body nesting | 256 levels | limit.nesting; the deeper part is skipped |
| Expression nesting | 512 levels | limit.expression-depth |
| Record owner chain | 512 levels | {error: "limit.nesting"} |
| Library files, total | 64 MiB | {error: "limit.input-size"} |
| JSON import | 500,000 elements | json.too-many-elements; the rest is not read |
New name in rename | 1 to 1,000 characters, no control characters | {error: "rename.invalid-name"} |
The browser build has no time limit of its own: run it in a worker and end the worker when it does not answer in time. Answers contain text from the input, such as names and documentation; escape it like any user input before you put it into a page.
Versions and stability
version() reports the engine version and the interface version (abi). Within a major version, operations, fields and diagnostic codes are not removed or changed in meaning; new operations, fields and codes can be added, so ignore what you do not know. A new major version is announced six months ahead.
Try the engine on your own models.
The evaluation is free for 60 days and includes the full developer guide and examples.