From 96157dc43b06b3f91f7a9ff46b383cd01cd199c8 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 11:57:17 -0400 Subject: [PATCH 1/6] docs(roadmap): correct and expand State Transition View gap scope Cross-checked against the OMG training corpus (25.Transitions) and the official spec's own Annex A.7 worked example. The gap is larger than originally scoped: attached-transition state bodies silently drop both the state and its transition; entry/do/exit action features have no AstBuilder support at all; inherited pseudostate-like features (start, done) don't resolve; and the initial-marker rendering already exists but is a pure first-declared-state heuristic, not semantically driven. --- ROADMAP.md | 69 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d7d24be3..e26d0052 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 From 0e7d68c82beb09ad35ef1332937e1ec0a5e831e2 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 12:50:32 -0400 Subject: [PATCH 2/6] Support attached-transition state bodies and inherited pseudostate features - AstBuilder: add MultiNodeCapture sentinel; synthesize attached transitions for behaviorUsageMember/targetTransitionUsageMember and entryActionMember/entryTransitionMember state body shapes; build minimal entry/do/exit action feature nodes; populate FeatureTyping in VisitStateUsage (previously always dropped). - ReferenceResolver: add TryResolveInheritedActionMember fallback so a transition's implicit start/done source resolves against the enclosing state's supertype chain, with a narrow last-resort fallback to stdlib Actions::Action for unqualified state/action definitions. - Add WorkspaceLoader and OMG corpus regression tests covering attached transitions, entry actions, explicit state typing, and inherited start/done resolution. --- .../Semantic/Model/AstBuilder.cs | 218 ++++++++++++- .../Semantic/Model/ReferenceResolver.cs | 152 ++++++++- .../Parser/OmgModelsTests.cs | 71 ++++ .../Semantic/WorkspaceLoaderTests.cs | 305 +++++++++++++++++- 4 files changed, 739 insertions(+), 7 deletions(-) diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs index 3e73ef8b..f20ab96f 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs @@ -245,6 +245,22 @@ private sealed class AnnotationCapture : SysmlNode public required SysmlAnnotation Annotation { get; init; } } + /// + /// Sentinel node used to carry more than one real up through the + /// generic ANTLR Visit pipeline from a single grammar alternative that actually + /// produces several sibling AST nodes (e.g. a stateBodyItem's attached-transition + /// shapes: the preceding state/entry-action usage plus one + /// per attached transition). Never appears in a real + /// list — always intercepts it and flattens its + /// into the owning node's children instead, mirroring how + /// is intercepted and routed into + /// rather than becoming a child itself. + /// + private sealed class MultiNodeCapture : SysmlNode + { + public required IReadOnlyList Nodes { get; init; } + } + /// public override SysmlNode? VisitPartDefinition(SysMLv2Parser.PartDefinitionContext context) { @@ -722,12 +738,20 @@ private static void CollectVerificationMembers(Antlr4.Runtime.Tree.IParseTree no /// public override SysmlNode? VisitStateUsage(SysMLv2Parser.StateUsageContext context) { - var name = GetDeclaredName(context.actionUsageDeclaration()?.usageDeclaration()?.identification()); + var decl = context.actionUsageDeclaration()?.usageDeclaration(); + var name = GetDeclaredName(decl?.identification()); if (name is null) { return null; } + // A state usage's own feature typing (e.g. `state vehicleStates : VehicleStates { ... }`) + // was previously dropped entirely — unlike BuildUsageNode's generic usage handling, no + // Typing edge was ever recorded for state usages. This is necessary so + // ReferenceResolver's inherited-pseudostate-feature fallback (start/done) can walk from + // this usage to its state def and on to Actions::Action via the Supertype chain. + var typing = ExtractFeatureTyping(decl?.featureSpecializationPart()); + // Collect the state body (nested state usages and transitions) as children, mirroring // VisitStateDefinition. Anonymous ("state x;") usages have no body items to collect. _namespaceStack.Add(name); @@ -739,11 +763,199 @@ private static void CollectVerificationMembers(Antlr4.Runtime.Tree.IParseTree no Name = name, QualifiedName = QualifyName(name), FeatureKeyword = "state", + FeatureTyping = typing, Children = children, Annotations = annotations, }; } + /// + /// Dispatches a single stateBodyItem alternative to the appropriate builder logic. + /// Most alternatives (nonBehaviorBodyItem, transitionUsageMember, + /// doActionMember, exitActionMember) already produce exactly one AST node via + /// the default ANTLR visitor dispatch, so they are simply passed through unchanged. The two + /// "attached transition" shapes — (sourceSuccessionMember)? behaviorUsageMember + /// (targetTransitionUsageMember)* and entryActionMember (entryTransitionMember)* + /// — each produce more than one sibling node (the preceding state/entry-action usage, plus + /// one per attached transition whose Source is + /// implicitly that preceding usage's declared name); without this override, ANTLR's default + /// VisitChildren aggregation silently discards every child result but the last, + /// dropping both the preceding usage and any earlier attached transitions. + /// + public override SysmlNode? VisitStateBodyItem(SysMLv2Parser.StateBodyItemContext context) + { + if (context.nonBehaviorBodyItem() is { } nonBehaviorBodyItem) + { + return Visit(nonBehaviorBodyItem); + } + + if (context.transitionUsageMember() is { } transitionUsageMember) + { + return Visit(transitionUsageMember); + } + + if (context.doActionMember() is { } doActionMember) + { + return Visit(doActionMember); + } + + if (context.exitActionMember() is { } exitActionMember) + { + return Visit(exitActionMember); + } + + if (context.behaviorUsageMember() is { } behaviorUsageMember) + { + var usageNode = Visit(behaviorUsageMember); + var targets = context.targetTransitionUsageMember(); + if (usageNode is null || targets.Length == 0) + { + return usageNode; + } + + var sourceName = usageNode.Name; + var nodes = new List { usageNode }; + foreach (var target in targets) + { + nodes.Add(BuildAttachedTransition(sourceName, target.targetTransitionUsage())); + } + + return new MultiNodeCapture { Nodes = nodes }; + } + + if (context.entryActionMember() is { } entryActionMember) + { + var entryNode = Visit(entryActionMember); + var transitions = context.entryTransitionMember(); + if (entryNode is null || transitions.Length == 0) + { + return entryNode; + } + + var sourceName = entryNode.Name; + var nodes = new List { entryNode }; + foreach (var transition in transitions) + { + nodes.Add(BuildEntryAttachedTransition(sourceName, transition)); + } + + return new MultiNodeCapture { Nodes = nodes }; + } + + return null; + } + + /// + /// Builds the for one targetTransitionUsageMember + /// attached after a state/action usage (e.g. accept Sig then starting;), whose + /// Source is implicitly the preceding usage's declared name (never present in the + /// grammar itself). Extraction mirrors 's handling of the + /// explicit transition ... then ...; form exactly (target via + /// , guard via the raw if expression text). + /// + private SysmlTransitionNode BuildAttachedTransition( + string? sourceName, + SysMLv2Parser.TargetTransitionUsageContext? usage) + { + var target = ConnectorEndReference( + usage?.transitionSuccessionMember()?.transitionSuccession()?.connectorEndMember()); + var guard = usage?.guardExpressionMember()?.ownedExpression()?.GetText(); + + return new SysmlTransitionNode + { + Source = sourceName, + Target = target, + Guard = guard, + }; + } + + /// + /// Builds the for one entryTransitionMember + /// attached after an entry action (e.g. entry action initial; ... then off;), + /// whose Source is implicitly the preceding entry action's declared name. Handles + /// both the guarded (guardedTargetSuccession: if ... then ...) and bare + /// (THEN transitionSuccessionMember) alternatives. + /// + private SysmlTransitionNode BuildEntryAttachedTransition( + string? sourceName, + SysMLv2Parser.EntryTransitionMemberContext transition) + { + var guarded = transition.guardedTargetSuccession(); + var successionMember = guarded?.transitionSuccessionMember() + ?? transition.transitionSuccessionMember(); + var target = ConnectorEndReference( + successionMember?.transitionSuccession()?.connectorEndMember()); + var guard = guarded?.guardExpressionMember()?.ownedExpression()?.GetText(); + + return new SysmlTransitionNode + { + Source = sourceName, + Target = target, + Guard = guard, + }; + } + + /// + public override SysmlNode? VisitEntryActionMember(SysMLv2Parser.EntryActionMemberContext context) + { + return BuildStateActionFeatureNode(context.stateActionUsage(), "entry"); + } + + /// + public override SysmlNode? VisitDoActionMember(SysMLv2Parser.DoActionMemberContext context) + { + return BuildStateActionFeatureNode(context.stateActionUsage(), "do"); + } + + /// + public override SysmlNode? VisitExitActionMember(SysMLv2Parser.ExitActionMemberContext context) + { + return BuildStateActionFeatureNode(context.stateActionUsage(), "exit"); + } + + /// + /// Builds a minimal, deliberately non-behavioral for an + /// entry/do/exit action member, registering only its declared name (so + /// it becomes a resolvable transition source per the OMG spec's Annex A.7 idiom, e.g. + /// entry action initial; ... transition initial then off;) with no children. The + /// nested action body's internal semantics — parameter bindings + /// (performSelfTest{ in vehicle = operatingVehicle; }), nested steps + /// (do action providePower { /* ... */ }), accept/send/assignment action-usage + /// alternatives — are deliberately NOT modeled; this tool only needs the named feature + /// itself to exist and be resolvable, not a full action-body AST. + /// + private SysmlFeatureNode BuildStateActionFeatureNode( + SysMLv2Parser.StateActionUsageContext? usage, + string keyword) + { + var name = ExtractStateActionName(usage); + + return new SysmlFeatureNode + { + Name = name, + QualifiedName = name is not null ? QualifyName(name) : null, + FeatureKeyword = keyword, + Children = Array.Empty(), + Annotations = Array.Empty(), + }; + } + + /// + /// Extracts the declared name of a stateActionUsage, or when + /// the action is unnamed. Only the named ACTION usageDeclaration? alternative of + /// performActionUsageDeclaration (e.g. action providePower { ... }) yields a + /// name; the unnamed reference-subsetting form (e.g. entry performSelfTest{ ... }, + /// which subsets/references an existing behavior rather than declaring a new named + /// feature) and the stateAcceptActionUsage/stateSendActionUsage/ + /// stateAssignmentActionUsage/emptyActionUsage_ alternatives are out of scope + /// per ROADMAP.md — only NAMED entry actions need to be resolvable transition sources. + /// + private static string? ExtractStateActionName(SysMLv2Parser.StateActionUsageContext? usage) + { + var declaration = usage?.statePerformActionUsage()?.performActionUsageDeclaration(); + return GetDeclaredName(declaration?.usageDeclaration()?.identification()); + } + /// public override SysmlNode? VisitTransitionUsage(SysMLv2Parser.TransitionUsageContext context) { @@ -1625,6 +1837,10 @@ private static IReadOnlyList GetSubclassificationSupertypes( { annotations.Add(capture.Annotation); } + else if (node is MultiNodeCapture multi) + { + children.AddRange(multi.Nodes); + } else if (node is not null) { children.Add(node); diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs index 8be14370..1b2e0af7 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs @@ -943,10 +943,14 @@ private void ResolveFeatureChains( } // Transition source/target (an implied/omitted Source produces no edge — documented - // limitation, since there is nothing to walk a chain from). + // limitation, since there is nothing to walk a chain from). Source additionally falls + // back to TryResolveInheritedActionMember (see its doc comment) when the ordinary + // dotted-chain walk fails — this is the only reference kind allowed that fallback, since + // it exists specifically to resolve inherited pseudostate-like features (`start`/`done`) + // used as a transition's implicit initial source (e.g. `first start then off;`). if (node is SysmlTransitionNode transition) { - var resolvedSource = ResolveFeatureChainSide( + var resolvedSource = ResolveTransitionSource( transition.Source, filePath, resolvedInFile, namespaceStack, imports); var resolvedTarget = ResolveFeatureChainSide( transition.Target, filePath, resolvedInFile, namespaceStack, imports); @@ -1019,6 +1023,150 @@ private void ResolveFeatureChains( return null; } + /// + /// Resolves a 's Source side. Behaves exactly like + /// (same diagnostic/deduplication behavior) except + /// that, when the ordinary dotted-chain walk fails, it also tries + /// before giving up — the narrow fallback + /// that lets an implied initial-pseudostate transition (e.g. first start then off;) + /// resolve start/done, inherited members that ordinary scope/import + /// resolution can never see since they are not declared or imported anywhere in the + /// referencing file. This fallback is deliberately scoped to Transition Source only + /// — not Target, and not connection/binding endpoints — per ROADMAP.md's explicit + /// instruction. + /// + private string? ResolveTransitionSource( + string? source, + string filePath, + HashSet resolvedInFile, + IReadOnlyList namespaceStack, + IReadOnlyList imports) + { + if (source is not { Length: > 0 }) + { + return null; + } + + if (TryResolveFeatureChain(source, namespaceStack, imports, out var resolved)) + { + return resolved; + } + + if (TryResolveInheritedActionMember(namespaceStack, source, out var inherited)) + { + return inherited; + } + + if (resolvedInFile.Add(source)) + { + _diagnostics.Add(new SysmlDiagnostic( + filePath, + 0, 0, + DiagnosticSeverity.Warning, + $"Unresolved reference: '{source}'")); + } + + return null; + } + + /// + /// Narrow fallback for resolving a Transition Source that names a member inherited + /// from the enclosing state/action's type or ancestor chain, rather than declared/imported + /// directly — the shape needed for real SysML v2 initial-pseudostate/entry-action idioms + /// like first start then off;, where start is a real feature every state + /// definition/usage inherits from Actions::Action + /// (action start: Action :>> startShot), not a special keyword. + /// + /// Resolution order: (1) if the enclosing node (the current + /// top) has a resolved edge (e.g. a + /// state x : SomeStateDef { ... } usage), follow it to the type node and walk its + /// chain via ; + /// (2) failing that, walk the enclosing node's own + /// / edges + /// directly via (covers a state def that + /// itself declares an explicit :> supertype); (3) only if both fail, and only for + /// the two well-known names ROADMAP.md calls out ("start"/"done"), and only + /// when the enclosing node is itself state/action-keyworded, fall back to a hardcoded, + /// narrowly-scoped last resort: look the name up directly among + /// Actions::Action's own direct children in the stdlib. This last-resort branch + /// exists because this codebase implements no general implicit-generalization/default- + /// supertype inference (a bare state def VehicleStates; with no explicit :> + /// clause never gets an automatic Supertype edge to States::StateAction/ + /// Actions::Action the way the real SysML v2 spec implies) — it is a deliberate, + /// explicitly-scoped simplification, not a general inherited-member resolver, and must not + /// be reused for any other reference kind or name. + /// + private bool TryResolveInheritedActionMember( + IReadOnlyList namespaceStack, + string name, + out string resolvedName) + { + resolvedName = string.Empty; + + if (namespaceStack.Count == 0) + { + return false; + } + + var enclosingNode = _symbolTable.Lookup(string.Join("::", namespaceStack)); + if (enclosingNode is null) + { + return false; + } + + var typingEdge = enclosingNode.ResolvedEdges.FirstOrDefault(e => e.Kind == SysmlEdgeKind.Typing); + if (typingEdge is not null) + { + var typeNode = _symbolTable.Lookup(typingEdge.TargetQualifiedName); + if (typeNode is not null) + { + var viaTyping = FindMemberInTypeHierarchy(typeNode, name, new HashSet()); + if (viaTyping?.QualifiedName is { Length: > 0 } viaTypingQualifiedName) + { + resolvedName = viaTypingQualifiedName; + return true; + } + } + } + + var viaAncestor = FindMemberInAncestorChain(enclosingNode, name, new HashSet()); + if (viaAncestor?.QualifiedName is { Length: > 0 } viaAncestorQualifiedName) + { + resolvedName = viaAncestorQualifiedName; + return true; + } + + // Last resort: only for the two well-known inherited pseudostate/action members named in + // ROADMAP.md, and only when the enclosing node is itself state/action-keyworded, so this + // simplification cannot silently misfire for unrelated names or node kinds. + if (name is not ("start" or "done")) + { + return false; + } + + var isStateOrActionKeyworded = enclosingNode switch + { + SysmlFeatureNode { FeatureKeyword: "state" or "action" } => true, + SysmlDefinitionNode { DefinitionKeyword: "state def" or "action def" } => true, + _ => false, + }; + + if (!isStateOrActionKeyworded) + { + return false; + } + + var actionDefinition = _symbolTable.Lookup("Actions::Action"); + var fallback = actionDefinition?.Children.FirstOrDefault(c => c.Name == name); + if (fallback?.QualifiedName is not { Length: > 0 } fallbackQualifiedName) + { + return false; + } + + resolvedName = fallbackQualifiedName; + return true; + } + /// /// Resolves a dotted feature chain (e.g. engine.fuelPort, /// rearAxle.leftHalfAxle.axleToWheelPort) to an instance-relative qualified path for diff --git a/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs index 466b28bf..899bb325 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Parser/OmgModelsTests.cs @@ -164,4 +164,75 @@ public async Task Binding_OmgCorpusFixture_ResolvesBindingEdgesViaImplicitRedefi bindingEdges); Assert.Equal(2, bindingEdges.Count); } + + /// + /// The dedicated 25.Transitions/TransitionActions.sysml corpus fixture exercises all + /// three ROADMAP.md sub-problems together: the attached-transition state-body idiom + /// (state off; accept VehicleStartSignal then starting;), named/unnamed entry/do/exit + /// action features (state on { entry performSelfTest{...}; do action providePower{...}; + /// exit action applyParkingBrake{...}; }), and an inherited-pseudostate-feature initial + /// transition (first start then off;, where start is inherited from + /// Actions::Action since state def VehicleStates; declares no explicit + /// supertype). Before the fix this fixture produced 0 resolved states/transitions plus + /// "Unresolved reference" warnings for every never-registered name; the fixed counts below + /// were confirmed by directly exporting this fixture and inspecting the resulting AST + /// (see the developer report for the exact command). + /// + [Fact] + public async Task Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions() + { + var omgRoot = FindOmgModelsRoot(); + var file = Path.Combine(omgRoot, "training", "25.Transitions", "TransitionActions.sysml"); + Assert.True(File.Exists(file), $"Expected fixture not found: {file}"); + + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([file], stdlibTable); + + Assert.DoesNotContain(result.Diagnostics, + d => d.Message.Contains("Unresolved reference: 'start'", StringComparison.Ordinal) || + d.Message.Contains("Unresolved reference: 'off'", StringComparison.Ordinal) || + d.Message.Contains("Unresolved reference: 'starting'", StringComparison.Ordinal) || + d.Message.Contains("Unresolved reference: 'on'", StringComparison.Ordinal)); + + var vehicleStates = (SysmlFeatureNode)result.Workspace!.Declarations["'Transition Actions'::vehicleStates"]; + + // 3 declared state-keyword children (off, starting, on); vehicleStates itself is the + // enclosing state usage, not counted among "states of the machine". + var declaredStates = vehicleStates.Children + .OfType() + .Where(f => f.FeatureKeyword == "state") + .ToList(); + Assert.Equal(3, declaredStates.Count); + Assert.Equal(["off", "starting", "on"], declaredStates.Select(s => s.Name)); + + // 4 transitions: start->off (pseudostate/initial), off->starting, starting->on, on->off — + // all fully resolved (each has a Transition ResolvedEdge, none unresolved). + var transitions = vehicleStates.Children.OfType().ToList(); + Assert.Equal(4, transitions.Count); + Assert.All(transitions, t => Assert.Contains(t.ResolvedEdges, e => e.Kind == SysmlEdgeKind.Transition)); + + var transitionEdges = transitions + .SelectMany(t => t.ResolvedEdges) + .Where(e => e.Kind == SysmlEdgeKind.Transition) + .Select(e => (Source: e.SourceQualifiedName ?? string.Empty, Target: e.TargetQualifiedName)) + .ToList(); + Assert.Contains(("Actions::Action::start", "'Transition Actions'::vehicleStates::off"), transitionEdges); + Assert.Contains( + ("'Transition Actions'::vehicleStates::off", "'Transition Actions'::vehicleStates::starting"), + transitionEdges); + Assert.Contains( + ("'Transition Actions'::vehicleStates::starting", "'Transition Actions'::vehicleStates::on"), + transitionEdges); + Assert.Contains( + ("'Transition Actions'::vehicleStates::on", "'Transition Actions'::vehicleStates::off"), + transitionEdges); + + // The "on" state's entry/do/exit action features are all registered: the entry action is + // the fixture's unnamed reference-subsetting form (Name null), do/exit are named. + var onState = declaredStates.Single(s => s.Name == "on"); + var actionFeatures = onState.Children.OfType().ToList(); + Assert.Contains(actionFeatures, f => f.FeatureKeyword == "entry" && f.Name is null); + Assert.Contains(actionFeatures, f => f.FeatureKeyword == "do" && f.Name == "providePower"); + Assert.Contains(actionFeatures, f => f.FeatureKeyword == "exit" && f.Name == "applyParkingBrake"); + } } diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs index bf01f36c..f880f956 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs @@ -2759,12 +2759,16 @@ state def Behavior { } /// - /// A transition with an implied/omitted Source (an accept ... then target; - /// form with no preceding state to walk from) should produce no Transition edge — - /// a documented limitation of this unit, not a crash or a partial edge. + /// A previously-broken attached-transition shape: state off; accept Signal via + /// requestPort then off; is the stateBodyItem: (sourceSuccessionMember)? + /// behaviorUsageMember (targetTransitionUsageMember)* grammar alternative, whose + /// transition's Source is implicitly the immediately preceding state off; + /// usage. Before the attached-transition fix this silently dropped both the state and the + /// transition (ANTLR's default VisitChildren keeps only the last child); now both + /// are preserved and the transition resolves to a genuine self-loop edge. /// [Fact] - public async Task WorkspaceLoader_LoadAsync_TransitionImpliedSource_ProducesNoEdge() + public async Task WorkspaceLoader_LoadAsync_AttachedTransitionAfterState_ResolvesSelfLoopEdge() { // Arrange var tempFile = Path.GetTempFileName() + ".sysml"; @@ -2786,6 +2790,50 @@ accept Signal via requestPort var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + // Assert + Assert.NotNull(result.Workspace); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Transition && + e.SourceQualifiedName == "P::Behavior::off" && + e.TargetQualifiedName == "P::Behavior::off"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A transition attached after a genuinely unnamed/anonymous preceding usage (an anonymous + /// action;, which VisitActionUsage intentionally returns + /// for) has no name to serve as the attached transition's implicit Source. This is + /// the one remaining documented limitation: no crash, no partial edge, just nothing + /// recorded for that body item. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_TransitionImpliedSource_ProducesNoEdge() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + item def Signal; + state def Behavior { + port requestPort; + action; + accept Signal via requestPort + then off; + state off; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + // Assert Assert.NotNull(result.Workspace); Assert.DoesNotContain(result.Workspace!.Index.AllEdges, @@ -2797,6 +2845,255 @@ accept Signal via requestPort } } + /// + /// Two attached transitions after the same preceding state usage (repeated + /// targetTransitionUsageMember) should both be captured, both with the preceding + /// usage's name as their implicit Source. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_MultipleAttachedTransitionsAfterState_CapturesAll() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + item def Sig1; + item def Sig2; + state def Behavior { + state a; + accept Sig1 then b; + accept Sig2 then c; + state b; + state c; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Transition && + e.SourceQualifiedName == "P::Behavior::a" && + e.TargetQualifiedName == "P::Behavior::b"); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Transition && + e.SourceQualifiedName == "P::Behavior::a" && + e.TargetQualifiedName == "P::Behavior::c"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// The entryActionMember (entryTransitionMember)* attached-transition shape (e.g. + /// entry action initial; then off;) should capture both the named entry-action + /// feature and its attached transition, whose implicit Source is the entry action's + /// declared name. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_EntryActionWithAttachedTransition_CapturesEntryFeatureAndTransition() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + state def Behavior { + entry action initial; + then off; + state off; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Transition && + e.SourceQualifiedName == "P::Behavior::initial" && + e.TargetQualifiedName == "P::Behavior::off"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// The OMG spec's Annex A.7-preferred style — a named entry action declared once, then + /// referenced from a separate explicit transition statement (rather than an + /// attached entryTransitionMember) — should register a resolvable feature: no + /// "Unresolved reference: 'initial'" diagnostic should be produced. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_NamedEntryAction_RegistersResolvableFeature() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + state def Behavior { + entry action initial; + state off; + transition initial then off; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + Assert.DoesNotContain(result.Diagnostics, + d => d.Message.Contains("Unresolved reference: 'initial'", StringComparison.Ordinal)); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Transition && + e.SourceQualifiedName == "P::Behavior::initial" && + e.TargetQualifiedName == "P::Behavior::off"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// The unnamed reference-subsetting form of an entry action (e.g. + /// entry performSelfTest;, which subsets/references an existing behavior rather + /// than declaring a new named feature) should still register a feature node (with + /// Name null) and must not throw. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_UnnamedEntryActionReferenceForm_NoNameNoCrash() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + action performSelfTest; + state def Behavior { + state on { + entry performSelfTest; + } + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// VisitStateUsage must record a Typing edge for a state usage's explicit + /// feature typing (e.g. state usage : X { ... }) — previously dropped entirely, + /// unlike every other usage kind's BuildUsageNode handling. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_StateUsageWithExplicitTyping_RecordsTypingEdge() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + state def X; + state usage : X { + first start then y; + state y; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Typing && + e.SourceQualifiedName == "P::usage" && + e.TargetQualifiedName == "P::X"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A transition's implicit initial-pseudostate Source (first start then y;) + /// must resolve start to the real stdlib member every state definition/usage + /// inherits from Actions::Action (action start: Action :>> startShot), + /// via ReferenceResolver.TryResolveInheritedActionMember's narrow fallback — even + /// though the user's own state def X; declares no explicit supertype at all (this + /// codebase implements no general implicit-generalization/default-supertype inference). + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_TransitionSourceStartFeature_ResolvesToStdlibActionMember() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + state def X; + state usage : X { + first start then y; + state y; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + Assert.DoesNotContain(result.Diagnostics, + d => d.Message.Contains("Unresolved reference: 'start'", StringComparison.Ordinal)); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Transition && + e.SourceQualifiedName == "Actions::Action::start" && + e.TargetQualifiedName == "P::usage::y"); + } + finally + { + File.Delete(tempFile); + } + } + /// /// A pathological, self-referential supertype cycle (A :> B :> A) must not /// cause FindMemberInTypeHierarchy's recursive supertype walk to hang or stack From 6e2e5f44cceb264c3002d2b9104efa67b09c8bae Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 12:50:39 -0400 Subject: [PATCH 3/6] Prefer resolved initial transition target in State Transition View layout - CollectStates: exclude pseudostate (start/done) and entry-action sources from ordinary state-box rendering. - Add FindInitialTargetIndex helper to locate the semantically-correct initial state from a pseudostate-sourced transition. - BuildLayout: use the resolved initial target for the initial marker, falling back to the first-declared state when no such transition exists. - Add layout tests covering pseudostate/entry-action source exclusion and semantic initial-marker selection. --- .../StateTransitionViewLayoutStrategy.cs | 67 ++++++- .../StateTransitionViewLayoutStrategyTests.cs | 180 ++++++++++++++++++ 2 files changed, 240 insertions(+), 7 deletions(-) diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/StateTransitionViewLayoutStrategy.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/StateTransitionViewLayoutStrategy.cs index 2e56ebcc..a86576b6 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/StateTransitionViewLayoutStrategy.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/StateTransitionViewLayoutStrategy.cs @@ -66,7 +66,7 @@ public LayoutTree BuildLayout(ViewContext context, RenderOptions options) return new LayoutTree(200.0, 100.0, []); } - var (states, index) = CollectStates(root, theme, scope); + var (states, index, pseudoSourceNames) = CollectStates(root, theme, scope); if (states.Count == 0) { return new LayoutTree(200.0, 100.0, []); @@ -112,8 +112,12 @@ public LayoutTree BuildLayout(ViewContext context, RenderOptions options) nodes.Add(MakeStateBox(states[i], stateRects[i])); } - // Initial pseudo-state entering the first declared state. - AddInitialMarker(stateRects[0], nodes); + // Initial pseudo-state entering the semantically-resolved initial target state when one + // exists (a real `first start then X;`/entry-action-sourced transition), falling back to + // the first-declared state heuristic otherwise (unchanged behavior for every model with no + // pseudostate-sourced transition, e.g. the gallery's 03-elevator-state.sysml). + var initialIndex = FindInitialTargetIndex(root, pseudoSourceNames, index) ?? 0; + AddInitialMarker(stateRects[initialIndex], nodes); // Transition edges (with guard labels), mapped from the algorithm's routed polylines. var crossings = AddTransitions(transitions, flowTransitions, placed.EdgePolylines, stateRects, offsetX, offsetY, nodes); @@ -265,14 +269,23 @@ private static double LabelRightExtent(IReadOnlyList nodes, Theme th /// states synthesized only from transition endpoints have no independent qualified name and are /// always added, since they exist only because a transition endpoint that already named them /// exists on the (already scope-selected) root. + /// + /// Also collects and returns PseudoSourceNames: names that must never become an ordinary + /// state box even when referenced only as a transition's source. This is the well-known, + /// narrowly-scoped set of inherited pseudostate/action feature names ("start"/ + /// "done", consistent with the same simplification ReferenceResolver's + /// TryResolveInheritedActionMember fallback uses) plus every declared entry-keyword + /// feature's name (the entryActionMember (entryTransitionMember)* idiom's implicit + /// transition source) declared directly on . /// - private static (IReadOnlyList States, Dictionary Index) CollectStates( + private static (IReadOnlyList States, Dictionary Index, HashSet PseudoSourceNames) CollectStates( SysmlDefinitionNode root, Theme theme, ExposedScope? scope) { var states = new List(); var index = new Dictionary(StringComparer.Ordinal); + var pseudoSourceNames = new HashSet(StringComparer.Ordinal) { "start", "done" }; void Add(string name) { @@ -287,8 +300,16 @@ void Add(string name) } // Declared state usages first (preserves declaration order for the initial-state choice). + // Entry/do/exit action features (FeatureKeyword != "state") are always skipped as ordinary + // state boxes; a named entry action's name is additionally recorded in + // pseudoSourceNames since it can be an implicit transition source (sub-problem 2's shape). foreach (var feature in root.Children.OfType()) { + if (feature.FeatureKeyword == "entry" && feature.Name is { } entryName) + { + pseudoSourceNames.Add(entryName); + } + if (feature.FeatureKeyword != "state" || feature.Name is null) { continue; @@ -303,10 +324,14 @@ void Add(string name) Add(feature.Name); } - // Any additional states referenced only by transition endpoints. + // Any additional states referenced only by transition endpoints. The source side is + // skipped when it names a pseudostate/entry-action feature — such a name must never be + // drawn as an ordinary state box (it is rendered, if at all, only via the initial-marker + // arrow computed by FindInitialTargetIndex). The target side is unaffected: a real state + // reached only via such a transition is still a real state and must still get a box. foreach (var transition in root.Children.OfType()) { - if (LastSegment(transition.Source) is { } s) + if (LastSegment(transition.Source) is { } s && !pseudoSourceNames.Contains(s)) { Add(s); } @@ -317,7 +342,35 @@ void Add(string name) } } - return (states, index); + return (states, index, pseudoSourceNames); + } + + /// + /// Finds the state index that the semantically-resolved initial transition targets — the first + /// transition (in declaration order) whose source names a pseudostate/entry-action feature (see + /// 's pseudoSourceNames) — or when no + /// such transition exists (or its target does not resolve to a known state), in which case the + /// caller falls back to the pre-existing first-declared-state heuristic. + /// + private static int? FindInitialTargetIndex( + SysmlDefinitionNode root, + HashSet pseudoSourceNames, + Dictionary index) + { + foreach (var transition in root.Children.OfType()) + { + if (LastSegment(transition.Source) is not { } source || !pseudoSourceNames.Contains(source)) + { + continue; + } + + if (LastSegment(transition.Target) is { } target && index.TryGetValue(target, out var targetIndex)) + { + return targetIndex; + } + } + + return null; } /// Resolves transition endpoints to state indices via their last name segment. diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs index 312ca430..3b74a3be 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/StateTransitionViewLayoutStrategyTests.cs @@ -520,4 +520,184 @@ public void StateTransitionView_BuildLayout_ExposedUsage_ResolvesThroughTypingTo Assert.Contains(boxes, b => b.Label == "b1"); Assert.Contains(boxes, b => b.Label == "b2"); } + + /// + /// A pseudostate-sourced initial transition (first start then X;) makes the initial + /// marker's arrow land on the semantically-resolved target, not the first-declared state — + /// confirmed here with declaration order deliberately chosen so the two disagree. + /// + [Fact] + public void StateTransitionView_BuildLayout_PseudostateSourceTransition_MarksResolvedTargetNotFirstDeclared() + { + // Arrange: "b" is declared first, but "first start then b" is not present — instead + // "start" (an inherited pseudostate feature, never declared as its own state box) targets + // "b" while "a" is declared first. This confirms the marker follows the resolved target + // ("b") rather than the first-declared state ("a"). + var strategy = new StateTransitionViewLayoutStrategy(); + var machine = new SysmlDefinitionNode + { + Name = "M", + QualifiedName = "P::M", + DefinitionKeyword = "state def", + Children = + [ + new SysmlTransitionNode { Source = "start", Target = "b" }, + new SysmlFeatureNode { Name = "a", QualifiedName = "P::M::a", FeatureKeyword = "state" }, + new SysmlFeatureNode { Name = "b", QualifiedName = "P::M::b", FeatureKeyword = "state" }, + new SysmlTransitionNode { Source = "a", Target = "b", Guard = null } + ] + }; + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary { ["P::M"] = machine } + }; + var context = new ViewContext("StateTransition", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: the initial marker's arrow terminates at "b"'s top edge (Y), not "a"'s — using Y + // rather than X since a linear a->b chain may share a column (same X) under the layered + // placement algorithm, but "a" (flow source) and "b" (flow target) never share the same Y. + var badge = Assert.Single(layout.Nodes.OfType()); + var bBox = layout.Nodes.OfType().Single(b => b.Keyword == "state" && b.Label == "b"); + var aBox = layout.Nodes.OfType().Single(b => b.Keyword == "state" && b.Label == "a"); + var markerArrow = layout.Nodes.OfType() + .Single(l => l.TargetEnd == EndMarkerStyle.FilledArrow && l.Waypoints.Count == 2 && + Math.Abs(l.Waypoints[0].X - badge.CentreX) < 0.01); + var arrowEndY = markerArrow.Waypoints[^1].Y; + Assert.Equal(bBox.Y, arrowEndY, precision: 3); + Assert.NotEqual(aBox.Y, arrowEndY, precision: 3); + } + + /// + /// No state box is ever labelled "start" — a pseudostate-sourced transition's source + /// must be excluded from ordinary state-box rendering (it would otherwise be synthesized as + /// an "additional state referenced only by a transition endpoint"). + /// + [Fact] + public void StateTransitionView_BuildLayout_PseudostateSourceTransition_NoSpuriousBox() + { + // Arrange + var strategy = new StateTransitionViewLayoutStrategy(); + var machine = new SysmlDefinitionNode + { + Name = "M", + QualifiedName = "P::M", + DefinitionKeyword = "state def", + Children = + [ + new SysmlTransitionNode { Source = "start", Target = "off" }, + new SysmlFeatureNode { Name = "off", QualifiedName = "P::M::off", FeatureKeyword = "state" } + ] + }; + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary { ["P::M"] = machine } + }; + var context = new ViewContext("StateTransition", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: exactly one state box ("off"), never a "start" box. + var stateBoxes = layout.Nodes.OfType().Where(b => b.Keyword == "state").ToList(); + Assert.Single(stateBoxes); + Assert.DoesNotContain(stateBoxes, b => b.Label == "start"); + } + + /// + /// The entryActionMember (entryTransitionMember)* shape's implicit source (a named + /// entry-action feature, e.g. entry action initial; then off;) is likewise excluded + /// from ordinary state-box rendering, and the initial marker still resolves to the correct + /// target via the entry action's name. + /// + [Fact] + public void StateTransitionView_BuildLayout_EntryActionSourceTransition_NoSpuriousBoxUsesResolvedTarget() + { + // Arrange + var strategy = new StateTransitionViewLayoutStrategy(); + var machine = new SysmlDefinitionNode + { + Name = "M", + QualifiedName = "P::M", + DefinitionKeyword = "state def", + Children = + [ + new SysmlFeatureNode { Name = "initial", QualifiedName = "P::M::initial", FeatureKeyword = "entry" }, + new SysmlFeatureNode { Name = "a", QualifiedName = "P::M::a", FeatureKeyword = "state" }, + new SysmlFeatureNode { Name = "off", QualifiedName = "P::M::off", FeatureKeyword = "state" }, + new SysmlTransitionNode { Source = "initial", Target = "off" } + ] + }; + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary { ["P::M"] = machine } + }; + var context = new ViewContext("StateTransition", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: two state boxes ("a", "off"), no "initial" box, and the initial marker arrow + // lands on "off" (the entry action's declared target) rather than "a" (first declared). + var stateBoxes = layout.Nodes.OfType().Where(b => b.Keyword == "state").ToList(); + Assert.Equal(2, stateBoxes.Count); + Assert.DoesNotContain(stateBoxes, b => b.Label == "initial"); + + var badge = Assert.Single(layout.Nodes.OfType()); + var offBox = stateBoxes.Single(b => b.Label == "off"); + var markerArrow = layout.Nodes.OfType() + .Single(l => l.TargetEnd == EndMarkerStyle.FilledArrow && l.Waypoints.Count == 2 && + Math.Abs(l.Waypoints[0].X - badge.CentreX) < 0.01); + var arrowEndX = markerArrow.Waypoints[^1].X; + Assert.InRange(arrowEndX, offBox.X, offBox.X + offBox.Width); + } + + /// + /// Regression guard: when no pseudostate-sourced transition exists, the initial marker's + /// arrow still lands on the first-declared state — the pre-existing heuristic — exactly as + /// before this fix (protects every pre-existing passing test in this file, and the gallery's + /// 03-elevator-state.sysml, which uses only guarded transition first idle if ... + /// then ...; chains with no pseudostate source at all). + /// + [Fact] + public void StateTransitionView_BuildLayout_NoExplicitInitialTransition_FallsBackToFirstDeclared() + { + // Arrange: ordinary declared-state-to-state transitions only, no "start"/entry source. + var strategy = new StateTransitionViewLayoutStrategy(); + var machine = new SysmlDefinitionNode + { + Name = "M", + QualifiedName = "P::M", + DefinitionKeyword = "state def", + Children = + [ + new SysmlFeatureNode { Name = "a", QualifiedName = "P::M::a", FeatureKeyword = "state" }, + new SysmlFeatureNode { Name = "b", QualifiedName = "P::M::b", FeatureKeyword = "state" }, + new SysmlTransitionNode { Source = "a", Target = "b", Guard = "t" } + ] + }; + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary { ["P::M"] = machine } + }; + var context = new ViewContext("StateTransition", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: the initial marker's arrow lands on "a" (the first-declared state). + var badge = Assert.Single(layout.Nodes.OfType()); + var aBox = layout.Nodes.OfType().Single(b => b.Keyword == "state" && b.Label == "a"); + var markerArrow = layout.Nodes.OfType() + .Single(l => l.TargetEnd == EndMarkerStyle.FilledArrow && l.Waypoints.Count == 2 && + Math.Abs(l.Waypoints[0].X - badge.CentreX) < 0.01); + var arrowEndX = markerArrow.Waypoints[^1].X; + Assert.InRange(arrowEndX, aBox.X, aBox.X + aBox.Width); + } } From c0ccee1414eeef7c670f502500b9682a65c0da3a Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 12:59:34 -0400 Subject: [PATCH 4/6] Document attached-transition, entry/exit action, and pseudostate support - Add SysML2Tools-Language-Semantic-Model-AstBuilder-AttachedTransitionStateBody and -EntryDoExitActionFeatures requirements with test traces. - Add SysML2Tools-Language-Semantic-Model-ReferenceResolver-InheritedTransitionSource requirement with test traces. - Add SysML2Tools-Core-Layout-Internal-StateTransitionViewLayoutStrategy-PseudostateExclusion and -SemanticInitialMarker requirements with test traces. - Update AstBuilder, ReferenceResolver, and StateTransitionViewLayoutStrategy design docs' Key Methods/Data Model sections to describe the new behavior. - Add Test Scenarios entries to the corresponding verification docs. --- .../state-transition-view-layout-strategy.md | 54 ++++++++++++---- .../semantic/model/ast-builder.md | 62 +++++++++++++++++++ .../semantic/model/reference-resolver.md | 58 +++++++++++++++-- ...state-transition-view-layout-strategy.yaml | 35 +++++++++++ .../semantic/model/ast-builder.yaml | 42 +++++++++++++ .../semantic/model/reference-resolver.yaml | 27 ++++++++ .../state-transition-view-layout-strategy.md | 12 ++++ .../semantic/model/ast-builder.md | 27 ++++++++ .../semantic/model/reference-resolver.md | 9 +++ 9 files changed, 311 insertions(+), 15 deletions(-) diff --git a/docs/design/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.md b/docs/design/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.md index 91d2e715..3eddcf10 100644 --- a/docs/design/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.md +++ b/docs/design/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.md @@ -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 @@ -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)` @@ -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 @@ -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 diff --git a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md index 792ef68c..64e6fa4d 100644 --- a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md @@ -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: @@ -242,6 +246,64 @@ placeholder form's feature typing (`verify requirement : ;`, 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` diff --git a/docs/design/sysml2-tools-language/semantic/model/reference-resolver.md b/docs/design/sysml2-tools-language/semantic/model/reference-resolver.md index c38f6b7c..05ebd67a 100644 --- a/docs/design/sysml2-tools-language/semantic/model/reference-resolver.md +++ b/docs/design/sysml2-tools-language/semantic/model/reference-resolver.md @@ -82,6 +82,14 @@ resolve — mirroring the existing Satisfy/Allocate both-sides-must-resolve cont appended to `node.ResolvedEdges` (pass-1 edges, if any, are preserved) and to the aggregate edge list. +For a `SysmlTransitionNode`, the `Source` side specifically is resolved via `ResolveTransitionSource` +rather than the plain `TryResolveFeatureChain` call used for `Target`: it tries +`TryResolveFeatureChain` first, and only when that fails, falls back to +`TryResolveInheritedActionMember` (see "Inherited Pseudostate Feature Resolution" below) before +the pair is given up on as unresolved. The `Target` side is never given this fallback — it is +scoped to `Source` only, matching the ROADMAP's own framing of this gap as specifically about an +implicit/inherited transition *source*. + `TryResolveFeatureChain(chain, namespaceStack, imports, out resolvedName)` splits the raw reference text on `.`. Segment 0 is resolved via the existing `TryResolve` four-step lookup (so it participates in the same scope/import resolution as any other single-name reference); a @@ -133,9 +141,50 @@ walk always terminates. no crash). - **Imported/wildcard-imported feature members are not merged into type member lookup** — only `Children` and `Supertype`-chain `Children` are searched. -- **An implied/omitted `SysmlTransitionNode.Source` produces no edge** — there is nothing to walk - a chain from, so no partial/misleading edge is emitted; this is a documented limitation, not a - defect. +- **An implied/omitted `SysmlTransitionNode.Source` produces no edge, except for the narrow + `start`/`done` inherited-pseudostate fallback described below** — there is nothing to walk a + chain from in the general case, so no partial/misleading edge is emitted; this remains a + documented limitation for every other implicit-source shape. + +##### Inherited Pseudostate Feature Resolution + +A transition's source commonly names a pseudostate feature (`start`/`done`) that the enclosing +state/action **inherits** from its supertype rather than declares itself — e.g. +`first start then off;` inside a `state usage : VehicleStates { ... }` with no local `start` +feature declared, or the synthesized `Source` of an attached transition following an +`entryActionMember`. `TryResolveFeatureChain` cannot see inherited members (it only resolves via +namespace/import scope), so without a dedicated fallback every such transition produced a false +"Unresolved reference" diagnostic and no `Transition` edge, even though the model is semantically +valid per the SysML v2 spec. + +`TryResolveInheritedActionMember(namespaceStack, name, out resolvedName)` is tried only after +`TryResolveFeatureChain` fails, and only for a `SysmlTransitionNode`'s `Source` (never `Target`): + +1. Looks up the enclosing node via `_symbolTable.Lookup(string.Join("::", namespaceStack))` — + the same joined-namespace-stack convention `TryResolveBareRedefinition` uses, since at the + point a `SysmlTransitionNode` is visited, `namespaceStack` reflects the enclosing state/usage + (transitions have no `Name` of their own to push). +2. If the enclosing node has a resolved `Typing` edge, follows it to the type node and searches + via `FindMemberInTypeHierarchy` (the same supertype-chain walk `TryResolveFeatureChain`'s + type-fallback branch uses). +3. Also tries `FindMemberInAncestorChain` directly on the enclosing node itself (covering the + `Supertype`/`Redefinition`-edge walk `TryResolveBareRedefinition` already performs elsewhere). +4. **Narrow, hardcoded last resort:** only when `name` is exactly `"start"` or `"done"`, and only + when the enclosing node has a state/action keyword (a `SysmlFeatureNode` with + `FeatureKeyword` `"state"`/`"action"`, or a `SysmlDefinitionNode` with `DefinitionKeyword` + `"state def"`/`"action def"`), looks up the standard library's `Actions::Action` type node's + direct children by name. This covers the common case of a `state def`/`state usage` with no + explicit `:>` supertype clause at all — whose implicit ultimate supertype, per the SysML v2 + standard library, is `Actions::Action` — since this codebase has no general + implicit-generalization/default-supertype inference mechanism (confirmed absent by inspection) + to derive that relationship automatically. + +This fallback is **intentionally narrow and not a general inherited-member resolver**: it is +scoped to Transition `Source` resolution only (step 4 additionally scoped to the two +well-known pseudostate names), consistent with how the ROADMAP itself frames this gap. A future +model using a different, custom state-machine base type that also needs `start`/`done`-like +inherited pseudostate features would not resolve via step 4 (only via steps 1–3, if it declares +an explicit supertype chain) — this is a known, accepted limitation, not a defect to fix here. ##### Requirement-Trace Edge Resolution @@ -248,7 +297,8 @@ unresolved names are present. - `SymbolTable` — `Contains` method used to check whether a supertype, typing, redefinition, or import name is registered; `Lookup` used by feature-chain resolution and `TryResolveBareRedefinition`/`FindMemberInAncestorChain` to walk from a resolved qualified name - back to its node. + back to its node; also used by `TryResolveInheritedActionMember` to look up the enclosing node + and (in its narrow last-resort branch) the stdlib `Actions::Action` type node. - `SysmlNode` hierarchy — traversed to collect `SupertypeNames`, `ImportedNames`, and `VerifiedRequirementNames`; checks for `SysmlFeatureNode.FeatureTyping` and `SysmlFeatureNode.RedefinedFeatureName`, `SysmlSatisfyNode` diff --git a/docs/reqstream/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.yaml b/docs/reqstream/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.yaml index ca0d6502..8d876c16 100644 --- a/docs/reqstream/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.yaml +++ b/docs/reqstream/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.yaml @@ -143,3 +143,38 @@ sections: defeating the intended scoping. tests: - StateTransitionView_BuildLayout_ExposeInnerStateOfNestedDefinition_SelectsNestedDefinitionNotAncestor + + - id: SysML2Tools-Core-Layout-Internal-StateTransitionViewLayoutStrategy-PseudostateExclusion + title: >- + StateTransitionViewLayoutStrategy's CollectStates shall never render an ordinary state + box for a transition source named "start"/"done" or matching a declared entry-action + feature's name, even when that source is otherwise only referenced by a transition + endpoint. + justification: | + A pseudostate ("start"/"done") or entry-action-kind transition source is not itself a + state and must never be drawn as one; without this exclusion, the existing + "additional states referenced only by a transition endpoint" pass would synthesize a + spurious box labelled "start" (or the entry action's name) for every model using the + attached-transition or entry-action-transition idiom, since that source name is never + separately declared as a state. Only the transition source side is excluded — the + target side of any transition is unaffected. + tests: + - StateTransitionView_BuildLayout_PseudostateSourceTransition_NoSpuriousBox + - StateTransitionView_BuildLayout_EntryActionSourceTransition_NoSpuriousBoxUsesResolvedTarget + + - id: SysML2Tools-Core-Layout-Internal-StateTransitionViewLayoutStrategy-SemanticInitialMarker + title: >- + StateTransitionViewLayoutStrategy's BuildLayout shall place the initial pseudo-state + marker at the target of the first transition whose source is a pseudostate/entry-action + name, falling back to the first-declared state only when no such transition exists. + justification: | + The first-declared-state heuristic used previously is only an approximation of "where + the machine starts" — it is wrong whenever a model contains an explicit + `first start then X;` (or attached entry-action-transition) idiom naming a different + state as the true initial target. Preferring the semantically-resolved target keeps the + marker correct for every model using this idiom, while the fallback preserves the + existing behavior (and the `03-elevator-state.sysml` gallery example's original + rendering) for every model that declares no explicit initial transition. + tests: + - StateTransitionView_BuildLayout_PseudostateSourceTransition_MarksResolvedTargetNotFirstDeclared + - StateTransitionView_BuildLayout_NoExplicitInitialTransition_FallsBackToFirstDeclared diff --git a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml index fe528564..3b8865f8 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml @@ -230,3 +230,45 @@ sections: - WorkspaceLoader_LoadAsync_BindingDottedChain_RecordsBindingEdge - WorkspaceLoader_LoadAsync_BindingUnresolvedEnd_ProducesWarningNoEdge - Binding_OmgCorpusFixture_ResolvesBindingEdgesViaImplicitRedefinitionNames + + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-AttachedTransitionStateBody + title: >- + AstBuilder shall synthesize a SysmlTransitionNode for each attached-transition + state body shape — a `behaviorUsageMember` followed by zero or more + `targetTransitionUsageMember`s, and an `entryActionMember` followed by zero or more + `entryTransitionMember`s — sourced from the preceding usage's name, in addition to + building the preceding usage itself. + justification: | + SysML v2's grammar attaches a `then`/`accept ... then`/guarded-succession + transition directly onto the immediately preceding state or entry-action usage + within the same `stateBodyItem` alternative, rather than as a standalone + `transitionUsageMember`. Previously, AstBuilder only visited the leading usage and + silently dropped every attached transition, so the OMG Annex A.7 idiom + `state off; accept Signal then starting;` produced no transition edge at all. A new + `MultiNodeCapture` sentinel (flattened by `CollectChildren`, mirroring the existing + `AnnotationCapture` pattern) lets a single `stateBodyItem` visit yield both the usage + node and one or more synthesized transition nodes. + tests: + - WorkspaceLoader_LoadAsync_AttachedTransitionAfterState_ResolvesSelfLoopEdge + - WorkspaceLoader_LoadAsync_MultipleAttachedTransitionsAfterState_CapturesAll + - WorkspaceLoader_LoadAsync_EntryActionWithAttachedTransition_CapturesEntryFeatureAndTransition + - WorkspaceLoader_LoadAsync_NamedEntryAction_RegistersResolvableFeature + - Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions + + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-EntryDoExitActionFeatures + title: >- + AstBuilder shall build a minimal SysmlFeatureNode (with FeatureKeyword "entry", + "do", or "exit" and no children) for each `entryActionMember`, `doActionMember`, and + `exitActionMember`, naming it only when the grammar's named `ACTION usageDeclaration?` + alternative supplies a declared name. + justification: | + Entry/do/exit action bodies are behavioral (statement sequences), which is out of + scope for this unit's declarative AST; capturing a minimal, non-behavioral feature + node (Children always empty) is sufficient to register a resolvable name in the + symbol table for the named alternative, while the unnamed reference-subsetting + alternative (e.g. `entry performSelfTest{...}`) intentionally yields a nameless + feature node rather than attempting to derive or evaluate a name. + tests: + - WorkspaceLoader_LoadAsync_UnnamedEntryActionReferenceForm_NoNameNoCrash + - WorkspaceLoader_LoadAsync_StateUsageWithExplicitTyping_RecordsTypingEdge + - Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions diff --git a/docs/reqstream/sysml2-tools-language/semantic/model/reference-resolver.yaml b/docs/reqstream/sysml2-tools-language/semantic/model/reference-resolver.yaml index 4897d87a..78cca315 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/reference-resolver.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/reference-resolver.yaml @@ -150,3 +150,30 @@ sections: - WorkspaceLoader_LoadAsync_BindingDottedChain_RecordsBindingEdge - WorkspaceLoader_LoadAsync_BindingUnresolvedEnd_ProducesWarningNoEdge - Binding_OmgCorpusFixture_ResolvesBindingEdgesViaImplicitRedefinitionNames + + - id: SysML2Tools-Language-Semantic-Model-ReferenceResolver-InheritedTransitionSource + title: >- + ReferenceResolver.ResolveFeatureChains shall, when resolving a SysmlTransitionNode's + Source, fall back to a narrowly-scoped inherited-action-member lookup + (TryResolveInheritedActionMember, reusing FindMemberInAncestorChain and + FindMemberInTypeHierarchy) after the standard feature-chain resolution fails, only for + the Source side — never the Target side. + justification: | + A transition's implicit source (e.g. `first start then off;`, or the bare `start` + feature an attached transition's synthesized Source implicitly names) commonly + references a pseudostate feature the enclosing state/action inherits from its + supertype rather than declares itself; the standard namespace/import-scoped lookup + cannot see inherited members, so without this fallback every such transition produced + a false "Unresolved reference" diagnostic and no Transition edge, even though the + model is semantically valid per the SysML v2 spec. As a narrow, deliberately + hardcoded last resort — only for the literal names "start"/"done", and only when the + enclosing node is a state- or action-keyword feature/definition — the fallback also + resolves against the standard library's `Actions::Action` direct children when no + user-declared supertype supplies the member, covering the common case of a `state def` + with no explicit `:>` clause (whose implicit ultimate supertype is `Actions::Action`). + This is intentionally not a general implicit-generalization/default-supertype resolver: + it is scoped to Transition Source resolution only, and to the two well-known + pseudostate-derived names, consistent with how the ROADMAP itself scopes this gap. + tests: + - WorkspaceLoader_LoadAsync_TransitionSourceStartFeature_ResolvesToStdlibActionMember + - Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions diff --git a/docs/verification/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.md b/docs/verification/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.md index 4393890d..124cc338 100644 --- a/docs/verification/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.md +++ b/docs/verification/sysml2-tools-core/layout/internal/state-transition-view-layout-strategy.md @@ -42,6 +42,14 @@ configuration are required beyond a standard .NET SDK installation. - A view whose resolved `Expose` edge names an inner state of a definition genuinely nested inside another eligible root candidate selects the nested definition, not the ancestor, even though the ancestor has more transitions and would win the old pure-score tie-break. +- A transition whose source is a pseudostate/entry-action name (`"start"`, or a declared + entry-action feature's name) is never rendered as its own state box. +- The initial-state marker's arrow lands on the resolved target of a pseudostate- or + entry-action-sourced transition, not the first-declared state, when such a transition exists. +- When no pseudostate/entry-action-sourced transition exists, the initial-state marker's arrow + still lands on the first-declared state, unchanged from the pre-existing heuristic (protecting + the `03-elevator-state.sysml` gallery example and every other model using no explicit initial + transition). ##### Test Scenarios @@ -60,3 +68,7 @@ configuration are required beyond a standard .NET SDK installation. | `StateTransitionView_BuildLayout_ExposeSingleState_DropsIsolatedOutOfScopeState` | Isolated state dropped | | `StateTransitionView_BuildLayout_ExposedUsage_ResolvesThroughTypingToRoot` | Usage resolves via `Typing` to root | | `StateTransitionView_BuildLayout_ExposeInnerStateOfNestedDefinition_SelectsNestedDefinitionNotAncestor` | Nested | +| `StateTransitionView_BuildLayout_PseudostateSourceTransition_MarksResolvedTargetNotFirstDeclared` | Resolved target | +| `StateTransitionView_BuildLayout_PseudostateSourceTransition_NoSpuriousBox` | No `"start"` box | +| `StateTransitionView_BuildLayout_EntryActionSourceTransition_NoSpuriousBoxUsesResolvedTarget` | No entry box | +| `StateTransitionView_BuildLayout_NoExplicitInitialTransition_FallsBackToFirstDeclared` | Fallback unchanged | diff --git a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md index 7e1fa3b1..cd92c637 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md @@ -53,6 +53,25 @@ external services or additional configuration are required beyond a standard .NE `SysmlConnectionNode` with `ConnectionKeyword == "binding"`, resolved via the same dotted-feature-chain walk as `connect`, producing a `Binding` edge when both sides resolve (or a Warning with no edge otherwise). +- `VisitStateBodyItem` synthesizes an attached `SysmlTransitionNode` for the + `behaviorUsageMember (targetTransitionUsageMember)*` shape (e.g. `state off; accept Signal + then starting;`), including the repeated (`*`) case, and for the + `entryActionMember (entryTransitionMember)*` shape (e.g. `entry action initial; then off;`), + in each case in addition to building the preceding usage/entry-action node itself. +- A named entry action (the OMG Annex A.7-preferred separate `entry action initial; ... + transition initial then off;` form) registers a resolvable feature — no "Unresolved reference: + 'initial'" diagnostic is produced. +- An unnamed entry-action reference form (e.g. `entry performSelfTest{...}`) produces a feature + node with `Name == null` and does not crash. +- `VisitStateUsage` populates `FeatureTyping` from an explicit `state usage : Type { ... }` form, + recording the expected `Typing` edge (previously always dropped for state usages). +- A transition source named `"start"` with no locally-declared `start` feature resolves against + the standard library's `Actions::Action` via the new inherited-pseudostate-feature fallback, + producing a `Transition` edge and no unresolved-reference diagnostic. +- The real OMG corpus fixture `training/25.Transitions/TransitionActions.sysml` parses with no + unresolved-reference diagnostics for `start`/`off`/`starting`/`on`, and produces the exact + expected declared-state and resolved-transition counts, including entry/do/exit action feature + nodes on the `on` state. ##### Test Scenarios @@ -85,3 +104,11 @@ external services or additional configuration are required beyond a standard .NE | Binding dotted-chain resolution | `WorkspaceLoader_LoadAsync_BindingDottedChain_RecordsBindingEdge` | | Binding unresolved end | `WorkspaceLoader_LoadAsync_BindingUnresolvedEnd_ProducesWarningNoEdge` | | Binding OMG corpus fixture | `Binding_OmgCorpusFixture_ResolvesBindingEdgesViaImplicitRedefinitionNames` | +| Attached transition (self-loop) | `WorkspaceLoader_LoadAsync_AttachedTransitionAfterState_ResolvesSelfLoopEdge` | +| Multiple attached transitions | `WorkspaceLoader_LoadAsync_MultipleAttachedTransitionsAfterState_CapturesAll` | +| Entry action | `WorkspaceLoader_LoadAsync_EntryActionWithAttachedTransition_CapturesEntryFeatureAndTransition` | +| Named entry action registers feature | `WorkspaceLoader_LoadAsync_NamedEntryAction_RegistersResolvableFeature` | +| Unnamed entry-action form, no crash | `WorkspaceLoader_LoadAsync_UnnamedEntryActionReferenceForm_NoNameNoCrash` | +| State usage explicit typing | `WorkspaceLoader_LoadAsync_StateUsageWithExplicitTyping_RecordsTypingEdge` | +| Transition source resolves | `WorkspaceLoader_LoadAsync_TransitionSourceStartFeature_ResolvesToStdlibActionMember` | +| OMG corpus fixture | `Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions` | diff --git a/docs/verification/sysml2-tools-language/semantic/model/reference-resolver.md b/docs/verification/sysml2-tools-language/semantic/model/reference-resolver.md index 2cdc59b8..f9aa6d62 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/reference-resolver.md +++ b/docs/verification/sysml2-tools-language/semantic/model/reference-resolver.md @@ -53,6 +53,13 @@ external services or additional configuration are required beyond a standard .NE instance-relative (e.g. `Drone::controller::power`, not the shared port type's own declared path), so that two structurally distinct endpoints of the same type never collapse to the same resolved name. +- A `SysmlTransitionNode`'s `Source` that names an inherited pseudostate feature (`"start"` + or `"done"`) not declared locally on the enclosing state/usage resolves via the new + `TryResolveInheritedActionMember` fallback — including the narrow last-resort case where no + explicit supertype is declared at all, resolving against the standard library's + `Actions::Action` direct children — producing a `Transition`-kind `SysmlEdge` and no + unresolved-reference diagnostic. The `Target` side of a transition never receives this + fallback. ##### Test Scenarios @@ -93,3 +100,5 @@ external services or additional configuration are required beyond a standard .NE | Unresolved chain endpoint | `WorkspaceLoader_LoadAsync_ConnectionUnresolvedEndpoint_ProducesWarningNoEdge` | | Supertype-cycle chain segment | `WorkspaceLoader_LoadAsync_ConnectionChain_SupertypeCycleTerminatesGracefully` | | OMG Connections example fixture | `WorkspaceLoader_LoadAsync_ConnectionsExampleFixture_RecordsConnectEdge` | +| Inherited transition source | `WorkspaceLoader_LoadAsync_TransitionSourceStartFeature_ResolvesToStdlibActionMember` | +| OMG Transitions corpus fixture | `Transition_OmgCorpusFixture_ResolvesAllStatesAndTransitions` | From 2ed8dd803caa2d956d1b8c2f9fecd6480ce14b2f Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 13:05:43 -0400 Subject: [PATCH 5/6] Add initial pseudostate transition and attached-transition idiom to gallery - 03-elevator-state.sysml: add 'first start then idle;' as a real initial transition (previously relied only on the first-declared-state heuristic), and convert the idle/doorsClosing pair to the attached-transition idiom ('state idle; accept callReceived then doorsClosing;'). - Regenerate ElevatorStateTransitionView.svg/.png; visually verified per diagram-quality.md's Agent Checklist. - Update the gallery README's descriptive text for this example. --- docs/gallery/README.md | 6 +- docs/gallery/models/03-elevator-state.sysml | 3 +- .../png/ElevatorStateTransitionView.png | Bin 24398 -> 24988 bytes .../svg/ElevatorStateTransitionView.svg | 59 +++++++++--------- 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/docs/gallery/README.md b/docs/gallery/README.md index c825e59a..440d40c4 100644 --- a/docs/gallery/README.md +++ b/docs/gallery/README.md @@ -98,7 +98,11 @@ SVG: [`svg/CoreLinkInterconnectionView.svg`](svg/CoreLinkInterconnectionView.svg ## 3. State Transition View — Elevator Controller Shows states placed top-to-bottom by the layered layout pipeline, an initial pseudo-state, and -guarded transitions routed as orthogonal `[guard]`-labelled arrows. +guarded transitions routed as orthogonal `[guard]`-labelled arrows. The initial marker targets +the state named by an explicit `first start then idle;` transition (rather than the +first-declared state), and the `idle`/`doorsClosing` pair uses the attached-transition idiom +(`state idle; accept callReceived then doorsClosing;`) instead of a standalone `transition` +statement. Model: [`models/03-elevator-state.sysml`](models/03-elevator-state.sysml) · SVG: [`svg/ElevatorStateTransitionView.svg`](svg/ElevatorStateTransitionView.svg) diff --git a/docs/gallery/models/03-elevator-state.sysml b/docs/gallery/models/03-elevator-state.sysml index a626d271..cb75869e 100644 --- a/docs/gallery/models/03-elevator-state.sysml +++ b/docs/gallery/models/03-elevator-state.sysml @@ -3,13 +3,14 @@ package ElevatorController { // An elevator control state machine. state def ElevatorStates { state idle; + accept callReceived then doorsClosing; state doorsOpening; state doorsOpen; state doorsClosing; state movingUp; state movingDown; - transition first idle if callReceived then doorsClosing; + first start then idle; transition first doorsClosing if doorsClosed then movingUp; transition first movingUp if atFloor then doorsOpening; transition first movingDown if atFloor then doorsOpening; diff --git a/docs/gallery/png/ElevatorStateTransitionView.png b/docs/gallery/png/ElevatorStateTransitionView.png index 2c59b7f221f2b43a83492e1a79d5d0cd244532b7..3ce280fdd89a88f0f5d465f8e19ab92efdac6a20 100644 GIT binary patch literal 24988 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 gHU2HF`~iQK{{3H=`YAPmARlo> + @@ -30,40 +30,39 @@ - - «state» - idle - - «state» - doorsOpening - - «state» - doorsOpen - - «state» - doorsClosing + + «state» + idle + + «state» + doorsOpening + + «state» + doorsOpen + + «state» + doorsClosing «state» movingUp «state» movingDown - - - - - - - - - - - [callReceived] - [doorsClosed] - [atFloor] - [atFloor] - [doorsAreOpen] - [timeout] + + + + + + + + + + + [doorsClosed] + [atFloor] + [atFloor] + [doorsAreOpen] + [timeout] [goingDown] - [idleTimeout] + [idleTimeout] From b6ff81d8d38bb16a55ebcce66359caf7c636a3b7 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 12 Jul 2026 13:16:10 -0400 Subject: [PATCH 6/6] Fix cspell unknown-word flag in ReferenceResolver comments --- .../Semantic/Model/ReferenceResolver.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs index 1b2e0af7..8e96c8d9 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs @@ -1086,7 +1086,7 @@ private void ResolveFeatureChains( /// directly via (covers a state def that /// itself declares an explicit :> supertype); (3) only if both fail, and only for /// the two well-known names ROADMAP.md calls out ("start"/"done"), and only - /// when the enclosing node is itself state/action-keyworded, fall back to a hardcoded, + /// when the enclosing node itself has a state/action keyword, fall back to a hardcoded, /// narrowly-scoped last resort: look the name up directly among /// Actions::Action's own direct children in the stdlib. This last-resort branch /// exists because this codebase implements no general implicit-generalization/default- @@ -1137,21 +1137,21 @@ private bool TryResolveInheritedActionMember( } // Last resort: only for the two well-known inherited pseudostate/action members named in - // ROADMAP.md, and only when the enclosing node is itself state/action-keyworded, so this + // ROADMAP.md, and only when the enclosing node itself has a state/action keyword, so this // simplification cannot silently misfire for unrelated names or node kinds. if (name is not ("start" or "done")) { return false; } - var isStateOrActionKeyworded = enclosingNode switch + var isStateOrActionKeyword = enclosingNode switch { SysmlFeatureNode { FeatureKeyword: "state" or "action" } => true, SysmlDefinitionNode { DefinitionKeyword: "state def" or "action def" } => true, _ => false, }; - if (!isStateOrActionKeyworded) + if (!isStateOrActionKeyword) { return false; }