SysML v2 Specification Compliance¶
Purpose: Document implementation coverage of SysML v2 / KerML behavioral semantics. UML 2.5.1 is cited only as reference semantics for an OpenSysML extension the SysML v2 notation has no production for and the bundled KerML semantic library (internal/core/libs/stdlib/) no performance for.
Related: TESTING.md (test contracts), ARCHITECTURE.md (runtime architecture), grammar-coverage.md (which OMG grammar productions our test inputs exercise — input-presence evidence, which implies nothing about compliance either way)
Current Implementation Status¶
✅ Fully Implemented & Tested (~98% of Targeted Features)¶
Calculations (14/14 features):
- Invocation with typed parameters
- Return expression evaluation (both return <expr>; and a bound return parameter return : T = <expr>;)
- Parameter binding (positional + named arguments)
- Parameter defaults (own and inherited)
- Inherited parameters and result through a typed calc usage, including redeclaration
- Nested calc invocation, and invocation from a constraint
- Statement bodies: local declarations, assignment, if/else, while, loop … until, for, and early return
- Purity and termination: a side effect or an outside assignment is rejected, and every loop iteration spends a step of the budget
- Control flow (if/else), including the conditional expression if c ? a else b evaluated lazily at runtime
- Unary operators (not, -, +)
- Type coercion (Integer→Real)
- Qualified names (A::B::C)
- Deterministic evaluation trace (parameter binding, sub-expression order, results)
- Error handling (unbound/unknown parameters, arity, missing return, recursion and step budgets)
- Calc usages as multi-output consumers: a usage's out features resolve, typecheck and evaluate as features (attribute z = c.b;), from one run of the body per usage per object
- Composition of multi-output calcs: a usage nested among a calc def's members binds its inputs from the enclosing evaluation's parameters and locals, one run per usage per object per bound input tuple
Constraints (7/7 features):
- Assert evaluation (boolean satisfaction)
- Assume evaluation (trusted preconditions)
- Bare expression as invariant
- Negated constraints (assert not)
- Unresolved feature detection
- Conditions of a nested constraint (assert constraint [name] { <expr> })
- Parameters a typed usage binds (constraint limit : MassLimit { in m = mass; })
Requirements (8/8 features):
- Require expression evaluation, in a requirement definition body as well as a usage
- Conditions stated through an anonymous nested constraint (require constraint { <expr> })
- The requirement's own attributes, inherited or rebound, in its conditions
- Subject binding evaluation
- Actor binding evaluation
- Assume expression evaluation, in both spellings
- Nested requirements
- A violated condition names the condition that failed
Actions (18/18 features):
- Initial/final node token placement
- Fork node (1→N parallelism)
- Join node (N→1 synchronization)
- Merge node (N→1 non-blocking)
- Decision node (guarded branching)
- Action execution nodes
- Nested action invocation
- Assignment statements in an action node's body
- Conditional statement (if <cond> { … } else { … }), nestable in either direction with a loop
- Pre-condition loop (while <cond> { … }) and post-condition loop (loop { … } until <cond>;)
- Iteration over a collection (for <x> in <collection> { … }, over every collection the expression layer produces; a non-collection input is reported)
- Send statement (⚠️ typed messages addressed to an object's port or receiving node, or routed through a connected port)
- Accept action (⚠️ takes the oldest message of its type, parking its token until one arrives; suspension is bounded by the executor — see the Action map)
- Object flow (pin-to-pin data)
- Succession edges
- Deadlock detection
- Token-flow tracing (infrastructure ready)
- Step budget enforcement
State Machines (core: faithful; advanced: partial):
- Initial/final state identification
- State entry/exit actions
- State do behavior (runs while the state is active; concurrently active states interleave)
- Transition firing
- Transition guard evaluation
- Transition effect actions
- AcceptEvent triggers (when signal)
- Sourceless transitions (accept...then, nested form)
- ChangeEvent triggers (when expression)
- TimeEvent triggers (after duration, at instant)
- Signal discrimination (name matching)
- Unmatched signal dropped
- Signals sent from entry/do/exit/effect actions reaching the machine
- CallEvent triggers (accept op(arg) notation, operation and argument matching)
- Completion transitions (nil trigger with guard evaluation)
- Hierarchical substates
- Orthogonal regions (concurrent states)
- Choice pseudostates (dynamic branching)
- Junction pseudostates (static branching)
- Fork pseudostates (one branch per orthogonal region)
- Join pseudostates (waits for every branch)
- Entry/exit point pseudostates (entry point <name>; / exit point <name>;)
- Choice/junction/entry/exit reached from inside an orthogonal region
- Nested action invocation in entry/do/exit/effect behaviors
- Run-to-completion semantics
- Event queue management
- Dangling transition detection (a transition names one source and one target vertex of its own machine; a routing pseudostate with no transition out of it reports)
- State visits tracking
- Multi-region event broadcasting
- History pseudostates: shallow and deep restoration (history / shallow history / deep history <name>;)
- Deferred events: retention and recall across hierarchy and orthogonal regions (defer <event>[, <event>]*;)
Expression Evaluation:
- Binary operators (+, -, *, /, <, >, ==, and, or)
- Exponentiation (**, ^) over Integer and Real operands, folded and evaluated by one implementation
- Unary operators (-, not)
- Literal values (Integer, Real, Boolean, String)
- Feature reference resolution
- Qualified name resolution (A::B::C)
- Type coercion (Integer→Real)
- Unresolved reference error handling
- KerML function library: the numeric functions of RealFunctions, RationalFunctions, NumericalFunctions, IntegerFunctions, NaturalFunctions, TrigFunctions, VectorFunctions and ComplexFunctions, all of StringFunctions, plus the library feature values TrigFunctions::pi and ComplexFunctions::i (see the Function Library row below)
Name Resolution: - Inherited feature resolution (follows specialization chains) - Named argument parameter binding - Redefinition target resolution (:>> featureName) - Control flow node scope registration
Test Coverage:
- 338 conformance cases (all passing: calc×79, state×71, action×62, requirement×12, instance×10, binding×9, send×8, constraint×7, multiplicity×7, unit×7, nested×6, object×6, satisfy×6, string×6, redefinition×5, variation×5, accept×4, port×4, and three each of attribute, ballandchain, enum, feature, filter, variant, two of viewpoint, and one each of connector, cubesat, two and view — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage)
- 194 runtime robustness cases (deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a select predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a terminate inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, a statement written directly among an action's members, accept suspension that can never end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic perform references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, exponentiation beyond the Integer range, a flow end that names no action node, a flow from a node that produced no value, a time-triggered accept with no clock, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a variant declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back)
- 524 runtime test functions (grep -c '^func Test' internal/core/runtime/*_test.go), the conformance, trace and robustness gates above among them
- 90 golden AST fixtures (including the implicitly typed connector forms and the standard behavioral notation — a named flow with from, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with until, then done, a decision else branch, a bodied exhibit state, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies and n-ary connector-end parsing tests)
- 106 golden execution traces (action×37, calc×30, state×28, constraint×4, string×4, accept×1, object×1, two×1 — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop until with then done, a decision's guarded and else branches, a named flow carrying a value between action nodes, an accept with a when trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation)
- 135 negative parser subtests
- 14 gRPC conformance cases and 8 gRPC robustness cases (internal/grpc/testdata/conformance/, internal/grpc/robustness_test.go)
- 5,477 tests and subtests, of which 5,468 pass and 9 skip (go test -race -count=1 -v ./...; 2,922 top-level Test functions). The skips are TestSubsettingTargetIsTheInheritedFeature, TestEval_CollectExpr, TestEval_SelectExpr, TestEval_BuiltinInvocation, TestRequirementEvaluation_SubjectNotFound, TestRequirementEvaluation_Complete, TestMultiplicityOfALibraryFeatureIsTheSameParsedAndRestored, TestPortabilityGateIsRequired, and TestHelperSolverProcess; each skips itself.
Detailed Semantic Compliance Map¶
How to Read This Map¶
Each row documents one behavioral semantic feature:
- Semantic Rule: the governing reference — the SysML v2 metamodel or the bundled KerML semantic library, and UML 2.5.1 only where neither has the concept
- Implementation: File:function implementing the semantics
- Test Case: Conformance/robustness test(s) exercising the feature
- Status:
- ✅ Faithful: Implements spec semantics with test coverage
- ⚠️ Approximate: Partial implementation or known deviations
- ❌ Not Yet Implemented: Parsed but not executable
- 🚧 Known Failure: Test exists but fails
Calculation (Calc)¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
| Calc invocation with typed parameters | invoke_calc.go InvokeCalc/invokeCalcShape |
calc_simple_add.sysml |
✅ Faithful |
Return expression evaluation (return <expr>;) |
invoke_calc.go calcResult/resultExpression + eval.go Eval |
calc_simple_add.sysml |
✅ Faithful |
Result as a bound return parameter (return : T = <expr>;) |
invoke_calc.go resultExpression |
calc_return_parameter.sysml |
✅ Faithful |
| Parameter binding (positional) | invoke_calc.go bindCalcParameter |
calc_simple_add.sysml |
✅ Faithful |
| Parameter binding (named arguments) | eval.go evalInvocation + invoke_calc.go InvokeCalcNamed |
calc_named_arguments.sysml |
✅ Faithful |
| Parameter default when no argument is passed | invoke_calc.go bindCalcParameter |
calc_parameter_defaults.sysml |
✅ Faithful |
| Parameters and result inherited through a typed calc usage | invoke_calc.go calcShapeOf/calcChain/calcParameters |
calc_inherited_parameters.sysml, calc_return_parameter.sysml |
✅ Faithful |
| Redeclared parameter keeps its inherited position and default, which stays the expression the supertype wrote and is evaluated where that calc wrote it | invoke_calc.go calcParameters (the owner carried down with the inherited default) |
calc_return_parameter.sysml, calc_usage_nested_shadowed_input.sysml, calc_usage_nested_test.go:TestNestedCalcUsageInheritedDefaultOfRedeclaredInput |
✅ Faithful |
| Nested calc invocation | eval.go evalInvocation → invoke_calc.go invokeCalc |
calc_nested_invocation.sysml |
✅ Faithful |
| Calc invoked from a constraint | context.go EvaluateConstraint → eval.go evalInvocation |
calc_from_constraint.sysml |
✅ Faithful |
| Deterministic evaluation trace (binding, sub-expression order, result) | trace.go RecordCalcEnter/RecordCalcBind/EndEval, eval.go Eval |
*.trace.golden via TestExecutionTrace, trace_calc_test.go:TestCalcTraceIsStableAcrossRuns |
✅ Faithful |
| Canonical rendering of unordered values in traces | trace.go FormatTraceValue |
trace_calc_test.go:TestFormatTraceValueCanonicalizesSets |
✅ Faithful |
| Unbound parameter detection | invoke_calc.go bindCalcParameter (ErrUnboundParameter) |
robustness_test.go:testCalcUnboundParameter |
✅ Faithful |
| Surplus positional arguments | invoke_calc.go checkArgs (ErrCalcArity) |
robustness_test.go:testCalcTooManyArguments |
✅ Faithful |
| Named argument that names no parameter | invoke_calc.go checkArgs (ErrUnknownParameter) |
robustness_test.go:testCalcUnknownNamedArgument |
✅ Faithful |
| Invoked symbol is not a calc | invoke_calc.go calcShapeOf (ErrNotACalc) |
robustness_test.go:testCalcSymbolIsNotACalc |
✅ Faithful |
| Recursive calc (direct or mutual) evaluates to its result, bounded by the run's calc depth budget | invoke_calc.go Context.enterCalc (ErrCalcRecursionLimit), budget from budget.go BudgetsFromEnv (SYSML_MAX_CALC_DEPTH, default 10000, ceiling 25000) |
calc_recursion_factorial.sysml, calc_recursion_fibonacci.sysml, calc_recursion_mutual.sysml, calc_recursion_descends_sequence.sysml, calc_recursion_beyond_nesting_bound.sysml, invoke_calc_recursion_test.go:TestRecursiveCalcSpendsTheDepthBudget, :TestRecursiveCalcDepthIsNotAFixedBound, :TestMutuallyRecursiveCalcsEvaluate |
✅ Faithful |
| A recursion that does not terminate spends a budget and is reported, never hanging or exhausting the stack | invoke_calc.go Context.enterCalc (ErrCalcRecursionLimit) and context.go step counter (ErrStepLimitExceeded); the ceiling on the depth budget keeps the report ahead of the goroutine stack limit |
robustness_test.go:testCalcDirectRecursion, :testCalcMutualRecursion, :testCalcRecursionSpendsStepBudget, :testCalcRecursionAtDepthCeiling, budget_test.go:TestBudgetFromValue (above the ceiling) |
✅ Faithful |
| Step budget bounds calc evaluation | context.go step counter (ErrStepLimitExceeded), budget from budget.go BudgetsFromEnv (SYSML_MAX_STEPS, default 10000000) |
robustness_test.go:testStepBudgetExceeded, budget_test.go:TestBudgetFromValue |
✅ Faithful |
Statement body (SysML v2 7.19, CalculationBodyItem carries the items of an action body): local declarations, assignment, if/else, while, loop … until, for, return |
parser/behavior.go parseCalcBody/atCalcStatement → lower/calc_body.go CalcBody → runtime/statements.go stmtEngine driven by invoke_calc.go runCalcBody |
calc_statement_body.sysml (golden AST), calc_iterative_factorial.sysml, calc_conditional_branch.sysml, calc_for_over_sequence.sysml, calc_loop_until_body.sysml |
✅ Faithful |
Early return out of a branch or a loop unwinds the blocks entered |
lower/action_graph.go Return + runtime/statements.go flowReturn |
calc_early_return_from_loop.sysml |
✅ Faithful |
| A body-local declaration of a branch or loop body is that block's own and does not leak | runtime/statements.go stmtEnv |
passes/typecheck_calc_body_test.go:TestCalcBodyLoopLocalIsNotVisibleOutside |
✅ Faithful |
| An inherited body evaluates in the scope of the calculation declaring it | invoke_calc.go calcBody (specialization chain, as calcResult/calcParameters) + statement Scope carried by the lowered IR |
invoke_calc_body_test.go:TestInheritedCalcBodyRunsInDeclaringScope |
✅ Faithful |
A calculation is pure: send, perform, accept, terminate and an assignment to a feature it does not declare are rejected |
runtime/calc_statements.go (ErrCalcSideEffect, ErrCalcExternalAssignment) |
robustness_test.go:testCalcSendIsRejected, :testCalcTerminateIsRejected, :testCalcAssignmentOutsideTheCalc |
✅ Faithful |
| A loop in a calculation terminates or fails: every iteration spends a step of the budget | runtime/statements.go loop/forLoop → context.go incrementStep; InvokeCalc/InvokeCalcNamed beginRun |
robustness_test.go:testCalcNonTerminatingLoop, budget_test.go:TestStepBudgetIsPerRunForInstancesAndCalcs |
✅ Faithful |
| A body running to its end without returning is an error, not a null result | invoke_calc.go runCalcBody (ErrCalcNoReturn) |
robustness_test.go:testCalcBodyNeverReturns |
✅ Faithful |
Control flow (if/else) in calc, including the conditional expression if c ? a else b evaluated lazily |
runtime/statements.go ifStatement; eval.go evalConditional |
calc_conditional_branch.sysml, calc_conditional_operator_base_case.sysml |
✅ Faithful |
The type tier reaches the expressions of a body whose result is the value of its last expression, as it reaches an explicit return — including the dimensional warning |
passes/typecheck.go checkBehaviorMember (an expression body member inferred as the value it computes) |
typecheck_calc_implicit_result_test.go:TestCalcBodyImplicitResultIsChecked, :TestCalcBodyImplicitResultWarnsOnDimensions, :TestCalcUsageImplicitResultIsChecked, :TestCalcBodyImplicitResultSkippedAfterNameError, calc_recursion_implicit_result.sysml |
✅ Faithful |
A non-Boolean if/loop condition in a calc is a type error, and a diagnostic at runtime |
passes/typecheck.go checkBehaviorMember; runtime/statements.go condition |
typecheck_calc_body_test.go:TestCalcBodyNonBooleanWhileCondition, robustness_test.go:testCalcNonBooleanCondition |
✅ Faithful |
| Statements and loop iterations in the evaluation trace | trace.go RecordStatement/RecordLoopIteration |
calc_iterative_factorial.trace.golden, calc_early_return_from_loop.trace.golden |
✅ Faithful |
| Missing return expression | invoke_calc.go calcShapeOf (ErrNoResultExpression: no result expression, no returning body, no output features and no output its body assigns) |
robustness_test.go:testCalcWithoutResult |
✅ Faithful |
An assignment in a body to an out the calculation declares binds that output for the activation, so a calc usage reads it (KerML 7.4.9: an invocation's outputs are features of that one evaluation). The parser gives a = expr and a := expr in statement position one AssignmentActionNode, so both spellings write the output alike |
runtime/statements.go stmtEngine.assign/stmtEnv.assignLocal → runtime/calc_statements.go calcStmtHost.declaredOutput/assignOuter; runtime/calc_usage.go calcRun.output, assignedOutputs; invoke_calc.go calcShape.BodyOutputs |
parser/testdata/parse/calc_output_assignment.golden, calc_output_assigned_in_body.sysml conformance (both spellings, a loop and a branch computing outputs) |
✅ Faithful |
| A body that both assigns an output and returns a value keeps the two apart: the return is the invocation's value, the assignment the output's, so reading the output answers what the body assigned | runtime/calc_usage.go runCalcUsage (memoizing a returned value under an output's name only for calcOutput.IsResult, the rule the row on a valued output and a return states) |
calc_output_assigned_in_body.sysml conformance (AssignAndReturn: cr.a is 6, AssignAndReturn(5) is 500) |
✅ Faithful |
| A calculation stating its own body — a result expression, or a body assigning an output it declares — is evaluated from it, so a library function of the same name never answers in its place | runtime/invoke_calc.go hasCalcBody consulted by runtime/library_functions.go libraryFunctionFor |
library_functions_test.go:TestLibraryFunctionDoesNotHijackADeclaredBody, :TestLibraryFunctionDoesNotHijackAnOutputAssignedInABody |
✅ Faithful |
An assignment target naming more than one segment (assign other::c := 1) reaches outside the body no host binds, so it is lowered as unsupported and reported rather than writing the last segment |
lower/action_graph.go lowerStatement (Unsupported) |
robustness_test.go:testQualifiedAssignmentTargetInAStateEffect |
✅ Faithful (an assignment through a feature chain, a.b := 1, is reported the same way; neither form is bound) |
An inout is bound by the invocation and rebound by an assignment in the body, the read answering what the body left |
runtime/calc_usage.go calcOutput.IsInOut/calcRun.output; runtime/calc_statements.go calcStmtHost.declaredOutput |
calc_output_assigned_in_body.sysml conformance (Bump) |
✅ Faithful |
| An output given a value by its declaration and assigned in the body is a typed error rather than a silent pick (the precedent of a feature valued two ways). Assignments to an output within the body are imperative like a body local's: the last one to run is the activation's binding, so an output may be initialized and then accumulated into, including once per loop iteration | runtime/calc_statements.go calcStmtHost.assignOuter (ErrConflictingOutput) |
robustness_test.go:testCalcOutputValuedAndAssigned, :testCalcOutputAssignedTwice, calc_output_assigned_in_body.sysml conformance (Accum) |
✅ Faithful |
| Reading a declared output the activation never assigned reports that output, not a missing result expression; an output only a branch that did not run would assign is unassigned for that activation | runtime/calc_usage.go calcRun.output (ErrOutputNotAssigned, a kind of ErrNoValue) |
robustness_test.go:testCalcOutputNeverAssignedByTheBody, :testCalcOutputAssignedInABranchNotTaken |
✅ Faithful |
| A calc usage's members are the parameters and outputs of the calc it is typed by, reachable through a feature chain (SysML 7.6.6, 7.17) | resolve/target.go ResolveTarget/memberChain + resolve/document.go resolveMemberChain → semantics Model.LookupMember |
passes/typecheck_calc_usage_test.go:TestCalcUsageOutputTypesAsTheOutputItNames, :TestCalcUsageOutputInsideAPartDefinition |
✅ Faithful |
| An output read through a chain types as that output declares, or as its default computes when it declares no type | passes/typecheck_expr.go inferFeatureChain/featurePrimType |
passes/typecheck_calc_usage_test.go:TestCalcUsageOutputTypedByItsDefaultIsChecked, :TestCalcUsageDeclaredOutputTypeIsChecked |
✅ Faithful |
| Reading a name the calc declares no output for is unresolved, reported once by the name-resolution tier | resolve/document.go resolveMemberChain; runtime/calc_usage.go calcRun.output (ErrUnknownOutput) |
passes/typecheck_calc_usage_test.go:TestCalcUsageUnknownOutputIsUnresolved, robustness_test.go:testCalcUsageUnknownOutput |
✅ Faithful |
A calc usage evaluates its body once and every out feature it declares is readable from that run (SysML 7.17) |
runtime/calc_usage.go CalcUsageOutput/CalcUsageOutputs/calcUsageRun → invoke_calc.go calcShapeOf/bindCalcParameters → runtime/statements.go stmtEngine |
calc_usage_multiple_outputs.sysml, calc_usage_statement_body.sysml, calc_usage_inherited_parameters.sysml, calc_usage_instance_slots.sysml |
✅ Faithful |
| Reading several outputs of one usage runs the body once, per usage and per object, reset with the run | runtime/calc_usage.go calcUsageRun (calcUsageKey) + context.go beginRun/beginExecutorRun |
calc_usage_multiple_outputs.trace.golden, calc_usage_statement_body.trace.golden, calc_usage_inherited_parameters.trace.golden |
✅ Faithful |
| A usage's inputs bind from its own member values, falling back to the defaults declared along its specialization chain, and may name a sibling feature of the object carrying the usage | runtime/calc_usage.go calcUsageRun → invoke_calc.go bindCalcParameters/calcParameters |
calc_usage_inherited_parameters.sysml, calc_usage_instance_slots.sysml |
✅ Faithful |
| A usage declared in a behavior's body — a calc's or an action's — binds its inputs in the environment of the evaluation reading it: the enclosing parameters and locals as the running body holds them, then the enclosing lexical scope, so an input naming an attribute the body assigned reads the assigned value rather than the declared one (SysML 7.17, a usage is a feature of the body declaring it) | runtime/calc_usage.go bindCalcUsage/enclosedByBehaviorBody, invoke_calc.go isActionSymbol, eval.go EvalContext.nestedEnv, invoke_calc.go bindCalcParameters |
calc_usage_nested_in_calc.sysml, action_body_local_calc_usage.sysml, calc_usage_nested_test.go:TestNestedCalcUsageBindsFromEnclosingParameters, :TestNestedCalcUsageBindsFromEnclosingLocals, :TestNestedCalcUsageChain, :TestNestedCalcUsageReadsEnclosingObject, action_body_local_usage_test.go:TestActionBodyLocalUsageBindsCurrentValues, :TestActionBodyLocalUsageBindsPerIteration |
✅ Faithful |
A nested input bound from a name of its own (in vx = vx) reads the enclosing binding: the inputs being bound are not in the environment their own values are evaluated in, so every one of them resolves names in the enclosing environment alike — in n = m; in m = n; swaps the two values rather than the second reading the first's fresh binding |
runtime/calc_usage.go bindCalcUsage, eval.go EvalContext.nestedEnv/Lookup |
calc_usage_nested_shadowed_input.sysml (shadowed and swapped names), calc_usage_nested_test.go:TestNestedCalcUsageShadowsEnclosingName, :TestNestedCalcUsageInputsDoNotSeeSiblings, robustness_test.go:testNestedCalcUsageSelfCycle (a default with nothing outside to name stays ErrCyclicFeatureValue) |
✅ Faithful |
| One run per usage, object and activation: two reads from one enclosing invocation run the body once, two invocations do not share it, and an iteration of a loop is an activation of its own. The activation replaces the input-value hash the memo key used, so two invocations whose inputs coincide still get their own run and no read can be answered from a run bound to other values | runtime/calc_usage.go calcUsageKey/calcUsageRun, context.go newActivation/endActivation |
calc_usage_nested_in_calc.trace.golden, calc_usage_nested_shadowed_input.trace.golden, calc_usage_nested_test.go:TestNestedCalcUsageRunsPerInputs, :TestNestedCalcUsageOwnOutputPerInvocation, calc_usage_snapshot_test.go:TestCalcUsageMemoDistinguishesEnclosingArguments |
✅ Faithful |
| Every output read from one evaluation of a usage sees one binding of its inputs: the inputs are bound when the evaluation starts and a later assignment to a feature they named does not rebind them for a later output read, so two outputs of one usage can never come from different input bindings (KerML 7.4.9, SysML 7.17 — a usage's outputs are features of one evaluation) | runtime/calc_usage.go calcUsageRun/calcUsageKey/forgetCalcUsage, context.go newActivation/endActivation |
calc_usage_outputs_one_binding.sysml + .trace.golden, calc_rk4_lunar_descent.sysml, calc_usage_snapshot_test.go:TestCalcUsageOutputsShareOneInputBinding, :TestCalcUsageOutputsInAssignmentLoop, :TestCalcUsageMemoDistinguishesEnclosingArguments, :TestCyclicCalcUsageOutputStillDiagnosed |
✅ Faithful |
| A calc usage declared in a loop or conditional body is executable, in the scope it is written in and with the lifetime of the block: an iteration binds it from that iteration's state and reading it again in the same iteration reuses that evaluation. This holds in an action's body as well as a calc's, both being run by the statement engine | lower/calc_body.go usageStatement → lower.DeclareUsage, runtime/statements.go declareUsage, runtime/calc_usage.go bodyUsageSymbol/enclosedByBehaviorBody |
calc_body_local_usage_and_range.sysml (golden AST), calc_rk4_lunar_descent.sysml, action_body_local_calc_usage.sysml, calc_usage_body_local_test.go:TestBodyLocalCalcUsageInLoopBindsPerIteration, :TestBodyLocalCalcUsageInBranch, :TestBodyLocalCalcUsageInNestedBodies, action_body_local_usage_test.go:TestActionBodyLocalUsageBindsCurrentValues, :TestActionBodyLocalUsageBindsPerIteration, robustness_test.go:testBodyLocalUsageOfANonCalc, :testBodyLocalDeclarationNotExecutable |
✅ Faithful |
| A feature chain through a part reads what the part's features carry, the part being materialized as the occurrence it denotes; an output of a calc usage the part declares evaluates in that part's context. The object being evaluated answers first: a feature value it carries for that part is the object read, and only a part no object in hand carries is materialized. A usage of several occurrences is a collection, not one object, so a chain through it is reported | runtime/eval.go evalFeatureChain/chainBase, runtime/calc_usage.go occurrenceOperand, runtime/instance.go occurrenceOf/occursOnce |
part_feature_chain_test.go:TestCalcUsageReadThroughPartChain, :TestPartChainReadsTheObjectInHand, :TestPartChainRejectsSeveralOccurrences, :TestCalcUsageChainDiagnostics, robustness_test.go:testUsageReadThroughAPartWithoutAnOutput |
✅ Faithful |
A chain through a multi-valued feature has, for its last feature, that feature's values over every object the features before it name (KerML 1.0 §7.3.4.6): subsystem.volume is the volumes of every object subsystem holds, in the collection's order, flattened one level per step as ->collect flattens its mapping. An empty collection yields no values; a chain reaching an object with no value for the feature reports the unset feature value |
runtime/eval.go chainMemberValue/chainOverElements |
conformance feature_chain_rollup_over_subsets, feature_chain_nested_multivalued, feature_chain_empty_collection, cubesat_mass_rollup; robustness_test.go:feature_chain_through_an_unset_slot, :feature_chain_spends_the_element_budget, chain_trace_test.go:TestChainOverCollectionTraceOrder |
✅ Faithful |
| A chain that stops at a calc usage rather than at one of its outputs names the outputs to read instead of reporting no value | runtime/eval.go evalCalcUsageMembers (ErrNoValue) |
part_feature_chain_test.go:TestCalcUsageChainDiagnostics, robustness_test.go:testUsageReadThroughAPartWithoutAnOutput |
✅ Faithful |
| A nested usage chain is bounded: the depth counted while an output binding is evaluated, the budget spent by the bodies it runs. One evaluation counts one level however its answer is reached, so an invocation whose result is a designated output and outputs of one calc naming each other spend nothing extra | invoke_calc.go enterCalc (ErrCalcRecursionLimit)/runCalcBody, runtime/calc_usage.go calcRun.enter/calcRun.value |
robustness_test.go:testNestedCalcUsageRecursionDepth, :testNestedCalcUsageStepBudget, :testNestedCalcUsageUnboundInput, :testNestedCalcUsageUnknownOutput, calc_usage_nested_test.go:TestNestedCalcUsageDepthCountedOnce, :TestCalcOutputChainIsNotNesting |
✅ Faithful |
An out default evaluates in the calc's own scope and may name inputs, body locals and other outputs |
runtime/calc_usage.go calcRun.output/lookupOutput + eval.go EvalContext.calcRun |
calc_usage_statement_body.sysml, calc_usage_inherited_parameters.sysml |
✅ Faithful |
| An output feature fed into a feature's default value (the parametric-budget pattern) | eval.go evalFeatureChain/evalCalcUsageMembers |
calc_usage_multiple_outputs.sysml, calc_usage_instance_slots.sysml |
✅ Faithful |
| Outputs valued from each other are a cyclic dependency, not a hang or a spent step budget | runtime/calc_usage.go calcRun.output (ErrCyclicOutput) |
robustness_test.go:testCalcUsageCyclicOutputs |
✅ Faithful |
| A usage leaving an input unbound, or reading an output with no value, is reported | runtime/calc_usage.go calcUsageRun (ErrUnboundParameter), calcRun.output (ErrNoValue) |
robustness_test.go:testCalcUsageUnboundInput, :testCalcUsageOutputWithoutAValue |
✅ Faithful |
| A calc usage typed by something that is not a calc is reported | invoke_calc.go calcShapeOf (ErrNotACalc) |
robustness_test.go:testCalcUsageSpecializesANonCalc |
✅ Faithful |
| The step budget bounds a usage's body the way it bounds an invocation | runtime/calc_usage.go CalcUsageOutput beginRun → context.go incrementStep (ErrStepLimitExceeded) |
robustness_test.go:testCalcUsageStepBudget |
✅ Faithful |
| An invocation yields exactly one result (KerML 7.4.9), so invoking a calc that computes several outputs and designates no result is rejected rather than answered with the first of them — and the diagnostic writes out the calc usage to declare instead, with the inputs to bind | runtime/calc_usage.go calcShape.designatedOutput/usageSpelling (ErrAmbiguousResult) |
robustness_test.go:testMultipleOutputsInvokedAsAnExpression, repl/runtime_commands_test.go:TestCalcWithSeveralOutputsIsNotInvocable |
✅ Faithful |
A calc with exactly one output and no return is invocable, that output being its result |
runtime/calc_usage.go calcShape.designatedOutput |
calc_usage_single_output.sysml |
✅ Faithful |
A return in a calc that also states an output supplies the invocation's value only: an output keeps the value its own binding computes, so a body returning something else does not change what reading that output answers. Only the result parameter (return : Real = …) takes the returned value directly |
runtime/calc_usage.go runCalcUsage (memoizing a returned value under an output's name only for calcOutput.IsResult) |
calc_valued_output_with_return.sysml conformance (Apart: ca.a is 6 while Apart(5) is 500; Together, Both) |
✅ Faithful |
%calc on a calc usage lists the outputs of one evaluation; a chain into a usage evaluates at the prompt |
repl/meta.go doCalc/calcUsageOutputs |
repl/runtime_commands_test.go:TestCalcUsageOutputsAtThePrompt |
✅ Faithful |
| A calc usage's outputs are evaluation results, not feature values of an object | runtime/calc_usage.go (no instance materialization) |
calc_usage_instance_slots.sysml (the features fed by the outputs are feature values; the usage itself is not) |
⚠️ Approximate (%instances and export show the features valued from outputs, not the usage's outputs themselves) |
Boolean operators evaluated at runtime (and, or, xor, implies, short-circuiting where they can) |
eval.go evalLogical |
calc_boolean_operators.sysml |
✅ Faithful |
Identity (===, !==), null coalescing (??, lazy) and remainder (%) evaluated at runtime |
eval.go evalIdentity/evalNullCoalesce/evalArithmetic |
calc_identity_operators.sysml, calc_null_coalesce.sysml, calc_modulo_operator.sysml |
✅ Faithful |
An operator with no runtime evaluation (hastype/istype classification, cast, all, bitwise complement) reports why |
eval.go unimplementedOperators (ErrUnsupportedOperator) |
eval_operator_test.go:TestUnimplementedOperatorReportsWhy |
❌ Not implemented (rejected with a typed diagnostic naming what it would need; type classification needs a runtime type on values. Metadata classification @/@@ is evaluated — see the row below) |
Metadata classification evaluated at runtime (p @ Safety, p @@ SysML::PartUsage, in a constraint, a calc body or an %eval; SysML v2 7.9.4 metadata, 8.4.4.2 ClassificationExpression) |
eval.go EvalContext.evalClassification over semantics/filter.go Model.EvalClassification (the same compiled predicate and Model.AllSupertypes conformance an element filter is decided by, so the two paths cannot disagree); eval.go classifiedElement settles the element the subject denotes — the object being evaluated for an implicit subject or self, the element a name names, or an object's classifier, a selected variant or an enumeration literal |
runtime/eval_classification_test.go (TestEvalClassificationOverNamedSubjects, TestEvalClassificationMetaVersusAnnotation, TestEvalClassificationInACalcBody, TestEvalClassificationOfTheObjectBeingEvaluated, TestEvalClassificationAgreesWithAnElementFilter — the verdicts of the two paths pinned equal); conformance/filter_classification_annotation_forms.sysml, filter_classification_meta_versus_annotation.sysml, filter_classification_implicit_subject.sysml, view_exposed_element_classification.sysml; robustness_test.go classification_outside_the_evaluable_subset; semantics/classification_test.go (the shared predicate itself) |
✅ Faithful (@T holds for metadata annotating the subject in every form the parser accepts, inherited conformance included, and for the subject's own metaclass; @@T holds for the metaclass alone. A subject that denotes no element, or a metadata type that does not resolve, is semantics.ErrFilterUnevaluable — reported, never answered false) |
lower..upper is the ordered sequence of integers the library declares it to be (IntegerFunctions::'..' returns Integer[0..*] ordered, and SequenceFunctions::subsequence maps over it), so every sequence operation, index and for applies to it unchanged. A descending range is empty, a bound that is not an Integer is a type error, and each element generated costs a step and an element of the budgets, so a range wider than either — including one whose width overflows — is ErrStepLimitExceeded or ErrElementLimitExceeded rather than an allocation |
runtime/range.go evalRange/rangeSequence/rangeBound/builtinIntegerRange (ErrTypeMismatch), registered in builtins.go |
calc_integer_range.sysml, range_test.go:TestIntegerRange, :TestIntegerRangeSequenceOperations, :TestIntegerRangeNonIntegerBound, :TestIntegerRangeSpendsTheStepBudget, :TestIntegerRangeExtremeBounds, :TestForOverIntegerRange, robustness_test.go:testRangeBoundIsNotAnInteger, :testRangeSpendsTheStepBudget |
✅ Faithful |
| Unary operators (not, -, +) | eval.go evalUnary |
calc_unary_operators.sysml |
✅ Faithful |
| Type coercion (Integer→Real) | eval.go:344 toReal |
calc_type_coercion.sysml |
✅ Faithful |
| Qualified names (A::B::C) | eval.go + resolve/ |
calc_qualified_names.sysml |
✅ Faithful |
A calc usage with several out features, a usage nested in a part or in a calc
def's body, and a chain into one (c.a) are all existing productions — a directed feature member of a
usage body and FeatureChainExpr — so multi-output calc usages added no grammar
and no golden AST fixture of their own. calc_defaults_and_invocation.sysml
(a typed calc usage binding an inherited parameter in its body) and
calc_statement_body.sysml lock the parse structure they reuse.
Constraint¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
| Assert evaluation (boolean satisfaction) | context.go:81 EvaluateConstraint |
constraint_literal.sysml |
✅ Faithful |
| Assume evaluation (trusted precondition) | context.go:81 (same path) |
constraint_assume.sysml |
✅ Faithful |
| Bare expression as invariant | context.go:81 |
constraint_literal.sysml |
✅ Faithful |
| Unresolved feature reference | resolve package + eval.go |
robustness_test.go:testConstraintMissingFeature |
✅ Faithful |
| Negated constraint (assert not) | eval.go:483 evalNeg |
constraint_negation.sysml |
✅ Faithful |
| A constraint a type carries is evaluated against an instance of it, so it reads that object's feature values rather than declared defaults | context.go EvaluateConstraintOn, eval.go NewEvalContextIn/selfFeatureValue |
instance_constraint_binding.sysml, repl/instance_test.go:TestConstraintBindsToInstance |
✅ Faithful |
A condition on a type reached only through a nested redefinition (part top : Top { part :>> mid { part :>> leaf { attribute :>> value = 99.0; } } }) is checked against that nested object, at any depth and across part boundaries, so it agrees with %features instead of answering from the declaration; a sibling the object does not redefine keeps its declared value |
condition.go conditionSubject, carriersUnder, nestedObjects |
conformance nested_redefinition_two_levels, nested_redefinition_inherited_leaf, runtime/subject_value_test.go:TestNestedRedefinitionIsTheConditionSubject, TestNestedSubjectUnderSuppliedObject |
✅ Faithful |
| With no object instantiated, such a condition still answers about the declaration | condition.go conditionSubject |
conformance nested_redefinition_no_object, runtime/subject_value_test.go:TestNestedRedefinitionIsTheConditionSubject |
✅ Faithful |
Two objects redefining the same nested feature differently make the subject a question, reported as ErrAmbiguousSubject naming the carriers rather than answered from either; the same declaration materialized twice is one object, the latest |
condition.go ErrAmbiguousSubject, conditionSubject, rootInstances |
runtime/subject_value_test.go:TestNestedSubjectAmbiguous, robustness_test.go:testNestedConditionSubjectIsAmbiguous, testDuplicateObjectsOfOneDeclaration |
✅ Faithful |
An object a caller instantiated directly is a subject in its own right — a definition nested in another (part def Outer { part def Big :> Inner { ... } }) and a nested usage (%instantiate Top::leaf) alike — while an object a feature value holds, or one materialized only to read a nested declaration through, is reached through its holder |
condition.go rootInstances over heldObjectIDs and readThrough |
runtime/subject_value_test.go:TestSubjectOfANestedDefinition, TestSubjectOfADirectlyInstantiatedNestedUsage |
✅ Faithful |
| A check reports the object it was evaluated against, so a verdict is labelled with that object rather than with the one supplied when the two differ | context.go CheckResult, CheckConstraintOn, CheckRequirementOn, satisfy.go CheckSatisfactionOn, grpc/verify.go subjectOf |
runtime/subject_report_test.go:TestCheckReportsTheChosenSubject, TestCheckWithNoObjectReportsNoSubject, grpc/verify_subject_test.go:TestVerifyConstraintNamesTheNestedSubject |
✅ Faithful |
A satisfaction assertion (assert satisfy Inner::lim by big;) chooses the object its requirement's conditions read by the same rule the requirement does, so by naming an object carrying the requirement nested answers about that nested object, and an ambiguous one is reported rather than picked. An assertion stating its own conditions (satisfy requirement fits { require … }) resolves against the usage itself, so it too answers about the object carrying it |
satisfy.go CheckSatisfactionOn, condition.go checkSubject |
runtime/subject_report_test.go:TestSatisfactionRoutesThroughTheSubjectRule, TestSatisfactionOfItsOwnConditionsRoutesThroughTheSubjectRule, satisfy_nested_subject.sysml, robustness_test.go:testSatisfactionSubjectIsAmbiguous |
✅ Faithful |
A verdict about a nested object is labelled with that object as a reader can type it back — the object the search started from plus the features walked to it (A::o::b), ending in the declaration it materializes as an ambiguity's labels do — rather than with the object the command named |
context.go CheckResult (SubjectRoot, SubjectPath), condition.go carriersUnder, carrierFeatures, repl/query.go reportedSubject |
repl/subject_label_test.go:TestVerdictNamesTheNestedSubject, runtime/subject_report_test.go:TestReportedFeaturesEndInTheDeclaration |
✅ Faithful |
An ambiguity names each carrier by the features walked to it, ending in the declaration it materializes (Bolt #3 (front::bolt) vs Bolt #5 (rear::bolt); Component #2 (small) vs #3 (large) where one collection gathers both), so carriers are told apart however they were reached |
condition.go carrierLabels, carrierPath, carriersUnder |
runtime/subject_value_test.go:TestAmbiguousCarriersAreNamedByTheirPath, robustness_test.go:testPartsSubsettingOneCollection |
✅ Faithful |
Over gRPC an ambiguous subject is a typed reason (FAILURE_REASON_AMBIGUOUS_SUBJECT), so a client tells it apart from a violated condition without reading the message |
grpc/verify.go failureReason, api/proto/sysml.proto FailureReason |
grpc/verify_subject_test.go:TestVerifyConstraintReportsAnAmbiguousSubject |
✅ Faithful |
| A requirement's subject/actor bindings are evaluated against the same object its conditions read | context.go CheckRequirementOn over memberBindings |
runtime/subject_report_test.go:TestSatisfactionRoutesThroughTheSubjectRule |
✅ Faithful |
One object stands for each declaration reached in that search: the objects of a part held with a multiplicity (part wheels : Wheel[4]), those of a part nested inside it, and an object left behind by an earlier materialization of its holder are each one candidate, while two declarations feeding one collection (part small : Component :> subsystem) are two |
condition.go carriersUnder over occurrenceOf, rootInstances over heldObjectIDs |
robustness_test.go:testNestedPartHeldWithAMultiplicity, testPartNestedInsideARepeatedPart, testPartsSubsettingOneCollection, testDuplicateObjectsHoldingAPlainPart, testDuplicateObjectsOfOneDeclaration |
✅ Faithful |
The search for that object descends a declaration once per path, so composition naming its own kind (part def Node { part next : Node; }) terminates instead of materializing objects until the step budget is spent |
condition.go carriersUnder |
robustness_test.go:testRecursiveCompositionSubjectSearch |
✅ Faithful |
A false assertion is a verdict, not a malfunction (ErrViolated), and is distinguishable from an evaluation failure |
errors.go ErrViolated, context.go EvaluateConstraintOn |
repl/instance_test.go:TestConstraintEvaluationErrorIsNotAViolation |
✅ Faithful |
A constraint usage inherits its conditions from the definition it is typed by (constraint limit : MassLimit;) |
context.go chainMembers over semantics.Model.AllSupertypes |
instance_inherited_constraint.sysml |
✅ Faithful |
A parameter a typed usage binds (constraint limit : MassLimit { in m = mass; }) is visible to the conditions it inherits, and masks both the declaration it redefines and a same-named member of the object carrying the usage — including the usage's own name |
condition.go conditionFeatures over Model.MembersOf, eval.go evalFeatureReference |
instance_constraint_bound_parameter.sysml, instance_constraint_parameter_name_collision.sysml, runtime/condition_test.go:TestConstraintUsageBindsInheritedParameter |
✅ Faithful |
The conditions of a nested constraint (assert constraint [name] { <expr> }) are the conditions of the member stating it |
parser/behavior.go tryParseNestedConstraint, condition.go appendConditions |
parser/behavior_require_member_test.go:TestConstraintMemberNestedBody |
✅ Faithful |
A constraint body that declares parameters (constraint c { in x : Real; assert x >= 0; }) states conditions like any other, including conditions that start with a keyword (assert true, assert not false, assume null != x, assert if …); a condition missing its expression is a diagnostic |
parser/behavior.go atConstraintCondition, parser/expr.go exprStartKeywords |
parse/constraint_parameterised_conditions.golden, parser/constraint_condition_test.go, parser/negative_test.go:constraint_params_assert_no_condition |
✅ Faithful |
A constraint carrying no condition yields no verdict (ErrNoConditions) rather than a vacuous pass |
errors.go ErrNoConditions, context.go EvaluateConstraintOn/EvaluateRequirementOn |
runtime/constraint_test.go:TestConstraintWithoutConditionsIsNotAVerdict |
✅ Faithful |
Instantiation and Feature Values (SysML v2 §7.6 Feature Values, KerML §8.3)¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
| A literal default is folded at instantiation | instance.go Instantiate |
instance_derived_slots.sysml (folded) |
✅ Faithful |
| A default that reads sibling features is derived per instance, evaluated against that object's feature values on demand | instance.go GetFeatureValue/evalFeatureValueDefault, eval.go selfFeatureValue |
instance_derived_slots.sysml (doubled) |
✅ Faithful |
| A default reaching through a nested part reads that part's own derived values | eval.go evalFeatureChain (via GetFeatureValue) |
instance_derived_slots.sysml (total) |
✅ Faithful |
| A default expression resolves declarations in the scope that declared the feature, while instance feature values take precedence | shape.go EffectiveFeature.DeclScope, eval.go EvalContext.self |
instance_derived_slots.sysml |
✅ Faithful |
| Mutually dependent defaults report a cycle rather than recursing to the step budget | context.go derivingSlots, errors.go ErrCyclicFeatureValue |
robustness_test.go:cyclic_derived_slot |
✅ Faithful |
| A default over an undeclared feature fails naming the feature value | instance.go evalFeatureValueDefault |
robustness_test.go:derived_slot_over_missing_feature |
✅ Faithful |
| A multi-valued feature holds its default's contents; a single value written on it is the collection's one element | instance.go GetFeatureValue |
runtime/instance_test.go:TestMultiValuedDefaultMaterializes, repl/instance_test.go:TestCollectionFeatureValuesShowTheirContents |
✅ Faithful |
A nested part usage with a body of its own is instantiated as that usage, so what its body declares wins over what its type declares, and an untyped nested part (part engine { ... }) still materializes |
instance.go compositeType, GetFeatureValue |
instance_nested_usage_body.sysml, instance_unnamed_redefinition.sysml, runtime/instance_test.go:TestNestedUsageBodyOverridesItsType, TestUntypedNestedPartMaterializes |
✅ Faithful (both the named form and an unnamed :>> power = 250.0;, which takes the name of what it redefines — see the KerML 7.3.4.5 row above) |
A single-bound multiplicity is both bounds, unless it is unbounded: [*] is 0..*, so a [*] feature materializes empty like [0..*] (KerML 1.0 §8.2.5.11, confirmed by OMG issue KERML11-204) |
semantics/multiplicity.go multiplicityRange |
semantics/multiplicity_test.go:TestMultiplicitySingleBoundStar, conformance multiplicity_unbounded_single_bound, parse/multiplicity_unbounded_and_subsetting.golden, passes/constraint_test.go:TestConstraint_RedefinitionUnboundedMultiplicity (a [*] redefinition keeps an inherited 0..* and loosens an inherited 1..*) |
✅ Faithful |
A required lower bound is materialized eagerly, so an unbounded one ([*..*]) or one past the materialization bound reports a multiplicity violation instead of allocating |
instance.go GetFeatureValue (maxMaterializedLowerBound, ErrMultiplicityViolation) |
robustness_test.go:multiplicity_infinite_lower_bound, :multiplicity_lower_bound_too_large |
⚠️ Approximate (a lower bound above 1000 is refused rather than materialized lazily) |
The values of a subsetting feature are values of the feature it subsets, so a nested part declared part a : Sub :> subsystem is one of the objects subsystem holds and a roll-up over subsystem sums over it (KerML 1.0 §7.3.4.4) |
subsetting.go subsettingContributions/relatedFeatureNames, instance.go GetFeatureValue |
conformance feature_chain_rollup_over_subsets, cubesat_mass_rollup, robustness_test.go:mutually_subsetting_features |
⚠️ Approximate (contributions are the subsetting features declared on the same object; the subsetted collection is read-only with respect to them, and a subsetting feature declared elsewhere for the same object contributes nothing) |
A bracket multiplicity is the population, not a factor: part cell : Component[4] holds four objects, each carrying its own feature values, so a roll-up such as sum(cell.basicMass) counts each object once and a per-object value multiplied by the same 4 double counts. The subsetting features are among the population and anonymous objects make up the rest of the required lower bound |
instance.go GetFeatureValue (subsettingContributions, then mult.Lower.Value - len(contributed) anonymous objects), collections.go sum |
conformance multiplicity_rollup_counts_each_instance_once (a [4] collection, and a [3] one whose named subsetting part is joined by two anonymous objects), feature_chain_rollup_over_subsets, cubesat_mass_rollup |
✅ Faithful |
A redefining feature is the feature it redefines, so part subsystems : Component[*] :>> Subsystems makes both names read one collection, and a chain of redefinitions (dry :>> own :>> mass) reads one feature value under every name even when a usage restates the redefinition (part sat : Sys { attribute :>> own = 10.0; }) (KerML 1.0 §7.3.4.5) |
subsetting.go aliasRedefinedFeatureValues, redefinitionGroups, sharedRedefinitionName, redefinedNames, isFeatureOf |
conformance cubesat_mass_rollup, redefinition_restated_in_a_usage, redefinition_multilevel_base_name, redefinition_value_under_either_name, redefinition_valued_under_two_names, robustness_test.go:one_feature_valued_under_two_names |
✅ Faithful (the shared feature value is the one the most specific declaration writing a value created, whichever name it wrote — the redefining name, the base name, or a name in between, including where the value is written in an abstract part usage a configuration specializes; one declaration valuing two names of the feature is ErrConflictingRedefinition rather than a silent pick) |
A usage of any kind whose body restates or adds features is instantiated as that usage, so a struct-typed attribute takes its value from a nested body (attribute :>> material { attribute :>> v = 3.0; }) at any depth, and a renaming redefinition that restates no type is typed by the feature it redefines (KerML 1.0 §7.4.7) |
instance.go CompositeTypeOf, declaresFeatures, shape.go extractType, declaredType |
conformance attribute_nested_value_body, parse/nested_value_body.golden, shape_test.go:TestFeaturesOf_TypeInheritedThroughRedefinition |
✅ Faithful |
A feature bound to a value takes its own features from that value, so a body of the same declaration valuing one of them (attribute :>> ringCost = 400.0 { attribute :>> v = 9.0; }) states two values for it and is reported; a body that only re-declares features (the stdlib's item :>> edges : Ellipse = shape { attribute :>> Shell::edges::innerSpaceDimension, Ellipse::innerSpaceDimension; }) states no second value and reads the bound one |
instance.go restatedInValuedBody, restatedValueInBody, GetFeatureValue (ErrValuedFeatureRestated) |
robustness_test.go:valued_feature_restated_in_a_body, conformance attribute_nested_value_body (valuedWithReDeclaredFeatures) |
✅ Faithful (the two values are equally specific here, neither one governing, so the model is reported rather than a value picked; a body over a value the redefined declaration wrote is the more specific declaration and governs instead — see the row below) |
A body on a redefining declaration valuing features the value it inherits would supply governs over that value, so part def Ring { attribute cost : Cost = template; } re-opened as part r : Ring { attribute :>> cost { attribute :>> v = 11.0; } } reads r.cost.v as 11.0: the more specific declaration of a feature is the one that holds (KerML 1.0 §7.3.4.5, FeatureValue in KerML.kerml) |
instance.go valueBinds, bodyGovernsInheritedValue, restatedValueInBody, valuesAFeature (a value stated anywhere in the body, at any depth, is what makes it govern), read by CompositeTypeOf, Instantiate, materializeFeatureValue and adopt.go derivedFeatureValue |
conformance attribute_body_over_inherited_value (incl. deepReDeclarationKeepsInheritedValue, valueStatedAtDepthGoverns), attribute_body_over_inherited_value_chain, runtime/instance_test.go:TestBodyGovernsAnInheritedValue |
⚠️ Approximate (the body supersedes the inherited value rather than merging with it — a feature the body does not value takes its type's own default, not the bound value's, since a FeatureValue binds a feature as a whole and KerML states no per-nested-feature composition of one) |
| A check made without materializing an object reads the same values one built from the declaration holds, so a condition naming a feature a body governs over reports it uninitialized rather than judging the superseded value, and an edit confined to a governing body changes the type's shape a carried-over object is admitted by | condition.go conditionFeatures, adopt.go writeShape, ShapeDigest, read by context.go memberBindings |
runtime/instance_test.go:TestConditionsDoNotReadAGovernedOverValue, runtime/adopt_test.go:TestShapeFollowsAGoverningValueBody |
✅ Faithful (the two readings agree; a feature the body governs is read from the object, so a check without one names it uninitialized rather than answering from a value the model replaced) |
A redefining feature that declares no value holds the value the redefined declaration wrote, evaluated in the scope that wrote it, so attribute grossMass :>> mass reads the inherited default under either name (KerML 1.0 §7.3.4.5) |
shape.go redefinedDefault, EffectiveFeature.DefaultScope, instance.go evalFeatureValueDefault |
conformance instance_redefined_attribute_default, subsetting_test.go:TestRedefiningFeatureHoldsTheRedefinedDefault |
✅ Faithful |
A multi-valued feature given a default holds that default's contents whether or not it is also typed, so attribute volumes : Real[0..*] = subsystem.volume is the chain's values rather than an empty typed collection |
instance.go GetFeatureValue, CompositeTypeOf |
conformance feature_chain_rollup_over_subsets, feature_chain_nested_multivalued, instance_test.go:TestTypedMultiValuedDefaultHoldsItsContents |
✅ Faithful |
A value type classifies values, not objects (Base::DataValue is abstract datatype DataValue specializes Anything, "entities that are values"), and a Real declares no features, so a valueless feature of one holds no value: every surface that reports a value reports it as <unset> — -instantiate/-e, %features, the JSON report, and pb.Value.unset / opensysml.UNSET on the wire — rather than as the empty object materialization creates for it |
runtime/instance.go Context.HoldsNoValue, UnsetText, isValueTypeSymbol; repl/meta.go formatValue/formatSlot/nestedInstances, repl/run.go namedValues/renderResults (which cmd/sysml/report.go renders as JSON); grpc/convert.go ValueToProtoIn, ProtoToValueIn (ErrUnsetNotAccepted); python/opensysml/values.py UnsetType/value_to_python |
conformance instance_valueless_value_typed_attribute, robustness_test.go:expression_over_a_slot_holding_no_value, repl/unset_feature_value_test.go, cmd/sysml/unset_feature_value_test.go:TestUnsetFeatureValueReadsTheSameOnEverySurface, gRPC conformance instantiate_unset_slot, grpc/unset_feature_value_test.go, python/tests/test_unset.py |
⚠️ Approximate (the spec settles neither what materialization creates for such a feature nor a rendering for it, so the empty object stays and only the reporting is unset; a valueless 1..1 feature draws no diagnostic, since the multiplicity check bounds values a model binds and this one binds none) |
Only a conforming multi-valued default is honoured: a default whose element count is within the declared multiplicity becomes the feature's value — a collection literal, one value, (), an expression's result, the objects a composite part[n] default names, quantities with their units — while one outside it is a multiplicity violation rather than a value broadcast to the lower bound, truncated to the upper one or dropped |
instance.go GetFeatureValue, checkDefaultCount (ErrMultiplicityViolation), semantics/multiplicity.go Range.CountViolation |
conformance multiplicity_default_merged, multiplicity_default_composite, multiplicity_default_nonconforming, multiplicity_default_redefinition, instance_test.go:TestCompositeMultiValuedDefaultHoldsTheNamedObjects, instance_test.go:TestNullDefaultHoldsNoElements, robustness_test.go:default_not_conforming_to_multiplicity, semantics/multiplicity_test.go:TestMultiplicityWithANonEvaluableBound (a bound that is not constant leaves that side unknown — the known side still bounds the count), :TestMultiplicityInfiniteLowerWithFiniteUpper |
✅ Faithful |
A feature that declares no multiplicity holds exactly one value, so a default is bound by the assumed 1..1 — attribute x : Real = (1.0, 2.0) is the multiplicity violation Real[1] = (1.0, 2.0) already is (KerML 1.0 §7.4.5) |
semantics/multiplicity.go AssumedRange, Model.EffectiveMultiplicityOf, used by runtime/shape.go extractMultiplicity and runtime/instance.go checkDefaultCount |
semantics/multiplicity_test.go:TestEffectiveMultiplicityAssumesOne, :TestMultiplicityOfANonUsage (a definition and a nil symbol declare none and take the assumption), conformance multiplicity_default_assumed, robustness_test.go:default_against_an_undeclared_multiplicity, cmd/sysml/materialize_test.go:TestCheckReportsMaterializationDiagnostics |
⚠️ Approximate (the assumption bounds a default where its value count is known — when the feature value materializes; the static tier still bounds only a declared multiplicity, so a multi-valued default on an undeclared one is reported at materialization rather than by passes.checkValueCount) |
Materializing an object is part of a run, so -instantiate reports every feature value that did not materialize and -validate reports no errors only for a run that found none: an invalid model exits 2 rather than 0 |
cmd/sysml/check.go runChecks, cmd/sysml/report.go reporter.finding, clean, status, over runtime/materialize.go Context.MaterializationErrors (read through repl/instantiate_report.go Session.InstantiateReport) |
cmd/sysml/materialize_test.go:TestCheckReportsMaterializationDiagnostics (exit code plus the diagnostic, for a clean model, a scalar default against [3], and a multi-valued default on an undeclared multiplicity), :TestCheckReportsMaterializationDiagnosticsAsJSON |
✅ Faithful for -instantiate/-validate, documented in docs/reference/cli.md § Exit status |
The prompt surface keeps the same rule: a meta-command that rendered a feature value it could not materialize — a %features listing carrying <error: …>, or an %eval of such a feature value, pinned to a context or not — answered nothing about it, so the failure is carried as the runtime's typed error in the session and a non-interactive (piped / non-TTY) run exits 2, while at a terminal the failure is reported at the prompt and the session goes on |
repl/run.go Session.HasErrors, MaterializationFailures, noteMaterializationFailure, noteIfMaterializationFailure, hasAnalysisErrors, recorded by repl/meta.go doFeatures/slotWalk, doEval and doEvalLine, read by cmd/sysml/main.go sessionStatus |
repl/materialize_status_test.go:TestFeatureValuesCarriesMaterializationFailureIntoStatus, :TestEvalCarriesMaterializationFailureIntoStatus, :TestPinnedEvalCarriesMaterializationFailureIntoStatus, :TestFeatureValuesOfAConformingModelLeaveNoFailure, :TestPromptContinuesAfterAMaterializationFailure, cmd/sysml/materialize_test.go:TestPipedSessionExitsOnAMaterializationFailure, :TestSessionStatusAtATerminal |
✅ Faithful |
| A feature value that could not be materialized is marked as such by the runtime that read it, so a surface tells it from any other failure to evaluate without matching rendered text, whatever expression it surfaced through; naming no feature value of the object is not such a failure | runtime/errors.go FeatureValueError, ErrFeatureValueMaterialization, marked at runtime/instance.go GetFeatureValue over materializeFeatureValue, tested by repl/run.go noteIfMaterializationFailure |
repl/materialize_status_test.go:TestPinnedEvalCarriesMaterializationFailureIntoStatus, :TestEvalOfAnUnknownFeatureValueIsNoMaterializationFailure, :TestFeatureValuesCarriesMaterializationFailureIntoStatus (the marked error still unwraps to ErrMultiplicityViolation) |
✅ Faithful |
The check reads what it can: a walk that spent its budget, met a part deeper than it descends, or met a kind already being expanded above it reports what it did not read rather than that there were no errors, and being no model error it stays 0 |
runtime/materialize.go materializeWalk.walk (bounded), cmd/sysml/check.go runChecks, cmd/sysml/report.go reporter.warn (runtime.materialize.bounded) |
runtime/materialize_test.go:TestMaterializationErrorsBoundsAWideModel, :TestMaterializationErrorsOfAConformingObject, cmd/sysml/materialize_test.go:TestCheckDoesNotReportCleanWhenNestingWasElided |
✅ Faithful |
| The multiplicity a redefining feature does not restate is the one it redefines, so a default it adds is bound by the redefined declaration's multiplicity (KerML 1.0 §7.3.4.5) | shape.go buildFeatures, redefinedMultiplicity, passes/typecheck_value.go effectiveRange |
conformance multiplicity_default_redefinition, passes/typecheck_value_test.go:TestValueCountAgainstRedefinedMultiplicity |
✅ Faithful |
A relationship target that resolves outside the object names no feature of it, so attribute totalmass :> ISQ::mass specializes the library feature and contributes nothing to a same-named feature of the object; a target the object carries under its name — including one a restating declaration masks — is a feature of it, and an unqualified target the declaring scope cannot see is looked up among the object's members |
subsetting.go relatedFeatures/isFeatureOf |
subsetting_test.go:TestSubsettingIgnoresALibraryFeatureOfTheSameName, conformance cubesat_mass_rollup |
✅ Faithful |
Redefinition in a Specialization (KerML §7.4.7 Redefinition, SysML v2 §7.6)¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
A usage that redefines an inherited usage (part derived :> base { part :>> inner { … } }) specializes what it redefines, so it keeps every nested member the redefined usage declared and overrides only what it restates |
semantics/model.go NewModel (attaches the model to resolve.Resolver, so a redefinition target reachable only through inheritance resolves and the redefining usage gains it as a supertype), consumed by runtime/shape.go FeaturesOf over Model.MembersOf |
redefinition_inherited_nested_values.sysml, ballandchain_variant_configuration.sysml, robustness_test.go:deep_specialization_chain_of_redefinitions, conflicting_redefinitions_at_several_levels |
✅ Faithful (multi-level chains, a redefinition of a redefinition, and conflicting restatements where the innermost wins; the merge is the inherited-member view, not a feature value-level merge in the instantiator) |
A union's instances are exactly those of its unioning types (KerML §8.3.3), so a type declared classifier MyWheel unions MyWheel1, MyWheel2 conforms to every type all of its unioning types conform to, and feature redefines rollsOn : MyWheel redefining rollsOn : Wheel is well-formed. Unioning is not a generalization edge — a union inherits nothing from its members — so it is resolved separately from DirectSupertypes |
semantics/model.go Model.Conforms → unionConforms, UnioningTypes |
passes/constraint_unions_test.go:TestConstraintRedefinitionConformsThroughUnion, :TestConstraintRedefinitionUnionMemberDoesNotConform, :TestConstraintRedefinitionUnionCycleTerminates |
✅ Faithful (conformance only: a union's members are not computed, and intersects/differences are not read) |
The type a redefinition must inherit the redefined feature from is the feature's featuring type where it declares one (member feature CC1_snapshots :>> Occurrences::Occurrence::snapshots featured by CC1; is featured by CC1, not by the feature it is written inside — KerML §7.4.5, §8.3.4.3), and a bare feature owned by a package has no featuring type, so nothing can inherit it and the rule does not apply |
passes/constraint.go checkRedefinition over featuringOwners (the declared featured by targets, else the lexical owner), isInheritedMember and isPackageLevelFeature |
passes/constraint_test.go:TestConstraint_RedefinitionUsesFeaturingType, :TestConstraint_PackageLevelRedefinitionHasNoInheritedOwner, :TestConstraint_PackageLevelUnfeaturedRedefinitionExemptsNoInheritedRule |
⚠️ Approximate (only a declared featured by is read — the featuring a nested feature implies is not computed — and a member whose owner is declared within the redefined feature's own scope is taken as inheriting it, which is the TimeVaryingFeatures.kerml shape rather than a general rule. The package-level exemption is decided by the absence of a featured by relationship, so a package-level feature that declares one is still checked) |
Variation and Variant (SysML v2 §7.20 Variant Modelling)¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
variation and variant are recorded on the declaration they modify, in every position they may appear (variation attribute/part/interface, nested or top-level) |
parser/defusage.go applyFeatureMods, atKindPrefix, ast/defusage.go Usage.IsVariation/IsVariant, round-tripped by export/rdf_out.go/rdf_in.go |
parser/testdata/parse/variation_and_variant.golden, parser/negative_test.go (variation_no_declaration, variation_attribute_no_name, variant_unclosed_body, variant_selection_no_variant_name) |
✅ Faithful |
| A variation is an abstract classifier of its variants: a variant specializes its variation and so carries the variation's type and features | semantics/model.go DirectSupertypes via semantics/variation.go VariationOwning |
semantics/variation_test.go:TestVariationAndVariantModifiers, TestVariantsOfInheritedThroughRedefinition |
✅ Faithful |
A variant is reachable through the variation feature's name (cut::cutIdeal), including through a feature chain (engagementRing.nesting::nestingTrue) and through a feature that redefines or specializes the variation |
semantics/variation.go Model.IsVariationFeature, Model.VariantsOf, Model.VariantOf, runtime/variation.go EvalContext.variantSegment, runtime/eval.go evalFeatureChain |
variation_attribute_selection.sysml, variation_part_selection.sysml, semantics/variation_test.go:TestIsVariationFeatureThroughSpecialization |
✅ Faithful |
Binding a variation usage to one of its variants selects that variant, with its nested values and its own nested features, whether the variation is read through an object's feature value or through its declaration (EvaluateConstraint, EvalWithScope, REPL %eval) |
runtime/variation.go Context.bindVariation, variantValue, EvalContext.bindVariationOf, runtime/instance.go GetFeatureValue (variation feature values resolve before ordinary defaults), runtime/value.go ValVariant |
variation_attribute_selection.sysml, variation_part_selection.sysml, variation_interface_selection.sysml, robustness_test.go:variation_read_through_its_declaration |
✅ Faithful |
A variation feature compares equal to the variant it is bound to (x == x::variantName), and unequal to any other variant — the form an asserted configuration constraint uses |
runtime/eval.go equality over ValVariant, runtime/value_equality.go |
variation_attribute_selection.sysml, variation_interface_selection.sysml, variation_interface_mismatch.sysml, ballandchain_variant_configuration.sysml |
✅ Faithful |
| A variation with no variant selected is a typed error, never a silently wrong value, and a variation is no occurrence of itself, so a chain through an unselected variation part reports the same rather than reading an object of the variation | runtime/errors.go ErrVariationUnselected, runtime/instance.go GetFeatureValue, runtime/eval.go evalFeatureReference, runtime/calc_usage.go occurrenceOperand |
variation_unselected.sysml, robustness_test.go:variation_without_a_selected_variant, chain_through_an_unselected_variation_part |
✅ Faithful |
| Selecting what is not a variant of the variation, or selecting two variants at once, are typed errors naming the variants available | runtime/errors.go ErrNotAVariant/ErrMultipleVariants, runtime/variation.go bindOneVariant, variantSummary, runtime/eval.go (a missing member under a variation feature) |
robustness_test.go:variation_bound_to_what_is_not_a_variant, variation_bound_to_two_variants, semantics/variation_test.go:TestSelectsVariantOfRejectsForeignVariant |
✅ Faithful |
A variant whose owner is not a variation offers no choice, so it stays an ordinary feature of its owner and the idle variant keyword is reported; an owner that is a variation point by specialization still offers its variants as choices |
passes/constraint.go checkVariantOutsideVariation (warning variant-outside-variation), runtime/shape.go buildFeatures and runtime/eval.go (only a variant of a variation point is a choice rather than a feature value, via semantics/variation.go Model.VariationPointOwning over IsVariationFeature, which semantics/model.go DirectSupertypes also uses so a variant specializes such a point and inherits its type, and VariantsOf uses so the choices offered are the choices a selection accepts) |
variant_outside_a_variation.sysml, variant_under_an_inherited_variation.sysml, robustness_test.go:variant_outside_a_variation, variant_under_a_redefined_variation, passes/constraint_test.go:TestConstraintVariantOutsideVariation, TestConstraintVariantInsideVariationOK, TestConstraintVariantUnderInheritedVariationOK, semantics/variation_test.go:TestVariantsOfExcludesAMisplacedInheritedVariant |
✅ Faithful |
| The object a selected variant stands for belongs to the selection that made it: two owners, or two variation points read through their declarations, each get their own object | runtime/variation.go variantObject (keyed by owning object, variation point and variant), variantValue |
robustness_test.go:two_owners_selecting_one_variant, two_ownerless_selections_of_one_variant, repeated_reads_of_a_variant_object |
✅ Faithful |
variation interface and its variant interface … connect … members |
parser/defusage.go (interface usages take the same modifiers), selection as above; the selected variant's connection is realized by runtime/variation.go variantInstance over runtime/connector.go materializeConnector, and routing follows it through runtime/routing.go routableConnections/realizedConnections/selectedVariant over lower/connection.go Connection.Variation/Variant/Owner and ToObjectConnections, with the object performing the behavior carried by runtime/context.go ExecuteActionPerformedBy/ExecuteStatePerformedBy |
variation_interface_selection.sysml, variation_interface_mismatch.sysml, ballandchain_interface_connected.sysml, ballandchain_interface_disconnected.sysml, ballandchain_variant_configuration.sysml, signal_test.go:TestRoutingHonorsTheSelectedVariantConnection, lower/connection_test.go:TestLowerVariantConnectionsCarryTheirVariation, :TestLowerObjectConnectionsAreOwnedByTheObject, variant_connection_per_owner.sysml, signal_test.go:TestRoutingIsPerOwnerVariantSelection |
✅ Faithful (the variant is selected and compares equal, so a configuration constraint over interface variants evaluates, and the connection that variant declares is a real runtime connector whose ends are the connected features, so port communication follows the selected variant and not the variants left unselected; a connection an object declares routes for that object, so two objects of one type selecting different variants each route their own) |
⚠️ Variant selection is not ordering-sensitive — a variation feature value resolves to one variant per instance — so no golden execution trace accompanies these rows.
Requirement¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
| Require expression evaluation, in a requirement definition body as well as a usage | parser/behavior.go parseRequirementBody (both parseDefinition and parseUsage paths), condition.go conditionsOf |
requirement_literal.sysml, requirement_def_body_require.sysml, parser/behavior_require_member_test.go:TestRequirementConditionForms |
✅ Faithful |
A condition stated through an anonymous nested constraint (require constraint { <expr> }, the form the OMG Domain Libraries use) is evaluated, and every condition of that body is kept |
parser/behavior.go parseNestedConstraintConditions, condition.go appendConditions |
requirement_nested_constraint.sysml, parser/behavior_require_member_test.go:TestRequireMemberRetainsConditions |
✅ Faithful |
A requirement's conditions see the requirement's own features — declared, inherited, or rebound by a typed usage (attribute :>> maxVerticalSpeed = 1.5;) |
condition.go conditionFeatures, eval.go evalFeatureReference |
requirement_own_attribute.sysml, requirement_nested_constraint.sysml, runtime/condition_test.go:TestRequirementConditionSeesOwnAttributes |
✅ Faithful |
A feature a condition names but which carries no value reports that (ErrNoValue) rather than being unresolved |
errors.go ErrNoValue, eval.go evalFeatureReference |
runtime/condition_test.go:TestRequirementConditionWithoutValueIsNotUnresolved |
✅ Faithful |
| A violated condition names the condition that failed, not only the element stating it | errors.go ViolationError, condition.go conditionText |
requirement_violated.sysml, runtime/condition_test.go:TestRequirementConditionSeesOwnAttributes |
✅ Faithful |
| Subject binding evaluation | context.go:148 EvaluateRequirement (Pass 1) |
requirement_subject.sysml |
✅ Faithful |
| Actor binding evaluation | context.go:148 EvaluateRequirement (Pass 1) |
requirement_actor.sysml |
✅ Faithful |
| Assume expression evaluation | context.go:148 EvaluateRequirement (Pass 2, doesn't fail) |
requirement_assume.sysml |
✅ Faithful |
A false required condition is a verdict, not a malfunction (ErrViolated), like a false assertion |
context.go EvaluateRequirementOn, errors.go ErrViolated |
repl/instance_test.go:TestRequirementViolationIsAVerdictNotAnError |
✅ Faithful |
| A requirement usage inherits assume/require conditions from the definition it is typed by, and the values it rebinds are the ones those conditions see | context.go chainMembers, condition.go conditionFeatures |
requirement_nested_constraint.sysml, requirement_violated.sysml |
✅ Faithful |
A subject may redeclare the one it inherits (subject subj : View[1] :>> RequirementCheck::subj;) |
parser/behavior.go parseSubjectMember, resolve/document.go, passes/typecheck.go checkSubjectMember |
parser/behavior_require_member_test.go:TestRequirementConditionForms, libs/stdlib_conformance_test.go (Systems Library/Views.sysml) |
✅ Faithful |
| Nested requirements | context.go:148 EvaluateRequirement (recursive) |
requirement_nested.sysml |
✅ Faithful |
satisfy <name> is an OwnedReferenceSubsetting of an existing usage, not a typing (SysML v2 §8.3.21.10 SatisfyRequirementUsage) |
parser/defusage.go parseDefUsage (ast.RelSubsets) |
parser/testdata/parse/satisfy_reference.golden |
✅ Faithful |
referencedFeatureTarget().oclIsKindOf(RequirementUsage) — satisfy/verify may only reference a requirement usage (incl. viewpoint/concern usages) |
passes/typecheck.go compatMessage, isRequirementUsageKind |
passes/typecheck_test.go TestTypeCheckSatisfyRequirementUsageOK, TestTypeCheckSatisfyViewpointUsageOK, TestTypeCheckSatisfyNonRequirementUsageError |
✅ Faithful |
assert satisfy <requirement> by <part>; is a verdict of its own: the assertion is evaluated as the requirement usage it is (SatisfyRequirementUsage, SysML v2 §8.3.21.10), with the requirement's subject parameter bound to the object the by feature supplies, so the conditions read that object's values |
runtime/satisfy.go SatisfyAssertionsIn, EvaluateSatisfactionOn, repl/meta.go doSatisfy |
satisfy_subject_binding.sysml, satisfy_inherited_conditions.sysml, runtime/satisfy_test.go, repl/satisfy_test.go:TestSatisfyVerdicts |
✅ Faithful |
An assertion may be negated (assert not constraint { … }, assert not satisfy … by …; Invariant::isNegated, SysML v2 §8.3.21.10), and holds exactly when the conditions it denies do not |
ast/defusage.go Usage.IsNegated, parser/defusage.go applyFeatureMods, runtime/condition.go evaluateConditions |
parser/testdata/parse/assert_negated.golden, satisfy_negated.sysml, runtime/negation_test.go |
✅ Faithful |
A negated element states no condition it can deny when its body holds only assumptions, which are trusted rather than checked, so it reports no condition to evaluate rather than a violation naming none |
runtime/condition.go evaluateConditions |
runtime/condition_test.go TestNegatedConstraintWithOnlyAssumptionsIsNotAVerdict |
✅ Faithful |
A negation denies the conditions of the constraint it is written on together — not (a and b), not not a and not b — so it holds as soon as one of them fails |
runtime/condition.go appendConditions, conditionHolds |
runtime/condition_test.go TestNegatedNestedConstraintNegatesTheConjunction, constraint_negated_group.sysml |
✅ Faithful |
An ObjectiveMembership's ownedObjectiveRequirement is a RequirementUsage (SysML v2 §8.3.22.4), so an objective is typed by a requirement definition or a specialization of one, never by a structural definition |
passes/typecheck.go compatibleTyping, isRequirementDefKind |
passes/typecheck_kinds_test.go TestTypeCheckObjectiveTypedByRequirementDefOK, TestTypeCheckObjectiveTypedByConcernDefOK, TestTypeCheckObjectiveTypedByPartDefError, TestTypeCheckObjectiveTypedByActionDefError |
✅ Faithful |
A SubjectMembership's ownedSubjectParameter is an unconstrained Usage (SysML v2 §8.3.21), so a definition of any kind types a subject — including the port def and action def the OMG training models use — and the rule applies however the requirement body is written, not only when the subject happens to parse as a usage |
passes/typecheck.go checkSubjectMember, compatibleTyping |
passes/typecheck_subject_test.go TestTypeCheckSubjectIsCheckedWhateverPrecedesIt, TestTypeCheckRequirementUsageSubjectIsChecked, TestTypeCheckSubjectWithoutResolvableTypeIsNotATypeError; typecheck_kinds_test.go TestTypeCheckSubjectTypedByAnyDefKindOK, TestTypeCheckSubjectTypedByUsageError |
✅ Faithful |
A definition specializes a definition of a comparable kind: a PartDefinition is an ItemDefinition (SysML v2 §8.3.9.2), so individual item def Alice :> Person names a part definition legally, while disjoint kinds — an occurrence and a data value (§8.4.5.1) — are refused |
passes/typecheck.go defKindParents, defKindSpecializes, defKindsComparable |
passes/typecheck_kinds_test.go TestTypeCheckSpecializeComparableKindOK, TestTypeCheckNewKindSpecializeCrossKindError; model/training_examples_test.go (28. Individuals/Individuals and Time Slices) |
✅ Faithful |
Every definition implicitly specializes the library definition of its kind, so the features that base declares resolve in its body — Items::Item declares start and done, which Parts::Part redefines |
semantics/implicit.go implicitDefinitionBases, implicitBase |
model/implicit_typing_test.go, model/training_examples_test.go (27. Occurrences/Time Slice and Snapshot Example) |
✅ Faithful |
A quantity expression (1.5 [m/s]) evaluates to a magnitude and the measurement reference it is written in (Quantities::ScalarQuantityValue is num + mRef), so a condition comparing values written with units reaches a verdict |
runtime/quantity.go evalIndexExpr, value.go ValQuantity |
requirement_quantity_same_unit.sysml, runtime/quantity_test.go:TestQuantityEvaluation, parser/testdata/parse/quantity_expression.golden |
✅ Faithful |
Name in the unit position of a quantity expression. x [u] invokes Quantities::'['(num, mRef), so u is an ordinary operand expression and its name is resolved by ordinary name resolution: resolution returns the nearest declaration the name reaches (KerML 8.2.3.5.3 Local and Visible Resolution, 8.2.3.5.4 Full Resolution), and the position's expected type (ScalarMeasurementReference) only decides whether what resolved conforms (KerML 8.2.3.5.1). A sibling named m therefore shadows an imported SI::m — resolution does not continue outward looking for a unit — and the quantity is rejected with a diagnostic naming the declaration, the namespace declaring it, and the qualified spelling of the unit it hid. One routine implements this for every evaluator (part feature value default, action/state attribute default, calc return, condition) |
semantics/units.go unitTermOfName, ShadowedUnitError, unitOutside; runtime/context.go chainMembers (a condition evaluates in its own body scope, as the other paths do) |
unit_shadowed_by_sibling_slot.sysml, unit_shadowed_by_sibling_action.sysml, unit_shadowed_by_sibling_calc.sysml, unit_shadowed_by_sibling_constraint.sysml, unit_shadowed_by_local_unit.sysml, unit_undeclared.sysml, robustness_test.go:quantity_unit_shadowed_by_sibling |
✅ Faithful |
A violated assertion renders a quantity operand as it was written (1.0 [m] > 500.0 [m]), since the bracket form is a quantity and not a sequence index |
runtime/condition.go conditionText (ast.IndexExpr) |
runtime/condition_test.go:TestViolationRendersQuantityOperands |
✅ Faithful |
Commensurable units convert before a comparison or a sum, through MeasurementUnit::unitConversion and unit-defining expressions reduced to base units — 1.5 [m/s] <= 5.4 [km/h] is true, exactly, at its boundary (a conversion factor is kept as a ratio, not evaluated) |
semantics/units.go UnitTermOf, Scale, ConvertMagnitude |
requirement_quantity_converted_unit.sysml, constraint_quantity_sum.sysml, semantics/units_test.go:TestScaleStaysExact |
✅ Faithful |
A unit is composed by the operation over quantities (10 [m] / 2 [s] is 5 [m/s]), and a ratio of like quantities is a number of no unit. A composed operand is parenthesized in the composed unit's text, so (m/s) * (kg/s) names m/s*(kg/s) rather than a unit that re-reads as m/(s*kg)/s |
runtime/quantity.go scaleQuantities, composedUnitText, groupUnitText, semantics/units.go UnitTerm.DividedBy |
constraint_quantity_quotient.sysml, calc_quantity_ratio.sysml, runtime/quantity_test.go:TestComposedUnitText |
✅ Faithful |
A quantity raised to a constant exponent raises its unit with it, and its magnitude comes from the one ** implementation the folder and the runtime share — so (0.0 [m]) ** -1.0 and an overflowing magnitude are the same typed errors as for a bare number rather than an infinity carried in a unit, and (2 [m]) ** 3 keeps an Integer magnitude |
runtime/quantity.go powQuantity, semantics/eval.go Pow, semantics/units.go UnitTerm.Pow |
runtime/quantity_test.go:TestQuantityExponentiation, TestQuantityExponentiationReports |
✅ Faithful |
An execution trace of a unit-carrying value renders the magnitude and the unit (5.0 [m/s]), as the REPL prints a quantity |
runtime/trace.go FormatTraceValue |
action_quantity_assign.trace.golden, runtime/quantity_test.go:TestFormatTraceValueQuantity |
✅ Faithful |
Incommensurable units are a typed error (ErrIncommensurableUnits), never a comparison of bare magnitudes that would equate 1.5 [m/s] with 1.5 [km/h] |
runtime/errors.go ErrIncommensurableUnits, quantity.go convertTo |
runtime/robustness_test.go quantity_incommensurable_comparison, runtime/quantity_test.go:TestQuantityIncommensurable |
✅ Faithful |
Statically detectable incommensurability is diagnosed before evaluation. A comparison or a sum whose operands' quantity dimensions are both statically determined and incommensurable (mass < 1000.0 [m]) is reported as a warning at the type tier, naming both units and both dimensions; the dimension comes from the stdlib QuantityDimension power factors (ISQ's L, M, T, …) and commensurability from the same UnitTerm.Commensurable the runtime applies, so the two cannot drift. Evaluation remains the hard error, and a warning changes no exit code |
semantics/dimension.go DimensionOfExpr, dimensionOf; passes/typecheck_dimension.go checkDimensions |
model/dimension_check_test.go, model/typecheck_expr_corpus_test.go (stdlib, examples and OMG corpus stay silent) |
⚠️ Approximate (static only — see below) |
⚠️ The static dimensional check reports only what a declaration determines, and is silent — by design, not by omission — wherever it does not. A feature's dimension is taken from the quantity value type it declares (attribute mass : ISQ::MassValue), never from the value it happens to be bound to, since an assign may replace that value with one of another dimension. So an operand whose dimension comes from an untyped attribute or parameter, a calculation result, an unresolved name, or a redefinition not yet bound is undetermined and no warning is raised (a parameter that does declare a quantity type, in actual : ISQ::MassValue, is determined and is checked); the mistake then surfaces at evaluation as it did before. Products and quotients are not checked, having no commensurability requirement, and a dimension the check derives through unit arithmetic (m/s) is compared by dimension alone.
⚠️ The spec's own QuantityCalculations::ConvertQuantity(x, targetMRef) is not an invocable function, and a quantity is not yet an instantiated ScalarQuantityValue object whose num/mRef features can be read by name: the unit is carried on the runtime value, not modelled as a library object. The gRPC value schema does carry a quantity: Value.quantity holds the magnitude as written, the unit as written, and the reduced unit term (scale and base-unit factors by FQN), so a Python caller reads a quantity feature value as opensysml.values.Quantity, sends one as an action input or a calc argument (opensysml/values.py Quantity.to_pb, connection.py _python_to_value), and a round trip preserves both magnitude and unit (internal/grpc/convert.go QuantityToProto/ProtoToQuantity/ProtoToValueIn). Sequence indexing (speeds#(3)), which the parser represents with the same node, is evaluated as the index it is (see Sequence Indexing and Collection Operations below): the two forms are told apart at the node (ast.IndexExpr.Bracket), so an index is never read as a magnitude in a unit, nor a quantity as an index.
⚠️ A requirement feature that carries no value of its own is read from the satisfying object's feature of that name, which is how a requirement stated over the values it checks (attribute verticalSpeed; compared against a limit) reaches a verdict from by. The spec supplies a subject's values to a requirement through the subject parameter (subject lander : Lander; then lander.verticalSpeed) or an explicit binding, not by matching names, so this fallback — the same one %requirement applies on an instance — is an approximation, and a requirement whose unbound feature happens to share a name with an unrelated feature of the subject would be checked against it. A requirement whose value comes from neither its own binding nor the subject (the lunar lander model's actualVerticalSpeed, produced by an analysis) still has no value to check and reports ErrNoValue.
Action (SysML v2 Actions — Systems Library/Actions.sysml, over KerML Performances)¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
| Initial node token placement | action_executor.go:425 initialize |
action_control_flow.sysml |
✅ Faithful |
| Final node termination | action_executor.go:512 stepFinalNode |
action_control_flow.sysml |
✅ Faithful |
One feature space per performance: a succession is a HappensBefore link (Kernel Semantic Library Occurrences.kerml) that orders occurrences and carries no values, so the steps of an action — concurrent ones included — read and write the features of the one action they belong to |
action_executor.go:29 ActionExecutor.data, :519 retireToken, :1028 Results |
conformance/action_fork_branches_share_features.sysml + trace golden, action_executor_test.go:TestActionExecutor_ForkNode_SharedFeatureSpace |
✅ Faithful |
Fork node (1→N parallelism): duplicates control only — ForkAction is a ControlAction, which "has no inherent behavior", and "fork behavior results from requiring that the target multiplicity of all outgoing succession connectors be 1..1" (Systems Library Actions.sysml) |
action_executor.go:529 stepForkNode |
action_control_flow.sysml, conformance/action_fork_branches_share_features.sysml |
✅ Faithful |
Join node (N→1 synchronization): "join behavior results from requiring that the source multiplicity of all incoming succession connectors be 1..1" (Systems Library Actions.sysml), so it synchronizes control and merges no values |
action_executor.go:558 stepJoinNode |
action_control_flow.sysml, conformance/action_fork_branches_share_features.sysml |
✅ Faithful |
| Concurrent branches assigning the same feature | action_executor.go:529 stepForkNode (branches write the shared space in step order) |
robustness_test.go:fork_branches_assigning_the_same_feature |
⚠️ Approximate (branches out of a fork are unordered by the spec, so a same-feature write from each leaves the value unspecified; the runtime steps tokens in a fixed order and the last write in that order stands — step order, not the order the branches are written in — deterministic per model, but not a spec-mandated outcome, and no conflict is reported) |
| Merge node (N→1 non-blocking) | action_executor.go:626 stepMergeNode |
action_control_flow.sysml |
✅ Faithful |
| Decision node (guarded branching) | action_executor.go:656 stepDecisionNode |
action_control_flow.sysml |
✅ Faithful |
| Action execution nodes | action_executor.go:723 stepActionExecutionNode |
action_control_flow.sysml |
✅ Faithful |
| Nested action invocation | action_executor.go:782 stepNestedAction, invoke_action.go invokeAction |
invoke_action_test.go:TestInvokeActionPassesParametersBothWays |
✅ Faithful |
Assignment statement in a body (assign x := <expr>) |
lower/action_graph.go lowerStatement Assign; runtime/action_statements.go execStatement |
action_send_accept.sysml, lower/action_body_test.go:TestActionBodyLowering |
✅ Faithful |
Conditional statement (if <cond> { … } else { … }) |
lower/action_graph.go lowerStatement/lowerBlock (If); runtime/action_statements.go execIf, execBlock |
action_if_else_then_branch.sysml, action_if_else_else_branch.sysml, action_if_no_else.sysml, action_nested_loop_if.sysml + trace golden, lower/action_body_test.go:TestActionBodyLoopAndConditionalLowering, passes/typecheck_test.go:TestTypeCheckNonBooleanControlFlowConditions |
✅ Faithful (the condition is evaluated outside both branches; each branch body is a namespace of its own, so the names it declares do not reach the enclosing behavior) |
Pre-condition loop (while <cond> { … }) |
lower/action_graph.go lowerStatement (Loop, ast.LoopWhile); runtime/action_statements.go execLoop |
action_while_loop.sysml + trace golden, action_while_loop_zero_iterations.sysml, parse/action_loop_forms.golden |
✅ Faithful (tested before every iteration, so the body may run no times) |
Post-condition loop (loop { … } until <cond>;) |
parser/behavior.go parseLoopAction; lower/action_graph.go (ast.LoopUntil); runtime/action_statements.go execLoop |
action_loop_until.sysml, action_loop_until_repeats.sysml + trace golden |
✅ Faithful (tested after every iteration, so the body runs at least once) |
Iteration over a collection (for <x> in <collection> { … }) |
ast/behavior.go WhileLoopActionNode.Variable/Collection; symbols/builder.go (the variable is a member of the loop's own scope); runtime/statements.go forLoop, forElements |
action_for_loop.sysml, action_for_over_produced_collections.sysml + trace golden, parse/action_loop_forms.golden, action_for_over_a_part_collection.sysml + trace golden, statements_test.go:TestForElementsOrder, robustness_test.go:for_over_a_value_no_expression_makes_iterable |
✅ Faithful (the collection is evaluated once, before the loop is entered. Every collection the expression layer produces is iterated: a sequence in the order the expression built it — a literal sequence as written, a range ascending and a descending range empty (range.go rangeSequence), a filter in the order of the collection it filtered, a collection-valued function's result as returned — a set in the order its canonical rendering sorts in, since a set has no order of its own, and null, which holds no element, not at all. A for input that is not a collection reports ErrTypeMismatch naming what it was given) |
A for input that is not a collection is reported, where a general collection reader coerces one |
runtime/statements.go forElements (ErrTypeMismatch), deliberately stricter than runtime/collections.go elementsOf, which keeps reading a scalar as the one-element collection KerML makes of it — a for over a scalar is a modelling error, and a single silent iteration hides it, while the general readers (a collection operator's argument, a multiplicity check) legitimately coerce |
action_for_over_a_scalar.sysml (typed error), action_for_over_a_part_collection.sysml (a valid collection input, nested in a part) + trace golden, statements_test.go:TestForElementsRejectsANonCollection, :TestElementsOfStillCoercesAScalar, robustness_test.go:for_over_a_scalar |
✅ Faithful (ruling: for requires a collection; elementsOf is unchanged for its other callers) |
| A non-terminating loop ends the execution rather than hanging it | runtime/action_statements.go execLoop (a step per iteration), context.go incrementStep |
action_loop_step_budget.sysml, robustness_test.go:non_terminating_loop_exhausts_step_budget |
✅ Faithful (reports ErrStepLimitExceeded, the same failure as any other runaway evaluation) |
| A legitimately long loop runs under a raised budget | budget.go BudgetsFromEnv (SYSML_MAX_STEPS) resolved at the REPL/CLI and gRPC entry points |
budget_test.go:TestRaisedBudgetRunsLongerLoop |
✅ Faithful (a 10 000-iteration loop that exhausts a 100 000-step budget completes under the default) |
| The budget bounds one run, not a session | context.go beginRun/beginExecutorRun (the step counter is reset when a run begins; a nested run, and every call into a run a caller drives step by step, shares the outer one's budget) |
budget_test.go:TestStepBudgetIsPerRun, :TestStepBudgetHoldsAcrossExecutorDrivenRun, :TestStepBudgetIsPerRunForInstancesAndCalcs |
✅ Faithful |
| A legitimately long action or simulation runs under raised sibling budgets | budget.go Budgets (SYSML_MAX_ACTION_STEPS, SYSML_MAX_EVENTS, SYSML_MAX_DO_STEPS), read by action_executor.go and state_executor.go from the context |
budget_test.go:TestActionStepBudgetIsConfigurable, budget_test.go:TestStateBudgetsAreConfigurable |
✅ Faithful (each bound counts its own unit and its error names the variable that raises it) |
A member-attached then sequences the members either side of it (action a; then action b;) |
parser/succession.go (desugared at parse time to the *ast.SuccessionEdge the then a b; notation builds), lowered by lower/action_graph.go and lower/state_graph.go like any other edge |
conformance/action_member_then_order.sysml + trace golden (declaration order is the reverse of the execution order), conformance/state_member_then_order.sysml (the same for a state's completion transitions), parser/succession_test.go:TestMemberAttachedThenDesugars, parse/succession_member_then.golden |
✅ Faithful (an end with no name to give is bound by position: SuccessionEdge.SourceMember/TargetMember refer to the member itself, so then send Show(x) to screen;, a then after an anonymous member and then loop action { … } all sequence what they are written beside. A then before a member the notation does not admit one in front of, such as an attribute or a definition, is a syntax error) |
| A succession end with no name is carried by identity, not by name | ast/behavior.go SuccessionEdge.SourceMember/TargetMember, ControlFlowEdge.SourceMember/TargetMember; parser/succession.go bindPositionalSource/bodyBuilder.add; lower/action_graph.go (the member is the graph node the edge reaches) |
parser/succession_test.go:TestSuccessionBindsUnnamedEndsByPosition, :TestPositionalSuccessionEndIsTheMemberItself, conformance/action_standard_loop_until_then_done.sysml |
⚠️ Approximate (the RDF mapping names an edge's ends by qualified name, so exporting a model whose succession has a positional end is reported as unsupported rather than written back — see docs/reference/rdf-mapping.md) |
Named flow with explicit ends (flow f from a.out to b.in;, SysML.xtext FlowUsage → PayloadFeatureSpecializationPart + FlowEndMember) |
parser/defusage.go parseFlowEnds/parseFlowTo (the name, from and feature-chain ends); ast/defusage.go FlowEnds; lower/action_graph.go lowerFlow/flowEnd (ObjectFlow, the flow's name included); runtime/action_executor.go applyDataFlows |
parse/behavior_flow_named_from.golden, parse/flow_payload_declaration.golden, negative_test.go:flow_from_without_to, :flow_named_from_no_source, conformance/action_flow_named_from.sysml, robustness_test.go:flow_end_naming_no_node, :flow_from_a_node_that_produced_nothing |
✅ Faithful (both ends, feature chains included, name a node and its pin, and the value at the source's pin is what the target reads; flow of x from a to b names the pin at both ends. An end naming something that is not a node of the action, and a source pin the node left empty, are reported rather than dropped) |
Accept with a trigger expression in an action body (accept when <cond>, accept at <instant>, accept after <duration>; SysML.xtext TriggerValuePart) |
parser/behavior.go parsePayloadParameter/parseTriggerExpression; lower/action_graph.go Accept.Trigger; runtime/action_executor.go triggerHolds |
parse/behavior_accept_trigger.golden, conformance/action_accept_when_trigger.sysml, negative_test.go:accept_when_no_condition, :accept_at_no_instant, robustness_test.go:action_accept_time_trigger, :action_accept_non_boolean_change_trigger |
⚠️ Approximate (a change trigger is tested each step and suspends the token until it holds; an action body has no clock, so accept at/accept after there reports ErrNoClock naming the state machine that does wait on time, rather than firing) |
Accept subsetting a declared event (action interrupt accept :> shutDown;, SysML.xtext PayloadParameter → PayloadFeatureSpecializationPart) |
parser/behavior.go parsePayloadParameter; lower/action_graph.go subsettingTarget → Accept.SubsetsEvent; runtime/action_executor.go (the subsetted event names the message the accept takes) |
parse/behavior_accept_subsets.golden, conformance/action_accept_subsets_event.sysml, negative_test.go:accept_subsets_no_event |
✅ Faithful (a send shutDown() to interrupt is taken by the accept subsetting shutDown, as a typed accept takes a message of its type) |
Send of an invoked signal or event (send Data(reading) via commPort;, then send fullyCharged() to self;) |
parser/behavior.go parseSendStatement; runtime/signal.go buildMessage/buildInvokedMessage/invokesCalc |
parse/behavior_send_via.golden, conformance/action_send_invocation_via_port.sysml, negative_test.go:send_via_no_port, :send_no_target |
✅ Faithful (the invoked name types the message and its arguments are its payload — a single positional argument also as value, which a typed accept binds; an invocation of a calc is still evaluated as an expression) |
Succession to a loop node, and a loop's until condition (then loop action { … } until battery >= 100;, SysML.xtext WhileLoopNode) |
parser/behavior.go parseLoopAction/parseWhileLoopAction; ast/behavior.go WhileLoopActionNode.Until; lower/action_graph.go (loop body and Until lowered); runtime/statements.go iteration |
parse/behavior_loop_until_succession.golden, conformance/action_standard_loop_until_then_done.sysml, negative_test.go:loop_until_no_condition, :loop_until_no_semicolon, passes/typecheck.go checkBehaviorMember (the until condition is checked Boolean) |
✅ Faithful (loop { … } until c tests after the iteration; while c action { … } until u tests c before and u after) |
A loop or branch body written as an action body parameter (loop action [<name>] { … }, for x in c action { … }, SysML.xtext ActionBodyParameter) is the body itself, named or not |
parser/behavior.go parseActionBodyParameter (marks ast.Usage.IsBodyParameter); lower/action_graph.go lowerStatement (lowered to the block the usage's scope owns); runtime/statements.go block |
lower/action_body_test.go:TestActionBodyParameterLowersToItsBlock, conformance/action_named_loop_body_parameter.sysml |
✅ Faithful (a name only scopes the members it declares — loop action charging { … } until charging.done — so the body runs either way; an empty body parameter runs as an empty body) |
then done; — a final node as a successor target |
parser/behavior.go parseFinalNode (reached by a succession); lower/action_graph.go Finals; runtime/action_executor.go stepFinalNode |
parse/behavior_loop_until_succession.golden, conformance/action_standard_loop_until_then_done.sysml, negative_test.go:then_done_no_semicolon |
✅ Faithful (the token reaching it ends the flow, as for a declared done end;) |
A flow ending at a node with no outgoing succession (action a; action b; first a then b;) |
lower/action_graph.go Edges (the node has no outgoing edge); runtime/action_executor.go retireToken, called from stepActionExecutionNode, stepNestedAction, stepStatementNode |
conformance/action_last_node_without_a_succession.sysml + trace golden, conformance/action_flow_ending_at_a_statement.sysml, action_executor_test.go:TestActionExecutor_NodeWithoutSuccessorsRetiresItsToken, robustness_test.go:action_whose_last_node_has_no_succession |
✅ Faithful (Actions::Action gives every action a done snapshot it inherits, so a flow that runs out of successions ends there and the action completes with what its features hold — an explicit done end; is one way to write that end, not a requirement. A node reached by a fork retires its own branch; the action completes when the last token does) |
A first end naming a node the body declares (first s1 then s2;, first s1; with then s1 s2;, SysML.xtext TargetSuccession after first) starts the flow at that node |
lower/action_graph.go resolveFirstNode (the named node becomes Initial and holds the succession leaving it) |
lower/action_first_node_test.go:TestToActionGraph_FirstNamesADeclaredNode, :TestToActionGraph_FirstNamesADeclaredNodeSplit, :TestToActionGraph_FirstDeclaresItsOwnInitialNode, conformance/action_first_names_a_declared_node.sysml and _split.sysml + trace goldens, parse/behavior_first_then_declared_node.golden, robustness_test.go:first_node_with_a_second_succession, :first_beside_an_initial_node, :first_naming_a_final_node |
✅ Faithful (first marks which node the flow starts at; only a first end naming nothing declared — first start; — is an initial node of its own. A body states one start, so a second first end, or one beside an initial node of its own, is rejected; naming a final node is rejected because the flow cannot start where it ends) |
A guard on a succession out of a node that is not a decision (first s1 if c then s2;, SysML.xtext GuardedSuccession) |
lower/action_graph.go (the guard is carried on the edge); runtime/action_executor.go enabledSuccessions (every guard out of the node is evaluated before a token leaves it) |
conformance/action_succession_guard_holds.sysml, action_succession_guard_fails.sysml, action_succession_guard_two_branches.sysml, action_succession_guard_fork_branch_pruned.sysml, action_succession_guard_not_boolean.sysml, action_succession_guard_two_hold.sysml, robustness_test.go succession_guard_failure_modes |
✅ Faithful (a guard that does not hold prunes the succession, since TransitionPerformance::transitionLink is HappensBefore[0..1]; two consequences the notation leaves open are tool-defined and pinned in the goldens: a node whose every succession is pruned ends its flow, and two guards holding at once out of one node is reported rather than resolved — a fork prunes only the branches whose guard fails) |
else branch of a decision in an action flow (if c then a; else b;, SysML.xtext DefaultTargetSuccession) |
parser/behavior.go (the else clause builds *ast.ControlFlowEdge{IsElse: true}); lower/action_graph.go (the else edge is the guardless alternative); runtime/action_executor.go stepDecisionNode |
parse/behavior_decision_else.golden, conformance/action_decision_else_branch.sysml, conformance/action_decision_guarded_branch.sysml, negative_test.go:decision_else_no_target |
✅ Faithful (the else edge is taken when no guarded branch out of the decision holds) |
Qualified succession at namespace level (first part1::action1 then requirement1;) |
parser/namespace.go (a succession is a namespace member), parser/succession.go |
parse/behavior_namespace_succession.golden, negative_test.go:namespace_succession_no_target |
⚠️ Approximate (parsed and carried in the AST with both ends; a succession outside a behavior body has no token flow to lower into, so it is not executed) |
| A statement written directly among an action's own members is reported, not ignored | lower/action_graph.go ToActionGraph first pass, statementKeyword |
robustness_test.go:statement_directly_in_an_action_body |
✅ Faithful (a statement runs as part of an action node's body; written beside first/then it has no name a succession could reach, so the execution reports it instead of dropping it) |
| A body member that is not an executable statement is reported, not skipped | lower/action_graph.go Unsupported; runtime/statements.go execute (lower.Unsupported) |
lower/action_body_test.go:TestActionBodyUnexecutableMemberIsLowered, lower/block_graph_test.go:TestBlockStatingItsOwnEdgeKeepsItsStatements, robustness_test.go:loop_body_of_unexecutable_statement, :block_flow_of_unexecutable_member |
✅ Faithful (a member of a loop or branch body outside what a block's flow executes — a part declaration, an edge the block states itself — fails the execution instead of producing a wrong answer silently, including in a block that does state a flow) |
A block has a token flow of its own: a nested action declaration or a perform in a loop or branch body is a node of it |
lower/block_graph.go blockNeedsFlow, lowerBlockFlow, nestedActionBlock (Block.Graph, ActionGraph.StatementRuns); runtime/statements.go runBlock, blockFlow, blockNode; runtime/action_statements.go effect (a perform invokes the action it names) |
action_block_flow_nested_action.sysml, action_block_flow_perform_in_loop.sysml, action_block_flow_if_branch.sysml, calc_block_flow_early_return.sysml, all + trace goldens; lower/block_graph_test.go:TestBlockDeclaringAnActionNodeIsLoweredToAFlow, :TestBlockFlowNestsTwoLevels, :TestNestedActionParametersAreLowered; robustness_test.go:non_terminating_loop_performing_an_action; calc_block_flow_assigns_output.sysml (a walker over lowered blocks reads Block.Steps(), so an output assigned in a block's flow counts); calc_block_flow_result_in_block.sysml (a result written among a block's own nodes returns from the behavior around it, as it does in a statement body) |
⚠️ Approximate (the nodes of a block's flow succeed one another in declaration order — the order the block writes them is the only flow a block states — and each spends a step, so a non-terminating loop performing an action reports ErrStepLimitExceeded. A nested action declares parameters and a nested action of its own, to any depth; a perform reads the values in scope where it stands and its outputs come back to them, so one in a loop body runs once per iteration. A return reached inside such a node ends the enclosing behavior. A block cannot state successions, forks or joins between its own nodes: an edge written in a block leaves its members in statement form, reported when reached) |
| Send statement (message passing) | lower/action_graph.go lowerBody (FeaturePath keeps the whole target path); runtime/signal.go buildMessage, post, postTo, resolveAddress, namedAddress/qualifiedAddress, featureAddress, addressOwner, portAddress/receiverAddress/objectAddress, PostMessage (deliveryOf), Message.reaches; runtime/invoke_action.go invokeAction (a performed action runs as the object performing its caller) |
action_send_accept.sysml, send_identity_same_named_ports.sysml, port_identity_own_port.sysml, send_identity_unroutable_target.sysml, lower/action_body_test.go:TestActionBodyLowering, lower/connection_test.go:TestLowerSendKeepsAddressedPath, signal_test.go:TestActionMessageReachesStateMachine, :TestAddressedSendStaysWithinTheSendingObject, :TestAddressedSendResolvesPortOfNamedObject, :TestAddressedSendDescendsToNestedPort, :TestAddressedSendToUnreachablePortIsTyped, send_identity_addressed_part.sysml, send_identity_performed_object.sysml, signal_test.go:TestDeliveryHoldsAConsumerToTheWholeDestination, :TestPerformedBehaviorRunsAsItsPerformer, :TestAddressedSendToQualifiedElementOfATwinObject, :TestInjectedMessageIsHeldToTheDestinationItNames, robustness_test.go:injected_message_names_a_receiver_no_accept_has |
✅ Faithful (a message is typed by what was sent and delivered by object identity: a target is resolved through the instance graph to the object owning it and the port path within it, and the destination is built whole or refused with UnroutableSendError, so a consumer takes a message only by satisfying every part of it — a same-named port or receiver of another object, or a sibling of the sender, never sees it. A behavior an object performs, however deeply, presents that object's identity, so only a behavior no object performs at all has none. A qualified target takes its object from its qualifier, and a message injected from outside the model is held to the destination its fields name) |
| Accept action (message consumption suspends the action) | action_executor.go stepNestedAction accept case (parks the token as Token.Wait), Step (StateWaiting), RunToCompletion, deadlockError; executor_common.go AcceptWait; runtime/signal.go TakeMessage |
action_accept_suspends_until_message.sysml + trace golden, action_accept_two_waiters.sysml + trace golden, action_send_accept.sysml, action_accept_message.sysml, signal_test.go:TestAcceptParksTokenUntilMessageArrives, :TestParkedAcceptTakesOnlyItsOwnMessage, robustness_test.go:accept_deadlock_never_satisfied, :accept_deadlock_reports_every_waiting_accept, :send_reaches_only_its_addressee, :accept_of_unsent_type, :send_via_unconnected_port |
⚠️ Approximate (an accept with no message it can take suspends the action at that node and resumes when one arrives, from a parallel branch or from another executor sharing the context; a run whose every remaining token is parked reports ErrAcceptDeadlock rather than hanging. Suspension is bounded by the executor: a nested action invoked synchronously, and an action driven by RunToCompletion, cannot wait for a message posted after the call begins) |
| An accept node's payload is visible by simple name to the other nodes of the same action body, and a nearer declaration shadows it (KerML 8.2.3.5.3) | resolve/accept_payload.go acceptPayload/acceptPayloadsIn, consulted by resolve/unqualified.go walkUnqualifiedHiding for the scope the accept node is declared in; runtime/action_executor.go binds the accepted value under the same name |
resolve/accept_payload_test.go:TestAcceptPayloadVisibleToSiblingNode, :TestAcceptPayloadTwoAcceptsInOneBody, :TestAcceptPayloadVisibleInNestedBody, :TestAcceptPayloadVisibleBeforeDeclaration, :TestAcceptPayloadShadowsOuterFeature, :TestAcceptPayloadUnresolvedStillReported, :TestAcceptPayloadDoesNotEscapeBody, conformance/accept_payload_nested_body.sysml, accept_payload_shadows_outer_feature.sysml, accept_payload_read_before_accept.sysml + trace golden, robustness_test.go:accept_payload_read_before_it_is_bound |
⚠️ Approximate (the payload is a parameter of the accept node, which KerML scoping does not make a member of the enclosing body, so the body's shared feature space is modelled by contributing the payload to the scope the accept node is declared in: every node resolving through that scope reads it, a nearer declaration still wins, and the payload neither escapes the body nor is reachable as A::msg. It is not offered by LSP completion, which lists a scope's own members) |
Send through a port (send x via p) routes over the connectors of the behavior and of the part performing it, to the ends that can receive the message: the direction of the port's flow features decides that, conjugated where a ~P types the end (SysML v2 §7.12.2, §7.15, §7.16). An end is joined as the whole path it was written with, so a nested p.q is itself and not a same-named port elsewhere. A send that reaches no receiving end is a typed error, not a message dropped |
lower/connection.go lowerConnections, FeaturePath (ends and the scope they resolve in); runtime/routing.go routableConnections, performerConnections, enclosingPart, receivingEnds, endReceives, portSymbol, UnroutableSendError; runtime/signal.go postVia, reaches; semantics/conjugation.go PortFeatures |
port_direction_conjugation.sysml, port_nested_port_path.sysml, port_interface_typed_connection.sysml, send_no_reachable_receiver.sysml, send_into_outbound_only_end.sysml, action_port_communication.sysml + trace golden, runtime/routing_test.go (conjugated end, outbound-only end, unjoined port, nested path, performing part's ports with and without an instance, part-to-behavior connector, interface-typed connection), signal_test.go:TestSendViaPortReachesConnectedAccept, robustness_test.go:send_via_unconnected_port |
⚠️ Approximate (routing honors direction, conjugation and the performing part's ports; a port declaring no flow features constrains no direction and so receives in either direction, and an end whose path this run cannot resolve is treated as able to receive rather than reported here. The performer is the object performing the behavior, or the part the behavior is declared in when no object performs it — a part reached through a chain of enclosing parts contributes only its own connectors) |
Accept through a port (accept msg : T via p) |
lower/action_graph.go acceptPort; runtime/action_executor.go stepNestedAction accept case |
action_port_communication.sysml, lower/connection_test.go:TestLowerAcceptRecordsViaPort, signal_test.go:TestPortRoutedMessageBypassesPortlessAccept, :TestAddressedMessageBypassesPortAccept |
✅ Faithful (an accept on a port takes only messages routed to that port, and an accept on none takes only addressed messages) |
An accept node is an action node (SysML.xtext ActionNode), so it stands wherever a statement does — a member of an action body, a statement of a loop or branch body, or the member a then sequences (then action engineStopped accept engineOff : EngineOff;) — and the accepting action's name stays distinct from the received payload's (engineStarted vs engineStart), both registered and resolvable |
parser/behavior.go parseActionMember, startsInlineSuccessionStatement, atAcceptNode, parseAcceptNode (declaration name, payload parameter, optional via port) |
parse/accept_action_statement.golden, runtime/testdata/conformance/accept_statement_via_port.sysml, parser/negative_test.go (accept_statement_no_payload, accept_statement_no_payload_type, accept_statement_via_no_port, then_accept_no_payload), robustness_test.go:accept_statement_deadlock_in_a_loop, lower/action_body_test.go:TestAcceptInALoopBodyIsLoweredAsUnsupported |
⚠️ Approximate (parsed, resolved and executed as an action-body node; an accept written in a loop or branch body would have to suspend a flow that has no token to park, so it is lowered as Unsupported and reported when reached rather than passed over — suspending a block's flow is not implemented) |
| Object flow (pin-to-pin data) | action_executor.go:673 applyDataFlows |
action_output.sysml |
✅ Faithful |
| Succession edges | lower/action_graph.go:ToActionGraph |
action_control_flow.sysml |
✅ Faithful |
| Deadlock detection | action_executor.go:72 Step |
action_executor_test.go:TestActionExecutor_Deadlock_JoinStarvation |
✅ Faithful |
| Step budget enforcement | context.go incrementStep; budget configured by SYSML_MAX_STEPS (budget.go BudgetsFromEnv) |
robustness_test.go:testStepBudgetExceeded, budget_test.go:TestRaisedBudgetRunsLongerLoop |
✅ Faithful (the reported limit is the effective one, and names the variable that raises it) |
State Machine (SysML v2 States — Systems Library/States.sysml, over KerML StatePerformances)¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
| Initial state identification | lower/state_graph.go:ToStateGraph; state_executor.go:686 initialize |
state_simple.sysml |
✅ Faithful |
A succession out of a state body's own entry subaction names the state it starts in (entry; then off;), the same as initial start; start then off; |
lower/state_graph.go collectTransitions SuccessionEdge case + isEntrySubaction |
lower/state_notation_test.go:TestToStateGraph_EntrySuccessionNamesInitialState, state_entry_succession_initial.sysml conformance, parser/testdata/parse/behavior_exhibit_state_body.golden |
✅ Faithful |
A transition out of a state body's own entry action names the state it starts in (entry action initial { } transition initial then off;), the entry action standing in for a start pseudostate (SysML v2 §7.19.3) |
ast/state_entry.go EntryActions/StateEntryActions/IsEntryAction; resolve/transition.go startAction; passes/state_transition.go machine.startActions; lower/state_graph.go startsAt |
resolve/transition_test.go, passes/state_transition_test.go:TestTransitionOutOfEntryActionIsLegal, lower/state_notation_test.go, state_entry_action_transition_initial.sysml conformance |
✅ Faithful (an ordinary action named as an endpoint is still reported) |
| Final state termination | state_executor.go:288 processNextEvent |
state_simple.sysml |
✅ Faithful |
| State entry actions | state_executor.go:749 enterState |
state_do_behavior.sysml |
✅ Faithful |
| State exit actions | state_executor.go:810 exitState |
state_transition_effect.sysml |
✅ Faithful |
An entry/do/exit action given by reference (entry warmUp;) is a performed action usage subsetting the referenced action (StateActionUsage → PerformedActionUsage → PerformActionUsageDeclaration) |
parser/behavior.go parseStateSubaction; parser/defusage.go parsePerformedActionReference |
parser/testdata/parse/state_subaction_reference.golden, parser/state_subaction_test.go, parser/negative_test.go:entry_reference_no_semicolon, resolve/state_subaction_test.go, runtime/state_behavior_test.go:TestStateSubactionByReferencePerformsAction |
✅ Faithful |
| State do behavior runs while its state is active, one action per round | state_executor.go startDoActivity, runDoRound |
state_do_behavior.sysml, state_do_activity_test.go |
✅ Faithful |
An entry, do or exit behavior written as an inline action body (entry action { … }, do action named { … }, exit action { … }) executes the statements it states, locals and loops among them, in any nesting of composite states; an empty body is a behavior that does nothing |
lower/state_behavior.go LowerBehaviors/lowerStateBehavior (the body is lowered to a Block, its locals in the block's own frame), lower/state_graph.go StateGraph.Behaviors; runtime/state_statements.go executeBehavior/stateStmtHost |
parser/testdata/parse/state_anonymous_action_body.golden, state_anonymous_action_body.sysml + trace golden (entry/do/exit ordering, nesting, empty bodies), robustness_test.go:testEmptyAnonymousActionBody, :testNonTerminatingAnonymousDoBody (ErrStepLimitExceeded) |
✅ Faithful |
An inline body is one action, so a do round runs it to its end: orthogonal regions interleave between rounds, not inside a body. The one-action-per-statement do { … } form is what interleaves statement by statement |
runtime/state_statements.go executeBehavior; state_executor.go runDoRound |
state_anonymous_do_atomic.sysml + trace golden (123456), against state_concurrent_do.sysml (124356) |
✅ Faithful |
A statement of an inline body may perform an action (entry action { assign c := c + 1; perform Bump; }), the performed action being lowered as an effect rather than an unsupported usage |
lower/action_graph.go lowerStatement (an action usage naming what it performs → Effect{EffectPerform}); runtime/state_statements.go stateStmtHost.effect |
state_anonymous_body_perform.sysml conformance |
✅ Faithful |
| A behavior that both performs an action and states a body of its own is reported rather than one of the two being chosen silently | lower/state_behavior.go lowerStateBehavior (Unsupported) |
robustness_test.go:testBehaviorPerformingAnActionAndStatingABody |
⚠️ Approximate (the form is rejected at execution, not at parse: whether SysML gives it a meaning is unadjudicated) |
| Concurrently active states interleave their do behaviors, in region declaration order | state_executor.go runDoRound, orderedActiveRegions |
state_concurrent_do.sysml + trace golden |
✅ Faithful |
| Exiting a state abandons the rest of its do behavior | state_executor.go exitState, stopDoActivity |
state_do_activity_test.go:TestDoBehaviorIsCancelledWhenItsStateIsExited |
✅ Faithful |
| A state completes only once its do behavior has finished | state_executor.go scheduleCompletionTransitions |
state_do_activity_test.go:TestCompletionWaitsForTheDoBehavior |
✅ Faithful |
| Deferred events retained while a deferring state is active, delivered afterwards in arrival order | parser/behavior.go parseDeferMember (defer <event>[, <event>]*;); lower/state_graph.go stateNodeFromUsage, collectDeferred; state_executor.go defersEvent, recallDeferredEvents |
parser/testdata/parse/state_defer.golden, parser/state_notation_test.go:TestDeferMemberParsing, lower/state_notation_test.go:TestToStateGraph_DeferNotation, state_deferred_event.sysml + state_undeferred_event.sysml conformance, state_deferred_test.go |
✅ Faithful |
Earliest transfer first (Occurrence::incomingTransferSort defaults to earlierFirstIncomingTransferSort): a recalled event is dispatched before the events that arrived while it was deferred, and a completion event before either |
executor_common.go eventHeap.Less, isCompletionEvent; state_executor.go recallDeferredEvents |
state_deferred_test.go:TestRecalledEventPrecedesLaterArrivals |
✅ Faithful |
A transition out of a composite state is enabled while any of its substates is active: matching walks outward from every active leaf through the lowered containment (StateGraph.ParentState), the innermost enabled transition wins for the same event, and a false guard does not consume it |
state_executor.go broadcastEvent, selectTransitions, losesToNestedTransition, nestedIn, activeLeaves, fireFrom, enabledTransition, acceptsSignal/acceptsSignalFrom (an enclosing state's accept takes a signal off the bus) |
conformance/state_composite_outer_transition.sysml, :state_composite_inner_priority.sysml, state_composite_transition_test.go:TestCompositeStateHandlesEventItsSubstateDoesNot, :TestFalseGuardInsideACompositeStateFallsOutward, :TestTransitionOutOfAnIntermediateCompositeStateKeepsItsOwnerActive, :TestOuterCompositeStateStillHandlesItsEventAfterASubstateMoved, robustness_test.go:signal_no_level_of_a_composite_state_accepts |
✅ Faithful |
| Taking a transition out of a composite state exits the states being left innermost-first, then runs the effect, then enters the target; leaving a composite with orthogonal regions exits every active region first | state_executor.go transitionToInto (exit to the least common ancestor), exitStates/exitedByAncestorRegion (a state whose region another state being left owns is exited by that state's recursive region teardown, so the chain walk does not exit it a second time), state_region_transition.go leaveRegion (which instead clears the region a state is active in, recording its history, before exiting that state, since a region's active state may be nested below it), moveBetweenRegions, exitRegionTo, leaveTopRegions |
conformance/state_composite_exit_order.sysml + trace golden (two levels of nesting), :state_composite_orthogonal_exit.sysml + trace golden, :state_composite_nested_regions_exit_once.sysml + trace golden (a region holding a composite with a region of its own: every level exits exactly once), robustness_test.go:exit_of_nested_regions_with_a_history_pseudostate |
✅ Faithful |
| A transition out of a composite state is external even when its target is that state itself or one of its substates: the source composite is exited and re-entered around the effect, and its regions restart at their initial states | state_executor.go transitionToInto and state_region_transition.go moveBetweenRegions (the exit boundary is the source's parent where the source encloses the target, so a composite self-transitioning inside an orthogonal region is exited too, while its sibling regions stay as they were), encloses, isComposite |
conformance/state_composite_self_transition.sysml + trace golden (exit substate, exit composite, effect, re-enter composite and its region), :state_composite_to_substate.sysml + trace golden, :state_composite_self_transition_in_region.sysml + trace golden, state_composite_transition_test.go:TestOneEventTakesACompositesTransitionOnce, robustness_test.go:composite_self_transition_with_no_substate_to_re_enter |
⚠️ Approximate (a composite state stating no starting substate — no initial and no region — is re-entered with no substate active, since nothing names the substate to enter; a self-transition on a simple state does not exit and re-enter it) |
| An event reaches only the regions still active when it is dispatched | state_executor.go broadcastEvent (each active leaf is re-checked before it is offered the event) |
state_deferred_test.go:TestExitedNestedRegionDoesNotReactToTheSameEvent |
✅ Faithful |
| Run-to-completion bounds one dispatch: only the leaves active when the event was taken off the queue are offered it, each taking at most one transition, so a state the event entered does not react to it as well | state_executor.go broadcastEvent, selectTransitions (the transitions are all selected against the configuration the event was dispatched for, then fired; leaves in sibling regions of one composite state select its transition once between them, and the dispatch ends once a final state completes the machine) |
state_composite_transition_test.go:TestOneEventTakesOneTransitionPerActiveLeaf, :TestOneEventTakesACompositesTransitionOnce |
✅ Faithful |
| Concurrent regions react to one event in region declaration order, whatever depth their active state sits at, and a transition one region takes disables only the states it left | state_executor.go broadcastEvent (candidates are ordered by the active leaves, which activeLeaves sorts by regionPath, the declaration index of every region between the machine and the leaf, rather than by depth), losesToNestedTransition |
conformance/state_composite_region_depth_order.sysml + trace golden (the deeper leaf's region is declared second and reacts second), :state_composite_region_deeper_first.sysml + trace golden (the mirror: it is declared first and reacts first), state_composite_transition_test.go:TestOneRegionsInnerTransitionLeavesAConcurrentRegionsOwnTransition |
✅ Faithful |
| A time trigger on a composite state counts from entering that state and fires while a substate is active; a substate moving does not restart it, and leaving the state destroys it, so re-entering the state times the whole interval again | state_executor.go scheduleTransitionEvents (the enclosing states of every active leaf are scheduled too), scheduleTimeTransitions, exitState (drops the state's timers and withdraws the time events they queued, through executor_common.go (*EventQueue).Withdraw), dispatchEvent (the expired timer is un-marked so a transition staying in its source re-arms it, and an event whose source was left is dropped) |
conformance/state_composite_outer_time_trigger.sysml, conformance/state_composite_region_time_trigger.sysml + trace goldens, :state_time_trigger_restarts_on_re_entry.sysml + trace golden (a state left before its timer expires fires only a full interval after it is re-entered, timed against a sibling region's clock), robustness_test.go:stale_composite_timer_in_a_region, state_composite_transition_test.go:TestTimedSelfTransitionFiresEveryPeriod (a timed self-loop fires once per period) |
✅ Faithful (a composite inside an orthogonal region fires region-locally: dispatchEvent routes through fireFrom, so the sibling regions stay as they were) |
| A change condition on a composite state is watched while a substate is active; a poll resolves the transitions it enables exactly as the equivalent signal does — innermost wins, one transition per region, a shared transition once — and a condition whose guard blocks it consumes nothing | state_change_trigger.go pollChangeEvents (selects through the shared selectCandidates and resolves conflicts with losesToNestedTransition, the same two steps broadcastEvent uses), observeChangeConditions, risenChangeTransition (the guard is evaluated before the transition is selected, so a blocked one leaves the condition watched by the states outward of it and by the other leaves), state_executor.go fireFrom |
state_composite_transition_test.go:TestChangeConditionOnACompositeStateFiresWhileASubstateIsActive, :TestGuardBlockedChangeConditionDoesNotSilenceTheOtherRegions, :TestChangeConditionTakesTheInnermostTransitionOnly, state_change_trigger_test.go:TestChangeTriggerConsumesTheRiseForALosingTransition |
✅ Faithful (RunToCompletion polls, so a when transition on a composite state fires without an external caller; one rise is one occurrence, so a transition that lost the conflict waits for the next rise rather than firing on the next poll) |
| Deferral by an ancestor state and across orthogonal regions | state_executor.go defersEvent |
state_deferred_test.go:TestCompositeStateDefersForItsSubstates, TestDeferralSpansOrthogonalRegions |
✅ Faithful |
| Deferring a non-dispatchable trigger reports | lower/state_graph.go collectDeferred |
robustness_test.go:defer_of_non_deferrable_trigger |
✅ Faithful |
| A transition's source and target name vertices of the machine they are written in, resolved by the name-resolution tier so a misspelled endpoint reports with the other name diagnostics rather than when the machine is lowered | resolve/transition.go (*Resolver).ResolveEndpoint (reported from resolve/document.go TransitionMember), consumed by lower/state_graph.go (*StateGraph).vertex via lower.Endpoints |
resolve/transition_test.go:TestResolveEndpointsThatNameVertices (sibling, nested, sibling region, entry/exit point, sourceless accept ... then), :TestResolveEndpointMisspelledIsReportedWithASuggestion, :TestResolveEndpointNotAVertexIsReported, :TestEndpointLookupForLoweringReportsNothing, state_transition_endpoint_qualified.sysml conformance, robustness_test.go:state_transition_endpoint_misspelled, :state_transition_endpoint_never_resolved |
⚠️ Approximate (a qualified endpoint resolves like any other name; an unqualified one, and one whose owners name only some of the scopes between, fall back to the first vertex of the machine whose name path ends in it, in declaration order, which is what the notation's leniency allows and what two vertices of the same name in sibling regions are told apart by only when the endpoint qualifies them. An endpoint naming no vertex leaves its edge out of the graph rather than failing the lowering, so a machine whose model was never resolved — the REPL and the gRPC handlers build a fresh resolver — still runs; an endpoint that resolves but names no vertex of this machine is reported by the state transition check, see the dangling-transition rows) |
| Transition firing | state_executor.go:535 fireTransition |
state_transition_effect.sysml |
✅ Faithful |
| Transition guard evaluation | state_executor.go:218 scheduleTransitionsForState |
state_choice_pseudostate.sysml |
✅ Faithful |
Transition effect actions, whether written as a statement (do assign x := 1) or as a performed action (do perform Bump) |
lower/state_behavior.go LowerBehaviors (the membership a performed action is contributed through is unwrapped and each behavior is lowered to statements); state_executor.go:535 fireTransition → state_statements.go executeBehavior |
state_transition_effect.sysml, state_transition_effect_perform.sysml conformance |
✅ Faithful |
| AcceptEvent triggers (when signal) | state_executor.go matchesEvent |
state_signal_discriminate.sysml |
✅ Faithful |
| Signals sent from state behaviors reaching the machine | state_statements.go stateStmtHost.send, state_executor.go deliverPendingSignal |
state_send_self_signal.sysml + trace golden, signal_test.go:TestSendOfNamedTypeReachesStateMachine |
✅ Faithful |
A signal in flight on the context bus is dispatched by a single step as well as by a run to completion, so the REPL debugger and RunToCompletion agree |
state_executor.go ProcessNextEvent, acceptableMessage, HasPendingSignal, HasPendingWork; repl/meta.go %advance |
repl/runtime_commands_test.go:TestAdvanceDeliversPendingPortSignal, state_transition_accept_via_port.sysml |
✅ Faithful |
CallEvent triggers (accept op(param) notation, operation and argument matching, arguments bound for guard/effect) |
parser/behavior.go parseTriggerEvent/parseCallEvent; symbols/bodyscopes.go newTriggerScope (parameters visible to the transition's own guard/effect); state_executor.go matchesEvent EventCall case, bindTriggerArguments, InvokeOperation |
parser/testdata/parse/state_call_trigger.golden, lower/trigger_test.go:TestTriggerClassification_CallTrigger, model/behavior_body_resolve_test.go call-trigger parameter cases, state_call_trigger{,_guard,_nested,_regions}.sysml conformance, signal_test.go:TestCallEventMatchesOperationName, :TestRejectedCallLeavesNoArgumentsBehind, robustness_test.go:call_of_unhandled_operation, :call_argument_of_wrong_type |
✅ Faithful (a call trigger on an enclosing composite state sees the invocation while a substate is active) |
Sourceless transitions (accept...then) |
lower/state_graph.go:487 collectTransitions Usage case, :302 resolve container |
accept_then_transition.sysml |
✅ Faithful (nested form only; flat form errors intentionally) |
| ChangeEvent triggers (when expr) | state_executor.go matchesEvent, RunToCompletion (polls after each micro-step and again at quiescence); state_change_trigger.go pollChangeEvents, SuspendReason |
state_executor_test.go:TestStateChangeEvent, state_change_trigger_test.go:TestChangeTriggerRunsWithoutAnExternalPoll, :TestChangeTriggerFiresOnRiseFromDoBehavior, :TestChangeTriggerDoesNotRefireUnchangedCondition, :TestChangeTriggerFalseConditionIsReported, conformance/state_change_trigger_autonomous.sysml, :state_change_trigger_rising_edge.sysml, :state_change_trigger_event_order.sysml + trace golden |
⚠️ Approximate (driven by the run itself and fired on the condition rising; KerML has no clock, so re-testing once per micro-step is a tool-defined cadence — see the known limitation) |
TimeEvent triggers (accept after <duration> relative, accept at <time> absolute) |
parser/behavior.go parseAcceptTransition; state_executor.go scheduleTransitionEvents, scheduleTimeTransitions, matchesEvent; state_time_trigger.go timeMagnitude (a duration carrying a unit is converted to the clock's SI::s) |
state_timed_triggers.sysml golden, state_timed_transitions.sysml conformance, state_executor_test.go:TestStateExecutor_AbsoluteTimeEvent, state_time_trigger_test.go:TestTimeTriggerUnitIsConverted, :TestTimeTriggerSubSecondUnit, :TestTimeTriggerAbsoluteInstantWithUnit, :TestTimeTriggerRejectsNonTimeDimension, conformance/state_time_quantity_seconds.sysml, :state_time_quantity_instant.sysml, :state_time_quantity_unit_ordering.sysml + trace golden, robustness_test.go:non_numeric_time_trigger |
✅ Faithful |
| Signal discrimination | state_executor.go:401 matchesEvent signal name |
state_signal_discriminate.sysml |
✅ Faithful |
| Unmatched signal dropped | state_executor.go matchesEvent |
state_signal_unmatched.sysml |
✅ Faithful (an injected event no transition matches is dropped; a message on the bus no active transition accepts is left in flight for another consumer, signal_test.go:TestStateMachineLeavesForeignSignalPending) |
| Hierarchical substates | state_executor.go:131 getParentChain, :147 getLCA |
state_orthogonal_regions.sysml |
✅ Faithful |
| Orthogonal regions | state_executor.go broadcastEvent, state_region_transition.go fireTransitionInRegion; region order from lower.StateGraph.TopRegions and CompositeStates |
state_orthogonal_regions.sysml, region_pseudostate_test.go:TestRegionPseudostateExitOrderIsDeterministic |
✅ Faithful |
| Choice pseudostates | state_region_transition.go pseudostateBranch (guards in declaration order) |
state_choice_pseudostate.sysml, state_region_choice.sysml |
✅ Faithful |
A succession (a then b;, initial s; s then a;) names its endpoints the way a transition does, so it reaches a nested, region-local or qualified vertex, and a pseudostate as well as a state |
lower/state_graph.go collectTransitions (UsageSuccession, SuccessionEdge and InitialNode cases) → (*StateGraph).endpointVertex/endpointState, over the same lower.Endpoints a transition's endpoints resolve through |
lower/state_graph_nested_test.go:TestSuccessionReachesAPseudostate, :TestSuccessionQualifiedTargetNamesTheVertexItQualifies, :TestToStateGraph_EntrySuccessionNamesInitialState |
✅ Faithful (previously matched the endpoint's last name segment against a flat state list, which reached no pseudostate and could bind a same-named vertex of another state) |
| Two regions may declare same-named pseudostates, and each is a vertex of its own | lower/state_graph.go StateGraph.Pseudostates (declaration-ordered slice, not keyed by name), addPseudostate |
lower/state_graph_nested_test.go:TestSameNamedPseudostatesInSiblingRegionsAreBothCollected |
✅ Faithful |
| Junction pseudostates | state_region_transition.go pseudostateBranch |
state_junction_pseudostate.sysml |
✅ Faithful (evaluated when entered, like a choice, rather than statically before the incoming transition) |
| Fork pseudostates (bypass targeted regions' initial states) | state_executor.go:706 fireForkTransition, :1028 enterStateInto |
state_fork_join.sysml golden, state_fork_join_pseudostate.trace.golden, fork_join_test.go:TestForkBypassesTargetedRegionInitials |
✅ Faithful |
| Join pseudostates | state_executor.go:782 fireJoinTransition, :827 joinSources (declaration order) |
pseudostate_test.go:TestJoinWaitsForEveryBranch, fork_join_test.go:TestForkJoinVisitOrderIsDeterministic |
✅ Faithful |
| Entry/exit point pseudostates | parser/behavior.go parseStateMember (entry point <name>; / exit point <name>;, point matched contextually); state_region_transition.go pseudostateTarget (routed like a junction) |
parser/testdata/parse/state_history_entry_exit.golden, parser/state_notation_test.go:TestHistoryAndPointPseudostateParsing, :TestPointIsNotReserved, state_entry_exit_points.sysml conformance, pseudostate_test.go:TestEntryAndExitPointPseudostates, region_pseudostate_test.go:TestRegionPseudostateExitRecordsHistory |
✅ Faithful |
| History pseudostates (shallow and deep). A region is recorded per region, in the state it was left in, so a region left by a transition that started inside its own composite state's region is restored to that composite state and its inner configuration rather than to the region's initial state; a region left with no active state at all has nothing to restore | parser/behavior.go parseStateMember (history <name>;, shallow history <name>;, deep history <name>;); state_executor.go fireHistoryTransition, :deepestRecorded, exitState (records the configuration left), :recordRegionHistory, :forgetRegionHistory, state_region_transition.go leaveRegion, exitRegionTo, lower/state_graph.go PseudostateOwner |
parser/testdata/parse/state_history_entry_exit.golden, parser/state_notation_test.go:TestHistoryAndPointPseudostateParsing, lower/state_notation_test.go:TestToStateGraph_HistoryAndPointNotation, state_shallow_history.sysml, state_deep_history.sysml, state_history_revisit.sysml + trace golden, state_deep_history_region_composite.sysml, history_test.go:TestShallowHistoryRestoresLastSubstate, :TestDeepHistoryRestoresInnermostSubstate, :TestHistoryRestoresOrthogonalRegions, :TestDeepHistoryRestoresBelowRegion, :TestDeepHistoryRestoresARegionLeftFromInsideItsCompositeState, :TestHistoryTakesDefaultTransitionWhenUnvisited, robustness_test.go:history_outside_composite_state, :history_without_record_or_default |
✅ Faithful |
| Composite state with regions entered by a plain transition | state_executor.go transitionToInto (keeps the region configuration entering it just built) |
history_test.go:TestHistoryRestoresOrthogonalRegions |
✅ Faithful |
| Leaving a composite state exits only its own regions | state_executor.go exitState (scoped to CompositeStates[state]) |
history_test.go:TestExitingNestedRegionsKeepsSiblingRegions |
✅ Faithful |
A transition between two regions of one composite state exits its source only: KerML StatePerformances::StateTransitionPerformance orders private succession [*] guard then [1] transitionLinkSource.exit, so the composite state is neither exited nor re-entered, the source's region is left without an active state, and the regions holding neither endpoint keep theirs. A target nested inside the target region's own composite state moves that inner region, exiting the state it was running. A region whose target is nested inside a composite state it is not running records that composite state as its active one. A source active in a region nested deeper than its target's region leaves its region set up to the level the two share, exiting the composite state holding its own region. The level is found by walking outward through the region declaring the region owner's nearest region-declared ancestor, so a region owned by a plain substate is not missed. A target outside the composite state still exits it and its regions — each state on the way out exactly once, the region a state is active in being cleared before that state is exited, since a region's active state may be nested below it — and a nested non-orthogonal transition still exits up to the endpoints' least common ancestor |
state_region_transition.go fireTransitionInRegion, concurrentRegionsFor, enclosingRegion, siblingRegionContaining, moveBetweenRegions, exitRegionTo, leaveRegion, isBelowOrEqual; state_executor.go getLCA (nested and outward transitions only) |
state_transition_cross_region.sysml + trace golden, state_transition_cross_region_third_region.sysml, state_transition_cross_region_nested_target.sysml, state_transition_cross_region_inactive_wrapper.sysml, state_transition_cross_region_deep_source.sysml, state_transition_cross_region_substate_owner.sysml, state_transition_leave_composite_substate_region.sysml, state_transition_sibling_region.sysml, state_region_cross_pseudostate.sysml + trace golden, cross_region_transition_test.go:TestCrossRegionTransitionExitsSourceOnly, :TestCrossRegionTransitionIntoNestedTargetExitsTheAbandonedState, :TestCrossRegionTransitionIntoInactiveCompositeRecordsTheEnteredState, :TestCrossRegionTransitionFromDeeperRegionExitsUpToTheSharedLevel, :TestCrossRegionTransitionFromARegionOwnedByASubstate, :TestNestedTransitionExitsUpToTheLCA, :TestTransitionOutOfCompositeStateExitsEveryRegion, :TestTransitionOutOfCompositeStateExitsNestedStatesOnce, region_pseudostate_test.go:TestRegionPseudostateIntoSiblingRegionExitsSourceOnly, robustness_test.go:state_cross_region_transitions_ping_pong |
✅ Faithful |
| Nested substates of a composite state declared textually | lower/state_graph.go stateNodeFromUsage (carries substates and nested pseudostates into the graph) |
lower/state_graph_nested_test.go:TestToStateGraph_NestedPseudostateOwner |
✅ Faithful |
| Choice/junction/entry/exit reached from inside an orthogonal region | state_region_transition.go fireTransitionInRegion, moveBetweenRegions, leaveRegion, pseudostateTarget |
state_region_choice.sysml, state_region_exit_pseudostate.sysml, state_region_cross_pseudostate.sysml + their trace goldens, region_pseudostate_test.go, robustness_test.go:region_pseudostate_without_satisfied_guard, :region_pseudostate_cycle |
✅ Faithful (a branch staying in the source region moves only that region; one into a sibling region of the same composite state exits the source only; one leaving the composite state exits the region set in declaration order, recording history on the way out) |
| Pseudostate chains (a pseudostate routing into another) | state_region_transition.go pseudostateTarget (cycle detected) |
region_pseudostate_test.go:TestRegionLocalJunctionChainIsFollowed, robustness_test.go:region_pseudostate_cycle |
✅ Faithful |
| Nested action invocation in entry/do/exit/effect | state_executor.go:1075 executeAction, invoke_action.go invokeAction |
state_behavior_test.go:TestStateDoExitAndTransitionEffectPerformAction |
✅ Faithful |
| Run-to-completion semantics | state_executor.go:288 processNextEvent |
state_executor_test.go:TestStateRunToCompletion |
✅ Faithful |
| Event queue management | state_executor.go:1127 EventQueue |
state_executor_test.go |
✅ Faithful |
| Deterministic dispatch order | executor_common.go eventHeap.Less (time, then arrival), state_executor.go orderedActiveRegions (region declaration order) |
state_call_trigger_regions.sysml |
✅ Faithful |
A transition names exactly one source and one target vertex, both of the machine it is written in (TransitionUsage::source: ActionUsage[1..1], ::target: ActionUsage[1..1] in the SysML v2 metamodel bundled as stdlib/Systems Library/SysML.sysml; KerML TransitionPerformances::TransitionPerformance takes one transitionLinkSource: Performance[1] and one transitionLink: HappensBefore[0..1]) |
passes/state_transition.go StateTransitionPass.Run → (*transitionChecker).checkEndpoint, over the vertices lower/vertices.go VertexDecls collects with the lowering's own collectVertices; lower/state_graph.go (*StateGraph).vertex keeps the typed construction error as the backstop |
passes/state_transition_test.go:TestTransitionTargetInSiblingRegionIsLegal, :TestTransitionTargetInSiblingRegionKeywordIsLegal, :TestTransitionTargetInUnrelatedMachineIsIllegal, :TestSuccessionTargetInUnrelatedMachineIsIllegal, :TestTransitionToEntryPointIsLegal, :TestTransitionTargetResolvingToNonVertexIsIllegal, :TestSourcelessAcceptTransitionIsLegal, :TestTransitionToFirstMarkerIsIllegal, :TestTransitionToFinalStateIsLegal, :TestStateUsageMachineIsChecked, conformance/state_transition_sibling_region.sysml, robustness_test.go:state_transition_endpoint_in_another_machine, :state_transition_endpoint_naming_a_first_marker |
✅ Faithful (a vertex of a sibling orthogonal region, an entry/exit point of a composite state and the sourceless accept … then form are legal; a vertex of another machine, a first/then marker named as a target and an endpoint resolving to a non-vertex are reported — the last of them by endpoint resolution, which owns it. A marker named as a source is left to lowering: see the row below) |
A routing pseudostate has a transition out of it, so a transition reaching it does not terminate nowhere (SysML v2 has no pseudostate notation or semantics; choice/junction/fork/join are the documented OpenSysML extension of docs/reference/grammar/README.md, whose reference semantics is UML 2.5.1 §15.7.18) |
passes/state_transition.go (*transitionChecker).checkMachine, routingPseudostate |
passes/state_transition_test.go:TestJunctionChainTerminatingNowhereIsIllegal, :TestJunctionWithOutgoingTransitionIsLegal, :TestJunctionLeftBySuccessionIsLegal, robustness_test.go:state_junction_without_an_outgoing_transition |
✅ Faithful (choice, junction, fork and join only; a history, entry or exit point is excluded, since what it resumes or delegates to need not be written as a transition out of it. The chain reaching such a pseudostate is acyclic, so state_region_transition.go cycle detection does not find it) |
A first/then marker named as a transition's source |
not checked; lower/state_graph.go (*StateGraph).vertex reports it as a construction error |
model/transition_first_test.go:TestTransitionFirstStart (pins the marker source as clean at check time) |
⚠️ Approximate (whether first start then off; beside transition t1 first start … then off; declares a second transition out of one initial pseudostate — illegal under UML 2.5.1 §15.7.18 — or names the same one twice is a reading of SysML v2 §7.19.3 this PR does not settle; the existing test pins it clean, so only lowering reports it) |
| Completion transitions | state_executor.go:218 scheduleTransitionsForState nil trigger |
state_simple.sysml |
✅ Faithful |
A transition written in the standard first/accept/then form, with the trigger on a line of its own, and with a name of its own (SysML.xtext TransitionUsage) |
parser/behavior.go parseTransitionMember/parseTransitionTail; lower/state_graph.go Transition.Name; runtime/state_executor.go transitionDescription (the name is what a diagnostic about the transition reports) |
parse/behavior_exhibit_state_body.golden, parse/state_transition_variants.golden, conformance/state_transition_accept_via_port.sysml, negative_test.go:transition_trigger_no_target, :transition_two_triggers, :transition_two_targets, :transition_do_without_action |
✅ Faithful |
accept … via <port> on a transition (SysML.xtext AcceptParameterPart) |
parser/behavior.go parseTransitionTail; lower/state_graph.go Transition.Via; runtime/state_executor.go matchesEvent/acceptsSignal/deliverPendingSignal |
conformance/state_transition_accept_via_port.sysml |
✅ Faithful (a transition naming a port fires only for an occurrence routed to that port; one naming none takes an addressed message, as before) |
A transition's accept payload is bound for its guard and effect (accept w : Warning do assign level := w) |
lower/state_graph.go classifyTrigger (AcceptEvent.Payload); runtime/state_executor.go bindAcceptPayload |
conformance/state_transition_accept_payload.sysml |
✅ Faithful (bound while the transition is taken and unbound if it does not fire, as a call trigger's arguments are) |
A transition triggered by a subsetted event (accept :> shutDown) |
lower/state_graph.go classifyTrigger (AcceptEvent.Subsets); runtime/state_executor.go triggerMatches/acceptsSignal |
parse/behavior_accept_subsets.golden, conformance/action_accept_subsets_event.sysml (the same matching rule in an action body) |
✅ Faithful |
A transition's do effect written as a statement is terminated by the transition's own ; (SysML.xtext TransitionUsage ends with ActionBody, while EffectBehaviorUsage carries no ;), in the standard first … then spelling and in OpenSysML's compact <source> to <target> spelling alike |
parser/behavior.go expectStatementEnd/atTransitionEffectStatement/atEffectEnd; parser/defusage.go parseUsage/parseReferenceMemberUsage |
parse/state_transition_effect_statement.golden, conformance/state_transition_effect_assign.sysml, conformance/state_transition_effect_assign_first_then.sysml (with their .trace.golden), negative_test.go:transition_effect_perform_two_semicolons, :transition_effect_assign_two_semicolons, :transition_effect_no_semicolon, :body_assignment_no_semicolon |
✅ Faithful (a second ; is an error, as ActionBody takes one terminator; a statement outside a transition effect still needs its own ;) |
Bodied exhibit state (exhibit state spacecraftModes { … }, SysML.xtext ExhibitStateUsage) |
parser/behavior.go (the exhibited state's body is parsed as a state body); ast/dump.go (the exhibit state keyword is recorded) |
parse/behavior_exhibit_state_body.golden, parse/classifier_behaviors.golden, negative_test.go:exhibit_state_unclosed_body |
✅ Faithful (the body parses, resolves and lowers as a state machine, and an object of the type runs it — see the Classifier Behaviors map) |
Classifier Behaviors (KerML §8.4.4.3 Behaviors / performances; SysML v2 §7.16 exhibit/perform)¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
| A behavior a type exhibits or performs is bound to every object of the type: materializing the object gives it an execution of its own, and two objects of one type run independently | lower/classifier_behavior.go ClassifierBehaviorOf/BehaviorMembers; runtime/classifier_behavior.go startClassifierBehaviors/attachClassifierBehavior; runtime/instance.go instantiateOwnedBy |
conformance/object_exhibits_state_machine.sysml, conformance/two_objects_exhibit_independently.sysml (with their .trace.golden), runtime/classifier_behavior_test.go, repl/classifier_behavior_test.go:TestStateDebugsTheMachineAnObjectExhibits |
✅ Faithful |
| A body of the behavior reads and writes the performing object's own feature values, and a message addressed to one object reaches that object's machine and not a sibling's | runtime/classifier_behavior.go assignPerformerFeature; runtime/state_statements.go, runtime/action_statements.go (assignment through the performer); runtime/signal.go addressOwner (the owner chain an address is resolved against) |
conformance/object_machine_writes_own_features.sysml, conformance/object_addressed_send_one_sibling.sysml |
✅ Faithful |
| Startup and quiescence are tool-defined (KerML gives no clock): feature values and constant defaults come first, then the behavior is initialized and run until no event is due at the current time, no do action is runnable and no message is in flight, bounded by the event and do-step budgets | runtime/classifier_behavior.go startClassifierBehaviors/drainObjectBehaviors; runtime/state_executor.go RunToQuiescence |
robustness_test.go:object_exhibited_machine_never_settles, :object_exhibited_machine_without_an_initial_state, conformance/object_exhibits_state_machine.trace.golden |
⚠️ Approximate (tool-defined: an exhausted budget is ErrBehaviorBudget, and a machine waiting on a timer is quiescent — advancing time drives it) |
A performed action parked at an accept is quiescent rather than deadlocked, and a message a sibling object sends later wakes it |
runtime/action_executor.go RunToQuiescence/HasPendingSignal; runtime/classifier_behavior.go hasPendingWork |
runtime/classifier_behavior_test.go:TestPerformedActionAwaitingAMessageIsWokenByASibling, :TestPerformedActionWithoutAFlowStillMaterializes |
⚠️ Approximate (tool-defined: a standalone %action run still reports ErrAcceptDeadlock, where nothing can post the awaited message) |
An object's parameter space is its own: an action's out parameter answers the caller even where the performing object declares a feature of that name |
runtime/action_statements.go assignOuter; runtime/action_executor.go declaresParameter |
runtime/classifier_behavior_test.go:TestOperationOutputNamedLikeAFeatureAnswersTheCaller |
✅ Faithful |
| A failed materialization leaves no behavior of the object attached or queued, and an edited model drops an object whose behavior body changed rather than resuming it on the values the old body wrote | runtime/classifier_behavior.go startClassifierBehaviors/forgetBehaviorsFrom; runtime/adopt.go writeBoundBehaviors |
runtime/classifier_behavior_test.go:TestFailedMaterializationLeavesNoBehaviorBehind, repl/classifier_behavior_test.go:TestRewritingTheExhibitedMachineDropsTheObject, :TestObjectMachineSurvivesAnUnrelatedDeclaration |
⚠️ Approximate (tool-defined: the spec describes one fixed model, so what a live execution does when the model is edited is a REPL policy) |
A second materialization of one name is a second object, with its own identity and its own behaviors; occurrenceOf remains the reuse path for a named occurrence |
runtime/instance.go instantiateOwnedBy; repl/query.go instantiateNamed (which object the name now denotes) |
robustness_test.go:second_instantiation_of_one_type, repl/classifier_behavior_test.go:TestSecondInstantiateIsAnotherObject |
⚠️ Approximate (tool-defined; the spec leaves object creation semantics open) |
| Invoking an operation of an object's type runs it with that object as performer, binding named arguments as the call machinery binds them | runtime/invoke_operation.go InvokeOperation; repl/meta.go %invoke |
runtime/classifier_behavior_test.go:TestInvokeOperationPerformedByTheObject, :TestInvokeOperationFailureModes, robustness_test.go:operation_invoked_with_unbound_parameters, repl/classifier_behavior_test.go:TestInvokeRunsAnOperationOnTheObject, :TestInvokeReportsItsFailureModes |
⚠️ Approximate (actions only, with named arguments: an operation given as a calc or constraint, and positional arguments, return ErrUnsupportedClassifierBehavior — see Known Limitations) |
Expression Evaluation¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
| Binary operators (+, -, , /, %, *, <, >, ==, ===) | eval.go evalOperator → evalArithmetic/evalComparison/evalEquality/evalIdentity |
calc_simple_add.sysml, calc_modulo_operator.sysml, calc_identity_operators.sysml |
✅ Faithful |
| Boolean operators (and, or, xor, implies), short-circuiting where they can | eval.go evalLogical |
constraint_literal.sysml, calc_boolean_operators.sysml |
✅ Faithful |
| Unary operators (-, +, not) | eval.go evalUnary |
calc_unary_operators.sysml |
✅ Faithful |
Conditional (if c ? a else b) and null coalescing (??), both lazy |
eval.go evalConditional/evalNullCoalesce |
calc_conditional_branch.sysml, calc_null_coalesce.sysml |
✅ Faithful |
| Literal values (Integer, Real, Boolean, String) | eval.go:109 evalLiteral* |
calc_simple_add.sysml |
✅ Faithful |
| Feature reference resolution | eval.go:141 evalFeatureReference |
constraint_literal.sysml |
✅ Faithful |
| Qualified name resolution (A::B::C) | eval.go:53 Eval + resolve/qualified.go |
calc_qualified_names.sysml |
✅ Faithful |
| Type coercion (Integer→Real) | eval.go:344 toReal |
calc_type_coercion.sysml |
✅ Faithful |
Exponentiation (**, ^) — Integer operands with a non-negative exponent give an Integer (IntegerFunctions::'**'), any other numeric pair a Real (RealFunctions::'**') |
semantics/eval.go Pow, shared by the folder's evalArithmetic and runtime/eval.go evalArithmetic |
calc_library_functions.sysml, exponentiation_test.go |
✅ Faithful |
An enumeration literal is a value of its enumeration (SysML v2 §7.6.4, §8.3.7 EnumerationUsage): a literal declaring no value evaluates to itself, and identity is the declaration it names — Color::red == Color::red is true, Color::red == Color::green false, and a literal of another enumeration false, since the two are unrelated values (the mismatch is a type-tier diagnostic, not a runtime one). A set keys a literal on that declaration, so the same literal twice is one element |
runtime/value.go ValEnumLiteral/Value.LiteralText, eval.go EvalContext.enumLiteralValue/valueEqual, value_equality.go valueKeyFunc, semantics/enumeration.go EnumerationOwning/LiteralsOf |
enum_literal_default_slot.sysml, eval_test.go:TestEval_EnumerationLiteralIsAValue, TestEval_EnumerationLiteralEquality, TestEval_EnumerationLiteralInASet, robustness_test.go:enumeration_name_that_is_not_a_literal |
✅ Faithful |
A literal of an enumeration specializing a scalar type (enum def GradePoints :> Real { A = 4.0; }) is a value of that type, so it evaluates to the value it declares and computes as one |
runtime/eval.go EvalContext.enumLiteralValue |
enum_literal_scalar_valued.sysml, eval_test.go:TestEval_EnumerationLiteralWithAValue |
✅ Faithful |
A literal is an occurrence of its enumeration, so the features it declares are readable (Level::high.n) and every read of that literal answers about one object |
runtime/eval.go chainMemberValue (ValEnumLiteral arm), Context.enumLiteralObject |
enum_literal_own_attributes.sysml, eval_test.go:TestEval_EnumerationLiteralOwnAttributes, TestEval_EnumerationLiteralReadTwiceIsOneObject, robustness_test.go:chain_through_a_literal_without_that_attribute |
✅ Faithful |
An enum-typed feature's default materializes as a feature value, and a literal renders as the enumeration writing it qualifies it (c = Color::red) in %features, in a trace and in a diagnostic |
repl/meta.go formatFeatureValue, runtime/trace.go FormatTraceValue, runtime/describe.go describeOperand |
enum_literal_default_slot.sysml, repl/meta_test.go:TestFeatureValuesEnumerationLiteral, describe_test.go:TestDescribeOperandEnumerationLiteral |
✅ Faithful |
A literal crossing the API boundary keeps its identity: it travels as Value.enum_literal carrying the declaration FQN, the enumeration's FQN and the qualified rendering, and an incoming literal is resolved against the model it names rather than reconstructed, so a literal no declaration of that model matches is an error and never a null |
grpc/convert.go enumLiteralToProto/enumLiteralFromProto, api/proto/sysml.proto EnumLiteral; Python opensysml/enumeration.py EnumLiteral, values.py, connection.py _python_to_value |
grpc/convert_enum_test.go:TestEnumLiteralToProto, TestEnumLiteralRoundTrip, TestEnumLiteralRoundTripInSequence, TestEnumLiteralUnresolvedIsAnError, TestInstantiate_EnumTypedFeatureValueCarriesLiteral; python/tests/test_enumeration.py, test_wire_compat.py:test_enum_literal_is_an_added_value_arm |
✅ Faithful (advertised as the enum_values capability; a literal of an enumeration specializing a scalar crosses as that scalar, as it evaluates to one) |
A quantity crosses the API boundary in both directions: Value.quantity carries the magnitude with the kind it was written in (Integer or Real), the unit as written and the reduced unit term, so a quantity read from the service can be sent back as an input and evaluates against the unit it names — commensurability is decided over the reduction, so a unit named without one is refused client-side rather than compared by bare magnitude |
opensysml/values.py Quantity.to_pb, Unit.to_pb/Unit.reduced, connection.py _python_to_value; service side grpc/convert.go ProtoToQuantity, ProtoToValueIn |
python/tests/test_quantity.py: test_a_quantity_encodes_as_the_message_the_service_decodes, test_an_unreduced_unit_is_refused_before_it_is_sent, and against a live service TestQuantityAgainstTheService::test_a_quantity_sent_as_a_calc_argument_round_trips, ::test_a_quantity_input_binds_into_an_action, ::test_a_sent_quantity_is_commensurable_with_the_models_own_units |
✅ Faithful |
| An unqualified name resolves as a written reference does — the enclosing scope chain, inherited members, imports, then the global index — and the declaration it finds is evaluated in its own declaring scope, so the imports in force where a value was written answer the names that value uses | runtime/eval.go evalFeatureReference (scope arm) via resolve/unqualified.go Resolver.LookupName, EvalContext.evalIn |
action_body_package_member.sysml, action_body_declarer_scope.sysml, body_scope_test.go:TestBodyScopeImportSpellings, robustness_test.go:action_body_unresolved_feature |
✅ Faithful |
Scope of an expression in a behavior body¶
An expression written inside an action or state machine body resolves its names
in the scope it was declared in, and the values live above that scope: a
frame binding (the action's feature space, a block-local declaration, a call trigger's
argument) shadows a same-named declaration the scope reaches, and the innermost
frame wins. The scope travels with the IR — internal/core/lower records it on
the graph, on each lowered statement and block, on each state and on each
transition when it lowers them — so the executors read a scope rather than
re-deriving one from symbol.Decl (AGENTS.md §4).
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
An attribute default in a behavior body is evaluated in that body's scope, so a unit an import brought in resolves (attribute h : LengthValue = 500.0 [m];) |
lower/action_graph.go lowerAttributes + ActionGraph.Scope; runtime/action_executor.go initializeAttributes; lower/state_graph.go + runtime/state_executor.go initializeAttributes |
action_body_quantity_descent.sysml, state_body_quantity_scope.sysml, parser/testdata/parse/action_body_quantity_statements.golden |
✅ Faithful |
A statement in a nested action node resolves in that node's scope; a statement in a loop body or an if branch in the block's own scope |
lower/action_graph.go lowerStatement, lowerBlock, childScope (lower/scope.go); runtime/action_statements.go evalIn |
action_body_quantity_descent.sysml, action_body_shadows_enclosing_scope.sysml |
✅ Faithful |
| A decision guard and an inline expression resolve in the action's own scope, its feature values shadowing it | runtime/action_executor.go stepDecisionNode, stepActionExecutionNode |
action_body_shadows_enclosing_scope.sysml, action_control_flow.sysml |
✅ Faithful |
| A transition's guard, effect, time-event duration and change-event condition resolve in the scope the transition was written in; a call trigger's parameters and an accept trigger's payload are visible to its guard and effect and nowhere else | lower/state_graph.go Transition.Scope/BodyScope (via symbols.TriggerScope); symbols/bodyscopes.go newTriggerScope, payloadParameterDefiner; resolve/document.go; runtime/state_executor.go passesGuard, scheduleTransitionsForState, pollChangeEvents, executeAction; runtime/state_region_transition.go runEffect |
state_body_quantity_scope.sysml, state_call_trigger_guard.sysml, state_call_trigger_regions.sysml, state_transition_accept_payload.sysml, model/behavior_body_resolve_test.go accept-payload case |
✅ Faithful |
| A state's entry, do and exit behaviors resolve in that state's scope, nested states and orthogonal regions included | lower/state_graph.go StateGraph.StateScopes, collectStates, collectRegionStates; runtime/state_executor.go stateScope |
state_body_quantity_scope.sysml, state_concurrent_do.sysml, state_region_cross_pseudostate.sysml |
✅ Faithful |
| A body member of an inherited or performed behavior is evaluated in the declarer's scope, not in the scope performing it | runtime/invoke_action.go invokeAction, runtime/context.go chainMembers + EvalContext.evalIn |
action_body_declarer_scope.sysml |
✅ Faithful |
| A frame binding shadows the enclosing scope, and an inner block shadows an outer one | runtime/action_statements.go evalIn (frames pushed over the scope), runtime/eval.go evalFeatureReference (frames consulted first) |
action_body_shadows_enclosing_scope.sysml, robustness_test.go:loop_body_declaration_does_not_leak |
✅ Faithful |
| A name or unit the declaring scope does not reach is reported, not evaluated as a bare magnitude | runtime/eval.go (ErrUnresolvedReference), semantics/units.go (ErrNotAUnit) |
robustness_test.go:action_body_unresolved_unit, :action_body_unresolved_feature, :state_body_unresolved_unit |
✅ Faithful |
A %constraint/%requirement verdict is evaluated in the element's declaring scope, with or without an instance |
repl/meta.go declaringScope |
repl/runtime_commands_test.go:TestConstraintResolvesUnitsOfItsOwnPackage |
✅ Faithful |
An expression typed at the prompt is evaluated in the namespace the session is working in — the namespace a member typed there would be written in — so that namespace's members and the units its imports bring in resolve unqualified (1.0 [m/s], mass * 2) |
repl/meta.go promptScope, doEval; repl/lookup.go lookupSymbol (a name the session declares nowhere is resolved there) |
repl/runtime_commands_test.go:TestEvalResolvesImportedUnitsUnqualified, TestPromptScopeIsTheLastNamespaceDeclared |
⚠️ Approximate (the notation says nothing about a prompt, so "the namespace the session works in" is the last one it declared: declaring a second package moves it, and the first package's members and imports are then reached by qualified name only — a context named explicitly, %eval in <qualified-name> : <expression>, pins it instead) |
Arguments of a %calc command are a list of expressions parsed by the expression parser, so an argument containing spaces — a quantity, a parenthesized expression, a nested call — is one argument; successive arguments are separated by a comma or by whitespace, and the invocation form Fall(a, b) is accepted |
repl/meta.go doCalc, parseExprList, splitCalcArgs; parser/parser.go Parser.Offset |
repl/runtime_commands_test.go:TestCalcParsesExpressionArguments, TestCalcSeparatesSignedArguments |
⚠️ Approximate (whitespace separates two arguments only where the first is a complete expression and the second is one — 5 -3 is two arguments, 5 - 3 one — since whitespace is no terminator in the notation; named arguments, Fall(v0 = …), are reported as unsupported rather than bound: the notation writes them in an invocation's parentheses, a production the prompt's argument list is not) |
| A quantity's magnitude is rendered in a result table by the same convention as a bare Real, the stored value keeping its full precision | repl/meta.go formatValue, formatConst; runtime/quantity.go Quantity.TextWithMagnitude |
repl/runtime_commands_test.go:TestFormatValueQuantityUsesRealFormatting |
✅ Faithful |
| A qualified name given to a command is the name the notation writes (KerML §7.2.5 unrestricted name): a segment needing quotes — a space, a keyword as a name, punctuation — is quoted, anywhere in the chain, and is one argument rather than being split on its space; the quoting is notation, so the name is normalized to the one the index records before lookup, and a name reported back is spelled so it can be typed into the next command | repl/meta.go parseArgs, indexOutsideName; repl/qualname.go plainName, notationName; repl/lookup.go lookupSymbol |
repl/qualname_test.go:TestQuotedNamesAcceptedByNameTakingCommands, :TestQuotedNameNamesTheSameObjectThroughout, :TestParseArgsKeepsQuotedNamesWhole, :TestQuotedNameFailuresAreReportedNotPanics, :TestNotationNameRoundTrips |
✅ Faithful |
The same spelling rule holds over gRPC: a symbol ID is resolved whether it is written quoted ('My Pkg'::Car) or in the unquoted spelling the index records (My Pkg::Car), on every RPC taking a symbol ID |
grpc/service.go lookupNamed, unquotedName |
grpc/spelling_test.go:TestInstantiateAcceptsBothSpellings, :TestSymbolIDSpellingHoldsOnEveryRPC, :TestSymbolIDSpellingDoesNotInventSymbols |
✅ Faithful |
An expression is evaluated in a context named explicitly — %eval in <qualified-name> : <expression> — either the named element's namespace or, where an object was materialized under that name, that object's feature values; :: inside the name is not the separator |
repl/meta.go doEvalLine, evalIn, splitPinnedContext, contextSeparator; repl/lookup.go objectNamed; runtime/eval.go Context.EvalWithScopeOn (one run, so the step budget bounds a pinned evaluation as it bounds an unpinned one) |
repl/evalin_test.go:TestEvalInNamespaceReadsItsMembers, :TestEvalInInstanceReadsItsFeatureValues, :TestEvalQualifiedNameIsNotAContextSeparator, :TestEvalInFailuresAreTypedNotPanics, :TestEvalWithoutContextIsUnchanged, :TestEvalInInstanceIsBoundedByTheStepBudget; runtime/robustness_test.go:eval_on_an_instance_spends_the_step_budget |
✅ Faithful |
A session that loses an object or a debugger session says so and why: a reload carries an object over where the reloaded declaration still resolves to the shape it was materialized against, and a reset, which can prove nothing, reports the loss so the next %instances/%features/%step explains it |
repl/session.go clear, resetLoss; repl/carryover.go carryOverObjects; repl/notices.go lossOnReset, lossAtSubmission, lossOnBudgets; runtime/context.go Adopt |
repl/reset_test.go:TestClearReportsWhatItTook, :TestCommandsAfterClearExplainTheLoss, :TestClearEndsDebuggerWithAReason, :TestReloadKeepsObjectsItStillResolves, :TestLoadThatChangesDeclarationsReportsTheLoss |
✅ Faithful |
| A submission whose text the parser cannot close (an unterminated body, quoted name or block comment, typed or loaded) is reported and kept out of the text the session analyses, so the next submission is parsed against what was there before it and drops nothing | repl/enclosure.go closesItsOwnText, maskedText; repl/session.go acceptFrom, joined, openDiagnostics, diagnostics |
repl/openinput_test.go:TestLoadedOpenFileDoesNotPoisonTheNextSubmission, :TestOpenTypedSubmissionKeepsTheBuffer, :TestOpenSubmissionKeepsReportingTheRestOfTheBuffer, :TestOpenSubmissionKeepsWarningSeverity, :TestOpenSubmissionEchoesItsOwnLine, :TestRetypingANamespaceDoesNotMergeAMaskedSubmission, :TestMaskedSubmissionIsNotReportedAsBlockingTheChecks, :TestOpenSubmissionSurfacesStayTyped, repl/session_test.go:TestUnparseableRedeclarationDropsNothing |
✅ Faithful (the buffer keeps the text for %save, so nothing typed is lost; a submission that does not close is not merged with the one after it, and its own findings keep the severity and code the parser gave them while the rest of the buffer is still analysed and reported) |
| Load-time diagnostics are reported the way interactive ones are, against the file and its own line numbering, and the non-interactive path's status reflects them | repl/run.go LoadFile, LoadFileSummary, Diagnostics, HasErrors; repl/render.go renderSyntax |
repl/openinput_test.go:TestLoadReportsSyntaxDiagnostics, :TestLoadFileSummaryReportsSyntaxDiagnostics, :TestLoadReportsUnresolvedReference, :TestReloadingAFixedFileClearsItsSyntaxError |
✅ Faithful (a syntax error in a loaded file is an error for HasErrors, which is what sysml -validate/%features in a script exits on; the exit status itself is cmd/sysml's) |
An expression whose subject is reached through a declaration is evaluated on the object in effect for it, so a nested redefinition is honored by %eval, %constraint and %requirement alike |
repl/lookup.go subjectFor, carrierInstances, nestedObjects, featureChainSymbol |
repl/subject_test.go:TestEvalThroughDeclarationHonorsNestedRedefinition, :TestEvalThroughDeclarationWithoutObjectUsesDeclaredValue, :TestEvalThroughDeclarationWithTwoNestedCarriersIsAmbiguous, :TestCheckHonorsNestedRedefinitionThroughDeclaration |
✅ Faithful (one shared subject seam: the runtime graph is walked to the object carrying the declaration, two carriers are an AmbiguousSubjectError, and no object at all still reads the declared value) |
| Two loaded files that open one package are two declarations of that name, and the load says so | repl/merge.go reopenedNamespaces; repl/notices.go dropReport.notice |
repl/reopen_test.go:TestLoadingTwoFilesThatOpenOnePackageSaysSo, :TestLoadingDistinctPackagesSaysNothingAboutReopening, :TestReloadingOneFileSaysNothingAboutReopening, :TestRetypingAPackageStillMergesInteractively |
⚠️ Approximate (maintainer decision, recorded here: a loaded namespace keeps its file's identity so re-loading that file replaces only its own contribution, which merging the two openings would make impossible without one file's edit deleting the other's members. Both openings' members are declared and reachable qualified; an unqualified reference across the two does not resolve, and the note says to qualify it. Re-typing a package at the prompt still folds into the one in the session) |
%view <name> reports a view's exposed elements, its nested views and its conformance to the viewpoints it satisfies |
repl/view.go Session.View, doView, viewElementLine, conformanceLines, concernEvaluator (the runtime requirement engine, through runtime.Context.CheckSatisfactionOn); over semantics/expose.go Model.ExposedElements, Model.NestedViews and semantics/conformance.go Model.ViewConformance |
repl/view_test.go:TestViewListsWhatItExposes, :TestViewListsNestedViews, :TestViewExposingNothingIsNoError, :TestViewOfANonViewIsTyped, :TestViewOfAnUnknownNameReports, :TestViewIsInHelpAndCompletion, :TestViewReportsViewpointConformance, :TestViewReportsAViolatedConcernPerElement, :TestViewWithoutASatisfyReportsNoConformance, :TestViewReportsASatisfyThatIsNoViewpoint, :TestViewConformanceOutputIsDeterministic, :TestViewCreatesNoObjectOfItsOwn, :TestViewLeavesNoAmbiguityForALaterCheck, :TestViewEvaluatesTheSessionObject, :TestViewSharesTheObjectOfAQuotedName |
✅ Faithful (a view exposing nothing says so; a non-view is semantics.ErrNotAView; a nested view is asked for its own exposed set; the report changes no model and is deterministic in ordering. A concern is evaluated against the object the session holds for an exposed element, and otherwise against one materialized in a runtime of the report's own, so a report registers no object of its own) |
%print [name] writes the session's model back as notation at the prompt — the whole buffer, or one element and its body — through the writer a .sysml save writes with, so comments and text survive and the print can be submitted again to rebuild the same model |
repl/print.go doPrint, printSession, printElement, declarationSpan; export/convert.go SysMLElement, ConvertTolerant → format.Source; repl/lookup.go lookupSymbol (the quoted/qualified spellings) |
repl/print_test.go:TestPrintWholeSession, :TestPrintElement, :TestPrintQuotedAndQualifiedNames, :TestPrintKeepsComments, :TestPrintRoundTripsThroughSubmit, :TestPrintEmptySession, :TestPrintUnresolvableName, :TestPrintSymbolWithoutNotation, :TestPrintSaysNothingAboutRDF, :TestPrintLeavesInstancesAndBufferUntouched, :TestPrintLeavesActionDebuggerRunning, :TestPrintLeavesStateDebuggerRunning, :TestPrintCompletion, :TestPrintOfUnparsableSessionWarns |
✅ Faithful (a read: nothing is materialized, the buffer is unchanged and a debugging session keeps running. An empty session, a name nothing declares and a symbol this session holds no source of each answer in one line; notation only, so no RDF notice follows a print) |
| A name the session cannot find is offered the qualified names it is known under, nearest scope first and bounded | repl/qualsuggest.go qualifiedSuggestions, nestedInNonNamespace, writableName; repl/discover.go notFoundError, suggestSymbol |
repl/qualsuggest_test.go:TestQualifiedSuggestionsPreferTheSession, :TestQualifiedSuggestionsPreferPackageMembers, :TestQualifiedSuggestionsAreCapped, :TestUnresolvedMessageOffersRankedNames, :TestQualifiedSuggestionsSkipUnwritableNames |
✅ Faithful (what the session declares outranks the library, a package's member outranks a name nested inside another element — a library function's parameter is no suggestion for a type — and at most suggest.Limit are offered) |
| A satisfaction verdict names the assertion in the notation's own spelling, quoting each inner name that needs quotes | repl/satisfy.go satisfyText; repl/qualname.go notationName |
repl/satisfy_test.go:TestSatisfyQuotesInnerNames |
⚠️ Approximate (the verdict line and the Verdict.Subject a caller reads are quoted; the condition text a runtime.ViolationError carries is rendered in runtime/condition.go, which this slice does not own, so a quoted name inside the condition of a failed assertion is still printed unquoted) |
KerML Function Library (KerML §9.3 Function Library)¶
The library declares these functions abstractly — a signature and no body — so
the runtime supplies the implementation. Dispatch is by the declaration's
qualified name, and a declaration that carries a body is evaluated from that
body — a result expression or a body assigning an output it declares — so a
model's own calc sqrt is never hijacked. An unqualified call that
resolves to no declaration dispatches by local name, which is what makes
sysml -e "sqrt(2.0)" evaluable in a model that imports no part of the library.
Arguments follow the vendored signatures: a Real parameter accepts an Integer
(ScalarValues declares Integer :> Rational :> Real), an Integer parameter
rejects a Real rather than truncating it, and a Natural parameter rejects a
negative value. A result that is not a finite value of the declared type — the
square root of a negative, an inverse sine outside [-1.0, 1.0], a floor
beyond the Integer range — is reported at evaluation rather than returned as a
NaN, an infinity or a wrapped integer.
Beyond the scalars, a vector is the sequence of its elements — which is what
VectorValues declares a NumericalVectorValue to be, elements with a
dimension equal to their number — and a Complex is the two-element sequence
(re, im) that ComplexFunctions::rect constructs, a Real being a Complex with
a zero imaginary part (ScalarValues declares Real :> Complex). The runtime has
no vector and no complex value kind of its own, so an operation whose argument is
a collection of vectors or of Complex values cannot tell it from one flat
vector or one Complex and is reported by name rather than answered (below).
A library feature — a named constant a library declares and gives no
evaluable value — takes its value from a seam of its own: libraryFeatures maps
the feature's qualified name to a Go thunk, consulted only for a symbol a library
document declared, so ordinary name resolution decides and a model's own pi
keeps its own value. The value is recomputed per read rather than cached, which
keeps a vector-valued feature from sharing one sequence between readers and keeps
the value independent of whether the library index cache was warm (a warm cache
restores library symbols without their AST).
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
RealFunctions: sqrt, abs, floor, round, max, min |
runtime/library_functions.go |
TestLibraryFunctionValues |
✅ Faithful |
RationalFunctions/NumericalFunctions: abs, max, min (kind-preserving), isZero, isUnit |
runtime/library_functions.go |
TestLibraryFunctionValues |
✅ Faithful |
IntegerFunctions: abs, max, min; NaturalFunctions: max, min |
runtime/library_functions.go |
TestLibraryFunctionValues |
✅ Faithful |
TrigFunctions: sin, cos, tan, cot, arcsin, arccos, arctan |
runtime/library_functions.go |
TestLibraryFunctionValues |
✅ Faithful |
| Domain, arity and argument-type failures reported at evaluation | runtime/library_functions.go bindAndApply |
TestLibraryFunctionErrors |
✅ Faithful |
| A declaration with a body is evaluated from that body | runtime/library_functions.go libraryFunctionFor |
TestLibraryFunctionDoesNotHijackADeclaredBody |
✅ Faithful |
Named argument binds to the parameter the signature declares (sin(theta = 0.0)); a name no parameter carries is reported (ErrUnknownParameter) rather than absorbed by an omitted [0..1] parameter |
runtime/library_functions.go bindAndApply, checkNamedArguments |
TestLibraryFunctionNamedArguments, TestVectorAndComplexNamedArguments, TestVectorAndComplexUnknownNamedArgument |
✅ Faithful |
TrigFunctions::deg/rad, whose library bodies (theta * 180 / pi, theta * pi / 180) had no evaluable pi |
runtime/library_functions.go (degreesFromRadians/radiansFromDegrees, over libraryPi) |
TestTrigDegreesAndRadians; conformance calc_library_trig_degrees (deg(rad(180.0))), calc_library_trig_degrees.trace.golden |
✅ Faithful |
VectorFunctions: VectorOf/CartesianVectorOf/CartesianThreeVectorOf, isZeroVector, '+', '-' (binary and the one-argument negation the signature's [0..1] second parameter allows), inner, norm, angle, scalarVectorMult/'*', vectorScalarMult, vectorScalarDiv, each with the cartesian* specialization the library declares of it |
runtime/library_functions.go registerVectorFunctions |
TestVectorFunctionValues, TestVectorFunctionScalarValues, TestVectorAndComplexNamedArguments, TestVectorAndComplexFunctionErrors; conformance calc_library_vector_functions, calc_library_vector_norm (+ golden traces) |
⚠️ Approximate: a vector is the sequence of its elements, so a mismatched dimension, a non-numeric element, an element or inner product outside the range of its kind, Real or Integer (elementArith, intArith, checkedNumeric) and a zero vector where an angle or a direction is asked for are typed errors, while a collection of vectors has no representation (below) |
ComplexFunctions: rect, polar, re, im, isZero, isUnit, abs, arg, '+', '-' (both arities), '*', '/', '**'/'^', '==' |
runtime/library_functions.go registerComplexFunctions |
TestComplexFunctionValues, TestComplexFunctionScalarValues, TestLibraryFunctionOptionalOperand, TestLibraryFunctionEmptyOptionalOperand (an empty collection written for a [0..1] operand is the same no value as null, argumentOmitted); conformance calc_library_complex_functions (i * i is -1.0) |
⚠️ Approximate: a Complex is the (re, im) pair, so a Real binds to a Complex parameter, division by zero is reported, and a collection of Complex values has no representation (below) |
A library feature with a library-supplied value: TrigFunctions::pi (which the library fixes by an invariant, not a value), ComplexFunctions::i (rect(0.0, 1.0)), VectorFunctions::cartesian3DZeroVector |
runtime/library_functions.go libraryFeatures, registerLibraryFeature, Context.libraryFeatureValue, libraryFeatureByName |
TestLibraryFeatureValue, TestLibraryFeatureValueFromACachedSymbol, TestLibraryFeatureValueLeavesAModelsOwnFeatureAlone, TestLibraryFeatureValueUnrepresentable, TestLibraryFeatureByName, TestLibraryFeatureImaginaryUnit |
✅ Faithful |
A library feature is read as a name, qualified (TrigFunctions::pi) or imported (i under import ComplexFunctions::*), and computes like any other value (2 * TrigFunctions::pi); a registered feature with no computable value reports itself (ErrUnevaluableLibraryFunction) rather than answering nothing |
runtime/eval.go evalName (both the resolved-scope and the qualified-member paths, consulting Context.libraryFeatureValue) |
TestLibraryFeatureNameReadFromACachedSymbol; conformance calc_library_feature_pi, calc_library_feature_imaginary_unit, calc_library_feature_zero_vector, calc_library_feature_unevaluable |
✅ Faithful |
| The seam is consulted after the ordinary lookups — frame binding, calc output, evaluated feature, instance feature value, enumeration literal — and answers only for a library-declared symbol, so a model feature shadowing a library name keeps its own value and name resolution decides which is read | runtime/eval.go evalName (seam after Lookup/lookupOutput/selfFeatureValue, before the declaration's body and the cannot-evaluate failure) |
conformance calc_library_feature_shadowed_by_model; TestLibraryFeatureValueLeavesAModelsOwnFeatureAlone |
✅ Faithful |
| A library declaration is answered by its built-in even where the vendored declaration carries a body, since a warm library index cache restores library symbols without their AST and the result must not depend on the cache | runtime/library_functions.go libraryFunctionFor, Context.libraryDeclared |
TestLibraryFunctionAnswersALibraryDeclarationWithABody, TestLibraryFunctionDoesNotHijackADeclaredBody (a model's own declaration keeps its body) |
✅ Faithful |
StringFunctions: '+', Length, Substring, '<', '>', '<=', '>=', '==', ToString — the whole vendored package. Length counts characters (one per Unicode code point, so Length("héllo") is 5 over 6 bytes); Substring takes 1-based inclusive character positions and reports a position outside 1..Length(x) naming the character it indexed (ErrIndexOutOfRange, one identity for a sequence index and a string position, worded for both), an upper below lower selecting no character, as SequenceFunctions::subsequence answers for such a range; '==' declares String[0..1] operands, so two omitted operands are equal and an omitted one is not equal to a string |
runtime/library_functions.go registerStringFunctions, stringLength, stringSubstring, stringOrdering, stringEquals, stringToString, concatStrings, compareStrings |
TestStringFunctionValues, TestStringFunctionErrors, TestStringFunctionNamedArguments, TestVendoredFunctionsAreAllDispatchable (gating StringFunctions); conformance string_functions, string_empty, string_substring_out_of_range (+ golden traces) |
✅ Faithful |
The operators StringFunctions declares evaluate over two String operands: '+' concatenates, and '<', '>', '<=', '>=' order strings by character (UTF-8 orders bytes as it orders code points, so a byte comparison is code-point order). An operand of another type is reported, naming the operator and both operand types, and neither operand is coerced |
runtime/eval.go evalArithmetic (ast.OpAdd over two ValString), evalComparison (OperandTypeError) |
TestStringOperators, TestStringOperatorErrors, robustness_test.go:testStringOperandOfTheWrongKind; conformance string_operators, string_comparison, string_compared_with_a_number (+ golden traces) |
✅ Faithful |
StringFunctions::'==' specializes DataFunctions::'==', which is equality over any two values, so the == operator answers false for a String and an Integer rather than reporting; the explicit call StringFunctions::'=='(s, 3) reports the non-String argument, as every String-declared signature does. Two strings are equal by their characters, which is also what a collection membership test asks |
runtime/eval.go evalEquality → runtime/value_equality.go valueEqual, valueKey (ValString); runtime/library_functions.go stringEquals |
TestStringOperators (s == three is false, ("a", "b")->includes("b")), TestStringFunctionErrors |
✅ Faithful |
A declaration this runtime has no representation for the values of reports itself by name (ErrUnevaluableLibraryFunction), never a wrong result |
runtime/library_functions.go registerUnevaluable |
TestUnevaluableLibraryFunctionsNameThemselves, TestVendoredFunctionsAreAllDispatchable (every vendored declaration of these packages either computes or names itself); conformance calc_library_unevaluable_function |
✅ Faithful |
Found, not fixed — numeric library declarations that remain unevaluable. Each reports itself by name rather than answering:
| Not implemented | Why |
|---|---|
MatrixFunctions |
The vendored Kernel Function Library declares no such package — only docs/ mention it — so there is nothing to dispatch. Not implemented rather than invented. |
VectorFunctions::sum/sum0, ComplexFunctions::sum/product |
An aggregation over a collection of vectors or of Complex values: a sequence of them flattens, so the grouping the aggregation sums over is already lost in the argument. Needs a vector and a complex value kind in runtime/value.go. |
VectorFunctions::cartesianZeroVector |
Declared as the 1-, 2- and 3-dimensional zero vectors as one feature of three vectors, which a flat sequence cannot hold. cartesian3DZeroVector has a value. |
ComplexFunctions::ToString/ToComplex |
No string notation for a Complex value is defined; inventing a rendering would make ToComplex(ToString(x)) a value nothing else in the library agrees on. |
Reading a library feature as a bare name at the REPL/CLI surface (%eval TrigFunctions::pi, sysml <model> -eval 'ComplexFunctions::i') |
A read inside a model or an expression consults the seam (above), but repl/meta.go resolves a lone name to a symbol and reads its usage.Value without the seam, so the bare form still reports has no value to evaluate, and cartesianZeroVector shows that generic message rather than its typed unevaluable reason. The hook belongs next to the enumeration-literal case in repl/meta.go (owned elsewhere). |
| Library functions in the checker's own name resolution | An unqualified call to a library function the model does not import evaluates, but the unresolved-reference diagnostic still reports the name; importing RealFunctions::* clears it. |
Sequence Indexing and Collection Operations (KerML §9.3 SequenceFunctions, CollectionFunctions, ControlFunctions)¶
A KerML sequence is not a value of its own kind: every value is a sequence — of
one element where it is a scalar, of none where it is null — which is how the
library's own isEmpty is seq == null and how 1->size() is 1. The runtime
takes that view of a value in runtime/collections.go elementsOf, so the
operations agree with the library's definitions for a scalar and for null as
well as for a sequence or a set.
The index is 1-based, verified against the vendored declaration rather than
assumed: SequenceFunctions::'#' declares in index: Positive[1] and
SequenceFunctions::head is defined as seq#(1), last as seq#(size(seq))
and subsequence as (startIndex..endIndex)->collect {in i; seq#(i)}. An index
of 0 is therefore not a position, and is reported rather than read as the first
element.
The library declares each operation with a body — size recursively as if
isEmpty(seq)? 0 else size(tail(seq)) + 1 — but that body is the specification
of the operation, not the way to compute it, so a name denoting the library
declaration dispatches to the implementation while a model's own declaration of
that name is still evaluated from its own body.
The three notations a model can write an operation in — the collect/select
notation (xs.{in x; …}, xs.?{in x; …}), the receiver form
(xs->collect {…}, xs->size()) and the plain call (size(xs),
SequenceFunctions::size(xs)) — all reach one implementation per operation, so
they cannot drift apart.
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
seq#(i) is the i-th element counting from 1 (SequenceFunctions::'#', in index: Positive[1]), and an index of 0, a negative index or one past the end is a typed error rather than an empty or zero value |
runtime/collections.go evalSequenceIndex, elementAt, indexOf; runtime/quantity.go evalIndexExpr (non-bracket arm) |
runtime/collections_test.go TestSequenceIndexing, TestSequenceIndexingErrors; conformance calc_sequence_index, calc_sequence_index_out_of_range, calc_sequence_index_zero, calc_sequence_index_non_integer; robustness_test.go:sequence_index_names_no_position |
✅ Faithful |
The index and the quantity expression share one AST node and are told apart by the notation that produced them (ast.IndexExpr.Bracket), so 5 [m] is a quantity and xs#(1) is an element |
parser/expr.go (Hash and LBracket arms), runtime/quantity.go evalIndexExpr |
parser/testdata/parse/quantity_expression.golden, parse/collection_operations.golden; runtime/collections_test.go TestSequenceIndexKeepsQuantityForm; conformance calc_sequence_index_and_quantity_form; parser/negative_test.go (index_no_paren, index_bracket_empty) |
✅ Faithful |
| An index that is not one whole number (a Real, a Boolean, a string, a collection) is a typed error, statically where it is written as a literal and at evaluation otherwise | passes/typecheck_expr.go inferIndex (the index conforms to Integer, a literal 0 and a literal past a written sequence's length; a whole number a model counts with is a position or not depending on its value, so an Integer-typed index is checked at evaluation rather than reported for not being declared Natural); runtime/collections.go indexOf |
passes/typecheck_index_test.go TestIndexNonIntegerIndexReported, TestIndexZeroReported, TestIndexPastWrittenSequenceReported, TestIndexTypedIntegerNotReported, TestIndexByLoopVariableNotReported; runtime/collections_test.go TestSequenceIndexingErrors |
✅ Faithful |
collect answers the mapper's result for each element, in order, with the parameter the body itself declares bound to the element and the scope the body was written in still visible |
runtime/collections.go builtinControlCollect, applyBody; runtime/eval.go evalCollectExpr, evalCollectionNotation |
runtime/collections_test.go TestCollectionResults; conformance calc_collect_over_sequence, calc_collect_names_outer_variable, calc_nested_collection_operations |
✅ Faithful |
select/reject/selectOne/forAll/exists require the Boolean[1] result their expr parameter declares, and a selector answering anything else is a typed error rather than a dropped element |
runtime/collections.go filter, quantify, applyPredicate |
runtime/collections_test.go TestCollectionResults, TestCollectionOperationErrors; conformance calc_select_over_sequence, calc_select_predicate_not_boolean; robustness_test.go:select_predicate_is_not_a_condition |
✅ Faithful |
A body called with a number of arguments it declares no parameters for is a typed error (ErrBodyArity), never a call with a parameter left unbound |
runtime/collections.go bodyOf |
runtime/collections_test.go TestCollectionOperationErrors; conformance calc_collection_body_wrong_arity; robustness_test.go:collection_body_of_the_wrong_arity; parser/negative_test.go (body_param_no_name) |
✅ Faithful |
SequenceFunctions: #, size, isEmpty, notEmpty, includes, includesOnly, excludes, equals, same, union, intersection, including, includingAt, excluding, subsequence, excludingAt, head, tail, last — each computing what the vendored body specifies, except includingAt (below), equals by value and same by identity |
runtime/collections.go, registered in runtime/builtins.go |
runtime/collections_test.go TestCollectionResults, TestCollectionScalarResults; conformance calc_collection_aggregators |
✅ Faithful, except the out-of-range endpoints of subsequence/excludingAt (see below) |
includingAt inserts the values before the 1-based index, shifting the tail right, so the result is longer than the input by the values inserted; index == size + 1 appends, and any other index outside 1..size + 1 is a typed error (ErrIndexOutOfRange) |
runtime/collections.go builtinSequenceIncludingAt, registered in runtime/builtins.go |
runtime/collections_test.go TestCollectionResults, TestCollectionOperationErrors, robustness_test.go:testNumericLibraryCallThatHasNoValue; conformance calc_sequence_including_at, calc_sequence_including_at_appends, calc_sequence_including_at_out_of_range |
⚠️ Approximate: the vendored body drops the element at index instead of shifting it right, which would leave addAt removing and the library with no insertion. Insertion is implemented on the maintainer's ruling and the vendored body is recorded as an OMG source bug (omg-issues.md) |
An endpoint of subsequence or excludingAt that is outside the sequence is a typed error (ErrIndexOutOfRange), while an empty range inside it is the empty sequence, which is how tail is subsequence(seq, 2) of a one-element sequence |
runtime/collections.go builtinSequenceSubsequence, builtinSequenceExcludingAt |
runtime/collections_test.go TestCollectionOperationErrors, TestCollectionResults |
⚠️ Approximate: the vendored bodies compose #, which returns Anything[0..1], so they answer the sequence unchanged for (1,2,3)->excludingAt(4) and truncate subsequence(1, 4). A position the sequence does not have is reported here rather than answered as if the model had asked for one it has |
CollectionFunctions: size, isEmpty, notEmpty, contains, containsAll, head, tail, last, # over a collection's elements, a set included |
runtime/collections.go, runtime/builtins.go |
runtime/collections_test.go TestCollectionScalarResults, TestCollectionOperationsOverSets |
✅ Faithful |
| An operation over an empty collection answers the empty collection and never calls its body, since there is no element to call it with | runtime/collections.go elementsOf (an empty collection yields no elements) |
conformance calc_collection_ops_over_empty; runtime/collections_test.go TestCollectionResults |
✅ Faithful |
ControlFunctions: collect, select, selectOne, reject, reduce, forAll, exists, allTrue, anyTrue, minimize, maximize |
runtime/collections.go, runtime/builtins.go |
runtime/collections_test.go TestCollectionResults, TestCollectionScalarResults, TestCollectionOperationErrors |
✅ Faithful |
NumericalFunctions::sum/product and the specializations that fix the identity of an empty aggregation (sum0(collection, 0), product1(collection, 1)), keeping the elements' kind: Integers sum to an Integer, a Real anywhere makes the result a Real, and an overflowing sum or product is reported rather than wrapped |
runtime/collections.go aggregate, foldNumeric |
runtime/collections_test.go TestCollectionScalarResults; conformance calc_empty_collection_aggregation |
✅ Faithful |
| A collection of quantities aggregates to a quantity, in the unit of its first element and converting the rest, as the binary operator does; mixing a bare number with a measured value reports incommensurable units | runtime/collections.go aggregate/aggregateQuantities, quantity.go addQuantities/scaleQuantities |
conformance cubesat_mass_rollup; runtime/collections_test.go:TestAggregateQuantities |
⚠️ Approximate (an empty collection has no unit to answer in, so it aggregates to the numeric identity 0/1) |
The unqualified, qualified and receiver (->) forms of an operation are one implementation, so (1,2,3)->size(), size((1,2,3)) and SequenceFunctions::size((1,2,3)) cannot disagree; a name the model itself declares still resolves to that declaration |
runtime/builtins.go builtinsByLocalName, Context.builtinFor; runtime/eval.go evalInvocation (receiver prepended as the first argument) |
runtime/collections_test.go TestCollectionScalarResults; conformance calc_collection_receiver_form |
✅ Faithful |
A sequence is flat: an element of a sequence expression that is itself a collection contributes its elements, so (xs, ys) is xs->union(ys) — which is how the library defines union, as the sequence expression (seq1, seq2) — and a mapper answering several values contributes them all |
runtime/eval.go evalSequenceExpr; runtime/collections.go builtinControlCollect; passes/typecheck_expr.go writtenLength (a written length is knowable only where every element is a literal, so a literal index past a sequence of names is left to evaluation) |
runtime/collections_test.go TestSequenceExpressionsAreFlat; conformance calc_sequence_expression_is_flat |
✅ Faithful |
A receiver binds by position, so a call written with both a receiver and named arguments (x->f(a = 1)) states no parameter for the receiver and is a typed error (ErrReceiverWithNamedArgs) rather than a call the receiver is dropped from |
runtime/eval.go evalInvocation; passes/typecheck_expr.go checkArguments |
runtime/collections_test.go TestCollectionOperationErrors, TestReceiverWithNamedArgumentsIsReported; passes/typecheck_expr_test.go TestExprInvocationReceiverWithNamedArguments |
✅ Faithful |
A collection operation is an expression wherever an expression is allowed, including inside a calc body's while and for loops |
runtime/collections.go, runtime/action_statements.go |
conformance calc_collection_ops_in_for_loop, calc_collection_ops_in_while_loop |
✅ Faithful |
| Every operation is bounded by the evaluation step budget, since each call of its body spends steps | runtime/eval.go Context.step |
robustness_test.go:collection_operation_step_budget |
✅ Faithful |
A materialized element is bounded on its own, since it is memory the collection keeps rather than work a step does: every path that adds one to a sequence — a range, a sequence literal, ->collect, union/intersection/including/excluding/subsequence/tail/select — charges the element budget (SYSML_MAX_ELEMENTS, default 1000000, ~104MB of Values) and reports ErrElementLimitExceeded, not the step limit. The count is what an evaluation holds, not what a run produced: a statement, and an evaluation outside a body alike (beginStep), releases the elements it materialized, so a loop or a long run building a small collection each step is bounded by its peak rather than its total, while a collection kept across statements had to be materialized in one of them and so was charged in full |
runtime/context.go chargeElements/elementScope/beginStep, runtime/statements.go statement, runtime/collections.go newSequence, runtime/range.go rangeSequence, runtime/eval.go evalSequenceExpr; budget from budget.go |
element_budget_test.go:TestElementBudgetBoundsEveryMaterialization, :TestElementBudgetIsNotTheStepBudget, :TestElementBudgetCountsElementsHeldNotProduced, :TestElementBudgetIsReleasedByEveryStep, :TestElementBudgetIsPerRun, robustness_test.go:collection_spends_the_element_budget, grpc/budget_test.go:TestNewServiceResolvesBudgets |
✅ Faithful |
| An activation ends with the body execution it belongs to, so what the calc usages read in a body computed is discarded when that execution ends rather than held for the whole run | runtime/statements.go finish/enterActivation, runtime/action_statements.go executeBody, runtime/invoke_calc.go runCalcBody |
action_activation_test.go:TestActionBodyActivationEndsWithTheBody, calc_usage_body_local_test.go |
✅ Faithful |
| A calc usage declared among a state machine's members binds its inputs from the values the machine has reached, as one in a calc's or an action's body does, so a guard reading it is answered over the running attribute rather than over what it was declared with | runtime/calc_usage.go enclosedByBehaviorBody, runtime/invoke_calc.go isStateSymbol |
conformance state_guard_reads_calc_usage |
✅ Faithful |
| An evaluation outside a body — a decision guard, an inline node expression, a transition guard, change condition or duration, an attribute default, a feature value default, an action argument, a constraint or requirement check — is a scope of its own, so what a calc usage answers it, and the elements a collection it evaluates materializes, live no longer than that step: the next guard reads the usage again over the values the step before it assigned. A read through a part's feature chain belongs to the evaluation making it and shares its activation | runtime/eval.go beginStep, runtime/state_executor.go evalStep, runtime/action_executor.go stepDecisionNode/stepActionExecutionNode/initializeAttributes, runtime/condition.go evaluateConditions, runtime/calc_usage.go calcUsageMemberValue |
conformance action_guard_reads_calc_usage; calc_usage_step_test.go:TestDecisionGuardReadsCalcUsagePerStep, :TestDecisionGuardsShareOneCalcUsageEvaluation, :TestPartChainReadBelongsToTheReadingActivation |
✅ Faithful |
A failing expression of literals alone is answered at the prompt with the failure itself, so sysml -e "(1,2,3)#(0)" reports the index rather than "no declarations loaded" |
repl/meta.go tryEvalLiteral, isLiteralAnswerError |
repl/runtime_commands_test.go TestEvalReportsTheAnswerOfALiteralExpressionThatFails |
✅ Faithful |
| A name the session declares is answered by that declaration, so the prompt's literal pass declines an expression using one rather than letting a library operation of the same unqualified name stand in for it | repl/meta.go tryEvalLiteral, declaresANameIn |
repl/runtime_commands_test.go TestEvalPrefersASessionDeclarationOverALibraryOperation |
✅ Faithful |
⚠️ A body parameter takes its type from the element type of whatever the operand
turns out to hold, which the expression checker does not track: an expression
over the parameter (xs.?{in e; e + 1}) therefore has no static type, and its
selector is checked at evaluation rather than where it is written. Statically the
checker reports what it can know — a selector whose result type is known and is
not Boolean, an index that is no whole number, a literal index of 0 or past a sequence
written out (passes/typecheck_expr.go inferIndex, inferSelect) — and the
runtime checks the rest, so no wrong answer results from what is left unchecked.
Found, not implemented — declared collection operations this runtime does not
evaluate. Each is a typed unresolved reference or unsupported error, never a
wrong answer:
| Not implemented | Why |
|---|---|
CollectionFunctions::'array#' and the Array/Matrix collections |
Needs a multi-dimensional array value; the runtime's collection values are a sequence and a set. |
ComplexFunctions::sum/product, VectorFunctions::sum/sum0 |
The other operations of both packages are implemented (see the numeric library table above); an aggregation over a collection of vectors or of Complex values is not, since a sequence of them flattens and loses the grouping it sums over. |
A reducer named rather than written (->reduce min, as the library's own minimize is defined) |
A function-valued name is not a runtime value: reduce takes the body expression form (->reduce {in a; in b; …}), and minimize/maximize are implemented directly rather than through reduce min. A named reducer is reported as a type error, not read as a body. |
SequenceFunctions::add/addAt/remove/removeAt, CollectionFunctions mutators |
These are behaviors, not functions: they declare an inout sequence, so they need mutable accumulation the language layer does not have. Deliberately out of scope. |
| Set coverage in the conformance corpus | No expression of the language produces a set today — a ValSet arises only through the embedding API — so the operations over a set are pinned at the unit level (TestCollectionOperationsOverSets, elementsOf) rather than by a .sysml fixture. A set-valued expression is separate work. |
at, first, reverse |
Not declared by the Kernel Function Library at all (head, #(1) and last are the declared spellings). Not implemented rather than invented. |
OpenSysML Extension Library (non-normative)¶
The OMG Kernel Function Library declares no exponential, no logarithm and no
two-argument arctangent: RealFunctions has sqrt/floor/round/abs/max/min/'**'/'^',
TrigFunctions has sin/cos/tan/cot/arcsin/arccos/arctan, and that
is all. The vendored OMG files stay byte-identical, so the missing signatures are
declared in a clearly non-normative OpenSysML extension instead:
internal/core/libs/stdlib/OpenSysML Libraries/OpenSysMLMathFunctions.kerml. It
is bundled by the same embed.FS as the vendored tree and enters the same
gates — TestStdlibConformance now reports 95/95 clean. It is OpenSysML code under
Apache 2.0, not OMG code under EPL-2.0; internal/core/libs/stdlib/NOTICE carves
the subdirectory out of the OMG notice.
Reachability. A model writes import OpenSysMLMathFunctions::*; (or calls
OpenSysMLMathFunctions::exp(x) qualified); both resolve like any other library
package, with no diagnostic. A bare exp(x) with no import is reported
unresolved reference: exp and does not evaluate: the name is legal only
under that import, since no OMG library declares it, so the call fails with
ErrUnimportedExtensionFunction naming the import rather than being answered by
a declaration the model never made visible. A bare sqrt(x) still evaluates —
the OMG function libraries are in force whatever a model imports.
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
exp(x) — e raised to the power x |
runtime/library_functions.go (math.Exp) |
TestLibraryFunctionValues |
✅ Faithful |
ln(x) — natural logarithm, defined for x > 0.0 |
runtime/library_functions.go naturalLog |
TestLibraryFunctionValues, TestLibraryFunctionErrors |
✅ Faithful |
log(x, base) — logarithm to an explicit base, so base 10 and base e are never confused; base 10 and base 2 use math.Log10/math.Log2, which are exact where the ratio of logarithms is not |
runtime/library_functions.go logToBase |
TestLibraryFunctionValues, TestLibraryFunctionErrors |
✅ Faithful |
atan2(y, x) — full-quadrant angle, parameters ordered as in IEEE 754 and math.Atan2 |
runtime/library_functions.go atan2Real |
TestLibraryFunctionValues, TestLibraryFunctionAtan2NamedArguments |
✅ Faithful |
ln(0.0), ln(-1.0), log(x, 1.0), log(-1.0, 10.0), atan2(0.0, 0.0) report a domain error; exp beyond the Real range reports an overflow |
runtime/library_functions.go |
TestLibraryFunctionErrors, TestRuntimeRobustness/extension_library_function_outside_its_domain |
✅ Faithful |
| The shipped declarations and the registered implementations cannot drift (names, parameter names, parameter order) | runtime/library_functions.go registry |
TestOpenSysMLMathFunctionsMatchTheShippedDeclarations |
✅ Faithful |
Evaluable from a calc def body under import OpenSysMLMathFunctions::*; |
runtime/invoke_calc.go |
calc_opensysml_math_functions.sysml + golden trace |
✅ Faithful |
| An unqualified call the model imports no declaration of fails with a typed error naming the function and the import that makes it legal, so the diagnostic and the behavior agree instead of contradicting each other | runtime/library_functions.go extensionLocalNames, unresolvedLibraryFunction, ErrUnimportedExtensionFunction, read by runtime/eval.go (unresolved-call dispatch) |
runtime/library_functions_test.go:TestUnimportedExtensionFunctionCallIsATypedError, TestLibraryFunctionUnqualifiedNames, TestRuntimeRobustness/calc_calls_an_unimported_extension_function |
✅ Faithful |
| The function listing covers every function the build implements, an extension one marked with the import its unqualified name needs, so a working function is advertised neither as unsupported nor as callable bare | runtime/builtin_names.go Builtin.RequiresImport, Builtins, read by repl/discover.go doBuiltins and repl/complete.go |
runtime/library_functions_test.go:TestBuiltinsListExtensionFunctionsWithTheirImport, repl/discover_test.go:TestBuiltinsListsAnExtensionFunctionWithItsImport |
✅ Faithful |
Static Expression Type Checking (KerML §7.4 Expressions, §8.3 Feature Values)¶
Checked before execution, at the type validation tier. Every rule is one-sided:
a diagnostic is reported only when both the expected and the actual type are
known, so unmodelled types never produce a false positive.
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
Scalar type lattice over ScalarValues |
semantics/exprtype.go PrimTypeOf/PrimConforms |
typecheck_expr_test.go |
✅ Faithful |
| Feature value conforms to declared type | passes/typecheck_expr.go checkUsageValue |
TestExprBindStringToIntegerAttribute |
✅ Faithful |
Arithmetic operand types (+ - * / % **) |
passes/typecheck_expr.go checkAddition/checkArithmetic |
TestExprAddIntegerAndStringRejected |
✅ Faithful (+ over two Strings is concatenation, per StringFunctions::'+'; a String with a number is rejected at this tier, and semantics.PrimConforms widens numerics only, so neither is coerced to the other) |
Boolean operand types (and or xor implies & \|, not) |
passes/typecheck_expr.go checkBinaryBoolean/checkUnaryBoolean |
TestExprAndOnIntegerRejected |
✅ Faithful |
Comparison operand types (< > <= >=) |
passes/typecheck_expr.go checkComparison |
TestExprComparisonOfBooleanRejected |
✅ Faithful |
Disjoint ==/!= operands (warning; '==' is declared over Anything) |
passes/typecheck_expr.go checkEquality |
TestExprEqualityAcrossDisjointTypesWarns |
✅ Faithful |
Boolean-valued contexts (constraint/assume/require, if/while/until, guards) |
passes/typecheck.go checkBehaviorMember (recursing into loop and branch bodies, so a nested condition is checked too) |
TestExprTransitionGuardMustBeBoolean, TestTypeCheckNonBooleanControlFlowConditions |
⚠️ Approximate (a condition whose type the expression checker can infer is checked here, before execution; a bare feature reference infers Unknown — see What We Don't (Yet) Support — and is caught by the executor instead) |
Change-event conditions (accept when <expr>, transition ... when <expr>) |
passes/typecheck.go checkTrigger |
TestExprAcceptWhenConditionMustBeBoolean |
⚠️ Approximate (accept when is always a condition; after transition ... when a bare name is a signal, so only expressions are checked there) |
Division/exponentiation result types (Natural/Natural -> Natural, Integer/Integer -> Rational) |
passes/typecheck_expr.go divisionResult |
TestExprWholeNumberDivisionAndPowerOK |
✅ Faithful |
Calc/action invocation arity, incl. inherited, partially redefined (:>>), and arrow-form receiver |
passes/typecheck_expr.go effectiveInParameters/checkArguments |
TestExprPartiallyRedefinedParametersKeepInheritedSignature |
✅ Faithful |
| Invocation argument types and named-argument names | passes/typecheck_expr.go checkArguments |
TestExprInvocationArgumentTypeMismatch |
✅ Faithful |
| No false positives on the shipped library and examples | corpus guard | model/typecheck_expr_corpus_test.go |
✅ Faithful |
| Non-scalar conformance of bound values (specialization hierarchy, enumeration literals) | passes/typecheck_value.go checkValueConformance |
TestValueUnrelatedInstanceDoesNot, TestValueEnumerationLiteralOfOtherEnum |
⚠️ Approximate (a value is typed only when it is a name or a literal; expressions producing an instance are not judged) |
| Multiplicity conformance of bound values | passes/typecheck_value.go checkValueCount, effectiveRange, wording shared with the runtime through semantics/multiplicity.go Range.CountViolation |
TestValueTooManyValuesForUpperBound, TestValueTooFewValuesForLowerBound, TestValueEmptyCollectionForLowerBound, TestValueCountAgainstRedefinedMultiplicity, TestValueNestedCollectionCountsItsElements, TestValueCollectionOfReferencesIsNotCountedStatically |
⚠️ Approximate (only a literal, or a collection literal of them, has a statically known element count — flattened as binding flattens it; a reference may itself be multi-valued, so it and any collection holding one are left to the runtime check when the feature value materializes) |
| Collection element types (each element against the feature's type) | passes/typecheck_expr.go checkUsageValue |
TestValueCollectionElementTypes |
✅ Faithful |
View and Viewpoint Members (SysML v2 §8.3.20 Views, §8.3.26 Viewpoints; SysML.xtext ViewRenderingMember, FramedConcernMember, StakeholderMember, ActorMember)¶
Each of these keywords owns a usage through a dedicated membership, and each
usage is written either as a reference to an existing element or as a
declaration introduced by the kind keyword the notation spells out. A
reference declares no name of its own: the name it answers to is its
reference's, derived by ast.EffectiveName (KerML §7.3.4.5), which is why a
reference to an inherited element is not a name conflict.
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
render owns a RenderingUsage through a ViewRenderingMembership: render asTree; references the rendering the view uses, render rendering r : AsTree; declares one. No ValuePart, and no definition form. The declaration's UsageDeclaration is optional, so it may be anonymous (render rendering : AsTree;) |
ast/defusage.go UsageViewRendering; parser/defusage.go parseDefUsage (render/frame dispatch) and parseReferenceMemberUsage; symbols/builder.go (SymbolRenderingUsage), passes/typecheck.go (typed by a rendering def), semantics/implicit.go (Views::Rendering), export/kinds.go (ViewRenderingMembership), export/rdf_in.go usageHead (which form to write back is decided by the reference, not the name, so an anonymous declaration keeps its kind keyword) |
parse/view_members.golden, export/testdata/convert/kind_keyword_synonyms.golden.sysml, parser/negative_test.go (render_definition, render_reference_value), passes/nameres_test.go TestRenderReferenceToInheritedRenderingIsNoConflict, TestRenderDeclarationOfInheritedNameConflicts, corpus 42. Views/* |
✅ Faithful (parse and naming; what a view renders is read from this member by view/view.go Renderer.KindOf — see the rendering rows) |
frame owns a ConcernUsage through a FramedConcernMembership: frame 'system breakdown'; references the concern framed, frame concern c : SafetyConcern; declares one, possibly anonymously (frame concern : SafetyConcern;). Its body is a requirement body |
ast/defusage.go UsageFramedConcern; parser/defusage.go (same dispatch, body parseRequirementBody); symbols/builder.go (SymbolConcernUsage), passes/typecheck.go (typed by a concern def), semantics/implicit.go (Requirements::ConcernCheck), resolve/document.go parameterizedByName, export/kinds.go (FramedConcernMembership) |
parse/view_members.golden, parser/negative_test.go (frame_definition), corpus 42. Views/Viewpoint Example.sysml |
⚠️ Approximate (frame concern SafetyConcern; follows the grammar's ConstraintUsageDeclaration and declares a concern usage named SafetyConcern; the reference to a concern of that name is frame SafetyConcern;. Framing is checked against the viewpoint's concerns by semantics/conformance.go Model.ViewConformance — see the viewpoint conformance row) |
stakeholder and actor own a PartUsage through a StakeholderMembership / ActorMembership; both are declarations (stakeholder se : Engineer;, actor driver : Person;) and neither has a definition form |
ast/defusage.go UsageStakeholder, UsageActor (the former ast.ActorMember node is gone); parser/defusage.go parseDefUsage; symbols/builder.go (SymbolPartUsage), passes/typecheck.go (typed by a part def), semantics/implicit.go (Parts::Part), runtime/context.go memberBindings (actor binding, name via ast.EffectiveName), export/kinds.go |
parse/view_members.golden, parse/requirement_members.golden, parser/behavior_test.go TestParseRequirementBody_Actor, parser/negative_test.go (stakeholder_definition, actor_definition, stakeholder_no_declaration, actor_no_declaration), runtime requirement_actor.sysml, corpus 41. Use Cases/*, 42. Views/* |
✅ Faithful (a stakeholder or actor definition is rejected: the notation has only the usage, typed by the party's definition) |
satisfy names the requirement satisfied (satisfy vehicleSpecification by vehicle;) or declares the satisfaction (satisfy requirement r : Req1 by v;); a view body's satisfy viewpoint; is the same form |
parser/defusage.go parseUsage UsageSatisfy branch (RequirementUsageKeyword UsageDeclaration?, then ValuePart?, then by) |
parse/satisfy_reference.golden (incl. the anonymous forms satisfy requirement by vehicle; and satisfy requirement : VehicleSpecification by vehicle;), parse/view_members.golden, parser/satisfy_subject_test.go |
⚠️ Approximate (the reference is recorded as a Subsetting rather than a ReferenceSubsetting, so passes/typecheck.go can require the target to be a requirement usage; a satisfy reference therefore takes no effective name, so semantics/conformance.go Model.SatisfyTarget reads the target from the Subsetting relationship instead) |
frame and render are also legal names (KerML has neither keyword; the Kernel Semantic Library writes in frame : SpatialFrame[1]). Only a name or the member's own kind keyword can follow the member keyword, so anything else — a multiplicity, a specialization, a type, a value, a body, ; — declares a feature named after the keyword |
parser/defusage.go atMemberKeywordUsedAsKeyword |
parse/view_members.golden (frame[0..1] : Engineer;, render :> frame;), libs/reserved_keyword_name_test.go, TestStdlibConformance |
⚠️ Approximate (the parser does not track the enclosing body kind, so the reading is decided by the following token alone: frame; inside a viewpoint declares a feature named frame instead of being diagnosed as a framing with no concern) |
expose in a view body is an Import |
see the Name Resolution section's expose rows |
parser/expose_test.go, resolve/expose_test.go, parse/view_expose.golden |
✅ Faithful (validateExposeOwningNamespace reports an expose outside a view usage — see the Name Resolution section's expose rows) |
Structural, Interface and Analysis Notation (SysML v2 §7.12 Ports, §8.2.2.14 Interfaces, §8.2.2.19 Analysis Cases, §8.3.9.11 Occurrences)¶
Notation exercised by the Open-MBEE corpus models (starkit, Dragon,
DesertKite/OOSEM, the spacecraft example notebooks). Conjugation is a semantic
relationship, not parser sugar: the ~ is kept on the typing relationship
(ast.Relationship.Conjugated) and the reversal of in/out is computed in the
semantics layer over the conjugation parity of the typing/specialization chain.
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
port p : ~P types a port by the conjugated port definition P::'~P' (§7.12.3); its features are P's with in/out reversed and inout unchanged, and conjugation composes, so a conjugate of a conjugate has P's directions |
ast/defusage.go Relationship.Conjugated; parser/defusage.go parseRelationshipClauseTarget; semantics/conjugation.go ConjugateDirection, superEdges, typeEdge, featureEdges, conjugatedSupertypes, PortFeatures, IsConjugated (a declaration's feature typing carries the conjugation, so it is read ahead of a redefinition clause written before it) |
parse/conjugated_port_type.golden, semantics/conjugation_test.go TestConjugationReversesDirections, TestDoubleConjugationRestoresDirections, TestConjugationOnRedefiningPort, parser/negative_test.go (conjugated_no_type, conjugated_no_type_after_name) |
✅ Faithful |
| A port usage conforms to the definition it conjugates, and two ports match when each named feature of one has a feature of the other with a conforming type and the conjugate direction (§7.12.2) | semantics/conjugation.go PortsConform, featuresMatchConjugate, featureTypesConform |
semantics/conjugation_test.go TestConjugatedPortConformance |
✅ Faithful |
| The ports at the two ends of an interface must have conjugate directed features: what one end sends the other receives | semantics/conjugation.go InterfaceEndPortMismatch, endPortFeatures; passes/constraint.go checkInterfaceEndConjugation (code port-conjugation) |
passes/constraint_test.go TestConstraintInterfaceEndConjugation, semantics/conjugation_test.go TestInterfaceEndConjugation |
⚠️ Approximate (reported as a warning, and only for an interface whose two ends both declare a resolvable port type; undirected features carry no flow, so they are not required to match; the ends of a connect/flow clause take their types by implicit redefinition of the interface's ends, which is checked as end identity, not as direction) |
Only a port usage or a connector end may be typed by a conjugated port definition, and ~ must name a port definition |
passes/typecheck.go checkConjugatedTyping |
passes/typecheck_test.go TestTypeCheckConjugatedTyping |
✅ Faithful |
An interface body may declare a default end with no declaration at all: end; is an anonymous port usage (§8.2.2.14.1 DefaultInterfaceEnd: isEnd ?= 'end' Usage) |
parser/parser.go bodyContext/pushBodyContext; parser/defusage.go parseAnonymousEnd, parseAnonymousEndUsage, anonymousUsageKind |
parse/end_usages.golden, resolve/analysis_test.go TestResolveDragonStructures |
✅ Faithful |
Anywhere else a bare end; is not standard notation: only DefaultInterfaceEnd makes the usage declaration optional, and every other end form (ReferenceUsage, EndUsagePrefix + a kind keyword) requires an Identification or a specialization part |
parser/defusage.go parseAnonymousEndUsage (typed diagnostic naming the fix, end ref;) |
parser/negative_test.go (end_outside_connector, end_outside_connector_package) |
✅ Faithful (see the known limitation on Dragon.sysml below) |
require Q::r { … } / assume Q::r { … } subset a requirement by a qualified reference, and the body may redefine a feature of it by its qualified (:>> R::f = expr) or its plain name (:>> f = expr), which the member inherits through the reference subsetting |
ast/behavior.go RequireMember.Reference, AssumeMember.Reference (a full *ast.QualifiedName, not a final segment); parser/behavior.go parseRequireMember, parseAssumeMember; resolve/document.go resolveConstraintReference, walkConstraintBody, lookupConstraintRefFeature |
parse/require_qualified_requirement.golden, resolve/analysis_test.go TestResolveQualifiedRequirement, TestResolveRequiredRequirementFeatureByPlainName, TestResolveRequiredRequirementUnknownPlainName, TestResolveQualifiedRequirementUnresolved, TestResolveQualifiedRedefinitionUnresolved, parser/negative_test.go (require_qualified_malformed_body, require_qualified_trailing_colons) |
⚠️ Approximate (only the braced spelling is read as a reference — see the known limitation below) |
A require/assume body is a namespace of its own, resolved to whatever depth it is written to: a declaration nested in it has its own body walked recursively, what the body declares is visible inward but does not leak outward, and the referenced requirement's features are offered to the body's direct members, which is what the reference subsetting inherits them to |
symbols/builder.go buildConstraintBodyScope, ConstraintBodyScope (a body-local child scope keyed by the member), read by symbols/bodyscopes.go, resolve/document.go walkConstraintBody and resolve/references.go |
resolve/constraint_body_test.go TestResolveTypoAtDepthTwoInARequireBody, TestResolveTypoAtDepthThreeInAnAssumeBody, TestResolveDeepRequireBodyResolves, TestResolveRequireBodyNamesDoNotLeakOutward, resolve/analysis_test.go TestResolveNestedRedefinitionPrefersOwnType |
✅ Faithful |
Every tier reads a require/assume body in that same body scope, so name resolution, type checking and condition evaluation agree on which declaration a name in the body denotes (a body-local name shadows one of the same name outside it) |
passes/typecheck.go checkBehaviorMember (AssumeMember/RequireMember route through symbols.ConstraintBodyScope), runtime/condition.go appendConditions, Condition.Owner (nearest owning scope, since a body-local scope owns no symbol) |
passes/typecheck_constraint_body_test.go TestTypecheckReadsARequireBodyLocalName, TestTypecheckReportsAMismatchNestedInARequireBody, TestTypecheckRequireBodyLocalNameShadowsTheEnclosingOne, TestTypecheckRequireBodyLocalValuesStayClean, runtime/condition_test.go TestRequireBodyConditionReadsABodyLocalName, runtime/conditions_of_test.go TestConditionsOfCarryScopeAndOwner |
✅ Faithful |
snapshot and timeslice are the two portion kinds of an occurrence usage (PortionKind, §8.3.9.11 OccurrenceUsage::portionKind); either prefix makes the declaration an occurrence usage whatever kind keyword follows. PortionUsage ends in Usage, whose declaration is optional, so an anonymous portion (timeslice;) is standard notation and is accepted without a diagnostic. Both keywords are read by the same prefix, so they agree on every declaration spelling, including a quoted name (snapshot 'launch event';) |
ast/defusage.go PortionKind, Usage.Portion; parser/defusage.go (portion prefix), parser/behavior.go (portion-prefixed behavior parameters); ast/dump.go; passes/typecheck.go declKind.portion, isOccurrenceUsage; export/rdf_out.go/rdf_in.go |
parse/occurrence_portions.golden, parser/occurrence_modifier_test.go, resolve/analysis_test.go TestResolveDragonStructures, parser/negative_test.go (timeslice_no_subject, timeslice_usage_no_type, timeslice_unterminated) |
✅ Faithful (parse, naming and resolution; a portion is not related to its whole occurrence at runtime — see below) |
The standard view/diagram library is part of the vendored stdlib, so view v : StandardViewDefinitions::gv; resolves |
libs/stdlib/Systems Library/StandardViewDefinitions.sysml (already vendored: the eight standard view definitions with their short names) |
model/standard_views_test.go TestStandardViewDefinitionsBundled, TestStdlibConformance |
✅ Faithful |
| A qualified reference whose first segment names no loaded namespace is reported as such, naming the declarations that do carry the trailing name | resolve/qualified.go (unresolved-namespace diagnostic), symbols/index.go FQNsEndingIn |
model/standard_views_test.go TestVendorViewNamespaceDiagnostic, resolve/analysis_test.go TestResolveMissingStandardViewNamespace |
✅ Faithful |
Every usage element subsets the most general base usage Base::things, whose that feature is therefore visible in a usage body (§7.6, [KerML 8.4.2]) |
semantics/implicit.go implicitBaseUsage, semantics/reference.go contributors |
semantics/implicit_test.go TestImplicitBaseUsageContributesThat, model/that_constraint_test.go TestThatResolvesInAssertedConstraint |
✅ Faithful (a member-contribution edge only: it is deliberately not a direct supertype, so conformance and DirectSupertypes are unchanged) |
The notation the Open-MBEE corpus models write stays clean at every validation tier: a conjugated interface and connection end, connect/flow between two such ends, and a portion prefixed onto a kind keyword and named as its whole occurrence is (item item1 { timeslice item item1; }) |
the rules above (parser/defusage.go end and portion prefixes, semantics/conjugation.go, passes/typecheck.go checkConjugatedTyping) |
passes/integration_test.go TestPassesGoldenCorpusNotation over testdata/passes/corpus_notation.golden ((no diagnostics)), parser/negative_test.go (conjugated_end_no_type) |
✅ Faithful |
A succession may be written with no keyword at the start of a namespace member: first a::b then c; (SuccessionAsUsage) |
parser/namespace.go parseMember, parseSuccessionAsUsage |
parse/succession_as_usage.golden |
✅ Faithful |
Each end of a binding connector is a ConnectorEnd, so it names a feature by a QualifiedName or by a feature chain each of whose chaining features is itself a qualified name (§8.2.2.9.2 BindingConnectorAsUsage/ConnectorEndMember, [KerML 8.3.3.2] OwnedReferenceSubsetting, OwnedFeatureChaining): bind A::b.C::d.e = F::g;. The chain is recorded segment by segment (nested ast.FeatureChainExpr whose Member is the full qualified name), and the end is a ReferenceSubsetting, so it resolves outside the connector rather than as an inherited redefinition |
parser/defusage.go parseUsage UsageBinding branch, bindingEnd, parseRelationshipTarget; resolve/document.go resolveFeatureChain (a qualified chaining feature resolves outward when the previous element has no such member) |
parse/binding_qualified_ends.golden, resolve/analysis_test.go TestResolveQualifiedBindingChain, parser/negative_test.go (binding_end_qualification_no_name, binding_end_chain_trailing_dot, binding_end_chain_trailing_dot_qualified, binding_end_unterminated, binding_end_no_target) |
✅ Faithful |
A connection usage may state its ends where its own name would go and then carry an ordinary body: connect x.p to r.p { … }, connect (a, b, c) { … } (§8.2.2.9.2 ConnectionUsage → ConnectorPart UsageBody). The keyword-less form is read by the same parseUsage path every declaration is, so the body's members are the usage's own members — nothing is parsed and discarded — and each end is a feature chain of any depth |
parser/defusage.go atDefUsageStart, parseDefUsage (connect branch), parseUsage (skipIdentification), parseTierBEnds, atConnectorShorthandEnds, atEndThenKeyword |
parse/connector_usage_body.golden, parser/negative_test.go (connect_body_unclosed, connect_to_no_target_before_body, connect_no_ends, connect_body_no_ends) |
✅ Faithful (the keyword is the ConnectorPart, so it states at least one end: connect; and connect { … } are reported. A multiplicity written after the keyword is the first end's, connect [1] a to [1] b) |
An interface usage may likewise state connector-style ends where its name would go, with or without a body: interface b1.p to b2.p { … }, interface differential.leftDiffPort to rearAxle.leftHalfAxle.axleToDiffPort; (§8.2.2.14 InterfaceUsage → InterfacePart), while interface named : Coupling connect a to b keeps stating its ends after connect |
parser/defusage.go parseUsage (skipIdentification, skipMultiplicity), parseTierBEnds, atConnectorShorthandEnds |
parse/interface_usage_shorthand.golden, parser/negative_test.go (interface_ends_no_target, interface_ends_no_to, interface_ends_unclosed_body) |
✅ Faithful |
A flow usage may carry a body after its ends, and its ends may be feature chains with the to on a continuation line: flow s1.x to s2.x { … } (§8.2.2.10 FlowUsage → FlowDeclaration UsageBody) |
parser/defusage.go parseUsage (skipIdentification via atFlowShorthand/from), atEndThenKeyword (the shorthand is recognized past a chain of any depth) |
parse/flow_usage_body.golden, parser/negative_test.go (flow_ends_no_target_before_body, flow_ends_unclosed_body) |
✅ Faithful |
A requirement-like body admits usage elements, so a connector, flow or message written with its kind keyword (connection connect r to x;) is a member of a requirement, constraint, concern, objective, use case or view body exactly as it is at package level (§8.2.2.19 RequirementBody → DefinitionBodyItem → UsageElement) |
parser/behavior.go parseRequirementMember, usageIsSubstantive (a connector or flow usage declares no name, so its ends are what make it substantive) |
parse/requirement_body_prefixed_usages.golden |
✅ Faithful |
Known limitations of this notation
- A body on a succession or on a control node is not read yet:
SuccessionAsUsageends inDefinitionBodyandMergeNode/DecisionNode/JoinNode/ForkNodeinActionBody, sofirst start then continue { … }andmerge continue { … }are standard notation, but OpenSysML requires;there. These are the outstanding false positives on the pilot validation corpus's3a-Function-based Behavior-1and5-State-based Behaviorfiles (a transition body is the same gap). Reading them needs body members onast.InitialNode, the four control nodes andast.SuccessionEdge/ControlFlowEdge, and lowering that executes them, so it is a feature of its own rather than a parser tweak, and no member is dropped in the meantime — the notation is reported, not silently accepted. Dragon.sysmldeclares bareend;members insideconnection def,flow defand nestedconnection defbodies (6 sites). This is not standard notation: aconnection def/flow defbody is an ordinaryDefinitionBody, whose members areNonOccurrenceUsageElement/OccurrenceUsageElement— neither includesDefaultInterfaceEnd(onlyInterfaceBodyItemdoes), and the only other keyword-less end,DefaultReferenceUsage, requires aUsageDeclaration. OpenSysML reports these with a typed diagnostic naming the conforming form (end ref;, whichReferenceUsagedoes allow) rather than inventing grammar.Dragon.sysml,OOSEM.sysmlandDesertKite.sysmltype their views by'SysML Standard Diagrams'::gv(7, 3 and 10 sites). No such namespace exists in the OMG release library or the pilot implementation — it is a tool-specific package, not part of the standard library — so it is not vendored under that name and no alias toStandardViewDefinitionsis fabricated. The diagnostic says the namespace is not loaded and points atStandardViewDefinitions::gv.- The two notations that had been suspected of being our false positives are
adjudicated as legal and are accepted: a conjugated end
(
end spacePort : ~CommunicationPort,spacecraft-example-model.sysml) and a portion prefixed onto a kind keyword (timeslice item item1,Dragon.sysml).ConjugatedPortTypingspecializesFeatureTyping(Systems Library/SysML.sysml:100), so any feature typing — a connection or interface end among them — may name a conjugated port definition; andPortionKindis an enumeration oftimeslice/snapshot(Systems Library/SysML.sysml:291) held byOccurrenceUsage::portionKind(:262), which anItemUsageis. Both parse, resolve and check clean over every tier, pinned bytestdata/passes/corpus_notation.golden, so neither notation is outstanding. What the models do still report is other notation:OOSEM.sysml(Open-MBEE/DesertKite.sysml, default branch) reports the three'SysML Standard Diagrams'::gverrors above and nothing else, andDesertKite.sysml— which lives only on that repository'sInitialDesignbranch — reports 7 errors that are ours, not the model's: a qualified name refused as abindend (3 sites, 6 errors) andconnection connect … ;refused inside arequirementbody (1 site). Both are parser defects owned by a separate session; they are not adjudicated here and no verdict above depends on them. - Only the braced spelling of a requirement-constraint reference sets
RequireMember.Reference/AssumeMember.Reference.CalculationBodyalso allows;, so standardrequire Q::r;is a reference too, but OpenSysML reads a body-lessrequire/assumemember as a condition expression, which the runtime evaluates as Boolean. Distinguishing the two spellings needs the name's resolution, not its syntax, and no spelling requires the referenced requirement's own conditions at runtime yet (runtime/condition.goappendConditionswalks only the member's own body), so the body-less form is left on the expression path rather than made a silent no-op. - Conjugation is not a runtime concept here: nothing is executed differently for a conjugated port, because ports carry no transfer semantics in the runtime yet (see "Major Features Not Implemented").
- A
snapshot/timesliceportion is recorded on the usage and resolves like any occurrence usage, but the runtime does not relate a portion to the occurrence it is a portion of, and no time ordering between portions is derived.
Usage Prefixes and KerML Classifier Declarations (SysML v2 §7.6 Usages, §8.3.9.11 Occurrences; KerML §8.2 Classifiers; SysML.xtext RefPrefix, IndividualUsage, PortionUsage, ForVariableDeclaration)¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
A lone occurrence modifier declares the kind of the usage it prefixes: individual i : V is an individual usage, snapshot s/timeslice ts/event e occurrence usages, and the modifier reaches the symbol kind rather than being recorded and ignored |
parser/defusage.go modifierImpliedKind (the kindless fallback of parseDefUsage), parser/behavior.go parseDirectionParameter; symbols/builder.go classifyUsage → usageSymbolKind |
parse/occurrence_modifier_anonymous.golden, parse/occurrence_individual_snapshot.golden, parse/occurrence_portions.golden, parser/occurrence_modifier_test.go:TestParseUsageOccurrenceModifiers, symbols/occurrence_modifier_kind_test.go:TestOccurrenceModifierDecidesSymbolKind |
✅ Faithful |
A modifier followed by a kind keyword and no name (individual part : Vehicle, ref item : Integer) declares an anonymous usage of that kind — the keyword is the kind, never the name, so individual part and individual item stay distinct — and the nameless form is reported so it can be named (ambiguous-modifier-kind), for the keywords the reading site actually consumes as a kind — ref frame : SpatialFrame names the usage frame, so it is not ambiguous |
parser/defusage.go warnAmbiguousModifierKind with declarationKindKeyword from parseDefUsage and parameterKindKeyword from parser/behavior.go parseDirectionParameter |
parser/modifier_kind_ambiguity_test.go:TestAmbiguousModifierKindWarns, :TestUnambiguousModifiedUsagesDoNotWarn, parse/occurrence_modifier_anonymous.golden |
✅ Faithful (a warning: the declaration is well-formed, Usage: UsageDeclaration? UsageCompletion, and the AST it yields is complete — only the name is missing) |
A parameter is a usage prefixed by its direction, and its declaration is optional, so in snapshot ; is an anonymous occurrence parameter that is registered as a member rather than dropped |
parser/behavior.go parseDirectionParameter; symbols/builder.go (anonymous member registration) |
parse/parameter_occurrence_modifiers.golden, symbols/occurrence_modifier_kind_test.go:TestAnonymousParameterBuildsSymbol |
⚠️ Approximate (a parameter with no kind keyword at all — in x : Real — is built as a part usage, while the same kindless declaration outside a parameter list is a plain feature (ast.UsageAttribute). Aligning the two is a one-line change to the kind default, but the two readings differ observably — the RDF export of every kindless parameter turns from sysml:PartUsage into sysml:AttributeUsage, and neither is the ReferenceUsage SysML v2 §7.6 actually names, which ast.UsageKind cannot yet carry — so the choice is escalated rather than taken) |
A for loop's variable is a usage declaration, so a keyword may name it (for step in c), with the reserved-keyword warning any keyword-named declaration gets, and a short name alone identifies it (for <v> in c) |
parser/behavior.go parseForAction (parseIdentificationStopping("in") rather than an Identifier token) |
parse/action_for_keyword_variable.golden, parser/for_loop_variable_test.go:TestForLoopVariableMayBeKeyword, :TestForLoopVariableMayBeShortNameOnly, :TestForLoopKeywordVariableWarns, :TestForLoopMalformedRecovers |
✅ Faithful |
A datatype declares a KerML DataType — a definition — so a feature can be typed by it whether or not it specializes anything (datatype D; as much as datatype Real specializes Complex;), as class, struct, assoc, behavior and interaction already were |
symbols/builder.go classifyUsage (the datatype keyword decides, ahead of the relationship-driven attribute classification) |
symbols/kerml_type_kind_test.go:TestKerMLTypeDeclarationsAreClassified |
⚠️ Approximate (a datatype is classified as an attribute definition, the SysML mapping of a DataType; a function stays a calcUsage, since the runtime resolves an invocation through it, so a function definition and a calc usage are still one kind) |
A classifier declares a plain KerML Classifier, and every definition is a Classifier — a DataType among them — so classifier C specializes D is well-formed whatever kind D is declared with, while the narrower class, struct and part def rows stay constrained to their own kind (KerML §8.3.2, §8.4.4.1; SysML v2 §8.4.5.1) |
passes/typecheck.go compatMessage (the declKind.isPlainClassifier row, told apart by the written keyword since classifier and class are both ast.DefClass) |
passes/typecheck_classifier_test.go:TestTypeCheckClassifierSpecializesAnyDefinitionOK, :TestTypeCheckPartDefSpecializesDataTypeStillRejected, :TestTypeCheckClassifierSpecializesUsageStillRejected |
✅ Faithful |
KerML has no definition/usage distinction — every declaration is a Type and a Specialization relates two Types (KerML §8.3.3) — so in a .kerml document class Person specializes Object is well-formed, and the SysML rule that only a definition may specialize does not apply. The target must still be a type: a package or an annotation is not one |
passes/typecheck.go compatMessage (the declKind.isKerML row of ast.RelSpecializes), declKind.lang from source.KindOf — the same file-kind mechanism the kerml-notation warning reads |
passes/typecheck_kerml_language_test.go:TestTypeCheckKerMLSpecializationClean, :TestTypeCheckSysMLSpecializationStillFires, :TestTypeCheckKerMLSpecializesNonTypeStillFires |
✅ Faithful |
A KerML FeatureTyping's type is any Type, a Feature among them (KerML §8.3.4.4), so in a .kerml document feature yy : y is typed by a feature and the SysML usage-kind taxonomy — which requires a definition — does not apply. A non-type target is still reported |
passes/typecheck.go compatMessage (the declKind.isKerML row of ast.RelTyping), isTypeKind |
passes/typecheck_kerml_language_test.go:TestTypeCheckKerMLTypingByFeatureClean, :TestTypeCheckSysMLTypingByFeatureStillFires, :TestTypeCheckKerMLTypedByNonTypeStillFires |
✅ Faithful |
| Only a Type may be a KerML specialization or typing target, and a Type is enumerated rather than assumed: every definition and usage kind and a KerML type declaration, but not a namespace, a dependency, an annotation or an alias — a resolvable alias is resolved to its target upstream, so one reaches the check only when it is cyclic and names no type. A kind added later is rejected until it is classified | passes/typecheck.go typeSymbolKinds, isTypeKind; checkTypeTarget (alias resolution) |
passes/typecheck_kerml_language_test.go:TestIsTypeKindIsAnAllowlist, :TestTypeCheckKerMLSpecializesAliasOfTypeClean, :TestTypeCheckKerMLSpecializesCyclicAliasStillFires |
✅ Faithful |
A metaclass is a Class (KerML §8.4.4), so metaclass AtomMetadata specializes Metaobject is a metaclass specializing a metaclass and is well-formed in either language; an unrelated definition kind is still a mismatch |
passes/typecheck.go defSymbolKind (ast.DefMetaclass → symbols.SymbolMetaclass, absent before, which made every metaclass declaration incomparable with its own kind) |
passes/typecheck_kerml_language_test.go:TestTypeCheckMetaclassSpecializesMetaclass, :TestTypeCheckMetaclassSpecializesPartDefStillFires |
✅ Faithful |
Name Resolution¶
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
| Inherited feature resolution | document.go:199 resolveRedefinition |
flow_payload_test.go |
✅ Faithful |
Declaration named with a keyword (action flow { ... }, attribute item : Integer) |
parser/defusage.go atKindPrefix, atSecondaryKind |
parser/namespace_keywords_test.go TestParseKeywordAsNameAfterKindKeyword, model/behavior_body_resolve_test.go TestKeywordNamedDeclarationIsReferenceable |
✅ Faithful (the name is kept and is referenceable) |
| Keywords reserved in name position (only an unrestricted name may spell one) | parser/namespace.go parseIdentification → Parser.Warnings, surfaced as passes.SeverityWarning code reserved-keyword-name by model/workspace.go |
parser/namespace_keywords_test.go TestParseKeywordAsNameIsReported, model/behavior_body_resolve_test.go TestReservedKeywordNameWarning, libs/reserved_keyword_name_test.go TestStdlibReservedKeywordNames |
⚠️ Approximate (reported as a warning, not an error, because the normative OMG library itself uses unquoted keyword names — step entry[1];, part done : Part;, attribute type : String[0..1]; — and must keep parsing clean; those eleven sites — including the Kernel Semantic Library's in frame : SpatialFrame[1], whose name SysML reserves for a view/viewpoint member — are pinned by the libs test so the set cannot grow silently) |
A keyword left in a parameter's name position names the parameter, quoted as KerML §7.2.4 requires (in 'type': Anything;, which the Kernel Function Library itself writes) or bare with the reserved-keyword warning, so the parameter is declared and its name resolves rather than the declaration failing to parse |
parser/behavior.go parseDirectionParameter (parseIdentificationStopping for the name, stopping on the post-multiplicity modifiers, rather than requiring an identifier token) |
parse/parameter_keyword_names.golden, resolve/parameter_keyword_name_test.go:TestResolveKeywordNamedParameter, :TestResolveKeywordNamedParameterUnresolved, parser/negative_test.go (keyword_named_parameter_no_type, parameter_default_no_value, parameter_redefines_no_target), conformance/calc_keyword_named_parameters.sysml, runtime/robustness_test.go:testCalcUnboundKeywordNamedParameter |
✅ Faithful (a keyword the parameter grammar reads as something else — a kind keyword, a relationship keyword, default, ordered, nonunique — still reads as that, not as the name) |
An event occurrence declares a feature like any other, so its name resolves from a value expression and from the trigger of a transition or an accept that names it; event <name> without the occurrence keyword references an existing occurrence (SysML v2 §8.3.13, SysML.xtext EventOccurrenceUsage), so a name that declares nothing is unresolved there |
parser/defusage.go parseUsage (the reference form is an ast.RelReferences relationship), resolved by resolve/target.go ResolveTarget and resolve/unqualified.go like any feature reference |
resolve/event_feature_test.go:TestResolveEventOccurrenceFeature, :TestResolveEventOccurrenceReferenceUnresolved |
✅ Faithful |
Keyword qualifying a kind keyword (var feature x, assert constraint { ... }, item part Shape) |
parser/defusage.go atKindPrefix, parseDefUsage |
parser/namespace_keywords_test.go TestParseKeywordBeforeKindKeywordIsNotAName |
✅ Faithful |
Words the grammar uses in one position only are not reserved: var marks a variable feature (KerML.xtext BasicFeaturePrefix, isVariable ?= 'var') and on is a literal in none of the pilot's grammars, so both name a feature everywhere else, as point does |
lexer/keywords.go (neither is in keywordList), parser/defusage.go atVarPrefix, atVarDeclaration |
lexer/lexer_test.go:TestContextualWordsAreIdentifiers, parser/contextual_keyword_name_test.go, parse/contextual_keyword_name_on.golden, parse/contextual_keyword_name_var.golden, parser/negative_test.go (state_named_on_no_semicolon, var_prefixed_declaration_no_type, var_prefix_without_kind_keyword) |
⚠️ Approximate (var with the kind keyword left out — KerML var a : Integer; — is still reported rather than parsed; the words listed under F8 of pilot-differential.md stay reserved) |
Binding connector ends (binding [1] bind [0..*] a.b = [0..*] c) |
parser/defusage.go parseUsage UsageBinding |
libs/reserved_keyword_name_test.go (the library's ShapeItems.sysml sites) |
✅ Faithful (bind is the ends keyword, formerly read as the connector's name) |
| Named argument resolution | document.go:205 (no name resolution) |
requirement_invocation_test.go |
✅ Faithful |
| Control flow node registration | builder.go InitialNode/FinalNode |
transition_first_test.go |
✅ Faithful |
| Redefinition target lookup | document.go:328 searchInheritedFeatureViaIndex |
localclock_test.go |
✅ Faithful |
| References in behavioral bodies (calc return, constraint/assume/require, assignment, entry/do/exit, transition guard and effect) | resolve/document.go resolveDecl |
model/behavior_body_resolve_test.go TestBehaviorBodyReferencesAreResolved |
✅ Faithful |
| State def bodies are state bodies whatever their first member is | parser/defusage.go DefState case (always parseStateBody) |
parse/state_def_region_pseudostate.golden (a state def whose first member is an attribute, followed by regions) |
✅ Faithful |
States a region declares with a body (state x { ... }) |
lower/state_graph.go collectRegionStates (memberships unwrapped, region and initial recorded) |
state_region_choice.sysml, region_pseudostate_test.go |
✅ Faithful |
| Substate, region, and named-pseudostate declarations | symbols/builder.go StateNode/StateRegion/PseudostateNode |
model/behavior_body_resolve_test.go TestBehaviorDeclarationsAreVisible |
✅ Faithful |
| Region-scoped state names (sibling regions may reuse a name) | symbols/builder.go StateRegion |
TestBehaviorDeclarationsAreVisible/sibling_regions_reuse_state_names |
✅ Faithful |
| Requirement actor declaration | symbols/builder.go (*ast.Usage of kind UsageActor) |
TestBehaviorDeclarationsAreVisible/requirement_actor_binding |
✅ Faithful |
Inherited member through a qualified-name segment (engine::'4cylEngine') |
resolve/qualified.go walkQualified → semantics.Model.LookupMember |
model/inherited_scope_resolve_test.go TestInheritedMembersAreVisible |
✅ Faithful |
Redefinition target that the redefinition shadows (part redefines engine) |
semantics/model.go inheritedFeature |
TestInheritedMembersAreVisible/nested_redefinition, TestRedefinitionDoesNotShadowItsTarget |
✅ Faithful |
Loop body as a namespace (loop { action a; } until a.x, for x in c { ... }) |
symbols/builder.go WhileLoopActionNode (including a for loop's iteration variable), resolve/document.go, passes/typecheck.go, resolve/references.go; at execution runtime/action_statements.go stmtEnv (a frame per entered block) |
TestBodyLocalDeclarationsAreVisible, TestBodyLocalNamesDoNotEscape, runtime/robustness_test.go:loop_body_declaration_does_not_leak |
✅ Faithful |
Body-expression parameters (c->forAll { in i : Positive; f(i) }) |
symbols/bodyscopes.go buildBodyScopes (scope linked into the document tree), read back by symbols.BodyExprScope in resolve/document.go and resolve/references.go |
TestBodyLocalDeclarationsAreVisible/body_expression_parameter, lsp TestRenameLeavesBodyExpressionParameters, TestRenameBodyExpressionParameterFromDeclaration, TestDefinitionBodyExpressionParameter |
✅ Faithful |
Features of the stdlib base type of an untyped usage (state normal; → States::StateAction::done) |
semantics/implicit.go implicitUsageBases, Model.implicitBase via semantics/model.go DirectSupertypes |
model/implicit_typing_test.go TestImplicitUsageBaseTypes, TestInheritedMembersResolveThroughUntypedUsage, semantics/implicit_test.go, lsp/implicit_typing_test.go |
⚠️ Approximate (the implicit base is the stdlib base definition of the usage kind, not the base feature it subsets, since library index records carry no specialization edges; connector/succession/flow/binding/satisfy/subject/objective usages take their type from what they relate to and get no base) |
Implicit redefinition of behavior/step parameters by position (out item image; in action focus : Focus redefines Focus::image), KerML 7.4.7.2/7.4.7.3, SysML v2 7.17.2 |
semantics/redefinition.go Model.implicitParameterRedefinitions, Model.parametersOf, reached from semantics/model.go DirectSupertypes |
semantics/redefinition_test.go, model/implicit_typing_test.go TestParameterRedefinitionAccompaniesTheImplicitBase, TestImplicitRedefinitionSuppliesInheritedMembers, passes/typecheck_expr_test.go TestExprRedeclaredParametersMatchByPositionNotName (the invocation signature matches by the same rule) |
✅ Faithful (owned parameters in lexical order redefine the parameter at the same position of each general behavior or step, matching direction; parameters a single general behavior leaves un-redefined are inherited after the owned ones; the kind's standard library base still applies alongside the redefinition, since the redefined parameter may itself be untyped) |
Implicit redefinition of a result parameter (return redefines the general calculation's result whatever its position), SysML v2 7.19.2 |
semantics/redefinition.go Model.implicitParameterRedefinitions (ast.Usage.IsResult, set by parser/behavior.go parseResultMember) |
semantics/redefinition_test.go TestImplicitResultParameterRedefinition |
✅ Faithful |
| A nested usage that is not a parameter and shares a name with an inherited feature | resolve/document.go Resolver.checkInheritedNames, conflictable, parameterizedByName, surfaced by passes/nameres.go (code name-conflict); the usage still only gets the standard library base of its kind from semantics/implicit.go |
passes/nameres_test.go TestNameResolutionPassReportsInheritedNameConflict, TestRedeclaredInheritedNameIsNoConflictWhenRedefined, TestInheritedNameConflictExemptsRedefiningFeatures, TestDistinctNestedNameIsNoConflict, model/implicit_typing_test.go TestLikeNamedUsageIsNotAnImplicitRedefinition, resolve/inherited_names_test.go TestRedeclaringAnInheritedNameConflictsUnlessItRedefines, TestRequirementParametersAreNotNameConflicts, TestRedefinitionTargetFoundTwoSupertypesUp (the target is searched up the whole specialization chain, not only the direct supertype) |
⚠️ Approximate (SysML v2 7.6.1 and KerML 7.3.2.1 make this a name conflict, reported at the name-resolution tier where inheritance is known; a feature that redefines what it shares the name with — explicitly, or implicitly by parameter position — does not conflict. The subject, actors and stakeholders of a case or requirement redefine the inherited ones by name (SysML v2 7.18.4, 7.19.4), which is not modelled and is not distinguishable from an ordinary feature at this tier, so the rule is not applied inside a case or requirement body at all (a concern and a viewpoint are requirements too) — a genuine conflict on an ordinary feature there goes unreported too) |
Effective name of an unnamed redefining feature (in item; in action shoot : Shoot is named image), KerML 7.3.4.5, SysML v2 7.6.5 |
symbols/builder.go effectiveIdent for a declared redefinition; for an implicit one resolve/unqualified.go Resolver.implicitlyNamedMember (with impliesNamingFeature), reached from walkUnqualifiedHiding and resolve/target.go memberChain, over symbols/scope.go Scope.AnonymousMembers and semantics/model.go DirectSupertypes |
passes/nameres_test.go TestImplicitlyRedefiningParameterBindsRedefinedName, TestImplicitlyRedefiningParameterDoesNotBindItsKeyword, symbols/builder_test.go TestUnnamedRedefinitionTakesRedefinedName |
✅ Faithful (the name is bound in the owning scope, so a sibling resolves it by simple name; an implicit redefinition's target is known only to the semantic model, so that binding is resolved lazily rather than when scopes are built) |
Implicit redefinition of connection/association ends by position (connection : PressureSeat connect bead references t.bead to ... redefines PressureSeat::bead), SysML v2 7.13.2, 7.14.2, KerML 7.4.6 |
semantics/connector.go Model.implicitEndRedefinitions, Model.endsOf, reached from semantics/model.go DirectSupertypes; the ends themselves are declared by symbols/builder.go buildConnectorEnds (ast.ConnectorEnd.DeclaredName) and the arity check is passes/constraint.go checkConnectorEndRedefinition (Model.UnmatchedConnectorEnds) |
semantics/connector_test.go (including TestImplicitEndRedefinitionOfAssociationUsage), model/connector_ends_test.go TestConnectorEndNamesResolve, TestConnectorEndArityMismatch |
✅ Faithful (an end of a connect clause that reference-subsets what it attaches to declares an end of the connector, in lexical order, and redefines the end at the same position of each connector the usage specializes; positions count every end of the clause, including one that only names what it attaches to; an explicit :>> governs, and an end past the general connector's last position is reported. Connection, interface, allocation, flow, succession, association and binding declarations are matched — a general whose own ends are not enumerable, such as an unparsed library type, suppresses the arity check rather than reporting) |
Reference subsetting contributes members (perform action takePhoto references takePicture;, perform providePower.generateTorque;) |
semantics/reference.go Model.ReferencedFeature, Model.MemberSources, consumed by semantics/members.go MembersOf/LookupMember; targets resolved by resolve/target.go ResolveTarget/ResolveReferenceTarget |
semantics/reference_test.go, resolve/target_test.go, model/perform_reference_test.go, parse/perform_reference.golden, runtime/testdata/conformance/action_perform_reference.sysml, runtime/robustness_test.go (perform_of_missing_action, perform_reference_cycle) |
✅ Faithful (a member-contribution relation, deliberately not a generalization — see below) |
Effective name of an unnamed feature that reference-subsets (perform providePower.generateTorque; declares generateTorque) or redefines (part :>> engine;, equivalently part redefines engine;, declares engine) |
ast/namespace.go NamingFeature, EffectiveName, TargetName, used by symbols/builder.go effectiveIdent, namingTargetNode, lower, runtime and passes; the reference that named a feature is hidden from its own resolution by symbols/symbol.go Symbol.NamingTarget and resolve/target.go refFilter |
symbols/perform_test.go, symbols/builder_test.go TestUnnamedRedefinitionTakesRedefinedName, TestRedefinitionDoesNotOverrideDeclaredName, TestReferenceSubsettingOutranksRedefinitionAsNamingFeature, TestTwoRedefinitionsLeaveFeatureAnonymous, TestShortNameLeavesTheNamingFeatureInPlace, resolve/document_test.go TestRedefinitionTargetSkipsTheNameItGaveAway, model/perform_reference_test.go |
✅ Faithful (a reference subsetting names the feature, and otherwise its single owned redefinition does; a declared name governs, and more than one redefinition leaves the feature anonymous. A declared short name does not suppress the derived name, since KerML derives effectiveName from declaredName alone. The naming feature's own effective name is approximated by the reference's last segment, since resolution has not run when scopes are built. A value on a member left anonymous by more than one redefinition reaches none of them, which passes/constraint.go checkUnnamedRedefinitionValue reports as a warning, code redefinition-no-derived-name, tested by passes/constraint_test.go TestConstraintUnnamedRedefinitionValue) |
A reference subsetting resolves outside the name it contributes, while the members its owner inherits and imports stay visible (part v : V { perform 'provide power'; }) |
resolve/target.go refFilter, Resolver.ResolveReferenceTarget, threaded through resolve/unqualified.go walkUnqualifiedHiding and applied in resolve/document.go resolveRelationships, resolve/references.go refCollector.relationships (via model.Workspace.ResolveReferenceInDoc) and runtime/invoke_action.go resolveActionSymbol; the inherited half is semantics/members.go Model.LookupContributedMember |
resolve/target_test.go TestReferenceTargetSkipsSelfBinding, semantics/reference_test.go TestPerformOfInheritedAction, lsp/definition_test.go TestDefinitionPerformChainMember (a chain member resolves through its operand, resolve.Reference.Chain), semantics/reference_test.go TestReferenceFindsSiblingDeclaredAfterIt, model/perform_reference_test.go (perform shadowing the action it performs), lsp/definition_test.go TestDefinitionPerformReference, runtime TestPerformShorthandRunsTheReferencedAction, conformance/action_perform_shorthand.sysml |
✅ Faithful |
The perform X; shorthand is an action node named X |
lower/action_graph.go getNodeName |
conformance/action_perform_shorthand.sysml (then start increment; names the perform statement) |
✅ Faithful |
N-ary connector ends (connection link connect (a, b, c)), SysML v2 7.13.2, 8.3.13 |
parser/defusage.go parseConnectorEnds (parenthesized end list, reached by both the named declaration and the anonymous connect …; body member); passes/constraint.go checkConnectorEnds (arity by kind); lower/connection.go lowerConnections, PeerPorts |
parse/connection_nary.golden, parser/connector_ends_nary_test.go TestParseNaryConnectorEndsKeepsEveryEnd, parser/negative_test.go (nary_connect_unclosed, nary_connect_trailing_comma, nary_connect_empty), passes/constraint_test.go TestConstraintConnectionNaryEndCountReachesTheChecker, lower/connection_test.go TestLowerNaryConnectionKeepsEveryEnd and TestLowerAnonymousNaryConnectionKeepsEveryEnd, parser/connector_ends_nary_test.go TestParseAnonymousInlineConnectKeepsEveryEnd, conformance/action_port_communication_nary.sysml and action_port_communication_nary_anonymous.sysml |
✅ Faithful (a connection, connector, interface or allocation keeps every end of a parenthesized list end to end — parse, constraint tier, lowering and port routing — whether or not it declares a name, and an interface or allocation beyond two ends is reported) |
Anonymous binary allocation (allocate torqueGenerator to powerTrain) |
parser/defusage.go atAllocateShorthand |
parse/perform_reference.golden |
✅ Faithful (both names are connector ends; formerly the first was read as the usage's name) |
An object of a connector usage holds the features it connects at its ends (connection link : Link connect a.p to b.q makes link.source be a.p), KerML 7.4.6, SysML v2 7.13.2 |
runtime/connector.go materializeConnectorFeatureValue, materializeConnector, attachConnectorEnd, bindEndFeatureValue, bindParticipants, reached from runtime/instance.go GetFeatureValue; end features synthesized by runtime/shape.go connectorEndFeatures; attachments and effective end names by semantics/connector.go Model.ConnectorEndAttachments, Model.IsConnectorUsage; inherited ends aliased by runtime/subsetting.go over Model.ImplicitEndRedefinitions |
connector_test.go (TestConnectorEndsAreTheConnectedFeatures, TestWritingAConnectedPortIsReadThroughTheEnd, TestConnectorEndFollowsAFeatureChain, TestConnectorEndAttachesToAPart, TestNaryConnectorKeepsEveryEnd, TestRedefinedEndSharesTheInheritedFeatureValue, TestEveryConnectorKindAttachesItsEnds), conformance/connector_end_identity.sysml (identity assertions), semantics/connector_test.go:TestConnectorEndAttachments, robustness_test.go:unattachable_connector_end, multiplicity_on_a_connector, connector_attached_to_itself, mutually_attached_connectors |
✅ Faithful (an end holds the very object the connector attaches to, so writing the connected port is read through the end and two connectors on different ports are distinguishable; ends are attached in declaration order, including n-ary and nested feature chains and an end attached to a part; an end that names no reachable feature is a typed ErrConnectorEnd with a source location rather than a fresh object or <unknown>, an end naming the connector it belongs to — directly or through another connector — is ErrCyclicFeatureValue, and a connector usage holding more than one connector is reported with where it was written) |
An untyped or anonymous connector usage materializes on the standard library base of its kind (interface iface connect a.p to b.q;, connect a.p to b.q;), SysML v2 7.13.2, 8.3.13 |
semantics/implicit.go implicitUsageBases (Connections::Connection, Interfaces::Interface, Allocations::Allocation); runtime/connector.go connectorBaseOf, anonymousConnectors; symbols/builder.go usageSymbolKind (a KerML connector is a connection usage) and runtime/shape.go isFeature (an allocation usage is a feature) |
conformance/connector_end_identity.sysml, ballandchain_interface_connected.sysml, connector_test.go (TestUntypedConnectorUsageMaterializes, TestAnonymousConnectorJoinsItsEnds, TestAnonymousConnectorIsMaterializedOnce, TestAnonymousSuccessionIsNoConnector, TestEveryConnectorKindAttachesItsEnds), parse/connection_implicit_type.golden |
✅ Faithful (a connection, interface, allocation or connector usage that names no definition is an object of its kind's library base with its ends attached, named form and anonymous form alike, and an anonymous one materializes once per object; a flow or binding states its ends by other syntax and is not a connect connector — its ends reach routing through lowering, not through connector-end feature values) |
A flow usage (flow f from a.out to b.in) and a binding usage (binding b bind a.p = b.p) are connectors of the kernel layer, but state their ends in their own syntax — Usage.FlowEnds, and a binding's source/target relationships — rather than in a connect clause |
parser/defusage.go parseFlowEnds and the UsageBinding clause; resolve/document.go isImplicitCalcResult; lower/connection.go (flow ends reach routing through lowering); lower/binding.go ToBindings/lowerBinding; runtime/binding.go objectBindings/resolveBindingValue/resolveBindingSet/attemptBinding/resolveBindingLocation; runtime/instance.go materializeFeatureValue; runtime/invoke_calc.go resultBindingExpr; semantics/connector.go Model.IsConnectorUsage deliberately covers only the connect forms |
parse/connection_implicit_type.golden (flow f from a.p to b.p;, binding bnd bind a.p = b.p;), lower/connection_test.go, lower/binding_test.go (TestToBindingsKeepsMultipleContributors), runtime/testdata/conformance/binding_value_forward.sysml, binding_value_reverse.sysml, binding_nested_end.sysml, binding_nested_end_reverse.sysml, binding_expression_end.sysml, binding_multivalued.sysml, binding_calc_result.sysml, binding_calc_result_reverse.sysml, binding_object_end.sysml, runtime/robustness_test.go binding cases (including exact collection-conflict, multiple-contributor, element-budget and distinct-object cases) |
⚠️ Approximate (a flow between action nodes carries its value through lowering; a binding declared in a materialized type/usage body is lowered to a bidirectional runtime value identity, including inherited and nested ends, expression-valued endpoints, exact sequence/set equality, multiplicity handling, element-budget charging during propagation, lazy adoption of an unmaterialized composite endpoint by the read-side object, conflict/cycle errors, and calc result binding. A named binding that states only one end — including binding bnd = x;, bind x;, and binding bnd of x; — lowers to no runtime binding, and bindings owned directly by packages/namespaces are not applied until namespace objects are materialized. Several bindings supplying unequal scalar values report a typed conflict; multiple bindings contributing to a multi-valued end are not yet element-wise merged and report a typed ErrBindingEnd instead of silently selecting one.) |
The of clause of a binding (binding b of full = level) names the feature the binding binds, so it is a reference subsetting rather than a typing (KerML 8.3.3.3.9, SysML v2 8.3.13) |
parser/defusage.go (the UsageBinding of clause records ast.RelReferences) |
parser/binding_of_test.go:TestBindingOfTargetIsAReference, parse/constraint_parameterised_conditions.golden |
✅ Faithful (formerly recorded as a typing, which reported the bound feature as "type must be a definition") |
A declared name wins over an effective one in the same namespace (part v { perform p; action p; }) |
symbols/scope.go PreferDeclared, used by LookupLocal and resolve/qualified.go's segment walk; symbols/builder.go (Symbol.EffectiveName) |
semantics/reference_test.go TestReferenceFindsSiblingDeclaredAfterIt, TestQualifiedNameThroughEffectiveNameIsNotAmbiguous, TestRepeatedPerformResolvesToTheAction |
✅ Faithful |
individual def X :> PartDef, x : IndividualDef kind compatibility, SysML v2 7.9.4 |
passes/typecheck.go occurrenceDefSymbolKinds/isOccurrenceDefKind (specialization) and isCompatibleTyping (typing) |
passes/typecheck_individuals_test.go, corpus gate (Verification Case Usage Example now clean) |
✅ Faithful (an individual def is an occurrence definition, so it may specialize an occurrence definition of any kind and may type a usage wherever an occurrence definition may; specializing a data type — an attribute or enumeration definition — stays an error per 8.4.5.1, and a usage kind that rejects an occurrence definition, such as a port usage, still rejects an individual definition) |
individual / snapshot usage modifiers (individual testSystem : TestSystem, snapshot occurrence takeoff : Flight), SysML v2 7.9.4, abstract syntax 8.3.9.11 (OccurrenceUsage::isIndividual, OccurrenceUsage::portionKind) |
ast/defusage.go Usage.IsIndividual/Usage.IsSnapshot, stored by parser/defusage.go parseUsage and parser/behavior.go parseDirectionParameter; consulted by passes/typecheck.go declKind.isOccurrenceUsage, compatMessage and isCompatibleTyping |
parser/occurrence_modifier_test.go, parse/occurrence_individual_snapshot.golden, parser/negative_test.go (individual_modifier_no_member, individual_usage_no_type, individual_usage_no_body, snapshot_usage_no_type, individual_parameter_no_type), passes/typecheck_individuals_test.go TestTypeCheckOccurrenceModifierWidensTypingOK, TestTypeCheckOccurrenceModifierRejectsDataType, TestTypeCheckDataTypeTypingWithoutModifierOK |
⚠️ Approximate (the modifier is orthogonal to the keyword that declares the usage, so individual part p is a part usage that is an individual, and either modifier makes the usage an occurrence usage: it may be typed by an occurrence definition of any kind and may not be typed by a data type — an attribute or enumeration definition — per 8.4.5.1. An individual occurrence takes Occurrences::Life as its implicit base (semantics/implicit.go implicitBase). The modifier is not yet reflected in the usage's symbol kind, so individual testSystem is still indexed as an attribute usage — the typing widening compensates) |
if/else branch bodies as namespaces |
ast/behavior.go IfBranchNode (parsed by parser/behavior.go parseIfBranch), symbols/builder.go IfActionNode/IfBranchNode, resolve/document.go, symbols/bodyscopes.go, resolve/references.go |
TestBodyLocalDeclarationsAreVisible/if_branch_body_reads_its_own_declaration, /else_branch_reuses_the_then_branch's_name, TestBodyLocalNamesDoNotEscape/if_branch_member_from_outside, /else_branch_member_from_the_then_branch, parse/action_if_branch_body.golden, lsp/if_branch_test.go, resolve TestImportRecursiveSkipsBodyLocalNames, repl TestLookupInScopeTreeSkipsBodyLocalNames |
✅ Faithful (each branch owns a body-local scope: names declared in a branch resolve inside it, do not escape to the enclosing behavior or to the sibling branch, and — like loop bodies — are excluded from recursive imports and the REPL scope-tree search; the condition is evaluated before either branch is entered, so it resolves in the enclosing scope only) |
| Transition source/target names | resolve/transition.go (*Resolver).ResolveEndpoint, walked from resolve/document.go (TransitionMember) |
resolve/transition_test.go |
⚠️ Approximate (resolved as references at the name-resolution tier, so a misspelled endpoint reports there with a suggestion and lowering consumes the resolved declaration; the leniencies are listed with the state machine row above — an unqualified endpoint falls back to the first vertex of the machine whose name path ends in it, and an endpoint naming no vertex leaves its edge out of the graph rather than failing the lowering) |
Signal trigger names (when sigX) |
— | TestBehaviorDeclarationsAreVisible/signal_trigger |
⚠️ Approximate (a bare trigger name is an injected event, not a declared element, so it is deliberately not resolved) |
Payload feature a flow/message declares in its of clause (message m of fuelCommand : FuelCommand) |
parser/defusage.go parseFlowEnds (declaration recorded as FlowEnds.PayloadDecl and kept as a member of the flow), resolve/document.go (the of name resolves in the flow's own scope) |
parse/flow_payload_declaration.golden, model/flow_payload_resolve_test.go TestDeclaredFlowPayloadIsAMember, TestFlowPayloadReferenceStillResolvesOutward |
✅ Faithful (the declared payload is a member of the message, so the of name and m.payload both resolve; the reference form of Type still resolves in the enclosing scope) |
| Accept-parameter visibility to sibling action nodes | runtime/action_executor.go the action's shared feature space |
action_accept_message.sysml |
⚠️ Approximate (the executor binds the payload into the action's shared feature space, which scoping does not model: a sibling node reading the parameter by simple name is reported unresolved) |
Unqualified library names in files that do not import their library (Boolean, Real) — only the public top-level members of a root namespace are globally visible, and a library member is not one ([SysML, 7.2] over [KerML, 8.2.3.2, 8.2.3.5]) |
resolve/suggest.go unresolvedMessage (the diagnostic names the qualified spelling) and resolve/fixes.go importFix (the offered fix writes private import X::*;, an import that serves the importing namespace without re-exporting onward) |
model/suggestion_test.go, model/examples_test.go, resolve/fixes_test.go TestTheImportFixWritesAPrivateImport, lsp/codeaction_test.go TestCodeActionImportsResolvableFQN |
✅ Faithful (such a name is genuinely unresolved: a model imports the library or qualifies the name, as OMG's own training files do; every surface says which qualified name was meant and offers both spellings) |
A member chain from that reads the object featuring the value being written, not the library declaration's own members ([SysML, 7.2] over [KerML, 8.4.2]) |
resolve/document.go featuringOf (a chain whose operand is Base::things::that resolves its members against the usage enclosing the expression, so both the usage's own members and those it inherits are reached through lookupMember), runtime/eval.go evalName (that evaluates to the object being evaluated) |
model/that_test.go TestThatChainsThroughTheFeaturingType, runtime/that_test.go TestThatReadsTheFeaturingObject, TestThatReadsTheInnermostFeaturingObject |
✅ Faithful (that.a resolves and evaluates through the featuring object, the innermost enclosing usage wins, an unowned member is unresolved member, and a that written where no usage encloses it stays unresolved rather than resolving to the library feature) |
A namespace re-exports what it imports with import X::*, transitively and wherever the name X resolves (KerML::Element, where KerML imports Kernel::*, which imports Core::*, which imports Root::*; KerML 7.2.5, 8.2.3.5) |
symbols/index.go ExpandWildcardImports (repeats expandRound to a fixpoint over the importers in name order, deriving the re-exports of the ones a change reached and dropping those its imports no longer support) and resolveWildcardTarget (searches the importing package's enclosing namespaces before the global one) |
symbols/index_test.go TestExpandWildcardImportsChainsAndIsOrderIndependent, TestExpandWildcardImportsPrefersTheEnclosingTarget, TestExpandWildcardImportsFollowsAReexportedTarget, TestExpandWildcardImportsIgnoresAnAmbiguousTarget, libs/loader_cache_test.go TestParsedAndRestoredIndexesAreEquivalent, model/training_examples_test.go TestTrainingExamplesCacheStateIndependent |
✅ Faithful (a chain of imports is followed to its end, and the result does not depend on iteration order or on whether the library was parsed or restored from the on-disk index cache; a target name resolves against the importing namespace's own imported memberships — wildcardTargetAt follows a name an earlier import re-exported to the FQN it was declared under — before the global namespace) |
The names a document's own root-level import X::* surfaces are visible in the scope tree that document builds for the editor, whether or not it declares anything else — a bare import at the REPL prompt included |
symbols/members.go SetDocName / DocNameOf (the tree carries the document name itself, no member being needed to hold it), read by resolve/filter.go documentOf |
resolve/imports_test.go TestRootImportInDocumentDeclaringNothingElse |
✅ Faithful |
A membership import surfaces the imported member under both of its names — the declared name and the short name, so import SI::kilogram makes kilogram and kg resolve — and under no other member's short name: what the import adds is the target's Membership, which carries a memberName and a memberShortName (KerML 8.2.4) |
resolve/unqualified.go matchImport (the target is found by member lookup, which knows both names, instead of by comparing the written last name part with the name being resolved) with importPrefixAvailable, which keeps that wider match from re-entering an import whose own prefix has no binding to resolve through |
resolve/imports_test.go TestMembershipImportAcceptsDeclaredAndShortNames (kg resolves; the sibling <g> gram the import does not name stays unresolvable), TestImportMembership, TestImportDoesNotLeakNonImported |
✅ Faithful |
An import carries a visibility indicator: the pinned OMG pilot grammars make it mandatory (fragment ImportPrefix : visibility = VisibilityIndicator 'import' ..., KerML.xtext:169-172, SysML.xtext:241-244, with no ? unlike the sibling MemberPrefix), and all 574 imports in the 254 OMG-authored corpus files carry one |
passes/import_visibility.go ImportVisibilityPass at LevelSyntax, code syntax/import-visibility, span on the import keyword; no parser or AST change — ast.Import.Visibility already records VisibilityDefault for the bare form |
passes/import_visibility_test.go, testdata/passes/import_no_visibility.sysml + golden, lsp/diagnostics_test.go TestPublishDiagnosticsReportsBareImportAsWarning |
⚠️ Approximate (reported as a warning, not an error: the bare form is unambiguous to parse, so hard-failing would reject existing models over notation. It does not gate the name-resolution, type or constraint tiers, and an expose is exempt — the pilot grammar gives it implicit protected visibility, SysML.xtext:2366-2372) |
A namespace declaration is KerML notation: namespace is a literal in KerML.xtext only (:125, NamespaceDeclaration), and a SysML file's root is RootNamespace : PackageBodyElement* (SysML.xtext:38), which admits no namespace declaration |
passes/nonstandard_notation.go NonstandardNotationPass at LevelSyntax, code kerml-notation, span on the namespace keyword; source/kind.go Kind/KindOf tells a .sysml file from a .kerml one |
passes/nonstandard_notation_test.go TestNamespaceInSysMLIsKerMLNotation, TestNamespaceInKerMLIsSilent, TestNamespaceInAnUnnamedDocumentIsKerMLNotation, repl/notation_test.go |
⚠️ Approximate (reported as a warning, not an error, and not in .kerml: both namespace N; and namespace N { … } are legal KerML, so the construct stays parsed and silent there. The REPL and CLI buffer is one document of no file kind, so it takes the SysML reading its prompt takes, and repl/session.go dropKerMLNotationOfKerMLFiles drops the finding for a snippet loaded from a .kerml file) |
Notation OpenSysML accepts that no pinned production admits — the state pseudostates (choice, junction, the history forms, entry/exit point), region, defer, the initial/final state markers, the initial/final/decision action-node spellings, and transition <src> to <tgt> — is diagnosed rather than silently accepted; the audit with citations is reference/grammar/conformance-audit.md |
passes/nonstandard_notation.go NonstandardNotationPass at LevelSyntax, code nonstandard-notation, span on the word; ast.InitialNode/FinalNode/DecisionNode/PseudostateNode Keyword and ast.TransitionMember.ToSpan record which spelling was written |
passes/nonstandard_notation_test.go TestStateExtensionsAreReported, TestActionNodeExtensionSpellingsAreReported, TestTransitionToSpellingIsReported, TestStandardNotationIsSilent, libs/nonstandard_notation_test.go TestStdlibHasNoNonstandardNotation |
⚠️ Approximate (a warning, never an error: the notation is a documented OpenSysML extension existing models use, so it keeps parsing and does not gate a higher tier. done, fork and join are silent — see the audit's judgment calls) |
A private import X::* is not re-exported by its namespace, and the names it brings in are visible only within that namespace (KerML 8.2.3.3) |
symbols/index.go applyReexportMarks / exportedChildren (a re-export is hidden while every document that surfaced it did so with a private import, and left out when that namespace is itself wildcard-imported) and LookupQualifiedFrom (a hidden name answers a lookup only when the referring namespace is the one that hid it, or is nested in it); resolve/qualified.go referringNamespaceFQN supplies that context for a qualified reference and HiddenFrom stops its member-lookup fallback, which reaches a cached symbol's children without consulting the marks, from resurfacing a hidden name; resolve/alias.go resolveCachedAliasTarget supplies the context for the target of a cached alias; resolve/unqualified.go matchImport enumerates a wildcard import's target through symbols/index.go LookupDirectChildrenFrom, which reads the same marks from the referring namespace unless the import is import all |
symbols/index_test.go TestExpandWildcardImportsDoesNotCarryOnAPrivateImport, TestLookupQualifiedFromSeesAPrivateImportOnlyFromWithin, TestHiddenFromReportsOnlyPrivatelySurfacedNames, TestLookupQualifiedReachesAPubliclyImportedName, TestLookupQualifiedAcrossAChainedPrivateImport, TestLookupDirectChildrenFromDropsPrivatelyImportedNames; resolve/qualified_test.go TestResolveQualifiedRejectsAPrivatelyImportedName, TestResolveQualifiedRejectsAPrivatelyImportedNameThroughMemberLookup, TestResolveQualifiedFromInsideAnUnnamedElement, TestResolveQualifiedReachesAPubliclyImportedName; resolve/visibility_test.go TestNamespaceImportSkipsAPrivatelyImportedName, TestNamespaceImportSkipsAPrivatelyImportedCachedName; resolve/alias_test.go TestAliasResolvesAPrivatelyImportedTargetFromCache, TestAliasResolvesAPrivatelyImportedTargetWhenParsed; model/visibility_reach_test.go TestPrivateWildcardImportIsNotReExportedAcrossDocuments |
✅ Faithful (neither a qualified nor an unqualified reference reaches a privately imported name from outside the importing namespace, whether the index was parsed or restored from cache; an import all still takes the target's private memberships, and a reference inside the importing namespace still sees them) |
A root-level import X::* surfaces its names in the importing document's own root namespace, so they are not names of another document's root (KerML 8.2.3.3, 8.2.4) |
symbols/index.go ReexportVisible (a root-level re-export answers a lookup only in a document holding a claim on it; a name under a namespace is visible wherever that namespace is), read by resolve/filter.go Resolver.admitsUnderName on the whole-index routes for a top-level name |
symbols/index_test.go TestARootReexportIsVisibleOnlyInItsOwnDocument; resolve/filter_test.go TestARootImportSurfacesNamesInItsOwnDocumentOnly; model/that_test.go TestRootImportServesItsOwnDocumentOnly |
✅ Faithful (a filter on a root-level import therefore hides what it rejects from every document, and the importing document keeps what it admits; the importing document keeps them at every visibility, private included — LookupQualifiedFrom leaves the private-import hiding of a root-level name to ReexportVisible, which decides it per document, rather than hiding it from the document that wrote the import) |
Visibility of the members a recursive import surfaces (import X::**, KerML 7.2.5) |
resolve/unqualified.go matchImport (both the membership and namespace branches filter through resolve/visibility.go visibleThroughImport) |
resolve/visibility_test.go TestRecursiveMembershipImportSkipsPrivate, TestNamespaceImportSkipsPrivate, TestImportAllReExportsPrivate |
✅ Faithful (a recursive membership import hides private members of the subtree it walks unless it is import all) |
expose in a view body is an Import (SysML v2 8.3.26.2 Expose, 8.3.26.3 MembershipExpose, 8.3.26.4 NamespaceExpose) |
parser/defusage.go (expose shares parser/namespace.go parseImportTail, so ::* yields a NamespaceExpose and ::** a recursive MembershipExpose; ast.Import.IsExpose, IsAll, protected Visibility) |
parser/expose_test.go TestParseExposeImportKind, TestParseExposeIsImportAllAndProtected, resolve/expose_test.go |
⚠️ Approximate (an Expose always imports all elements regardless of visibility — validateExposeIsImportAll — so its exposed elements resolve inside the view body, in views that specialize it, and not outside; validateExposeOwningNamespace is implemented — see the row below) |
| Protected import visible in specializations of the importing definition or usage (SysML v2 7.5.3) | resolve/visibility.go inheritedThroughSpecialization (a protected or public import reaches specializations, a private one does not — KerML 8.2.3.3) and lookupInheritedImports (walks semantics.Model.DirectSupertypes upward from the referring scope's owner, breadth-first and cycle-guarded, matching each supertype's inherited imports through the same matchImport); resolve/unqualified.go walkUnqualifiedHiding consults it after the imports declared in the scope itself |
resolve/protected_test.go TestProtectedImportReachesADirectSpecialization, TestProtectedImportReachesATransitiveSpecialization, TestProtectedImportReachesAUsageTypedByTheImporter, TestProtectedImportDoesNotReachAnUnrelatedNamespace, TestPrivateImportDoesNotReachASpecialization, TestProtectedImportAllReachesASpecializationWithPrivateMembers, TestExposeReachesASpecializingView, TestInheritedImportWalkTerminatesOnASpecializationCycle; model/visibility_reach_test.go TestProtectedImportReachesSpecializationsAcrossDocuments, TestProtectedImportDoesNotReachAnUnrelatedDocument, TestExposeReachesASpecializingViewAcrossDocuments |
✅ Faithful (an expose is protected, so it reaches a specializing view the same way; a feature typing is a generalization edge — KerML 8.3.4.6 — so an import declared in a definition is also reached from a usage typed by it, and an unrelated namespace sees nothing) |
A protected import is re-exported only to what specializes the importing definition or usage: under part def Base { protected import Lib::Pub; }, part def Sub :> Base { public import Base::*; } reaches Pub and an unrelated part def Other { public import Base::*; } does not (SysML v2 7.5.3, KerML 8.2.3.3) |
resolve/unqualified.go importVisibleFrom (a protected import answers a lookup through another namespace's re-export only when the referring scope's owner specializes the namespace that declared it) over resolve/visibility.go specializes / specializationChain, one breadth-first cycle-guarded walk of semantics.Model.DirectSupertypes shared with lookupInheritedImports |
resolve/protected_test.go TestProtectedImportReexportFollowsSpecialization, TestProtectedImportDoesNotReachAnUnrelatedNamespace, TestProtectedImportReachesATransitiveSpecialization |
✅ Faithful (a re-export widens who may reach a protected import, never what it imports: the visibility test is applied at the referring scope, so the same name is answered for a specialization and refused for a sibling) |
validateExposeOwningNamespace — the importOwningNamespace of an Expose must be a ViewUsage (SysML v2 8.3.26.2) |
passes/expose.go checkExposeOwners / exposeOwnerDiagnostic, run by passes/constraint.go ConstraintPass at LevelConstraint; code expose-owning-namespace |
passes/expose_test.go TestExposeOwningNamespace, model/expose_owner_test.go TestExposeOwnerAcrossDocuments |
✅ Faithful (usage-only reading, per maintainer decision: an expose owned by a view usage is legal, one in a view def body is a warning since OpenSysML resolves it — resolve/expose_test.go TestExposeInViewDefinitionBody — and any other owner is an error; a package or namespace body rejects expose in the parser) |
A namespace's filter restricts the imported memberships it re-exports (package P { public import Q::*; filter @Safety; }, KerML 8.2.4, SysML v2 7.4.4) |
symbols/filter.go NamespaceFiltersIn and symbols/index.go reexportGated (each way a name is re-exported records the conditions along it as one route; a name with no route is ungated) — the index records the gate and never evaluates it; resolve/filter.go Resolver.admitsUnderName evaluates a candidate against the routes through semantics.Model.SatisfiesElementFilter, and resolve/unqualified.go matchImport, resolve/qualified.go and symbols/index.go LookupDirectChildrenFrom share that one admission test; a filter member of a definition or usage body — a view narrowing what its expose lines surface — reaches the same test through resolve/filter.go Resolver.importAdmits, which composes symbols.NamespaceFiltersIn(scope) with the import's own clause; a filter at a document's root gates that document's root-level imports alone, since each document owns its root namespace (symbols/filter.go namespaceFiltersGating, keyed per document by symbols/index.go gateKeyOf) |
symbols/index_test.go TestExpandWildcardImportsGatesAFilteredReexport, TestExpandWildcardImportsKeepsAnUnfilteredRoute, TestExpandWildcardImportsRecordsAGateOnce; resolve/filter_test.go; model/element_filter_test.go (including TestFilterMemberOfADefinitionBodyRestrictsItsImports, TestFilterConditionResolvesThroughTheImportsItFilters, TestARootFilterRestrictsOnlyItsOwnDocument), symbols/index_test.go TestRootNamespaceFiltersGateOnlyTheirOwnDocument; libs/loader_cache_test.go TestFilteredImportsSurviveCacheRestore |
✅ Faithful (a filter restricts only what the namespace re-exports, per maintainer decision: its own directly declared members are never hidden, only the condition's own names resolve unfiltered — otherwise a condition could not name a metadata type the namespace itself imports, since the condition's own names would be filtered by the condition (resolve.Resolver.InCondition) — and a name reached by an unfiltered route as well as a filtered one stays visible: the routes are alternatives, and the conditions along one route are a conjunction) |
An import or expose filter restricts what that import brings in (import P::*[@Safety];, expose vehicle::**[@Safety];, SysML v2 7.4.4, 8.3.26) |
ast.Import.FilterExpr reaches the index through symbols/index.go wildcardImport.filter, gating the memberships that import surfaces; the same admitsUnderName decides them, so the qualified and the unqualified route agree, including the whole-index fallback for a top-level name (resolve/qualified.go lookupGlobalTop, which a root-level filtered import would otherwise bypass); the condition's own names are resolved as references like a filter member's, so a typo in the clause is an unresolved reference and editor navigation over it works (resolve/document.go, resolve/references.go) |
resolve/filter_test.go, model/element_filter_test.go (a filtered expose in a view surfaces a strict subset, an element the condition rejects is unresolvable by either route, TestAFileLevelFilteredImportHidesWhatItRejects, TestAnUnresolvedNameInAnImportFilterIsReported), parse/element_filters.golden |
✅ Faithful (an import's condition and the filters of the namespace it imports compose: the intersection is what the importer sees) |
| A view's exposed elements are queryable (SysML v2 7.24 Views and Viewpoints, 8.3.26 Expose) | semantics/expose.go Model.ExposedElements (a view's own expose relationships, then those of the views it specializes since an Expose is protected, in declaration order and once each) and Model.NestedViews (the views in its body, to walk a view tree), enumerated by resolve/filter.go Resolver.ImportedElements — the same admission, visibility and filter gating a lookup through that import makes, so the exposed set is what the view body actually resolves |
semantics/expose_test.go (TestExposedElementsNamespaceWildcard, TestExposedElementsRecursiveExpose, TestExposedElementsWithAnElementFilter, TestExposedElementsWithAViewBodyFilter, TestExposedElementsOfNestedViews, TestExposedElementsExposingAnotherView, TestExposedElementsInheritedFromAViewDefinition, TestExposedElementsOfAViewExposingNothing, TestExposedElementsOfANonView) |
✅ Faithful (an empty exposed set is no error; asking a non-view is semantics.ErrNotAView. The REPL surface is %view — repl/view.go doView) |
A view's exposed set is rendered as the rendering its render member states, and as a containment tree where it states none (SysML v2 7.24 Views and Viewpoints, §10.2 — the rendering is tool-defined) |
semantics/rendering.go Model.ViewRenderings (the render members of the view and of the views it specializes) and Model.RenderingTarget (the rendering the member references or declares); view/view.go Renderer.KindOf, Renderer.Render (the kind, then the exposed set from Model.ExposedElements), view/tree.go, view/interconnection.go (the model's own connector and flow ends, not source text), view/behavior.go (the lowered lower.StateGraph/lower.ActionGraph, never a re-parse of symbol.Decl), view/table.go (the exposed elements, the elements declared in them and the nested views, as rows), view/text.go, view/mermaid.go and view/markdown.go (the forms, chosen per kind by Kind.MachineForm in view/form.go) |
view/render_test.go TestGoldenRenderings (text and machine-readable goldens for tree, interconnection, state, action, a filtered view and a table, from .sysml fixtures), TestTreeRenderingShowsNestedViewsAndDefaults, TestInterconnectionRenderingDrawsConnections, TestStateRenderingComesFromTheLoweredGraph, TestActionRenderingComesFromTheLoweredGraph, TestRenderingUsesFilteredAndInheritedExposure, TestTableRenderingRows, TestTableFormsAreMarkdownNotMermaid, TestMermaidLabelsAreEscaped |
✅ Faithful to the notation, tool-defined in output (the kinds produced are a tree, an interconnection diagram, a state machine, an action flow and a table; state and action renderings read the graphs the runtime executes, so a rendering cannot drift from what runs. Mermaid is the machine-readable form of the graph-shaped kinds and Markdown that of a table; SysML §10.2 specifies no artifact) |
| A rendering this build does not produce, a name that is no view, a view exposing nothing, and an exposed element a rendering cannot represent are each explicit | view/view.go UnsupportedKindError (wrapping view.ErrUnsupportedKind, naming the kind, the view and the rendering it stated), Renderer.Render (semantics.ErrNotAView for a non-view, as %view answers), Rendering.Empty and view/text.go (an empty artifact saying whether the view exposes nothing or nothing exposed was representable), Rendering.Notices (what a kind could not draw) |
view/render_test.go TestUnsupportedRenderingKinds, TestRenderingSomethingThatIsNoView, TestRenderingAViewExposingNothing, TestRenderingReportsWhatItCannotRepresent; repl/view_render_test.go TestRenderOfAnUnsupportedKindNamesIt, TestRenderOfANonViewIsTyped, TestRenderOfAViewExposingNothingSaysSo; cmd/sysml/render_test.go TestRenderReportsWhatItCouldNotDo |
✅ Faithful (a stated kind that is not produced — sequence, geometry, textual — is a typed error naming it, never a substituted rendering; a form the kind is not written in is a WrongFormError naming the one it is; an element that cannot be drawn is reported, not dropped) |
%render <name> [text\|mermaid\|markdown] renders a view at the prompt, and sysml -render <view> [-render-form <form>] [-o file] outside it |
repl/view.go Session.ViewRendering, renderLines, viewRenderer (a semantic model and resolver of its own over the session's symbol index, so no runtime is built); repl/meta.go (the %render arm, help entry), repl/complete.go (the form completion); cmd/sysml/render.go runRender, reportRenderNotices, writeArtifact (artifact on stdout or -o, every notice on stderr), cmd/sysml/main.go (mutually exclusive with -convert and with the check flags) |
repl/view_render_test.go TestRenderDefaultsToATree, TestRenderWritesMermaidWhenAskedFor, TestRenderOfATabularView, TestRenderOfAnUnknownNameReports, TestRenderMisuseShowsUsage, TestRenderIsInHelpAndCompletion, TestRenderBetweenStepsDisturbsNothing; cmd/sysml/render_test.go TestRenderWritesTheArtifactOnStdout, TestRenderTextFormAndOutputFile, TestRenderOfATabularView |
✅ Faithful (%render is a read: it materializes nothing, adds no object to the session, leaves the submission buffer alone and leaves an %action/%state debugging session stepping the same graph and objects — pinned by TestRenderBetweenStepsDisturbsNothing. %view's report is unchanged) |
| A view's conformance to the viewpoints it satisfies is evaluated (SysML v2 7.24 Views and Viewpoints; a concern is a requirement, so its conditions are evaluated by the requirement engine) | semantics/conformance.go Model.ViewConformance (per satisfy claiming conformance — IsViewpointSatisfy, since a satisfy stating a subject asserts its requirement of that subject instead, as the stdlib View does: Model.SatisfyTarget, then every concern Model.FramedConcernsOf reports for the viewpoint, matched against the view's own, inherited and nested framings, and evaluated against the exposed elements the concern's subject admits through the ConcernEvaluator the caller supplies — implemented in repl/view.go concernEvaluator over runtime.Context.CheckSatisfactionOn, so condition evaluation is not reimplemented); passes/constraint.go checkViewSatisfyTarget (code view-satisfy-viewpoint) |
semantics/conformance_test.go (framing direct, inherited, nested and inherited-from-a-viewpoint; a missing concern; a false condition; an unevaluable one; no admissible subject; an unresolved stakeholder; a satisfy that is no viewpoint; determinism), passes/constraint_test.go TestConstraintViewSatisfyNonViewpointRequirement, TestConstraintViewSatisfyViewpointOK, TestConstraintSatisfyOutsideAViewIsNotChecked, TestConstraintViewSatisfyRequirementBySubjectOK, conformance/viewpoint_framed_concern_conditions.sysml, viewpoint_concern_without_condition.sysml |
⚠️ Approximate (the structural question — is every framed concern framed by the view — and the conditions are evaluated faithfully; the verdict rules are tool-defined, since SysML v2 leaves verification verdicts non-normative: a concern must hold of every exposed element its subject admits, a concern framed by a nested view counts for its container, and what cannot be evaluated — no condition, no admissible subject, an unresolved party, a framing whose concern reference does not resolve, a viewpoint framing no concern at all — is reported as unevaluable with a reason, never as a pass) |
A filter condition is a model-level predicate over one candidate element, with the candidate as the implicit self (@Safety, @@Safety, and/or/xor/not/implies, and a comparison of an annotation's feature) |
semantics/filter.go Model.CompileElementFilter / EvalElementFilter / SatisfiesElementFilter (compiled once per condition, memoized per candidate) over the annotations semantics/annotations.go collects — prefix metadata (#Safety part def P;), a metadata member of the element's body, and metadata m about X; — with conformance through Model.AllSupertypes, so @Safety matches a metadata type specializing Safety, and a metaclass classification (@SysML::PartUsage) reads the candidate's own kind |
semantics/filter_test.go, model/element_filter_test.go, semantics/cached_library_test.go TestNamespaceFilterOverALibraryIsTheSameParsedAndRestored (the condition as parsed and the same condition compiled into an index-cache record classify alike), TestAnnotationFactsOfALibraryElementSurviveTheCache |
⚠️ Approximate (evaluated against a symbol, not a value: there is no instance at name-resolution time. The subset above is what the corpus writes; a condition outside it is reported, not guessed — see the row below. @/@@ in the runtime value evaluator remains unimplemented, line 214) |
| A filter condition that is not boolean-valued, or that OpenSysML cannot evaluate | passes/filter.go ElementFilterPass at LevelType, over semantics/filter.go Model.CheckElementFilter; codes filter-not-boolean (error — a condition that is not a predicate can select nothing) and filter-not-evaluable (warning) |
passes/filter_test.go TestFilterNotBooleanIsReported, TestFilterNotEvaluableIsReported, TestFilterConditionsInTheSupportedSubsetAreClean |
✅ Faithful (an unevaluable condition is reported and not applied, so the elements it would have selected from all stay visible: a verdict OpenSysML could not reach never silently hides model content — maintainer decision, recorded here because the spec does not say). A feature bound to nothing is not that case: reading it yields the empty sequence, which an ordering propagates and ==/!= decide against any value (KerML DataFunctions, where an ordering takes DataValue[1] and equality [0..1]), so @Safety and Safety::isMandatory == true does not hold of an element annotated @Safety alone and does not surface it — a verdict, not a reported failure. An annotation inherits the values its metadata type declares, so a default the type gives the feature is what the comparison reads (semantics/annotations.go addTypeDefaults; semantics/filter_test.go TestFilterUnsetAnnotationFeature, TestFilterAnnotationFeatureDefault, model/element_filter_test.go TestFilteredImportRejectsAnUnboundAnnotationFeature) |
A KerML declaration specializes the library type its keyword implies (class → Occurrences::Occurrence, struct → Objects::Object, assoc/association → Links::Link, behavior → Performances::Performance, function/predicate → the matching evaluation, interaction → Transfers::Transfer, metaclass → Metaobjects::Metaobject, datatype → Base::DataValue, classifier/type → Base::Anything), so the library members it implies are inherited (KerML 1.0 §8.4.2) |
semantics/implicit.go implicitKerMLBases, read by implicitBase when source.KindOf says the document is KerML (the F3/F8 file-kind mechanism, no second notion) — a bare feature is left with no base rather than being given the SysML attribute one; libs/record.go (cache format 17) stores the semantic supertype edges so a restored library symbol inherits alike |
semantics/implicit_test.go:TestKerMLImplicitDefinitionBases, :TestSysMLImplicitDefinitionBasesRemainKindBased, semantics/cached_library_test.go:TestInheritedImplicitBaseIsTheSameParsedAndRestored, libs/loader_cache_test.go:TestLoaderCachePreservesRedefinitionMemberEdges |
⚠️ Approximate (the keyword table above is what the KerML root exercises; a keyword outside it gets no implicit base, and the implied feature bases KerML also defines are not modelled) |
A declared generalization suppresses the implicit one only when it already reaches that base, directly or indirectly (KerML 1.0 §8.4.2), so struct MyWheel specializes Wheel still specializes Objects::Object when classifier Wheel; reaches only Base::Anything |
semantics/implicit.go declaredGeneralizationReaches (declared generalizations only, and cycle-guarded, so it cannot re-enter the closure the implicit base feeds), consulted for KerML documents; a SysML declaration keeps taking its supertypes from its own declaration |
semantics/implicit_test.go:TestImplicitBaseOfExplicitSupertypeIsTransitive, :TestSysMLImplicitBaseOfExplicitSupertypeIsTransitive |
⚠️ Approximate (KerML only: the SysML side is unchanged, and moving it needs its own adjudication) |
A public membership import is re-exported to importers of the importing namespace, a root-level import is visible from a nested package of the same document, and an imported name may prefix a qualified name (KerML 1.0 §8.2.4, SysML v2 §7.4.4) |
resolve/unqualified.go lookupImportedMember, matchImport (recursion-guarded), resolve/qualified.go (imported members of the current namespace before the global fallback), resolve/filter.go (a namespace's children include what its public imports re-export), symbols/builder.go (the root scope keeps its document node, so root-level imports are found from a nested scope) |
resolve/visibility_test.go:TestPublicMembershipImportIsReexported, :TestPrivateMembershipImportIsNotReexported, :TestCyclicMembershipImportsTerminate, resolve/imports_test.go:TestRootImportVisibleInNestedPackage, :TestImportPrefixResolvesThroughSiblingImport |
✅ Faithful (private stays unexported, and the traversal is lazy and cycle-safe) |
A name written in a declaration's header (featured by, crosses, a subsetting) resolves against the members and imports of that declaration's own body before the enclosing scope, and those members stay reachable from outside by qualified name and by feature chain |
resolve/document.go resolveHeaderRelationships, headerHasName, findFeaturedByTargets (a featuring type is traversed for inherited members like a supertype); resolve/resolver.go lookupMember (contributed members, then the scope's own imports) |
resolve/document_test.go:TestResolveDeclarationBodyMembersFromHeaders, :TestResolveFeatureChainThroughImportedBodyMember, resolve/inherited_names_test.go:TestInheritedMembersThroughGeneralFeature |
⚠️ Approximate (the body is preferred only for a name it declares or imports itself, not for one it inherits, and a typing in the header is excluded — TestValueTypeNameNotShadowedByOwnMembers fixes that a value type is not shadowed by a member of the body it types) |
Design note: references is a member-contribution edge, not a generalization¶
A perform action usage relates the action it performs through a
ReferenceSubsetting, written references or ::> (SysML v2 §7.17.6; the
derived PerformActionUsage::performedAction comes from that owned reference
subsetting, §8.3.17.14). KerML makes ReferenceSubsetting a syntactically
distinguished kind of Subsetting (§8.3.3.3.9), which is why the referenced
feature's members are visible on the referencing one.
It is nevertheless kept out of semantics.Model.DirectSupertypes. Subsetting
in this implementation drives conformance and implicit typing, and a perform
statement is not a subtype of the action it performs for those purposes: making
it one would give perform action takePhoto references takePicture; the type of
takePicture and silently change conformance results elsewhere. Instead
Model.MemberSources — the union of the generalization edges and the reference
subsetting, breadth-first and cycle-guarded — is what member lookup consumes, so
takePhoto.focus resolves while AllSupertypes(takePhoto) stays free of
takePicture.
Two consequences of the spec's naming rules fall out of this and are implemented
alongside it: an unnamed feature takes the effective name of the feature it
references (KerML Feature::effectiveName), so perform providePower.generateTorque;
declares generateTorque; and because that name is bound in the same scope the
reference resolves in, the reference is resolved outside its own binding: a
refFilter hides just that borrowed binding for the duration of the lookup,
leaving each scope's declarations, inherited members and imports intact, so a
perform of an action the owner inherits from its type still resolves.
Pilot Constraint Rules We Do Not Implement (P6 of the pilot differential)¶
Four constraint-tier rules the pinned reference implementation (pilot 2026-05)
enforces and we do not. They were adjudicated diagnostic by diagnostic in
pilot-differential.md (§P6, follow-ups F20–F23) against
minimal reproducers run through that reference; the rule text is its validator
(org.omg.kerml.xtext/.../KerMLValidator.xtend) plus the constraint text on the
generated metamodel. Nothing here is implemented, so the "Implementation" column
names where the rule would belong rather than where it lives.
| Semantic Rule | Implementation | Test Case | Status |
|---|---|---|---|
validateSubsettingFeaturingTypes — subsettingFeature.canAccess(subsettedFeature) (KerML Subsetting): a feature of a type is not reachable by :: from outside it, since the subsetting feature's featuring types must feature the subsetted one (transitively, through featuring types that are themselves features) |
would be passes/constraint.go at LevelConstraint, over a canAccess predicate semantics does not compute today |
passes/pilot_p6_gaps_test.go TestConstraintSubsettingFeaturingTypesNotImplemented (skipped, F20) |
❌ Not implemented (all five corpus occurrences are downstream of the pilot failing to parse our namespace over-acceptance; the rule itself is real — reproducer in the P6 adjudication) |
validateFlowEndSubsetting — a FlowEnd must subset a feature that is not merely redefined: each end names the feature the payload leaves from or arrives at, so it can redefine Transfer::source::sourceOutput / Transfer::target::targetInput (SysML v2 FlowEnd) |
would be passes/constraint.go beside checkConnectorEndRedefinition, over the connector ends semantics/lower already build |
passes/pilot_p6_gaps_test.go TestConstraintFlowEndSubsettingNotImplemented (skipped, F21), with the accepted counterpart TestConstraintDottedFlowEndsAreAccepted |
❌ Not implemented (examples/views-demo.sysml:44 flow of Fuel from tank to thruster; violates it and we accept it) |
validateElementFilterMembershipIsModelLevelEvaluable — condition.isModelLevelEvaluable: an invocation is evaluable when its function is a model-level-evaluable library function and every argument is; a feature reference when its referent is a self-reference, or owned by a Metaclass/MetadataFeature, or has no featuring type and an evaluable value expression |
ours exists but diverges: passes/filter.go ElementFilterPass / semantics.Model.CheckElementFilter (filter-not-evaluable, warning, LevelType) |
passes/pilot_p6_gaps_test.go TestFilterModelLevelEvaluableFalsePositive, TestFilterModelLevelEvaluableFalseNegative (both skipped, F22) |
⚠️ Approximate, divergent both ways (we warn on filter 1 + 2 > 0;, which the reference accepts; we are silent on a reference to a feature with a featuring type, which it rejects) |
validateInvocationExpressionInstantiatedType — instantiatedType must be a Behavior, or a Feature typed by exactly one Behavior |
would be passes/typecheck_expr.go beside inferInvocation, which checks arity and argument types but never the kind of the invoked declaration |
passes/pilot_p6_gaps_test.go TestTypeCheckInvocationInstantiatedTypeNotImplemented (skipped, F23) |
❌ Not implemented (part def Widget; part w = Widget(); draws Must invoke a behavior or a behavioral feature from the reference and nothing from us) |
What We Don't (Yet) Support¶
Decisions to Reassess¶
Deliberate limitations whose current handling should be revisited once the feature they wait on lands (this repository has issues disabled, so follow-ups are tracked here):
| Deferred until | Reassess |
|---|---|
| A parameter of a behavior or step whose general type comes from the library index | semantics/redefinition.go ownedParameters reads the declaration's members, so a general behavior with no parsed AST (a cached library symbol) contributes no positions and its parameters are not redefined. Needs parameter order in IndexRecord, like the Supers row below. |
Scalar type inference for a bare feature reference (passes/typecheck_expr.go infer) |
A condition that is a plain name — while total { … }, total : Integer — infers Unknown, so checkBoolean passes it and the executor reports it (runtime/action_statements.go evalCondition) instead. Once a feature reference infers its declared scalar type, this becomes a typecheck error like the literal and operator cases, and the runtime check goes back to being unreachable. |
Specialization edges in the library index (libs/loader.go recordEntries drops Supers) |
implicitUsageBases maps each usage kind to its stdlib base definition because the base feature the spec has usages subset would be a dead end for member lookup. With the edges recorded, the map should name the base feature the spec names. |
Major Features Not Implemented (UML-referenced; no SysML v2 notation or KerML performance)¶
Activity Diagrams (Advanced): - Interruptible regions - Expansion regions (parallel/iterative) - Streaming pins - Exception handlers - Structured activities with pin connectors
State Machines (Advanced): - Protocol state machines
Object Model:
- Dynamic object creation/destruction (an object is materialized once, and nothing destroys it)
- Operation invocation given as a calc or constraint, or with positional arguments (an action operation with named arguments runs — see the Classifier Behaviors map)
- Routing to a second object of one usage (a via send follows the connections and an addressed send resolves an object, but the object reached is the one this context holds as its target's occurrence — see Known Limitations)
Type System: - Full generic/specialization validation - Interface realization - Redefinition conformance checking - Subsetting validation
Advanced SysML v2: - Analysis cases with verification semantics - Use case execution - Allocation execution semantics beyond materializing the allocation and its ends
What Can't Be Claimed for Spec Compliance¶
Intentionally Unspecified (No Normative Semantics):
- Verification verdict evaluation (VerdictKind/PassIf) - SysML v2 §9.3.2: "evaluation... intentionally not specified normatively"
- Variability/variation selection - SysML v2 §9.4: "Selection of variants is not specified normatively" — OpenSysML selects the variant a variation usage is bound to (attribute :>> cut = cut::cutIdeal;) and errors on an unselected, unknown, or multiply-selected variation; see the Variation and Variant map
- View/viewpoint rendering - SysML v2 §10.2: "rendering semantics intentionally left to tools". OpenSysML does render a view — a tree, an interconnection diagram, a state machine, an action flow or a table, as text, Mermaid or a Markdown table (%render, sysml -render; see the rendering rows) — but the artifact itself is this tool's output, not a normative form, and the kinds it does not produce are a typed view.UnsupportedKindError rather than a substituted rendering. The viewpoint conformance verdicts OpenSysML reports are likewise tool-defined (see the viewpoint conformance row)
- Allocation execution - SysML v2 §9.2.4: syntax defined, execution semantics not normative
Implementable But Not Yet Done: - Binding and flow connector objects (their ends reach routing, but neither is materialized as a connector object — see the Structural map) - Interruptible regions (spec exists, needs token cancellation) - Exception handlers (spec exists, needs exception propagation)
Implementation Files¶
Runtime Execution (internal/core/runtime/)¶
| File | Purpose | Lines |
|---|---|---|
context.go |
Execution context, constraint/requirement evaluation | ~430 |
invoke_calc.go |
Calc invocation: parameter/result resolution across specialization, binding, recursion bound | ~300 |
action_executor.go |
Token-flow semantics, control flow nodes, nested actions | ~729 |
state_executor.go |
Event-driven state machines, transitions, hierarchical states, pseudostates | ~1149 |
eval.go |
Expression evaluation (operators, literals, features) | ~758 |
value.go |
Runtime value representation (ValConst, ValString, ValInstance) | ~150 |
trace.go |
Deterministic execution and calc-evaluation trace recording, canonical value rendering | ~290 |
conformance_test.go |
Conformance gate | ~480 |
robustness_test.go |
Failure-mode tests | ~830 |
trace_test.go |
Golden trace test infrastructure | ~200 |
trace_calc_test.go |
Trace determinism and canonical rendering unit tests | ~180 |
Symbol Resolution (internal/core/resolve/)¶
| File | Purpose | Lines |
|---|---|---|
document.go |
Name resolution, inheritance chain lookup | ~750 |
qualified.go |
Qualified name resolution (A::B::C) | ~200 |
Symbol Tables (internal/core/symbols/)¶
| File | Purpose | Lines |
|---|---|---|
builder.go |
AST → symbol table, control flow node registration | ~380 |
scope.go |
Scope tree, member lookup | ~250 |
Testing Infrastructure¶
See TESTING.md for complete test contract details.
Test counts: stated once, in the Test Coverage list near the top of this document — the per-prefix conformance breakdown and the trace, fixture, negative and robustness figures are all there, and every other page links here rather than restating them (CONTRIBUTING.md).
Quality Gates:
- Parser: 95/95 stdlib files clean (94 vendored OMG, 1 OpenSysML extension)
- Execution conformance: every case passing, with known_failures.txt empty
- Training examples: 100/100 clean (no files recorded in internal/core/model/testdata/training_examples_expected.txt)
- No regressions: All tests pass on every commit
The training-example gate needs the corpus, which is not vendored: run
./scripts/download-training-examples.shfirst. The gate skips while the corpus is absent, so run the script before claiming a change is clean locally. CI downloads it (.github/workflows/pr.yml) and setsOPENSYSML_REQUIRE_TRAINING_CORPUS=1, which turns an absent corpus into a failure, so the gate can no longer skip green there. The gate runs against an empty semantic cache (t.Setenv("XDG_CACHE_HOME", t.TempDir())), so it reports the same 100/100 on any machine.
Model Persistence and RDF Interchange¶
Implementation: internal/core/rdf, internal/core/export
User surfaces: %save (internal/repl/meta.go), sysml -convert (cmd/sysml/main.go)
Reference: the RDF mapping — the mapping, the CLI, and the limitations in full
Two representations are supported, SysML textual notation and RDF Turtle. No JSON is used as an input, an output, or an intermediate form.
Notation is stable. RDF Turtle is experimental as of 0.1.0: the statuses below report how faithful the mapping is to what it covers, not that the mapping covers a whole model or that its vocabulary is settled — see the mapping's status.
| Capability | Implementation | Test Case | Status |
|---|---|---|---|
| Save a model as notation, preserving comments and notes | export.Convert → format.Source (token stream, not an AST re-print) |
export_test.go:TestSaveKeepsComments, repl/save_test.go:TestMetaSaveSysML |
✅ Faithful |
| Save a model as RDF Turtle | export.ToRDF + rdf.WriteTurtle |
repl/save_test.go:TestMetaSaveTurtle, golden .golden.ttl fixtures |
✅ Faithful |
| Notation → RDF for every definition/usage keyword the parser accepts | export/kinds.go metaclass tables, rdf_out.go encode |
export_test.go:TestGoldenConversions (18 fixtures) |
✅ Faithful |
| RDF → notation for the mapped subset | rdf_in.go ToSysML |
export_test.go:TestGoldenConversions, TestConvertedNotationParses |
✅ Faithful |
Round trip preserves the graph (sysml→ttl→sysml→ttl is stable) |
both directions | export_test.go:TestRoundTripIsLossless |
✅ Faithful |
Deterministic, reversible element IRIs keyed by qualified name, with ids in [A-Za-z0-9_-]+ |
rdf/vocab.go ElementIRI, rdf/ids.go EncodeElementID/DecodeElementID |
rdf_test.go:TestElementIRIRoundTrip, rdf/ids_test.go, export_test.go:TestElementIRIsEncodeQualifiedNames, TestFixtureElementIDsRoundTrip |
✅ Faithful |
| Declaration order preserved across a format with no order | sysx:memberIndex |
TestRoundTripIsLossless |
✅ Faithful |
Turtle writer/parser (prefixes, a, ;/, grouping, typed and language literals, long strings, escapes, @base) |
rdf/turtle_write.go, rdf/turtle_parse.go |
rdf_test.go:TestTurtleRoundTrip, TestParseTurtleForms, TestParseTurtleEscapes |
✅ Faithful |
| Syntax errors rejected, never partially converted | export.SyntaxError, rdf.ParseError (with line) |
export_test.go:TestSyntaxErrorIsReported, cmd/sysml/convert_test.go:TestConvertErrors |
✅ Faithful |
| Unsupported RDF reported, never silently dropped | export.UnsupportedError |
rdf_test.go:TestParseTurtleRejects, export_test.go:TestUnsupportedTurtleConstructs/TestUnknownMetaclassIsUnsupported/TestForeignGraph |
✅ Faithful |
| Expression-valued positions (values, bounds, guards, filters) | carried as source text, not expression trees | TestRoundTripIsLossless |
⚠️ Approximate — converts back exactly, but not queryable by SPARQL |
End-binding heads (connect, bind, flow, succession, transition, accept, satisfy) |
carried as sysx:sourceText with structural properties alongside |
export_test.go:TestVerbatimHeadsRoundTrip |
⚠️ Approximate — exact through OpenSysML; a foreign graph without the text is reported as unsupported rather than guessed |
Accept-action shorthand (action X accept p : T [via Port]) |
parameter encoded structurally; printer rebuilds the shorthand | export_test.go fixture testdata/convert/accept.sysml, parser/testdata/parse/accept_action_shorthand.golden |
✅ Faithful |
then succession between members |
sysml:SuccessionAsUsage with sourceFeature/targetFeature, from the one edge node every then parses to, in every body that admits one (a state's regions included) |
export_test.go:TestSuccessionRoundTrips, :TestSuccessionRoundTripsInEveryBody, parser/succession_test.go:TestMemberAttachedThenDesugars, :TestMemberAttachedThenInRegionDesugars |
⚠️ Approximate — a then beside a member with no name cannot be named by these ends: it warns (unnamed-succession-end) and no edge is recorded |
Behavioral nodes of an action or state body (initial/final node, perform, send, accept, terminate, assign, fork/join/merge/decision, while/loop/for, if/else, states, substates, regions, entry/do/exit, defer, pseudostates, transitions) |
export/behavior.go metaclasses and sysx: properties, encoded and printed back |
export/behavior_test.go:TestBehavioralModelsComeBackByteIdentical, :TestActionNodeMetaclasses, :TestLoopAndConditionalMetaclasses, :TestStateMachineMetaclasses, :TestStateMembersRoundTrip, fixtures testdata/convert/action_nodes.sysml, loops_conditionals.sysml, state_machine.sysml |
⚠️ Approximate — the nodes round-trip byte-identically, but the conditions and expressions they carry are source text, and a succession that does not name both of its ends is refused (export/behavior_test.go:TestUnsupportedBehavioralShapesAreReported) |
| Two members of one namespace sharing a name | refused: the qualified name is an element's graph identity | export_test.go:TestDuplicateNameIsUnsupported |
❌ Rejected rather than merged |
| Ownership cycle in an input graph | refused: no root owns the element, so printing would emit an empty document | export_test.go:TestOwnershipCycleIsUnsupported |
❌ Rejected rather than emitting an empty file |
Lexical // and /* */ trivia across the RDF hop |
no element owns trivia; doc/comment are declarations and do convert |
export_test.go:TestCommentsThroughRDF |
❌ Not carried through .ttl (a direct .sysml save keeps it) |
| Blank nodes, RDF collections, bare literal shorthands | rejected by rdf.ParseTurtle |
rdf_test.go:TestParseTurtleRejects |
❌ Not supported (by design; see docs/reference/rdf-mapping.md) |
Vocabulary: sysml: = https://www.omg.org/spec/SysML# and elmt: =
urn:sysmlv2:element: match the Flexo MMS SysML v2 service's Namespaces.kt.
That is only vocabulary compatibility: the service's reader derives an element's
@id from the substring after the final :, and requireValidId permits only
[a-zA-Z0-9_-]+, so OpenSysML's qualified-name element IRIs are not addressable
through its API. The reader also ignores predicates outside sysml: and
urn:sysmlv2:annotation:json:, so sysx: triples do not survive that path.
Paged listing and query require sysml:elementId, while roots filtering uses
sysml:owner and sysml:owningRelatedElement; these are roadmap D3 work.
Whether such a graph loads into a running Flexo triplestore has not been demonstrated. Properties the SysML metamodel
does not define are confined to sysx: = urn:opensysml:sysml:: memberIndex,
hasBody and sourceText carry order, body presence and verbatim heads,
prefixMetadata, filter, isNamespaceImport, isRecursive and isExpose
carry notation the metamodel has no property for, and the behavioral properties
(guard, expression, payload, subactionKind, …) carry the parts of a
behavioral node the metamodel has no predicate for.
What can't be claimed: this is not a normative SysML v2 → RDF/OWL mapping.
OMG's abstract syntax has no standard RDF serialization, so the property names
follow the metamodel's own attribute names and the Flexo service's conventions.
A model converted here is faithful to itself on a round trip; it is not
guaranteed to be interpreted identically by an unrelated SysML RDF tool, and no
round trip through a third-party triplestore has been demonstrated. The
vocabulary may also change without a compatibility path, so a .ttl is an
artifact to regenerate rather than the copy of record.
Source-Preserving Model Editing¶
Implementation: internal/core/edit, internal/grpc/edit.go (ApplyEdits)
User surfaces: opensysml — Model.edit(), Editor.set_value, Editor.rename,
Editor.apply (python/opensysml/edit.py)
Reference: the Python guide
The standard defines an API for changing a model (SysML v2 API & Services commits); this is not that API. It is a source-level edit of the notation a model was parsed from: the AST stays immutable, the edit rewrites bytes of the source guided by the spans the parse recorded, and the result is read back before it is returned. Two operations are defined, and only two.
| Rule | Implementation (file:function) | Tests | Status |
|---|---|---|---|
A feature's value is set by replacing the expression of an existing = <expr>, or by adding one before the declaration's terminating ; when it has none |
edit/locate.go Model.valueSplice, Model.terminator (the ; from the token stream, not a search) |
edit_test.go:TestSetValueReplacesOnlyTheValueSpan, :TestSetValueAddsValueToValuelessFeature, :TestSetValueKinds (quantity, string, boolean, a feature reference, an expression, a feature nested three levels deep, a feature reached through redefines), grpc/edit_test.go:TestApplyEditsSetValuePreservesSource, python/tests/test_edit.py |
✅ Faithful |
| A declaration is renamed by rewriting its own name token | edit/locate.go Model.renameSplice, declIdent |
edit_test.go:TestRenameRewritesTheNameTokenOnly, grpc/edit_test.go:TestApplyEditsAddsValueAndRenames |
⚠️ Approximate — the name token only. References are not rewritten (next row) |
| A rename whose element is referenced is refused, naming where the references are made | edit/locate.go Model.referringTo (resolve.References + Resolver.PartSymbol), FailureRenameReferenced |
edit_test.go:TestRenameRefusedWhenReferenced, grpc/edit_test.go:TestApplyEditsRenameReferencedNamesReferrers, python/tests/test_edit.py |
✅ Refused rather than half-done — the referrers are reported as the FQN of the namespace each reference is made from, not of the referring expression |
Targets are named by the id a read reports (Symbol.id, an FQN), and only a declaration of the edited model's own source can be edited |
edit/locate.go Model.target (Index.LookupQualifiedFrom + GetFQN, DocName check) |
edit_test.go:TestRefusals (unknown target, target outside this source, target carries no value) |
✅ Faithful — a name that resolves outside the edited source, the standard library included, is refused rather than editing a file the caller did not name |
| Edits apply in one pass, right-to-left by offset, and every byte outside an edited span is identical to the parsed source — an edited span covers the target's own tokens only, since a node's span runs on to the next token and so contains the whitespace and comments written after it | edit/edit.go Apply, splice ordering, edit/locate.go Model.tokenSpan |
edit_test.go:TestSetValueKeepsWhatFollowsTheValue, edit_test.go:TestSetValuePreservesCommentsAndBlankLines, :TestApplyManyEditsInOnePass, the assertOnlySpanChanged check in every case (the original is rebuilt from the result by undoing each applied edit), python/tests/test_edit.py (byte comparison around each AppliedEdit) |
✅ Faithful — nothing is reformatted, so format.Source is not run over the result |
| Two operations that would edit overlapping bytes are refused | edit/edit.go Apply (FailureOverlappingEdits) |
edit_test.go:TestRefusals (overlapping edits), grpc/edit_test.go:TestApplyEditsRefusalIsAResponse |
✅ Faithful |
| A new value that does not lex/parse as one expression, or a new name that is not an identifier, is refused before anything is spliced | edit/validate.go Model.checkValue, checkName |
edit_test.go:TestRefusals (value does not parse, value is not one expression, value is empty, new name is not an identifier, new name is a keyword, new name is empty) |
✅ Faithful |
| A rename to a name that already means something where the element is declared is refused | edit/locate.go Model.nameTaken (Resolver.LookupNameExcluding from the element's own scope, with the element's own binding hidden), FailureInvalidName |
edit_test.go:TestRefusals (new name is a sibling's name), :TestRenameShadowingAnOuterNameIsRefused |
✅ Faithful — a sibling of that name makes the qualified name ambiguous, and a name reached through an enclosing namespace, an import or inheritance would be shadowed, so every expression reading it would silently read the renamed element instead. Re-analysis cannot catch either, since the name still resolves. The refusal is conservative: a rename onto any name visible at the element's position is refused, even where shadowing would have been intended |
| The edited source is re-lexed, re-parsed and re-analysed, and content is not returned when the edit introduced an error — a value that parses but names nothing included | edit/validate.go validateResult (parser + passes.Analyze over the edited document in an index built for it), reported as FailureResultInvalid with the diagnostics |
edit_test.go:TestResultInvalidCarriesDiagnostics, :TestPreExistingErrorsDoNotRefuseAnEdit (only errors the edit introduces refuse it), :TestSemanticValidationSkippedWithoutIndexSource, grpc/edit_test.go:TestApplyEditsRefusalIsAResponse (empty content on every refusal) |
✅ Faithful — the service never returns notation its own parser cannot read back |
| Every refusal is a typed failure kind, never a silent no-op, and a refused request applies nothing | edit/errors.go Error/Failure, api/proto/sysml.proto EditFailure, python/opensysml/errors.py (EditError and its subclasses) |
edit_test.go:TestRefusedEditLeavesNothingApplied, grpc/edit_test.go:TestApplyEditsRefusalIsAResponse, python/tests/test_edit.py, test_wire_compat.py:test_edit_failure_kinds_keep_their_values |
✅ Faithful |
A model evicted from the service's cache is NOT_FOUND, as Convert reports it |
internal/grpc/edit.go Service.ApplyEdits |
grpc/edit_test.go:TestApplyEditsUncachedModelIsNotFound, python/tests/test_edit.py (ModelNotFoundError) |
✅ Faithful |
| Creating or deleting an element, and building a model from the client | — | — | ❌ Not supported (by design): an edit changes the source of a model that already declares what it declares. There is no AST printer, so a model assembled client-side cannot be written out |
| The client negotiates the capability before calling | python/opensysml/capabilities.py CAPABILITY_APPLY_EDITS, connection.py Connection.apply_edits |
python/tests/test_edit.py (MissingCapabilityError, raised before any RPC) |
✅ Faithful |
gRPC Service Layer¶
Implementation: internal/grpc/service.go
Status: ✅ Functional, ✅ §5.2 test contract satisfied for the wrapper
Runtime RPC Handlers¶
| RPC | Implementation | Status | Tests |
|---|---|---|---|
| GetServerInfo | service.go Service.GetServerInfo, capability names in Capabilities() |
✅ Faithful — reports the build version (informational; a source build reports dev) and the capabilities this build supports by name, currently type_facts, convert, verification, query, enum_values, evaluate_subject, symbol_attributes, unset_value, feature_values and apply_edits. A capability is added, never renamed or dropped with its behaviour intact, so a client requires one instead of comparing versions; a service predating this RPC answers UNIMPLEMENTED, which the client reads as supporting no capability |
service_test.go:TestGetServerInfo, TestGetServerInfoTypeFactsCapabilityIsHonest, python/tests/test_capabilities.py |
| ParseFile | service.go Service.ParseFile (parser + passes.Analyze + stdlib load) |
✅ Faithful — the cache is keyed by the file name and content the service read, so repeated parses of an unchanged source hit it whatever the request's (ignored) content_hash says, a hash disagreeing with its content cannot serve another model, and identical content read under two names keeps a record each, since their diagnostics name different files |
runtime_test.go:TestParseFile_*, service_test.go:TestParseFileCachesByContentRead, service_test.go:TestParseFileCachesPerFileName, every conformance case |
| ParseFile (standard library) | grpc/libindex.go indexPool, buildLibraryIndex, indexPoolSizeFromEnv (SYSML_GRPC_INDEX_POOL, default 4, 0 disables), grpc/service.go Service.Prewarm/Close/ParseFile, wired at startup in cmd/sysml-grpc/main.go |
✅ Faithful — the library does not depend on the model, so a cache miss adds its document to an index built ahead of it rather than loading the library again, and keeps that index with its cached model. What a model resolves against is unchanged (same source list, same expansion, same persist step); an index is handed out once, so no two cached models share one, and an empty pool builds one on the request path, so a result never depends on prewarming. A cold ParseFile on examples/combined-behavioral-demo.sysml measures ~0.5–0.9 ms against a warm pool where it measured ~100–128 ms building the library per model, with the same zero diagnostics |
grpc/libindex_test.go:TestPooledIndexMatchesFreshlyBuiltIndex (identical diagnostics and identical qualified lookups over the whole index, pooled vs built inline), :TestParseFileServesModelsFromThePrewarmedPool, :TestParseFileTakesNoIndexOnACacheHit, :TestCachedModelsOwnTheirIndex, :TestIndexPoolFallsBackToBuildingInline, :TestIndexPoolCloseStopsPrewarmingAndStillServes, :TestIndexPoolSizeFromEnv, :TestIndexPoolStopsAtItsSize, :TestIndexPoolBuildsOneIndexAtATime, libs/cache_test.go:TestCacheStoreConcurrentWritersOfOneKey, :TestCachePruneRemovesStaleTempFiles, robustness_test.go:parse_with_unavailable_standard_library, BenchmarkParseFileColdPooled/ColdInline |
| Convert | export.go Service.Convert, conversion in internal/core/export |
✅ Faithful for what OpenSysML writes — SysML/KerML notation and RDF Turtle, from a loaded model named by its model_hash, a path the service opens, or inline content, with the format names sysml -convert takes and canonical names reported back. A model_hash converts the source that parse read, so a file edited since then does not change what is written, and a model evicted from the cache is NOT_FOUND rather than converted as something else; a file_path is read afresh, for a caller who does want the file as it stands. Notation to notation is source-preserving (comments and layout survive); a graph direction returns an equivalent model, not identical bytes, and drops comments, per docs/reference/rdf-mapping.md. A conversion that cannot be written faithfully returns error plus the diagnostics rather than partial output, and tolerate_syntax_errors is honored for notation to notation only, since a graph built from an unparsed declaration would lose it silently |
export_test.go:TestConvert*, TestConvertModelHashConvertsWhatWasParsed, TestConvertUncachedModelHashIsNotFound, python/tests/test_conversion.py |
| Query | query.go Service.Query (SysML v2 API & Services Query) |
✅ Faithful to the standard's query model, ⚠️ approximate on three @type names — see the construct map below |
query_test.go:TestQuery*, python/tests/test_query.py |
| GetSymbol | service.go:126-145; static type facts in typefacts.go (SymbolInfo.type_info, .multiplicity, .specializations) computed by a per-model resolver + semantics context cached on the model and locked for the duration of a conversion |
✅ Faithful — reports the declared and resolved type, the library scalar it reduces to, quantity/unit, the declared multiplicity, and every generalization edge (specializes, subsets, redefines, typing) in declaration order; an unresolved name is reported unresolved rather than guessed. SymbolInfo.attributes carries the attributes the element actually has — own and inherited, in that order, a redefinition masking what it redefines — each with the resolved type, unit and constant default the service resolves (following specializes/subsets/redefines for what a declaration leaves out); a default that is not constant is reported as absent rather than guessed. Advertised as the symbol_attributes capability, since an older service reports an empty set, which cannot be told from an element with no attributes |
service_test.go:TestGetSymbol_, typefacts_test.go:TestTypeInfo, TestSpecializations*, TestMultiplicity*, TestSymbolContextConcurrentConversion, attributes_test.go, conformance symbol_attributes, python/tests/test_symbol.py |
| GetDiagnostics | service.go:148-169 (parser + semantic) | ✅ Faithful | runtime_test.go (implicit) |
| Evaluate | service.go Service.Evaluate |
✅ Faithful — evaluates in a lexical scope (context_symbol_id) and, with subject_symbol_id, against an instantiated object, the way %eval does after %instantiate: a feature then reads that object's feature value rather than the declared default, and the subject's own scope resolves inherited features when no context is named. A subject that is not a symbol, or that cannot be instantiated, is reported in-band rather than evaluated as something else; no subject leaves the existing behaviour unchanged. Advertised as the evaluate_subject capability, which a client requires before sending a subject, since a service predating it drops the field and answers with the declared default |
runtime_test.go:TestEvaluate_*, TestEvaluateWithSubject*, conformance evaluate_arithmetic, evaluate_subject_slot, evaluate_no_subject_default, evaluate_subject_not_found, python/tests/test_model_surface_integration.py |
| Instantiate | service.go (feature values read through Instance.GetFeatureValue, so a derived default is evaluated against the instance; InstanceGraphToProto in convert.go returns every instance reachable from the root in InstantiateResponse.instances) |
✅ Faithful — a composite feature value still marshals as the child's id, and that child is carried in the same response, so a nested object is reachable over gRPC without a follow-up RPC; expansion is bounded at depth 8 and stops at a type already on the path, as %features bounds it, so a self-referential part cannot instantiate forever |
runtime_test.go:TestInstantiate_*, instance_graph_test.go:TestInstantiate_ReturnsNestedInstances, _ReturnsDeepNestedInstances, _CollectionOfInstances, _FeatureValueErrorReported, _SelfReferentialPartTerminates, _MutuallyRecursivePartsTerminate, conformance instantiate_part, instantiate_derived_slot |
| ExecuteAction | service.go:265-312 | ✅ Faithful | runtime_test.go:TestExecuteAction_*, conformance execute_action_inputs, execute_action_no_initial |
| ExecuteState | service.go:315-355 | ✅ Faithful | runtime_test.go:TestExecuteState_*, conformance execute_state_transitions |
| ApplyEdits | internal/grpc/edit.go Service.ApplyEdits, engine in internal/core/edit |
✅ Faithful for the two operations it defines — sets a feature's value and renames a declaration, on the source the named model was parsed from, splicing the spans the operations reach and leaving every other byte identical. The edited source is re-parsed and re-analysed before it is returned, so a refusal carries diagnostics and no content; a model evicted from the cache is NOT_FOUND rather than edited as something else. Element creation and deletion are not offered, and a rename does not rewrite references — see Source-Preserving Model Editing below. Advertised as the apply_edits capability |
internal/grpc/edit_test.go, internal/core/edit/edit_test.go, python/tests/test_edit.py, python/tests/test_wire_compat.py:test_apply_edits_is_an_added_rpc |
Server Process Lifecycle (cmd/sysml-grpc)¶
| Rule | Implementation (file:function) | Tests | Status |
|---|---|---|---|
The published binary starts on the port it is given (-port 0 taking an ephemeral one), logs the address it listens on, serves RPCs, and stops on SIGINT/SIGTERM with a graceful stop and exit status 0 |
cmd/sysml-grpc/main.go main (net.Listen, then grpc.Server.GracefulStop on the signal, bounded by a 30s forced Stop) |
cmd/sysml-grpc/lifecycle_test.go:TestServiceServesRPCsAndShutsDownCleanly (built binary as a process, readiness from its own listening line, GetServerInfo → ParseFile → Instantiate), :TestShutdownWithAnOpenConnection, :TestHealthEndpointReportsTheBuild |
✅ Faithful — the process ends on its own; a client connection left open does not hold it |
| A start that cannot proceed is a reported error and a non-zero exit, never a hang: an unknown flag, a port already in use, a cache size that is not a positive count | cmd/sysml-grpc/main.go main (flag.Parse, sysmlgrpc.NewService, net.Listen) |
cmd/sysml-grpc/lifecycle_test.go:TestUnknownFlagExitsNonzero, :TestOccupiedPortExitsNonzero, :TestInvalidCacheSizeExitsNonzero |
✅ Faithful — each names what failed on stderr and exits non-zero |
A request naming something the service does not have — a file that does not exist, a model hash it never issued — is NotFound on that call, and the service keeps serving |
internal/grpc/service.go Service.ParseFile, Service.GetSymbol |
cmd/sysml-grpc/lifecycle_test.go:TestMissingModelIsATypedError (a later GetServerInfo is still answered and the process still exits 0) |
✅ Faithful |
/health answers on the -health-port while the server runs, reporting the build, and no other path is served |
cmd/sysml-grpc/main.go healthHandler |
cmd/sysml-grpc/server_test.go:TestHealthHandlerAnswersOnlyHealth, lifecycle_test.go:TestHealthEndpointReportsTheBuild |
✅ Faithful |
SysML v2 API & Services Query Conformance¶
Standard: the Query, Constraint, PrimitiveConstraint and CompositeConstraint
components of the SysML v2 API & Services OpenAPI schema (api/openapi.yaml in
Systems-Modeling/SysML-v2-API-Java-Client). This is the only query surface the standard
defines; the language has none.
Reference: docs/reference/api.md § "SysML v2 API & Services Query" documents the property table,
the @type mapping and the comparison choices.
| Construct | Implementation (file:function) | Tests | Status |
|---|---|---|---|
Query.scope — elements considered, empty being the whole model |
query.go:queryEval.candidates, elementWalk |
TestQueryScopeRestrictsToAnElementAndItsNested, TestQueryWithoutWhereSelectsWholeScope, TestQueryScopeMayNameALibraryElement |
✅ Faithful — a scope entry is a qualified name (or the standard's {"@id": …} reference, translated client-side) and covers that element and everything nested in it, parents first in declaration order. A library element may be named, so the loaded stdlib is queryable |
Query.select — properties projected |
query.go:projectedProperties, queryEval.project |
TestQuerySelectProjectsOnlyThoseProperties, TestQuerySelectReportsEveryPropertyByDefault, TestQueryOmitsPropertiesAnElementDoesNotHave |
✅ Faithful — an empty selection reports every queryable property; a property an element does not have is absent rather than empty |
Query.where absent |
query.go:queryEval.matches |
TestQueryWithoutWhereSelectsWholeScope |
✅ Faithful — selects the whole scope |
PrimitiveConstraint with = |
query.go:queryEval.matchesPrimitive, equalsAny |
TestQueryByTypeSelectsThatMetamodelType, TestQueryEqualMatchesAnyOfAListedValue, TestQueryIsAbstractSelectsAbstractDefinitions, TestQueryTypePropertyReportsResolvedType |
✅ Faithful — textual equality, matching any listed value, which is how the standard's clients write a @type filter |
PrimitiveConstraint with > / < |
query.go:validateOrdered, compareOrdered, parseOrdered |
TestQueryOrderedComparisonOnMultiplicity, TestQueryOrderedComparisonOnUnorderedPropertyFails, TestQueryOrderedComparisonAgainstNonNumberFails, TestQueryOrderedComparisonNeedsOneOperand |
✅ Faithful, with documented choices — numeric, one operand, ordered properties only (multiplicityLower/Upper); * is infinity. A non-ordered property, an unparsable operand or more than one operand is INVALID_ARGUMENT, never a false verdict |
PrimitiveConstraint.inverse |
query.go:queryEval.matchesPrimitive |
TestQueryInverseNegatesTheVerdict |
✅ Faithful — negates its own constraint's verdict, so a constraint and its inverse partition the scope |
CompositeConstraint with and / or, nested |
query.go:queryEval.matchesComposite, validateComposite |
TestQueryCompositeNesting, TestQueryFaultUnderADecisiveConstraintIsReported |
✅ Faithful — nests arbitrarily and short-circuits when evaluating, but every nested constraint is validated first, so a malformed one under an already-decisive sibling is still reported |
Property names — @id, @type, name, declaredName, qualifiedName, owner, isAbstract, type, multiplicityLower, multiplicityUpper |
query.go:queryProperties (single source of truth), QueryPropertyNames |
TestQuerySelectReportsEveryPropertyByDefault, TestQueryUnknownPropertyFailsRatherThanMatchingNothing, TestQuerySelectUnknownPropertyFails |
✅ Faithful for the set implemented — the set is closed and an unknown property is a typed QueryError (INVALID_ARGUMENT) listing the ones that exist, never an empty answer. Other metamodel properties (documentation, isComposite, …) are not queryable: known limitation |
@type — symbol kind → metamodel type name |
query.go:metamodelTypeNames, MetamodelTypeName |
TestMetamodelTypeNameCoversEveryKind, TestQueryByTypeSelectsThatMetamodelType |
⚠️ Approximate for three kinds — total over every kind a parsed declaration can have, but an individual definition/usage reports OccurrenceDefinition/OccurrenceUsage (an individual is an occurrence with isIndividual set), a connector end reports Feature (a Feature with isEnd set), and an alias reports Membership. Everything else is the metamodel's own name |
| Malformed query — no constraint form, no operator, no operand, empty composite | query.go:QueryError, validateConstraint, Service.Query |
TestQueryMalformedConstraintsFail, TestQueryUnsetQueryFails, TestQueryUnknownScopeFails, TestQueryFaultIsReportedWithNoElementsToConsider |
✅ Faithful — the where tree is validated before any element is read, so every malformed shape fails with INVALID_ARGUMENT naming what is wrong however few elements the scope holds; an unknown scope is an error, not an empty answer |
| Empty result | query.go:Service.Query |
TestQueryMatchingNothingIsEmptyNotAnError, python/tests/test_query.py |
✅ Faithful — a well-formed query selecting nothing answers with no elements |
| Capability negotiation | service.go:CapabilityQuery, python/opensysml/capabilities.py:CAPABILITY_QUERY |
TestQueryCapabilityIsReported, python/tests/test_query.py:test_query_requires_the_capability |
✅ Faithful — reported from GetServerInfo as query; the Python client refuses to send to a service that does not report it |
| Standard JSON payload accepted verbatim | python/opensysml/query.py:build_query, Model.query |
python/tests/test_query.py (test_cookbook_payload_translates_verbatim and the malformed-payload table) |
✅ Faithful — a cookbook payload is sent unchanged; @type tags, symbolic operators and scalar-or-list value are translated to the RPC's oneof and enums, and a payload the standard does not describe raises QueryError before anything is sent |
Known limitations (query):
- No graph traversal and no transitive closure, by design of the standard: there are no
path expressions, no joins, no "all elements under X" and no "everything specializing Y"
constraint — containment is expressible only as a
scope, and specialization not at all, even thoughsemantics.Modelcan answer it. TheQueryRPC is an interop surface for the standard's clients, not OpenSysML's expressive query story. Query.owningProjectandQuery.@idare accepted in a payload and ignored: this service holds parsed models, not projects or commits.- Results are unordered by the standard and are returned in declaration order here; there is no paging.
- Only the properties tabulated above are queryable;
documentation, metadata annotations and derived metamodel properties are not. - An element carries the value of a property it declares; an inherited value is not reported
(
typereports the resolved type of the feature itself). - An element with no qualified identity — an unnamed
doc, an anonymous usage orconnect— is not answered at all: its qualified name has an empty segment, so it is neither a unique@idnor a name ascopecould use (TestQueryOmitsElementsWithNoQualifiedIdentity). Nor is one declared inside an action body — anifbranch, a loop body — whose owner-less scope names it only locally, so the name resolves to no element (TestQueryOmitsBodyLocalDeclarations). - A standard-library element restored from cache carries no declaration and may carry no symbol
kind, so it reports no
isAbstractand, for such a kind, no@type— it is answered, but never matches a@type =comparison.
Test Coverage (AGENTS.md §5.2 Four-Layer Contract)¶
Current:
- ✅ Layer 1 (Golden AST): Covered via parser tests (fixtures in internal/core/parser/testdata/)
- ✅ Layer 2 (Execution conformance): internal/grpc/conformance_test.go drives Evaluate, Instantiate, ExecuteAction, ExecuteState and GetSymbol from .sysml + .expected.json pairs in internal/grpc/testdata/conformance/ (count in the Test Coverage list near the top; three of them failure modes), each parsed through the ParseFile RPC so the whole wrapper is exercised. Schema: that directory's README.md.
- ✅ Layer 3 (Golden traces): N/A — the wrapper adds no ordering behavior of its own; traces are pinned at the runtime tier.
- ✅ Layer 4 (Robustness): internal/grpc/robustness_test.go covers the wrapper's failure modes (unknown model hash, unknown symbol, malformed expression, a standard library that did not load); execution-level failure modes stay pinned in internal/core/runtime/robustness_test.go.
Rationale: the gRPC layer is a protocol wrapper over internal/core/runtime, which carries full §5.2 compliance for execution semantics. Its own conformance cases assert what the wrapper is responsible for: symbol lookup by FQN, input binding, value marshalling in both directions (including which Value oneof arm is set), the state-visit trace, and in-band error reporting.
Known Limitations (Non-blocking)¶
Runtime:
- an inline entry/do/exit body is one action, so an outgoing transition interrupts a do
body only between rounds, never between its statements; the one-action-per-statement
do { … } form is the interruptible spelling
- entering a composite state runs its own entry body before the region's initial
transition reaches the substate, so a parent's do body can run before a substate's
entry body (state_anonymous_action_body.trace.golden). Pre-existing region-entry
scheduling, not specific to inline bodies
- an entry/do/exit behavior that both performs an action and states a body of its own is
reported at execution rather than at parse time: which of the two SysML means is
unadjudicated, so neither is chosen
- a calc output only a branch that did not run would assign is unassigned for that
activation; the body is not statically required to bind every output it declares
- a calc whose only computation is rebinding an inout in its body, with no out and no
return, is still reported as having no result expression: an inout is bound by the
invocation, so it does not count as an output the body computes
- an object carried over an unrelated declaration keeps its identity but not its execution:
an execution belongs to the analysis it started in, so its behaviors are started again from
their initial states in the rebuilt analysis, what the discarded run wrote is dropped, and
the restart is reported. Re-declaring what the object runs drops the object itself with a
reported reason instead (runtime/adopt.go Adopt/restartBehaviors, writeBoundBehaviors;
repl/session.go rebindRestartedMachine;
runtime/adopt_test.go:TestAdoptRestartsACarriedObjectsBehavior,
:TestAdoptRefusesAnObjectWhoseBehaviorCannotRestart,
repl/classifier_behavior_test.go:TestObjectMachineRestartsOverAnUnrelatedDeclaration,
:TestRestartedMachineRunsInTheNewContext, :TestRewritingTheExhibitedMachineDropsTheObject).
Tool-defined: the spec has no notion of re-analysing an edited model
- a nested body over a value the redefined declaration wrote governs over that value, but supersedes it whole: a feature the body does not value takes its type's own default rather than the bound value's, since a FeatureValue binds a feature as a whole
Python bindings:
- generated typed classes (opensysml.generate) cover structural usages only: behavioral
and connector usages are not instance feature values, so no property is emitted for them.
specializes, subsets and redefines all become Python base classes, in declaration
order, and a redefining feature takes the type and multiplicity it does not restate from
what it redefines; a base another declared base already specializes is left implicit,
and a base order that linearizes no way at all keeps the bases it can and records what
it left out as a comment, rather than emitting a module that fails to import. Redefinition narrowing is still not checked
- TypedObject.from_instance rejects an instance whose type another generated class
describes, and accepts a generated subclass of the expected one; it accepts a type
no generated class describes, because Instantiate on a usage reports the usage's
own FQN (Demo::myCar), which the client cannot relate to the definition typing
it. A wrong-typed instance is therefore caught only when its type has a generated
class of its own; unchecked(instance) bypasses the check deliberately
- a service opensysml did not spawn is never stopped by it: attaching takes no ownership
reference and writes no state, so only the spawning process stops the service it recorded,
when its last connection is released. The record authenticates the pid it names by the
process start time written with it, so a reused pid is cleaned up rather than signalled
(connection.py:_write_ownership_record, _authenticate_record,
python/tests/test_lifecycle.py, test_stale_service.py)
- an instance_id outside an Instantiate response (an Evaluate result, say)
is still a bare int64: those responses carry no instance graph to resolve it
- init.py:11-16 - Shadows builtins (RuntimeError, eval)
- a downloaded binary is verified against the digest binary.py:PINNED_SHA256 pins for its
release, independent of the origin that served it; a version with no pin fails rather than
falling back to the served .sha256, unless $OPENSYSML_ALLOW_UNPINNED_DOWNLOAD names that
repository (or is 1) and accepts same-origin trust. A opensysml release pins only service releases published before it, so a
newer service needs a newer opensysml or that opt-in
(scripts/pin_release_checksums.py, python/tests/test_binary.py)
Standard behavioral notation:
- a succession written at namespace level (first part1::action1 then requirement1;) is
parsed and carried with both ends, but there is no enclosing behavior to lower it into,
so it does not execute
- accept at/accept after inside an action body reports ErrNoClock: the action
executor has no clock. The same trigger on a state transition waits on time and fires
- a succession end with no name is carried by identity, so a model whose succession has a
positional end is reported as unsupported by the RDF export rather than written back
- a state machine's change conditions are re-tested once per micro-step and again at
quiescence, and fire on the condition rising, one rise being consumed by the poll that
observed it. KerML has no clock, so the cadence is a tool-defined choice, not a spec
requirement: the coarser per-RTC-step alternative was considered and may be revisited. A
machine that cannot progress reports which condition it waits on
(StateExecutor.SuspendReason) rather than completing silently
- an addressed send reaches the object this context holds as the occurrence of the usage its
target names; a second object instantiated of that same usage is a different object, which a
send addressed to the usage does not reach
- a flow whose ends name no feature to carry (flow a to b; between two action nodes) and
a flow whose end names something that is not a node of the action are reported when the
graph is built: the notation needs a payload or a pin at each end
- a nested action node written inside a loop or branch body is lowered as unsupported and
reported when reached; only statements, and the body parameter the body itself is written
as, execute in those bodies
- the Open-MBEE corpus models still report the two OMG-side notations adjudicated above
(end ; outside an interface body, 'SysML Standard Diagrams'::gv) and unresolved library
references in the notebook models (Scalarattributes::String, start, envelopingShapes,
mRefs). The conjugated end and timeslice item item1 are legal and are accepted
- what DesertKite.sysml (branch InitialDesign) and OOSEM.sysml report, and nothing else:
the 'SysML Standard Diagrams'::gv sites above; attribute 'Animal Capture Rate' :>> OOSEM::MOE;,
where MOE is the short name of a member of OOSEM::'OOSEM Measures' and so is not a member of
OOSEM itself; and 3 references to the decision node __unnamed1 of 'Move with Herd', which
resolve to nothing because a control node's name is not registered as a symbol
(symbols/builder.go buildDecl)
Go gRPC layer:
- SymbolInfo.attributes reports only defaults that fold to a model-level
constant; one written as a feature reference or a call is reported absent
rather than guessed (internal/grpc/attributes.go)
- metadata["type"]/metadata["specializes"] still report only the first edge, kept
for compatibility; specializations is the complete list
- runtime instances are request-local, so an id is resolvable only against the
response that carried it; there is no RPC that fetches an instance by id later
- a quantity crosses in both directions: every outbound path reads one, and an
ExecuteAction input or EvaluateCalc argument carrying one is decoded against the
model's index (ProtoToValueIn), so a unit that does not resolve — or resolves to
something that is not a measurement unit — is reported rather than bound as an
unusable value. The Python client has no quantity encoder yet, so a caller
cannot send one until that lands with the rest of the Python API surface
These are documented for transparency; none block production use.
Language Server (internal/lsp, cmd/sysml-lsp)¶
Standard: LSP 3.17 § Lifecycle Messages. Reference: docs/reference/api.md § internal/lsp.
Measured coverage: 115 tests and subtests in internal/lsp, of which 107 are top-level Test functions, plus the built-binary lifecycle tests in cmd/sysml-lsp.
| Rule | Implementation (file:function) | Tests | Status |
|---|---|---|---|
The server is started by an editor over stdin/stdout, and a client that names the transport on the command line (--stdio, as TransportKind.stdio sends) is served rather than rejected |
cmd/sysml-lsp/main.go run (explicit stdio flag; Go's flag accepts -stdio and --stdio) |
cmd/sysml-lsp/lifecycle_test.go:TestStdioTransportServesTheLifecycle (both spellings, built binary over pipes), cmd/sysml-lsp/main_test.go:TestCommandLine |
✅ Faithful — the flag is a documented no-op because stdio is the only transport; every other unknown flag still exits 2 with usage, so a typo is not swallowed |
shutdown is answered, and afterwards every request but exit is answered InvalidRequest (-32600); a non-exit notification is dropped |
internal/lsp/lifecycle.go Server.Shutdown, Server.lifecycleHandler (wraps the handler chain on the read loop, ahead of async dispatch) |
internal/lsp/lifecycle_test.go:TestAfterShutdownOnlyExitIsServed, :TestNotificationAfterShutdownIsDropped, cmd/sysml-lsp/lifecycle_test.go:TestRequestAfterShutdownIsInvalidRequest |
✅ Faithful |
exit ends the process: status 0 after a preceding shutdown, 1 otherwise |
internal/lsp/lifecycle.go Server.Exit, Server.ExitCode; internal/lsp/server.go Server.Run (returns on the exit signal and closes the connection itself); cmd/sysml-lsp/main.go serve |
internal/lsp/lifecycle_test.go:TestExitEndsTheSessionWithTheStatusLSPRequires, cmd/sysml-lsp/lifecycle_test.go:TestExitAfterShutdownEndsTheProcess, :TestExitWithoutShutdownIsNonzero, :TestClosedStreamEndsTheProcess |
✅ Faithful — Run returns rather than being killed from a handler, so the process leaves no server behind per editor window |
Constraint Solving (internal/core/solve) — advertised extension, not a conformance claim¶
Standard: none. SysML v2 defines evaluation of Invariant, RequirementUsage and assert satisfy against concrete values; it defines no solving semantics. This package is therefore an optional, additive extension that OpenSysML advertises, not a compliance item.
The runtime evaluator (internal/core/runtime) remains the normative semantics: nothing here changes a verdict it reaches, and no verdict is ever derived from a translation. The package answers the questions the evaluator cannot — satisfiability, conflicting subsets, value synthesis — by translating conditions into a solver-independent term IR, writing that IR as an SMT-LIB2 script, and optionally running an external solver over it. No solver is bundled and no module dependency or cgo is added: a solver is an external process, so an absent one is a typed error and the feature is opt-in.
Conditions come from the evaluator's own collection (runtime.Context.ConditionsOf, the accessor over conditionsOf/appendConditions), so a translation encodes exactly what the evaluator checks, in the same order, with the same distinctions: require versus assume, negation, and a body meaning the conjunction of its conditions (a negated body denies that conjunction).
| Rule | Implementation (file:function) | Tests | Status |
|---|---|---|---|
The conditions translated are the conditions the evaluator checks — inherited first, require versus assume kept, negation kept, a negated body denied as one conjunction |
internal/core/runtime/condition.go Context.ConditionsOf, conditionsOf, appendConditions (one collection, shared) |
internal/core/runtime/conditions_of_test.go, internal/core/solve/translate_test.go |
✅ Faithful to the evaluator |
A constraint, requirement or assert satisfy translates to a Query: declared variables, finite datatype sorts, asserted terms, and the provenance (condition text, element, declaring symbol, file and span) of each assertion |
internal/core/solve/translate.go Constraint, Requirement, Satisfaction, Translate; query.go |
internal/core/solve/translate_test.go, satisfy_test.go |
✅ |
Sorts come from the semantic type facts, never from the literals written: Boolean→Bool, Natural/Integer→Int (a Natural also declared non-negative), Rational/Real/Number→Real, String→String, an enumeration definition or a variation point→a finite datatype sort |
internal/core/solve/reference.go sortOf, datatype |
internal/core/solve/translate_test.go, goldens ring_variants.smt2 |
✅ |
| A quantity is normalized to the base units its unit reduces to, through the existing unit model, as an exact rational — so no rounding enters a script — and an incommensurable comparison or sum refuses at translation time | internal/core/solve/translate.go (quantity handling over semantics units/dimensions) |
internal/core/solve/translate_test.go, golden touchdown.smt2 |
✅ |
Anything outside the subset refuses with a typed ErrNotTranslatable naming the construct and where it was written; one refused conjunct fails the whole query, so no partial script exists to answer sat/unsat about conditions it does not contain |
internal/core/solve/errors.go NotTranslatableError; translate.go |
internal/core/solve/translate_test.go (one case per unsupported construct, plus all-or-nothing) |
✅ |
The script is deterministic byte for byte: declarations ordered by name, assertions in evaluation order, generated names stable, set-logic chosen from the sorts and operators actually used |
internal/core/solve/smtlib.go Script; logic.go Query.Logic |
internal/core/solve/golden_test.go (.smt2 goldens, -update, translate-twice), smtlib_unit_test.go |
✅ |
Translatable subset: not, and/&, or/|, xor, implies, if c ? a else b; ==, !=; <, <=, >, >=; +, -, *, unary -/+, real and integer division / and remainder % (truncating toward zero as the evaluator does, with a non-zero divisor asserted); boolean, integer, real and string literals; quantity expressions (450.0 [km/h]); references to scalar-valued features and feature chains that ground in one; enumeration literals and variation-point variants.
Deliberately out of subset (each refuses, none is silently dropped):
- collections and quantifiers — sequences, sets, ->select, ->collect, ->forAll, ->exists, ->size, indexing #(i), ranges, collection-valued features (bounded expansion is not implemented)
- invocations of any kind, calc included: a body may iterate or read state, and folding one is the evaluator's job
- SMT-LIB's own Euclidean div/mod, and real %, which the evaluator answers by floating-point remainder
- exponentiation (**, ^)
- classification and metadata operators (hastype, istype, @, @@, as, meta, all, ===, !==, ??, ~, null), complex numbers, and string operations other than equality
- comparing or adding magnitudes of different dimensions, features whose type determines no scalar sort, unresolved names, and feature chains that ground in nothing
Known limitation: a variable stands for the values a feature may take, constrained only by its sort; a query with no partial assignment asserts no value a model declares, and an assert satisfy … by x translates the requirement's conditions read through the requirement's own parameters rather than substituting x. Such a query asks what the conditions permit, not what one object holds; a query with a partial assignment fixes the values an object holds or the model declares (see the synthesis rows below). Optimization is a later step.
Solving a query — experimental, opt-in¶
A solver is run as a process speaking SMT-LIB2 on standard input: OPENSYSML_SMT names one explicitly, else z3 and then cvc5 are looked for on PATH. OPENSYSML_SMT_TIMEOUT overrides the 10s budget one query is given.
| Rule | Implementation (file:function) | Tests | Status |
|---|---|---|---|
A solver is discovered, never assumed: the override first, then z3 and cvc5 on PATH; an absent solver is a typed NoSolverError naming what to install, never a silent skip and never a fabricated verdict |
internal/core/solve/solver.go Discover, newSolver; errors.go NoSolverError |
solve/solver_test.go:TestDiscovery, repl/check_test.go:TestCheckReportsAnAbsentSolver |
✅ |
The three verdicts stay distinct to the user: sat, unsat, unknown. A timeout, or arithmetic the solver gave up on, is unknown with the reason it gave — never reported as either of the others |
solve/solver.go Solver.Solve, session.verdict, session.reasonUnknown; repl/check.go SolveStatus |
solve/solver_test.go:TestSolverVerdicts, :TestSolverTimeout, repl/check_test.go:TestCheckReportsAnUndecidedAnswer |
✅ |
A solver process failure — crash, non-zero exit, malformed or missing reply, a model naming an undeclared variable — is a typed SolverProcessError, distinguished from unknown |
solve/solver.go session.read, session.finish, Solver.processError; errors.go SolverProcessError |
solve/solver_test.go:TestSolverProcessFailures, :TestSolverBadModel, repl/check_test.go:TestCheckReportsASolverProcessFailure |
✅ |
On sat the model is read back and rendered in OpenSysML's own terms: the feature's qualified name, a quantity's magnitude in the base unit the query normalized to, an enumeration literal or variant by name — never raw SMT-LIB. A value the notation has no literal for is marked as the solver's own rather than mistaken for one |
solve/model.go assign, renderValue; solve/sort.go smtName |
solve/model_test.go, solve/solver_test.go:TestSolverVerdicts |
✅ |
Integer / and % mean what the evaluator means: for a spread of sign combinations, the solved quotient and remainder equal what internal/core/runtime computes for the same expression |
solve/term.go TruncDiv, TruncRem; translate.go multiplicative, remainder |
solve/agreement_test.go:TestSolvedIntegerDivisionAgreesWithEvaluator, :TestSolvedDivisionRejectsEuclideanAnswer |
✅ |
| Division by zero, which the evaluator refuses and SMT-LIB leaves total: a literal zero divisor refuses translation, and any other divisor — integer or real — carries a non-zero side condition, so no model is found by choosing zero | solve/translate.go divisor, guard |
solve/translate_test.go:TestRefusals, solve/agreement_test.go:TestDivisorGuardRulesOutDivisionByZero |
✅ |
%check <name> reports the verdict for a constraint, requirement or satisfaction assertion, and on sat the assignment. It is a read: nothing is materialized, and an action or state debugging session keeps running |
repl/check.go CheckSolve, doCheck; repl/meta.go (command table, dispatch) |
repl/check_test.go |
✅ Faithful — an experimental surface, kept apart from %constraint/%satisfy, whose VerdictStatus it never collapses into |
An unsat verdict is explained by an unsat core: the script names each assertion, cores are turned on, and the core is asked for only once the verdict is unsat. Labels are assertion positions, so each core member is the Assertion — and Provenance — it came from, rather than a table beside the query |
solve/smtlib.go CoreScript, CoreLabel, coreLabelIndex; solve/core.go session.explain, session.unsatCore |
solve/core_test.go:TestCoreScriptShape, :TestCoreLabelsRoundTrip, :TestCoreGolden, :TestExplainedTwoConditionConflict |
✅ |
A reported core is minimal, or says it is not: reduction drops one member at a time, each round a fresh solver process, and Core.Minimal means dropping any one member left the rest satisfiable. A core past DefaultMaxCoreMembers, out of the OPENSYSML_SMT_CORE_BUDGET budget, or a round the solver did not decide, is reported as it stands with Core.Note saying why |
solve/core.go Solver.Explain, Solver.reduce, coreBudgetFromEnv |
solve/core_test.go:TestExplainReducesANonMinimalCore, :TestExplainReportsAnUnreducedCoreHonestly, :TestCoreBudgetFromEnv |
✅ |
A solver that refuses cores, names an assertion the query did not assert, repeats one, answers unreadably or reports an empty core is a typed CoreError (both an ErrNoCore and an ErrSolverProcess), and one that fails mid-reduction stays the SolverProcessError it is — never an empty or invented core |
solve/core.go session.unsatCore, Solver.coreError, Solver.reduce; errors.go CoreError |
solve/core_test.go:TestExplainCoreFailures, :TestExplainReportsAFailureWhileShrinking |
✅ |
%explain <name> prints the conflicting conditions of an unsatisfiable constraint, requirement or satisfaction assertion — role, condition as written, declaring element, file:line:col — in the query's assertion order, including RoleDomain bounds and RoleDefined guards, and names the supertype an inherited condition was declared by. sat points at %check, unknown explains nothing, and it is a read that leaves a debugging session running |
repl/explain.go ExplainSolve, explainQuery, conflictLines, doExplain; repl/meta.go (command table, dispatch) |
repl/explain_test.go |
✅ Faithful — the same experimental surface as %check, and no verdict is fabricated when no solver is installed |
Not a conformance claim: satisfiability is not evaluation. %check answers sat about conditions %constraint cannot evaluate at all (an unbound parameter has no value), and sat never means a condition holds of any object. A core says which conditions cannot hold together, not that any object violates them. Solving and conflict explanation are an experimental OpenSysML extension: SysML v2 defines no solving semantics, and verdicts about a model remain the evaluator's.
Known limitations: a variable divisor sets Query.Nonlinear, so unknown is an expected verdict there, and OPENSYSML_SMT names an executable, not a command line with arguments.
SMT-LIB portability — which backends the extension supports¶
OPENSYSML_SMT may name any executable speaking SMT-LIB2 on standard input, so what a backend must support is stated, probed and reported rather than assumed of z3.
| Rule | Implementation (file:function) | Tests | Status |
|---|---|---|---|
The logic set is the narrowest logic of the SMT-LIB 2.6 logic list that covers the sorts and operators the query actually uses: QF_UF, QF_LIA/QF_NIA, QF_LRA/QF_NRA, and AUFLIRA/AUFNIRA for a query over both Int and Real, the list defining no quantifier-free mixed logic. Truncating integer division by a literal divisor keeps the linear logic (div/mod are the Ints theory's), a variable divisor selects the nonlinear one, and no logic is widened to avoid a hard case |
internal/core/solve/logic.go Query.Features, Query.LogicChoice, Query.Logic |
solve/smtlib_unit_test.go:TestLogicSelection (a case per feature), :TestNonStandardLogicIsExplained, goldens mission_budget.smt2, ring_variants.smt2, touchdown.smt2 |
✅ |
A non-standard logic is emitted only where the list defines none for the feature — datatypes and strings — and says so: LogicChoice.Standard is false, LogicChoice.Why names the features, and the script carries a comment above (set-logic ALL) |
solve/logic.go NonStandardLogic, unstandardisedWhy; smtlib.go Script, CoreScript |
solve/smtlib_unit_test.go:TestNonStandardLogicIsExplained, golden ring_variants.smt2 |
✅ Non-standard by necessity, declared as such |
What the layer requires of a backend is enumerated as capabilities — model output, unsat cores, incremental checks, declare-datatypes, strings, div/mod, nonlinear and mixed arithmetic, the non-standard logic, (maximize)/(minimize) and :opt.priority — and a query says which of them it needs |
solve/capability.go Capability, AllCapabilities, Capability.Feature; logic.go Query.Requires |
solve/capability_test.go:TestQueryRequires, :TestCapabilitiesProbeCapableBackend |
✅ |
A backend is probed rather than believed: one small script per capability, run at most once per executable per process (cached), and only for what the query and operation need — never a subprocess per query. A caller who knows its backend declares them instead (DeclaredCapabilities) and nothing is probed |
solve/capability.go Solver.Capabilities, Solver.require, runProbe, capabilityCache, DeclaredCapabilities |
solve/capability_test.go:TestCapabilitiesProbeCapableBackend (probe count), :TestDeclaredCapabilitiesSkipProbing |
✅ |
A capability the backend rejects makes the request a typed UnsupportedCapabilityError (an ErrUnsupportedCapability) naming the backend, the missing feature, the operation and what the backend said, refused before the query runs — never a silent degrade, a widened logic or a fabricated verdict |
solve/capability.go Solver.require; errors.go UnsupportedCapabilityError, Unsupported; solver.go Solver.Solve; core.go Solver.Explain; configure.go Solver.Configurations |
solve/capability_test.go:TestCapabilitiesProbeIncapableBackend, :TestUnsupportedCapabilityIsRefused |
✅ |
Not supporting a feature, not being runnable, and not deciding stay three different reports: a probe the backend neither answered nor rejected settles nothing, so the query runs and its own unknown verdict or SolverProcessError is what is reported; an absent executable is a SolverProcessError rather than a claim about features; and a probe reply SMT-LIB does not define at all (maybe) is a SolverProcessError too, refusal being kept for (error …), unsupported or a defined reply that contradicts the check |
solve/capability.go capState, smtlibResponse, Capabilities.Undetermined, Capabilities.Missing |
solve/capability_test.go:TestUndeterminedCapabilityDoesNotRefuse, :TestCapabilitiesMissingBackend, :TestUnreadableReplyIsAProcessFailure, :TestUnsupportedReplyIsARefusal, solver_test.go:TestSolveReportsAnUndecidedAnswer |
✅ |
Portability is measured, not asserted: a harness runs one query per feature against whatever OPENSYSML_SMT names and reports each as pass, refuse (the backend rejected a capability it needs) or fail (anything else, including a script the backend would not parse — ours to fix in the writer) |
solve/portability_test.go portabilityCases, TestPortability, runPortabilityCase |
solve/portability_test.go:TestPortability (run with z3 4.8.12 and cvc5 1.3.4), :TestPortabilityGateIsRequired; CI pr.yml / config.yml portability jobs |
✅ |
Verified backends (probed on the machine this was written on, not read off documentation): z3 4.8.12 supports every capability above. cvc5 1.3.4 supports all but the two z3 optimization extensions — it rejects (maximize …) as a parse error and answers unsupported to :opt.priority — so every command works on it except %optimize, which refuses on it by naming the missing extension. Per-platform install instructions and the same matrix for users are 1. Install: solver compatibility.
Differential agreement with the evaluator — the evidence for the translation¶
The translation is checked against the normative evaluator rather than asserted to be faithful. For an element whose conditions translate, and a concrete assignment of the features they read, the property gated is: the query conjoined with that assignment is sat exactly when the evaluator says the conditions hold, and unsat exactly when it says they do not.
| Rule | Implementation (file:function) | Tests | Status |
|---|---|---|---|
| An assignment is pinned by equality assertions on a derived query — the AST and the translated query are never mutated — and every variable the query declares is pinned, so the solver decides the question the evaluator answered and not a weaker one | internal/core/solve/differential_test.go pinnedQuery, pinsOf, pinTerm |
solve/differential_corpus_test.go, differential_random_test.go |
✅ |
| A disagreement is reported with what debugging it needs: element, condition text, per-variable values, the evaluator's verdict or typed error, the solver status, the exact SMT-LIB script, and locations | solve/differential_test.go diffGate.check, diffGate.compare, diffSummary.report |
as above | ✅ |
A solver unknown is recorded, not counted as disagreement; an evaluator typed error is no verdict; and ErrDivisionByZero is required to correspond to the guarded query being unsat for that assignment rather than being skipped |
solve/differential_test.go compare, status |
solve/differential_corpus_test.go, differential_random_test.go |
✅ |
| Coverage is counted, never invisible: translated, skipped-by-refusal, agreed, disagreed, unknown, evaluator-refused, assumption-unmet and without-values are summarized per corpus, with each refusal's reason | solve/differential_test.go diffSummary |
TestDifferentialConformanceCorpus, TestDifferentialStandardLibrary, TestDifferentialTrainingCorpus |
✅ |
| The gate runs over the runtime conformance corpus on the values those fixtures declare, the bundled standard library, and the OMG training corpus | solve/differential_corpus_test.go conditionElements, hostOf, hostsOf (an element is checked on the object carrying the values it reads: the usage stating it, or the usages specializing an abstract family) |
the three tests above | ✅ |
Randomized assignments over the translatable subset — boundaries, negatives, zero divisors, mixed signs, enumerations, variants and quantities in non-base units — are deterministic: a fixed default seed, OPENSYSML_DIFF_SEED to reproduce one, OPENSYSML_DIFF_SWEEP for a longer run |
solve/differential_random_test.go TestDifferentialRandomizedAssignments |
as named | ✅ |
No solver installed means the gate skips loudly and checks nothing, and CI sets OPENSYSML_REQUIRE_SMT=1 so that skipping is a failure there |
solve/agreement_test.go requireSolver; solve/differential_test.go newGate |
.github/workflows/pr.yml, .circleci/config.yml (differential agreement gate steps) |
✅ |
Two real disagreements the gate found, both fixed at their root:
- A real quotient by zero answered an infinity.
internal/core/runtime/eval.goreturned+Inffora / 0.0, soa / b > 0.000001"held" withb = 0.0, while integer division, real remainder, quantity division and the constant folder all reportErrDivisionByZero. Real division now reports it too (runtime/eval_operator_test.go:TestRealDivisionByZeroIsReported). - A variation point had two sorts. A usage redefining a variation attribute was given a finite sort of its own, distinct from the one the variation declaring the variants was given, so
nesting == nesting::nestingTruerefused as mismatched operands. Both now read the variation that declares the variants (solve/reference.govariationDeclaring,solve/translate_test.go:TestVariationSortIsShared).
What agreement proves, and what it does not: the gate is evidence that the translation is faithful to the evaluator for the cases it covered — the concrete assignments of the corpora and of the generated models, within the translatable subset. It is not a conformance claim, it says nothing about the elements that refuse translation, and where the two ever differ the evaluator is right by definition.
Value synthesis and variant configuration — experimental, opt-in¶
| Rule | Implementation (file:function) | Tests | Status |
|---|---|---|---|
| A query takes a partial assignment: pins fix some features to the values the model already fixes and leave the rest free, so the solver synthesises them. Pinning nothing translates exactly the script it always did | solve/pin.go Pin, PinSource, translator.fix; solve/translate.go TranslateWith, ConstraintWith, RequirementWith, SatisfactionWith, translator.pinnedAssertions |
solve/pin_test.go:TestNoPinsTranslateAsBefore, :TestPinnedValueIsAssertedAndReported, :TestGoldenWithFixedValues, :TestFixedValuesAreOrderedByTheirVariable |
✅ |
| A pinned value is read where the evaluator reads it — the object's feature values, else the declared default, through the runtime — and carries its provenance: held by an object, declared by the model, or chosen by the user | solve/pin.go Fixed, FixedFor, fixedValue; repl/synth.go Session.declaredPins, owningElement |
solve/pin_test.go:TestFixedReadsTheValuesTheModelDeclares, repl/synth_test.go:TestSolveKeepsWhatAnObjectHolds, :TestSolveSynthesisesWhatIsFree |
✅ |
A pinned quantity is normalized through the same unit machinery the translator uses, exactly: 5.4 [km/h] is fixed as the rational 1.5 in base units, and a value whose dimension does not match its feature is refused |
solve/pin.go translator.pinQuantity, translator.commensurable, ratOfConst, ratOfFloat; semantics/dimension.go Model.DimensionOfFeature, Model.DimensionOfUnit |
solve/pin_test.go:TestPinnedQuantityIsScaledExactly, :TestPinRefusesAnIncommensurableQuantity, :TestPinnedBareNumberRefusesAMeasuredFeature |
✅ |
A pinned enumeration literal or variant is fixed as the datatype constructor the writer declares, and a value the subset cannot represent — or one of the wrong type — is a typed PinError wrapping ErrNotPinnable, never a silent drop |
solve/pin.go translator.pinTerm, translator.pinDatatype, translator.pinRefusal, PinError, ErrNotPinnable; solve/reference.go (datatype construction, Sort.Variation) |
solve/pin_test.go:TestPinnedEnumerationNamesItsConstructor, :TestPinRefusesAValueWithNoLiteral, :TestPinRefusesAValueOfTheWrongType, :TestPinnedStringIsAsserted |
✅ |
unsat under a partial assignment means no values exist consistent with what is already fixed, reported as exactly that and distinct from the unpinned unsat; pinned assertions carry roles and indices, so an unsat core names the fixed values that conflict |
solve/query.go RolePinned, Query.Fixes, Query.Free; repl/synth.go Session.noValuesLines, Session.conflictingFixed |
solve/synthesis_test.go:TestSynthesisIsUnsatWhenTheFixedValuesForbidIt, :TestFixedValuesInAConflictAreNamed, repl/synth_test.go:TestSolveReportsNoValuesConsistentWithWhatIsFixed, :TestSolveReportsConditionsThatConflictOnTheirOwn |
✅ |
A synthesised model is one witness, not a canonical answer, and is reported as such; values are rendered in OpenSysML's terms through the same model rendering %check uses |
solve/solver.go Result.Model (documented non-canonical); repl/synth.go Session.synthesise, fixedLines, synthesisedLines |
solve/synthesis_test.go:TestSynthesisFillsWhatIsNotFixed, :TestWitnessIsReportedInOpenSysMLTerms, repl/synth_test.go:TestSolveReportsAQuantityAsDeclared |
✅ |
Variant configuration: a chosen selection is checked, a consistent one synthesised, and consistent selections enumerated — one fresh check-sat per solution, each excluding the whole previous assignment, built from the solver's own model |
solve/configure.go Solver.Configurations, session.enumerate, session.deny, session.blocking, Query.Variations, Query.FixValue; repl/synth.go Session.checkSelection, Session.synthesiseConfiguration |
solve/configure_test.go:TestConfigurationsEnumeratesEveryConsistentSelection, :TestVariationsAreTheVariationPointsRead, :TestFixValueChecksWhatItIsGiven, :TestChosenSelectionCanConflict |
✅ |
The enumeration is bounded by DefaultMaxConfigurations (OPENSYSML_SMT_MAX_CONFIGURATIONS overrides it): reaching the bound sets Result.Truncated with Result.AtBound, a solver that stops deciding or runs out of time sets Result.Undecided (with Result.TimedOut for a deadline) and keeps the selections already found rather than discarding them, and results are called exhaustive only after a final check-sat answered unsat |
solve/configure.go MaxConfigurationsFromEnv, session.enumerate, partialResult, foundBeforeDeadline; solve/solver.go Solver.solve, Result.AtBound, Result.Undecided; repl/synth.go Session.enumerateConfigurations, truncation |
solve/configure_test.go:TestMaxConfigurationsFromEnv, :TestConfigurationsStopAtTheirBound, :TestConfigurationsKeepWhatTheyFoundWhenTimeRunsOut, repl/synth_test.go:TestConfigureStopsAtItsBound, :TestConfigureEnumeratesEverySelection |
✅ |
Nested variation points, variants under constraints of their own, assume versus require roles and a denied (assert not) element are configured as the variables and assertions they already are; a query reading no variation point is a typed NoVariationsError, not an empty enumeration |
solve/configure.go Query.Variations, ErrNoVariations, NoVariationsError; solve/translate.go (roles, negation) |
solve/configure_test.go:TestConfigurationsOfAConstrainedVariant, :TestConfiguringWithoutVariationsIsTyped, :TestFixValueRefusesAFeatureWithNoValuesToName, solve/synthesis_test.go:TestSynthesisRespectsAssumedConditions, :TestSynthesisForADeniedElement |
✅ |
%solve <name> synthesises values for a constraint, requirement or satisfaction assertion, printing what was already fixed and what the solver chose, and saying the answer is one witness. It is a read: nothing is materialized and a debugging session keeps running |
repl/synth.go Session.SolveValues, Session.doSolve; repl/check.go pinner; repl/meta.go (command table, dispatch) |
repl/synth_test.go:TestSolveSynthesisesWhatIsFree, :TestSolveAndConfigureKeepADebuggingSession, :TestSolveUnderANaturalDomainAndADivisorGuard, solve/synthesis_test.go:TestSynthesisUnderANaturalDomainAndADivisorGuard |
✅ Faithful — the same experimental surface as %check, reusing SolveStatus/SolveReport and never collapsing into VerdictStatus |
%configure <name> [<variation>=<variant>…] [all [<count>]] checks a chosen selection, synthesises one, or enumerates them up to a bound. An unknown variation point, a name that is not a variant of it, a variation chosen twice, a malformed count and a mixed request are each a message saying what to write instead |
repl/synth.go Session.ConfigureVariants, parseConfigure, Session.configureQuery, chooseVariants, matchVariation, matchVariant, Session.doConfigure; repl/meta.go (command table, dispatch) |
repl/synth_test.go:TestConfigureSynthesisesASelection, :TestConfigureChecksAChosenSelection, :TestConfigureRejectsWhatItCannotAnswer, :TestConfigureOnAnElementReadingNoVariation, :TestConfigureWithoutAName |
✅ Faithful — distinct messages for no solver, an untranslatable element, a solver failure and unknown, never a silent skip |
Known limitations of synthesis and configuration: a synthesised model is one of possibly many, and its choice is the solver's, as is the order selections are enumerated in; the enumeration is bounded and says when it was truncated rather than implying exhaustiveness; a variant is configured as the value of a variation point, not as an object, so nothing is materialized and features a variant would only have once bound are not constrained; a feature the conditions read whose value cannot be read as a scalar stays free and is reported as such rather than fixed; and a variation point outside the translatable subset refuses with ErrNotTranslatable like any other untranslatable condition.
Objective optimization — experimental, opt-in¶
SysML v2 states no direction, no value and no solving semantics for objective, so the contract this layer reads is OpenSysML's own, stated here and in solve/doc.go.
| Rule | Implementation (file:function) | Tests | Status |
|---|---|---|---|
An objective's direction is the trade-study definition typing it — TradeStudies::MinimizeObjective or MaximizeObjective, specializations included, matched by symbol identity so a type merely named alike is not one. An objective typed by neither is a typed ObjectiveError wrapping ErrNotOptimizable, never a guessed direction |
runtime/analysis.go Context.objectiveDirection, Context.specializesLibraryType; solve/objective.go translator.direction |
runtime/analysis_test.go:TestObjectivesOfDirectionValueAndOrder, :TestObjectivesOfDirectionThroughSpecialization, :TestObjectivesOfWithoutDirection, solve/objective_test.go:TestObjectiveRefusals, repl/optimize_test.go:TestOptimizeRefusesAnObjectiveWithoutADirection |
✅ |
The value improved is the expression the objective states for the library's best feature (objective o : MinimizeObjective { attribute :>> best = expression; }), since an ObjectiveMembership owns a requirement usage (§8.3.22.4) which carries no scalar value of its own; a value bound directly is read too, where a model can write one. An objective stating no value is a typed refusal |
runtime/analysis.go Context.objectiveOf, Context.bestValueOf, Context.statesObjectiveBest, restatesFeatureNamed |
runtime/analysis_test.go:TestObjectivesOfValueScope, :TestObjectivesOfWithoutValue, :TestObjectivesOfRedeclared, solve/objective_test.go:TestObjectiveDirectionAndValue, :TestObjectiveRefusals |
✅ |
What is feasible is the case's own conditions — require/assume/assert/inv, inherited first, in the evaluator's order — together with the conditions each objective states in its own body; the trade-study conditions an objective inherits are about choosing among alternatives, not about feasible values, so they are left out |
runtime/analysis.go Context.CaseConditionsOf, Context.appendCheckedConstraint, Context.ownConditionsOf; solve/objective.go AnalysisWith |
runtime/analysis_test.go:TestCaseConditionsOf, :TestCaseConditionsOfInherited, :TestObjectivesOfOwnConditions, solve/objective_test.go:TestObjectiveOwnConditions, repl/optimize_test.go:TestOptimizeReportsTheGreatestValue |
✅ |
| Objectives, values and conditions are read through the runtime's own surfaces, so what is optimized is what the evaluator would evaluate: no declaration is re-parsed in the solver layer and no AST is mutated | runtime/analysis.go Context.ObjectivesOf, RequireAnalysis; solve/objective.go Analysis |
runtime/analysis_test.go:TestObjectivesOfAnalysisUsage, :TestRequireAnalysis, solve/objective_test.go:TestObjectivesInDeclarationOrder, :TestAnalysisRefusesANonAnalysis |
✅ |
| An objective term joins the query as a first-class part of it: it declares variables, decides the logic, contributes datatype sorts, declared domains and divisor guards, and normalizes quantities exactly as an asserted condition does. A query with no objective writes the byte-identical script it always did | solve/query.go Objective, Direction, Query.Objectives, Query.Optimizes, Query.Logic; solve/objective.go translator.optimize |
solve/objective_test.go:TestObjectiveVariablesAreDeclared, :TestObjectiveDecidesLogic, :TestQuantityObjectiveIsNormalized, :TestObjectiveOverVariantSelection, :TestObjectiveWithGuardedDivision, :TestObjectiveFreeScriptsAreUnchanged |
✅ |
An objective outside the translatable subset refuses with a typed ObjectiveError naming the objective, why it was refused, what to write instead and where it was written — a nonlinear term (an optimizer improves a linear objective) included — never a silent skip |
solve/errors.go ObjectiveError, NoObjectiveError, ErrNotOptimizable, ErrNoObjective; solve/objective.go translator.refuseObjective |
solve/objective_test.go:TestObjectiveRefusals, solve/optimize_test.go:TestObjectiveErrorMessage, :TestNoObjectiveErrorMessage, repl/optimize_test.go:TestOptimizeRefusesANonlinearObjective, :TestOptimizeReportsAnAnalysisStatingNoObjective |
✅ |
Several objectives are optimized lexicographically in declaration order, and the script says so itself with (set-option :opt.priority lex) rather than relying on a backend default — z3's box mode returns a model attaining only one of the optima it reports, which would make "the assignment attaining the optimum" untrue |
solve/smtlib.go writeScript, objectiveComment (priority, ordered (minimize e)/(maximize e), provenance comments) |
solve/objective_test.go:TestObjectiveScriptIsExplicitlyLexicographic, :TestSingleObjectiveScriptStatesPriority, goldens objective_lexicographic.smt2, objective_mass.smt2, objective_variants.smt2, objective_guarded.smt2, solve/optimum_test.go:TestLexicographicOptima |
✅ |
(minimize …)/(maximize …), (get-objectives) and :opt.priority are solver extensions, not SMT-LIB2, and cvc5 implements none of them: the backend's capabilities are settled by the shared capability model before a query is sent (probed once and cached, or declared by the caller), and a backend without them is a typed NoOptimizationError wrapping both ErrNoOptimization and the UnsupportedCapabilityError that settled it. Nothing is degraded to a plain check-sat and presented as an optimum |
solve/optimize.go Solver.requireOptimization; solve/capability.go CapOptimization, CapOptimizationPriority, Solver.require |
solve/optimize_test.go:TestOptimizeRefusesABackendWithoutOptimization, :TestOptimizeRefusesABackendWithoutTheOptimizationCapability, :TestOptimumClassification, repl/optimize_test.go:TestOptimizeReportsABackendWithoutOptimization |
✅ |
Every optimum is verified rather than trusted: the objective's value is read back from the reported model, and a further check asks whether any assignment does lexicographically better — unsat is what makes the answer an optimum. z3 4.8.12 (what apt and CI install) reports 9.5 as the maximum of x under x < 10.5, which this refutes instead of reporting |
solve/optimize.go session.optimize, session.attained, session.verifyOptimal, better, classifyOptima |
solve/optimum_test.go:TestOptimumIsVerifiedIndependently, :TestBoundThatIsNotAttained, solve/optimize_test.go:TestOptimumClassification (refuted by verification, verification undecided) |
✅ |
The answers optimization adds stay apart, and none of them fabricates a number: OptimumAttained (verified), OptimumUnbounded (oo), OptimumBounded (an infinitesimal or interval bound, reported as a bound), OptimumUnverified, OptimumUndecided. unsat and unknown remain the verdicts they are, with no optima invented, and a solver answering unreadably is an OptimumError wrapping ErrNoOptimum and ErrSolverProcess |
solve/optimize.go OptimumStatus, Optimum, parseOptimum, classifyOptimum; solve/solver.go Result.Optima |
solve/optimize_test.go:TestOptimumClassification, :TestOptimizeRejectsUnreadableOptima, :TestOptimizeKeepsVerdictsApart, :TestOptimizeProcessFailures, :TestOptimizeRefusesAQueryWithoutObjectives, :TestOptimizeCarriesTheObjectiveThrough, solve/optimum_test.go:TestUnboundedOptimum, :TestUnsatisfiableAnalysis |
✅ |
%optimize <name> reports each objective's optimum for an analysis definition or usage, with its declared unit and the assignment attaining it, through the same model rendering %check uses. It is a read: nothing is materialized and a debugging session keeps running |
repl/optimize.go Session.OptimizeSolve, optimizeQuery, optimumLines, doOptimize; repl/check.go SolveUnbounded, SolveNoOptimum; repl/meta.go (command table, dispatch) |
repl/optimize_test.go:TestOptimizeReportsTheLeastValue, :TestOptimizeReportsObjectivesInDeclarationOrder, :TestOptimizeKeepsADebuggingSessionAndMaterializesNothing, :TestOptimizeIsListedInHelpAndCompletion |
✅ Faithful — the same experimental surface as %check, reusing SolveStatus/SolveReport and never collapsing into VerdictStatus |
objective execution status changes with this PR: an objective was parsed, typechecked and otherwise inert; it is now executed as an optimization query by %optimize. This is not SysML v2 conformance — the spec defines no solving — and the runtime evaluator remains normative for every verdict about a model.
Known limitations of optimization: it needs z3, since optimization is a z3 extension; the objective term must be numeric and linear, so a nonlinear objective refuses (a computed divisor makes a term nonlinear, which is why divisor guards are exercised through the case's conditions rather than the objective's); an old z3 can report an optimum verification refutes, and that is reported as no optimum rather than as a value; boxed and Pareto multi-objective semantics are not offered, only lexicographic declaration order; and an analysis case whose conditions bound nothing is legitimately unbounded rather than an error.