Complete API reference for OpenSysML packages.
github.com/Open-MBEE/OpenSysML/client/opensysml is what a Go program outside this repository
imports: parse, symbol lookup, evaluation and instantiation, answered in process by the engine the
importing binary already links, or over Connect against a service someone else runs. It is the
only package covered by a compatibility commitment; everything under internal/ below is
documented for contributors and may change without notice.
go get github.com/Open-MBEE/OpenSysML@latestclient, err := opensysml.New() // in process; Dial(address) for a remote service
defer client.Close()
model, err := client.ParseFile(ctx, "vehicle.sysml")
mass, err := client.Evaluate(ctx, model, "mass", opensysml.WithSubject("Demo::sedan"))Nothing else is installed — the standard library is embedded in the module and the v1 operations
never shell out — and a Client is safe to share across goroutines, so one per process is the
intended shape. ParseFile and ParseSource each parse one document; ParseFiles and
ParseDocuments parse several as one model, each keeping its own name, so an import between them
resolves and a diagnostic locates itself in the file it came from.
ExecuteAction, ExecuteState and RunAnalysis answer one run, under the scheduling policy
WithSchedule/Schedule names (declared, reverse, seed:<n>), on the object
PerformedBy/Subject names — a part definition or usage, or a path from one into its parts
such as Mission::mission.vehicle, made anew for the run. ExploreAction,
ExploreState and ExploreAnalysis answer every run: they take the explore policy — the
default when none is given, or explore:runs=N,depth=D to set its budget — and report an
Exploration, one Outcome per distinct result with the number of linearizations that reached
it and one run's choices as its Witness, plus whether the search was Complete or which
BudgetsHit ended it (Status() renders it as the sysml command does). A run that fails under
some order is an Outcome whose Error is set, not a failure of the call. The two families
refuse each other's policies with CodeInvalidArgument, and exploring requires the
schedule_explore capability alongside schedule.
A Session (opensysml.OpenSession(client, model)) is the interactive counterpart of those
one-run calls: it keeps its clock, its schedule (SetSchedule) and the objects it instantiated
between calls, so Instantiate, ActiveStates, Transitions, Accepts, Send, Advance,
Perform, Feature, SetFeature, Evaluate and Members play a model one step at a time and
answer facts about it. It is in-process only — opened from a New client, refused by a Dial
client with CodeUnimplemented — and is not part of the Client interface; the package README
explains why.
ListEngines names the analysis engines the service answers with, as EngineInfo in name order.
VerifyConstraint, VerifyRequirement, VerifySatisfaction and ValidateInstance take WithEngine(name) and
RunAnalysis and ExploreAnalysis take Engine(name), and Calculate (EvaluateCalc with
options, its arguments under CalcArguments) takes CalcEngine(name), to put the question to one engine,
EngineAll to ask every engine that covers it, or EngineAuto (the default) to leave the choice
to the service; a name the service does not register is CodeInvalidArgument, and a named engine
needs the engines capability. Verdict, Calculation and Analysis each carry a Standing:
the Engine that answered, the Strength of its evidence and the Bounds it ran under.
exploration, err := client.ExploreAction(ctx, model, "Demo::race", nil)
for _, outcome := range exploration.Outcomes {
fmt.Println(outcome.Outputs["winner"], outcome.Linearizations, outcome.Witness)
}
fmt.Println(exploration.Status()) // complete (6 runs)ApplyEdits rewrites a model's source and answers an EditResult: Documents is the edited
notation of every document the batch reached, each under the name the parse gave it, and
Content is the same notation for a model of exactly one document — the field the sole-document
contract answered first, kept so a caller written against it is unchanged; it is empty for a model
of several, even when the batch rewrote only one of them. The operations name elements declared
in the model's first document; ApplyDocumentEdits names another. A rename or cascade delete
follows its references into the model's other documents, every touched document is re-parsed
and re-analysed together, and either all of them are answered or the refusal — an *EditError
whose Referrers name each referrer with its document — carries none. The client sets the
request's accept_documents, which is what lets a model of several documents be edited: a
request without it, as every earlier client sends, is refused on such a model with
FAILED_PRECONDITION as before, so a caller reading content alone is never handed an empty one.
A service advertising apply_edits without edit_documents (CapabilityEditDocuments) predates
Documents: it edits a model of one document and answers Content alone, so a caller checks the
capability before reading Documents, Referrers or an applied edit's Document.
result, err := client.ApplyEdits(ctx, model, opensysml.Rename{Target: "Lib::Engine", NewName: "Motor"})
for _, doc := range result.Documents {
os.WriteFile(doc.Name, []byte(doc.Content), 0o644)
}Its errors, ownership rules, capability negotiation and v1 boundary are in client/opensysml/README.md, and the other client languages are on client libraries. A program with no client library that posts JSON to the service by hand decodes the answers by the wire contract.
Editor.add_member(owner, kind, name, type=None, multiplicity=None, value=None, specializes=None) and its typed add_* helpers create declarations while
preserving untouched source bytes. Editor.delete(target, cascade=False)
removes declarations transactionally; Editor.move(target, owner) carries one
into another namespace of the same document and respells the references the
move would break. opensysml.loads(content, language=None, strict=False) loads inline SysML or KerML for this workflow.
OpenSysML is organized into core packages under internal/core/, with frontends in internal/lsp/ and internal/repl/.
Package Organization:
github.com/Open-MBEE/OpenSysML
├── internal/core/ # Core language implementation
│ ├── source/ # Source files and position tracking
│ ├── lexer/ # Tokenization
│ ├── parser/ # Parsing to AST
│ ├── ast/ # Abstract Syntax Tree
│ ├── symbols/ # Symbol tables and scopes
│ ├── resolve/ # Name resolution
│ ├── semantics/ # Type system and semantic queries
│ ├── passes/ # Validation passes
│ ├── lower/ # AST → execution IR (ActionGraph/StateGraph)
│ ├── runtime/ # Execution runtime
│ ├── model/ # Workspace and document management
│ └── libs/ # Standard library handling
├── internal/lsp/ # Language Server Protocol
├── internal/grpc/ # gRPC service implementation
└── internal/repl/ # Interactive REPL
Source file management and position tracking.
Key Types:
-
SourceFile— Represents a source file with content and line indexingName() string— File pathContent() []byte— Raw bytesLineCount() int— Number of linesLine(n int) string— Get line content
-
Span— Position range in source (offset-based)Start, End int— Byte offsetsContains(pos int) boolOverlaps(other Span) bool
Usage:
src := source.New("example.sysml", []byte("part Wheel;"))
span := source.Span{Start: 0, End: 4} // "part"Tokenization of SysML v2 textual notation.
Key Types:
-
Lexer— Scanner for SysML v2 tokensNext() Token— Get next tokenPeek() Token— Look ahead without consuming
-
Token— Single token with positionKind TokenKind— Token type (keyword, identifier, literal, operator)Span source.Span— Position in sourceText string— Raw text
-
TokenKind— Enum of all token types- Keywords:
KwPackage,KwPart,KwAttribute,KwAction, etc. (~200 keywords) - Literals:
LitInteger,LitReal,LitString,LitBool - Operators:
OpPlus,OpMinus,OpEq, etc. - Structure:
LBrace,RBrace,Semicolon,Comma, etc.
- Keywords:
Usage:
lex := lexer.New(source.New("test", []byte("part Wheel { }")))
for tok := lex.Next(); tok.Kind != lexer.EOF; tok = lex.Next() {
fmt.Println(tok.Kind, tok.Text)
}Recursive-descent parser producing AST.
Entry Points:
New(src *source.SourceFile) *Parser— Create parser(*Parser).ParseFile() *ast.RootNamespace— Parse complete file
Key Functions:
parseDefinition()— Parse def (part, attribute, action, etc.)parseUsage()— Parse usageparseExpression()— Parse expressionsparseActionBody()— Parse behavioral action body (Phase C3)
Error Recovery:
Parser always produces a complete tree. Errors result in ast.ErrorNode placeholders.
Usage:
p := parser.New(source.New("test.sysml", content))
root := p.ParseFile()
// root is always non-nil, check root.Errors for diagnosticsAbstract Syntax Tree nodes (syntax-only, immutable).
Node Interface:
All AST nodes implement:
type Node interface {
Span() source.Span
LeadingTrivia() []Trivia
TrailingTrivia() []Trivia
}Key Types:
Namespace & Elements:
RootNamespace— Top-level (one per file)Package— Package declarationImport— Import statement
Definitions & Usages:
-
Definition— Base for all defs (part, attribute, action, etc.).Kind DefinitionKind— Type of definition.Ident *Identifier— Name.Members []Node— Body contents.Specializations []*QualifiedName— Generalization edges.Visibility VisibilityKind
-
Usage— Base for all usages.Kind UsageKind.Ident *Identifier.Members []Node.Multiplicity *MultiplicityExpr.Value Node— Default value expression
Expressions:
LiteralInteger,LiteralReal,LiteralBool,LiteralStringFeatureReference— Reference to feature by nameFeatureChainExpr— Dot notation (x.y.z)UnaryExpr,BinaryExpr,ConditionalExprInvocationExpr— Function/calc invocationSequenceExpr,CollectExpr,SelectExpr— Collection operations
Behavioral Nodes (Phase C3):
InitialNode,FinalNode— Control flow start/endForkNode,JoinNode— ConcurrencyMergeNode,DecisionNode— BranchingActionExecutionNode— Action invocationSuccessionEdge— Flow between nodes.Source, .Target *QualifiedName.Guard Node— Optional guard expression
Architecture Rule: AST is immutable after parsing. All semantic information lives in side tables.
Symbol tables and scope trees.
Key Types:
-
Symbol— Represents a declared nameName string— IdentifierKind SymbolKind— Type of symbolDecl ast.Node— AST node that declares itScope *Scope— Child scope (if compound)OwnerScope *Scope— Parent scopeVisibility ast.VisibilityKind
-
Scope— Lexical scopeParent() *ScopeNode() ast.NodeChildren() []*ScopeLookupLocal(name string) (*Symbol, bool)— Local lookup onlyLookupLocalAll(name string) []*Symbol— All with that name (short+primary)MemberNames() []string— All declared names in order
-
Index— Global symbol indexDocumentRoot(name string) *Scope— Get document root scope
Usage:
idx := symbols.NewIndex()
idx.AddDocument("example.sysml", root) // root is *ast.RootNamespace
scope := idx.DocumentRoot("example.sysml")
sym, ok := scope.LookupLocal("Wheel")Important: Short name + primary name alias the same *Symbol. Dedupe by pointer when iterating.
Name resolution (lazy, memoized).
Key Types:
Resolver— Name resolverResolveQualified(scope *symbols.Scope, qn *ast.QualifiedName) (*symbols.Symbol, bool)ResolveUnqualified(scope *symbols.Scope, name string) (*symbols.Symbol, bool)ResolveImport(importNode *ast.Import) []*symbols.Symbol
Usage:
res := resolve.New(index)
sym, ok := res.ResolveQualified(scope, qname)Results are memoized internally.
Type system, conformance, semantic queries.
Key Type: Model
Central semantic query engine. Built from resolver:
model := semantics.NewModel(resolver)Methods:
Type relationships:
DirectSupertypes(sym *symbols.Symbol) []*symbols.Symbol— Immediate generalizationsAllSupertypes(sym *symbols.Symbol) []*symbols.Symbol— Transitive closureConforms(a, b *symbols.Symbol) bool— Type conformance checkHasSpecializationCycle(sym *symbols.Symbol) bool— Cycle detection
Members:
MembersOf(sym *symbols.Symbol) []*symbols.Symbol— All members (local + inherited with masking)LookupMember(sym *symbols.Symbol, name string) (*symbols.Symbol, bool)
Multiplicity:
MultiplicityOf(sym *symbols.Symbol) (Range, bool)— Extract multiplicity boundsRange{Lower, Upper Bound}Bound{Value int64, Infinite bool, Known bool}
Constant evaluation:
Eval(n ast.Node) (Value, bool)— Constant-folder for literals and operatorsValue{Kind ValueKind, Int int64, Real float64, Bool bool}ValueKind∈ {ValInt, ValReal, ValBool, ValInfinity, ValInvalid}
Note: Eval() is a constant-folder only. For full runtime evaluation, see internal/core/runtime.
Pluggable validation passes.
Architecture:
Validation runs in tiers:
- Syntax — Checks for ErrorNodes
- Name Resolution — Validates all names resolve
- Type Checking — Type conformance
- Constraints — Deep semantic rules
Higher tiers skip if lower tier fails.
Key Types:
-
Pass— Validation pass interfaceLevel() PassLevel— Which tierRun(ctx *Context, name string, root *ast.RootNamespace) []Diagnostic
-
Context— Provides access to resolver and modelResolver() *resolve.ResolverModel() *semantics.Model
-
Diagnostic— Error/warningSeverity Severity— Error, Warning, InfoSpan source.SpanMessage stringCode string— stable identifier of what was found (syntax, a pass or rule code,choice-point,guard-unevaluable); carried as-is on the wire asDiagnostic.code
Usage:
// Analyze runs the default pass registry internally.
diagnostics := passes.Analyze("example.sysml", root, parseDiags, idx)Execution runtime (Tiers 1-5: instances, expressions, behaviors).
Key Types:
Value System:
-
Value— Runtime valueKind ValueKind— Type tagConst semantics.Value— Integer/real/bool (for ValConst)Str string,Instance int64,Sequence *Sequence,Set *Set
-
ValueKind— EnumValConst— Integer/real/bool (stored in Const field)ValNull,ValString,ValInstance,ValSequence,ValSet
Instance Model:
Instance— Runtime instanceID int64— Unique instance IDType *symbols.Symbol— Type symbolFeatureValues map[string]*FeatureValue— Feature values by feature name
Execution Context:
-
Context— Runtime execution contextInstantiate(sym *symbols.Symbol) (*Instance, error)— Create instanceEval(expr ast.Node, env map[*symbols.Symbol]Value) (Value, error)— Evaluate expressionInvokeCalc(sym *symbols.Symbol, args []Value, scope *symbols.Scope) (Value, error)— Invoke calculationEvaluateConstraint(sym *symbols.Symbol, scope *symbols.Scope) (bool, error)— Evaluate constraintEvaluateRequirement(sym *symbols.Symbol, scope *symbols.Scope) (bool, error)— Evaluate requirementExecuteAction(sym *symbols.Symbol) (map[string]Value, error)— Execute action to completionExecuteState(sym *symbols.Symbol) (map[string]Value, error)— Execute state machine until final/suspendedCreateActionExecutor(sym *symbols.Symbol) (*ActionExecutor, error)— Create action executor for debuggingCreateStateExecutor(sym *symbols.Symbol) (*StateExecutor, error)— Create state executor for debuggingSetSchedule(policy SchedulePolicy) error— Set the scheduling policy the runs started from now on resolve their choice points under; a run already under way keeps the one it started with.exploreis refused withErrExploreUndriven: an exploration replays whole runs over fresh contexts, soExploredrives it rather than one context running under itReschedule(policy SchedulePolicy) error—SetSchedulereaching the runs driven call by call as well — the clock and the behaviors the objects run — which choose under the policy from their next step on as a run started under it would, their configurations, pending events, clock and choices so far kept; the session surface'sSetSchedule.ErrRescheduleMidRunfrom inside a stepSchedule() SchedulePolicy— The policy the next run resolves its choice points underClock() *Clock— The simulation clock every executor of the context reads and waits on:Now()its current instant inSI::s,Waits()every state timer and actionaccept after/accept atregistered on it in due order,NextDue()the earliestAdvance(duration float64) (AdvanceReport, error)— Move the clock bydurationseconds, running every state event, action token, change-condition poll and do round that comes due on the way, instant by instant, within the run budgets; executors due at one instant are ordered by the scheduling policy and recorded as aChoiceDueOrderchoice point. A negative or NaN duration isErrNegativeDuration; zero dispatches what is already due. The report counts the events, do steps and action steps run, the signals dropped and the notes recordedStateOutcomeWithEvents(sym *symbols.Symbol, events []string) (Outcome, error)— Run a state machine on the events and answer itsOutcome: the state it rests in, the states it entered and the values it holds
-
SchedulePolicy— How the executors resolve a run's choice points (several steppable tokens in one step, several holding guards at a decision, several enabled transitions for one event, several regions reacting to one event, several executors due at one instant of the clock; which same-step write to one feature stands follows from the token order). The zero value andDefaultSchedulePolicyarereverse, what every run did before policies were selectable; every.expected.jsonand.trace.goldenrecorded under it still holdsParseSchedulePolicy(spelling string) (SchedulePolicy, error)— Readdeclared,reverse,seed:<n>(n a non-negative decimal integer),explore[:runs=<n>,depth=<d>](n at least 1, d at least 0, in either order, each at most once;DefaultExploreBudget, 1024 runs and 64 choice points deep, for one not given) orreplay:<file>(a file of choice lines, read here withParseChoices); the empty spelling is the default. Any other spelling — an unknown name,seedorseed:without a number,seed:-1,seed:abc,explore:with nothing after the colon,explore:runs=0,replay:without a file, a file that cannot be read, is empty or has a line spelling no choice (a header ofno choice pointsalone is a witness of a run with none) — is a*SchedulePolicyError(Spelling,Reason) matchingErrInvalidSchedulePolicyundererrors.Is, so every surface refuses it before anything runsExplorePolicy(budget ExploreBudget) (SchedulePolicy, error)— Theexplorepolicy with the budget given, refusing one outside the bounds aboveExploration() (ExploreBudget, bool)— The budget when the policy isexploreReplayPolicy(choices []ChoiceTaken) SchedulePolicy— Thereplaypolicy over a witness held in memory, spelledreplay;Replay() ([]ChoiceTaken, bool)reads a replay policy's choices backReplaySpelling(spelling string) bool— Whether a spelling asks for a replay, for a surface with no files of the caller's to refuse before parsingParseChoices(text string) ([]ChoiceTaken, error)— Read a witness header: choices asChoiceTaken.Stringspells them, one per line or joined by;, up to the first blank line after them (what follows, such as a trace body, is ignored); a line spelling no choice is a*ChoiceParseError(Line,Text,Reason)String() string— The spellingParseSchedulePolicyreads the policy back fromIsDefault() bool— Whether the policy isreverseSchedulePolicyNames— The accepted spellings, for usage textreplay:<file>follows the witness move for move — each choice point the run reaches takes the file's next line, which must name that step and pick among the alternatives the run offers — and resolves the rest asreversewould, one token a step, once the lines are spent. A run that could not follow a line, or ended with lines left over, keeps it:Context.Unfollowed() erroris the*ReplayError(Move,Choice,Faced;errors.Is(err, ErrReplayRefused)) the run also fails with, nil when the witness was followed whole or the policy was anotherdeclaredsteps tokens in the order they were spawned and takes the first holding guard and first enabled transition in declaration order;seed:<n>draws every resolution from a pseudo-random sequence the seed fixes, the same on every platform, so one seed replays one run and two seeds may take two linearizations. A policy changes which alternative each choice point takes, never whether it is reported: theTakenof eachChoicePointis what the policy took
-
Explore(stop context.Context, policy SchedulePolicy, fresh func() (*Context, error), run func(*Context) (Outcome, error)) (*Exploration, error)— Run a behavior underexploreonce per linearization within the budget: each run starts from theContextfreshbuilds over the model and lowering they all share, records the alternative taken at every choice point, and each later run replays a recorded prefix and takes an untried alternative at its end — the first run's choice points each varied once, earliest first, before any is varied twice.runperforms one run and answers itsOutcome; an error it returns is theOutcome.Errof an outcome of its own, so a run some orders fail is reported rather than ending the search. Astopthat ends between runs ends the exploration with its error before the next context is built. A policy other thanexploreisErrNotExploring; a replay that does not meet the choice points its prefix recorded isErrExplorationDiverged, since the model's runs are then not a function of their choicesOutcome— What one run came to, in the observables a conformance case compares:Outputs, and for a state machineFinalStateandStateVisits;Errfor a run that failed.String()renders it in a fixed order;Exploretells outcomes apart by a stricter identity that quotes every name, so two runs agreeing on their observables are one outcome and no output name can spell another outcome's rendering. An object a run holds is spelled by its type and feature values, not by the id the run gave it, so two runs building the same object are one outcome whatever their ids.(*Context).ActionOutcome(outputs),(*StateExecutor).Outcome(),(*Context).AnalysisOutcome(result)and(*Context).VerifiedOutcome(result, verdicts)build one over the run's context; a case's verdicts are values namedobjective <name>,assertion <name>andverdict <case>Exploration—Budget,Runs, the distinctOutcomesin canonical order andBudgetsHit,runsbeforedepth, empty whenComplete().Status()renderscomplete (N runs)orincomplete: <budget> budget <limit> hit after N runsExploredOutcome— OneOutcomewith theLinearizationsthat reached it, theWitness(one run'sChoiceTakensequence,FormatChoicesrenders it) andWitnessRunChoiceTaken— One resolved choice point: itsKind,Step,Where, theAlternativesandAmongit had and theTaken/Tookit resolved to
Behavioral Execution (Tier 5):
-
Token— Control token for action execution, carrying no values of its own. A token is the executor's record of where a performance is along the successions of the lowered graph; the ordering it realizes is KerML'sHappensBefore, not a Petri-net or fUML semanticsID int64— Unique token IDLocation ast.Node— Current node (InitialNode, ActionExecutionNode, etc.)
-
ActionExecutor— Succession-ordered action execution engine (a token queue over the loweredActionGraph)Step() error— Advance all tokens one step; a breakpoint met inside a token's body ends the step there, with no other token stepped, and the next step steps the other tokens before resuming the paused oneStepToBreakpoint() error—Stephonoring breakpoints on the nodes tokens sit on asRunToCompletiondoes: a step onto one suspends the run there, and the next step resumes past itRunToCompletion() error— Execute until StateCompleted (max 10k steps)Tokens() []Token— Get active tokens (copy)State() ExecutionState— Current execution state (Ready/Running/Completed/Suspended)Results() map[string]Value— The values the action's features hold, and undernode.pin(p.v,leg.inner.v) those the latest performance of each nested action node holds; complete once execution has completedData() map[string]Value— The live feature space of the action's own performance, which every token in its flow shares. A nested action node performs in a frame of its own, so its pins are not here; read them fromResults()SetBreakpoint(nodeName string)— Set breakpoint on node: a run pauses when a token reaches it, or before a body performs it when it is a node anifbranch or a loop body declares;NodeNames()lists every node a breakpoint may nameClearBreakpoints()— Clear all breakpointsRelease()— End the run for good, ending the paused work of every token a breakpoint left mid-body; a laterStep()orRunToCompletion()returnsErrExecutorReleased, whether the run had completed or not. Call it when abandoning an executor that may be suspendedActionSymbol() *symbols.Symbol— Get action symbol
-
StateExecutor— Event-driven state machine executionProcessNextEvent() error— Process next event from queueCurrentState() ast.Node— Get current StateNodeStateStack() []*ast.StateNode— Get active configuration (hierarchical states)StateData() map[string]Value— Get state machine variablesEventQueue() *EventQueue— Get event queueCurrentTime() float64— Read the context's shared simulation clock (Context.Clock().Now())State() ExecutionState— Get execution stateStateMachineSymbol() *symbols.Symbol— Get state machine symbol
-
ExecutionState— EnumStateReady— Initialized, not startedStateRunning— ExecutingStateCompleted— Finished (final node/state reached)StateSuspended— Paused (waiting for events)
-
Event— State machine eventID int64— Unique event IDType EventType— Time/Change/Accept/CallTimestamp float64— Event timestamp (for TimeEvent)Payload map[string]Value— Event data
Built-in Functions:
Registered KerML builtins:
- Arithmetic:
+,-,*,/,%,** - Comparison:
==,!=,<,>,<=,>= - Boolean:
and,or,xor,not,implies - Collections:
size,isEmpty,->select,->collect - String:
+(concat),size,substring
Usage:
Tier 1-3 (Instances & Expressions):
// The model-derived part (memoized shapes, targets, literals) is built once and
// shared by every context over it; a context is one run's own state.
shared := runtime.NewModel(model, resolver)
// Honour the OPENSYSML_MAX_* budgets instead of the defaults with:
// budgets, err := runtime.BudgetsFromEnv()
// err = ctx.SetBudgets(budgets)
ctx := runtime.NewContext(shared, runtime.DefaultMaxSteps)
inst, _ := ctx.Instantiate(wheelSym)
fv, _ := inst.GetFeatureValue(ctx, "diameter")
result, _ := ctx.InvokeCalc(addSym, []Value{v1, v2}, scope)Tier 5 (Actions):
// Execute action to completion
results, err := ctx.ExecuteAction(myActionSym)
if err != nil { /* handle error */ }
result := results["result"]
// Or under another scheduling policy, refusing a spelling that names none
policy, err := runtime.ParseSchedulePolicy("seed:7")
if err != nil { /* *runtime.SchedulePolicyError */ }
if err := ctx.SetSchedule(policy); err != nil { /* runtime.ErrExploreUndriven for explore */ }
results, err = ctx.ExecuteAction(myActionSym) // the same run on every call
// Or every run: one fresh context per linearization over the shared model
explore, _ := runtime.ParseSchedulePolicy("explore:runs=64")
exploration, err := runtime.Explore(context.Background(), explore,
func() (*runtime.Context, error) { return runtime.NewContext(shared, runtime.DefaultMaxSteps), nil },
func(c *runtime.Context) (runtime.Outcome, error) {
outputs, err := c.ExecuteAction(myActionSym)
return c.ActionOutcome(outputs), err
})
for _, o := range exploration.Outcomes {
fmt.Println(o.Outcome, o.Linearizations, runtime.FormatChoices(o.Witness))
}
fmt.Println(exploration.Status()) // complete (6 runs), or the budget hit
// Or debug step-by-step
exec, _ := ctx.CreateActionExecutor(myActionSym)
exec.Initialize()
for exec.State() != StateCompleted {
exec.Step()
tokens := exec.Tokens()
// inspect tokens
}Tier 5 (State Machines):
// Execute state machine
stateData, err := ctx.ExecuteState(stateMachineSym)
if err != nil { /* handle error */ }
// Or debug with events
exec, _ := ctx.CreateStateExecutor(stateMachineSym)
exec.Initialize()
for exec.State() != StateCompleted {
exec.ProcessNextEvent()
fmt.Printf("State: %s, Time: %f\n", exec.CurrentState(), exec.CurrentTime())
}Workspace and document management.
Key Types:
-
Workspace— Multi-file workspaceAddDocument(name string, src *source.SourceFile) *DocumentGetDocument(name string) (*Document, bool)RemoveDocument(name string)Index() *symbols.Index— Global symbol indexDiagnostics(name string) []diag.Diagnostic
-
Document— Single source fileName() stringSource() *source.SourceFileRoot() *ast.RootNamespaceScope() *symbols.ScopeVersion() int— Increments on update
Usage:
ws := model.NewWorkspace()
doc := ws.AddDocument("example.sysml", src)
diagnostics := ws.Diagnostics("example.sysml")Standard library bundling and caching.
Key Functions:
Load(name string) (*source.SourceFile, error)— Load stdlib fileListFiles() []string— All stdlib files
Standard library is embedded in the binary using Go embed.FS.
Language Server Protocol implementation.
Key Type:
Server— LSP serverRun(ctx context.Context, conn io.ReadWriteCloser) error
Transport: stdio only. sysml-lsp also accepts --stdio/-stdio as an explicit no-op, because
standard language clients (including the bundled VS Code extension) name the transport on the command
line. Any other unknown flag is still rejected with exit status 2.
Lifecycle (LSP 3.17): shutdown is answered, after which every request other than exit is answered
InvalidRequest (-32600) and non-exit notifications are dropped. exit makes Run return and the
process terminate — status 0 after a preceding shutdown, 1 otherwise.
Current Capabilities:
- Document synchronization (open/change/close)
- Diagnostics (syntax + semantic errors)
- Hover (type info)
- Go to definition
- References
- Document symbols
- Workspace symbols
- Completion
Usage:
ws := model.NewWorkspace()
srv := lsp.NewServer(ws)
srv.Run(ctx, stdio{}) // stdio implements io.ReadWriteCloserInteractive REPL implementation.
Key Types:
Session— REPL session state- Accumulates declarations across inputs
- Tracks runtime context and instances
Entry Point:
Loop(reader LineReader, out io.Writer, session *Session) error
LineReader Interface:
type LineReader interface {
ReadLine(prompt string) (string, error)
}Meta Commands:
%help,%list,%clear,%load <file>%search <substring>— List the declared and library symbols whose qualified name contains the substring, with the kind of each%builtins— List the library functions the runtime implements directly, each with the package an import must name for its bare name to resolve%instantiate <name>,%features <name>,%instances%eval <expr>%calc <name> [args...]— Invoke calculation with arguments%constraint <name>— Evaluate constraint%requirement <name>— Evaluate requirement%satisfy [name]— Evaluate satisfaction assertions (assert satisfy <requirement> by <part>;), every one in the model or the ones a named element states
Usage:
session := repl.NewSession()
repl.Loop(reader, os.Stdout, session)This section describes the structured API Query surface; OSLC Query
text is the second spelling over the same elements, and
Which query is which places both beside the
document queries, Evaluate and solve.
The gRPC service implements the query surface the SysML v2 API & Services
standard defines, so a client that speaks that API — the
SysML-v2-API-Java-Client,
the SysML v2 API Cookbook notebooks, MATLAB System Composer's executeQuery —
can filter a model OpenSysML parsed. The standard's schema is authoritative:
api/openapi.yaml in the Java client, components Query, Constraint,
PrimitiveConstraint, CompositeConstraint.
Implementation: internal/grpc/query.go (Service.Query), reported from
GetServerInfo as the query capability. Python: model.query(...)
(client/python/opensysml/query.py). The JSON a hand-written client sends and
receives for this call is shown, captured, on
the wire contract.
Query scope[] elements to consider; empty is the whole loaded model
select[] properties to report; empty reports every one
where one Constraint; absent selects the whole scope
Constraint PrimitiveConstraint | CompositeConstraint (a protobuf oneof)
PrimitiveConstraint property, operator (= > <), value[], inverse
CompositeConstraint operator (and | or), constraint[] (nests arbitrarily)
The RPC takes the same shape, with the standard's JSON names preserved, so translation from the standard's JSON is mechanical:
rpc Query(QueryRequest) returns (QueryResponse); // api/proto/sysml.protoA cookbook payload, sent verbatim through the Python client:
model = opensysml.load("examples/vehicle.sysml")
model.query({"@type": "Query", "where": {
"@type": "PrimitiveConstraint",
"operator": "=", "property": "@type", "value": ["PartUsage"]}})Each answered element is @id (its qualified name), @type and the selected
properties it has. A property an element does not have is absent, not empty.
An element with no qualified identity — an unnamed doc, an anonymous usage, an
anonymous connect — is not answered: its qualified name has an empty
segment (Demo::), so it is neither unique nor a name a scope could use. The
standard identifies an element by @id, and such an element has none
(TestQueryOmitsElementsWithNoQualifiedIdentity).
Neither is one declared inside an action body — a branch of an if, a loop body —
since the body is owned by no element and so names its declarations only locally
(step, not Demo::Drive::step): that name identifies no element and could not
be used as a scope (TestQueryOmitsBodyLocalDeclarations). An answered @id is
always a qualified name that the model resolves back to that element.
The set is closed and is the single source of truth in
queryProperties (internal/grpc/query.go). A property outside it is an
INVALID_ARGUMENT error listing the ones that exist — never a silently empty
answer.
| Property | Reports | Ordered |
|---|---|---|
@id |
The element's qualified name, which is also how scope names it |
|
qualifiedName |
Same as @id |
|
@type |
The element's metamodel type (table below) | |
name |
The element's own name, the last segment of its qualified name | |
declaredName |
name, absent when the name is an effective name borrowed from a referenced feature |
|
shortName |
The element's effective short name (<'HLR-R001'> declares HLR-R001); absent when it has none |
|
declaredShortName |
shortName, absent when the short name is borrowed from a redefined or subsetted feature |
|
documentation |
The body text of the element's doc comment, delimiters and indentation removed; absent when undocumented. This single-valued record reports the first body of an element declaring several — a document query's Project carries every body |
|
owner |
Qualified name of the owning element; absent for a top-level element, whose owner is the document root | |
isAbstract |
true/false for a definition or usage; absent for anything else, and for a standard-library element restored from cache, which carries no declaration |
|
type |
Qualified name of the resolved type of a typed feature; absent when untyped or unresolved | |
multiplicityLower |
Declared lower bound | ✅ |
multiplicityUpper |
Declared upper bound, * when unbounded |
✅ |
Mapping OpenSysML's symbol kinds onto the standard's metamodel type names is the
substantive design decision here; metamodelTypeNames
(internal/grpc/query.go) is the single source of truth, refined per element by
MetamodelTypeNameOf for the kinds one kind spans several metaclasses for, and
TestMetamodelTypeNameCoversEveryKind keeps it total over every kind a parsed
declaration can have. A standard-library element restored from cache may carry no
kind at all, and then reports no @type: it is answered, but never matches a
@type = comparison (and is kept by the inverse of one).
| OpenSysML kind | @type |
|---|---|
package, namespace |
Package, Namespace |
partDef / partUsage |
PartDefinition / PartUsage |
attributeDef / attributeUsage |
AttributeDefinition / AttributeUsage |
itemDef / itemUsage |
ItemDefinition / ItemUsage |
occurrenceDef / occurrenceUsage |
OccurrenceDefinition / OccurrenceUsage |
portDef / portUsage |
PortDefinition / PortUsage |
interfaceDef / interfaceUsage |
InterfaceDefinition / InterfaceUsage |
connectionDef / connectionUsage |
ConnectionDefinition / ConnectionUsage |
flowDef / flowUsage, allocationDef / allocationUsage |
FlowDefinition / FlowUsage, AllocationDefinition / AllocationUsage |
actionDef / actionUsage, stateDef / stateUsage |
ActionDefinition / ActionUsage, StateDefinition / StateUsage |
calcDef / calcUsage |
CalculationDefinition / CalculationUsage |
constraintDef / constraintUsage, requirementDef / requirementUsage |
ConstraintDefinition / ConstraintUsage, RequirementDefinition / RequirementUsage |
caseDef / caseUsage and the analysis / verification / use-case forms |
CaseDefinition / CaseUsage, AnalysisCase…, VerificationCase…, UseCase… |
viewDef, viewpointDef, renderingDef, concernDef and their usages |
ViewDefinition, ViewpointDefinition, RenderingDefinition, ConcernDefinition and …Usage |
enumerationDef / enumerationUsage, metadataDef / metadataUsage, metaclass |
EnumerationDefinition / EnumerationUsage, MetadataDefinition / MetadataUsage, Metaclass |
comment, documentation, textualRepresentation, dependency |
Comment, Documentation, TextualRepresentation, Dependency |
Three kinds spell a metamodel type the notation's keyword does not name directly. Each is the metaclass the pinned grammar's own production returns, so none of the three is an approximation:
| OpenSysML kind | @type |
Why |
|---|---|---|
individualDef / individualUsage |
OccurrenceDefinition / OccurrenceUsage |
IndividualDefinition returns SysML::OccurrenceDefinition (SysML.xtext): an individual is an occurrence, with isIndividual set. The metamodel has no individual class to report |
connectorEnd |
ReferenceUsage, or PortUsage on an interface |
ConnectorEnd returns SysML::ReferenceUsage and InterfaceEnd returns SysML::PortUsage (SysML.xtext), so the end is named by the connector that declares it — MetamodelTypeNameOf reads the owning declaration to tell the two apart |
alias |
Membership |
AliasMember returns SysML::Membership: an alias is a named membership, not an element of its own |
One kind reports a type only when the model being queried declares it.
kerMLType covers KerML's class, struct, assoc, behavior, predicate
and interaction, which are distinct metaclasses the symbol kind does not
record; MetamodelTypeNameOf recovers the metaclass from the declaration's
keyword. A kerMLType restored from the library cache carries no declaration and
so reports no @type, like any cached element whose kind was not retained.
Where the standard is vague, these are the choices this implementation makes:
=compares the property's value as text, and matches if it equals any of the listed values. The standard writes one value and its clients write a list for@type; one value is the degenerate case of the same rule.>and<compare numbers, and require exactly one operand and an ordered property (themultiplicity*pair). A non-ordered property, more than one operand, or an operand that is not a number is anINVALID_ARGUMENTerror, not a false verdict.*parses as infinity, somultiplicityUpper > 1holds for an unbounded feature.- An element that has no value for the property fails the comparison; this is a fact about the element, not a fault in the query.
inversenegates the verdict of its own constraint, so a constraint and its inverse partition the scope.and/orcombine nested verdicts, short-circuiting once one is decisive.- The whole
wheretree is judged before any element is read, so a fault in it — an unknown property, a missing operator, an empty composite,>on an unordered property — is reported whatever the scope holds, including a model that declares nothing (TestQueryFaultIsReportedWithNoElementsToConsider), and wherever it sits, including under an already-decisive sibling. scopeconsiders each named element and everything nested inside it, in declaration order, parents first; a name the model does not have is an error. An empty scope enumerates the parsed document (not the standard library, which is reachable by naming a library element as a scope).
The standard's query model is deliberately weak, and this is an interop surface; the document queries are the expressive one:
- No graph traversal and no transitive closure. There is no "all elements
under X", no "everything that specializes Y", no path expressions and no joins.
Containment is expressible only as a
scope; specialization is not expressible at all, even thoughsemantics.Modelcan answer it. - No
owningProject/@idon the query resource (single-model service, no project or commit store), no derived or computed properties, and no ordering or paging of results — elements come back in declaration order.
The native document pipeline — document queries (calc def specializing
DocumentQueries::Query) and documents (part def specializing
DocumentQueries::Document) — is served by two RPCs, so a CI pipeline or an
external tool can run what %run-query and %render-document run without a
REPL or a script:
rpc RunDocumentQuery(RunDocumentQueryRequest) returns (RunDocumentQueryResponse);
rpc RenderDocument(RenderDocumentRequest) returns (RenderDocumentResponse);Implementation: internal/grpc/docquery.go (Service.RunDocumentQuery,
Service.RenderDocument), reported from GetServerInfo as the
document_query and render_document capabilities. Python:
model.run_document_query(...) and model.render_document(...)
(client/python/opensysml/document.py). The JSON shape of a binding and of the
result table, captured, is on the wire contract.
Both name a loaded model by its hash and a definition by qualified name, and
compose the engine packages the REPL and CLI compose — queryplan →
queryexec for a query, docplan → docir → docrender for a document — so
the rows and the Markdown are the ones those surfaces produce, in the same
deterministic order.
RunDocumentQuery takes typed parameter bindings — an element by qualified
name, an object the service holds, a string, an integer, a real, a boolean, or
a list of these for a multi-valued parameter — and answers the projected column
names and typed rows: each row's element and each cell's values, an element
carried as its qualified name plus its metamodel type (the @type mapping
above), an object as its id, its path and the usage it stands for.
RenderDocument takes no bindings, because a document binds its queries'
parameters in the model; it answers the rendered CommonMark Markdown,
byte-identical to -render-document on the same model.
Both run over the model's runtime and the objects it holds. Instantiate
creates an object for the model named by hash and the service keeps it, under
the qualified name it was instantiated as, for as long as the model stays
cached — the counterpart of the session %instantiate/-instantiate fill
(Objects the session holds).
A binding names a held object by the id Instantiate answered (instance_id)
or by a path (path): the usage name (car, Garage::car), the id (#2), or
a walk through feature values from either (car.wheels[2], #2.wheels[2];
indexes count from 1), exactly what %run-query accepts. Given both, the path
is followed and must reach the object with that id. Instantiating a name again
makes the name denote the new object; the earlier one stays held, reached by
id. What one model holds is bounded: OPENSYSML_GRPC_MAX_HELD_OBJECTS
(default 10000, nested objects counted) is the most objects a cached model
keeps, and an Instantiate whose objects would pass it — at the root or at a
part under it — fails whole with RESOURCE_EXHAUSTED naming the variable,
leaving none of them; a query or render that materializes a part past the
bound fails the same way. Nothing is evicted behind an id a client still
holds; the objects are released together when the model leaves the cache
(Environment variables). A row that is an object
is answered as an object (DocumentObject) — its id, its path and the usage
element it stands for — and so is an object-valued cell such as car.engine
or the two entries of car.wheels.
DocumentQueries::Objects(type = T) enumerates what the model holds, named
objects by qualified name then displaced ones by id, and answers no rows before
the first Instantiate; a document rendered after one reports the objects'
current values rather than their declared defaults.
A query over Verdicts runs over the same population — the held object a
binding names, or the element as declared — and answers its rows as verdict
values (DocumentVerdict): the assertion checked as an element value, its kind
and text, the path of the object checked, the verdict (holds, violated,
undecided), the condition found false, the reason, and the verdict kinds of
the verification cases verifying the requirement. A verdict is answered, never
bound — a binding carrying one is INVALID_ARGUMENT
(Which constraints and requirements hold).
Go: opensysml.ObjectByID(id) and opensysml.ObjectByPath("car.wheels[2]")
bind, and an answered opensysml.Object carries ID, Path and Element;
a row over objects sets Row.Object. Python: ObjectRef(id=2),
ObjectRef(path="car.wheels[2]") bind, and an answered ObjectRef carries
id, path and the usage as element; a row over objects sets row.object.
Failures keep the engine's message, append the declaring document where the
failure carries provenance, and map onto status codes by whose fault they are:
an unknown model or an unknown query/document is NOT_FOUND, as is an object
binding naming an id or a usage no Instantiate created — while the model
holds no objects at all, the message says so and names the RPC that creates
one; a symbol of the wrong kind, a malformed request, or a wrong binding
(unknown, missing, mistyped, wrong multiplicity, naming an element the model
does not have, an object path that does not reach an object — a namespace, a
scalar feature, an index out of range — or an object bound with both an id and a
path that reaches a different object) is INVALID_ARGUMENT; an exhausted visit
or invocation budget is RESOURCE_EXHAUSTED, as is an Instantiate, query or
render that would take a model past the most objects it holds; an operation
the engine does not execute — a fault in the model's own definitions rather
than in the request — is FAILED_PRECONDITION;
and a request the service lacks the capability for is UNIMPLEMENTED, naming
it.
Known limitations: the response carries no provenance for successful rows (only failures name their source), and the document IR is not exposed in a structured form — Markdown is the only document form served, as it is the only form the build writes.
import (
"github.com/Open-MBEE/OpenSysML/internal/core/source"
"github.com/Open-MBEE/OpenSysML/internal/core/parser"
)
src := source.New("example.sysml", []byte(`
part Wheel {
attribute diameter : Real;
}
`))
p := parser.New(src)
root := p.ParseFile()
// root is always non-nil, check root.Errors for parse errorsimport (
"github.com/Open-MBEE/OpenSysML/internal/core/symbols"
)
idx := symbols.NewIndex()
idx.AddDocument("example.sysml", root)
scope := idx.DocumentRoot("example.sysml")
sym, ok := scope.LookupLocal("Wheel")import (
"github.com/Open-MBEE/OpenSysML/internal/core/resolve"
)
res := resolve.New(idx)
sym, ok := res.ResolveQualified(scope, qualifiedName)import (
"github.com/Open-MBEE/OpenSysML/internal/core/semantics"
)
model := semantics.NewModel(res)
members := model.MembersOf(wheelSym)
conforms := model.Conforms(wheelSym, vehiclePartSym)import (
"github.com/Open-MBEE/OpenSysML/internal/core/passes"
)
// Analyze wires up the default pass registry and context internally.
diagnostics := passes.Analyze("example.sysml", root, parseDiags, idx)import (
"github.com/Open-MBEE/OpenSysML/internal/core/runtime"
)
rtCtx := runtime.NewContext(runtime.NewModel(model, resolver), runtime.DefaultMaxSteps)
inst, _ := rtCtx.Instantiate(wheelSym)
diameter, _ := inst.GetFeatureValue(rtCtx, "diameter")
fmt.Println(diameter.Value) // Value{Kind: ValConst, Real: 16.0}AST nodes are syntax-only and immutable after parsing. All semantic information (types, resolved references, instance values) lives in side tables keyed by ast.Node or *symbols.Symbol.
- Name resolution: computed on-demand, cached
- Semantic queries: computed on-demand, cached
- Passes: run only when diagnostics requested
Documents can be updated individually. Symbol index and caches invalidate only affected documents.
Source → Lexer → Parser → AST
↓
Symbols (side table)
↓
Resolve (side table)
↓
Semantics (side table)
↓
Passes (diagnostics)
↓
Runtime (values, instances)
Each layer is independent and testable.
All packages have comprehensive test coverage:
go test ./internal/core/parser # Parser tests
go test ./internal/core/symbols # Symbol table tests
go test ./internal/core/semantics # Semantic tests
go test ./internal/core/runtime # Runtime tests
go test ./... # All testsTest fixtures in testdata/*.sysml.
- ARCHITECTURE.md — System architecture and design decisions
- the guide — Getting started guide
- Wire contract — Connect + JSON field by field, for a client with no generated library
- OMG SysML v2.1 Beta 1 Spec — Language specification (2026-08 release)