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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 48 additions & 21 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,27 +101,54 @@ primitives (bar, diamond, pentagon, note). `LayoutActivation`/`LayoutBand` alrea
**Visual gate:** sequence shows activation bars + a fragment; action flow shows a fork/join and
a decision/merge with correct shapes.

### State Transition View: implied-source (initial-pseudostate) transitions

SysML v2 allows a state transition with no explicit source state (`then TargetState;`),
meaning an implicit/default transition taken automatically on entry to the enclosing region —
the UML/SysML "initial pseudostate" concept. Today `ReferenceResolver`'s `Transition` edge is
only recorded when both a source and target resolve, so an omitted source produces no edge at
all (a documented limitation, not a crash) — confirmed against the real OMG corpus fixture
`training/12.BindingConnectors/...` cross-checks during the General View relationship-edges
work.

Closing this requires more than an edge-resolution tweak: `AstBuilder`/`ReferenceResolver` need
to distinguish "no source specified" from "source failed to resolve," and
`StateTransitionViewLayoutStrategy` needs to render the conventional small filled-circle
initial-pseudostate marker with an edge into the target state, rather than only ever connecting
two named states.

**Scope:** `AstBuilder` (transition parsing), `SysmlEdge`/`ReferenceResolver` (distinguishing
implied-source from unresolved-source), `StateTransitionViewLayoutStrategy` (initial-pseudostate
marker + edge rendering).
**Visual gate:** a state machine with a `then InitialState;`-shaped entry transition renders a
filled-circle initial marker with an edge into that state.
### State Transition View: attached-transition states, entry/exit actions, inherited pseudostate features

Investigation for this item (cross-checking the real OMG corpus fixture
`training/25.Transitions/TransitionActions.sysml` and the OMG spec's own worked example, `formal-26-03-02.md`
Annex A.7 "States") found the actual gap is significantly larger than originally scoped here, and is a
correctness bug, not just a missing notation refinement:

1. **Attached-transition state bodies are silently dropped (both the state AND its transition).**
SysML v2's most common compact state-machine idiom writes a state's outgoing transition directly after it
with no `transition`/`first` keyword at all, e.g. `state off; accept Sig then starting;` — grammatically
`stateBodyItem: (sourceSuccessionMember)? behaviorUsageMember (targetTransitionUsageMember)*`, where the
transition's source is *implicitly* the immediately preceding state usage in the same body item. The
parallel shape `entryActionMember (entryTransitionMember)*` (e.g. `entry action initial; then off;`) has
the identical problem. `AstBuilder` has no visitor override for either shape, so ANTLR's default
`VisitChildren` (which returns only the last child's result, discarding earlier ones) causes **both** the
preceding usage and its attached transition(s) to vanish — confirmed: `vehicleStates` in the fixture above
should register 4 states and 4 transitions, but exporting it today yields 0 states and 1 transition, plus
"Unresolved reference" warnings for the never-registered state names.
2. **Entry/do/exit action features are entirely unmodeled.** `entryActionMember`, `doActionMember`,
`exitActionMember` (and their nested `statePerformActionUsage`/`stateAcceptActionUsage`/etc.) have no
`AstBuilder` visitor at all — not even the well-formed, spec-preferred style of declaring a named entry
action and referencing it from a separate `transition` statement (OMG spec Annex A.7: `entry action
initial; ... transition initial then off;`) currently registers a resolvable `initial` feature.
3. **Inherited pseudostate-like features don't resolve.** The training corpus's explicit form of an initial
transition, `first start then off;`, references `start` — a real feature every state definition/usage
inherits from `Action` (`Stdlib/SystemsLibrary/Actions.sysml`'s `action start: Action :>> startShot`), not
a special keyword. `ReferenceResolver`'s feature-chain walk only looks up local/imported names, not
inherited members, so `start` (and `done`, its counterpart) fail to resolve even once (1)/(2) are fixed.
4. **Initial-pseudostate marker rendering is already partially implemented but purely heuristic.**
`StateTransitionViewLayoutStrategy.AddInitialMarker` already draws the conventional filled-circle marker
with an arrow into the *first declared* state, unconditionally, regardless of whether a real initial
transition resolves. Once (1)-(3) let `start`/`initial`-sourced transitions resolve, the layout strategy
needs to: (a) prefer the semantically-resolved initial-transition target over the first-declared-state
guess when one exists, and (b) exclude pseudostate/entry-action source features (e.g. `start`, `initial`)
from being drawn as ordinary state boxes — today `CollectStates`'s "states referenced only by transition
endpoints" fallback would add them as a spurious extra box.

**Scope:** `AstBuilder` (new handling for the `behaviorUsageMember (targetTransitionUsageMember)*` and
`entryActionMember (entryTransitionMember)*` state-body shapes, producing the preceding usage node plus one
`SysmlTransitionNode` per attached transition with an implicit `Source`; minimal feature-node support for
entry/do/exit action declarations so their names are resolvable); `ReferenceResolver`/`TryResolveFeatureChain`
(inherited-member lookup so `start`/`done` resolve); `StateTransitionViewLayoutStrategy` (prefer a resolved
initial transition over the first-declared-state heuristic; exclude pseudostate/entry-action sources from
ordinary state-box rendering).
**Visual gate:** a state machine using the `state X; accept ... then Y;` idiom renders every state and every
attached transition correctly; a `first start then InitialState;`-shaped (or `entry action initial; ...
transition initial then X;`-shaped) entry transition renders a filled-circle initial marker with an edge into
the correct (resolved) state, with no spurious `start`/`initial` box.

### Interconnection View: genuine cross-boundary connector routing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

`StateTransitionViewLayoutStrategy` implements `ILayoutStrategy` to produce a State Transition
View diagram. It renders state usages as rounded boxes placed top-to-bottom through
`LayeredPlacement`, an initial pseudo-state marker entering the first declared state, and transitions
as orthogonal arrows annotated with their guard conditions.
`LayeredPlacement`, an initial pseudo-state marker entering the semantically-correct initial
state (falling back to the first declared state), and transitions as orthogonal arrows annotated
with their guard conditions.

##### Data Model

Expand All @@ -22,8 +23,9 @@ private records carry intermediate data: `StateItem` (a state with its computed
Entry point. Resolves the view's `expose` scope via `ExposeScopeResolver.ResolveExposedScope`,
selects the root state definition via `FindRoot(workspace, scope)`, collects its states via
`CollectStates(root, theme, scope)`, resolves its transitions, places the state boxes, adds the
initial marker and the transition edges, and assembles the tree. Returns a minimal 200×100 empty
`LayoutTree` when no root or no states are found.
initial marker (targeting the semantically-resolved initial state when one is found, otherwise
the first-declared state) and the transition edges, and assembles the tree. Returns a minimal
200×100 empty `LayoutTree` when no root or no states are found.

###### `FindRoot(workspace, scope)` and `CollectStates(root, theme, scope)`

Expand All @@ -34,17 +36,33 @@ nested definition and its ancestor can both be relevant), the most specific (dee
qualified name) relevant candidate is preferred via `ExposeScopeResolver.IsMoreSpecificCandidate`,
with the transition-count tie-break used only to break ties among equally specific candidates;
this ordering does not apply when `scope` is `null`. `CollectStates` gathers the declared `state`
usages first (preserving declaration order so the first declared state becomes the initial
state), excluding — when a scope is resolved — any declared state feature whose qualified name
fails `ExposeScopeResolver.IsInSubjectScope`; it then adds any additional state named only by a
transition endpoint **unconditionally** (this second pass has no independent qualified name of its
own to scope against, since it exists solely because a transition names it), building a name →
index lookup.
usages first (preserving declaration order so the first declared state becomes the
fallback-heuristic initial state), excluding — when a scope is resolved — any declared state
feature whose qualified name fails `ExposeScopeResolver.IsInSubjectScope`; it then adds any
additional state named only by a transition endpoint, **except** when that endpoint is a
transition *source* whose name is a pseudostate name (`"start"`/`"done"`) or matches a declared
entry-action feature's name — such a source is never itself a state and must never be rendered as
a box (see "Pseudostate/Entry-Action Source Exclusion" below); building a name → index lookup, and
returning the set of excluded pseudostate/entry-action source names alongside it for
`FindInitialTargetIndex` to use.

###### `ResolveTransitions(root, index)`

Maps each transition's source and target — by their last `::`-separated name segment — to state
indices, carrying the optional guard.
indices, carrying the optional guard. Because pseudostate/entry-action transition sources are
never added to `index` by `CollectStates`, a transition whose source is such a name is silently
dropped here exactly like any other unresolvable endpoint — no additional change was needed in
this method for the pseudostate exclusion to take effect.

###### `FindInitialTargetIndex(root, pseudoSourceNames, index)`

Scans the root's transitions for the first whose source name is in `pseudoSourceNames`
(`"start"`/`"done"`, or a declared entry-action feature's name), and returns the resolved
target's state index, or `null` when no such transition exists. `BuildLayout` uses this index in
preference to index 0 (the first-declared state) when placing the initial marker, falling back to
index 0 only when this returns `null` — preserving the pre-existing heuristic unchanged for every
model that declares no explicit initial-pseudostate/entry-action transition (including the
`03-elevator-state.sysml` gallery example prior to its own initial-transition addition).

###### Placement and routing

Expand Down Expand Up @@ -98,6 +116,20 @@ existing name-lookup approach naturally omits any transition whose endpoint stat
synthesized `--auto` view, whose `ViewNode` is `null`) resolves no scope, so `FindRoot` considers
every candidate and `CollectStates` keeps every state, unchanged from the pre-scoping behavior.

##### Pseudostate/Entry-Action Source Exclusion

A transition's source can name a pseudostate (`start`/`done`) or an entry-action feature rather
than a genuine declared state — the OMG Annex A.7 idioms `first start then off;` and
`entry action initial; then off;` both produce a `SysmlTransitionNode` whose `Source` is such a
name. Neither name is ever separately declared as a `state` usage, so before this exclusion
existed, `CollectStates`'s "additional state referenced only by a transition endpoint" pass would
synthesize a spurious box labelled `"start"` (or the entry action's name) purely because a
transition referenced it — a state box for something that is not a state. `CollectStates` now
seeds a `pseudoSourceNames` set with the literal names `"start"` and `"done"` plus every declared
`entry`-keyword feature's `Name`, and skips adding a transition's source-side name to the state
list whenever it appears in that set; the target side of any transition is never affected by this
exclusion, since a transition's target is always a genuine state.

##### Error Handling

Null `context` or `options` arguments throw `ArgumentNullException`. The absence of an eligible
Expand Down
62 changes: 62 additions & 0 deletions docs/design/sysml2-tools-language/semantic/model/ast-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ stack with `::` to form the fully-qualified name.
| `VisitRequirementUsage` | `RequirementUsageContext` | `SysmlFeatureNode` (`FeatureKeyword = "requirement"`) |
| `VisitDependency` | `DependencyContext` | `SysmlDependencyNode` |
| `VisitBindingConnectorAsUsage` | `BindingConnectorAsUsageContext` | `SysmlConnectionNode` (kind `binding`) |
| `VisitStateBodyItem` | `StateBodyItemContext` | `SysmlNode` or `MultiNodeCapture` (usage + transitions) |
| `VisitEntryActionMember` | `EntryActionMemberContext` | `SysmlFeatureNode` (`FeatureKeyword = "entry"`) |
| `VisitDoActionMember` | `DoActionMemberContext` | `SysmlFeatureNode` (`FeatureKeyword = "do"`) |
| `VisitExitActionMember` | `ExitActionMemberContext` | `SysmlFeatureNode` (`FeatureKeyword = "exit"`) |

`GetDeclaredName(IdentificationContext)` handles the three grammar alternatives:

Expand Down Expand Up @@ -242,6 +246,64 @@ placeholder form's feature typing (`verify requirement <name> : <Type>;`, via th
because nothing else in `AstBuilder` currently visits into `requirementBody`/`caseBody`
subtrees.

`VisitStateUsage` additionally calls `ExtractFeatureTyping(decl?.featureSpecializationPart())` and
sets the result on the constructed `SysmlFeatureNode`'s `FeatureTyping` property — previously this
was always left `null` for state usages, unlike every other usage kind built via `BuildUsageNode`.
This is a prerequisite for `StateTransitionViewLayoutStrategy`'s expose-scoping root selection
(which resolves an exposed usage's type via its `Typing` edge) and for
`ReferenceResolver.TryResolveInheritedActionMember` (below) to find the enclosing usage's
supertype when its source has no explicit `state usage : Type { ... }` form.

**Attached-transition state bodies and entry/do/exit action features.** The `stateBodyItem`
grammar rule has six alternatives; two of them attach a transition directly onto the immediately
preceding usage within the same alternative rather than exposing it as a separate
`transitionUsageMember`: `behaviorUsageMember (targetTransitionUsageMember)*` (e.g.
`state off; accept Signal then starting;`) and `entryActionMember (entryTransitionMember)*` (e.g.
`entry action initial; then off;`). Before `VisitStateBodyItem` existed, `AstBuilder` only ever
visited the leading usage of each alternative (via the default `VisitChildren` aggregation) and
silently dropped every attached transition, so this common OMG Annex A.7 idiom produced no
transition edge at all.

`VisitStateBodyItem` dispatches all six alternatives explicitly:

- `nonBehaviorBodyItem`, `transitionUsageMember`, `doActionMember`, `exitActionMember` — passed
straight through to `Visit(...)` (no attached-transition shape applies).
- `behaviorUsageMember (targetTransitionUsageMember)*` — visits the usage, then calls
`BuildAttachedTransition` once per `targetTransitionUsageMember` entry, each producing a
`SysmlTransitionNode` whose `Source` is the visited usage's `Name`.
- `entryActionMember (entryTransitionMember)*` — visits the entry action (via
`VisitEntryActionMember`, below), then calls `BuildEntryAttachedTransition` once per
`entryTransitionMember` entry (handling both the `guardedTargetSuccession` and bare `THEN`
grammar alternatives), each producing a `SysmlTransitionNode` sourced from the entry action's
name.

When one or more attached transitions are produced, `VisitStateBodyItem` returns a
`MultiNodeCapture` (a private nested `SysmlNode` subtype, mirroring the existing
`AnnotationCapture` sentinel pattern exactly) wrapping the usage plus its transition(s); when none
are produced, it returns the visited usage/pass-through result directly with no wrapping.
`CollectChildren` — the only collection helper that ever visits a `stateBodyItem()` (confirmed by
inspection: `VisitStateDefinition` and `VisitStateUsage` are the sole two call sites) — flattens
any `MultiNodeCapture.Nodes` it encounters into the resulting `Children` list, exactly as it
already does for `AnnotationCapture`. Like `AnnotationCapture`, `MultiNodeCapture` is never
registered as a `[JsonDerivedType]` and must never reach a real `Children` list; the same
single-call-site guarantee that protects `AnnotationCapture` protects it here.

`VisitEntryActionMember`, `VisitDoActionMember`, and `VisitExitActionMember` each delegate to a
shared `BuildStateActionFeatureNode(usage, keyword)` helper, which builds a **minimal**
`SysmlFeatureNode` — `FeatureKeyword` set to `"entry"`/`"do"`/`"exit"` respectively, `Children`
always empty — using `ExtractStateActionName` to determine the node's `Name`. Entry/do/exit
action *bodies* are behavioral (statement sequences: assignments, sends, control flow), which is
out of scope for this unit's declarative AST; this deliberately mirrors the existing
`VisitRequirementUsage` "minimal capture" pattern rather than attempting to model action-body
statements. `ExtractStateActionName` only derives a name for the named
`ACTION usageDeclaration?` grammar alternative (e.g. `do action providePower { ... }` →
`"providePower"`); for the unnamed reference-subsetting alternative (e.g.
`entry performSelfTest{...}`, a reference to an inherited/imported behavior rather than a new
declaration) it deliberately returns `null` rather than attempting to derive or evaluate a name —
the resulting feature node still registers as an (unnamed, unregistered) AST child so no
information is lost, but it is not itself a resolvable symbol. This scope boundary is intentional
and matches the ROADMAP's framing of entry/do/exit action support as "minimal, non-behavioral."

##### Error Handling

Anonymous elements (null declared names) are silently skipped — visitor methods return `null`
Expand Down
Loading
Loading