diff --git a/.cspell.yaml b/.cspell.yaml index ac88fcf0..893f2e52 100644 --- a/.cspell.yaml +++ b/.cspell.yaml @@ -52,6 +52,7 @@ words: - setlocal - Skia - Subsetting + - subsettings - Sugiyama - Tagawa - Toda diff --git a/ROADMAP.md b/ROADMAP.md index d57dbe02..88181a35 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,30 +14,12 @@ The work falls into three themes: ## Notation & view conformance -### Connector-end & line-style conformance - -Bring routed connectors into line with the SysML v2 notation — the highest-value, broadest-impact -change. - -- Wire the already-defined end markers to relationships: **filled/hollow diamonds** for - composite/reference membership; **redefinition** crossbar variant. -- Switch succession/transition/message end markers to the spec style (**open V**, stroke-only) - and make **successions dashed** — pending confirmation against the training PDF; keep a theme - switch if the training material differs from `clause-8.2.3`. -- Add `LineStyle.Dashed` usage where required; ensure end markers render correctly at clearance - stubs. - -**Scope:** `LayoutLine` end-marker/line-style assignments in the view strategies; renderer -marker defs (already present). No new engines. -**Visual gate:** state/action/sequence/general galleries match the spec end shapes; membership -diamonds appear where membership is shown. - ### Additional relationship edges (General View) Render the relationships currently omitted from the General View, each routed via `ChannelRouter` and carrying the correct spec end shape: -- Redefinition, subsetting (where shown as edges), feature typing, dependency, containment, +- Subsetting (where shown as edges), feature typing, dependency, containment, connection/binding, allocation. - Shared-bus generalization (multiple subtypes merging into one line to a supertype) as an optional readability refinement. diff --git a/docs/design/introduction.md b/docs/design/introduction.md index 23ead676..7a559ea2 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -51,8 +51,8 @@ system, subsystem, and unit levels: - **SysmlNode** (Unit) — public AST node hierarchy: nine types with JSON polymorphism - **AstBuilder** (Unit) — builds AST from ANTLR4 CST with qualified names and supertype lists - **SymbolTable** (Unit) — registry mapping qualified names to declaration nodes - - **ReferenceResolver** (Unit) — resolves supertype, typing, import, satisfy, verify, - allocate, and (in a second pass) dotted feature-chain connect/transition references; + - **ReferenceResolver** (Unit) — resolves supertype, typing, redefinition, import, satisfy, + verify, allocate, and (in a second pass) dotted feature-chain connect/transition references; detects circular imports; returns a `SemanticIndex` of resolved edges - **SupertypeWalker** (Unit) — walks specialization chains; detects cyclic specialization - **SysmlEdge** (Unit) — public resolved-reference record (Supertype/Typing/Import/ @@ -77,7 +77,8 @@ system, subsystem, and unit levels: `DemaConsulting.Rendering.Layout` layered algorithm - **Internal** (Subsystem) — per-view layout strategies - **GeneralViewLayoutStrategy** (Unit) — general view: package-grouped definitions placed by - the layered algorithm with orthogonal specialization and membership edges + the layered algorithm with orthogonal specialization, membership, attribute-typing, and + redefinition edges - **InterconnectionViewLayoutStrategy** (Unit) — internal structure: nested parts, ports, connectors - **StateTransitionViewLayoutStrategy** (Unit) — state machine: states and guarded transitions placed top-to-bottom by the layered algorithm (DOWN direction) with orthogonal transitions diff --git a/docs/design/sysml2-tools-core/layout/internal.md b/docs/design/sysml2-tools-core/layout/internal.md index 7b11729d..b6cb2c29 100644 --- a/docs/design/sysml2-tools-core/layout/internal.md +++ b/docs/design/sysml2-tools-core/layout/internal.md @@ -12,7 +12,7 @@ The subsystem contains one strategy per supported view type: | Unit | Responsibility | | --- | --- | -| `GeneralViewLayoutStrategy` | Lays out user definitions grouped by package with specialization edges | +| `GeneralViewLayoutStrategy` | Lays out definitions by package with specialization, membership, typing, redefinition | | `InterconnectionViewLayoutStrategy` | Lays out the internal parts, ports, and connections of one part definition | | `StateTransitionViewLayoutStrategy` | Lays out states, an initial marker, and guarded transitions | | `ActionFlowViewLayoutStrategy` | Lays out actions top-to-bottom with start/done markers and successions | diff --git a/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md b/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md index 96b1146c..86a7df9f 100644 --- a/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md +++ b/docs/design/sysml2-tools-core/layout/internal/general-view-layout-strategy.md @@ -6,7 +6,7 @@ renders every user-defined definition (part, port, interface, requirement, action, and so on) as a keyword-labeled box, groups the boxes that belong to a package inside a folder-shaped container, lists each definition's owned usages in compartments, and draws specialization, -membership, and attribute-typing edges orthogonally between the boxes. The whole diagram — package +membership, attribute-typing, and redefinition edges orthogonally between the boxes. The whole diagram — package folders and definitions alike — is expressed as a single `DemaConsulting.Rendering` `LayoutGraph` and placed with one `HierarchicalLayoutAlgorithm.Apply` call: the root scope packs package folders and top-level definitions by reading order (`ContainmentLayoutAlgorithm`), while each folder's own @@ -21,14 +21,19 @@ box title and folder-tab geometry come from `BoxMetrics` in `DemaConsulting.Rend parameters. Layout constants (`MinBoxWidth`, `CharWidthFactor`) are declared as `private const` fields. Private records carry intermediate data: `DefBox` (a user definition with its computed size, keyword, supertype names, memberships, and compartments), `ModelEdge` (a resolved -specialization/membership/attribute-typing relationship expressed by qualified name, together with -its target end marker and edge kind), `Location` (a located definition's graph node and owning -package, used to resolve and scope edges), and `TruncatedFolder` (a depth-truncated package folder's -leaf graph node and hidden-definition count, used to stamp its ellipsis label onto the placed box -after layout). The private `EdgeKind` enumeration classifies each edge as `Specialization`, -`Membership`, or `Typing`; the `LineStyleForKind` helper maps this kind to a rendered line style -(dashed for `Typing`, solid for the others), so an attribute-typing dependency is visually distinct -from the structural relationships. +specialization/membership/attribute-typing/redefinition relationship expressed by qualified +name, together with its target end marker and edge kind), `Location` (a located definition's +graph node and owning package, used to resolve and scope edges), and `TruncatedFolder` (a +depth-truncated package folder's leaf graph node and hidden-definition count, used to stamp its +ellipsis label onto the placed box after layout). `FeatureMembership` (a private record) carries +each owned feature's keyword, raw type reference (`TypeName`, nullable — a feature may declare a +redefinition with no explicit type annotation), simple `Name`, and raw +`RedefinedFeatureName` reference; `CollectMemberships` includes a feature when either `TypeName` +or `RedefinedFeatureName` is present. The private `EdgeKind` enumeration classifies each edge as +`Specialization`, `Membership`, `Typing`, or `Redefinition`; the `LineStyleForKind` helper maps +this kind to a rendered line style (dashed for `Typing`, solid for the others — including +`Redefinition`), so an attribute-typing dependency is visually distinct from the structural +relationships. ##### Key Methods @@ -39,8 +44,8 @@ Entry point. First resolves the view's exposed-name scope via the shared `CollectDefinitions` to gather user definitions restricted to that scope (or every definition when no scope applies); returns a minimal 200×100 empty `LayoutTree` when none are found. Otherwise groups the definitions by package with `GroupByPackage`, resolves the specialization/membership/ -attribute-typing relationships into qualified-name edges with `BuildModelEdges`, builds the single -input `LayoutGraph` with `BuildGraph`, and places the whole graph with one +attribute-typing/redefinition relationships into qualified-name edges with `BuildModelEdges`, +builds the single input `LayoutGraph` with `BuildGraph`, and places the whole graph with one `HierarchicalLayoutAlgorithm().Apply(graph, LayoutOptions.ForAlgorithm("containment"))` call — passing the desired root-scope leaf algorithm through the options parameter (not `graph.Set(CoreOptions.Algorithm, …)`) so a caller going through `LayoutEngine.Layout(graph)` later @@ -67,8 +72,9 @@ Top-level definitions (no package prefix) become plain leaves directly on the ro ###### `BuildModelEdges(defs)` -Resolves every specialization (subtype → supertype), structural membership (member-type → owner), and -attribute-typing (owner → attribute-type) relationship — across every definition, regardless of +Resolves every specialization (subtype → supertype), structural membership (member-type → owner), +attribute-typing (owner → attribute-type), and redefinition (subtype → the owning definition of +the redefined feature) relationship — across every definition, regardless of package — into a flat list of qualified-name `ModelEdge`s. Specialization edges carry an open triangular end marker at the supertype; `part`/`port` memberships carry a filled diamond and `ref` memberships a hollow diamond at the owner; other memberships are not drawn. In addition, each @@ -78,7 +84,19 @@ chevron at the type end and rendered as a dashed line. Attribute typing is a usa not composition, so it uses the OMG dependency notation (dashed line with an open arrowhead) rather than a membership diamond, and it connects otherwise-disconnected attribute and enumeration definitions into the cluster near the definitions that reference them. Unresolved types and -self-references are skipped. Whether an edge's endpoints actually receive a graph node — i.e., +self-references are skipped. Finally, each feature with a non-null `RedefinedFeatureName` +contributes a **redefinition** edge from the subtype to the owning definition of the redefined +feature, carrying a hollow-triangle-crossbar end marker at the owner and rendered as a solid line +via `ResolveRedefinitionOwner`: a qualified reference (containing `::`) strips the text before the +*last* `::` segment and resolves it directly via `TryResolveQualified`; a bare-name reference +instead walks the redefining definition's own `SupertypeNames` (resolved the same way), checking +each resolved supertype's own `Memberships` for a matching simple `Name`, and recurses +transitively up the chain (guarded by a `HashSet` of visited qualified names to prevent an +infinite loop on a malformed cyclic supertype graph) when the immediate supertype does not declare +it. Neither resolving nor a self-referential result (`owner == def.QualifiedName`) produces an +edge or a diagnostic — consistent with the existing `TryResolveQualified`-failure-is-silent +convention used by the other three edge kinds in this method. Whether an edge's endpoints actually +receive a graph node — i.e., were not depth-truncated — is decided later, in `BuildGraph`. ###### `BuildGraph(groups, modelEdges, theme, depthLimit)` @@ -142,7 +160,8 @@ produces valid geometry, so no crossing warnings are emitted. "parsed but not yet evaluated" filter-expression warning text. - The `LayoutTree`, `LayoutBox`, `LayoutCompartment`, `LayoutLine`, `LayoutLabel`, and `Point2D` data types (`DemaConsulting.Rendering`). -- `FeatureMembership` (private record) — carries the keyword and type reference of one owned feature. +- `FeatureMembership` (private record) — carries the keyword, nullable type reference, simple + name, and nullable redefined-feature reference of one owned feature. ##### Callers diff --git a/docs/design/sysml2-tools-language/semantic.md b/docs/design/sysml2-tools-language/semantic.md index 3b60944c..177c3747 100644 --- a/docs/design/sysml2-tools-language/semantic.md +++ b/docs/design/sysml2-tools-language/semantic.md @@ -58,8 +58,8 @@ optionally seeded with a pre-populated symbol table. - *Contract*: Exposes `IReadOnlyList Files`, `IReadOnlySet StdlibNames`, `IReadOnlyDictionary Declarations` mapping qualified names to declaration nodes, and `SemanticIndex Index` — a reverse-lookup index over all resolved edge kinds - (supertype, typing, import, satisfy, verify, allocate, connect, transition, and expose; see - Semantic Model Subsystem). + (supertype, typing, redefinition, import, satisfy, verify, allocate, connect, transition, and + expose; see Semantic Model Subsystem). ### Design diff --git a/docs/design/sysml2-tools-language/semantic/model.md b/docs/design/sysml2-tools-language/semantic/model.md index 3048f3b8..957ed580 100644 --- a/docs/design/sysml2-tools-language/semantic/model.md +++ b/docs/design/sysml2-tools-language/semantic/model.md @@ -57,7 +57,7 @@ over resolved edges. | --- | --- | | `AstBuilder` | Visits ANTLR4 CST; builds typed AST nodes with qualified names and supertype lists | | `SymbolTable` | Registry mapping fully-qualified names to their AST nodes | -| `ReferenceResolver` | Resolves supertype/typing/import/satisfy/verify/allocate/connect/transition/expose refs | +| `ReferenceResolver` | Resolves supertype/typing/redefinition/import/satisfy/verify/allocate/connect/transition | | `SupertypeWalker` | Walks specialization chains; detects cyclic specialization | | `SysmlNode` | Public abstract base record (and subtypes) modeling one parsed AST element | | `SysmlEdge` | Public record modeling one resolved reference (Supertype/Typing/Import/Satisfy/Verify/Allocate/etc.) | 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 dae1912d..c1e776ae 100644 --- a/docs/design/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/design/sysml2-tools-language/semantic/model/ast-builder.md @@ -47,6 +47,35 @@ alongside the existing `ImportedNamespace` property — letting `ReferenceResolv references uniformly with `SupertypeNames` and `FeatureTyping` without any node-type special-casing. +`BuildUsageNode` additionally calls `ExtractRedefinedFeature(decl?.featureSpecializationPart())` +alongside `ExtractFeatureTyping`, setting the result on the constructed `SysmlFeatureNode`'s +`RedefinedFeatureName` property. `ExtractRedefinedFeature` mirrors `ExtractFeatureTyping`'s exact +structure: it loops `featureSpecializationPart().featureSpecialization()`, and for each entry +whose `redefinitions()` is non-null, first checks `redefinitions().redefines()?.ownedRedefinition()` +(the first redefined feature, held by the `redefines`/`:>>` clause) and returns its `.GetText()`; +if that clause is absent, it falls back to the first non-null entry of +`redefinitions().ownedRedefinition()` (the `redefines (COMMA ownedRedefinition)*` list's +additional entries). It returns `null` when no `redefinitions()` is present anywhere in the +part — i.e. the feature declares no redefinition. Both the `redefines` keyword form and the +`:>>` operator form parse into the same `RedefinesContext` (they differ only in which terminal — +`REDEFINES` or `COLON_GT_GT` — the grammar matched), so `ExtractRedefinedFeature` handles both +forms identically without needing to branch on which token was used. The raw reference text is +captured verbatim — including qualified `Owner::feature` forms — with no resolution attempted; +resolution happens later, in `ReferenceResolver`. + +`BuildUsageNode` also calls `ExtractSubsettingTargetNames(decl?.featureSpecializationPart())`, +setting the result on the constructed `SysmlFeatureNode`'s inherited `SupertypeNames` property — +mirroring `ExtractRedefinedFeature`'s structure (first checking +`subsettings().subsets()?.ownedSubsetting()`, the target held by the `subsets`/`:>` clause, then +falling back to `subsettings().ownedSubsetting()`'s remaining comma-separated entries) but +collecting *every* match into a list rather than returning only the first. Before this, a +usage-level `subsets`/`:>` clause (as opposed to a definition-level `:>` specialization, already +handled by `GetSubclassificationSupertypes`) was never extracted at all, so it produced no +`Supertype` edge and was invisible to `ReferenceResolver` — silently breaking its bare-name +redefinition ancestor-chain walk whenever the redefining feature's owner was itself a +usage-level subsetting rather than a `part def` specialization (the exact shape used by the OMG +corpus fixture `1c-PartsTreeRedefinition.sysml`'s `part vehicle1_c1 :> vehicle1 { ... }`). + `VisitAnnotatingElement(AnnotatingElementContext)` intercepts the `comment` and `documentation` grammar alternatives of `annotatingElement` (`comment | documentation | textualRepresentation | metadataFeature`) and returns a private `AnnotationCapture` sentinel node wrapping a 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 d0c080cd..5c47703f 100644 --- a/docs/design/sysml2-tools-language/semantic/model/reference-resolver.md +++ b/docs/design/sysml2-tools-language/semantic/model/reference-resolver.md @@ -7,8 +7,9 @@ 1. **Import graph cycle detection** — builds a directed graph of import relationships between files and uses depth-first search to detect cycles. 2. **Reference resolution (pass 1)** — checks each `SupertypeName`, `SysmlFeatureNode.FeatureTyping`, - `ImportedName`, `VerifiedRequirementNames` entry, `SysmlSatisfyNode` subject/requirement, and - `SysmlConnectionNode` (`ConnectionKeyword == "allocation"`) endpoint in all AST nodes against + `SysmlFeatureNode.RedefinedFeatureName`, `ImportedName`, `VerifiedRequirementNames` entry, + `SysmlSatisfyNode` subject/requirement, and `SysmlConnectionNode` + (`ConnectionKeyword == "allocation"`) endpoint in all AST nodes against the symbol table, emitting a Warning for any name not found and recording a `SysmlEdge` for any name (or pair of names) that resolves. 3. **Feature-chain resolution (pass 2)** — after pass 1 has completed for every file root, @@ -34,13 +35,19 @@ outputs the exact qualified name that matched (`resolvedName`). Steps 3–4 reso namespace name itself via `ResolveNamespaceName` before building the `"{ns}::{name}"` candidate (see the "Nested-Namespace Import Resolution" deviation below). -`ResolveNode` traverses each AST node's `SupertypeNames`, the node's `FeatureTyping` (when the -node is a `SysmlFeatureNode`), and `ImportedNames` uniformly. For each name that resolves via +`ResolveNode` traverses each AST node's `SupertypeNames`, the node's `FeatureTyping` and +`RedefinedFeatureName` (both only when the node is a `SysmlFeatureNode`), and `ImportedNames` +uniformly. For each name that resolves via `TryResolve`, a `SysmlEdge` is appended to a per-node list, tagged with `SysmlEdgeKind.Supertype`, -`SysmlEdgeKind.Typing`, or `SysmlEdgeKind.Import` respectively; `Source` is the current node's +`SysmlEdgeKind.Typing`, `SysmlEdgeKind.Redefinition`, or `SysmlEdgeKind.Import` respectively; +`Source` is the current node's `QualifiedName` (`null` for anonymous nodes such as import statements) and `Target` is `resolvedName`. Any non-empty per-node edge list is attached to `node.ResolvedEdges` and -appended to the aggregate edge list returned by `ResolveAll`. +appended to the aggregate edge list returned by `ResolveAll`. The `RedefinedFeatureName` block +is a line-for-line mirror of the `FeatureTyping` block immediately preceding it — same +`TryResolve` call, same `resolvedInFile` deduplication, same Warning diagnostic message format — +since both are a single raw reference captured on a `SysmlFeatureNode` with identical resolution +semantics. For each name that does not resolve (and is not already reported in this file), a Warning diagnostic is emitted. The `resolvedInFile` set prevents duplicate warnings for the same name @@ -168,6 +175,33 @@ paths: `TryResolve`'s own Step 2), falling back to the raw name unchanged if nothing matches — so already-qualified or genuinely-unresolvable namespaces behave exactly as before; only previously-failing nested-namespace import cases gain resolution. +- **`TryResolveBareRedefinition`/`FindMemberInAncestorChain` (bare-name inherited-redefinition + fallback).** The `RedefinedFeatureName` block previously called only `TryResolve`, which is + namespace/import-scoped and never walks a supertype chain — but per the SysML v2 spec, a + `redefines`/`:>>` target's entire purpose is referencing a member the redefining feature + *inherits*, not one declared or imported into the same lexical scope. This is the dominant + real-world shape (e.g. `RedefinitionExample.sysml`'s bare `smallEng redefines eng`, where `eng` + is a member of `SmallVehicle`'s supertype `Vehicle`), so the original code produced a false + unresolved-reference Warning and no edge for the standard case. `TryResolveBareRedefinition` + looks up the immediate owner node via `_symbolTable.Lookup` on the joined `namespaceStack` + (the owner's qualified name, since the owner always pushes its own name before its children + are visited), then delegates to `FindMemberInAncestorChain`, which walks the owner's own + direct children and — recursively, cycle-guarded — any ancestor reachable via its + `SysmlEdgeKind.Supertype` **and** `SysmlEdgeKind.Redefinition` resolved edges. Both edge kinds + must be followed, not supertype-only: `1c-PartsTreeRedefinition.sysml`'s nested `frontAxle_c1 + redefines frontAxle` has an owner (`frontAxleAssembly_c1`) with no `SupertypeNames` at all — + the only path to the inherited `frontAxle` member is via the owner's own already-resolved + `Redefinition` edge to `frontAxleAssembly`. This fallback is tried only after `TryResolve` + fails, so it is additive and cannot regress a previously-successful qualified/scoped + resolution. The same fallback is also tried for a `SysmlFeatureNode`'s own `SupertypeNames` + entries (but not a definition's): a usage-level `subsets`/`:>` clause (captured into + `SupertypeNames` by `AstBuilder.ExtractSubsettingTargetNames`) is, like `redefines`, commonly a + bare reference to a member the owner inherits — e.g. `1c-PartsTreeRedefinition.sysml`'s `part + frontWheel_1 subsets frontWheel = frontWheel#(1);`, where `frontWheel` is a member of + `frontAxleAssembly_c1`'s redefined ancestor `frontAxleAssembly`, not of `frontAxleAssembly_c1` + itself. A definition's own `SupertypeNames` (from `part def X :> Y`) name the supertype + directly rather than an inherited member, so the fallback is intentionally skipped for + non-feature nodes. ##### Error Handling @@ -177,16 +211,20 @@ unresolved names are present. ##### Dependencies -- `SymbolTable` — `Contains` method used to check whether a supertype, typing, or import name - is registered; `Lookup` used by feature-chain resolution to walk from a resolved segment's - qualified name back to its node. +- `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. - `SysmlNode` hierarchy — traversed to collect `SupertypeNames`, `ImportedNames`, and - `VerifiedRequirementNames`; checks for `SysmlFeatureNode.FeatureTyping`, `SysmlSatisfyNode` + `VerifiedRequirementNames`; checks for `SysmlFeatureNode.FeatureTyping` and + `SysmlFeatureNode.RedefinedFeatureName`, `SysmlSatisfyNode` (`SubjectName`/`RequirementName`), `SysmlConnectionNode` with `ConnectionKeyword == "allocation"` (`EndpointA`/`EndpointB`), `SysmlConnectionNode` with `ConnectionKeyword == "connection"` or `"message"`, `SysmlTransitionNode` (`Source`/`Target`), and `SysmlViewNode` (`ExposedNames`; `RenderTargetName`/`FilterExpressionText` are never read); reads - `ResolvedEdges` (`Typing`/`Supertype` kinds) during feature-chain resolution. + `ResolvedEdges` (`Typing`/`Supertype` kinds during feature-chain resolution; `Supertype` + **and** `Redefinition` kinds during `FindMemberInAncestorChain`'s bare-redefinition ancestor + walk). - `SysmlEdge`, `SemanticIndex` — resolved references are recorded as `SysmlEdge` instances and aggregated into the returned `SemanticIndex`. - `SysmlDiagnostic`, `DiagnosticSeverity` — used to construct and emit Warning diagnostics. diff --git a/docs/design/sysml2-tools-language/semantic/model/sysml-edge.md b/docs/design/sysml2-tools-language/semantic/model/sysml-edge.md index d32f695a..8d1f29ae 100644 --- a/docs/design/sysml2-tools-language/semantic/model/sysml-edge.md +++ b/docs/design/sysml2-tools-language/semantic/model/sysml-edge.md @@ -4,12 +4,12 @@ `SysmlEdge` and `SysmlEdgeKind` model a single resolved directed reference between two qualified names in the semantic model. Edges are produced by `ReferenceResolver` while -walking supertype, feature-typing, import, satisfy, verify, allocate, connect, transition, -and expose references, and are the raw material indexed by `SemanticIndex`. +walking supertype, feature-typing, redefinition, import, satisfy, verify, allocate, connect, +transition, and expose references, and are the raw material indexed by `SemanticIndex`. ##### Types -`SysmlEdgeKind` is an enum with nine members: +`SysmlEdgeKind` is an enum with ten members: - `Supertype` — a specialization reference (`SupertypeNames` / `specializes` / `:>`). - `Typing` — a feature typing reference (`SysmlFeatureNode.FeatureTyping`, the type after `:`). @@ -41,6 +41,11 @@ and expose references, and are the raw material indexed by `SemanticIndex`. workspace, unchanged from the pre-scoping baseline. `SysmlViewNode.RenderTargetName` (a rendering-style/format selector, e.g. `asTreeDiagram`) is captured but never resolved into an edge and has no effect on scope. +- `Redefinition` — a feature redefinition reference (`redefines X;` / `:>> X`), sourced from the + redefining feature's own qualified name (`SysmlFeatureNode.RedefinedFeatureName`) and + targeting the resolved redefined-feature reference. Rendered by `GeneralViewLayoutStrategy` as + a solid line with a hollow-triangle-crossbar end marker at the owning definition of the + redefined feature. `SysmlEdge` is a sealed positional record with three properties: diff --git a/docs/design/sysml2-tools-language/semantic/model/sysml-node.md b/docs/design/sysml2-tools-language/semantic/model/sysml-node.md index b31fbfb9..5d019336 100644 --- a/docs/design/sysml2-tools-language/semantic/model/sysml-node.md +++ b/docs/design/sysml2-tools-language/semantic/model/sysml-node.md @@ -41,8 +41,8 @@ All nodes carry: (mirroring the `SupertypeNames`/`ImportedNames` loops) into `SysmlEdgeKind.Verify` edges sourced from this node — there is no standalone verify-usage node type; this list is the sole producer of `Verify` edges. -- `ResolvedEdges` — resolved outgoing `SysmlEdge` entries (supertype, typing, import, satisfy, - verify, allocate, connect, transition, expose), populated post-construction by +- `ResolvedEdges` — resolved outgoing `SysmlEdge` entries (supertype, typing, redefinition, + import, satisfy, verify, allocate, connect, transition, expose), populated post-construction by `ReferenceResolver`; a settable (not `init`) property since resolution runs after the AST is built and the symbol table is fully populated. Empty for stdlib-only nodes, which are registered but never passed through @@ -64,6 +64,20 @@ There are no behavioral methods beyond the inherited `object` members. `SysmlImp - `DefinitionKeyword` — the grammar keyword string (e.g., `"part def"`, `"attribute def"`). +`SysmlFeatureNode` adds: + +- `FeatureKeyword` — the usage keyword string (e.g., `"part"`, `"port"`, `"attribute"`, `"ref"`). +- `FeatureTyping` — the raw reference text of the type after `:` (or `typed by`), or null when + untyped. Resolved by `ReferenceResolver` into a `SysmlEdgeKind.Typing` edge. +- `RedefinedFeatureName` — the raw reference text of the feature's `redefines ;`/ + `:>> ` clause, or null when the feature declares no redefinition. Extracted by + `AstBuilder.ExtractRedefinedFeature`, mirroring `ExtractFeatureTyping`'s structure exactly + (walking `redefinitions()` instead of `typings()`). Captured verbatim only — including + qualified `Owner::feature` forms — with no resolution attempted at this stage. Resolved by + `ReferenceResolver` into a `SysmlEdgeKind.Redefinition` edge, and rendered by + `GeneralViewLayoutStrategy` as a hollow-triangle-crossbar marker. +- `Multiplicity` — the multiplicity text (e.g., `"[4]"`, `"[0..*]"`), or null when unspecified. + `SysmlConnectionNode` adds: - `ConnectionKeyword` — the connection keyword (e.g., `"connection"`, `"binding"`, or the @@ -143,12 +157,13 @@ elements are filtered out by `AstBuilder` before a node is constructed. for usages) from both `VisitViewDefinition` (`view def`) and `VisitViewUsage` (`view`, the only form that can carry `expose`). - `SymbolTable` — traverses the node hierarchy via `Children`; reads `QualifiedName`. -- `ReferenceResolver` — reads `SupertypeNames`, `FeatureTyping`, `ImportedNames`, +- `ReferenceResolver` — reads `SupertypeNames`, `FeatureTyping`, `RedefinedFeatureName`, + `ImportedNames`, `VerifiedRequirementNames`, `Children`; checks for `SysmlImportNode`, `SysmlSatisfyNode`, the `"allocation"` `SysmlConnectionNode` variant, the `"connection"`/`"message"` `SysmlConnectionNode` variants, `SysmlTransitionNode`, and `SysmlViewNode` (reading `ExposedNames`; `RenderTargetName`/`FilterExpressionText` are never read); writes `ResolvedEdges` after resolving references (in two passes — - supertype/typing/import/satisfy/verify/allocate/expose, then feature-chain + supertype/typing/redefinition/import/satisfy/verify/allocate/expose, then feature-chain connect/transition). - `SupertypeWalker` — reads `SupertypeNames` on each node retrieved from `SymbolTable`. diff --git a/docs/gallery/README.md b/docs/gallery/README.md index daeba663..6ad8497f 100644 --- a/docs/gallery/README.md +++ b/docs/gallery/README.md @@ -29,6 +29,10 @@ requirement) grouped in a package folder, with typed compartments (attributes, ports, parts) and specialization edges. Definitions are placed by a layered (ELK-style) engine with orthogonal edge routing. +`RacingMotor` also redefines `Motor`'s inherited `maxThrust` attribute +(`attribute :>> maxThrust : Mass;`), demonstrating the hollow-triangle-crossbar +marker used for `redefines`/`:>>` relationships. + Model: [`models/01-drone-general.sysml`](models/01-drone-general.sysml) · SVG: [`svg/DroneGeneralView.svg`](svg/DroneGeneralView.svg) diff --git a/docs/gallery/models/01-drone-general.sysml b/docs/gallery/models/01-drone-general.sysml index ce5676cb..12b7f9e6 100644 --- a/docs/gallery/models/01-drone-general.sysml +++ b/docs/gallery/models/01-drone-general.sysml @@ -45,7 +45,12 @@ package QuadcopterDrone { part def Frame; // ===== Specializations ===== - part def RacingMotor :> Motor; + // RacingMotor redefines the inherited maxThrust attribute to demonstrate + // the redefinition crossbar marker (hollow-triangle-crossbar) rendered + // for `:>>`/`redefines` relationships in the General View. + part def RacingMotor :> Motor { + attribute :>> maxThrust : Mass; + } part def EnduranceBattery :> Battery; // ===== The integrated drone ===== diff --git a/docs/gallery/png/DroneGeneralView.png b/docs/gallery/png/DroneGeneralView.png index 867252ee..beb99dd5 100644 Binary files a/docs/gallery/png/DroneGeneralView.png and b/docs/gallery/png/DroneGeneralView.png differ diff --git a/docs/gallery/svg/DroneGeneralView.svg b/docs/gallery/svg/DroneGeneralView.svg index a22947d0..47f00b21 100644 --- a/docs/gallery/svg/DroneGeneralView.svg +++ b/docs/gallery/svg/DroneGeneralView.svg @@ -1,4 +1,4 @@ - + @@ -30,18 +30,18 @@ - - «package» - QuadcopterDrone + + «package» + QuadcopterDrone «interface def» PowerBus «interface def» DataBus - - «port def» - PowerPort + + «port def» + PowerPort «port def» TelemetryPort @@ -51,106 +51,111 @@ «port def» SensorPort - - «attribute def» - Mass - - «attribute def» - Voltage - - «enum def» - FlightMode - - «part def» - Battery - - attributes - capacity : Voltage - - ports - output : PowerPort - - «part def» - FlightController - - attributes - mode : FlightMode - - ports - power : PowerPort - telemetry : TelemetryPort - motors : MotorControlPort - sensors : SensorPort - - «part def» - Motor - - attributes - maxThrust : Mass - - ports - control : MotorControlPort - - «part def» - Propeller - - «part def» - ImuSensor - - ports - data : SensorPort - - «part def» - GpsSensor - - ports - data : SensorPort - - «part def» - Frame - - «part def» - RacingMotor - - «part def» - EnduranceBattery - - «part def» - Drone - - attributes - totalMass : Mass - - parts - airframe : Frame - battery : Battery - controller : FlightController - motors : Motor [4] - propellers : Propeller [4] - imu : ImuSensor - gps : GpsSensor - - «requirement def» - FlightTimeRequirement - - - - - - - - - - - - - - - - - - - - - + + «attribute def» + Mass + + «attribute def» + Voltage + + «enum def» + FlightMode + + «part def» + Battery + + attributes + capacity : Voltage + + ports + output : PowerPort + + «part def» + FlightController + + attributes + mode : FlightMode + + ports + power : PowerPort + telemetry : TelemetryPort + motors : MotorControlPort + sensors : SensorPort + + «part def» + Motor + + attributes + maxThrust : Mass + + ports + control : MotorControlPort + + «part def» + Propeller + + «part def» + ImuSensor + + ports + data : SensorPort + + «part def» + GpsSensor + + ports + data : SensorPort + + «part def» + Frame + + «part def» + RacingMotor + + attributes + : Mass + + «part def» + EnduranceBattery + + «part def» + Drone + + attributes + totalMass : Mass + + parts + airframe : Frame + battery : Battery + controller : FlightController + motors : Motor [4] + propellers : Propeller [4] + imu : ImuSensor + gps : GpsSensor + + «requirement def» + FlightTimeRequirement + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/reqstream/sysml2-tools-core/layout/internal/general-view-layout-strategy.yaml b/docs/reqstream/sysml2-tools-core/layout/internal/general-view-layout-strategy.yaml index 9eb27212..9a435399 100644 --- a/docs/reqstream/sysml2-tools-core/layout/internal/general-view-layout-strategy.yaml +++ b/docs/reqstream/sysml2-tools-core/layout/internal/general-view-layout-strategy.yaml @@ -203,3 +203,51 @@ sections: limitation visible rather than a silent gap. tests: - GeneralViewLayoutStrategy_BuildLayout_FilterExpressionPresent_EmitsNotYetEvaluatedWarning + + - id: SysML2Tools-Core-Layout-Internal-GeneralViewLayoutStrategy-Redefinition-QualifiedForm + title: >- + GeneralViewLayoutStrategy shall connect a subtype feature that declares a qualified + `Owner::feature` `redefines`/`:>>` reference to the named owner with a solid line + ending in a hollow-triangle-with-crossbar end marker at the owner. + justification: | + `redefines`/`:>>` feature redefinition is core SysML v2 model structure completing the + ROADMAP's "Connector-end & line-style conformance" item: the already-existing + `HollowTriangleCrossbar` end-marker style needed a producing model edge to ever appear + in a rendered General View. A qualified reference names its owner directly, matching + the shape observed in the OMG corpus (e.g. + `test/SysMLModels/OMG/validation/01-PartsTree/1a-PartsTree.sysml`'s qualified + `redefines Vehicle::mass`). + tests: + - GeneralViewLayoutStrategy_BuildLayout_QualifiedRedefinition_ProducesHollowTriangleCrossbarEdgeToOwner + + - id: SysML2Tools-Core-Layout-Internal-GeneralViewLayoutStrategy-Redefinition-BareNameForm + title: >- + GeneralViewLayoutStrategy shall connect a subtype feature that declares a bare-name + `redefines`/`:>>` reference to the ancestor definition that declares a matching member, + found by walking the subtype's own supertype chain transitively, with a solid line + ending in a hollow-triangle-with-crossbar end marker at that ancestor. + justification: | + The dominant real-world shape for `redefines` per the SysML v2 spec is a bare name + referencing a member the subtype inherits, matching the shape observed in the OMG + corpus (e.g. + `test/SysMLModels/OMG/training/05.Redefinition/RedefinitionExample.sysml`'s bare-name + `redefines eng`). Resolving transitively (not just one supertype hop) is required + because the declaring ancestor is frequently two or more hops away from the redefining + subtype. + tests: + - GeneralViewLayoutStrategy_BuildLayout_BareNameRedefinition_ProducesHollowTriangleCrossbarEdge + - GeneralViewLayoutStrategy_BuildLayout_TransitiveBareNameRedefinition_ProducesHollowTriangleCrossbarEdgeToDeclaringAncestor + + - id: SysML2Tools-Core-Layout-Internal-GeneralViewLayoutStrategy-Redefinition-NoEdgeOnFailure + title: >- + GeneralViewLayoutStrategy shall produce no redefinition edge and no diagnostic when a + `redefines`/`:>>` reference cannot be resolved to any ancestor member, or when it + resolves back to the redefining feature's own owning definition. + justification: | + Graceful degradation for an unresolvable or self-referential redefinition reference is + consistent with how every other reference kind in this unit behaves on failure: false + edge is drawn, and diagram layout must never throw for a reference that cannot be + resolved. + tests: + - GeneralViewLayoutStrategy_BuildLayout_UnresolvableRedefinition_ProducesNoEdge + - GeneralViewLayoutStrategy_BuildLayout_SelfReferentialRedefinition_ProducesNoEdge diff --git a/docs/reqstream/sysml2-tools-language.yaml b/docs/reqstream/sysml2-tools-language.yaml index b8ed65e5..cdee4a03 100644 --- a/docs/reqstream/sysml2-tools-language.yaml +++ b/docs/reqstream/sysml2-tools-language.yaml @@ -52,9 +52,9 @@ sections: - id: SysML2Tools-Language-ReverseIndex title: >- - The Language library shall persist resolved supertype, feature-typing, and import - references as edges on each AST node and expose a reverse-lookup index over all - resolved edges via the loaded workspace. + The Language library shall persist resolved supertype, feature-typing, redefinition, + and import references as edges on each AST node and expose a reverse-lookup index over + all resolved edges via the loaded workspace. justification: | A persisted, queryable reverse index over resolved qualified-name references is the foundational data structure required by the `query` command's uses/used-by/impact/ @@ -68,6 +68,8 @@ sections: - WorkspaceLoader_LoadAsync_SupertypeAcrossEnclosingNamespace_RecordsResolvedTargetName - WorkspaceLoader_LoadAsync_UnresolvedImport_ProducesWarningNoCrash - WorkspaceLoader_LoadAsync_MultiKindFixtureModel_IndexAnswersIncomingAndOutgoingQueries + - WorkspaceLoader_LoadAsync_BareRedefinitionOfInheritedFeature_RecordsRedefinitionEdgeNoWarning + - WorkspaceLoader_LoadAsync_UnresolvedRedefinition_ProducesWarningNoEdge - id: SysML2Tools-Language-AnnotationCapture title: >- 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 7693db5d..6d3cf0a4 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/ast-builder.yaml @@ -30,8 +30,9 @@ sections: - id: SysML2Tools-Language-Semantic-Model-AstBuilder-Supertypes title: >- - AstBuilder shall extract supertype qualified names from subclassification and - specialization parts and attach them to the corresponding AST node. + AstBuilder shall extract supertype qualified names from a definition's + subclassification and specialization parts and attach them to the corresponding + AST node. justification: | Supertype names are required for reference resolution and supertype walking. They must be extracted during AST construction before symbol table operations. @@ -39,6 +40,22 @@ sections: - WorkspaceLoader_LoadAsync_SpecializesChain_Registered - WorkspaceLoader_LoadAsync_UnresolvedReference_ProducesWarning + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-UsageSubsetting + title: >- + AstBuilder shall extract a usage/feature's own `subsets`/`:>` clause (as distinct + from its `redefines`/`:>>` clause) into the same SupertypeNames property used for + definition-level supertypes. + justification: | + A usage-level `subsets`/`:>` clause (e.g. `part vehicle1_c1 :> vehicle1`) was + previously never extracted at all, so it produced no Supertype edge and was silently + invisible to reference resolution — this broke ReferenceResolver's bare-name + redefinition ancestor-chain walk whenever the redefining feature's owner was itself a + usage-level subsetting (not a `part def` specialization), the exact shape used by the + OMG corpus fixture `1c-PartsTreeRedefinition.sysml`. + tests: + - WorkspaceLoader_LoadAsync_UsageLevelSubsetting_PopulatesSupertypeNames + - WorkspaceLoader_LoadAsync_1cPartsTreeRedefinitionFixture_NoUnresolvedReferenceWarnings + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-ViewRenderTarget title: >- AstBuilder shall capture a view's render target reference text (from its @@ -100,3 +117,23 @@ sections: tests: - WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge - RenderSubsystem_OmgSafetyFeatureViewsCorpus_RendersAllNamedViewUsages + + - id: SysML2Tools-Language-Semantic-Model-AstBuilder-RedefinedFeature + title: >- + AstBuilder shall extract a feature's redefined-feature reference text (from its + `redefines ;`/`:>> ` clause) as raw, unresolved data on the + corresponding SysmlFeatureNode's RedefinedFeatureName property, mirroring how + FeatureTyping is extracted. + justification: | + Capturing the raw redefinition reference during AST construction is the + prerequisite for ReferenceResolver to resolve it into a Redefinition edge, and for + GeneralViewLayoutStrategy to render the hollow-triangle-crossbar marker. The + extraction mirrors ExtractFeatureTyping's structure exactly (walking + featureSpecialization()'s redefinitions() instead of typings()), supporting both the + `redefines` keyword form and the `:>>` operator form, and both bare-name and + qualified `Owner::feature` reference shapes. + tests: + - WorkspaceLoader_LoadAsync_RedefinesKeyword_CapturesRedefinedFeatureName + - WorkspaceLoader_LoadAsync_ColonGtGtOperator_CapturesRedefinedFeatureName + - WorkspaceLoader_LoadAsync_QualifiedRedefinition_CapturesRawText + - WorkspaceLoader_LoadAsync_NoRedefinition_RedefinedFeatureNameIsNull 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 712fa961..18a1fe3b 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/reference-resolver.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/reference-resolver.yaml @@ -10,12 +10,22 @@ sections: - id: SysML2Tools-Language-Semantic-Model-ReferenceResolver-Resolve title: >- ReferenceResolver.ResolveAll shall check each supertype name referenced by - AST nodes against the symbol table and produce a Warning for any unresolved name. + AST nodes against the symbol table and produce a Warning for any unresolved name — + for a usage/feature node, falling back to the same bare-name supertype/redefinition + ancestor-chain walk used for RedefinedFeatureName when the standard lookup fails, + since a usage-level `subsets`/`:>` clause commonly names an inherited member. justification: | Unresolved references indicate model incompleteness. Reporting as Warnings allows - the caller to continue analysis and provide partial results. + the caller to continue analysis and provide partial results. A usage-level + `subsets`/`:>` clause (e.g. `part frontWheel_1 subsets frontWheel = ...;`) is, per the + SysML v2 spec, commonly a bare reference to a member the owner inherits rather than + declares itself — the same dominant shape as a bare `redefines` reference — so + omitting the ancestor-chain fallback here would reintroduce the same false-warning + defect this fix addresses for RedefinedFeatureName. The fallback is skipped for + definition-level SupertypeNames (`part def X :> Y`), which name the supertype directly. tests: - WorkspaceLoader_LoadAsync_UnresolvedReference_ProducesWarning + - WorkspaceLoader_LoadAsync_1cPartsTreeRedefinitionFixture_NoUnresolvedReferenceWarnings - id: SysML2Tools-Language-Semantic-Model-ReferenceResolver-CircularImport title: >- @@ -73,3 +83,33 @@ sections: - WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic - WorkspaceLoader_LoadAsync_ViewFilterExpression_CapturesTextVerbatimNoEdge - RenderSubsystem_OmgSafetyFeatureViewsCorpus_RendersAllNamedViewUsages + + - id: SysML2Tools-Language-Semantic-Model-ReferenceResolver-RedefinitionResolution + title: >- + ReferenceResolver.ResolveAll shall resolve a feature's RedefinedFeatureName against + the symbol table into a Redefinition-kind edge — trying the standard + namespace/import-scoped lookup first, then falling back to a supertype/redefinition + ancestor-chain walk for the dominant bare-name-referencing-an-inherited-member case — + and produce an unresolved-reference Warning diagnostic (with no corresponding edge) + only when neither strategy resolves it. + justification: | + Resolving RedefinedFeatureName into a Redefinition edge is the prerequisite for + GeneralViewLayoutStrategy to render the hollow-triangle-crossbar marker, and the + unresolved-reference diagnostic surfaces a bogus `redefines`/`:>>` target the same way + an unresolved supertype or typing reference is surfaced. The plain namespace/import + lookup used for FeatureTyping is not sufficient here: per the SysML v2 spec, a + `redefines`/`:>>` target's entire purpose is referencing a member the redefining + feature *inherits* (typically declared on a supertype, or reachable only via the + owner's own already-resolved Redefinition edge), not one declared or imported into the + same lexical scope — the dominant shape across the OMG corpus (e.g. + `RedefinitionExample.sysml`'s `smallEng redefines eng`, + `1c-PartsTreeRedefinition.sysml`'s nested `frontAxle_c1 redefines frontAxle`). The + ancestor-chain fallback (`TryResolveBareRedefinition`/`FindMemberInAncestorChain`) is + additive and only activates once the standard lookup fails, so previously-successful + qualified/scoped resolutions are unaffected. + tests: + - WorkspaceLoader_LoadAsync_ResolvedRedefinition_RecordsRedefinitionEdge + - WorkspaceLoader_LoadAsync_UnresolvedRedefinition_ProducesWarningNoEdge + - WorkspaceLoader_LoadAsync_BareRedefinitionOfInheritedFeature_RecordsRedefinitionEdgeNoWarning + - WorkspaceLoader_LoadAsync_RedefinitionExampleFixture_NoUnresolvedReferenceWarnings + - WorkspaceLoader_LoadAsync_1cPartsTreeRedefinitionFixture_NoUnresolvedReferenceWarnings diff --git a/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml b/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml index b8664419..0ecc04b9 100644 --- a/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml +++ b/docs/reqstream/sysml2-tools-language/semantic/model/sysml-node.yaml @@ -64,3 +64,19 @@ sections: tests: - WorkspaceLoader_LoadAsync_ViewFilterExpression_CapturesTextVerbatimNoEdge - WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty + + - id: SysML2Tools-Language-Semantic-Model-SysmlNode-RedefinedFeatureName + title: >- + SysmlFeatureNode shall expose RedefinedFeatureName: the raw reference text of its + `redefines ;`/`:>> ` clause, or null when the feature declares no + redefinition. + justification: | + Making a feature's redefinition reference user-observable data, mirroring how + FeatureTyping is exposed, is the prerequisite for ReferenceResolver to resolve it into + a `Redefinition`-kind edge that GeneralViewLayoutStrategy renders as a + hollow-triangle-crossbar marker. + tests: + - WorkspaceLoader_LoadAsync_RedefinesKeyword_CapturesRedefinedFeatureName + - WorkspaceLoader_LoadAsync_ColonGtGtOperator_CapturesRedefinedFeatureName + - WorkspaceLoader_LoadAsync_QualifiedRedefinition_CapturesRawText + - WorkspaceLoader_LoadAsync_NoRedefinition_RedefinedFeatureNameIsNull diff --git a/docs/verification/sysml2-tools-core/layout/internal/general-view-layout-strategy.md b/docs/verification/sysml2-tools-core/layout/internal/general-view-layout-strategy.md index 7630d93b..0161b700 100644 --- a/docs/verification/sysml2-tools-core/layout/internal/general-view-layout-strategy.md +++ b/docs/verification/sysml2-tools-core/layout/internal/general-view-layout-strategy.md @@ -6,7 +6,7 @@ that construct a synthetic `SysmlWorkspace` of definitions, invoke `BuildLayout`, and assert on the returned `LayoutTree`. A recursive helper collects boxes from the (possibly nested) node tree so assertions can confirm box keywords, folder shapes, compartments, and specialization, membership, -and attribute-typing lines. No +attribute-typing, and redefinition lines. No mocking is required; the strategy depends only on the in-memory model, `LayeredPlacement`, and render options, all constructed directly by the tests. @@ -55,6 +55,20 @@ configuration are required beyond a standard .NET SDK installation. pre-scoping-change 2-argument `ViewContext` construction used throughout the rest of this test file) renders every non-stdlib definition in the workspace, unchanged from before this feature — the critical regression guard confirming full backward compatibility. +- A subtype feature that redefines a bare-named inherited feature (declared on a resolved + supertype in the view) yields a solid line with a hollow-triangle-crossbar end marker at the + supertype that declares the redefined feature. +- A subtype feature that redefines a bare-named feature declared two or more supertype hops away + yields a hollow-triangle-crossbar edge targeting the actual declaring ancestor, not the + immediate supertype, proving the bare-name walk is transitive. +- A subtype feature that redefines a qualified `Owner::feature` reference yields a + hollow-triangle-crossbar edge directly to the named owner, without needing the owner to be an + immediate or transitive supertype. +- An unresolvable redefinition reference (neither a qualified owner nor a bare name found + anywhere in the supertype chain) produces no edge, and layout completes without throwing. +- A genuinely self-referential redefinition (a feature's `redefines` target resolves back to its + own owning definition, via a self-referential supertype cycle) produces no edge, and layout + completes without throwing. ##### Test Scenarios @@ -106,3 +120,17 @@ configuration are required beyond a standard .NET SDK installation. A non-null `FilterExpressionText` emits the "parsed but not yet evaluated" warning - `GeneralViewLayoutStrategy_BuildLayout_NullViewNode_RendersFullWorkspaceUnchanged`: A `null` `ViewNode` (`--auto`/default) renders every definition, unchanged (regression guard) +- `GeneralViewLayoutStrategy_BuildLayout_BareNameRedefinition_ProducesHollowTriangleCrossbarEdge`: + A bare-name redefinition produces a solid hollow-triangle-crossbar edge to the supertype that + declares the redefined feature +- `GeneralViewLayoutStrategy_BuildLayout_TransitiveBareNameRedefinition_ProducesHollowTriangleCrossbarEdgeToDeclaringAncestor`: + A bare-name redefinition whose declaring ancestor is two supertype hops away produces a + hollow-triangle-crossbar edge to that ancestor, not the immediate supertype +- `GeneralViewLayoutStrategy_BuildLayout_QualifiedRedefinition_ProducesHollowTriangleCrossbarEdgeToOwner`: + A qualified `Owner::feature` redefinition produces a hollow-triangle-crossbar edge to the + named owner +- `GeneralViewLayoutStrategy_BuildLayout_UnresolvableRedefinition_ProducesNoEdge`: + An unresolvable redefinition produces no edge and does not throw +- `GeneralViewLayoutStrategy_BuildLayout_SelfReferentialRedefinition_ProducesNoEdge`: + A genuinely self-referential redefinition (resolving back to its own owning definition via a + self-referential supertype cycle) produces no edge and does not throw diff --git a/docs/verification/sysml2-tools-language.md b/docs/verification/sysml2-tools-language.md index 0329a2b5..6d0bebfc 100644 --- a/docs/verification/sysml2-tools-language.md +++ b/docs/verification/sysml2-tools-language.md @@ -21,9 +21,11 @@ SDK installation. - `WorkspaceLoader` correctly registers qualified names from SysML packages and definitions. - Unresolved supertype references produce `Warning`-severity diagnostics. - Circular imports between two files produce at least one `Warning`-severity diagnostic. -- Resolved supertype, feature-typing, and import references are recorded as `SysmlEdge` - entries queryable via `SysmlWorkspace.Index.GetOutgoingEdges`/`GetIncomingEdges` in both - directions. +- Resolved supertype, feature-typing, redefinition, and import references are recorded as + `SysmlEdge` entries queryable via `SysmlWorkspace.Index.GetOutgoingEdges`/`GetIncomingEdges` + in both directions. A bare-name `redefines`/`:>>` target inherited from an ancestor resolves + correctly regardless of the ancestor's declaration order relative to the redefining feature, + whether within one file or split across files. - Comment (`comment`) and documentation (`doc`) annotating-element text is captured verbatim onto the owning node's `SysmlNode.Annotations` list, in source order, and is empty (never null) for nodes with none. @@ -58,6 +60,18 @@ Primary acceptance evidence is provided by: is queryable from both directions via `SysmlWorkspace.Index`. - `WorkspaceLoader_LoadAsync_ResolvedFeatureTyping_RecordsTypingEdge` — resolved feature typing is recorded as a `Typing`-kind edge. +- `WorkspaceLoader_LoadAsync_BareRedefinitionOfInheritedFeature_RecordsRedefinitionEdgeNoWarning` + / `WorkspaceLoader_LoadAsync_UnresolvedRedefinition_ProducesWarningNoEdge` — a resolvable + bare-name inherited-feature `redefines` reference is recorded as a `Redefinition`-kind edge, + and an unresolvable one produces a Warning diagnostic and no edge. +- `WorkspaceLoader_LoadAsync_OutOfOrderRedefinitionChain_RecordsRedefinitionEdgeNoWarning` + / `WorkspaceLoader_LoadAsync_CrossFileOutOfOrderRedefinitionChain_RecordsRedefinitionEdgeNoWarning` + — regression coverage proving a bare-name redefinition resolves correctly through a + multi-hop ancestor chain regardless of declaration order, within one file and split across + two files. +- `WorkspaceLoader_LoadAsync_RedefinitionExampleFixture_NoUnresolvedReferenceWarnings` / + `WorkspaceLoader_LoadAsync_1cPartsTreeRedefinitionFixture_NoUnresolvedReferenceWarnings` — + real OMG fixture files exercise bare-name redefinition resolution end-to-end. - `WorkspaceLoader_LoadAsync_WildcardImport_RecordsImportEdge` / `WorkspaceLoader_LoadAsync_NamedImport_RecordsImportEdge` — resolved imports are recorded as `Import`-kind edges. diff --git a/docs/verification/sysml2-tools-language/semantic.md b/docs/verification/sysml2-tools-language/semantic.md index 5e21e98d..c35264d4 100644 --- a/docs/verification/sysml2-tools-language/semantic.md +++ b/docs/verification/sysml2-tools-language/semantic.md @@ -24,6 +24,9 @@ Temporary files are created in `Path.GetTempPath()` and cleaned up after each te - Part definitions register their qualified names. - Unresolved supertype references produce Warning diagnostics. - Circular imports produce Warning diagnostics without infinite loops. +- Resolved `redefines`/`:>>` references — including a bare-name reference to a member inherited + from an ancestor declared anywhere in the ancestor chain, regardless of declaration order — are + recorded as `Redefinition`-kind edges; unresolvable references produce Warning diagnostics. ### Test Scenarios 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 e221b438..d610c580 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md +++ b/docs/verification/sysml2-tools-language/semantic/model/ast-builder.md @@ -22,6 +22,9 @@ external services or additional configuration are required beyond a standard .NE - An element with only a short name `< shortName >` (no declared name) is not registered. - A definition with `specializes KnownType` produces a `SupertypeNames` entry that resolves without a Warning when `KnownType` is registered. +- A usage/feature's own usage-level `subsets`/`:>` clause (distinct from a definition's + `specializes`/`:>` supertype clause) directly populates that feature node's `SupertypeNames` + with the expected target name. - `VisitViewDefinition` captures `render ;` and `filter [];` members' raw text on the corresponding `SysmlViewNode`, and leaves both null for a view with an empty body. - `VisitViewUsage` (a named `view` usage, not a `view def` definition) captures the same @@ -29,6 +32,10 @@ external services or additional configuration are required beyond a standard .NE populated `ExposedNames`. This also makes every named `view` usage its own renderable declaration, an intentional capability addition beyond `expose` capture alone (see the ast-builder design doc). +- `BuildUsageNode` captures a feature's redefinition reference on `RedefinedFeatureName` for both + the `redefines` keyword form and the `:>>` operator form, for both a bare simple name and a + qualified `Owner::feature` form (captured verbatim, unresolved), and leaves it null for a + feature that declares no redefinition. ##### Test Scenarios @@ -38,8 +45,13 @@ external services or additional configuration are required beyond a standard .NE | Qualified name from namespace stack | `WorkspaceLoader_LoadAsync_NestedPackages_RegistersQualifiedNames` | | Definition registration | `WorkspaceLoader_LoadAsync_PartDef_RegistersDefinition` | | Supertype extraction | `WorkspaceLoader_LoadAsync_SpecializesChain_Registered` | +| Usage-level `subsets`/`:>` capture | `WorkspaceLoader_LoadAsync_UsageLevelSubsetting_PopulatesSupertypeNames` | | `VisitViewDefinition` render | `WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic` | | `VisitViewDefinition` filter capture | `WorkspaceLoader_LoadAsync_ViewFilterExpression_CapturesTextVerbatimNoEdge` | | `VisitViewUsage` expose capture | `WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge` | | `VisitViewUsage` renderable declaration | `RenderSubsystem_OmgSafetyFeatureViewsCorpus_RendersAllNamedViewUsages` | | Empty view body regression guard | `WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty` | +| Redefinition, `redefines` keyword | `WorkspaceLoader_LoadAsync_RedefinesKeyword_CapturesRedefinedFeatureName` | +| Redefinition capture, `:>>` operator | `WorkspaceLoader_LoadAsync_ColonGtGtOperator_CapturesRedefinedFeatureName` | +| Redefinition capture, qualified form | `WorkspaceLoader_LoadAsync_QualifiedRedefinition_CapturesRawText` | +| No redefinition leaves field null | `WorkspaceLoader_LoadAsync_NoRedefinition_RedefinedFeatureNameIsNull` | 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 e734d90f..76ea99d1 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/reference-resolver.md +++ b/docs/verification/sysml2-tools-language/semantic/model/reference-resolver.md @@ -33,6 +33,9 @@ external services or additional configuration are required beyond a standard .NE per the SysML v2 grammar, never content) is captured on `SysmlViewNode.RenderTargetName` but never inspected by `ReferenceResolver` — no edge is produced and no diagnostic is emitted for it, even when the named identifier is not declared anywhere in the file. +- A resolved feature redefinition reference (`redefines X;` / `:>> X`) is recorded as a + `Redefinition`-kind `SysmlEdge`; an unresolved one produces a Warning diagnostic naming the + unresolved identifier and no edge, mirroring `FeatureTyping`'s resolution behavior exactly. ##### Test Scenarios @@ -50,3 +53,10 @@ external services or additional configuration are required beyond a standard .NE | RenderTargetName captured raw | `WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic` | | Resolved expose name records edge | `WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge` | | E2E diagnostic visibility | `RenderSubsystem_ViewsWithDistinctExposeTargets_ProduceDifferingOutputsAndDiagnostic` | +| Resolved redefinition records edge | `WorkspaceLoader_LoadAsync_ResolvedRedefinition_RecordsRedefinitionEdge` | +| Unresolved redefinition — no edge | `WorkspaceLoader_LoadAsync_UnresolvedRedefinition_ProducesWarningNoEdge` | +| Bare-name feature | `WorkspaceLoader_LoadAsync_BareRedefinitionOfInheritedFeature_RecordsRedefinitionEdgeNoWarning` | +| Out-of-order | `WorkspaceLoader_LoadAsync_OutOfOrderRedefinitionChain_RecordsRedefinitionEdgeNoWarning` | +| Cross-file | `WorkspaceLoader_LoadAsync_CrossFileOutOfOrderRedefinitionChain_RecordsRedefinitionEdgeNoWarning` | +| OMG `RedefinitionExample` | `WorkspaceLoader_LoadAsync_RedefinitionExampleFixture_NoUnresolvedReferenceWarnings` | +| OMG PartsTree fixture | `WorkspaceLoader_LoadAsync_1cPartsTreeRedefinitionFixture_NoUnresolvedReferenceWarnings` | diff --git a/docs/verification/sysml2-tools-language/semantic/model/sysml-edge.md b/docs/verification/sysml2-tools-language/semantic/model/sysml-edge.md index 73cd012c..6f1f2fb6 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/sysml-edge.md +++ b/docs/verification/sysml2-tools-language/semantic/model/sysml-edge.md @@ -24,6 +24,8 @@ external services or additional configuration are required beyond a standard .NE `Kind == SysmlEdgeKind.Expose`; `SysmlEdgeKind.Render` no longer exists, since a view's `render ;` member is never resolved into an edge (it names a rendering style/format, not content). +- A resolved feature redefinition reference produces a `SysmlEdge` with + `Kind == SysmlEdgeKind.Redefinition`. ##### Test Scenarios @@ -35,3 +37,4 @@ external services or additional configuration are required beyond a standard .NE | Import edge recorded (named) | `WorkspaceLoader_LoadAsync_NamedImport_RecordsImportEdge` | | RenderTargetName never resolved | `WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic` | | Expose edge recorded | `WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge` | +| Redefinition edge recorded | `WorkspaceLoader_LoadAsync_ResolvedRedefinition_RecordsRedefinitionEdge` | diff --git a/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md b/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md index 9376e5ba..ff8ab168 100644 --- a/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md +++ b/docs/verification/sysml2-tools-language/semantic/model/sysml-node.md @@ -20,7 +20,9 @@ external services or additional configuration are required beyond a standard .NE - `SysmlDefinitionNode` is constructed with the correct `QualifiedName` and `DefinitionKeyword` for a `part def` declaration; its qualified name appears in `Declarations`. - `SysmlNode.SupertypeNames` is populated correctly for a definition with a `specializes` - clause; the name is checked by `ReferenceResolver`. + clause; the name is checked by `ReferenceResolver`. It is likewise populated for a + usage/feature's own usage-level `subsets`/`:>` clause, distinct from a definition-level + supertype. - `SysmlImportNode.ImportedNamespace` is extracted and used by `ReferenceResolver` to build the import graph. - `SysmlNode.ResolvedEdges` is populated by `ReferenceResolver` with the resolved outgoing @@ -33,6 +35,10 @@ external services or additional configuration are required beyond a standard .NE evaluated), and are `null`/empty for a view with no such members. `RenderTargetName` is captured but never resolved into an edge or diagnostic (it names a rendering style/format, not content); `ExposedNames` is the only field independently resolved by `ReferenceResolver`. +- `SysmlFeatureNode.RedefinedFeatureName` is populated verbatim from a feature's + `redefines`/`:>>` clause (bare-name and qualified `Owner::feature` forms, both keyword and + operator syntax), and is `null` for a feature with no redefinition. It is resolved by + `ReferenceResolver` into a `Redefinition`-kind edge, mirroring `FeatureTyping`. ##### Test Scenarios @@ -41,9 +47,14 @@ external services or additional configuration are required beyond a standard .NE | `SysmlPackageNode` construction | `WorkspaceLoader_LoadAsync_SinglePackage_RegistersDeclaration` | | `SysmlDefinitionNode` construction | `WorkspaceLoader_LoadAsync_PartDef_RegistersDefinition` | | `SupertypeNames` population | `WorkspaceLoader_LoadAsync_SpecializesChain_Registered` | +| `SupertypeNames` usage-level population | `WorkspaceLoader_LoadAsync_UsageLevelSubsetting_PopulatesSupertypeNames` | | `ResolvedEdges` populated | `WorkspaceLoader_LoadAsync_ResolvedSupertype_RecordsSupertypeEdge` | | `Annotations` populated | `WorkspaceLoader_LoadAsync_CommentAndDocumentation_CapturesBothInSourceOrder` | | `RenderTargetName` unresolved | `WorkspaceLoader_LoadAsync_ViewRenderTarget_CapturedRawNeverResolvedNoDiagnostic` | | `FilterExpressionText` verbatim | `WorkspaceLoader_LoadAsync_ViewFilterExpression_CapturesTextVerbatimNoEdge` | | `SysmlViewNode.ExposedNames` from a `view` usage | `WorkspaceLoader_LoadAsync_ViewUsageWithExpose_RecordsExposeEdge` | | Empty view body leaves all fields null/empty | `WorkspaceLoader_LoadAsync_ViewEmptyBody_AllNewFieldsNullOrEmpty` | +| `RedefinedFeatureName` — `redefines` | `WorkspaceLoader_LoadAsync_RedefinesKeyword_CapturesRedefinedFeatureName` | +| `RedefinedFeatureName` — `:>>` operator | `WorkspaceLoader_LoadAsync_ColonGtGtOperator_CapturesRedefinedFeatureName` | +| `RedefinedFeatureName` — qualified | `WorkspaceLoader_LoadAsync_QualifiedRedefinition_CapturesRawText` | +| `RedefinedFeatureName` — null when absent | `WorkspaceLoader_LoadAsync_NoRedefinition_RedefinedFeatureNameIsNull` | diff --git a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs index b9af9456..2ef238a0 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Layout/Internal/GeneralViewLayoutStrategy.cs @@ -41,8 +41,12 @@ internal sealed class GeneralViewLayoutStrategy : ILayoutStrategy /// Approximate width-per-character factor relative to font size. private const double CharWidthFactor = 0.62; - /// A feature membership: the keyword and the raw typing reference of one owned feature. - private sealed record FeatureMembership(string Keyword, string TypeName); + /// + /// A feature membership: the keyword, raw typing reference (if any), simple name, and raw + /// redefined-feature reference (if any) of one owned feature. is + /// nullable because a feature may declare a redefinition without an explicit type annotation. + /// + private sealed record FeatureMembership(string Keyword, string? TypeName, string? Name, string? RedefinedFeatureName); /// /// The classification of an edge, which selects its line style so the renderer can distinguish @@ -62,6 +66,12 @@ private enum EdgeKind /// notation (dashed + open arrowhead) rather than a membership diamond. /// Typing, + + /// + /// Subtype feature redefinition → the owning definition of the redefined feature: solid + /// line, hollow-triangle-with-crossbar at the owner. + /// + Redefinition, } /// @@ -247,8 +257,9 @@ private static string FormatFeatureRow(SysmlFeatureNode feature) }; /// - /// Collects the feature memberships of a definition: the keyword and type reference of each - /// owned feature that carries a type annotation. + /// Collects the feature memberships of a definition: the keyword, type reference (if any), simple + /// name, and redefined-feature reference (if any) of each owned feature that carries a type + /// annotation and/or a redefinition. /// private static IReadOnlyList CollectMemberships(SysmlDefinitionNode def) { @@ -260,10 +271,11 @@ private static IReadOnlyList CollectMemberships(SysmlDefiniti continue; } - if (feature.FeatureTyping is { Length: > 0 } typing) + var typing = feature.FeatureTyping is { Length: > 0 } ft ? ft : null; + if (typing is not null || feature.RedefinedFeatureName is not null) { var keyword = string.IsNullOrEmpty(feature.FeatureKeyword) ? "feature" : feature.FeatureKeyword; - result.Add(new FeatureMembership(keyword, typing)); + result.Add(new FeatureMembership(keyword, typing, feature.Name, feature.RedefinedFeatureName)); } } @@ -368,6 +380,8 @@ private static List BuildModelEdges(IReadOnlyList defs) bySimple.TryAdd(def.SimpleName, def.QualifiedName); } + var defByQualified = defs.ToDictionary(d => d.QualifiedName, StringComparer.Ordinal); + var edges = new List(); foreach (var def in defs) { @@ -393,7 +407,8 @@ private static List BuildModelEdges(IReadOnlyList defs) }; if (arrowhead != EndMarkerStyle.None && - TryResolveQualified(membership.TypeName, byQualified, bySimple, out var memberType) && + membership.TypeName is { Length: > 0 } memberTypeName && + TryResolveQualified(memberTypeName, byQualified, bySimple, out var memberType) && memberType != def.QualifiedName) { edges.Add(new ModelEdge(memberType, def.QualifiedName, arrowhead, EdgeKind.Membership)); @@ -411,17 +426,99 @@ private static List BuildModelEdges(IReadOnlyList defs) continue; } - if (TryResolveQualified(membership.TypeName, byQualified, bySimple, out var attrType) && + if (membership.TypeName is { Length: > 0 } attrTypeName && + TryResolveQualified(attrTypeName, byQualified, bySimple, out var attrType) && attrType != def.QualifiedName) { edges.Add(new ModelEdge(def.QualifiedName, attrType, EndMarkerStyle.OpenChevron, EdgeKind.Typing)); } } + + // Redefinition: subtype → the owning definition of the redefined feature, hollow + // triangle with crossbar at the owner (target) end. A qualified reference + // (Owner::feature) resolves the owner directly; a bare reference is looked up by + // walking the definition's own supertype chain for a matching member name. + foreach (var membership in def.Memberships) + { + if (membership.RedefinedFeatureName is not { Length: > 0 } redefinedRef) + { + continue; + } + + var owner = ResolveRedefinitionOwner(def, redefinedRef, byQualified, bySimple, defByQualified); + if (owner is not null && owner != def.QualifiedName) + { + edges.Add(new ModelEdge(def.QualifiedName, owner, EndMarkerStyle.HollowTriangleCrossbar, EdgeKind.Redefinition)); + } + } } return edges; } + /// + /// Resolves the owning definition of a redefined feature reference. A qualified reference + /// (containing ::) resolves the owner directly by stripping the trailing feature-name + /// segment; a bare reference is resolved by walking the redefining definition's own supertype + /// chain (transitively, with a cycle guard) for a matching member name. + /// + private static string? ResolveRedefinitionOwner( + DefBox def, + string redefinedRef, + HashSet byQualified, + Dictionary bySimple, + IReadOnlyDictionary defByQualified) + { + var sep = redefinedRef.LastIndexOf("::", StringComparison.Ordinal); + if (sep >= 0) + { + var ownerRef = redefinedRef[..sep]; + return TryResolveQualified(ownerRef, byQualified, bySimple, out var owner) ? owner : null; + } + + return ResolveBareRedefinitionOwner(def, redefinedRef, byQualified, bySimple, defByQualified, new HashSet(StringComparer.Ordinal)); + } + + /// + /// Walks a definition's supertype chain (transitively, with a cycle guard) looking for a + /// definition that declares a member feature with the given simple name. + /// + private static string? ResolveBareRedefinitionOwner( + DefBox def, + string bareName, + HashSet byQualified, + Dictionary bySimple, + IReadOnlyDictionary defByQualified, + HashSet visited) + { + if (!visited.Add(def.QualifiedName)) + { + return null; + } + + foreach (var supertype in def.SupertypeNames) + { + if (!TryResolveQualified(supertype, byQualified, bySimple, out var superQualified) || + !defByQualified.TryGetValue(superQualified, out var superDef)) + { + continue; + } + + if (superDef.Memberships.Any(m => m.Name == bareName)) + { + return superDef.QualifiedName; + } + + var found = ResolveBareRedefinitionOwner(superDef, bareName, byQualified, bySimple, defByQualified, visited); + if (found is not null) + { + return found; + } + } + + return null; + } + /// Resolves a supertype/type reference to a definition's qualified name, by qualified then simple name. private static bool TryResolveQualified( string reference, diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs index f21a8805..a95be87a 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/AstBuilder.cs @@ -683,6 +683,8 @@ private static (string? A, string? B) ExtractConnectorEnds(SysMLv2Parser.Connect var decl = usage.usageDeclaration(); var name = GetDeclaredName(decl?.identification()); var typing = ExtractFeatureTyping(decl?.featureSpecializationPart()); + var redefined = ExtractRedefinedFeature(decl?.featureSpecializationPart()); + var supertypeNames = ExtractSubsettingTargetNames(decl?.featureSpecializationPart()); var multiplicity = ExtractMultiplicity(decl?.featureSpecializationPart()); // Named usages contribute a namespace segment for any nested usages they own. @@ -711,6 +713,8 @@ private static (string? A, string? B) ExtractConnectorEnds(SysMLv2Parser.Connect QualifiedName = qualifiedName, FeatureKeyword = keyword, FeatureTyping = typing, + RedefinedFeatureName = redefined, + SupertypeNames = supertypeNames, Multiplicity = multiplicity, Children = children, Annotations = annotations, @@ -773,6 +777,91 @@ private static (string? A, string? B) ExtractConnectorEnds(SysMLv2Parser.Connect return ft.qualifiedName()?.GetText(); } + /// + /// Extracts the first redefined-feature raw reference text from a feature specialization + /// part (the target that follows redefines/:>>), or null when the + /// feature declares no redefinition. + /// + private static string? ExtractRedefinedFeature(SysMLv2Parser.FeatureSpecializationPartContext? fsp) + { + if (fsp is null) + { + return null; + } + + foreach (var fs in fsp.featureSpecialization()) + { + var redefinitions = fs.redefinitions(); + if (redefinitions is null) + { + continue; + } + + // The first redefined feature is held by the redefines clause; additional + // redefinitions follow as a list. + var fromRedefines = redefinitions.redefines()?.ownedRedefinition(); + if (fromRedefines is not null) + { + return fromRedefines.GetText(); + } + + var fromList = redefinitions.ownedRedefinition().FirstOrDefault(owned => owned is not null); + if (fromList is not null) + { + return fromList.GetText(); + } + } + + return null; + } + + /// + /// Extracts the raw reference text of every subsets <target>;/:> + /// <target> clause on a usage/feature — a subsetting reference, grammatically + /// distinct from a redefinition () even though both + /// share the same featureSpecialization alternative structure. Mirrors + /// 's structure for pulling both the first (held by + /// the subsets clause) and any subsequent comma-separated targets. Populates + /// SysmlFeatureNode.SupertypeNames so that a usage-level :> (e.g. + /// part vehicle1_c1 :> vehicle1) is resolved into a edge the same uniform way a definition-level :> + /// already is, which 's bare-name redefinition + /// ancestor-chain walk depends on to reach an inherited member through a usage's own + /// subsetting ancestor. + /// + private static IReadOnlyList ExtractSubsettingTargetNames(SysMLv2Parser.FeatureSpecializationPartContext? fsp) + { + if (fsp is null) + { + return Array.Empty(); + } + + var names = new List(); + foreach (var fs in fsp.featureSpecialization()) + { + var subsettingPart = fs.subsettings(); + if (subsettingPart is null) + { + continue; + } + + // The first subsetting target is held by the `subsets`/`:>` clause; additional + // targets follow as a comma-separated list. + var fromSubsets = subsettingPart.subsets()?.ownedSubsetting(); + if (fromSubsets is not null) + { + names.Add(fromSubsets.GetText()); + } + + foreach (var owned in subsettingPart.ownedSubsetting()) + { + names.Add(owned.GetText()); + } + } + + return names; + } + /// /// Extracts the multiplicity text (e.g. [4]) from a feature specialization part, /// or null when no multiplicity is declared. diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs index d0931be0..bebc4b24 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/ReferenceResolver.cs @@ -39,6 +39,43 @@ public ReferenceResolver(SymbolTable symbolTable, List diagnost /// A over all resolved edges discovered while walking /// . /// + /// + /// Resolution runs in three passes over : + /// + /// + /// Pass 1 () resolves every ordinary + /// namespace/import-scoped reference (supertype, typing, redefinition, import, + /// verify, satisfy, allocate, expose) for every file root. Any bare-name + /// redefines/:>> target or usage-level subsets/:> + /// supertype name that the plain namespace/import lookup cannot resolve is + /// not warned about yet — it is instead recorded as a pending candidate, + /// because resolving it requires walking an ancestor's + /// , which may not yet be populated if that + /// ancestor has not been visited yet by this same single top-to-bottom, + /// document-order DFS (declared later in the same file, or in a file processed + /// later in ). + /// + /// + /// Pass 2 () resolves every pending + /// candidate collected by pass 1, once, after pass 1 has completed for + /// every file root — so every ancestor's Supertype/ + /// Redefinition edges from ordinary pass 1 resolution are already attached, + /// regardless of declaration order. This mirrors the existing feature-chain + /// precedent below exactly: a single extra pass after pass 1 is sufficient because + /// the only edges this walk depends on are the ones pass 1 always attaches via + /// plain . Diagnostics for names that still cannot be + /// resolved are emitted here instead. + /// + /// + /// Pass 3 () resolves dotted feature + /// chains (connection/message endpoints, transition source/target) into + /// Connect/Transition edges — see that method's remarks for why it, too, must run + /// after pass 1 completes for all file roots. Passes 2 and 3 resolve disjoint edge + /// kinds and neither depends on the other's output, so their relative order does + /// not matter. + /// + /// + /// public SemanticIndex ResolveAll(IEnumerable<(string FilePath, SysmlNode? Root)> fileRoots) { // Build import graph first @@ -48,20 +85,36 @@ public SemanticIndex ResolveAll(IEnumerable<(string FilePath, SysmlNode? Root)> // Detect circular imports DetectCircularImports(importGraph); - // Resolve references in each file using the per-file import context, accumulating edges + // Shared per-file "already warned about this name" deduplication sets, keyed by file + // path. Promoted out of the pass-1 loop below so pass 2 (ResolveBareRedefinitions) + // reuses the *same* set for a given file, avoiding duplicate warnings for a name that + // pass 1 could not resolve immediately and pass 2 also fails to resolve. + var resolvedInFileByPath = new Dictionary>(StringComparer.Ordinal); + + // Pass 1: resolve every ordinary namespace/import-scoped reference for all file roots, + // accumulating edges. Bare-name redefinition/usage-subsetting candidates that cannot be + // resolved immediately are collected into pendingBareRedefinitions instead of being + // warned about right away (see the ResolveAll remarks above). var edges = new List(); + var pendingBareRedefinitions = new List(); foreach (var (filePath, root) in fileRootsList.Where(r => r.Root is not null)) { var imports = CollectImportNodes(root!); - ResolveNode(root!, filePath, new HashSet(), new List(), imports, edges); + var resolvedInFile = GetOrAddResolvedInFileSet(resolvedInFileByPath, filePath); + ResolveNode(root!, filePath, resolvedInFile, new List(), imports, edges, pendingBareRedefinitions); } - // Pass 2: resolve dotted feature chains (connection/message endpoints, transition - // source/target) into Connect/Transition edges. This must run as a second pass, after - // pass 1 has resolved Typing/Supertype edges for ALL file roots, because a chain walk - // may depend on a Typing/Supertype edge belonging to a node visited later in document - // order within the same file, or to a node in a different file entirely (see - // "Feature-Chain Resolution" in the design doc for the full rationale). + // Pass 2: resolve the bare-name redefinition/usage-subsetting candidates collected by + // pass 1, now that every file root's ordinary Supertype/Redefinition edges are attached + // (see the ResolveAll remarks above for the full rationale). + ResolveBareRedefinitions(pendingBareRedefinitions, resolvedInFileByPath, edges); + + // Pass 3: resolve dotted feature chains (connection/message endpoints, transition + // source/target) into Connect/Transition edges. This must run after pass 1 has resolved + // Typing/Supertype edges for ALL file roots, because a chain walk may depend on a + // Typing/Supertype edge belonging to a node visited later in document order within the + // same file, or to a node in a different file entirely (see "Feature-Chain Resolution" in + // the design doc for the full rationale). foreach (var (filePath, root) in fileRootsList.Where(r => r.Root is not null)) { var imports = CollectImportNodes(root!); @@ -71,6 +124,110 @@ public SemanticIndex ResolveAll(IEnumerable<(string FilePath, SysmlNode? Root)> return new SemanticIndex(edges); } + /// + /// Gets the existing per-file "already warned about this name" deduplication set for + /// , or creates and registers a new empty one if none exists + /// yet. Shared by (pass 1) and + /// (pass 2) so a name that pass 1 could not resolve + /// immediately, and that pass 2 also fails to resolve, is warned about exactly once. + /// + private static HashSet GetOrAddResolvedInFileSet( + Dictionary> resolvedInFileByPath, + string filePath) + { + if (!resolvedInFileByPath.TryGetValue(filePath, out var resolvedInFile)) + { + resolvedInFile = new HashSet(StringComparer.Ordinal); + resolvedInFileByPath[filePath] = resolvedInFile; + } + + return resolvedInFile; + } + + /// + /// A bare-name redefines/:>> target, or usage-level + /// subsets/:> supertype name, that pass 1's plain namespace/import-scoped + /// lookup could not resolve. Recorded by + /// and resolved later by , once every file root's + /// ordinary edges have been attached. + /// + /// + /// The qualified name of the node that owns/declares the redefining or subsetting + /// feature — i.e. the enclosing namespace scope in effect when the candidate was + /// recorded — whose ancestor chain must be walked to find . + /// + /// The bare (unqualified) name to resolve. + /// The edge kind ( for usage-level + /// subsetting, for redefines) to record on + /// success. + /// + /// The qualified name of the redefining/subsetting feature itself, used as the edge's + /// subject on success. + /// + /// Source file path used when constructing a diagnostic on failure. + private sealed record PendingBareRedefinition( + string OwnerQualifiedName, + string Name, + SysmlEdgeKind Kind, + string SubjectQualifiedName, + string FilePath); + + /// + /// Resolves every bare-name redefinition/usage-subsetting candidate collected by pass 1 + /// () via the ancestor-chain walk in + /// . Must run once, after pass 1 has completed for + /// every file root, so every ancestor's own / + /// edges — attached by pass 1's ordinary + /// resolution — are populated regardless of the declaration order + /// of the ancestor relative to the descendant doing the walking (see the + /// remarks for the full rationale). A candidate that still cannot + /// be resolved here produces the "Unresolved reference" Warning diagnostic that pass 1 + /// deferred. + /// + /// The candidates collected by pass 1. + /// + /// Shared per-file "already warned about this name" deduplication sets, keyed by file + /// path (see ). + /// + /// + /// Aggregate list of all edges resolved so far across the whole file-root traversal; + /// appended to by this method on success. + /// + private void ResolveBareRedefinitions( + List pending, + Dictionary> resolvedInFileByPath, + List edges) + { + foreach (var candidate in pending) + { + if (TryResolveBareRedefinition(candidate.OwnerQualifiedName, candidate.Name, out var resolvedName)) + { + var edge = new SysmlEdge(candidate.SubjectQualifiedName, resolvedName, candidate.Kind); + edges.Add(edge); + + var subjectNode = _symbolTable.Lookup(candidate.SubjectQualifiedName); + if (subjectNode is not null) + { + subjectNode.ResolvedEdges = subjectNode.ResolvedEdges.Count > 0 + ? [.. subjectNode.ResolvedEdges, edge] + : [edge]; + } + + continue; + } + + var resolvedInFile = GetOrAddResolvedInFileSet(resolvedInFileByPath, candidate.FilePath); + if (resolvedInFile.Add(candidate.Name)) + { + _diagnostics.Add(new SysmlDiagnostic( + candidate.FilePath, + 0, 0, + DiagnosticSeverity.Warning, + $"Unresolved reference: '{candidate.Name}'")); + } + } + } + /// /// Builds an import graph mapping each top-level namespace name to the set of namespace /// names it imports. The file-level root is a nameless container; we key on the @@ -342,7 +499,13 @@ private string ResolveNamespaceName(string ns, IReadOnlyList namespaceSt /// Resolves supertype, feature-typing, and import references in the given AST node and /// its descendants, emitting a Warning diagnostic for each name that cannot be resolved /// through the four-step lookup and recording a for each name - /// that does resolve. + /// that does resolve. A bare-name redefines/:>> target or usage-level + /// subsets/:> supertype name that this method's plain + /// namespace/import-scoped lookup cannot resolve is neither warned about nor dropped here + /// — it is instead appended to for + /// to resolve in a later pass, once every file + /// root's ordinary edges are attached (see the remarks for the + /// full rationale). /// /// The AST node to process. /// Source file path used when constructing diagnostics. @@ -361,23 +524,46 @@ private string ResolveNamespaceName(string ns, IReadOnlyList namespaceSt /// appended to by this method, mirroring and /// . /// + /// + /// Aggregate list of bare-name redefinition/usage-subsetting candidates that could not be + /// resolved immediately; appended to by this method, resolved later by + /// . + /// private void ResolveNode( SysmlNode node, string filePath, HashSet resolvedInFile, List namespaceStack, IReadOnlyList imports, - List edges) + List edges, + List pendingBareRedefinitions) { var nodeEdges = new List(); - // Resolve each supertype name using the current namespace context and file imports + // Resolve each supertype name using the current namespace context and file imports. For + // a usage/feature node (SysmlFeatureNode), a SupertypeNames entry may come from a usage- + // level `subsets`/`:>` clause — which, like a bare `redefines` reference, commonly names + // a member the owner *inherits* (declared on a supertype/redefinition ancestor) rather + // than one declared/imported into the same lexical scope — so such a name is deferred to + // the bare-redefinition ancestor-chain fallback (see ResolveBareRedefinitions) rather than + // warned about immediately here. Definition-level SupertypeNames (from `part def X :> Y`) + // name the supertype itself directly, not an inherited member, so no fallback applies and + // an immediate warning is correct for non-feature nodes. foreach (var supertypeName in node.SupertypeNames) { if (TryResolve(supertypeName, namespaceStack, imports, out var resolvedSupertype)) { nodeEdges.Add(new SysmlEdge(node.QualifiedName, resolvedSupertype, SysmlEdgeKind.Supertype)); } + else if (node is SysmlFeatureNode) + { + pendingBareRedefinitions.Add(new PendingBareRedefinition( + string.Join("::", namespaceStack), + supertypeName, + SysmlEdgeKind.Supertype, + node.QualifiedName ?? string.Empty, + filePath)); + } else if (resolvedInFile.Add(supertypeName)) { _diagnostics.Add(new SysmlDiagnostic( @@ -405,6 +591,30 @@ private void ResolveNode( } } + // Feature redefinition — the target referenced after 'redefines'/':>>' on a usage/feature + // element. The standard namespace/import-scoped TryResolve is tried first (handles + // qualified forms like "Vehicle::eng" and same-scope bare names); since the dominant + // real-world shape for `redefines` is a bare name referencing a member the owner + // *inherits* rather than declares/imports itself, a failure here is deferred to the + // bare-redefinition ancestor-chain fallback (see ResolveBareRedefinitions) rather than + // warned about immediately. + if (node is SysmlFeatureNode { RedefinedFeatureName: { } redefined }) + { + if (TryResolve(redefined, namespaceStack, imports, out var resolvedRedefined)) + { + nodeEdges.Add(new SysmlEdge(node.QualifiedName, resolvedRedefined, SysmlEdgeKind.Redefinition)); + } + else + { + pendingBareRedefinitions.Add(new PendingBareRedefinition( + string.Join("::", namespaceStack), + redefined, + SysmlEdgeKind.Redefinition, + node.QualifiedName ?? string.Empty, + filePath)); + } + } + // Imports — uniform with SupertypeNames, now that ImportedNames is populated by AstBuilder foreach (var importedName in node.ImportedNames) { @@ -582,7 +792,7 @@ private void ResolveNode( foreach (var child in node.Children) { - ResolveNode(child, filePath, resolvedInFile, namespaceStack, imports, edges); + ResolveNode(child, filePath, resolvedInFile, namespaceStack, imports, edges, pendingBareRedefinitions); } if (pushed) @@ -845,4 +1055,98 @@ private bool TryResolveFeatureChain( return null; } + + /// + /// Attempts to resolve a bare-name redefines/:>> target (or + /// usage-level subsets/:> supertype name) that + /// could not resolve because the referenced member is inherited from an ancestor + /// (declared on a supertype, or reachable only by following the owner's own + /// already-resolved edge) rather than declared + /// or imported into the same lexical scope as the redefining/subsetting feature. This is + /// the dominant real-world shape for redefines per the SysML v2 spec — its entire + /// purpose is referencing an inherited member — so this fallback is tried by + /// whenever the standard namespace/import-scoped + /// lookup fails in pass 1. Called only from pass 2, after every + /// file root's ordinary / + /// edges have been attached, so the ancestor + /// chain walk below sees a fully-populated for + /// every ancestor regardless of that ancestor's declaration order relative to + /// . + /// + /// + /// The fully-qualified name of the node that owns/declares the redefining or subsetting + /// feature, whose ancestor chain is walked to find . + /// + /// The bare (unqualified) name to resolve. + /// + /// When this method returns , the fully-qualified name of the + /// matched ancestor member. When it returns , set to + /// . + /// + /// + /// if an ancestor member named was found; + /// otherwise. + /// + private bool TryResolveBareRedefinition( + string ownerQualifiedName, + string name, + out string resolvedName) + { + var ownerNode = _symbolTable.Lookup(ownerQualifiedName); + var found = ownerNode is null ? null : FindMemberInAncestorChain(ownerNode, name, new HashSet()); + + if (found?.QualifiedName is not { Length: > 0 } foundQualifiedName) + { + resolvedName = string.Empty; + return false; + } + + resolvedName = foundQualifiedName; + return true; + } + + /// + /// Finds a member named in 's own direct + /// children, or — recursively — in any ancestor reachable by following its + /// and + /// resolved edges. Both edge kinds must be followed (not supertype-only) because a nested + /// redefining feature's owner sometimes has no SupertypeNames of its own at all — + /// the only path to the inherited member is via the owner's own already-resolved + /// Redefinition edge (see 1c-PartsTreeRedefinition.sysml's nested + /// frontAxle_c1 redefines frontAxle, whose owner frontAxleAssembly_c1 only + /// redefines frontAxleAssembly and has no supertype of its own). + /// guards against cycles, keyed on qualified name, mirroring + /// 's guard pattern. + /// + private SysmlNode? FindMemberInAncestorChain(SysmlNode node, string name, HashSet visited) + { + if (node.QualifiedName is { Length: > 0 } qualifiedName && !visited.Add(qualifiedName)) + { + return null; + } + + var direct = node.Children.FirstOrDefault(c => c.Name == name); + if (direct is not null) + { + return direct; + } + + foreach (var edge in node.ResolvedEdges.Where( + e => e.Kind is SysmlEdgeKind.Supertype or SysmlEdgeKind.Redefinition)) + { + var ancestorNode = _symbolTable.Lookup(edge.TargetQualifiedName); + if (ancestorNode is null) + { + continue; + } + + var found = FindMemberInAncestorChain(ancestorNode, name, visited); + if (found is not null) + { + return found; + } + } + + return null; + } } diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlEdge.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlEdge.cs index 184e2e43..fdd85e82 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlEdge.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlEdge.cs @@ -77,6 +77,13 @@ public enum SysmlEdgeKind /// the pre-scoping baseline. /// Expose, + + /// + /// A feature redefinition reference ( / + /// redefines X / :>> X), from the redefining feature's owner to the + /// resolved redefined-feature reference. + /// + Redefinition, } /// diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs index 496ff7a2..1283fdf9 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs @@ -140,6 +140,13 @@ public sealed class SysmlFeatureNode : SysmlNode /// public string? FeatureTyping { get; init; } + /// + /// Gets the raw reference text of this feature's redefines <target>;/ + /// :>> <target> clause, or null when the feature declares no + /// redefinition. + /// + public string? RedefinedFeatureName { get; init; } + /// /// Gets the multiplicity text (e.g., "[4]", "[0..*]"), or null when unspecified. /// diff --git a/src/DemaConsulting.SysML2Tools.Stdlib/Resources/stdlib.json.gz b/src/DemaConsulting.SysML2Tools.Stdlib/Resources/stdlib.json.gz index f857ad42..b4d4994e 100644 Binary files a/src/DemaConsulting.SysML2Tools.Stdlib/Resources/stdlib.json.gz and b/src/DemaConsulting.SysML2Tools.Stdlib/Resources/stdlib.json.gz differ diff --git a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs index 03851be4..cf75363e 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Layout/GeneralViewLayoutStrategyTests.cs @@ -478,6 +478,251 @@ public void GeneralViewLayoutStrategy_BuildLayout_AttributeTyping_ProducesDashed Assert.Null(diamondEdge); } + /// + /// A subtype feature that redefines a bare-named inherited feature (declared on a resolved + /// supertype in the view) emits a solid hollow-triangle-with-crossbar line from the subtype + /// to the supertype that declares the redefined feature. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_BareNameRedefinition_ProducesHollowTriangleCrossbarEdge() + { + // Arrange: Vehicle declares "eng"; SmallVehicle specializes Vehicle and redefines "eng" by + // bare name (no Owner:: qualifier). + var strategy = new GeneralViewLayoutStrategy(); + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["P::Vehicle"] = new SysmlDefinitionNode + { + Name = "Vehicle", + QualifiedName = "P::Vehicle", + DefinitionKeyword = "part def", + Children = + [ + new SysmlFeatureNode { Name = "eng", QualifiedName = "P::Vehicle::eng", FeatureKeyword = "attribute", FeatureTyping = "Real" } + ] + }, + ["P::SmallVehicle"] = new SysmlDefinitionNode + { + Name = "SmallVehicle", + QualifiedName = "P::SmallVehicle", + DefinitionKeyword = "part def", + SupertypeNames = ["Vehicle"], + Children = + [ + new SysmlFeatureNode { Name = "smallEng", QualifiedName = "P::SmallVehicle::smallEng", FeatureKeyword = "attribute", FeatureTyping = "Real", RedefinedFeatureName = "eng" } + ] + } + } + }; + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: a solid line with a hollow-triangle-crossbar arrowhead exists, from SmallVehicle to Vehicle + var redefinitionEdge = CollectLines(layout.Nodes) + .FirstOrDefault(l => l.TargetEnd == EndMarkerStyle.HollowTriangleCrossbar); + Assert.NotNull(redefinitionEdge); + Assert.Equal(LineStyle.Solid, redefinitionEdge!.LineStyle); + } + + /// + /// A subtype feature that redefines a qualified Owner::feature reference emits a + /// hollow-triangle-with-crossbar edge to the named owner, without needing to walk the + /// supertype chain. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_QualifiedRedefinition_ProducesHollowTriangleCrossbarEdgeToOwner() + { + // Arrange: Car redefines "Vehicle::mass" directly by qualified reference. + var strategy = new GeneralViewLayoutStrategy(); + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["P::Vehicle"] = new SysmlDefinitionNode + { + Name = "Vehicle", + QualifiedName = "P::Vehicle", + DefinitionKeyword = "part def", + Children = + [ + new SysmlFeatureNode { Name = "mass", QualifiedName = "P::Vehicle::mass", FeatureKeyword = "attribute", FeatureTyping = "Real" } + ] + }, + ["P::Car"] = new SysmlDefinitionNode + { + Name = "Car", + QualifiedName = "P::Car", + DefinitionKeyword = "part def", + SupertypeNames = ["Vehicle"], + Children = + [ + new SysmlFeatureNode { Name = "carMass", QualifiedName = "P::Car::carMass", FeatureKeyword = "attribute", FeatureTyping = "Real", RedefinedFeatureName = "Vehicle::mass" } + ] + } + } + }; + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: a hollow-triangle-crossbar arrowhead edge is emitted, resolved via the qualified reference + var redefinitionEdge = CollectLines(layout.Nodes) + .FirstOrDefault(l => l.TargetEnd == EndMarkerStyle.HollowTriangleCrossbar); + Assert.NotNull(redefinitionEdge); + } + + /// + /// An unresolvable redefinition reference (neither a qualified owner nor a bare name found + /// anywhere in the supertype chain) produces no redefinition edge and does not throw. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_UnresolvableRedefinition_ProducesNoEdge() + { + // Arrange: Vehicle redefines a bare name that does not exist anywhere in scope. + var strategy = new GeneralViewLayoutStrategy(); + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["P::Vehicle"] = new SysmlDefinitionNode + { + Name = "Vehicle", + QualifiedName = "P::Vehicle", + DefinitionKeyword = "part def", + Children = + [ + new SysmlFeatureNode { Name = "eng", QualifiedName = "P::Vehicle::eng", FeatureKeyword = "attribute", FeatureTyping = "Real", RedefinedFeatureName = "nonExistentFeature" } + ] + } + } + }; + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light); + + // Act: laying out must not throw even though the redefinition cannot be resolved. + var layout = strategy.BuildLayout(context, options); + + // Assert: no hollow-triangle-crossbar edge is produced. + var redefinitionEdge = CollectLines(layout.Nodes) + .FirstOrDefault(l => l.TargetEnd == EndMarkerStyle.HollowTriangleCrossbar); + Assert.Null(redefinitionEdge); + } + + /// + /// A bare-name redefinition whose declaring ancestor is two supertype hops away + /// (Mid :> Parent :> GrandParent, with GrandParent declaring the redefined + /// member) produces a hollow-triangle-crossbar edge targeting the actual declaring + /// ancestor (GrandParent), not the immediate supertype (Parent) — proving the + /// bare-name walk is transitive, not limited to a single hop. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_TransitiveBareNameRedefinition_ProducesHollowTriangleCrossbarEdgeToDeclaringAncestor() + { + // Arrange: GrandParent declares "feat"; Parent specializes GrandParent with no members of + // its own; Mid specializes Parent and redefines "feat" by bare name. + var strategy = new GeneralViewLayoutStrategy(); + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["P::GrandParent"] = new SysmlDefinitionNode + { + Name = "GrandParent", + QualifiedName = "P::GrandParent", + DefinitionKeyword = "part def", + Children = + [ + new SysmlFeatureNode { Name = "feat", QualifiedName = "P::GrandParent::feat", FeatureKeyword = "attribute", FeatureTyping = "Real" } + ] + }, + ["P::Parent"] = new SysmlDefinitionNode + { + Name = "Parent", + QualifiedName = "P::Parent", + DefinitionKeyword = "part def", + SupertypeNames = ["GrandParent"], + }, + ["P::Mid"] = new SysmlDefinitionNode + { + Name = "Mid", + QualifiedName = "P::Mid", + DefinitionKeyword = "part def", + SupertypeNames = ["Parent"], + Children = + [ + new SysmlFeatureNode { Name = "subFeat", QualifiedName = "P::Mid::subFeat", FeatureKeyword = "attribute", FeatureTyping = "Real", RedefinedFeatureName = "feat" } + ] + } + } + }; + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light); + + // Act + var layout = strategy.BuildLayout(context, options); + + // Assert: the crossbar edge targets GrandParent (the actual declaring ancestor), and there + // is exactly one such edge (not one for each hop of the chain). + var redefinitionEdges = CollectLines(layout.Nodes) + .Where(l => l.TargetEnd == EndMarkerStyle.HollowTriangleCrossbar) + .ToList(); + Assert.Single(redefinitionEdges); + Assert.Equal(LineStyle.Solid, redefinitionEdges[0].LineStyle); + } + + /// + /// A genuinely self-referential redefinition — a definition whose own supertype chain + /// cycles back to itself, such that the bare-name walk resolves the redefined member's + /// owner back to the very definition doing the redefining — produces no redefinition edge + /// and does not throw. This is distinct from + /// , + /// which covers a name that cannot be found anywhere, not a name that resolves back to the + /// definition itself. + /// + [Fact] + public void GeneralViewLayoutStrategy_BuildLayout_SelfReferentialRedefinition_ProducesNoEdge() + { + // Arrange: Standalone lists itself as its own supertype (a self-cycle), and redefines its + // own "otherFeat" member by bare name — so the walk resolves the owner back to Standalone. + var strategy = new GeneralViewLayoutStrategy(); + var workspace = new SysmlWorkspace + { + Declarations = new Dictionary + { + ["P::Standalone"] = new SysmlDefinitionNode + { + Name = "Standalone", + QualifiedName = "P::Standalone", + DefinitionKeyword = "part def", + SupertypeNames = ["Standalone"], + Children = + [ + new SysmlFeatureNode { Name = "selfFeat", QualifiedName = "P::Standalone::selfFeat", FeatureKeyword = "attribute", FeatureTyping = "Real", RedefinedFeatureName = "otherFeat" }, + new SysmlFeatureNode { Name = "otherFeat", QualifiedName = "P::Standalone::otherFeat", FeatureKeyword = "attribute", FeatureTyping = "Real" } + ] + } + } + }; + var context = new ViewContext("v", workspace); + var options = new RenderOptions(Themes.Light); + + // Act: laying out must not throw despite the self-referential supertype cycle. + var layout = strategy.BuildLayout(context, options); + + // Assert: no hollow-triangle-crossbar edge is produced, since the resolved owner is + // Standalone itself. + var redefinitionEdge = CollectLines(layout.Nodes) + .FirstOrDefault(l => l.TargetEnd == EndMarkerStyle.HollowTriangleCrossbar); + Assert.Null(redefinitionEdge); + } + /// /// A definition with TWO attribute-typed features of the SAME in-view type produces two /// identical owner→type intra-group edges. The layered pipeline de-duplicates the identical diff --git a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs index ceeb7f37..d4c93c1f 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Semantic/WorkspaceLoaderTests.cs @@ -847,6 +847,512 @@ part def X { } } + /// + /// A feature using the redefines keyword form should capture the raw redefined-feature + /// reference text in . + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_RedefinesKeyword_CapturesRedefinedFeatureName() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + part def Vehicle { + attribute eng : Real; + } + part def SmallVehicle :> Vehicle { + attribute smallEng : Real redefines eng; + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var smallVehicle = Assert.IsType( + result.Workspace!.Declarations["SmallVehicle"]); + var smallEng = smallVehicle.Children + .OfType() + .First(f => f.Name == "smallEng"); + + Assert.Equal("eng", smallEng.RedefinedFeatureName); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A feature using the :>> operator form should capture the same raw + /// redefined-feature reference text as the redefines keyword form. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_ColonGtGtOperator_CapturesRedefinedFeatureName() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + part def Vehicle { + attribute eng : Real; + } + part def SmallVehicle :> Vehicle { + attribute smallEng : Real :>> eng; + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var smallVehicle = Assert.IsType( + result.Workspace!.Declarations["SmallVehicle"]); + var smallEng = smallVehicle.Children + .OfType() + .First(f => f.Name == "smallEng"); + + Assert.Equal("eng", smallEng.RedefinedFeatureName); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A qualified redefinition reference (Owner::feature) should capture the raw + /// qualified text verbatim, without any resolution applied at the AST-building stage. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_QualifiedRedefinition_CapturesRawText() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + part def Vehicle { + attribute mass : Real; + } + part def Car :> Vehicle { + attribute carMass : Real redefines Vehicle::mass; + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var car = Assert.IsType( + result.Workspace!.Declarations["Car"]); + var carMass = car.Children + .OfType() + .First(f => f.Name == "carMass"); + + Assert.Equal("Vehicle::mass", carMass.RedefinedFeatureName); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A feature declaring no redefinition should leave + /// + /// null. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_NoRedefinition_RedefinedFeatureNameIsNull() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + part def Vehicle { + attribute mass : Real; + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var vehicle = Assert.IsType( + result.Workspace!.Declarations["Vehicle"]); + var mass = vehicle.Children + .OfType() + .First(f => f.Name == "mass"); + + Assert.Null(mass.RedefinedFeatureName); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A resolved redefined-feature reference should be recorded as a Redefinition edge + /// in the workspace's , queryable from both directions. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_ResolvedRedefinition_RecordsRedefinitionEdge() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + attribute eng : Real; + part def SmallVehicle { + attribute smallEng : Real redefines eng; + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var outgoing = result.Workspace!.Index.GetOutgoingEdges("SmallVehicle::smallEng"); + Assert.Contains(outgoing, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Redefinition && + e.TargetQualifiedName == "eng"); + + var incoming = result.Workspace.Index.GetIncomingEdges("eng"); + Assert.Contains(incoming, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Redefinition && + e.SourceQualifiedName == "SmallVehicle::smallEng"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A feature redefining a non-existent feature should produce a Warning diagnostic (same + /// message format as unresolved supertype/typing references) and must not produce a + /// Redefinition edge for that reference. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_UnresolvedRedefinition_ProducesWarningNoEdge() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + package P { + part def X { + attribute y : Real redefines NonExistentFeature; + } + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + Assert.Contains(result.Diagnostics, + d => d.Severity == DemaConsulting.SysML2Tools.Parser.DiagnosticSeverity.Warning && + d.Message.Contains("NonExistentFeature")); + Assert.DoesNotContain(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Redefinition && + e.TargetQualifiedName == "NonExistentFeature"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// A bare-name redefines eng where eng is a member declared only on the + /// redefining feature's owner's supertype (not on the owner itself, and not + /// imported) — the dominant real-world shape per the SysML v2 spec — should still + /// resolve to a Redefinition edge, and must not produce a false + /// "Unresolved reference" Warning diagnostic. Mirrors + /// 05.Redefinition/RedefinitionExample.sysml's SmallVehicle::smallEng + /// redefines eng shape exactly. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_BareRedefinitionOfInheritedFeature_RecordsRedefinitionEdgeNoWarning() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + part def Engine; + part def SmallEngine :> Engine; + + part def Vehicle { + part eng : Engine; + } + + part def SmallVehicle :> Vehicle { + part smallEng : SmallEngine redefines eng; + } + """, 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.Redefinition && + e.SourceQualifiedName == "SmallVehicle::smallEng" && + e.TargetQualifiedName == "Vehicle::eng"); + Assert.DoesNotContain(result.Diagnostics, + d => d.Severity == DemaConsulting.SysML2Tools.Parser.DiagnosticSeverity.Warning && + d.Message.Contains("eng")); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Loading the OMG 05.Redefinition/RedefinitionExample.sysml training fixture + /// should not produce any false "Unresolved reference" Warning diagnostics — regression + /// coverage for the bare-name redefines eng/redefines cyl inherited-member + /// forms that previously failed to resolve. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_RedefinitionExampleFixture_NoUnresolvedReferenceWarnings() + { + // Arrange + var modelsRoot = FindSysMLModelsRoot(); + if (modelsRoot is null) + { + return; + } + + var fixturePath = Path.Combine(modelsRoot, "OMG", "training", "05.Redefinition", "RedefinitionExample.sysml"); + if (!File.Exists(fixturePath)) + { + return; + } + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([fixturePath], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + Assert.DoesNotContain(result.Diagnostics, + d => d.Severity == DemaConsulting.SysML2Tools.Parser.DiagnosticSeverity.Warning && + d.Message.StartsWith("Unresolved reference:", StringComparison.Ordinal)); + } + + /// + /// Loading the OMG 1c-PartsTreeRedefinition.sysml validation fixture should not + /// produce any false "Unresolved reference" Warning diagnostics for the redefined/ + /// subsetting feature names in this fixture — regression coverage for the nested + /// bare-name redefines frontAxleAssembly/redefines frontAxle/redefines + /// rearAxleAssembly/redefines rearAxle forms (where the innermost owner, e.g. + /// frontAxleAssembly_c1, has no supertype of its own and the inherited member is + /// only reachable via the owner's own Redefinition edge), plus the sibling + /// bare-name subsets frontWheel/subsets rearWheel forms. Pre-existing, + /// unrelated stdlib-coverage gaps in this fixture (SI::kg, ISQ::mass) are + /// intentionally excluded from this assertion — this test targets only the + /// redefinition-resolution regression, not full-fixture zero-warning coverage. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_1cPartsTreeRedefinitionFixture_NoUnresolvedReferenceWarnings() + { + // Arrange + var modelsRoot = FindSysMLModelsRoot(); + if (modelsRoot is null) + { + return; + } + + var fixturePath = Path.Combine( + modelsRoot, "OMG", "validation", "01-PartsTree", "1c-PartsTreeRedefinition.sysml"); + if (!File.Exists(fixturePath)) + { + return; + } + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([fixturePath], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + string[] previouslyFalseUnresolvedNames = + [ + "frontAxleAssembly", "frontAxle", "rearAxleAssembly", "rearAxle", "frontWheel", "rearWheel", + ]; + Assert.DoesNotContain(result.Diagnostics, + d => d.Severity == DemaConsulting.SysML2Tools.Parser.DiagnosticSeverity.Warning && + d.Message.StartsWith("Unresolved reference:", StringComparison.Ordinal) && + previouslyFalseUnresolvedNames.Any(name => d.Message.Contains($"'{name}'", StringComparison.Ordinal))); + } + + /// + /// A bare-name redefines feat where the ancestor declaring feat is two + /// supertype hops away (Mid :> Parent :> GrandParent), and the whole chain is + /// declared out of document order (Mid first, then Parent, then + /// GrandParent) — reproducing the reported single-pass hazard exactly: under the + /// old inline-fallback implementation, Mid's bare-name walk ran before + /// Parent/GrandParent had been visited by the same DFS pass, so their + /// ResolvedEdges were still empty and the walk silently failed, producing a false + /// "Unresolved reference" warning even though the reference is semantically valid. The + /// corrected two-pass resolution must still produce the Redefinition edge and no + /// warning regardless of this declaration order. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_OutOfOrderRedefinitionChain_RecordsRedefinitionEdgeNoWarning() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + part def Mid :> Parent { + attribute value : Real redefines feat; + } + part def Parent :> GrandParent; + part def GrandParent { + attribute feat : Real; + } + """, 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.Redefinition && + e.SourceQualifiedName == "Mid::value" && + e.TargetQualifiedName == "GrandParent::feat"); + Assert.DoesNotContain(result.Diagnostics, + d => d.Severity == DemaConsulting.SysML2Tools.Parser.DiagnosticSeverity.Warning && + d.Message.Contains("feat")); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// The same out-of-order ancestor-chain shape as + /// , + /// but split across two files, with the file containing the redefining feature listed + /// before the file containing its ancestors in 's + /// file-path array — exercising the cross-file variant of the same document-order hazard, + /// since pass 1 iterates file roots in call order. + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_CrossFileOutOfOrderRedefinitionChain_RecordsRedefinitionEdgeNoWarning() + { + // Arrange + var descendantFile = Path.GetTempFileName() + ".sysml"; + var ancestorsFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(descendantFile, """ + part def Mid :> Parent { + attribute value : Real redefines feat; + } + """, TestContext.Current.CancellationToken); + await File.WriteAllTextAsync(ancestorsFile, """ + part def Parent :> GrandParent; + part def GrandParent { + attribute feat : Real; + } + """, TestContext.Current.CancellationToken); + + // Act — descendant file listed first, ancestors file listed second + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([descendantFile, ancestorsFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + Assert.Contains(result.Workspace!.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Redefinition && + e.SourceQualifiedName == "Mid::value" && + e.TargetQualifiedName == "GrandParent::feat"); + Assert.DoesNotContain(result.Diagnostics, + d => d.Severity == DemaConsulting.SysML2Tools.Parser.DiagnosticSeverity.Warning && + d.Message.Contains("feat")); + } + finally + { + File.Delete(descendantFile); + File.Delete(ancestorsFile); + } + } + + /// + /// A usage/feature node's usage-level subsets/:> specialization (as + /// opposed to a definition-level part def X :> Y supertype) should directly + /// populate that feature node's + /// with the expected target name, and produce a resolved Supertype edge — a direct + /// assertion of the usage-level capture behavior, independent of any redefinition context + /// (previously only indirectly covered via the absence of a false warning in the OMG + /// fixture regression tests above). + /// + [Fact] + public async Task WorkspaceLoader_LoadAsync_UsageLevelSubsetting_PopulatesSupertypeNames() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".sysml"; + try + { + await File.WriteAllTextAsync(tempFile, """ + part def Thing { + part y : Thing; + part x : Thing subsets y; + } + """, TestContext.Current.CancellationToken); + + // Act + var (stdlibTable, _) = StdlibProvider.GetSymbolTable(); + var result = await WorkspaceLoader.LoadAsync([tempFile], stdlibTable); + + // Assert + Assert.NotNull(result.Workspace); + var thingNode = result.Workspace!.Declarations["Thing"]; + var xNode = Assert.Single(thingNode.Children, c => c.Name == "x"); + Assert.Contains("y", xNode.SupertypeNames); + Assert.Contains(result.Workspace.Index.AllEdges, + e => e.Kind == DemaConsulting.SysML2Tools.Semantic.Model.SysmlEdgeKind.Supertype && + e.SourceQualifiedName == "Thing::x" && + e.TargetQualifiedName == "Thing::y"); + } + finally + { + File.Delete(tempFile); + } + } + /// /// A wildcard import (import Other::*;) should be recorded as an Import /// edge whose target is the imported namespace, queryable via