OpenSysML is Go, and Go compiles it for two WebAssembly targets. This page says what each is for, how to build and run them, what works in them, and what a WebAssembly host cannot do — with the message each limitation answers with, so a refusal is never mistaken for a defect.
Stable releases ship sysml-wasm.wasm with its matching wasm_exec.js, list
both in the signed SHA256SUMS.txt, and cover them with SLSA provenance.
Nightly snapshots list the same assets in their cosign-signed checksum
manifest; like every nightly asset, they have no SLSA provenance. The npm
package @openmbee/opensysml-wasm carries both assets. Other WebAssembly builds
remain available from source for hosts that run modules rather than executables.
make build-wasm # both targets
make build-wasm-wasip1 # or one
make build-wasm-jsThe output is one directory per target, seven commands each, stamped with the same version information a native build carries:
bin/wasm/wasip1/{sysml,sysml-lsp,sysml-grpc,sysml-engine,sysml-syntax,sysml-core,sysml-wasm}.wasm
bin/wasm/js/{sysml,sysml-lsp,sysml-grpc,sysml-engine,sysml-syntax,sysml-core,sysml-wasm}.wasm
bin/wasm/js/wasm_exec.js # the runtime a browser page includes
wasm_exec.js is the library, not a runner: a page loads it with a <script> tag, and Node
runs these modules with $(go env GOROOT)/lib/wasm/wasm_exec_node.js, which loads its own copy
beside it.
make build is unchanged and stays native; a WebAssembly build is always asked for.
make build-engine, make build-core, make build-syntax and make build-sysml-wasm
build the JSON commands natively into bin/, where each serves its JSON-RPC over standard
input and output as its WASI build does. They are opt-in as well: make build and
make install leave them out of the native executables; the combined JavaScript
WebAssembly module is published separately.
make build-wasm-prod builds a smaller sysml-prod.wasm for each target with -tags sysml_prod
(make build-prod is the native counterpart). It leaves out SysML v1 migration, repository sync,
-compile, the HTML and PDF document forms, FMU import, profiling and the REPL's %features … json. A flag of a left-out group is hidden from -help and refused with status 2. Each group
also has a tag of its own (sysml_nov1, sysml_nosync, sysml_nocodegen, sysml_nodocpdf,
sysml_nofmi, sysml_noprofile, sysml_noreplext).
wasip1 binaries run under any WASI preview 1 runtime. Wasmtime is
the one Go's own runner uses:
wasmtime run --dir=/ --env PWD="$PWD" bin/wasm/wasip1/sysml.wasm model.sysml -e 'total'--dir=/ preopens the filesystem root, which is how the working directory becomes reachable: a
runtime that preopens nothing can run -version, but can open no model. --env PWD="$PWD" is
what makes model.sysml resolve where you are — a wasip1 program's os.Getwd is the PWD it
was given and nothing else, so without it the path resolves against /.
The Go toolchain ships the same runner as a script,
$(go env GOROOT)/lib/wasm/go_wasip1_wasm_exec: it opens /, supplies the working
directory and environment each runtime needs, and uses wasmtime unless GOWASIRUNTIME names
wasmer, wazero or wasmedge.
A host without one of those can use Node's WASI implementation directly:
// wasi.mjs — node wasi.mjs <binary.wasm> [args...]
import { WASI } from 'node:wasi';
import fs from 'node:fs';
const [binary, ...args] = process.argv.slice(2);
const wasi = new WASI({
version: 'preview1',
args: [binary, ...args],
// PWD is what a wasip1 program resolves relative paths against; process.cwd()
// keeps it right where the shell does not export it.
env: { ...process.env, PWD: process.cwd() },
preopens: { '/': '/' }, // a deployment opens only the directories it needs
returnOnExit: true,
});
const module = await WebAssembly.compile(fs.readFileSync(binary));
const instance = await WebAssembly.instantiate(module, wasi.getImportObject());
process.exit(wasi.start(instance));Node's WASI host cannot block waiting for a pipe: a read from one with nothing ready comes back
EAGAIN rather than waiting. Drive its processes from a file, or from arguments — -version,
-validate, -eval, -engines and the prompt itself all work that way — and use a runtime that
reads pipes properly for anything that speaks a protocol over its standard input.
js binaries run through the toolchain's wasm_exec, under Node or in a browser:
node --stack-size=8192 --no-concurrent-sparkplug "$(go env GOROOT)/lib/wasm/wasm_exec_node.js" bin/wasm/js/sysml.wasm -version
# the same, with the stack size the runner itself sets:
"$(go env GOROOT)/lib/wasm/go_js_wasm_exec" bin/wasm/js/sysml.wasm -versionThree constraints apply to js builds:
- argv and the environment share about 8 KiB. It writes both into linear memory at a fixed
offset and refuses past
wasmMinDataAddr; a large environment fails beforemainruns withtotal length of command line and environment variables exceeds limit. Run it with a small environment —PATH,HOME,TMPDIRare enough for these commands. - The stack needs raising. Node's default JavaScript stack is too small for deep model
traversal;
--stack-size=8192is what Go's own runner passes. - Node can hang at exit. A command that exits right after starting (
-version, say) occasionally prints its output and then never exits: Node deadlocks while shutting down if its background compiler is still running.--no-concurrent-sparkplugavoids it.
In a browser, nothing wires a page's input to the module's standard input: an embedder provides that itself. The commands take their input from arguments and files, so the parts that need no interactive stream work as they do under Node.
sysml-engine is the in-process half of the service: the execution
RPCs — ParseSources, Evaluate, Instantiate, ExecuteAction, ExecuteState — answered
with the same JSON sysml-grpc emits, but without the protobuf machinery, the analysis
framework or a held-object store, which is what keeps a WebAssembly build small enough to
embed in a page. A model parsed through it hashes to the same modelHash the service
returns, and a request a service client encodes decodes identically here.
It also serves RenderView, an engine-only call that returns the drawing data for a
declared view or a targeted pseudo-view. It is not an RPC of sysml-grpc. A request names
the cached model and a declared view's qualified name or the target of a pseudo-view:
{
"modelHash": "…",
"view": "#interconnection:OpenSysMLStack::stack",
"ports": "minimal"
}ports is optional: empty or minimal includes only ports an interconnection edge ends at,
and full includes every port. Each node's optional ports array has id, name, optional
type and optional direction; undirected ports omit the direction. An edge's optional
fromPort and toPort identify the endpoint ports by those IDs. The response also carries
the view kind, stated rendering, notices, canvas, nodes, edges, and, for tables, columns and
rows; its node and edge fields use the same JSON names as opensysml/render.
The engine has no current-document context, so a pseudo-view must name an element, for
example #tree:OpenSysMLStack::stack. An untargeted #tree or #interconnection is refused
with InvalidArgument and the supported pseudo-view spellings.
The js build installs a host surface instead of reading a pipe: load it through
wasm_exec.js with no arguments and globalThis.sysmlEngine appears with
const answer = JSON.parse(sysmlEngine.call(method, paramsJSON));
sysmlEngine.version;where call is synchronous — it runs the method on the JS thread and returns the JSON-RPC
response envelope ({"jsonrpc":"2.0","id":null,"result":…} or …"error":{"code":…,"message":…}})
as a string. Pass -stdio and the js build serves the pipe instead, as the native and
wasip1 builds always do: the same Content-Length frames and JSON-RPC 2.0 bodies
sysml-grpc -transport stdio speaks, answered sequentially.
Measured on a go1.25 js/wasm build of this tree: about 27.7 MB of module,
6.9 MB gzipped, 4.9 MB under Brotli.
Requests decode the lowerCamelCase field names protojson and protobuf-es emit; the proto
snake_case spellings protojson also accepts are not read. What it does not serve is refused
rather than dropped: an exploring schedule is answered
Unimplemented with exploration is not served by sysml-engine: exploring schedules are served by sysml-grpc, and every other method name — verification, document queries, tools — answers
<Method> is not served by sysml-engine. Within the served methods: diagnostics are the
parser's syntax diagnostics only, since the engine does not run the analysis tier; a
ParseSources response carries no roots; and a frame whose Content-Type is a protobuf
body is answered only application/json bodies are served. Tool-backed engines are the
service's job too — the engine builds a runtime context directly, so behavior an external
engine would compute stays on sysml-grpc.
sysml-syntax is the purely syntactic half: it answers Parse, Format and Tokens on
content a request carries, with no name resolution and no standard library, which is what keeps
it even smaller than the engine. Like the engine, the js build installs a host surface —
globalThis.sysmlSyntax with the same synchronous call(method, paramsJSON) — and the same
-stdio flag swaps it for the Content-Length-framed JSON-RPC 2.0 pipe the native and
wasip1 builds always serve.
Parsetakes{content, language}and answers{"diagnostics":[…]}: the parser's syntax errors, then its warnings, each as the Diagnostic message aParseFileanswer carries; a clean document answers{}.Formattakes{content, language, tolerateSyntaxErrors}and answers{content, diagnostics, error}as asysml→sysmlConvertdoes: the re-indented source, or — when the input has syntax errors and the request did not tolerate them —errorwith the converter's message anddiagnosticslisting them. It refuses invalid input unlesstolerateSyntaxErrorsis set.Tokenstakes{content, language}and answers{"legend":{…},"data":[…]}: the full LSP semantic-tokens legend and the relative-encoded token data for the document's lexical classes only — keywords, comments, strings and numbers. With no symbol table it emits no declaration/reference classes and no modifiers.
language is "sysml" or "kerml" (empty means SysML), inline content is named <content>,
and every other method answers <Method> is not served by sysml-syntax. Being syntactic only,
it reports no semantic diagnostics.
Measured on a go1.25 js/wasm build of this tree: about 5.0 MB of module,
1.32 MB gzipped, 0.97 MB under Brotli.
sysml-core serves the model-validation surface of sysml-grpc without protobuf, Connect,
or the execution runtime. It embeds the standard library, parses one or more SysML or KerML
documents together, runs the validation passes on models that parse cleanly, and reports
diagnostics and shared symbol facts.
ParseSourcesparses a set of inline or file-backed documents as one model and returns its hash, one root per document, and diagnostics.ParseFileparses a single inline document or file and returns its hash, root and diagnostics.GetDiagnosticsreturns the parser and validation diagnostics for a cached model.GetSymbolreturns the symbol's type, multiplicity, specialization and attribute facts.
The js build installs globalThis.sysmlCore with version and synchronous
call(method, paramsJSON), which returns the JSON-RPC envelope string. Passing -stdio
selects the same sequential, Content-Length-framed JSON-RPC pipe used by the native and
wasip1 builds. Requests and responses use the lowerCamelCase protojson field names.
The core does not execute models or serve conversion, query, document, verification or tool
methods. Those execution methods belong to sysml-engine; every other unsupported method
belongs to sysml-grpc. Such calls answer Unimplemented with that routing guidance rather
than being silently ignored. File-backed requests still require the host to make the requested
paths readable; the standard library itself is embedded.
Measured on a go1.25 js/wasm build: 20,601,256 raw bytes, 5,444,030 bytes with gzip
-9, and 3,843,951 bytes with Brotli.
The Node and browser client adapter is documented in
WebAssembly, without a service.
Stable and nightly releases include the module and matching runtime; the npm
package is @openmbee/opensysml-wasm.
sysml-wasm combines the parsing, validation and execution methods of sysml-core and
sysml-engine in one WebAssembly module. It serves ParseSources, ParseFile,
GetDiagnostics, GetSymbol, Evaluate, Instantiate, ExecuteAction, ExecuteState
and GetServerInfo. Parsing and symbol facts route through the core; evaluation and
execution route through the engine. ParseSources and ParseFile check that both frontends
produce the same model hash and return the core response, including roots and diagnostics.
It does not dispatch the engine-only RenderView call.
A hash from either parse method is shared by every method that takes a modelHash, including
GetDiagnostics, GetSymbol, Evaluate, Instantiate, ExecuteAction and ExecuteState.
The module retains the 16 most recently used models; an evicted hash is evicted for every method.
GetServerInfo returns the build version and these capabilities, in order:
type_facts, enum_values, evaluate_subject, symbol_attributes, unset_value,
feature_values, inline_language, strict_conformance, parse_sources, complex_values,
structured_values, measurement_refs, function_values, set_values, tensor_values,
infinity_value, diagnostic_codes, schedule, final_time, metaobject_values,
undetermined_value, performer, big_int_values.
The js build installs one synchronous host surface, globalThis.sysmlWasm, with
version and call(method, paramsJSON). The call returns a JSON-RPC envelope string.
Passing -stdio selects the sequential, Content-Length-framed JSON-RPC pipe instead;
native and wasip1 builds always use that pipe. Every other method answers Unimplemented
with <Method> is not served by sysml-wasm: it is served by sysml-grpc.
Measured on a go1.25.11 js/wasm build with -s -w -trimpath: 31,526,528 raw bytes,
7,790,873 bytes gzipped with gzip -9, and 5,479,244 bytes with Brotli -q 11.
Everything that is the language implementation rather than the host around it:
-validate,-e/-eval,-query,-convert,-renderand every document form but PDF,-engines, and the rest of the CLI;- the prompt, reading a line at a time from whatever the host wires up;
sysml-lsp -stdio, the whole language server protocol over standard input and output;sysml-grpc -transport stdio, the service over the same framing, with its JSON-RPC bodies.
Each of these is refused with the reason, not left to fail on the platform's own words.
| Not available | What it answers |
|---|---|
External processes — SMT solvers, external engines, PDF rendering, -compile |
a WebAssembly build cannot start external processes, named for what could not run: an SMT solver cannot run: … in sysml -engines, weasyprint cannot run: … from -doc-form pdf, codegen: cc cannot run: … from -compile, codegen: go cannot run: … from -compile -target go. Go's WebAssembly targets start no process at all, so installing the tool cannot help and the message says so instead of advising it |
sysml-grpc -transport grpc and -transport connect |
… binds an address, and a WebAssembly build's network reaches only the process it runs in …; use -transport stdio. Go's net on these targets reaches only the same process, so a listener would report an address no client outside could dial and wait on it forever |
| HTTP clients, such as the client that reads and pushes a Flexo branch | A transport error from the request. Outbound requests have no socket to make |
Prompt history, tab completion, Ctrl-C, terminal width |
Nothing is faked: lines are read without editing, renderings are written unbounded, and an interrupt is the host's to deliver |
"Is it a terminal?" — for at a terminal status and for - on standard input |
Never a terminal. Neither WASI preview 1 nor a browser offers the query, so a - reads the lines it is sent and ends at end of input, exactly as a redirected pipe does natively |
The prompt is the one behavior that differs by degree: a native build writes it only where stdin is a terminal, and a WebAssembly build always writes it, because a host that cannot say whether its input is a terminal still needs one to be usable. A script that does not want it can drop it.
make wasm-checkcompiles and vets the whole tree for both targets, links each command, and runs them under Node
— the WASI host above for wasip1, wasm_exec for js. Node 24 or later is required: Node 22's
WASI host crashes on the wasip1 modules. Without Node, the run half skips with the reason, and
OPENSYSML_REQUIRE_WASM=1, which both CI systems set, turns that skip into a failure so a green
run cannot be a skipped one.
The wasip1 cases are driven with files and end each server at end of input, for the reason
under Running a WASI build; the js cases hold a pipe open across a
request and its answer, and run a full service session and a full language server
initialize/shutdown. Both halves say so where they differ.