Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build build-sysml build-prod build-wasm-prod build-lsp build-grpc build-engine build-core build-syntax build-wasm build-wasm-wasip1 build-wasm-js wasm-check static-check windows-versioninfo-check man man-check install-tree pgo-profile conformance conformance-pkg conformance-rust conformance-julia conformance-matlab test test-shard coverage lint clean install help fuml-expected python-test python-coverage scripts-coverage node-coverage python-install proto proto-buf python-proto proto-ts proto-rust proto-lint proto-breaking vscode-grammar vscode-build vscode-package docs docs-install docs-serve docs-engine-assets docs-counts docs-check changelog-check changelog-render self-model
.PHONY: all build build-sysml build-prod build-wasm-prod build-lsp build-grpc build-engine build-core build-syntax build-sysml-wasm build-wasm build-wasm-wasip1 build-wasm-js wasm-check static-check windows-versioninfo-check man man-check install-tree pgo-profile conformance conformance-pkg conformance-rust conformance-julia conformance-matlab test test-shard coverage lint clean install help fuml-expected python-test python-coverage scripts-coverage node-coverage python-install proto proto-buf python-proto proto-ts proto-rust proto-lint proto-breaking vscode-grammar vscode-build vscode-package docs docs-install docs-serve docs-engine-assets docs-counts docs-check changelog-check changelog-render self-model

# Version information
# Only release tags describe a build; the moving `nightly` tag is not a version.
Expand Down Expand Up @@ -70,11 +70,10 @@ TOOLS_DIR := tools

# The commands whose manual pages are generated and shipped, in section 1.
COMMANDS := sysml sysml-lsp sysml-grpc
# sysml-engine, sysml-syntax and sysml-core serve the execution, syntactic and
# validation RPCs over JSON so a client needs no protobuf. Natively they are
# built only on request (`build-engine`, `build-core`, `build-syntax`) and stay
# out of `build`, `install`, the release and the manual pages.
WASM_COMMANDS := $(COMMANDS) sysml-engine sysml-syntax sysml-core
# sysml-engine, sysml-syntax, sysml-core and sysml-wasm serve JSON RPC surfaces
# without protobuf. Natively they are built only on request and stay out of
# `build`, `install`, the release and the manual pages.
WASM_COMMANDS := $(COMMANDS) sysml-engine sysml-syntax sysml-core sysml-wasm
MAN_DIR := packaging/man/man1
MAN_PAGES := $(addprefix $(MAN_DIR)/,$(addsuffix .1,$(COMMANDS)))

Expand Down Expand Up @@ -136,6 +135,12 @@ build-core: ## Build bin/sysml-core natively (opt-in; not released)
$(GO_BUILD) -o $(BIN_DIR)/sysml-core ./cmd/sysml-core
@echo "✓ Built $(BIN_DIR)/sysml-core ($(VERSION))"

build-sysml-wasm: ## Build bin/sysml-wasm natively (opt-in; not released)
@echo "Building sysml-wasm..."
@mkdir -p $(BIN_DIR)
$(GO_BUILD) -o $(BIN_DIR)/sysml-wasm ./cmd/sysml-wasm
@echo "✓ Built $(BIN_DIR)/sysml-wasm ($(VERSION))"

build-syntax: ## Build bin/sysml-syntax natively (opt-in; not released)
@echo "Building sysml-syntax..."
@mkdir -p $(BIN_DIR)
Expand All @@ -146,7 +151,7 @@ build-syntax: ## Build bin/sysml-syntax natively (opt-in; not released)
# host), GOOS=js under Node or a browser through the toolchain's wasm_exec.js. The
# version stamps are the -X flags every other build passes; there is no Windows
# resource to embed and no libc to link. `build` stays native: these are opt-in.
build-wasm: build-wasm-wasip1 build-wasm-js ## Build all three commands for both WebAssembly targets
build-wasm: build-wasm-wasip1 build-wasm-js ## Build all seven commands for both WebAssembly targets

build-wasm-wasip1: ## Build bin/wasm/wasip1/*.wasm, runnable under a WASI preview 1 runtime
@echo "Building WebAssembly (wasip1)..."
Expand Down
1 change: 1 addition & 0 deletions changes/unreleased/sysml-wasm.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **`sysml-wasm` combines the execution and validation JSON-RPC surfaces in one WebAssembly module.** It routes parsing and symbol facts to the core and evaluation and execution to the engine, shares a single standard-library snapshot, and reports supported capabilities through `GetServerInfo`. JavaScript hosts call `globalThis.sysmlWasm.call(method, paramsJSON)` synchronously; native and WASI builds serve JSON-RPC over stdio. See `docs/reference/wasm.md`.
49 changes: 49 additions & 0 deletions cmd/sysml-wasm/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2025 Open‐MBEE Foundation. All rights reserved.
// Use of this source code is governed by the LICENSE file.

package main

import (
"context"
"flag"
"fmt"
"os"

"github.com/Open-MBEE/OpenSysML/internal/frontend/combined"
)

var (
Version = "dev"
Commit = "unknown"
BuildTime = "unknown"
GoVersion = "unknown"
)

func main() {
showVersion := flag.Bool("version", false, "Show version and exit")
useStdio := flag.Bool("stdio", false, "Serve stdio (the default outside the js WebAssembly target)")
flag.Parse()

if *showVersion {
fmt.Printf("sysml-wasm %s\n", Version)
fmt.Printf(" Commit: %s\n", Commit)
fmt.Printf(" Build time: %s\n", BuildTime)
fmt.Printf(" Go version: %s\n", GoVersion)
return
}

server, err := combined.New(Version)
if err != nil {
fmt.Fprintf(os.Stderr, "sysml-wasm: %v\n", err)
os.Exit(1)
}
os.Exit(serve(server, *useStdio))
}

func serveStdio(server *combined.Server) int {
if err := server.Serve(context.Background(), os.Stdin, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "sysml-wasm: stdio session ended in a protocol error: %v\n", err)
return 1
}
return 0
}
38 changes: 38 additions & 0 deletions cmd/sysml-wasm/serve_js.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//go:build js

package main

import (
"context"
"syscall/js"

"github.com/Open-MBEE/OpenSysML/internal/frontend/combined"
"github.com/Open-MBEE/OpenSysML/internal/frontend/jsonrpc"
)

func serve(server *combined.Server, useStdio bool) int {
if useStdio {
return serveStdio(server)
}
js.Global().Set("sysmlWasm", map[string]any{
"version": Version,
"call": js.FuncOf(func(_ js.Value, args []js.Value) any {
method, params := "", ""
if len(args) > 0 {
method = args[0].String()
}
if len(args) > 1 {
params = args[1].String()
}
var body []byte
var err error
if params == "" {
body, err = server.Call(context.Background(), method, nil)
} else {
body, err = server.Call(context.Background(), method, []byte(params))
}
return jsonrpc.Envelope(body, err)
}),
})
select {}
}
9 changes: 9 additions & 0 deletions cmd/sysml-wasm/serve_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !js

package main

import "github.com/Open-MBEE/OpenSysML/internal/frontend/combined"

func serve(server *combined.Server, _ bool) int {
return serveStdio(server)
}
42 changes: 35 additions & 7 deletions docs/reference/wasm.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ make build-wasm-wasip1 # or one
make build-wasm-js
```

The output is one directory per target, six commands each, stamped with the same version
The 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}.wasm
bin/wasm/js/{sysml,sysml-lsp,sysml-grpc,sysml-engine,sysml-syntax,sysml-core}.wasm
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
```

Expand All @@ -31,10 +31,10 @@ beside it.

`make build` is unchanged and stays native; a WebAssembly build is always asked for.

`make build-engine`, `make build-core` and `make build-syntax` build `sysml-engine`,
`sysml-core` and `sysml-syntax` 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, and no release ships them.
`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, and no release ships them.

`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,
Expand Down Expand Up @@ -209,6 +209,34 @@ 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 combined module

`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.

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`.

## What works

Everything that is the language implementation rather than the host around it:
Expand Down
Loading
Loading