SysML v2 Execution Environment — Architecture¶
Module: github.com/Open-MBEE/OpenSysML
Language: Go 1.23+
Overview¶
A SysML v2 and KerML 1.1 implementation delivering the integrated tooling experience systems engineers expect from modern language ecosystems (Python, Rust, Go).
Core Components¶
- Language Server (
sysml-lsp) — IDE support with live diagnostics, semantic hover, go-to-definition, intelligent completion, and workspace-wide symbol search - Interactive REPL (
sysml) — Exploratory modeling: define models incrementally, evaluate expressions, instantiate parts, inspect runtime state - Execution Runtime — Instantiate parts, evaluate constraints, execute calc/analysis cases, simulate behavioral models
- Toolchain — Workspace management, dependency resolution, incremental compilation, bundled stdlib, persistent caches
Design Principles¶
- Performance: sub-millisecond parsing, a single static binary, and no JVM or Eclipse runtime
- Completeness: SysML v2 textual notation support (98 of 98 standard library files parse cleanly: 94 vendored OMG files and 4 OpenSysML extensions)
- Executable models: beyond validation, a runtime that instantiates, evaluates and simulates
- Incremental and lazy: parse immediately and resolve semantics on demand, following the precedent set by gopls and rust-analyzer
- Immutable AST: all semantic state resides in side tables keyed by node or symbol
This architecture is also written as a SysML v2 model of itself in
examples/self-model/, where the stages carry the Go
packages that implement them, the invariants below are requirements the tool evaluates, and
make self-model renders the diagrams.
Architecture Layers¶
┌─────────────────────────────────────────────────────────┐
│ Frontends: LSP Server │ Interactive REPL │
├─────────────────────────────────────────────────────────┤
│ Workspace: Multi-file projects, dependency management │
├─────────────────────────────────────────────────────────┤
│ Semantic Engine: Types, resolution, validation │
├─────────────────────────────────────────────────────────┤
│ Execution Runtime: Expressions, instances, behaviors │
├─────────────────────────────────────────────────────────┤
│ Parser/Lexer: Hand-written recursive descent │
├─────────────────────────────────────────────────────────┤
│ AST: Syntax-only, immutable (semantics in side tables) │
└─────────────────────────────────────────────────────────┘
Module Structure¶
github.com/Open-MBEE/OpenSysML
├── cmd/
│ ├── sysml-lsp/ # LSP server binary
│ ├── sysml-grpc/ # gRPC server binary
│ └── sysml/ # Interactive REPL binary
├── internal/ # One directory per layer; a package imports only the layers below it
│ ├── syntax/
│ │ ├── source/ # Source files, spans, line indexing
│ │ ├── diag/ # Diagnostic and Severity, quick fixes, ConformanceMode
│ │ ├── lexer/ # Hand-written scanner (~200 keywords)
│ │ ├── parser/ # Recursive-descent parser
│ │ ├── ast/ # Syntax tree nodes (immutable); ast/astcodec encodes them
│ │ ├── pack/ # Packed model files
│ │ └── format/ # Notation formatter
│ ├── semantic/
│ │ ├── symbols/ # Symbol tables, scope trees
│ │ ├── resolve/ # Name resolution (lazy, memoized)
│ │ ├── suggest/ # Name suggestions for unresolved references
│ │ ├── semantics/ # Type system, conformance, multiplicity, invocation selection
│ │ ├── identity/ # Element identities and the normative ids
│ │ ├── highlight/ # Semantic token classification
│ │ └── query/ # The API element-query model and its OSLC prefix map
│ ├── ir/
│ │ ├── lower/ # AST → execution IR (ActionGraph/StateGraph)
│ │ ├── queryplan/ # Document-query definitions → immutable query plans
│ │ ├── docplan/ # Document definitions → immutable document plans
│ │ └── view/ # View and viewpoint evaluation
│ ├── check/
│ │ ├── passes/ # Validation passes (syntax → constraints); kit, behavior, document, diagram, identity
│ │ └── edit/ # Workspace edits and the rename conflict check
│ ├── exec/
│ │ ├── runtime/ # Execution engine (eval, instances, builtins)
│ │ ├── solve/ # SMT-backed solving and configuration
│ │ ├── smt/ # SMT-LIB translation
│ │ ├── analysis/ # Analysis questions, engines and the graphs:1/sources forms (analysis/modelform, analysis/enginewire)
│ │ ├── engines/ # External analysis engines
│ │ └── objref/ # Object references
│ ├── translate/
│ │ ├── rdf/ # RDF graphs, Turtle reading and writing, the SysML vocabulary (rdf/ontology)
│ │ ├── export/ # The RDF mapping: ToRDF (tree → graph) and ToSysML (graph → notation)
│ │ ├── xmi/ # XMI element tree indexed by xmi:id; xmi/sysmlv1 reads SysML v1 exports over it
│ │ ├── migrate/ # SysML v1 XMI → SysML v2 notation
│ │ ├── convert/ # Conversion entry point: formats, Convert, Migrate, SyntaxError
│ │ ├── codegen/ # Code generation from models
│ │ └── interop/ # Flexo MMS (interop/flexo) and repository sync (interop/reposync)
│ ├── doc/
│ │ ├── queryexec/ # Query plan execution → typed, ordered row sets
│ │ ├── docir/ # Document plan evaluation → backend-agnostic document tree
│ │ ├── docrender/ # Markdown and HTML document backends
│ │ └── docpdf/ # PDF document backend
│ ├── workspace/
│ │ ├── model/ # Workspace, document management
│ │ ├── libs/ # Standard library bundling & caching; libs/errata declares its errata
│ │ ├── project/ # Project files
│ │ └── envvar/ # Environment variables the binaries read
│ └── frontend/
│ ├── protoconv/ # Runtime values and instance graphs ↔ API protobuf messages
│ ├── grpc/ # gRPC service implementation
│ ├── lsp/ # LSP protocol implementation
│ ├── repl/ # REPL loop implementation
│ ├── stdiorpc/ # JSON-RPC over stdio
│ └── usage/ # Command-line usage and manual pages
├── client/python/ # Python client bindings (opensysml)
├── client/rust/ # Rust client (opensysml) and its conformance runner
├── api/proto/ # Protobuf service definitions
├── tests/ # Black-box suites, benchmarks, shared fixtures (tests/parser, tests/grpc, tests/testdata, …)
├── tools/ # Nested module: referees, censuses and generators never linked by a shipped binary
├── examples/ # Example models and demos
└── docs/ # Documentation
The layering test (tests/hygiene/layering_test.go) checks the directory order against go list -f '{{.Imports}}':
| layer | may import |
|---|---|
syntax |
syntax |
semantic |
syntax |
ir |
syntax, semantic |
check |
syntax, semantic, ir |
exec |
syntax, semantic, ir |
translate |
syntax, semantic, ir, exec |
doc |
syntax, semantic, ir, exec, translate |
workspace |
every layer below it, check included |
frontend |
every layer |
Two edges cross the order by design and stay pinned: translate/export imports syntax/parser, because the graph → notation decoder parses by nature, and exec/runtime receives its expression parser and argument typer from the caller (SetExpressionParser, SetArgumentTyper) rather than importing check. The remaining upward edges the test tolerates are listed in its tolerated table with the package that carries each.
Core Pipeline¶
Static Analysis Path:
1. Source & Lexer (internal/syntax/source, internal/syntax/lexer)¶
- SourceFile: Input file (.sysml or .kerml) with byte content
- Notation text:
sourcealso owns the keyword sets (Keywords,IsKeyword,IsKeywordIn,IsIdentifier) and the helpers that read and write notation text without a parse —NameText/QualifiedNameText,StringValue/StringText,CommentBody— so layers that never tokenize (semantics, runtime, export) do not import the lexer - Lexer: Hand-written scanner producing tokens with full position tracking
- Trivia: Comments and whitespace tracked as leading/trailing trivia
- Keywords: ~200 SysML keywords (case-sensitive, taken from
source.Keywords())
2. Parser (internal/syntax/parser)¶
- Hand-written recursive descent (chosen over ANTLR4/yacc/JNI bridge)
- Rationale: Zero overhead, full error recovery, sub-ms parses for keystroke-latency feedback
- Entry:
parser.New(source).ParseFile() → *ast.RootNamespace - Always produces tree: ErrorNodes on bad input, parsing never fails
- Grammar source: OMG pilot Xtext grammars (SysML.xtext + KerMLExpressions)
3. AST (internal/syntax/ast)¶
Key architectural rule: AST is syntax-only, immutable after parse
- Node interface:
{Span() source.Span; LeadingTrivia()/TrailingTrivia() []Trivia} - NodeBase: Embedded by all nodes
- No semantic info in AST: All derived data lives in side tables keyed by node/symbol
- Expression AST: Full SysML v2 expression grammar (literals, operators, feature refs, invocations, collections, lambdas)
- Behavioral AST: Action control-flow nodes (InitialNode, FinalNode, ForkNode, JoinNode, MergeNode, DecisionNode, ActionExecutionNode), succession edges with guards
4. Symbols & Resolution (internal/semantic/symbols, internal/semantic/resolve)¶
- Symbol:
{Name, Kind, Decl ast.Node, Visibility, Scope, OwnerScope} - Scope:
{Parent(), Node(), Children(), LookupLocal(name), MemberNames()} - Index:
DocumentRoot(name) *Scope— global qualified-name index - Resolver: Lazy name resolution, memoized,
ResolveQualified(scope, *ast.QualifiedName) (*Symbol, bool) - Deduplication: Short+primary names alias same
*Symbol— dedupe by pointer when walking
5. Semantic Model (internal/semantic/semantics)¶
Runtime's primary substrate. Built via NewModel(*resolve.Resolver). All results memoized in side tables.
model.go:DirectSupertypes(sym)— resolved generalization edges (specializes/subsets/redefines/typing)AllSupertypes(sym)— transitive, cycle-safeConforms(a, b) bool— conformance checkingHasSpecializationCycle(sym) boolmembers.go:MembersOf(sym)— local + inherited members with maskingLookupMember(sym, name)— member lookup- Effective feature list per type (substrate for runtime instantiation)
multiplicity.go:MultiplicityOf(sym) (Range, bool)— parse multiplicity boundsRange{Lower, Upper Bound};Bound{Value int64, Infinite bool, Known bool}eval.go:Eval(n ast.Node) (Value, bool)— constant-folder (seed of runtime)Value{Kind ValueKind, Int, Real, Bool}— int/real/bool/infinity only- Returns
ok=falsefor feature refs, strings, null, invocations, collections - Runtime Tier 3 extends this to full evaluator
invocation.go:SelectCall(scope, e, performs)/SelectInvocation(scope, e, args, performs)— overload selection for a call, memoized per call siteInvocationArgs(e),ChainCallee(e)— the positional arguments and the chain a callx.f(a)applies, as the checker and the runtime both read themArgumentTyper— the seam through which the checker's static argument typing (passes.NewArgumentTyper) is installed withSetArgumentTyper;HasArgumentTyperreports whether one is. Without one,SelectCalltypes arguments by arity and names alone
6. Validation Passes (internal/check/passes)¶
Pluggable validation tiers:
- PassLevel:
{LevelSyntax, LevelNameResolution, LevelType, LevelConstraint} - Pass:
{Level() PassLevel; Run(ctx, name, root) []diag.Diagnostic} - Context: Exposes
Resolver()+Model()(both lazy, memoized) andDownstreamOfFailure(ref)— did a lower tier report a blocking diagnostic inside this reference? - DefaultRegistry: SyntaxPass, NameResolutionPass, TypeCheckPass, ConstraintPass
- Packages: the framework —
Pass,PassLevel,Context,Options,Gathersand the symbol and member walkers — isinternal/check/passes/kit, a leaf the root re-exports as type aliases. The checks are grouped by domain:passes/behavior(state transitions, succession endpoints, control nodes),passes/document(document plans and queries),passes/diagram(layout, view renderings),passes/identity(identity metadata, with the workspace-wide identity gather), and the root for the rest — the registry, the static expression typer and the checks that call into it, the constraint checker and the structural rules. The root is the one place a check is registered, so it imports every domain package and no domain package imports the root;tests/hygienepins both directions - Tiered execution: a document-scoped pass at a higher tier is skipped once a lower tier errors; a pass marked
ElementScopedruns and gates itself per subject throughContext.DownstreamOfFailure(element-scoped tier gating) - Diagnostics:
DiagnosticandSeveritylive ininternal/syntax/diag, a leaf package besidesource, so the runtime and the parser report findings in the same type without importing the validation suite; the strict/default conformance switch isdiag.ConformanceModein the same package - Quick fixes: A
Diagnosticcarries thediag.Fixvalues the layer reporting it attached, so an editor offers edits without parsing messages - Argument typing:
NewArgumentTyperis the checker's expression typing as asemantics.ArgumentTyper;NewTypedModel(resolver)is a semantic model with it installed, which is what every path that builds aruntime.Model(REPL, LSP workspace, gRPC cache, the analysis drivers) constructs. The runtime never installs it itself: a call selected on a model without one fails withruntime.ErrNoArgumentTyperrather than selecting on weaker typing than validation used, andtests/hygienechecks the production construction sites
6a. Highlighting (internal/semantic/highlight)¶
- Semantic tokens:
Tokens(content, root, scope, SegmentResolver)— keywords, comments and literals from the lexer; declared names from the symbol table; reference segments from the resolver - Ordered and disjoint: the result is sorted by offset with overlaps dropped, semantics winning, so a consumer encodes it directly
- Vocabulary: LSP token types and modifiers (
Classes(),Modifiers()give legend order)
7. Workspace (internal/workspace/model)¶
- Single source of truth: Owns document set + global index + diagnostic cache
- reverse reference index
- Document:
{source, AST, scope, version} - One Workspace per session (LSP/REPL)
- Reverse reference index (
refindex.go): every name segment written in a workspace document, keyed by the element it denotes (symbols.KeyOf, i.e. declaring document + declaration span — stable across reindexing, unlike a*Symbol). Each segment is stored under two identities: the element it reaches (after invocation overload selection; a tied call reaches nothing) and the name it writes (an alias, where one was written), with theresolve.Referenceit is a segment of. Find References matches either; Rename edits only the written name, andRenameConflictchecks each occurrence for capture throughedit.CheckRename— a trial reading of the reference with that segment respelled (Resolver.ProbeReading, which keeps what each segment reached even where the whole name then fails), so a chain member is read in its operand's type, a redefinition target among the generals, and a qualifier respelled onto an element lacking the rest of the name is still seen — the check the batch edit API shares. Built lazily on the first query after a change, over all documents with one shared resolver and semantic model, under the workspace's write lock; never built on thedidChangepath. Any mutation (reindexLocked,removeLocked, a conformance-mode switch) drops the whole index, because an edit to one document can change what a name in another resolves to (a shadowing declaration, an import target, an alias, an overload that ties a call). Library documents are never enumerated; only workspace documents are.
8. Standard library (internal/workspace/libs)¶
- Source of truth: the 98 library files under
internal/workspace/libs/stdlib/, embedded in the binary;OPENSYSML_LIBRARY_PATHsubstitutes a directory of files for them. - Shared base:
libs.SharedBase()builds one frozensymbols.Indexof the library per process; every model is an overlay over it (NewOverlay), reading the library without copying it. - Snapshot:
internal/workspace/libs/stdlib.snapshotis that frozen index — syntax trees, scopes, symbols, wildcard-import expansion, facts — serialized at generation time (go generate ./internal/workspace/libs,make stdlib-snapshot) and embedded. A process decodes it instead of parsing, when its recorded digest of the library files and its format version match the files in hand and its CRC-32C over the stream holds; otherwise (an edited file, a library-path override, a stale or damaged blob) it parses the files as before. The snapshot is a derived artifact: never edit it, regenerate it, andTestEmbeddedSnapshotIsCurrentplusmake stdlib-snapshot-checkin CI fail when it lags the files. - Encoding:
internal/syntax/pack(varint scalars over a string table) andinternal/syntax/ast/astcodec(a node table, every node type, index references in place of pointers);symbols.WriteSnapshot/ReadSnapshotnumber scopes and symbols the same way. No reflection orencoding/gob. Decoding reproduces the object graph a fresh load builds, sharing and all, whichTestSnapshotIndexMatchesFreshLoadchecks structurally. - Facts cache:
$XDG_CACHE_HOME/sysml-ls/libsstill holds derived facts for library sets the snapshot does not cover, keyed by content digest and build.
9. Conversion (internal/translate/convert, internal/translate/export, internal/translate/migrate)¶
- Entry point:
internal/translate/convertnames the formats (ParseFormat,FormatOfPath) and drives every conversionsysml -convert,%save,%printand the service'sConvertmake:Convert/ConvertTolerantparse notation and report aSyntaxError,Migrateruns the SysML v1 migration and writes its notation,SysMLToRDFparses and encodes.cmd/sysml,repl,grpcandinterop/flexocall it; nothing below it imports it. - The mapping:
internal/translate/exporttranslates between a parsed tree and a graph —ToRDFandToSysML— and never migrates; it parses only where the decoder needs the grammar (to check preserved source text still encodes to the graph, to judge an expression's binding, and to read names).internal/translate/migratereads SysML v1 XMI and writes SysML v2 notation, and knows nothing of RDF. A hygiene test pinsexportfree ofmigrateand the entry point as the only package besides the CLI (which prints the migration report) that imports it. The result sidecar a migration writes (-migration-results) has its schema ininternal/translate/simresults, which the migrator fills and the comparison (repl,-compare-results) reads without the migrator.
Execution Runtime Architecture¶
Package: internal/exec/runtime
Not a Pass: Execution is stateful/iterative/value-producing (different shape than diagnostic-emitting pass)
Tier 1 — Feature Flattening ✅¶
Harden MembersOf into stable, ordered effective-feature list per type:
- Own + inherited − redefined/masked
- Each entry: type + multiplicity + default-value expression
- Schema for instance materialization
Tier 2 — Instance Model ✅¶
- Value: Extends
semantics.Value→null, strings, instance references, collections (sequences/sets) - Instance: Typed object with one feature value per effective feature (Tier 1)
- Instantiation: Materialize instance graph from
part/itemusage - Recursively instantiate composite features
- Multiplicity governs feature value cardinality
- Lazy feature value materialization
Tier 3 — Expression Evaluator ✅¶
Full evaluator with user-defined calc invocation, constraint evaluation, and requirement evaluation:
- Feature access x.y.z resolved against instance feature values
- KerML operator library (->select, ->collect, size, string ops)
- Calc invocation: Resolve calc symbol → extract params/return → bind args to parameters → evaluate return expression. Overloads are selected through semantics.Model.SelectCall with the argument typing the model carries (ErrNoArgumentTyper when it carries none)
- Function values (function_value.go, ValFunction): a calc definition, a calc usage with an unsupplied input or an in calc parameter read as a value is the calc's lowered calcShape plus the environment it was read in — declaring scope, the object it was read off, and, for a calc declared inside a behavior body, the frames through the innermost active run of that behavior (EvalContext.enclosingRun, by frame.runs) — never a caller's frames, and none when no such run is active. Invoking one (f(a) through a calc-typed parameter, or SampledFunctions::Sample applying its calculation) takes the calc invocation path (invokeCalcShapeIn), never a closure over statements; ValExpr remains the distinct kind for an expression body a collection operation evaluates per element
- Constraint evaluation: Extract assert/assume members → evaluate boolean expressions → check satisfaction (with optional not negation)
- Requirement evaluation: Extract subject/assume/require/actor members → validate bindings → evaluate conditions
- Scoped evaluation: EvalContext.scope for name resolution, frame stack for parameter bindings
- Membership unwrapping: Runtime automatically unwraps AST Membership nodes when extracting members
- Compiled calc tier (compile.go, compiled_ops.go, compiled_stmts.go): a calc whose body is scalar — Integer/Real/Boolean literals, its effective in parameters (flattened through the specialization chain and redefinition exactly as calcShape lays them out for bindCalcParameters), the arithmetic, comparison, equality, identity, logical and conditional operators, body-local scalar declarations, return and if/else statements, invocations of other such calcs (cycles included, positional or by name), and the standard library's scalar functions and constants (sqrt, ln, sin, TrigFunctions::pi, … — dispatched through the resolved symbol to the same Go implementation the evaluator calls, never by bare name, so a model's own sqrt is an ordinary calc) — is compiled on its first invocation into a tree of Go closures over an unboxed scalar frame, held in a side table on the Context's calcShape (the AST is untouched, and a new Context compiles afresh). Statements are compiled from the lowered calcShape.Steps: each declaration takes a fresh frame slot at compile time, masking an earlier binding of its name for the rest of its block, so shadowing and order behave as stmtEngine does, and a name read before its declaration is declined rather than guessed. It reproduces the evaluator's values, errors and per-node step charges exactly; the differential test (compile_differential_test.go) checks that over every calc in the fixture and example trees, and compile_constructs_test.go over focused fixtures in testdata/compiled/. Anything outside the subset — calc usages, out features, feature chains, self, collections and the library functions over them, quantities, strings, loops and assignments, a local without a value, a body that may run off its end, non-literal defaults — keeps the calc, and every calc calling it, on the evaluator.
- Fallback rule: a traced Context (ctx.trace != nil), a non-scalar argument, an unbound parameter without a default, a receiver object where the body reads a library constant, and OPENSYSML_CALC_COMPILE=0 run the whole invocation on the reference evaluator; the tier never falls back for a sub-expression. Argument checking (calcShape.checkArgs: arity and unknown names; the evaluator's refusal of a receiver beside named arguments) precedes the dispatch, so both tiers report those identically.
- Unlocks: Constraint checking against concrete values, calc execution, requirement validation, runtime behavioral verification
Tier 4 — Behavioral AST ✅¶
Parse + model all behavioral bodies with unified fallback grammar:
- Calc bodies — return expressions + mixed parameter declarations (✅ fully executable)
- Constraint bodies — assert/assume with optional not negation (✅ fully executable)
- Requirement bodies — subject/assume/require/actor declarations (✅ fully executable)
- Action bodies — Control flow nodes (initial/final/fork/join/merge/decision) + action execution nodes + succession edges (✅ parsed, executor infrastructure complete)
- State bodies — Entry/do/exit behaviors, substates, transitions with triggers/guards/effects (✅ parsed, executor infrastructure complete)
- Unified Grammar: Body parsers use graceful fallback to general member grammar (no terminal keyword whitelists)
- Status: All parsers complete. Calc/constraint/requirement fully executable. Action/state executors complete with control flow keywords, nested invocation, send statement.
Tier 5 — Behavioral Interpreter ✅ Complete¶
Package: internal/exec/runtime
Status: Complete. Conformance gate: every case passing (calc/constraint/requirement/satisfy/action/state all functional); count in the measured counts.
Spec Alignment: The governing reference is the SysML v2 metamodel or the bundled KerML semantic library (internal/workspace/libs/stdlib/); UML 2.5.1 is a fallback only where the SysML v2 notation has no production for a concept and the KerML library no performance for it (state-body fork/join, history, regions). The runtime is not a UML or fUML activity engine: "token" names the executor's bookkeeping for where each performance is along the successions of the lowered graph, an implementation device, not the semantic model. What the tokens realize is succession order: a succession is a KerML HappensBefore link (Occurrences.kerml), which orders occurrences in time and carries no values — a SuccessionFlow is the form that carries a payload (KerML.kerml: Succession specializes Connector, SuccessionFlow specializes Succession, Flow). State machine execution is Occurrences::Occurrence::isRunToCompletion over its runToCompletionScope ("determines whether transition performances might happen during state entry performances within the run to completion scope"), with event dispatch isDispatch / dispatchScope. See SPEC_COMPLIANCE.md for the detailed compliance mapping, and the pilot differential for what is checked against the reference implementation.
Architecture:
- ActionExecutor — succession-ordered action execution, scheduled as a token queue over the lowered
ActionGraph - Token-based control flow (initial → action → final, first/done keywords)
fork/joinfor parallelism,decide/mergefor branching (Actions::ForkAction,JoinAction,DecisionAction,MergeAction)- Nested action invocation with attribute initialization
- Send statement for message passing
- ObjectFlow for pin-to-pin data routing
- Deadlock detection via progress tracking
- Golden trace recording with deterministic token ordering
- APIs:
Step(),RunToCompletion(),Tokens(),SetBreakpoint(),SetTrace() -
Breakpoints and stepping observe action nodes, so a calc an action invokes is one step to them; only a
TraceRecorderobserves sub-expressions, and it keeps the calc on the evaluator (Tier 3's fallback rule) -
StateExecutor — Event-driven state machine execution
- Initial/final state keywords (initial/final)
- Entry/exit/do behaviors (
doruns while its state is active, one action per round, interleaved with the do behaviors of the states active alongside it) - TimeEvent waits registered on the
Context'sClock, which every executor of the context shares (clock.go,advance.go:Context.Advanceruns all due work instant by instant; executors due together are a scheduler choice) - ChangeEvent condition polling
- Guard evaluation for transitions
- Transition effect actions
- Hierarchical states with LCA-based entry/exit propagation
- Orthogonal regions with multi-region event broadcasting; the order sibling regions react in is a scheduler choice, drawn per firing among the regions still active (
dispatchInOrder), for change triggers as for queued events - Choice + Junction pseudostates
- Golden trace recording for transitions/entry/exit
- APIs:
ProcessNextEvent(),CurrentState(),EventQueue(),StateData(),SetTrace() - Deferred events: an event no active transition handles is retained while a state deferring it is active, and delivered afterwards in arrival order
-
CallEvent matches the operation named by the trigger (
signal.go,state_executor.go;signal_test.go:TestCallEventMatchesOperationName) -
Scheduler and choice points — one resolution rule for what the library leaves unordered (design note)
- Six
ChoiceKinds (choice.go): token order within a step, decision branch, same-step write order, transition, region order, due order; each site resolves through the run'sschedulerand then records aChoicePoint, an informationalRunNote(diagnostic codechoice-point) that never alters the run - Every decision guard is evaluated so a second holding one is seen; a later guard that cannot be evaluated is an
UnevaluableGuardnote (guard-unevaluable), not a failure SchedulePolicy(scheduler.go):reverse(default and zero value — exactly what every run did before policies existed),declared,seed:<n>(a PCG generator the run consumes, replayed by the seed),replay:<file>(a witness's choice lines followed move for move, thenreverse's picks one token a step; a move the run cannot make is a typedReplayError,replay.go),explore[:runs=N,depth=D]Explore(explore.go) replays whole runs from a freshContexteach, a recorded choice prefix then an untried alternative, the first run's choices each varied once, earliest first, before any twice (explore_queue.go), withinExploreBudget(default 1024 runs, 64 choice points); reports distinct outcomes byOutcome.identitywith linearization counts and a witness, andincompletewhen a bound stopped it-
The scheduler lives in the run's
runStatebeside the budget and notes; a run driven call by call (beginExecutorRun) keeps its own across interleaved runs, and a probe (beginProbe) restores the scheduler's position and notes nothing -
Context Integration — Public runtime APIs
InvokeCalc(symbol, args)— Invoke calculation with arguments, return resultEvaluateConstraint(symbol)— Evaluate constraint, return satisfaction boolean (assert/assume)EvaluateRequirement(symbol)— Evaluate requirement, return satisfaction boolean (require/subject/actor/assume/nested)ExecuteAction(symbol)— Run action to completion, return resultsExecuteState(symbol)— Run state machine until final/suspendedCreateActionExecutor(symbol)— Create executor for debuggingCreateStateExecutor(symbol)— Create executor for debuggingSetSchedule(policy),Schedule()— the policy runs started from now on resolve their choice points under (exploreis refused:Exploredrives it)-
Notes(),Choices(),UnevaluableGuards()— what the last run recorded -
Notation text the run reads — the runtime imports no parser. The two places a run receives notation as text — a witness file's
input <feature> = <value>lines (replay.go) and the unit a tool answers a value in (tool.go,Context.UnitOf) — are read through theruntime.ExpressionParserthe frontend installs on theModel(Model.SetExpressionParser, normallyparser.ParseOneExpression). Reaching either with none installed is the typedErrNoExpressionParser, never a refused witness or a tool's malformed output;tests/hygienechecks that every shipped construction site installs it and thatinternal/exec/runtimedoes not depend oninternal/syntax/parser.
Implementation:
- context.go (460 lines) — Public Execute/Invoke/Evaluate APIs, step budget enforcement
- action_executor.go (729 lines) — Token-flow engine with nested actions, send statement
- state_executor.go (1149 lines) — Event-driven state machine with do behaviors
- executor_common.go — Token, Event, EventQueue, ExecutionState
- scheduler.go, choice.go, action_choice.go, explore.go — scheduling policies, choice-point notes, bounded exploration
- trace.go (154 lines) — Deterministic execution trace recorder
- eval.go — Expression evaluation (binary/unary operators, literals, feature references, qualified names, type coercion)
- Lowering to execution IR lives in internal/ir/lower/ (ToActionGraph, ToStateGraph)
Testing:
- Golden ASTs: tests/parser/testdata/parse/ — count in the measured counts
- Negative tests: tests/parser/negative_test.go and the *Negative* tests beside the parser — count in the measured counts
- Unit tests: action_executor_test.go, state_executor_test.go (action, state)
- Conformance gate: .sysml + .expected.json pairs, all passing - conformance_test.go — counts and per-category breakdown in the measured counts; a case whose model admits several results lists them as outcomes, each cited to the semantic oracle, and is explored to prove every one reachable and nothing else; TestExecutionConformanceUnderPolicies re-runs the suite under declared and seed:1
- Golden traces: .trace.golden files - trace_test.go — count in the measured counts; .trace.order files state the partial order a trace must respect (a < b), and a case with outcomes owns a <case>.<policy>.trace.golden per sweep policy
- Exploration: explore_test.go — every linearization reached once, determinism, each budget's incompleteness, an error as an outcome, transition, region and due order
- Robustness: failure-mode cases (deadlock, unbound params, missing features, dangling transitions, sourceless accept, step budget, pseudostate dead ends and cycles, history and defer misuse, send/accept misrouting, calc arity/recursion, perform reference failures) - robustness_test.go and the per-feature robustness_*_test.go
- Coverage: All behavioral types fully functional. Action: 14/14 features ✅. State: 13/13 features ✅. Calc: 8/8 ✅. Constraint: 5/5 ✅. Requirement: 5/5 ✅. Evaluation: 7/7 ✅.
Measured Compliance: See SPEC_COMPLIANCE.md for semantic rule → implementation → test case mapping with status (✅ faithful / ⚠️ approximate / ❌ not yet implemented).
Tier 6 — Analysis & Verification Drivers ⏳ (Future)¶
- Analysis case: subject → calc chain → result values
- Verification case: evaluate requirements → pass/fail
- Entry points: REPL/LSP commands (
%run,%verify) - Design: the analysis framework — registered engines behind one contract, one scale for the strength of an answer, parallel isolated runs; and bring your own engine — how a user's engine, strategy or tool registers and what its answers are worth
- Landed:
internal/exec/analysis— the contract (Question,Engine,Result,Claim,Strength,Budget), a per-ownerRegistrywithautodispatch, and therun,explore,sweepandsolveengines as adapters over the runtime andinternal/exec/solve; the REPL session and the gRPC service ask every check, run, exploration, sweep and solver question through it with no change to what they print or return
LSP Server¶
Package: internal/frontend/lsp
Binary: cmd/sysml-lsp
Status: ✅ Complete (stdio protocol, 10 LSP features, tested end to end)
Features¶
Lifecycle:
- initialize — Advertise server capabilities, record the session's folders
- initialized — Scan those folders and index every .sysml/.kerml file they
hold, so cross-file names resolve without the editor opening each file
- shutdown / exit — Graceful termination
Document Synchronization:
- textDocument/didOpen — Track opened documents (the buffer becomes authoritative)
- textDocument/didChange — Incremental updates (UTF-8 byte offsets)
- textDocument/didClose — Revert to the file's on-disk content; the document
stays indexed, since other documents resolve names through it, but its markers
are withdrawn — only open documents carry diagnostics
- textDocument/didSave — Refresh diagnostics for every open document
- workspace/didChangeWatchedFiles — Reindex files created, edited or deleted
outside the editor; a deletion leaves an open buffer alone
- workspace/didChangeWorkspaceFolders — Walk a folder added mid-session and
unindex what a removed one contributed, open buffers aside
Diagnostics: - Publish on document open/change; the edited document immediately, the other open ones on a coalesced sweep once the edit burst settles, since each sweep re-analyzes them - Withdrawn (empty set) for a document the workspace no longer holds - Syntax errors (parser) - Semantic errors (name resolution, type checking, validation passes) - Real-time feedback
Hover (textDocument/hover): - Symbol info: name, kind, type, multiplicity - Definition source location - Documentation comments (future)
Go-to-Definition (textDocument/definition): - Navigate to symbol declaration - Follows qualified name chains - Cross-document navigation
Find References (textDocument/references): - Find all usages of symbol, in every workspace document, at whichever segment of a qualified name denotes it - Include declaration option, reported in the declaring document - Answered from the workspace's reverse reference index (a lookup, not a scan); Rename reads the same index
Rename (textDocument/prepareRename, textDocument/rename):
- Rewrites the name under the cursor — long or <short> — at its declaration
and wherever a reference in any workspace document writes it
- Refused with an error naming the element the new name would mean when that
name is already taken where the element is declared, or when a rewritten
reference would afterwards read another element (edit.CheckRename)
Completion (textDocument/completion):
- Trigger characters: :, .
- Symbol-based suggestions
- Future: keyword completion, snippet support
Semantic Tokens (textDocument/semanticTokens/full, /range):
- Legend advertised at initialize; tokens classified by internal/semantic/highlight
- Keywords, comments and literals from the token stream; names from the symbol
table and the resolver, with declaration/definition/readonly/abstract modifiers
- Encoded relative to the previous token, split per line; no delta support
Code Actions (textDocument/codeAction):
- Quick fixes only, from the diag.Fix values parser and resolver
diagnostics carry — spelling of an unresolved name, importing the namespace
declaring it, inserting a semicolon the parser located exactly
Document Symbols (textDocument/documentSymbol): - Outline view (packages, parts, attributes, actions, states) - Hierarchical structure - Navigate within file
Workspace Symbols (workspace/symbol): - Global symbol search - Fuzzy matching - Aggregates across every indexed document, opened or not
Implementation¶
Architecture:
- server.go — Server lifecycle, stdio transport
- base.go — Stub handlers for unimplemented LSP methods
- handler.go — Custom didChange with pointer-valued Range (full vs incremental edits)
- sync.go — Document synchronization (didOpen/didChange/didClose/didSave)
- files.go — Folder scan and watched-file events (the on-disk half of the workspace)
- lifecycle.go — Initialize capabilities advertisement
- diagnostics.go — Error publishing
- hover.go, completion.go, definition.go, references.go, symbols.go — Feature implementations
- posmap.go — UTF-8 offset ↔ LSP line/character conversion
- semantictokens.go, codeaction.go — Semantic tokens and quick fixes
- walk.go — Reference lookup over resolve.References
Testing: - Tests covering every feature — count in the measured counts - Integration tests with mock clients - Incremental sync edge cases (astral plane characters, multi-change, offset-zero insertion)
Usage:
Editor Setup: - VS Code: Generic LSP Client extension + workspace settings - Neovim: nvim-lspconfig custom server - Emacs: lsp-mode manual server registration
See the guide for VS Code configuration.
REPL Integration¶
Package: internal/frontend/repl
Binary: cmd/sysml
Commands¶
Document management:
- %help — Show help
- %list — List current session declarations
- %clear — Reset session
- %load <file> — Load .sysml file
Runtime execution:
- %instantiate <name> — Create instance from part def
- %eval <expr> — Evaluate expression (feature refs + literals)
- %features <object> [all|depth <n>] [json] — Show an object's features and their values, bounded unless asked for whole or to a depth; json writes the graph in the API's InstantiateResponse shape
- %instances — List all created instances
Behavioral execution:
- %calc <name> [args...] — Invoke calculation with literal arguments (e.g., %calc add 10 20)
- %constraint <name> — Evaluate constraint, check assert/assume satisfaction
- %requirement <name> — Evaluate requirement, validate subject/require/actor conditions
- %satisfy [name] — Evaluate satisfaction assertions, with the requirement's subject bound to the object by names
- %schedule [policy] — Show or set the policy the next run resolves its choice points under (reverse, declared, seed:<n>, replay:<file>); explore is refused, since a debugging session steps one run
Action debugging:
- %action <name> [<object>] — Start debugging action execution, optionally performed by an instantiated object
- %step — Advance all tokens one step
- %continue — Run action to completion, or to the first breakpoint hit
- %tokens — Show active tokens with location + data
- %break <nodeName> — Set breakpoint on node; %continue stops when a token reaches it
State machine debugging:
- %state <name> [<object>] — Start debugging state machine, optionally performed by an instantiated object
- %events — Show event queue length
- %current — Show current state, stack, stateData, time
- %advance <time> — Advance simulation time by <time> units, processing every event due
- %stop — Stop debugging session
Implementation¶
- Session: Manages document + runtime context + instances + debugging sessions
- getOrCreateRuntime(): Lazy init, builds index from current document
- Runtime commands wire to:
runtime.Context.Instantiate(),runtime.Context.Eval(),runtime.Context.InvokeCalc()runtime.Context.EvaluateConstraint(),runtime.Context.EvaluateRequirement()runtime.Context.ExecuteAction(),runtime.Context.ExecuteState()runtime.Context.CreateActionExecutor(),runtime.Context.CreateStateExecutor()- Argument parsing:
%calcparses literal args via wrapper parsing (part { attribute arg = <expr>; }) + Membership unwrapping - Debugging sessions: Session tracks active ActionExecutor/StateExecutor for step-by-step control
- Protobuf output:
%features … jsonserializes the instance graph throughinternal/frontend/protoconv(InstanceGraphToProtoWithin,GraphBounds), the same conversion the gRPC service uses forInstantiate; the REPL, and sosysml, do not link the service or its transports
Technology Choices¶
Why Go?¶
- Goroutines: Concurrent reindex/query handling
- Single binary: Cross-platform, no JVM/runtime dependencies
- LSP track record: gopls demonstrates Go's suitability for language servers
- Performance: Fast compilation, efficient memory model
Why Hand-Written Parser?¶
Alternatives rejected: ANTLR4-Go, goyacc, JNI/gRPC bridge to pilot
Rationale: - Zero runtime overhead - Full control over error recovery - Sub-millisecond parses (keystroke-latency diagnostics) - Trade-off accepted: Manual grammar translation from Xtext
Incremental & Lazy Analysis¶
Precedents: gopls, rust-analyzer
- Parse immediately (syntax errors visible instantly)
- Defer name resolution / type checking until requested
- Memoize all semantic queries
- Result: Interactive performance even on large workspaces
Development Status¶
| Component | Status |
|---|---|
| Lexer/Parser (structural + behavioral) | ✅ Operational (105/105 stdlib clean - see conformance gate) |
| Symbol resolution & type system | ✅ Complete |
| Validation passes (syntax → constraints) | ✅ Complete |
| Expression evaluator & instance model (Tiers 1-3) | ✅ Complete |
| Workspace/reindex/file watching | ✅ Complete |
| Behavioral parser (all behavioral bodies) | ✅ Complete |
| Calc invocation & constraint evaluation | ✅ Complete |
| Action execution engine (Tier 5) | ✅ Complete |
| State machine runtime (Tier 5) | ✅ Complete |
| REPL debugging commands | ✅ Complete |
| REPL implementation | ✅ Complete |
| Standard library bundling | ✅ Complete |
| LSP server implementation | ✅ Complete |
Parser coverage: 106/106 bundled library files parse cleanly — the 94 official SysML v2 standard library files and the 12 non-normative OpenSysML extensions: OpenSysML Libraries/OpenSysMLMathFunctions.kerml, DocumentQueries.sysml, IdentityMetadata.sysml, OOSEM.sysml, DiagramLayout.sysml, MOSA.sysml, RandomFunctions.kerml, Simulation.sysml, StateSpaceIntegration.sysml, Stochastic.sysml, AnalysisRecords.sysml and MigrationMetadata.sysml. Conformance verified by stdlib_conformance_test.go. Grammar reference available at OMG Xtext grammar.
Testing Strategy¶
Parser Test Contract¶
New grammar features require a four-layer test contract to ensure correctness and prevent regressions:
1. Conformance Gate¶
- Purpose: Ensure stdlib continues to parse cleanly
- Location:
internal/workspace/libs/stdlib_conformance_test.go - Test:
TestStdlibConformanceloads all 106 bundled library files - Acceptance: 106/106 files parse without errors
- Allowlist:
testdata/stdlib_known_failures.txt(currently empty) - Failure mode: Regression breaks previously-working stdlib files
Usage:
2. Golden AST Snapshots¶
- Purpose: Verify AST structure matches expected output
- Location:
tests/parser/golden_test.go - Fixtures:
testdata/parse/*.sysmland*.kerml(one representative file per construct) - Goldens:
testdata/parse/*.golden(AST dumps) - Acceptance: Parse output matches golden file
- Update flag:
go test -run TestGolden -update(regenerate goldens after intentional changes)
Coverage: - Package/namespace declarations - Part/attribute definitions and usages - Connections and relationships - Requirements and constraints - State machines - Calculations - Enumerations - Imports and aliases - Metadata annotations
3. Round-Trip Serialization¶
- Status: Explicitly deferred (no faithful SysML printer exists)
- Rationale:
ast.Dump()is debug-only, not spec-compliant - Future work: If SysML printer added, verify
parse(print(parse(input))) == parse(input)
4. Negative Test Suite¶
- Purpose: Verify parser rejects malformed input gracefully
- Location:
tests/parser/negative_test.go - Test:
TestNegative, one subtest per malformed input - Acceptance: each case produces diagnostics rather than panicking
- Coverage: Unclosed blocks, unexpected tokens, invalid syntax, incomplete behavioral members
Example:
Behavioral Test Contract¶
New behavioral features (actions, states, calc, constraints, requirements) require a four-layer test contract to ensure execution correctness:
1. Golden AST Fixtures¶
- Purpose: Lock in parse structure before execution changes
- Location:
tests/parser/testdata/parse/(behavioral fixtures) - Coverage: the behavioral fixtures (action, calc, constraint, requirement, state) among the whole set
- Acceptance:
TestGoldenpasses, AST dumps match expectations - Update flag:
go test -run TestGolden -update
Behavioral fixtures:
- action_control_flow.sysml, action_if_branch_body.sysml, action_mixed_params.sysml, action_send_port.sysml
- state.sysml, state_full.sysml, state_transition_variants.sysml, state_call_trigger.sysml, state_def_region_pseudostate.sysml, state_defer.sysml, state_fork_join.sysml, state_history.sysml, state_timed_triggers.sysml
- calc.sysml, calc_defaults_and_invocation.sysml, calc_return.sysml, calc_return_parameter.sysml
- constraint_assert_assume.sysml
- requirement.sysml, requirement_members.sysml
2. Execution Conformance Gate¶
- Purpose: Verify behavioral execution produces expected outcomes
- Location:
internal/exec/runtime/conformance_test.go - Test:
TestExecutionConformanceruns.sysml+.expected.jsonpairs;TestExecutionConformanceUnderPoliciesruns them again underdeclaredandseed:1 - Schema:
internal/exec/runtime/testdata/conformance/README.md(outcome format for each behavioral type;outcomeswith anadmissiblecitation when the model admits several) - Allowlist:
known_failures.txt(currently empty — all cases pass) - Acceptance: Expected outputs/satisfaction match actual execution results under every policy; a case listing
outcomesis explored, and every listed outcome must be reached and no other (design note)
Coverage (by fixture prefix, all passing; counts in the measured counts):
- Calc: parameter binding, return values, defaults, inherited parameters, unary operators, type coercion, qualified names, body-local usages, statement bodies, nested and from-constraint invocation
- Action: token flow, outputs, nested invocation, send/accept, port communication, perform reference and shorthand, accept...then, flows, loops and decisions
- State: simple, do behavior, concurrent do, transition effect, choice/junction/fork-join pseudostates, orthogonal regions and region pseudostates, shallow/deep history, deferred/undeferred events, call and timed triggers, signal discrimination/unmatched, self signal
- Requirement: require/subject/actor/assume satisfaction, nested
- Instance: derived feature values, constraint binding, inherited constraints, nested usage bodies
- Unit and quantity evaluation
- Constraint: assert/assume satisfaction, negation
- Satisfy assertions, variations, redefinitions, variants, feature chains, string operations, nested behaviors, element filters, the ball-and-chain model, and one each of attribute, connector, cubesat and view
Usage:
3. Golden Execution Traces¶
- Purpose: verify how execution proceeds (ordering, scheduling), not only the final result
- Location:
internal/exec/runtime/trace_test.go - Test:
TestExecutionTracecompares executor traces against.trace.golden, and against the partial order a.trace.orderstates (a < bper line) - Determinism: Token sorting by ID, fixed event queue tie-breaking; each
choicethe run made is a trace line, so a golden pins one linearization and a case withoutcomesowns one golden per sweep policy (<case>.<policy>.trace.golden) - Acceptance: Trace output matches golden file
- Update flag:
go test -run TestExecutionTrace -update-traces - Coverage:
.trace.goldenfiles for action, calc, state, constraint, accept and string execution
Trace format:
- Action: step N: token T1@node1, token T2@node2 (sorted)
- State: entry: StateName [hasEntryAction], transition: From -> To [event], exit: StateName [hasExitAction]
4. Runtime Robustness Tests¶
- Purpose: Verify malformed/pathological behaviors fail gracefully (typed errors, no panics/hangs)
- Location:
internal/exec/runtime/robustness_test.goand onerobustness_<feature>_test.goper feature - Test: the
TestRuntimeRobustness*functions, one subtest per failure mode; a feature's cases live in its own file and function, so two features never edit one registry - Acceptance: All return typed errors, never panic, timeout guard (60s) prevents hangs
Failure modes:
- Deadlocked action (join starvation)
- Decision with no satisfied guard
- State machine with dangling transition
- Sourceless accept...then written first in its body or after a member that is not a state
- Calc with unbound parameter, surplus or unknown-named arguments, no result, non-calc target, direct or mutual recursion
- Constraint referencing missing feature
- Step budget exceeded
- Fork/join misuse (branches sharing a region, join with one incoming branch)
- Region pseudostate with no satisfied guard, or a cycle
- Non-numeric time trigger
- Send that reaches only its addressee, accept of an unsent type, send through an unconnected port
- History outside a composite state, or without a record or default
- Defer of a non-deferrable trigger
- Non-terminating do behavior
- Call of an unhandled operation, call argument of the wrong type
- perform of a missing action, perform reference cycle
Usage:
Declared errata (tools/oracle/errata)¶
The three pilot oracles read OMG-published material, which is sometimes wrong itself.
tools/oracle/errata is the registry of those defects: file, line, published bytes, the specification
clause violated, the derivation, and the corrected text where the intended reading is unambiguous.
The overlay mechanism and the entries for the bundled standard library live in the product's
internal/workspace/libs/errata, which internal/workspace/libs applies on read; the registry adds the
corpus entries and the corrected copy of a corpus root an oracle runs over a second time.
The published corpus is never written to — corrections are applied to a copy under the oracle's
output directory — and an entry whose published text no longer matches the bytes on disk fails a
test rather than rotting. Each oracle reports both censuses; the as-published one stays the
conformance statement. See the declared errata overlay.
Behavioral Semantics Map¶
See: SPEC_COMPLIANCE.md for semantic rule → implementation → test case → status mapping.
Every behavioral feature must have: - Semantic rule reference: the SysML v2 metamodel or the bundled KerML semantic library, and UML 2.5.1 only where neither has the concept - Implementation location (file:function) - Test case(s) exercising the feature - Status: ✅ Faithful / ⚠️ Approximate / ❌ Not Yet Implemented / ⛔ Deliberate Divergence / 🚧 Known Failure
Measured against the pinned reference (PILOT_TAG=2026-08, artifact 0.62.0). Every number below is generated by make docs-counts from the committed baselines and gated; none of them is typed in by hand.
- Corpus agreement: 345 of 380 files agree diagnostic-by-diagnostic; 41 diagnostics are ours alone and 1614 the reference's alone, and the first number must be read by root: our diagnostics against the reference's own corpora fell while our non-standard-notation warnings on our own example models rose (differential,
go run -C tools ./cmd/pilot-diff). - Declared-diagnostic silence: of the 512 declared
errorsrows in the reference's own Xpect suites, we report nothing for 0. 245 we report word-for-word; 248 wording-only and 7 location-only differences are agreement in substance and are not counted as gaps; 0 more we report as a warning and 2 elsewhere in the file (Xpect oracle,go run -C tools ./cmd/pilot-xpect). - Scope agreement: 230 of 230 declared scope assertions match exactly (same source).
- Permissiveness gaps: of 310 invalid models we wrote ourselves, the reference rejects 4 that we accept by default, and 297 both reject; 4 further cases agree only when we are asked strictly. We authored every one of these cases ourselves, so the denominator measures the reach of our own corpus and not our conformance; agreement reached only under an opt-in strict mode is weaker evidence than agreement by default (rejection oracle,
go run -C tools ./cmd/pilot-reject). - Declared errata: the registry declares 12 defect(s) in the published reference material — 4 with a specification-derived correction, 8 documented without one, since no intended reading can be inferred (OMG issues,
tools/oracle/errata). Every figure above is as published and stays the conformance statement; running the same oracles over the corrected text instead reports 346 of 380 files agreeing, 40 diagnostics ours alone and 1614 the reference's alone, 0 declared rows we are silent on, and 0 of 310 authored cases the reference alone rejects. The corrected figures are diagnostic only: an erratum never reclassifies a divergence category, and the published corpus is never edited. - Self-assessed surface: the action, state-machine and classifier-behavior rows have no external referee at all — the four refereed figures above cannot see them, because the pinned artifact evaluates expressions but executes neither actions nor state machines. Spec compliance counts them.
What these numbers cannot show: the OMG corpora are demonstrations rather than an official conformance suite; the differential is one-directional, comparing the diagnostics the two implementations report on the same files; the Xpect suites are the pilot authors' test intent rather than a certification oracle; and none of these is a percentage of the specification — no global compliance figure is claimed anywhere.
Row bookkeeping: the ✅/⚠️/❌/⛔ status of each tracked rule stays in spec compliance as a census of our own row list, counted when the documentation site is built rather than committed. It moves when rows are rewritten and does not move when an oracle does, so it is not the progress measure.
Calc/constraint/requirement functional. Action/state executor infrastructure complete (fork/join/decision, TimeEvent/ChangeEvent, guards, hierarchy, orthogonal regions all tested); every conformance case passes. Fork/join, shallow/deep history and deferred events are implemented and reachable from source text — see docs/project/spec-compliance.md and docs/reference/grammar/README.md.
Unit & Integration Tests¶
- Unit tests: Per-package test coverage (lexer, parser, semantics, runtime)
- Integration tests: End-to-end REPL/runtime scenarios
- Test fixtures:
tests/testdata/*.sysml,tests/testdata/*.kerml - Golden files: Expected parse/resolve/diagnostic outputs
- Verification:
go test ./...(all tests pass),go build ./...(clean build)
Contributing New Grammar Features¶
When adding parser support for new SysML v2 constructs:
- ✅ Add representative example to
testdata/parse/*.sysml - ✅ Run
go test -run TestGolden -updateto generate golden - ✅ Verify
TestStdlibConformancestill passes (no regressions) - ✅ Add negative test case if construct has error conditions
Contributing New Behavioral Features¶
When adding execution support for behavioral constructs (actions, states, calc, constraints, requirements):
- ✅ Add golden AST fixture to
tests/parser/testdata/parse/(if not already covered) - ✅ Implement semantics in
internal/exec/runtime/(executor or evaluator) - ✅ Add conformance case:
.sysml+.expected.jsonininternal/exec/runtime/testdata/conformance/ - ✅ Add golden trace case:
.trace.goldenfor ordering-sensitive features (fork/join, transitions) - ✅ Add robustness test for failure modes (deadlock, unbound params, missing refs)
- ✅ Update
docs/project/spec-compliance.mdwith semantic rule → implementation → test → status - ✅ Verify all tests pass:
go test ./internal/syntax/parser/ ./internal/exec/runtime/
See CONTRIBUTING.md for full contribution guidelines.
References¶
- OMG SysML v2.1 Beta 1 Spec: https://www.omg.org/spec/SysML/2.0 (2026-08 release)
- Pilot Implementation: SysML-v2-Pilot-Implementation 2026-08
- Pilot Xtext Grammar:
SysML.xtext+KerMLExpressions(OMG reference implementation) - Metamodel: OMG SysML v2 metamodel (semantic foundation)
- Precedents: gopls (Go LSP), rust-analyzer (Rust LSP), IPython/Jupyter (REPL design)