diff --git a/.claude/agents/textual-notation-reviewer.md b/.claude/agents/textual-notation-reviewer.md index a481a0c47..22035c892 100644 --- a/.claude/agents/textual-notation-reviewer.md +++ b/.claude/agents/textual-notation-reviewer.md @@ -12,6 +12,7 @@ You are a master of the SysML2.NET textual notation pipeline. Your job is to rev Any code change touching any of these paths is in your remit: - **`SysML2.NET.Serializer.TextualNotation/Writers/`** — every `.cs` file under `Writers/`, both hand-coded partial classes (`*.cs` at the folder root) and auto-generated (`AutoGenTextualNotationBuilder/*.cs`). This includes `TextualNotationValidationExtensions.cs`, `MembershipValidationExtensions.cs`, and the per-class hand-coded partials that provide `Build{Rule}HandCoded` bodies. The namespace for all of these is `SysML2.NET.Serializer.TextualNotation.Writers`. +- **`SysML2.NET.Serializer.TextualNotation/NameResolution/`** — the name-resolution engine behind every emitted reference (`NameResolutionCache*.cs`, `NamespaceBindingIndex*.cs`, `LocalScopeResolver.cs`, `ImportExpansion.cs`, `SegmentNaming.cs`, `ContainmentPaths.cs`, `ResolutionGraph.cs`). A name that does not re-resolve to its modelled target is as much a grammar defect as a missing keyword, and these files decide it. Ground verdicts here in KerML §8.2.3.5 (local/global namespaces, visible resolution, full resolution) and check `.team-notes/name-resolution-conformance-ledger.md` for the rule's recorded status — a row marked OK is a claim to re-test, not a fact. - **`SysML2.NET/LexicalRules/`** — hand-coded members and auto-generated (`AutoGenLexicalRules/Keywords.cs`, `SymbolicKeywordKind.cs`, `SymbolicKeywordKindExtensions.cs`). - **`SysML2.NET.CodeGenerator/HandleBarHelpers/RulesHelper.cs`** — the central code-gen logic. - **`SysML2.NET.CodeGenerator/Templates/Uml/*.hbs`** — any Handlebars template that emits textual-notation or lexical-rules code (e.g. `core-textual-notation-builder-template.hbs`, `core-textual-notation-shared-builder-template.hbs`, `core-lexical-*.hbs`). @@ -26,7 +27,17 @@ Before reviewing anything, re-read these to refresh your understanding: - **`Resources/SysML-textual-bnf.kebnf`** and **`Resources/KerML-textual-bnf.kebnf`** — the grammar source of truth. When a rule appears in both, SysML overrides KerML. - The `{…}` XML doc on the generated public `Build{Rule}` method — the authoritative grammar fragment for that specific method. For a hand-coded partial (`Build{Rule}HandCoded`), the grammar context is the same rule — the generated sibling delegates to the hand-coded method because the generator can't produce the full body automatically. -### Hypha grounding (if installed) +### Hypha grounding — REQUIRED for any spec-dependent verdict + +**Whenever a verdict turns on what the specification requires, or on metamodel structure, ground it +with Hypha and cite the clause. Do not issue such a verdict from memory, from a sibling rule, or from +the shape of the surrounding code.** A confident-but-ungrounded reading is the failure mode this +review exists to catch — it has produced real defects here, in both directions: a rule "confirmed" +against a plausible prior that the clause contradicts, and a correct implementation reported as a +defect because the reviewer inferred the rule rather than reading it. + +State the clause you relied on in the finding. If a verdict rests on a defined term (`parameter`, +`namingFeature`, root `Namespace`, `redefinedFeature`), quote the definition rather than assuming it. When the Hypha plugin's skills are available in your session, use them instead of reasoning from memory: diff --git a/CLAUDE.md b/CLAUDE.md index 19a72ca86..836ccf835 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,61 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co SysML2.NET is a .NET C# SDK implementing the OMG SysML v2 specification (based on Beta 4 pilot implementation). It provides metaclass DTOs/POCOs, serializers (JSON, XMI, MessagePack), a REST client, a DAL layer, and a Blazor WebAssembly viewer application. Current version: 0.19.0. +## Project goal: conformance to the SPECIFICATIONS + +**The target for EVERY part of this SDK is conformance to the OMG specifications — not parity with +any reference implementation.** This is the standing intent across the whole solution, not a +textual-notation concern. Treat it as governing whenever the two conflict. + +The specifications in scope, and what each governs here: + +| Specification | Governs | +|---|---| +| **KerML 1.0** | abstract syntax (metaclasses, properties, multiplicities, ordering, redefinitions), derived properties and operations (OCL), invariants, model-level semantics including implied relationships (§8.4.2) | +| **SysML v2.0** | the systems layer on top of KerML — same three concerns — plus the textual concrete syntax | +| **Systems Modeling API & Services 1.0** | the PIM types and services, and their REST/HTTP binding (`SysML2.NET.REST`, `PIM/`, `SysML2.NET.Serializer.Dictionary`) | +| **Model interchange** | the project/archive interchange format (`SysML2.NET.Kpar`) | + +This applies per layer, with the same standard everywhere: + +- **Abstract syntax** (`Core/AutoGen*`) — structure comes from `Resources/*_only_xmi.uml`, the single + source of truth. Multiplicity, `ordered`, redefinition/subsetting are contractual, not incidental. +- **Derived properties and operations** (`Extend/`) — the OCL in the XMI is the contract. Translate + it faithfully AND to its intent; where the OCL is terse or leans on a defined term, ground the + intent (`hypha:spec-citation`) rather than guessing from a sibling. +- **Constraints/invariants** — a validation rule is conformant only if it implements the stated + invariant, not an approximation that happens to pass the corpus. +- **Concrete syntax** — the KEBNF and the clauses it comes from; deviations recorded, never silently + encoded. +- **Semantics** — implied relationships and library specializations follow the clause, not what makes + a particular model render nicely. +- **API/PIM** — service behaviour and payload shape follow the API specification, not the behaviour + of a particular server. + +Rules that hold at every layer: + +- **The specification decides.** Where a reference implementation and the specification disagree, the + specification wins and the divergence is recorded (`GRAMMAR.md` "Known KEBNF divergences", the + deviation ledger, or `GrammarErrata.cs`). Never implement a behaviour whose only justification is + "the reference implementation does it". +- **A reference implementation or its corpus is a test oracle, not the definition of correct.** The + pilot's `.sysml` files, a reference API server's responses, a sample model — all catch regressions + cheaply, and agreement is usually evidence of correctness. A diff is a question, not a verdict. + Note specifically that the pilot's serializer replays the author's original source text wherever + the abstract syntax records no choice (optional keywords, name spellings, formatting), so on those + points it is not an authority at all — which is what the ledger's ACCEPT categories encode. +- **Where the spec is genuinely ambiguous**, pick a reading, cite the clause, and say in one line + that the clause admits more than one — do not silently encode one reading as fact. +- **Where the spec offers several valid forms and ranks none**, the choice is ours: make it a setting + (see issue #359) or state it as a house convention. Do not present it as a requirement. + +**Not yet achieved anywhere — do not claim conformance for any layer.** Confidence today rests +largely on end-to-end comparison against sample data, which measures non-regression rather than +conformance. What each layer needs before conformance can be asserted: **round-trip tests** (write → +re-read → assert the same model; this exists for no serializer today, including the textual writer) +and **per-invariant unit tests** so an individual rule — a derivation, a constraint, a resolution +rule, a service contract — is falsifiable in isolation rather than only via an end-to-end diff. + ## Build & Test Commands ```bash @@ -224,6 +279,57 @@ Direct pushes to `development` or `master` are forbidden. All work lives on a fe **Why this split**: the user is the reviewer of record. The commit is the review and the push is the delivery — both are the user's calls. The agent's git involvement is bounded to: (a) create the branch locally + push the empty ref (so the user's push later is frictionless), and (b) leave the rest alone. This was tightened after two failures: first the agent auto-pushed branches to `development` directly, then over-corrected by auto-committing on the user's behalf. +## Comments: write as few as possible + +Comments break readability. Default to **none**; every comment kept needs a justification. This +applies to production code, tests, and the generator alike. + +- **Delete rather than write.** Prefer a better name or an extracted method over an explanation. +- **Never narrate the what** — a comment paraphrasing the line below it is noise. +- **No history, no benchmarks, no "this used to…"/"previously"/"an earlier approach"/"was reverted".** + Source control holds it. Never put timing measurements in code. +- **No worked examples, no specific case names.** Never justify a rule with a particular model, a + validation-corpus file (`13a-Model Containment`, `ISQ::mass`, `first start;`), or an issue number. + State the rule; if it needs authority, cite the clause (`KerML §8.2.3.5.3`) and nothing more. +- **Never reference the OMG pilot implementation** — or any other tool — as the reason for behaviour. + The writer implements the **specification**, not another implementation's choices. Where the spec + is genuinely ambiguous, say so in one sentence with the clause; where the KEBNF is defective, that + belongs in `GrammarErrata.cs` with its rationale, not scattered through the writers. +- **No notes to future editors** ("keep in step with X", "do not remove"). Encode it in a guard or a + test. +- **XML docs still required on every type and member** (`DEVELOPMENT_STANDARDS.md` §5.1), but held to + one sentence per tag, two as the ceiling — no `` elaborations, no essays. + +### The bright line — the ONLY test for keeping an inline comment + +Earlier wording said to keep "a non-obvious *why*". That is not testable and gets self-served. The +rule is: + +> **A comment may ONLY state a constraint that would cause a reader to break something if they did +> not know it. A comment may NEVER explain why the change was made.** + +Apply it as a question with a yes/no answer: *if a reader deleted or rewrote this code without the +comment, would they introduce a defect?* No → delete the comment. "It helps the reviewer understand +my change" is not a yes; that belongs in the commit message. + +**Budget: at most 2 added comment lines per change.** Over that, delete until it fits or ask first. + +**Signature words that mean you are writing a commit message, not a comment.** If an added comment +line contains any of `now`, `previously`, `rather than`, `instead of`, `used to`, `was `, `no +longer`, `we `, `I `, or restates a `` already on the same member — delete it. A +`PreToolUse` hook rejects these on `Edit`/`Write`, so it fails loudly rather than reaching review. + +Worked example of the failure, from this repo: + +```csharp +// The supplier now records each resource's root as it is read rather than re-deriving it, so +// interior elements no longer reach this list and no metaclass filter is needed to keep them out. +var otherRootNamespaces = globalNamespaces?.Where(c => c != null && !ReferenceEquals(c, rootNamespace)) +``` + +Two banned signatures (`now`, `rather than`), it annotates a self-evident `Where`, and deleting it +costs a reader nothing. The commit message was the right home for all of it. + ## Quality rules - **OCL index base is 1-based; translate positional access accordingly and NEVER mix the two forms.** OCL collections are 1-based (`->at(1)` is the first element; `->first()` ≡ `->at(1)`). Two correct C# forms, applied by target: diff --git a/SysML2.NET.CodeGenerator/Extensions/GrammarErrata.cs b/SysML2.NET.CodeGenerator/Extensions/GrammarErrata.cs index c03ed1bce..79eecc5f2 100644 --- a/SysML2.NET.CodeGenerator/Extensions/GrammarErrata.cs +++ b/SysML2.NET.CodeGenerator/Extensions/GrammarErrata.cs @@ -90,6 +90,22 @@ public static class GrammarErrata "parameter path emits 'out verdict', which re-parses as a plain FeatureMembership with " + "direction out and so loses the metaclass. CalculationBodyItem is already declared in the " + "same file, so the replacement resolves without any further correction."), + new("OccurrenceUsagePrefix", + "OccurrenceUsagePrefix : OccurrenceUsage =\n BasicUsagePrefix", + "OccurrenceUsagePrefix : OccurrenceUsage =\n UnextendedUsagePrefix", + "SysML 8.2.2.9.2 builds OccurrenceUsagePrefix on 'BasicUsagePrefix', which contains no " + + "'EndUsagePrefix', so 'isEnd' is UNREACHABLE for every occurrence usage — 'end port p1: P;' " + + "and 'end item a;' cannot be written at all. This is issue #124 item 7, already recorded as " + + "an accepted divergence in SysML2.NET.CodeGenerator/GRAMMAR.md. Three sources say the " + + "notation is real: (1) the pilot's own grammar reaches the end prefix here (org.omg.sysml.xtext " + + "SysML.xtext, OccurrenceUsagePrefix); (2) the corpus writes it — 'end port p1: P;' in the " + + "Simple Tests ConjugationTest and 'end [1] item a : A' in ConnectionTest; (3) the metamodel " + + "sets isEnd on exactly those elements. There is no admissible alternative spelling: " + + "'BasicUsagePrefix' reaches 'RefPrefix', whose 'isConstant ?= constant' then renders the " + + "pilot's transform-set isConstant as 'constant port p1: P' — which drops isEnd AND asserts a " + + "constant the source never wrote. 'UnextendedUsagePrefix = EndUsagePrefix | BasicUsagePrefix' " + + "is declared in the same file and dispatches on isEnd, so a non-end usage keeps the exact " + + "BasicUsagePrefix behaviour and only an end usage changes."), new("DefinitionElement", " | InterfaceDefinition\n | PortDefinition", " | InterfaceDefinition\n | AllocationDefinition\n | PortDefinition", diff --git a/SysML2.NET.CodeGenerator/GRAMMAR.md b/SysML2.NET.CodeGenerator/GRAMMAR.md index 0cad5b8fd..c57d02443 100644 --- a/SysML2.NET.CodeGenerator/GRAMMAR.md +++ b/SysML2.NET.CodeGenerator/GRAMMAR.md @@ -231,10 +231,40 @@ re-diagnosed: | #3 | `MetadataUsage` not wired into any dispatch point | 14-Language Extensions | | #9 | `SatisfyRequirementUsage` requires `assert` | 08-Requirements | | #10 | `CaseBodyItem` admits no `ReturnParameterMember` | 10-Analysis and Trades | -| #11 | `EnumeratedValue` cannot carry prefix metadata (`#Security enum secret`) | 13-Model Containment, 14-Language Extensions | +| #11 | `EnumeratedValue` cannot carry prefix metadata (`#Security enum secret`) | **CONFIRMED** — see below | 13-Model Containment, 14-Language Extensions, `Simple Tests/MetadataTest` | Items #2, #4, #5, #6 concern productions with no corpus coverage. +### #124 item 11 — confirmed, and it silently DROPS a FeatureTyping + +``` +EnumerationUsageMember : VariantMembership = MemberPrefix ownedRelatedElement += EnumeratedValue +EnumeratedValue : EnumerationUsage = 'enum'? Usage ← no prefix slot +EnumerationUsage : EnumerationUsage = UsagePrefix 'enum' Usage +``` + +`EnumeratedValue` has no `UsagePrefix`, so — unlike `EnumerationUsage` — nothing consumes a +`PrefixMetadataMember` from the cursor. The consequence is worse than an unwritable keyword: the +annotation is the FIRST entry in the value's `ownedRelationship`, so the cursor is still parked on it +when `BuildUsage` reaches the positional `FeatureSpecializationPart` guard +(`cursor.Current is IFeatureTyping || …`). That guard fails and **the typing is never emitted**. + +`Simple Tests/MetadataTest` shows it exactly — all three values carry a `FeatureTyping` to +`ClassificationLevel`, but only the annotated one loses it: + +``` +enum uncl: ClassificationLevel = 0; ← [FeatureTyping, FeatureValue] +enum conf: ClassificationLevel = 1; ← [FeatureTyping, FeatureValue] +enum secret = 2 { @ Security; } ← [OwningMembership(MetadataUsage), FeatureTyping, FeatureValue] +``` + +**Fix when this is taken up:** emit the annotation as a prefix and advance the cursor past it before +delegating to `Usage`, which yields the pilot's `#Security enum secret : ClassificationLevel = 2;`. +That is the deviation this item already licenses, and it restores the typing as a side effect. +`BuildEnumeratedValue` is generated, so the change belongs in the generator — as a HandCoded fallback +for this rule, the way `EntryTransitionMember` (item 8) is handled. `MetadataTest` stays out of +validation until then. + ## Model ↔ notation reconciliations (NOT divergences) Cases where the grammar offers two conformant productions for one model, so the writer must choose. @@ -305,3 +335,40 @@ Each condition alone is refuted by one of the three; the conjunction fits all of `IsValidForDefaultReferenceUsage` still encodes the one spec-mandated case (`!IsEnd && Direction.HasValue`): a directed usage is always referential, so the keyword is redundant there. + +### `DefaultInterfaceEnd` vs `PortUsage` — the optional `port` keyword on an interface end + +``` +InterfaceOccurrenceUsageElement : Usage = DefaultInterfaceEnd | StructureUsageElement | BehaviorUsageElement +DefaultInterfaceEnd : PortUsage = isEnd ?= 'end' Usage ← end p1: P; +PortUsage = OccurrenceUsagePrefix 'port' Usage ← end port p1: P; +``` + +Both alternatives of `InterfaceOccurrenceUsageElement` reach `PortUsage`, so `end p1: P;` and +`end port p1: P;` round-trip to the same metaclass and nothing records which the author wrote. + +The spec settles the OPTIONALITY and one hard condition on it. SysML 2.0 §7.14.2 Interface +Definitions and Usages (p. 109, normative): "All the end features of an interface definition or +usage must be port usages, so the use of the `port` keyword is optional on such end features if no +owned cross feature is declared on the end." + +`DefaultInterfaceEnd` has no notation for a cross feature — only `EndUsagePrefix` carries the +`( ownedRelationship += OwnedCrossFeatureMember )?` slot — so an end that owns one MUST take the +`port` form. `IsValidForDefaultInterfaceEnd` implements exactly that condition +(`IsEnd && OwnedCrossFeature() == null`); where both forms are open the writer takes the +keyword-less one, the production's first alternative. That choice is ours, not a requirement. + +### `BinaryConnectorPart` vs `NaryConnectorPart` — a two-end connector + +``` +ConnectorPart : ConnectionUsage = BinaryConnectorPart | NaryConnectorPart +BinaryConnectorPart = ownedRelationship += ConnectorEndMember 'to' ownedRelationship += ConnectorEndMember +NaryConnectorPart = '(' ownedRelationship += ConnectorEndMember ',' ownedRelationship += ConnectorEndMember + ( ',' ownedRelationship += ConnectorEndMember )* ')' +``` + +`NaryConnectorPart` admits exactly two ends, so for a two-end connector `connect a to b` and +`connect (a, b)` are both conformant and produce the same model. Three or more ends leave only the +n-ary form. The writer prefers the binary form at exactly two ends — a house convention, not a +requirement. `IsValidForBinaryConnectorPart`, `IsValidForBinaryConnectorDeclaration` and +`IsValidForBinaryInterfacePart` each count `EndFeatureMembership` children for it. diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleGenerationContext.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleGenerationContext.cs index 9518e796e..c8e5aaee7 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleGenerationContext.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleGenerationContext.cs @@ -125,6 +125,15 @@ public TextualNotationRule FindRule(string ruleName) /// public int LoopProgressCheckCounter { get; set; } + /// + /// Monotonically-incrementing counter used to produce unique role-based-consumption variable + /// names (e.g. elementAsOwningMembership0) across the emission of a single rule body. + /// Required because a TryTake out-variable shares the enclosing scope, so two role-based + /// consumptions of the same target type in one generated method would collide (CS0136). + /// Incremented by RuleProcessor.TryEmitPinnedRuleConsumption. + /// + public int TakenElementCounter { get; set; } + /// /// Determines whether the next sibling element is a terminal that uses AppendLine /// (e.g., {, }, ;), in which case a trailing space would be unnecessary. diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs index 8c3ae3400..1c610d034 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- // // // Copyright 2022-2026 Starion Group S.A. @@ -76,10 +76,17 @@ private void EmitCollectionNonTerminalLoop(EncodedTextWriter writer, IClass umlC var whileTypeExclusion = this.ResolveCollectionWhileTypeCondition(cursorVariableName, umlClass, referencedRule, propertyName, ruleGenerationContext); + // A dispatcher loop must test the types it can consume; a wider condition stalls it. + var dispatcherTypeCondition = TryResolveDispatcherConsumedTypeCondition(cursorVariableName, referencedRule, propertyName, umlClass, ruleGenerationContext); + string whileCondition; var whileConditionIsBareNullTest = false; - if (!string.IsNullOrWhiteSpace(whileTypeExclusion)) + if (dispatcherTypeCondition != null) + { + whileCondition = dispatcherTypeCondition; + } + else if (!string.IsNullOrWhiteSpace(whileTypeExclusion)) { whileCondition = whileTypeExclusion; } @@ -136,14 +143,8 @@ private void EmitCollectionNonTerminalLoop(EncodedTextWriter writer, IClass umlC } } - // A guarded body-item rule (IsGuardedBodyItemRule) admits elements that have no - // notation, and its per-item builder refuses to consume them WITHOUT advancing the - // cursor. The loop must therefore test the same predicate as the enclosing entry - // guard: a bare non-null test spins forever on the first refused element. The - // guarded form only replaces the BARE null-test fallback: when a type-derived - // while-condition already bounds the loop (next-type exclusion or content-type - // guard, e.g. CalculationBodyPart's `is not IResultExpressionMembership`), that - // stronger structural bound stays. + // A guarded body-item rule refuses elements without advancing, so the loop must test the same + // predicate as the entry guard. if (whileConditionIsBareNullTest && IsGuardedBodyItemRule(nonTerminalElement.Name)) { var guardVariableName = $"{targetProperty.Name.LowerCaseFirstLetter()}BodyItem"; @@ -193,7 +194,7 @@ private void EmitCollectionNonTerminalLoop(EncodedTextWriter writer, IClass umlC /// The group's bare (non-assignment) non-terminal alternatives /// The cursor driving the repeated group /// The current - private static void EmitCollectionGroupFallThrough(EncodedTextWriter writer, IClass umlClass, List dispatcherNonTerminals, string cursorVariableName, RuleGenerationContext ruleGenerationContext) + private static bool EmitCollectionGroupFallThrough(EncodedTextWriter writer, IClass umlClass, List dispatcherNonTerminals, string cursorVariableName, RuleGenerationContext ruleGenerationContext) { var dispatcherCalls = dispatcherNonTerminals .Select(nonTerminal => @@ -209,20 +210,161 @@ private static void EmitCollectionGroupFallThrough(EncodedTextWriter writer, ICl if (dispatcherCalls.Count == 0) { - writer.WriteSafeString($"{cursorVariableName}.Move();{Environment.NewLine}"); + var ruleName = ruleGenerationContext.NamedElementToGenerate?.Name ?? "Unknown"; + writer.WriteSafeString($"throw new System.InvalidOperationException($\"The textual notation writer cannot place the current element ({{{cursorVariableName}.Current?.GetType().Name}}) while building '{ruleName}' — no alternative of the rule claims it, so it would be silently dropped.\");{Environment.NewLine}"); - return; + return true; } foreach (var dispatcherCall in dispatcherCalls) { writer.WriteSafeString($"{dispatcherCall}{Environment.NewLine}"); } + + return false; } /// /// Resolves the type condition for a collection while loop. /// + /// + /// Builds a POSITIVE while condition for a * loop whose body is a pure dispatcher + /// rule (every alternative a bare non-terminal), testing exactly the element types that dispatcher + /// consumes from the loop's cursor — e.g. FeatureSpecialization consumes the + /// Specialization kinds, so a FeatureMembership must not enter the loop. + /// + /// The cursor the loop reads. + /// The rule invoked per iteration. + /// The collection property the loop consumes from. + /// The class hosting the current rule (provides the UML cache). + /// The current . + /// The condition, or when the rule is not a resolvable dispatcher. + /// + /// Declines when the consumed set cannot be fully resolved, or when a following sibling consumes a + /// type the set also admits — the existing next-type exclusion is the correct guard there, and a + /// positive test would let the loop swallow the sibling's element. + /// + private static string TryResolveDispatcherConsumedTypeCondition(string cursorVariableName, TextualNotationRule referencedRule, string propertyName, IClass umlClass, RuleGenerationContext ruleGenerationContext) + { + var isPureDispatcher = referencedRule.Alternatives.Count > 1 + && referencedRule.Alternatives.All(alternative => alternative.Elements.Count == 1 && alternative.Elements[0] is NonTerminalElement); + + if (!isPureDispatcher) + { + return null; + } + + var consumedClasses = new List(); + + foreach (var alternative in referencedRule.Alternatives) + { + var alternativeClasses = CollectCursorConsumedClasses((NonTerminalElement)alternative.Elements[0], propertyName, umlClass, ruleGenerationContext, new HashSet(StringComparer.Ordinal)); + + // An incomplete set would silently skip the elements it failed to account for. + if (alternativeClasses.Count == 0) + { + return null; + } + + consumedClasses.AddRange(alternativeClasses.Where(consumedClass => !consumedClasses.Contains(consumedClass))); + } + + var siblings = ruleGenerationContext.CurrentSiblingElements; + var nextIndex = ruleGenerationContext.CurrentElementIndex + 1; + + if (siblings != null && nextIndex < siblings.Count && siblings[nextIndex] is AssignmentElement { Operator: "+=" } nextAssignment) + { + var nextTypeName = ResolveAssignmentTargetTypeName(nextAssignment, umlClass, ruleGenerationContext); + + if (nextTypeName != null && consumedClasses.Any(consumedClass => nextTypeName.StartsWith(consumedClass.QueryFullyQualifiedTypeName(), StringComparison.Ordinal))) + { + return null; + } + } + + var typeNames = consumedClasses.Select(consumedClass => consumedClass.QueryFullyQualifiedTypeName()).ToList(); + + return typeNames.Count == 1 + ? $"{cursorVariableName}.Current is {typeNames[0]}" + : $"{cursorVariableName}.Current is ({string.Join(" or ", typeNames)})"; + } + + /// + /// Recursively collects the element classes a rule consumes from + /// via +=, descending through bare non-terminal references. + /// + /// The rule reference to walk. + /// The collection property whose consumption is collected. + /// The class hosting the current rule (provides the UML cache). + /// The current . + /// Rule names already walked, preventing infinite recursion. + /// The distinct consumed classes; empty when none resolve. + private static List CollectCursorConsumedClasses(NonTerminalElement nonTerminalElement, string propertyName, IClass umlClass, RuleGenerationContext ruleGenerationContext, HashSet visitedRules) + { + var consumedClasses = new List(); + + if (!visitedRules.Add(nonTerminalElement.Name)) + { + return consumedClasses; + } + + var rule = ruleGenerationContext.FindRule(nonTerminalElement.Name); + + if (rule == null) + { + return consumedClasses; + } + + foreach (var element in rule.Alternatives.SelectMany(alternative => FlattenRuleElements(alternative.Elements))) + { + switch (element) + { + case AssignmentElement { Operator: "+=", Value: NonTerminalElement valueNonTerminal } assignment + when string.Equals(assignment.Property, propertyName, StringComparison.OrdinalIgnoreCase): + { + var itemRule = ruleGenerationContext.FindRule(valueNonTerminal.Name); + var itemClass = RuleQueryUtilities.FindClass(umlClass.Cache, itemRule?.EffectiveTarget ?? valueNonTerminal.Name); + + if (itemClass != null && !consumedClasses.Contains(itemClass)) + { + consumedClasses.Add(itemClass); + } + + break; + } + + case NonTerminalElement nestedNonTerminal: + consumedClasses.AddRange(CollectCursorConsumedClasses(nestedNonTerminal, propertyName, umlClass, ruleGenerationContext, visitedRules) + .Where(nestedClass => !consumedClasses.Contains(nestedClass))); + break; + } + } + + return consumedClasses; + } + + /// + /// Flattens a rule-element sequence, expanding group alternatives so nested assignments and + /// references are visited. + /// + /// The elements to flatten. + /// The flattened element sequence. + private static IEnumerable FlattenRuleElements(IEnumerable elements) + { + foreach (var element in elements) + { + yield return element; + + if (element is GroupElement groupElement) + { + foreach (var nested in FlattenRuleElements(groupElement.Alternatives.SelectMany(alternative => alternative.Elements))) + { + yield return nested; + } + } + } + } + private string ResolveCollectionWhileTypeCondition(string cursorVariableName, IClass umlClass, TextualNotationRule collectionRule, string outerPropertyName, RuleGenerationContext ruleGenerationContext) { var siblings = ruleGenerationContext.CurrentSiblingElements; @@ -251,12 +393,7 @@ private string ResolveCollectionWhileTypeCondition(string cursorVariableName, IC var itemRule = ruleGenerationContext.FindRule(assignmentNonTerminals[0].Name); var itemTypeTarget = itemRule != null ? itemRule.EffectiveTarget : null; - // The item rule's own target is the WRAPPER type for a thin owning wrapper - // (X : OwningMembership = … ownedRelatedElement = Y), which every sibling wrapper on this - // cursor also satisfies — `individual def` consumed its own EmptyMultiplicityMember as a - // DefinitionExtensionKeyword and emitted a stray '#'. Prefer the wrapped-type guard when - // one is available; otherwise keep the coarse test rather than falling through to a - // weaker condition that could admit elements the loop body will not consume. + // A thin owning wrapper's own target is satisfied by every sibling wrapper on this cursor. var wrappedTypeGuard = this.ResolveContentTypeGuard(cursorVariableName, collectionRule, outerPropertyName, umlClass, ruleGenerationContext); if (!string.IsNullOrWhiteSpace(wrappedTypeGuard)) @@ -333,9 +470,7 @@ private string ResolveContentTypeGuard(string cursorVariableName, TextualNotatio return null; } - // An allowlisted content rule needs an ABSENCE constraint on the referenced element's own - // contents, which body-shape analysis cannot express — delegate to the hand-coded predicate - // rather than emitting the shape-derived type check below. + // An absence constraint on the referenced element's contents is not derivable from body shape. if (RequiresHandCodedContentGuard(referencedRule.RuleName)) { var handCodedGuardVariableName = $"{referencedRule.RuleName.LowerCaseFirstLetter()}Guard{ruleGenerationContext.NarrowedTypeCheckCounter}"; @@ -533,8 +668,6 @@ private static string TryBuildCursorTypedCheck(EncodedTextWriter writer, Textual return null; } - // Inner is non-null for thin owning wrappers; the wrapped type is the discriminator that - // distinguishes the wrapper from sibling OwningMembership subtypes on the same cursor. var resolvedTypes = new List<(string Wrapper, string Inner)>(); foreach (var assignmentElement in collectionAssignments) diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs index f1e3358e6..7ca847381 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs @@ -106,8 +106,7 @@ internal void ProcessRuleElement(EncodedTextWriter writer, IClass umlClass, Rule } } - // A `( X )+ X` shape must leave one element for the mandatory tail, or the - // tail emits its terminals against an exhausted cursor (`a.b.` became `a.b..`). + // A `( X )+ X` shape must leave one element for the mandatory tail. var reservationGuard = ResolveTrailingConsumptionReservation(cursorToUse, umlClass, ruleGenerationContext); if (groupTypeGuard.StartsWith("__FULL_GUARD__")) @@ -145,12 +144,8 @@ internal void ProcessRuleElement(EncodedTextWriter writer, IClass umlClass, Rule .OfType() .ToList(); - // Alternatives of the repeated group that are a BARE non-terminal rather than a - // `+=` assignment (e.g. `TypeBodyElement` in - // `( TypeBodyElement | ownedRelationship += ReturnFeatureMember )*`). Such a rule is a - // per-item dispatcher over the SAME cursor that advances the cursor itself, so it - // becomes the loop's fall-through arm — without it the group's switch has no case for - // those elements and the trailing `Move()` silently discards them. + // A bare non-terminal alternative is a per-item dispatcher that advances the cursor itself, so + // it becomes the loop's fall-through arm. var groupDispatcherNonTerminals = groupElement.Alternatives .SelectMany(alternative => alternative.Elements) .OfType() @@ -187,10 +182,7 @@ internal void ProcessRuleElement(EncodedTextWriter writer, IClass umlClass, Rule var groupOrderedElements = RuleQueryUtilities.OrderElementsByInheritance(groupNonTerminals, umlClass.Cache, ruleGenerationContext); - // A `*` group must not swallow an element that a FOLLOWING sibling consumes - // from the same cursor — `( … )* ( ownedRelationship += ResultExpressionMember )?` - // left the trailing optional facing an exhausted cursor and dropped the result - // expression. Exclude the next sibling's target type from the loop condition. + // Exclude the next sibling's target type, or the group swallows the element it consumes. var groupWhileCondition = this.ResolveCollectionWhileTypeCondition(groupCursorVarName, umlClass, null, groupPropertyName, ruleGenerationContext); if (string.IsNullOrWhiteSpace(groupWhileCondition)) @@ -225,8 +217,10 @@ internal void ProcessRuleElement(EncodedTextWriter writer, IClass umlClass, Rule } writer.WriteSafeString($"default:{Environment.NewLine}"); - EmitCollectionGroupFallThrough(writer, umlClass, groupDispatcherNonTerminals, groupCursorVarName, ruleGenerationContext); - writer.WriteSafeString($"break;{Environment.NewLine}"); + if (!EmitCollectionGroupFallThrough(writer, umlClass, groupDispatcherNonTerminals, groupCursorVarName, ruleGenerationContext)) + { + writer.WriteSafeString($"break;{Environment.NewLine}"); + } writer.WriteSafeString($"}}{Environment.NewLine}"); EmitLoopProgressAssertion(writer, groupCursorVarName, groupPositionVariableName, groupElement.TextualNotationRule?.RuleName ?? groupPropertyName); @@ -327,24 +321,9 @@ private static string ResolveTrailingConsumptionReservation(CursorDefinition cur /// The class hosting the current rule (provides the UML cache). /// The additional guard clause, or an empty string when the repetition cannot match one. /// - /// The grammar never repeats a result member: it always gives one its OWN slot in the enclosing rule - /// (EmptyResultMember, ConstructorResultMember, ReturnParameterMember), never a - /// comma-separated repetition. A repetition that shares the enclosing rule's cursor will therefore - /// swallow it whenever the repetition's guard type happens to be one of its supertypes: - /// - /// InvocationExpression = ownedRelationship += InstantiatedTypeMember - /// ArgumentList - /// ownedRelationship += EmptyResultMember - /// PositionalArgumentList = ownedRelationship += ArgumentMember - /// ( ',' ownedRelationship += ArgumentMember )* - /// - /// ArgumentMember : ParameterMembership and EmptyResultMember : ReturnParameterMembership - /// — a ParameterMembership — so the loop emits a separator for it and then renders nothing: - /// f(a, ). - /// Gating on subtype overlap keeps this general without touching repetitions it cannot affect. - /// Of the fifteen comma-repetition shapes across both grammars only two — ArgumentMember and - /// NamedArgumentMember — guard on a supertype of ReturnParameterMembership; the rest test - /// relationship types or sibling membership subtypes, for which the clause would be dead code. + /// The grammar never repeats a result member — it always gives one its own slot in the enclosing rule + /// — so a repetition sharing that cursor swallows it whenever the repetition's guard type is one of + /// its supertypes. Gated on subtype overlap, which leaves repetitions it cannot affect untouched. /// private static string ResolveResultMemberExclusion(CursorDefinition cursorDefinition, IClass itemTargetClass, IClass umlClass) { @@ -383,6 +362,18 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass if (assignmentElement.Value is NonTerminalElement nonTerminalElement) { var cursorToUse = ruleGenerationContext.DefinedCursors.Single(x => x.ApplicableRuleElements.Contains(assignmentElement)); + + // PendingCursorMove lands the Move() inside the type-discrimination block, so the cursor + // advances only on real `+=` consumption. + var shouldEmitCursorMove = !isPartOfMultipleAlternative + && assignmentElement.Container is not GroupElement { IsCollection: true }; + + if (shouldEmitCursorMove + && TryEmitPinnedRuleConsumption(writer, umlClass, nonTerminalElement, cursorToUse, ruleGenerationContext)) + { + return; + } + var usedVariable = $"{cursorToUse.CursorVariableName}.Current"; var previousVariableName = ruleGenerationContext.CurrentVariableName; @@ -390,12 +381,6 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass var previousCaller = ruleGenerationContext.CallerRule; ruleGenerationContext.CallerRule = assignmentElement; - // Route Move() through PendingCursorMove so it lands INSIDE the type-discrimination - // block — the cursor advances only on real += consumption (Golden Rule). Collection - // groups and multi-alternative dispatchers emit their own move. - var shouldEmitCursorMove = !isPartOfMultipleAlternative - && assignmentElement.Container is not GroupElement { IsCollection: true }; - if (shouldEmitCursorMove) { ruleGenerationContext.PendingCursorMove = $"{Environment.NewLine}{cursorToUse.CursorVariableName}.Move();{Environment.NewLine}"; @@ -465,11 +450,9 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass } else if (assignmentElement.Value is NonTerminalElement { Name: "STRING_VALUE" }) { - // STRING_VALUE carries its own quotes — '"' ( STRING_CHARACTER | - // ESCAPE_SEQUENCE )* '"' — and the model holds the DECODED string, so the - // writer owns re-encoding it. See AppendStringValue. Keyed on the TERMINAL - // rather than the property type: a String-typed test would also quote - // declaredName and every other string the grammar writes bare. + // STRING_VALUE carries its own quotes and the model holds the DECODED string, so the writer owns + // re-encoding it. Keyed on the TERMINAL, not the property type, which would also quote every + // string the grammar writes bare. writer.WriteSafeString($"SharedTextualNotationBuilder.AppendStringValue(stringBuilder, poco.{targetPropertyName});"); } else if (string.Equals(targetPropertyName, "Operator", StringComparison.Ordinal)) @@ -515,13 +498,7 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass } else if (targetProperty.QueryIsEnum()) { - // A result member's own keyword already conveys the direction, so writing it - // again emits `return out verdict` where the notation is `return verdict`. - // Testing the owning Membership follows the metamodel's own idiom — Feature's - // parameter-redefinition constraint selects parameters with `direction <> null` - // and then rejects those whose owningFeatureMembership is a result membership. - // Both OMG names come from NotationInvariants, which reports either of them - // going missing rather than letting this rule switch itself off. + // A result member's own keyword already conveys the direction. var impliedDirectionProperty = NotationInvariants.QueryMetamodelName(NotationInvariants.ImpliedDirectionProperty); var isImpliedDirectionProperty = string.Equals(targetProperty.Name, impliedDirectionProperty, StringComparison.Ordinal); @@ -639,13 +616,65 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass } else { - // The grammar's property does not resolve against the metamodel class (e.g. the kebnf's - // `ownedFeatureMember` typo) — delegate to the HandCoded sibling. + // The grammar's property does not resolve against the metamodel class. var handCodedRuleName = assignmentElement.TextualNotationRule?.RuleName ?? "Unknown"; EmitHandCodedFallback(writer, handCodedRuleName, ruleGenerationContext); } } + /// + /// Emits the role-based consumption for a += assignment whose referenced rule carries a + /// role discriminator (a pinned enum constant like { kind = 'guard' }, or a structural + /// signature) — TryTake with the rule's predicate — locating the element by ROLE instead + /// of by cursor position. The matching guard condition is emitted by + /// with the IDENTICAL predicate, so condition + /// and consumption agree on the element they select. + /// + /// The used to write output. + /// The class hosting the current rule (provides the UML cache). + /// The referenced rule's . + /// The cursor the assignment consumes from. + /// The current . + /// true when the consumption was emitted; false when the rule is not discriminating and the positional path must run. + private static bool TryEmitPinnedRuleConsumption(EncodedTextWriter writer, IClass umlClass, NonTerminalElement nonTerminalElement, CursorDefinition cursorDefinition, RuleGenerationContext ruleGenerationContext) + { + var referencedRule = ruleGenerationContext.FindRule(nonTerminalElement.Name); + var typeTarget = referencedRule?.EffectiveTarget; + + if (typeTarget == null) + { + return false; + } + + var targetClass = RuleQueryUtilities.FindClass(umlClass.Cache, typeTarget); + var targetTypeName = targetClass?.QueryFullyQualifiedTypeName(); + + if (targetTypeName == null) + { + return false; + } + + var predicate = ResolveRoleBasedPredicate(referencedRule, targetClass, ruleGenerationContext); + + if (predicate == null) + { + return false; + } + + var takenVariableName = $"elementAs{targetClass.Name}{ruleGenerationContext.TakenElementCounter++}"; + + var builderCall = string.Equals(typeTarget, ruleGenerationContext.NamedElementToGenerate.Name, StringComparison.Ordinal) + ? $"Build{nonTerminalElement.Name}({takenVariableName}, writerContext, stringBuilder);" + : $"{typeTarget}TextualNotationBuilder.Build{nonTerminalElement.Name}({takenVariableName}, writerContext, stringBuilder);"; + + writer.WriteSafeString($"{Environment.NewLine}if ({cursorDefinition.CursorVariableName}.TryTake<{targetTypeName}>({predicate}, out var {takenVariableName})){Environment.NewLine}"); + writer.WriteSafeString($"{{{Environment.NewLine}"); + writer.WriteSafeString(builderCall); + writer.WriteSafeString($"{Environment.NewLine}}}"); + + return true; + } + /// /// Process a /// diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs index 597c3f448..2e327eb12 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- // // // Copyright 2022-2026 Starion Group S.A. @@ -230,9 +230,8 @@ private bool TryHandleEmptyVsNonEmptyMembership(EncodedTextWriter writer, IClass var typeName = emptyTarget.QueryFullyQualifiedTypeName(); var cursorVarName = cursor.CursorVariableName; - // An "Empty" wrapper rule usually still ASSIGNS its collection (EmptyUsage = {}), so a - // Count-based discriminator can never select the empty branch. When the branches wrap - // different classes, discriminate on the WRAPPED element type instead. + // An "Empty" wrapper rule usually still assigns its collection, so a Count-based discriminator + // cannot select the empty branch; discriminate on the wrapped element type instead. var wrappedNonEmptyTypeName = QueryWrappedElementTypeName(nonEmptyBranch.NonTerminal, umlClass, ruleGenerationContext); var wrappedEmptyTypeName = QueryWrappedElementTypeName(emptyBranch.NonTerminal, umlClass, ruleGenerationContext); @@ -594,13 +593,16 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer, { var allProperties = duplicateGroup.Key.QueryAllProperties(); - var elementBoolProps = new List<(NonTerminalElement RuleElement, List BoolProps)>(); + var elementBoolProps = new List<(NonTerminalElement RuleElement, List BoolProps, List SettableBoolProps)>(); foreach (var ruleElement in duplicateGroup.Value.Select(x => x.RuleElement)) { var referencedRule = ruleGenerationContext.AllRules.Single(x => x.RuleName == ruleElement.Name); var booleanProperties = RuleQueryUtilities.QueryBooleanAssignmentProperties(referencedRule, ruleGenerationContext.AllRules); - elementBoolProps.Add((ruleElement, booleanProperties)); + + // A sibling that may set the property optionally can satisfy a guard on it too. + var settableBooleanProperties = RuleQueryUtilities.QueryAllBooleanAssignmentProperties(referencedRule, ruleGenerationContext.AllRules); + elementBoolProps.Add((ruleElement, booleanProperties, settableBooleanProperties)); } for (var elementIndex = 0; elementIndex < elementBoolProps.Count; elementIndex++) @@ -612,7 +614,7 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer, { if (otherIndex != elementIndex) { - foreach (var prop in elementBoolProps[otherIndex].BoolProps) + foreach (var prop in elementBoolProps[otherIndex].SettableBoolProps) { othersProperties.Add(prop); } @@ -695,10 +697,7 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer, } } - // Subtype-overlap guard synthesis: a duplicate group's unguarded fall-through case - // is only safe when no sibling alternative targets a SUPERTYPE of the group's class - // (whose dispatcher may handle this group's subtypes internally). When overlap is - // detected, synthesise a `when` guard from the rule's parsed body. + // An unguarded fall-through is only safe when no sibling alternative targets a supertype. foreach (var duplicateGroup in duplicateClasses) { var stillUnguarded = duplicateGroup.Value @@ -740,9 +739,7 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer, } } - // Hand-coded alternative guards: an alternative whose discriminator cannot be derived - // from the rule body at all (it needs cursor lookahead, not a property test) is - // allowlisted by rule name and delegates to a hand-coded IsValidFor{Rule}. + // A discriminator needing cursor lookahead cannot be derived from the rule body. foreach (var unguarded in mappedNonTerminalElements .Select(element => element.RuleElement) .Where(ruleElement => RequiresHandCodedAlternativeGuard(ruleElement.Name) @@ -751,12 +748,8 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer, whenGuards[unguarded] = $"{{0}}.IsValidFor{unguarded.Name}(writerContext)"; } - // Self-default guard synthesis: when the rule uses its own target class as one - // alternative (e.g. `FeatureElement : Feature = Feature | Step | …`), that arm is the - // catch-all for inline subclass forms — sibling arms need property-derived `when` - // guards (e.g. `DeclaredName != null`) so an anonymous subclass instance declines the - // match and falls through. Without the self-default shape, most-derived-first - // ordering alone suffices and no guards are emitted. + // A rule using its own target class as an alternative makes that arm the catch-all, so sibling + // arms need guards to let an anonymous subclass instance fall through. var generatingClassForSelfDefault = ruleGenerationContext.NamedElementToGenerate as IClass; var isSelfDefault = generatingClassForSelfDefault != null && mappedNonTerminalElements.Any(element => element.UmlClass == generatingClassForSelfDefault); @@ -789,6 +782,8 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer, } } + AssertDuplicateGroupGuardsAreDisjoint(duplicateClasses, whenGuards); + var reorderedElements = new List<(NonTerminalElement RuleElement, IClass UmlClass)>(); var processedDuplicateClasses = new HashSet(); @@ -812,19 +807,11 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer, var defaultElement = mappedNonTerminalElements .LastOrDefault(x => x.UmlClass == ruleGenerationContext.NamedElementToGenerate && !whenGuards.ContainsKey(x.RuleElement)); - // Ordered by: non-default arms first (the rule's own target class is the catch-all and - // must sit last), then most-derived first so a subtype arm always precedes an arm - // targeting its supertype. - // - // OrderBy/ThenByDescending is STABLE, which is load-bearing rather than incidental: - // arms of equal inheritance depth are mutually disjoint, so their relative order does - // not affect dispatch — but it does affect the emitted TEXT. The previous - // List.Sort is introsort and therefore unstable, and the comparison carried no - // secondary key, so adding one alternative anywhere in a rule could reshuffle unrelated - // equal-depth arms and produce diff noise that reads like a behavioural change but is - // not. Adding AllocationDefinition to DefinitionElement did exactly that, silently - // reordering MetadataDefinition / ViewDefinition / RenderingDefinition. Falling back to - // declaration order keeps every regeneration minimal and deterministic. + // Ordered by: non-default arms first (the rule's own target class is the catch-all and must sit + // last), then most-derived first so a subtype arm precedes an arm targeting its supertype. + // The sort must be STABLE: equal-depth arms are mutually disjoint, so their relative order does + // not affect dispatch but does affect the emitted text, and an unstable sort makes regeneration + // non-deterministic. mappedNonTerminalElements = [ .. mappedNonTerminalElements @@ -1353,6 +1340,46 @@ private static void CollectReachableTargetClasses(string ruleName, IXmiElementCa } } + /// + /// Fails generation when a duplicate dispatch group's arms cannot be told apart — more than one + /// arm without a guard, or two arms carrying the IDENTICAL guard — because C# when clauses + /// evaluate in order and the first arm would silently claim every instance the second one owns. + /// This is the first tranche of the disjointness safety net: it proves nothing about guards that + /// differ textually but overlap semantically, only that no arm is TRIVIALLY unreachable. + /// + /// The duplicate dispatch groups, keyed by their shared target class. + /// The resolved when guard per alternative, after every guard tier ran. + /// When a group's arms are not trivially disjoint. + private static void AssertDuplicateGroupGuardsAreDisjoint(Dictionary> duplicateClasses, Dictionary whenGuards) + { + foreach (var duplicateGroup in duplicateClasses) + { + var unguardedRuleNames = duplicateGroup.Value + .Where(element => !whenGuards.ContainsKey(element.RuleElement)) + .Select(element => element.RuleElement.Name) + .ToList(); + + if (unguardedRuleNames.Count > 1) + { + throw new InvalidOperationException( + $"Alternatives '{string.Join("', '", unguardedRuleNames)}' all target '{duplicateGroup.Key.Name}' and none of them resolved a distinguishing guard — the first arm would silently claim every instance. Add a discriminator (pinned constant, boolean assignment, or IsValidFor guard) for all but one."); + } + + var collidingGuardGroup = duplicateGroup.Value + .Where(element => whenGuards.ContainsKey(element.RuleElement)) + .GroupBy(element => whenGuards[element.RuleElement], StringComparer.Ordinal) + .FirstOrDefault(guardGroup => guardGroup.Count() > 1); + + if (collidingGuardGroup != null) + { + var collidingRuleNames = collidingGuardGroup.Select(element => element.RuleElement.Name); + + throw new InvalidOperationException( + $"Alternatives '{string.Join("', '", collidingRuleNames)}' targeting '{duplicateGroup.Key.Name}' resolved the IDENTICAL guard '{collidingGuardGroup.Key}' — the first arm would silently claim every instance. Their discriminators must be disjoint."); + } + } + } + /// /// Runs the structural-predicate walk for and returns the raw clause /// list, seeding the per-walk visited-rules set and cursor-state bookkeeping. @@ -1395,25 +1422,24 @@ private static List CollectGuardClausesForRule(TextualNotationRule rule, /// /// Combines per-alternative clause lists into one predicate: each alternative's clauses joined /// by &&, alternatives joined by || (exactly one parses at runtime). - /// Empty alternatives are dropped and identical ones collapse. + /// Identical alternatives collapse. A clauseless alternative constrains nothing, so the whole + /// disjunction it belongs to is unconstrained and yields no predicate. /// /// One list of synthesized clauses per grammar alternative. - /// A single combined predicate string, or null when every alternative is empty. + /// A single combined predicate string, or null when any alternative is unconstrained. private static string CombineAlternativesAsOr(List> perAlternativeClauses) { - var nonEmpty = perAlternativeClauses - .Where(altClauses => altClauses.Count > 0) - .Select(altClauses => altClauses.Count == 1 - ? altClauses[0] - : "(" + string.Join(" && ", altClauses) + ")") - .ToList(); - - if (nonEmpty.Count == 0) + if (perAlternativeClauses.Count == 0 || perAlternativeClauses.Any(altClauses => altClauses.Count == 0)) { return null; } - var distinct = nonEmpty.Distinct(StringComparer.Ordinal).ToList(); + var distinct = perAlternativeClauses + .Select(altClauses => altClauses.Count == 1 + ? altClauses[0] + : "(" + string.Join(" && ", altClauses) + ")") + .Distinct(StringComparer.Ordinal) + .ToList(); if (distinct.Count == 1) { @@ -1499,8 +1525,6 @@ private static void CollectGuardClauses(IEnumerable elements, IEnum } } - // Depth exhausted, rule not found, or already visited — conservatively - // suppress later cursor predicates. cursorMayHaveAdvanced = true; break; } @@ -1547,7 +1571,7 @@ private static void CollectGuardClauses(IEnumerable elements, IEnum /// prop = NonTerminal{0}.{Prop} is I{RHS-target} (narrows when possible, else != null) /// ownedRelationship += NonTerminal (first occurrence) → cursor predicate /// prop += NonTerminal for any other collection → {0}.{Prop}.OfType<I{RHS-target}>().Any() - /// prop ?= 'kw'null (already handled by the boolean discriminator pass) + /// prop ?= 'kw'{0}.{Prop} — the keyword is mandatory within its production, so the property is necessarily set /// /// /// The to translate @@ -1559,7 +1583,7 @@ private static void CollectGuardClauses(IEnumerable elements, IEnum /// A template clause, or null when no clause applies. private static string TryBuildClauseForAssignment(AssignmentElement assignment, IEnumerable targetProperties, IXmiElementCache cache, IReadOnlyList allRules, ref bool firstCursorEmitted, bool cursorMayHaveAdvanced) { - if (assignment?.Property == null || assignment.Operator == "?=") + if (assignment?.Property == null) { return null; } @@ -1588,6 +1612,8 @@ private static string TryBuildClauseForAssignment(AssignmentElement assignment, return BuildScalarAssignmentClause(assignment, matchingProperty, propertyAccessor, cache, allRules); case "+=": return BuildCollectionAssignmentClause(assignment, propertyAccessor, cache, allRules, ref firstCursorEmitted, cursorMayHaveAdvanced); + case "?=": + return $"{{0}}.{propertyAccessor}"; default: return null; } @@ -1635,7 +1661,6 @@ private static string BuildScalarAssignmentClause(AssignmentElement assignment, if (rhsTargetClass != null) { - // `is I{Rhs}` needs a reference-typed property to compile. return matchingProperty.QueryIsReferenceType() ? $"{{0}}.{propertyAccessor} is {rhsTargetClass.QueryFullyQualifiedTypeName()}" : null; diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs index 76087a158..78b63c68b 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs @@ -29,6 +29,7 @@ namespace SysML2.NET.CodeGenerator.HandleBarHelpers using SysML2.NET.CodeGenerator.Extensions; using SysML2.NET.CodeGenerator.Grammar.Model; + using uml4net.Classification; using uml4net.Extensions; using uml4net.SimpleClassifiers; using uml4net.StructuredClassifiers; @@ -113,14 +114,9 @@ private void EmitElements(EncodedTextWriter writer, IClass umlClass, List - /// Text-free members the pilot is KNOWN to store before the elements the production declares ahead of - /// them, verified against real pilot output. - /// Deliberately an allowlist, not a structural rule. Emitting no text does NOT imply the model - /// may store the element anywhere: storage order is a per-rule implementation detail and it goes BOTH - /// ways. EmptyMultiplicityMember is stored FIRST though declared last, while - /// EmptyResultMember / ReturnParameterMembership is stored LAST as declared (in the - /// OperatorExpression family, InvocationExpression and FeatureReferenceExpression). - /// Hoisting the latter would strand the cursor on it. Only add a name here after checking real output. + /// Text-free members stored before the elements the production declares ahead of them. + /// An allowlist, not a structural rule: emitting no text does not imply the element may be + /// stored anywhere, and storage order goes both ways. /// private static readonly HashSet HoistableTextFreeMembers = new(StringComparer.Ordinal) { @@ -130,11 +126,8 @@ private void EmitElements(EncodedTextWriter writer, IClass umlClass, List /// Moves a lone += element whose production emits NO text to the front of the alternative. /// Such an element has no observable position in the notation, so the grammar cannot constrain - /// where the parser puts it in the collection — and the pilot does not always put it where the - /// production does. Consuming it first keeps the cursor aligned for the elements that DO emit text; - /// leaving it in place strands the cursor on it (e.g. IndividualDefinition declares - /// EmptyMultiplicityMember last but the model stores it first, hiding the Subclassification - /// that Definition must read). + /// where it is stored. Consuming it first keeps the cursor aligned for the elements that do emit + /// text; leaving it in place strands the cursor on it. /// Only a LONE such element is hoisted: when several appear (e.g. TransitionUsage's two /// EmptyParameterMembers) their relative order decides which pairs with which sibling, so /// moving them would change meaning. @@ -152,10 +145,7 @@ private static List HoistSingleNonNotationalConsumption(ListThe property pattern, or when nothing enum-typed is pinned. private static string ResolvePinnedConstantPattern(TextualNotationRule referencedRule, IClass targetClass) { + return TryResolvePinnedConstant(referencedRule, targetClass, out var propertyName, out var enumTypeName, out var literalName) + ? $"{{ {propertyName}: {enumTypeName}.{literalName} }}" + : null; + } + + /// + /// Builds the C# lambda predicate testing a constant a rule pins via a non-parsing assignment + /// ({ kind = 'guard' }), for use with the role-based cursor primitives + /// Contains and TryTake. The condition site and the consumption site MUST both use + /// this identical predicate so they agree on the element they select. + /// + /// The rule whose pinned constant is sought. + /// The rule's target . + /// The lambda predicate, or when nothing enum-typed is pinned. + private static string ResolvePinnedConstantPredicate(TextualNotationRule referencedRule, IClass targetClass) + { + return TryResolvePinnedConstant(referencedRule, targetClass, out var propertyName, out var enumTypeName, out var literalName) + ? $"candidate => candidate.{propertyName} == {enumTypeName}.{literalName}" + : null; + } + + /// + /// Resolves the components of a constant a rule pins to an enum-typed property through a + /// non-parsing assignment ({ kind = 'guard' }). Sibling rules sharing one target type are + /// distinguishable ONLY by such a constant, so it is the rule's role discriminator. + /// + /// The rule whose pinned constant is sought. + /// The rule's target . + /// The pinned property's C# name. + /// The fully-qualified name of the property's enumeration type. + /// The pinned enumeration literal's C# name. + /// true when the rule pins an enum-typed constant; false otherwise. + private static bool TryResolvePinnedConstant(TextualNotationRule referencedRule, IClass targetClass, out string propertyName, out string enumTypeName, out string literalName) + { + propertyName = null; + enumTypeName = null; + literalName = null; + var pinnedAssignment = referencedRule.Alternatives .SelectMany(alternative => alternative.Elements) .OfType() @@ -383,19 +409,187 @@ private static string ResolvePinnedConstantPattern(TextualNotationRule reference if (pinnedAssignment == null) { - return null; + return false; } var property = targetClass.QueryAllProperties() .FirstOrDefault(x => string.Equals(x.Name, pinnedAssignment.PropertyName, StringComparison.OrdinalIgnoreCase)); if (property?.Type is not IEnumeration) + { + return false; + } + + propertyName = property.Name.CapitalizeFirstLetter(); + enumTypeName = property.Type.QueryFullyQualifiedTypeName(); + literalName = pinnedAssignment.Value.Trim('\'').CapitalizeFirstLetter(); + return true; + } + + /// + /// Builds the non-consuming, role-based guard condition for a += assignment whose + /// referenced rule carries a role discriminator — {cursor}.Contains<T>(predicate) — + /// locating the element by ROLE instead of by cursor position. Returns + /// when no discriminating predicate resolves, so the caller falls back to positional access. + /// + /// The += assignment to build the condition for. + /// The class hosting the current rule (provides the UML cache). + /// The current . + /// The cursor variable the condition probes. + /// The Contains condition, or when not applicable. + private static string TryResolveRoleBasedContainsCondition(AssignmentElement assignmentElement, IClass umlClass, RuleGenerationContext ruleGenerationContext, string cursorVariableName) + { + if (assignmentElement.Value is not NonTerminalElement nonTerminalElement) + { + return null; + } + + var referencedRule = ruleGenerationContext.FindRule(nonTerminalElement.Name); + var typeTarget = referencedRule?.EffectiveTarget; + + if (typeTarget == null) { return null; } - var literalName = pinnedAssignment.Value.Trim('\'').CapitalizeFirstLetter(); - return $"{{ {property.Name.CapitalizeFirstLetter()}: {property.Type.QueryFullyQualifiedTypeName()}.{literalName} }}"; + var targetClass = RuleQueryUtilities.FindClass(umlClass.Cache, typeTarget); + var targetTypeName = targetClass?.QueryFullyQualifiedTypeName(); + + if (targetTypeName == null) + { + return null; + } + + var predicate = ResolveRoleBasedPredicate(referencedRule, targetClass, ruleGenerationContext); + + return predicate == null ? null : $"{cursorVariableName}.Contains<{targetTypeName}>({predicate})"; + } + + /// + /// Resolves the role predicate for a referenced rule: the enum constant the rule pins takes + /// precedence (the metamodel-provided discriminator); otherwise the rule's structural signature + /// is synthesised from its body. Returns when neither is discriminating, + /// in which case the caller MUST stay positional — scanning ahead on a weak predicate steals + /// elements that belong to later consumptions. + /// + /// The rule whose role predicate is sought. + /// The rule's target . + /// The current . + /// The lambda predicate, or when the rule is not discriminating. + private static string ResolveRoleBasedPredicate(TextualNotationRule referencedRule, IClass targetClass, RuleGenerationContext ruleGenerationContext) + { + return ResolvePinnedConstantPredicate(referencedRule, targetClass) + ?? ResolveStructuralSignaturePredicate(referencedRule, targetClass, ruleGenerationContext); + } + + /// + /// Synthesises a candidate-level structural-signature predicate from a rule's body: one clause + /// per mandatory discriminating assignment (owned-content shape or literal equality), clauses + /// AND-combined per alternative, alternatives OR-combined. EVERY alternative must contribute at + /// least one clause — an alternative without one admits any instance of the target type, which + /// would make the scan-ahead greedy — otherwise the whole rule is deemed non-discriminating. + /// + /// The rule whose structural signature is synthesised. + /// The rule's target (the candidate's type). + /// The current . + /// The lambda predicate, or when any alternative is non-discriminating. + private static string ResolveStructuralSignaturePredicate(TextualNotationRule referencedRule, IClass targetClass, RuleGenerationContext ruleGenerationContext) + { + var targetProperties = targetClass.QueryAllProperties(); + var alternativeClauseSets = new List>(); + + foreach (var alternative in referencedRule.Alternatives) + { + // `A && A` is `A`, so dedupe as the OR-side already does. + var clauses = alternative.Elements + .Select(element => TryBuildCandidateSignatureClause(element, targetProperties, targetClass, ruleGenerationContext)) + .Where(clause => clause != null) + .Distinct(StringComparer.Ordinal) + .ToList(); + + if (clauses.Count == 0) + { + return null; + } + + alternativeClauseSets.Add(clauses); + } + + // OR-ing a stronger conjunction with a weaker sibling is just the weaker sibling — + // `(B && A) || A` ≡ `A` — so drop every alternative whose clause set strictly contains + // another alternative's set. + var retainedClauseSets = alternativeClauseSets + .Where(clauseSet => !alternativeClauseSets.Any(otherSet => otherSet.Count < clauseSet.Count && otherSet.All(clauseSet.Contains))) + .ToList(); + + var distinctPredicates = retainedClauseSets + .Select(clauseSet => clauseSet.Count == 1 ? clauseSet[0] : $"({string.Join(" && ", clauseSet)})") + .Distinct(StringComparer.Ordinal) + .ToList(); + + var combined = distinctPredicates.Count == 1 ? distinctPredicates[0] : string.Join(" || ", distinctPredicates); + + return $"candidate => {combined}"; + } + + /// + /// Builds one candidate-level signature clause for a rule-body element, or + /// when the element carries no discriminating information. Discriminating shapes: a mandatory + /// prop += NonTerminal owned-content projection (excluding root metaclasses, whose + /// OfType test is tautological) and a mandatory prop = 'literal' equality on an + /// enum- or string-typed property. + /// + /// The rule-body element to translate. + /// All properties of the candidate's target class. + /// The candidate's target . + /// The current . + /// The clause over candidate, or . + private static string TryBuildCandidateSignatureClause(RuleElement ruleElement, IEnumerable targetProperties, IClass targetClass, RuleGenerationContext ruleGenerationContext) + { + if (ruleElement is not AssignmentElement { IsOptional: false, IsCollection: false } assignment) + { + return null; + } + + var matchingProperty = targetProperties.FirstOrDefault(property => string.Equals(property.Name, assignment.Property, StringComparison.OrdinalIgnoreCase)); + + if (matchingProperty == null) + { + return null; + } + + switch (assignment) + { + case { Operator: "+=", Value: NonTerminalElement rhsNonTerminal } when matchingProperty.QueryIsEnumerable(): + { + var rhsRule = ruleGenerationContext.FindRule(rhsNonTerminal.Name); + var rhsTarget = rhsRule?.EffectiveTarget ?? rhsNonTerminal.Name; + var rhsClass = RuleQueryUtilities.FindClass(targetClass.Cache, rhsTarget); + + // OfType over a root metaclass admits ANY populated collection — no discrimination. + if (rhsClass == null || rhsClass.Name is "Element" or "Relationship") + { + return null; + } + + return $"candidate.{matchingProperty.QueryPropertyNameBasedOnUmlProperties()}.OfType<{rhsClass.QueryFullyQualifiedTypeName()}>().Any()"; + } + + case { Operator: "=", Value: TerminalElement literalTerminal } when !string.IsNullOrWhiteSpace(literalTerminal.Value): + { + if (matchingProperty.Type is IEnumeration) + { + return $"candidate.{matchingProperty.Name.CapitalizeFirstLetter()} == {matchingProperty.Type.QueryFullyQualifiedTypeName()}.{literalTerminal.Value.Trim('\'').CapitalizeFirstLetter()}"; + } + + return matchingProperty.QueryIsString() + ? $"candidate.{matchingProperty.QueryPropertyNameBasedOnUmlProperties()} == \"{literalTerminal.Value}\"" + : null; + } + + default: + return null; + } } /// @@ -492,14 +686,36 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass, if (consumptionAssignments.Count > 1) { var conditionParts = new List(); + var emittedPinnedCondition = false; for (var consumptionIndex = 0; consumptionIndex < consumptionAssignments.Count; consumptionIndex++) { + var consumptionAssignment = consumptionAssignments[consumptionIndex]; + + // A role-based part holds regardless of cursor position, so a positional tail-walk conjunct no + // longer lines up with it and would reintroduce the silent drop. + if (emittedPinnedCondition && !elements.Contains(consumptionAssignment)) + { + continue; + } + + // A discriminating rule is located by ROLE (Contains + predicate), + // not by cursor position — its consumption site takes the element + // with TryTake and the identical predicate. + var pinnedCondition = TryResolveRoleBasedContainsCondition(consumptionAssignment, umlClass, ruleGenerationContext, iterator.CursorVariableName); + + if (pinnedCondition != null) + { + conditionParts.Add(pinnedCondition); + emittedPinnedCondition = true; + continue; + } + var cursorAccess = consumptionIndex == 0 ? $"{iterator.CursorVariableName}.Current" : $"{iterator.CursorVariableName}.GetNext({consumptionIndex})"; - var typeName = ResolveAssignmentTargetTypeName(consumptionAssignments[consumptionIndex], umlClass, ruleGenerationContext); + var typeName = ResolveAssignmentTargetTypeName(consumptionAssignment, umlClass, ruleGenerationContext); conditionParts.Add(typeName == null ? $"{cursorAccess} != null" @@ -510,14 +726,14 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass, } else { - // Guard on the TYPE the assignment consumes, not on mere cursor non-emptiness — - // a bare non-null test also passes for the next UNRELATED relationship and emits - // the group's terminals spuriously (e.g. AcceptParameterPart's `via`). + var pinnedSingleCondition = TryResolveRoleBasedContainsCondition(assigment, umlClass, ruleGenerationContext, iterator.CursorVariableName); + + // Guard on the consumed TYPE: a bare non-null test emits the group's terminals spuriously. var singleTypeName = ResolveAssignmentTargetTypeName(assigment, umlClass, ruleGenerationContext); - ifStatementContent.Add(singleTypeName == null + ifStatementContent.Add(pinnedSingleCondition ?? (singleTypeName == null ? property.QueryIfStatementContentForNonEmpty(iterator.CursorVariableName) - : $"{iterator.CursorVariableName}.Current is {singleTypeName}"); + : $"{iterator.CursorVariableName}.Current is {singleTypeName}")); } } } @@ -576,6 +792,15 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass, } } + // A nested optional group can carry the only notation for its content, so the outer group must + // be entered whenever that content is present. + var nestedGroupCondition = TryResolveNestedGroupCursorCondition(umlClass, elements, ruleGenerationContext); + + if (nestedGroupCondition != null) + { + inlineConditionParts.Add(nestedGroupCondition); + } + var optionalCollectionCondition = TryResolveOptionalCollectionGroupCondition(umlClass, elements, ruleGenerationContext); if (optionalCollectionCondition != null) @@ -610,15 +835,40 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass, } /// - /// Resolves the guard for an optional group whose only variable content is a *-quantified - /// bare non-terminal — e.g. ( '{' ActionBodyItem* '}' )?. Such a group must be emitted only - /// when its loop would iterate at least once: the group's own terminals carry no information, so a - /// property-based condition wrongly emits an empty { } whenever any unrelated property is set. + /// Builds the condition testing whether a NESTED optional group inside an optional group has + /// content on the cursor — the outer group must be entered to emit it, even when the outer + /// group's own elements would all be empty. /// - /// The related - /// The optional group's elements - /// The current - /// The cursor-based condition, or when the group is not that shape. + /// The class hosting the current rule (provides the UML cache). + /// The outer optional group's elements. + /// The current . + /// The condition, or when no nested group consumes a cursor. + private static string TryResolveNestedGroupCursorCondition(IClass umlClass, List elements, RuleGenerationContext ruleGenerationContext) + { + var nestedAssignment = elements + .OfType() + .Where(group => group.IsOptional) + .SelectMany(group => group.Alternatives.SelectMany(groupAlternative => groupAlternative.Elements)) + .OfType() + .FirstOrDefault(assignment => assignment is { Operator: "+=", Value: NonTerminalElement }); + + if (nestedAssignment == null) + { + return null; + } + + var cursor = ruleGenerationContext.DefinedCursors.FirstOrDefault(definedCursor => definedCursor.ApplicableRuleElements.Contains(nestedAssignment)); + + if (cursor == null) + { + return null; + } + + var typeName = ResolveAssignmentTargetTypeName(nestedAssignment, umlClass, ruleGenerationContext); + + return typeName == null ? null : $"{cursor.CursorVariableName}.Current is {typeName}"; + } + private static string TryResolveOptionalCollectionGroupCondition(IClass umlClass, List elements, RuleGenerationContext ruleGenerationContext) { var nonTerminals = elements.OfType().ToList(); @@ -643,8 +893,6 @@ private static string TryResolveOptionalCollectionGroupCondition(IClass umlClass return null; } - // The cursor is declared up-front by DeclareAllRequiredCursors; if it is absent this is not the - // shape we handle, so fall back rather than emit a second declaration. var existingCursor = ruleGenerationContext.DefinedCursors.SingleOrDefault(x => x.IsCursorValidForProperty(targetProperty)); if (existingCursor == null) @@ -885,20 +1133,17 @@ private void EmitNonTerminalThenAssignmentDispatch(EncodedTextWriter writer, ICl /// The current private void ProcessMultiElementAlternatives(EncodedTextWriter writer, IClass umlClass, IReadOnlyCollection alternatives, RuleGenerationContext ruleGenerationContext) { - // When all alternatives consist exclusively of terminal elements (and optionally non-parsing assignments), handle via code-gen if (alternatives.All(alt => alt.Elements.Count > 0 && alt.Elements.All(element => element is TerminalElement or NonParsingAssignmentElement))) { EmitTerminalOnlyAlternatives(writer, umlClass, alternatives, ruleGenerationContext); return; } - // Detect pattern: property=[QualifiedName] | property=NonTerminal{containment+=property} if (alternatives.Count == 2 && TryEmitQualifiedNameOrChainAlternatives(writer, umlClass, alternatives, ruleGenerationContext)) { return; } - // Multi-element alternatives (e.g., ';' | '{' NamespaceBodyElement* '}') var firstAlt = alternatives.ElementAt(0); var hasTerminalOnlyFirstAlt = firstAlt.Elements.Count == 1 && firstAlt.Elements[0] is TerminalElement; @@ -908,7 +1153,6 @@ private void ProcessMultiElementAlternatives(EncodedTextWriter writer, IClass um } else { - // Try each pattern handler in order; fall back to HandCoded if none match if (this.TryHandleOperatorLiteralAlternation(writer, umlClass, alternatives, ruleGenerationContext)) { return; @@ -1067,16 +1311,8 @@ private static bool TryEmitQualifiedNameOrChainAlternatives(EncodedTextWriter wr var chainTypeName = chainTargetClass.QueryFullyQualifiedTypeName(); var chainVarName = $"chained{resolvedPropertyName}As{chainTargetClass.Name}"; - string builderCallString; - - if (typeTarget == ruleGenerationContext.NamedElementToGenerate.Name) - { - builderCallString = $"Build{chainNonTerminal.Name}({chainVarName}, writerContext, stringBuilder);"; - } - else - { - builderCallString = $"{typeTarget}TextualNotationBuilder.Build{chainNonTerminal.Name}({chainVarName}, writerContext, stringBuilder);"; - } + var builderCallString = typeTarget == ruleGenerationContext.NamedElementToGenerate.Name ? $"Build{chainNonTerminal.Name}({chainVarName}, writerContext, stringBuilder);" + : $"{typeTarget}TextualNotationBuilder.Build{chainNonTerminal.Name}({chainVarName}, writerContext, stringBuilder);"; writer.WriteSafeString($"if ({variableName}.{resolvedContainmentName}.Contains({variableName}.{resolvedPropertyName}) && {variableName}.{resolvedPropertyName} is {chainTypeName} {chainVarName}){Environment.NewLine}"); writer.WriteSafeString($"{{{Environment.NewLine}"); @@ -1086,10 +1322,8 @@ private static bool TryEmitQualifiedNameOrChainAlternatives(EncodedTextWriter wr writer.WriteSafeString($"{{{Environment.NewLine}"); writer.WriteSafeString($"SharedTextualNotationBuilder.AppendQualifiedName(stringBuilder,{variableName}.{resolvedPropertyName}, writerContext, poco);{Environment.NewLine}"); - // Both alternatives denote the same notational prefix, so when the chain rule ends in a - // terminal (FeatureChainPrefix's trailing '.') the [QualifiedName] branch must emit it too. - // The kebnf omits that '.' on the reference alternative of FlowEndSubsetting; the pilot's - // Xtext grammar spells it out on both, and the kebnf is immutable, so it is recovered here. + // Both alternatives denote the same notational prefix, so a trailing terminal must be emitted on + // both. The KEBNF omits it on one and is immutable, so it is recovered here. var chainTrailingTerminal = QueryTrailingTerminal(referencedRule); if (chainTrailingTerminal == null) @@ -1195,14 +1429,7 @@ private bool TryEmitSubclassRuleDispatchAlternatives(EncodedTextWriter writer, I // A NonTerminal-valued assignment emits its own null guard inside ProcessAssignmentElement; // only a value-literal assignment (e.g. [QualifiedName]) needs the guard supplied here. - if (assignmentElement.Value is ValueLiteralElement) - { - writer.WriteSafeString($"else if ({targetProperty.QueryIfStatementContentForNonEmpty(variableName)}){Environment.NewLine}"); - } - else - { - writer.WriteSafeString($"else{Environment.NewLine}"); - } + writer.WriteSafeString(assignmentElement.Value is ValueLiteralElement ? $"else if ({targetProperty.QueryIfStatementContentForNonEmpty(variableName)}){Environment.NewLine}" : $"else{Environment.NewLine}"); writer.WriteSafeString($"{{{Environment.NewLine}"); this.ProcessAssignmentElement(writer, umlClass, ruleGenerationContext, assignmentElement, true); @@ -1441,14 +1668,8 @@ private void EmitTerminalVsBodyWithCollectionNonTerminals(EncodedTextWriter writ ? $".IsValidFor{collectionNonTerminals[0].Name}(writerContext)" : string.Empty; - if (requiresIsValidForGuard) - { - writer.WriteSafeString($"if (writerContext.CursorCache.GetOrCreateCursor(poco.Id, \"{targetProperty.Name}\", poco.{propertyAccessName}).Current is not SysML2.NET.Core.POCO.Root.Elements.IRelationship emptyBodyCandidate || !emptyBodyCandidate{guardCallSuffix}){Environment.NewLine}"); - } - else - { - writer.WriteSafeString($"if(writerContext.CursorCache.GetOrCreateCursor(poco.Id, \"{targetProperty.Name}\", poco.{propertyAccessName}).Current == null){Environment.NewLine}"); - } + writer.WriteSafeString(requiresIsValidForGuard ? $"if (writerContext.CursorCache.GetOrCreateCursor(poco.Id, \"{targetProperty.Name}\", poco.{propertyAccessName}).Current is not SysML2.NET.Core.POCO.Root.Elements.IRelationship emptyBodyCandidate || !emptyBodyCandidate{guardCallSuffix}){Environment.NewLine}" + : $"if(writerContext.CursorCache.GetOrCreateCursor(poco.Id, \"{targetProperty.Name}\", poco.{propertyAccessName}).Current == null){Environment.NewLine}"); writer.WriteSafeString($"{{{Environment.NewLine}"); writer.WriteSafeString($"stringBuilder.AppendLine(\"{terminalValue}\");{Environment.NewLine}"); @@ -1596,37 +1817,11 @@ private void EmitTerminalVsBodyWithSingleNonTerminal(EncodedTextWriter writer, I /// by an IsValidFor{Rule} predicate instead of a bare null-test. /// /// - /// Allowlisted by name, deliberately, because the four entries encode TWO unrelated concerns and no - /// single predicate can derive both: - /// - /// - /// CaseBodyItem, DefinitionBodyItem - /// A trailing consumer reads the SAME cursor after the loop, so an unguarded loop - /// swallows it — CaseBody's own ( ownedRelationship += ResultExpressionMember )?, and - /// PortDefinition's trailing ConjugatedPortDefinitionMember reached through - /// Definition → DefinitionBody. This is a property OF THE GRAMMAR and - /// derives it. - /// - /// - /// InterfaceBodyItem, ActionBodyItem - /// The item builder declines an element it cannot render WITHOUT advancing the cursor - /// — SharedTextualNotationBuilder's default: arm, and ActionBodyItem's outer - /// if (IsValidForActionBodyItem). The guard is what keeps such an element from ever reaching - /// the dispatcher. InterfaceBody is the clean witness that this is NOT the grammar concern: - /// it is the last element of both InterfaceDefinition and InterfaceUsage, so no - /// trailing consumer exists, yet the guard is still load-bearing. - /// - /// - /// The second concern is not derivable. It depends on the internal control flow of a - /// hand-written method: BuildStateBodyItemHandCoded is equally hand-coded yet DRAINS the - /// cursor in its own while, so it can never stall its caller — a "the item builder is - /// hand-coded" heuristic would over-guard it. Deciding it would mean analysing that C#. - /// What removes the risk instead is CollectionCursor.AssertAdvancedSince, emitted by - /// at the foot of every generated cursor loop: a stalled - /// iteration now throws immediately instead of hanging. That matters because a hang is invisible to - /// a corpus that compares output — dropping InterfaceBodyItem from this list once produced a - /// fully green 33-case run. With the assertion in place this allowlist governs OUTPUT CORRECTNESS - /// (do not swallow the result expression, do not emit a bare ref;) rather than termination. + /// Allowlisted by name because the entries encode two unrelated concerns: a trailing consumer reading + /// the same cursor after the loop (derivable, see ), and an + /// item builder that declines an element without advancing the cursor (not derivable — it depends on + /// the control flow of a hand-written method). The allowlist governs output correctness; + /// CollectionCursor.AssertAdvancedSince covers termination. /// /// /// Emits the capture of a cursor's position immediately before a loop body, and returns the name of @@ -1673,20 +1868,27 @@ private static bool IsGuardedBodyItemRule(string bodyItemRuleName) /// be supplied by a hand-coded IsValidFor{Rule} guard. /// /// - /// Currently FunctionOperationExpression. Its arm in NonFeatureChainPrimaryExpression + /// DefaultInterfaceEnd. Its own body yields only isEnd ?= 'end', but SysML 2.0 + /// §7.14.2 makes the port keyword optional on an interface end "if no owned cross feature is + /// declared on the end" — so an end carrying one must fall through to the sibling + /// StructureUsageElement arm, which reaches PortUsage and its EndUsagePrefix + /// cross-feature slot. An absence constraint on the element's own contents is not expressible as a + /// body-shape predicate. + /// FunctionOperationExpression. Its arm in NonFeatureChainPrimaryExpression /// targets InvocationExpression and sits above the SequenceExpression arm, which /// targets the supertype Expression. A sequence (a, b, c) is an OperatorExpression /// with operator = "," — hence an InvocationExpression whose first owned relationship is /// an IParameterMembership, which is all the unguarded arm tested — so every sequence was /// swallowed and rendered with a spurious ->. Telling the two apart needs the SECOND owned /// relationship (Membership for x->f(), ParameterMembership for a sequence), - /// i.e. cursor lookahead, which no body-shape analysis can produce. + /// i.e. cursor lookahead, which no body-shape analysis can produce. /// /// The KEBNF rule name of the alternative /// true if the codegen should emit a hand-coded IsValidFor{Rule} guard private static bool RequiresHandCodedAlternativeGuard(string alternativeRuleName) { - return string.Equals(alternativeRuleName, "FunctionOperationExpression", StringComparison.Ordinal); + return string.Equals(alternativeRuleName, "FunctionOperationExpression", StringComparison.Ordinal) + || string.Equals(alternativeRuleName, "DefaultInterfaceEnd", StringComparison.Ordinal); } /// diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleQueryUtilities.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleQueryUtilities.cs index 59ef7d791..4b64de28a 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleQueryUtilities.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleQueryUtilities.cs @@ -145,6 +145,33 @@ internal static List QueryBooleanAssignmentProperties(TextualNotationRul return result; } + /// + /// Collects every ?= property a rule can set, INCLUDING the conditional ones + /// (inside an optional / repeating / alternation position). + /// + /// The rule to inspect + /// All available rules for resolving NonTerminal references + /// The property names the rule may set. + /// + /// This is the set to test a SIBLING against when deciding whether a boolean is a valid + /// discriminator: a property the sibling merely MAY set is still one it can satisfy at runtime, + /// so guarding on it lets the first arm claim the sibling's instances. PortionUsage takes + /// ( isIndividual ?= 'individual' )? optionally while IndividualUsage takes it + /// unconditionally — testing only unconditional assignments made isIndividual look unique + /// and every individual snapshot was written as a plain individual. + /// + internal static List QueryAllBooleanAssignmentProperties(TextualNotationRule rule, IReadOnlyList allRules) + { + var result = new List(); + + foreach (var alternative in rule.Alternatives) + { + CollectBooleanAssignmentProperties(alternative.Elements, allRules, result, new HashSet(), isConditional: false, includeConditional: true); + } + + return result; + } + /// /// Recursively collects boolean ?= assignment property names from a /// list of . Only assignments at @@ -161,13 +188,17 @@ internal static List QueryBooleanAssignmentProperties(TextualNotationRul /// repeating / alternation group (directly or transitively) so any /// ?= reached from here cannot serve as a guaranteed discriminator. /// - internal static void CollectBooleanAssignmentProperties(IReadOnlyList elements, IReadOnlyList allRules, List result, HashSet visited, bool isConditional) + /// + /// true to collect conditional ?= assignments as well — used when testing what a + /// SIBLING alternative may set; see . + /// + internal static void CollectBooleanAssignmentProperties(IReadOnlyList elements, IReadOnlyList allRules, List result, HashSet visited, bool isConditional, bool includeConditional = false) { foreach (var element in elements) { switch (element) { - case AssignmentElement { Operator: "?=" } assignment when !isConditional: + case AssignmentElement { Operator: "?=" } assignment when includeConditional || !isConditional: result.Add(assignment.Property); break; @@ -180,7 +211,7 @@ internal static void CollectBooleanAssignmentProperties(IReadOnlyList(() => NullImpliedRelationshipProvider.Instance)); + + // KerML 7.4.7.2 matches a subclassifier's OWNED parameters against each superclassifier's + // `parameter`, which is `directedFeature` — so `base` contributes its inherited parameters, + // ordered after the one it owns, and the return parameter is excluded on both sides. + var baseBehavior = new Behavior { Id = Guid.NewGuid() }; + var baseFirst = AddParameter(baseBehavior); + var baseSecond = AddParameter(baseBehavior); + + var middle = new Behavior { Id = Guid.NewGuid() }; + var middleOwned = AddParameter(middle); + var middleResult = AddResult(middle); + middleResult.Direction = FeatureDirectionKind.Out; + Specialize(middle, baseBehavior); + + var subtype = new Behavior { Id = Guid.NewGuid() }; + var first = AddParameter(subtype); + var second = AddParameter(subtype); + var third = AddParameter(subtype); + var result = AddResult(subtype); + result.Direction = FeatureDirectionKind.Out; + Specialize(subtype, middle); + + using (Assert.EnterMultipleScope()) + { + Assert.That(rule.ConstraintName, Is.EqualTo("checkFeatureParameterRedefinition")); + + // `parameter` is every directed Feature, so it carries the return parameter and the + // inherited ones; the constraint rejects only the former. + Assert.That(middle.parameter, Is.EqualTo(new[] { middleOwned, middleResult, baseFirst, baseSecond })); + + Assert.That(RedefinedBy(rule, first), Is.EqualTo(new[] { middleOwned })); + Assert.That(RedefinedBy(rule, second), Is.EqualTo(new[] { baseFirst })); + Assert.That(RedefinedBy(rule, third), Is.EqualTo(new[] { baseSecond })); + + // A return parameter is not a parameter for this constraint, on either side. + Assert.That(rule.Apply(result), Is.Empty); + + Assert.That(() => rule.Apply(null), Throws.TypeOf()); + Assert.That(rule.Apply(new Feature { Id = Guid.NewGuid() }), Is.Empty); + Assert.That(rule.Apply(AddParameter(new Classifier { Id = Guid.NewGuid() })), Is.Empty); + } + } + [Test] public void VerifyFeatureResultRedefinitionRule() { @@ -208,7 +257,7 @@ public void VerifyEveryRedefinitionConstraintIsCoveredOrDeliberatelyNot() [ new FeatureEndRedefinitionRule(this.factory), new FeatureResultRedefinitionRule(this.factory), - new FeatureParameterRedefinitionRule(this.factory), + new FeatureParameterRedefinitionRule(this.factory, new Lazy(() => NullImpliedRelationshipProvider.Instance)), new RequirementUsageObjectiveRedefinitionRule(this.factory), new AssignmentActionUsageReferentRedefinitionRule(this.factory), new FeatureChainExpressionSourceTargetRedefinitionRule(this.factory), @@ -271,6 +320,14 @@ private static Feature AddResult(IType owner) return result; } + private static Feature AddParameter(IType owner) + { + var parameter = new Feature { Id = Guid.NewGuid(), Direction = FeatureDirectionKind.In }; + Own(owner, parameter, new FeatureMembership { Id = Guid.NewGuid() }); + + return parameter; + } + private static void Own(IElement owner, IElement owned, IRelationship membership = null) { var relationship = membership ?? new FeatureMembership { Id = Guid.NewGuid() }; diff --git a/SysML2.NET.Semantics/Extensions/ServiceCollectionExtensions.cs b/SysML2.NET.Semantics/Extensions/ServiceCollectionExtensions.cs index f14b12c01..84b542f8d 100644 --- a/SysML2.NET.Semantics/Extensions/ServiceCollectionExtensions.cs +++ b/SysML2.NET.Semantics/Extensions/ServiceCollectionExtensions.cs @@ -73,6 +73,7 @@ public static IServiceCollection AddSysML2Semantics(this IServiceCollection serv services.AddScoped(); services.AddScoped(serviceProvider => new ImpliedRuleGuardRegistry(serviceProvider.GetServices())); services.AddScoped(); + services.AddScoped(serviceProvider => new Lazy(serviceProvider.GetRequiredService)); services.AddImpliedRelationshipRule(); services.AddImpliedRelationshipRule(); diff --git a/SysML2.NET.Semantics/Implied/ImpliedRelationshipProvider.cs b/SysML2.NET.Semantics/Implied/ImpliedRelationshipProvider.cs index 198f0f94f..476b14b77 100644 --- a/SysML2.NET.Semantics/Implied/ImpliedRelationshipProvider.cs +++ b/SysML2.NET.Semantics/Implied/ImpliedRelationshipProvider.cs @@ -285,6 +285,10 @@ private static IReadOnlyList ApplyRule(IImpliedRelationshipRule r { return []; } + catch (NotSupportedException) + { + return []; + } } /// diff --git a/SysML2.NET.Semantics/Implied/Rules/FeatureParameterRedefinitionRule.cs b/SysML2.NET.Semantics/Implied/Rules/FeatureParameterRedefinitionRule.cs index 0d0a07545..8bfd60c6f 100644 --- a/SysML2.NET.Semantics/Implied/Rules/FeatureParameterRedefinitionRule.cs +++ b/SysML2.NET.Semantics/Implied/Rules/FeatureParameterRedefinitionRule.cs @@ -42,9 +42,9 @@ namespace SysML2.NET.Semantics.Implied.Rules /// ->select(direction <> null)->reject(owningFeatureMembership.oclIsKindOf( /// ReturnParameterMembership)) in … ownedParameters->size() >= i implies /// redefines(ownedParameters->at(i)). - /// Parameters are the directed owned Features EXCLUDING the return parameter, matched positionally - /// against the supertype's. An InvocationExpression that already declares an explicit (non-implied) - /// Redefinition is excluded, since the modeller has bound its arguments by hand. + /// The subtype contributes its directed OWNED Features excluding the return parameter; the supertype + /// contributes its parameter, matched positionally. An InvocationExpression that already declares an + /// explicit (non-implied) Redefinition is excluded, since the modeller has bound its arguments by hand. /// public class FeatureParameterRedefinitionRule : IImpliedRelationshipRule { @@ -53,14 +53,22 @@ public class FeatureParameterRedefinitionRule : IImpliedRelationshipRule /// private readonly IImpliedRelationshipFactory factory; + /// + /// The provider supplying the owning Type's implied Specializations, resolved lazily because it is + /// the same provider this rule is registered with. + /// + private readonly Lazy provider; + /// /// Initializes a new instance of the class. /// /// The factory creating the detached Redefinitions. - /// Thrown when is null. - public FeatureParameterRedefinitionRule(IImpliedRelationshipFactory factory) + /// The provider supplying the owning Type's implied Specializations. + /// Thrown when or is null. + public FeatureParameterRedefinitionRule(IImpliedRelationshipFactory factory, Lazy provider) { this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); + this.provider = provider ?? throw new ArgumentNullException(nameof(provider)); } /// @@ -96,15 +104,29 @@ public IReadOnlyList Apply(IElement element) return [ - ..parameter.owningType.ownedSpecialization - .Select(specialization => specialization.General) + ..this.QuerySupertypes(parameter.owningType) .Where(supertype => supertype is IBehavior or IStep) - .Select(QueryParameters) + .Select(QuerySupertypeParameters) .Where(supertypeParameters => supertypeParameters.Count > position) .Select(supertypeParameters => this.factory.CreateImpliedRedefinition(parameter, supertypeParameters[position])) ]; } + /// + /// Returns the direct supertypes of a Type over the effective model: those its declared + /// Specializations reach, together with those its implied Specializations reach (KerML §8.4.2). + /// + /// The Type owning the parameter. + /// The direct supertypes, declared ones first. + private IEnumerable QuerySupertypes(IType owningType) + { + return owningType.ownedSpecialization + .Concat(this.provider.Value.GetImpliedSpecializations(owningType)) + .Select(specialization => specialization.General) + .Where(supertype => supertype != null && !ReferenceEquals(supertype, owningType)) + .Distinct(); + } + /// /// Asserts whether a Type's parameters are subject to the constraint. /// @@ -131,5 +153,28 @@ private static List QueryParameters(IType type) .Where(ownedFeature => ownedFeature.Direction.HasValue) .Where(ownedFeature => ownedFeature.owningFeatureMembership is not IReturnParameterMembership)]; } + + /// + /// Returns the parameters a supertype offers for positional matching, excluding the return parameter. + /// + /// The supertype to inspect. + /// The parameters, owned ones first and inherited ones after. + /// + /// The supertype side is parameter — which KerML defines as directedFeature, so inherited + /// parameters count and are ordered after owned ones — because §7.4.7.2 requires a subclassifier's owned + /// parameters to redefine "the parameter at the same position" of each superclassifier. The constraint's + /// OCL formalises this side as ownedFeature, which is narrower than the clause it formalises. + /// + private static List QuerySupertypeParameters(IType supertype) + { + var parameters = supertype switch + { + IBehavior behavior => behavior.parameter, + IStep step => step.parameter, + _ => supertype.directedFeature + }; + + return [..parameters.Where(parameter => parameter.owningFeatureMembership is not IReturnParameterMembership)]; + } } } diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1a-Parts Tree.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1a-Parts Tree.sysml index 4acd7af5b..3e1f0ea56 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1a-Parts Tree.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1a-Parts Tree.sysml @@ -5,9 +5,9 @@ package '1a-Parts Tree' { attribute mass :> ISQ::mass { doc /* - * The 'mass' attribute property is declared here to be a - * specialization (subset) of the general 'mass' quantity - * from the 'ISQ' (International System of Quantities) + * The 'mass' attribute property is declared here to be a + * specialization (subset) of the general 'mass' quantity + * from the 'ISQ' (International System of Quantities) * library model. */ @@ -35,7 +35,7 @@ package '1a-Parts Tree' { */ attribute mass :>> Vehicle::mass = 1750[kg] { /* - * This redefines the 'mass' attribute property from 'Vehicle' to + * This redefines the 'mass' attribute property from 'Vehicle' to * give it a fixed attribute. */ } diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1c-Parts Tree Redefinition.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1c-Parts Tree Redefinition.sysml index 61591ce10..f85aa6757 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1c-Parts Tree Redefinition.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1c-Parts Tree Redefinition.sysml @@ -34,7 +34,7 @@ package '1c-Parts Tree Redefinition' { } part vehicle1_c1 :> vehicle1 { /* - * 'vehicle1_c1' is a specialization of 'vehicle1' (technically + * 'vehicle1_c1' is a specialization of 'vehicle1' (technically * a subset). It inherits all the parts of 'vehicle1' and * only needs to specify additional or redefined parts. */ diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1d-Parts Tree with Reference.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1d-Parts Tree with Reference.sysml index 5169c7330..458f38171 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1d-Parts Tree with Reference.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1d-Parts Tree with Reference.sysml @@ -13,7 +13,7 @@ package '1d-Parts Tree with Reference' { ref hitchBall: HitchBall { /* * 'vehicle1_c1'::'hitchBall' is a reference property that - * references a hitch ball that is not part of this vehicle. + * references a hitch ball that is not part of this vehicle. * If 'vehicle1_c1' is removed or destroyed, this does not * effect the hitchBall referenced here. */ @@ -35,7 +35,7 @@ package '1d-Parts Tree with Reference' { * This is a shorthand for a binding connector between the * 'trailerCoupler' here and the 'trailerCoupler' in 'trailerHitch'. * The binding connector is now contained within the 'trailer1' - * part, though, rather than being at the system level. + * part, though, rather than being at the system level. */ } } diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/02-Parts Interconnection/2a-Parts Interconnection.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/02-Parts Interconnection/2a-Parts Interconnection.sysml index 54ca2d4ec..7763424bb 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/02-Parts Interconnection/2a-Parts Interconnection.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/02-Parts Interconnection/2a-Parts Interconnection.sysml @@ -84,7 +84,7 @@ package '2a-Parts Interconnection' { part engine: Engine; interface : EngineToTransmissionInterface connect engine.drivePwrPort to transmission.clutchPort { /* - * A usage of an interface definition connects two ports relative to + * A usage of an interface definition connects two ports relative to * a containing context. */ } @@ -117,7 +117,7 @@ package '2a-Parts Interconnection' { } interface differential.leftDiffPort to rearAxle.leftHalfAxle.axleToDiffPort { /* - * A connection can be to a port that is arbitrarily deeply nested, on either end. + * A connection can be to a port that is arbitrarily deeply nested, on either end. */ } interface differential.rightDiffPort to rearAxle.rightHalfAxle.axleToDiffPort; diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/04-Functional Allocation/4a-Functional Allocation.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/04-Functional Allocation/4a-Functional Allocation.sysml index 4789b46c0..622fcc4de 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/04-Functional Allocation/4a-Functional Allocation.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/04-Functional Allocation/4a-Functional Allocation.sysml @@ -9,7 +9,7 @@ package '4a-Functional Allocation' { perform 'provide power' { doc /* - * This allocates the action '3a-Function-based Behavior-1'::'provide power' as an enacted + * This allocates the action '3a-Function-based Behavior-1'::'provide power' as an enacted * performance of 'vehicle_c1_functional_allocation'. */ @@ -21,7 +21,7 @@ package '4a-Functional Allocation' { } perform 'provide power'.'generate torque' { /* - * This allocates one of the sub-steps of 'provide power' to a sub-part of vehicle_c1. + * This allocates one of the sub-steps of 'provide power' to a sub-part of vehicle_c1. */ in fuelCmd = fuelCmdPort.fuelCmd; out engineTorque = drivePwrPort.engineTorque; diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-1.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-1.sysml index 9b649bd01..ff28c2da4 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-1.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-1.sysml @@ -38,7 +38,7 @@ package '5-State-based Behavior-1' { package Usages { private import Definitions::*; /* - * These actions are used enabled in the state usage + * These actions are used enabled in the state usage * 'vehicle states', in addition to 'provide power'. */ action 'perform self test': 'Perform Self Test'; diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-1a.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-1a.sysml index 55998edf9..ff1cb8d12 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-1a.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-1a.sysml @@ -39,7 +39,7 @@ package '5-State-based Behavior-1a' { package Usages { private import Definitions::*; /* - * These actions are used enabled in the state usage + * These actions are used enabled in the state usage * 'vehicle states', in addition to 'provide power'. */ action 'provide power': 'Provide Power'; diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-2.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-2.sysml index 2e7a7f0d1..09fabdb80 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-2.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/05-State-based Behavior/5-State-based Behavior-2.sysml @@ -35,7 +35,7 @@ package '5-State-based Behavior-2' { entry; then off; /* - * The following uses a shorthand for a transition whose source + * The following uses a shorthand for a transition whose source * is the immediately preceding state. */ state off; diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/06-Individual and Snapshots/6-Individual and Snapshots.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/06-Individual and Snapshots/6-Individual and Snapshots.sysml index 21fe49047..3bdc641e4 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/06-Individual and Snapshots/6-Individual and Snapshots.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/06-Individual and Snapshots/6-Individual and Snapshots.sysml @@ -91,8 +91,8 @@ package '6-Individual and Snapshots' { :>> acceleration = a0; exhibit vehicleStates.on { /* - * This asserts that the snapshot exhibits the referenced - * state, which means that the vehicle must me in the state + * This asserts that the snapshot exhibits the referenced + * state, which means that the vehicle must me in the state * at the time of the snapshot. */ } diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/07-Variant Configuration/7a1-Variant Configuration - General Concept-a.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/07-Variant Configuration/7a1-Variant Configuration - General Concept-a.sysml index dacbec9db..f8f28bb21 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/07-Variant Configuration/7a1-Variant Configuration - General Concept-a.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/07-Variant Configuration/7a1-Variant Configuration - General Concept-a.sysml @@ -49,7 +49,7 @@ package '7a1-Variant Configuration - General Concept-a' { part :>> subsystemA = subsystemA::subsystem1; part :>> subsystemB = subsystemB::subsystem3 { part :>> part5 { - perform action :>> doXorY = '7a1-Variant Configuration - General Concept-a'::doX; + perform action :>> doXorY = doX; } } } @@ -57,7 +57,7 @@ package '7a1-Variant Configuration - General Concept-a' { part :>> subsystemA = subsystemA::subsystem2; part :>> subsystemB = subsystemB::subsystem4 { part :>> part5 { - perform action :>> doXorY = '7a1-Variant Configuration - General Concept-a'::doY; + perform action :>> doXorY = doY; } } } diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/14-Language Extensions/14a-Language Extensions.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/14-Language Extensions/14a-Language Extensions.sysml index affcb27e2..68f0beb26 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/14-Language Extensions/14a-Language Extensions.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/14-Language Extensions/14a-Language Extensions.sysml @@ -7,7 +7,7 @@ package '14a-Language Extensions' { enum secret; } metadata def Classified { - :>> annotatedElement : SysML::Systems::PartUsage; + :>> annotatedElement : SysML::PartUsage; attribute classificationLevel: ClassificationLevel[1]; } } diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/14-Language Extensions/14c-Language Extensions.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/14-Language Extensions/14c-Language Extensions.sysml index 0d2b88335..9acdfb2e9 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/14-Language Extensions/14c-Language Extensions.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/14-Language Extensions/14c-Language Extensions.sysml @@ -62,42 +62,42 @@ package '14c-Language-Extensions' { ref status: Status; } metadata def SituationMetadata :> SemanticMetadata { - :>> baseType default = situations meta SysML::Systems::Usage; + :>> baseType default = situations meta SysML::Usage; } metadata def CauseMetadata :> situation { - :>> baseType = causes meta SysML::Systems::Usage; + :>> baseType = causes meta SysML::Usage; } metadata def FailureModeMetadata :> situation { - :>> baseType = failureModes meta SysML::Systems::Usage; + :>> baseType = failureModes meta SysML::Usage; } metadata def EffectMetadata :> situation { - :>> baseType = effects meta SysML::Systems::Usage; + :>> baseType = effects meta SysML::Usage; } metadata def FMEAItemMetadata :> situation { - :> annotatedElement : SysML::Systems::ItemDefinition; - :> annotatedElement : SysML::Systems::ItemUsage; - :>> baseType = fmeaItems meta SysML::Systems::Usage; + :> annotatedElement : SysML::ItemDefinition; + :> annotatedElement : SysML::ItemUsage; + :>> baseType = fmeaItems meta SysML::Usage; } metadata def CausationMetadata :> SemanticMetadata { - :>> annotatedElement : SysML::Systems::ConnectionUsage; - :>> baseType = causations meta SysML::Systems::Usage; + :>> annotatedElement : SysML::ConnectionUsage; + :>> baseType = causations meta SysML::Usage; } metadata def FMEARequirementMetadata :> SemanticMetadata { - :>> annotatedElement : SysML::Systems::RequirementUsage; - :>> baseType = fmeaRequirements meta SysML::Systems::Usage; + :>> annotatedElement : SysML::RequirementUsage; + :>> baseType = fmeaRequirements meta SysML::Usage; } metadata def ViolationMetadata :> SemanticMetadata { - :>> annotatedElement : SysML::Systems::ConnectionUsage; - :>> baseType = violations meta SysML::Systems::Usage; + :>> annotatedElement : SysML::ConnectionUsage; + :>> baseType = violations meta SysML::Usage; } abstract metadata def ControllingMeasureMetadata :> SemanticMetadata { - :>> annotatedElement : SysML::Systems::ConnectionUsage; + :>> annotatedElement : SysML::ConnectionUsage; } metadata def PreventionMetadata :> ControllingMeasureMetadata { - :>> baseType = preventions meta SysML::Systems::Usage; + :>> baseType = preventions meta SysML::Usage; } metadata def MitigationMetadata :> ControllingMeasureMetadata { - :>> baseType = mitigations meta SysML::Systems::Usage; + :>> baseType = mitigations meta SysML::Usage; } } package FMEAUserModel { diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_01-Constants.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_01-Constants.sysml index c2a91542b..282003bae 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_01-Constants.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_01-Constants.sysml @@ -35,7 +35,7 @@ package '15_01-Constants' { * The reference source is: * CODATA - Task Group on Fundamental Physical Constants (TGFC) - 2018 CODATA recommended values * See https://codata.org/initiatives/strategic-programme/fundamental-physical-constants/ - * For the actual values see https://pml.nist.gov/cuu/Constants/ + * For the actual values see https://pml.nist.gov/cuu/Constants/ */ attribute 'fine structure constant': DimensionOneValue = 0.0072973525693[one]; diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_05-Unification of Expression and Constraint Definition.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_05-Unification of Expression and Constraint Definition.sysml index f48f52bb0..3ce8b64b3 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_05-Unification of Expression and Constraint Definition.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_05-Unification of Expression and Constraint Definition.sysml @@ -9,7 +9,7 @@ package '15_05-Unification of Expression and Constraint Definition' { attribute length: LengthValue[1] = 4.82[m]; part wheelAssy: WheelAssy[4]; constraint discBrakeConstraint: DiscBrakeConstraint { doc /* - * This constraint is computed, but not asserted. This means a tool can identify + * This constraint is computed, but not asserted. This means a tool can identify * when it is violated without the model being inconsistent. */ in wheelAssy = Vehicle_2::wheelAssy; } diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_06-System of Quantities.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_06-System of Quantities.sysml index 622197b63..7ba18004a 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_06-System of Quantities.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_06-System of Quantities.sysml @@ -6,17 +6,17 @@ package '15_06-System of Quantities' { * Its structure is modeled after the International System of Quantities (ISQ): * - Quantity dimension is defined as the product of powers of a selected set of base quantities. * - A system of quantities is multi-dimensional space spanned by the powers of its base quantities. - * - Any base quantity is modeled as a specialization of a SimpleUnit. Such a specialized SimpleUnit defines one base unit vector - * (with power one by definition), e.g. MassUnit with symbol M, that establishes a base quantity dimension for the system of quantities, + * - Any base quantity is modeled as a specialization of a SimpleUnit. Such a specialized SimpleUnit defines one base unit vector + * (with power one by definition), e.g. MassUnit with symbol M, that establishes a base quantity dimension for the system of quantities, * without committing yet to a particular choice of measurement unit. * - To complete the system of quantities any number of derived quantities can be added. - * - A derived quantity is modeled as a specialization of a DerivedUnit. A DerivedUnit is defined in terms of so-called UnitPowerFactors. + * - A derived quantity is modeled as a specialization of a DerivedUnit. A DerivedUnit is defined in terms of so-called UnitPowerFactors. * Each UnitPowerFactor is a combination of a base (or other derived) quantity and an exponent. - * - As an example the AccelerationUnit (specialization of DerivedUnit) can be defined as the combination of LengthUnit (symbol L) + * - As an example the AccelerationUnit (specialization of DerivedUnit) can be defined as the combination of LengthUnit (symbol L) * to the power 1 and TimeUnit (symbol T) to the power -2, so having quantity dimension L¹⋅T⁻². - * - A quantity of dimension one is defined as a derived quantity for which the effective exponent for each + * - A quantity of dimension one is defined as a derived quantity for which the effective exponent for each * of its base quantity power factors is zero. Historically a quantity of dimension one was also called a dimensionless quantity. - * - A quantity of dimension one may be defined by adding all quantity power factors that cancel out by having positive and negative + * - A quantity of dimension one may be defined by adding all quantity power factors that cancel out by having positive and negative * exponents. Doing so enables distinction between different 'kinds of' quantities of dimension one, e.g: * angle (L¹⋅L⁻¹), mass ratio (L¹⋅L⁻¹), power ratio (L²⋅M⋅T⁻³⋅L⁻²⋅M⁻¹⋅T³), Mach number (L¹⋅T⁻¹⋅L⁻¹⋅T¹). * @@ -27,7 +27,7 @@ package '15_06-System of Quantities' { * Above capabilities were implemented in: * - standard library Quantities: * TensorQuantityValue, VectorQuantityValue, ScalarQuantityValue, - * tensorQuantities, vectorQuantities, scalarQuantities, + * tensorQuantities, vectorQuantities, scalarQuantities, * SystemOfQuantities * - standard library MeasurementReferences: * TensorMeasurementReference, VectorMeasurementReference, ScalarMeasurementReference, diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_07-System of Units and Scales.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_07-System of Units and Scales.sysml index 3bcd8857d..4896b6b17 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_07-System of Units and Scales.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_07-System of Units and Scales.sysml @@ -10,17 +10,17 @@ package '15_07-System of Units and Scales' { * - An actual measurement unit is modeled as a usage of a specialization of either SimpleUnit or DerivedUnit, e.g. TimeUnit or ForceUnit, * see the SI package. * - The quantity dimension of the actual unit usage must match the quantity dimension of the generic quantity unit definition that it is a usage of. - * - A system of units and scales must define exactly one selected base unit for each base quantity in the associated system of quantities. The collection of + * - A system of units and scales must define exactly one selected base unit for each base quantity in the associated system of quantities. The collection of * base units forms the foundation for automated quantity value conversion between any pair of compatible units and/or scales. * - If only a measurement unit is used on a quantity value, it implies expression on a ratio scale, in other words only the ratio between the actual quantity value, - * and the defined unit value is of importance. On ratio scales for one kind of quantity that only differ in their unit (e.g. metre and inch) + * and the defined unit value is of importance. On ratio scales for one kind of quantity that only differ in their unit (e.g. metre and inch) * zero is zero no matter what unit is selected. - * - A unit may carry a conversion factor definition w.r.t. to another reference unit. It can be a conversion by convention (e.g. between metre and foot) or - * via an ISO/IEC 80000 prefix symbol that indicates a decimal or binary multiple or sub-multiple (e.g. kilo, nano, mega, kibi, mebi, ...). See package SIPrefixes. + * - A unit may carry a conversion factor definition w.r.t. to another reference unit. It can be a conversion by convention (e.g. between metre and foot) or + * via an ISO/IEC 80000 prefix symbol that indicates a decimal or binary multiple or sub-multiple (e.g. kilo, nano, mega, kibi, mebi, ...). See package SIPrefixes. * - In addition to measurement units / ratio scales also other types of measurement scales are supported. The additional scales are: - * - ordinal scales (e.g. Beaufort wind force, Richter Scale, Rockwell C hardness scale), - * - interval scales (e.g. absolute temperature in deg C or F), - * - cyclic ratio scales (e.g. rotation angle with modulus 360 degree), + * - ordinal scales (e.g. Beaufort wind force, Richter Scale, Rockwell C hardness scale), + * - interval scales (e.g. absolute temperature in deg C or F), + * - cyclic ratio scales (e.g. rotation angle with modulus 360 degree), * - logarithmic scales (e.g. dB(A) or dBA sound pressure level w.r.t. a reference ambient pressure, dB(m) or dBm power ratio w.r.t. 1 mW). * - Any base unit quantity is modeled as a specialization of a SimpleUnit. This specialized SimpleUnit (e.g. MassUnit) defines one base unit vector (with power one by definition) * that establishes a base quantity dimension for the system of quantities, without committing yet to a particular choice of measurement unit. @@ -33,7 +33,7 @@ package '15_07-System of Units and Scales' { * Above capabilities were implemented in: * - standard library MeasurementReferences: * TensorMeasurementReference, VectorMeasurementReference, ScalarMeasurementReference, - * MeasurementUnit, OrdinalScale, IntervalScale, CyclicRatioScale, LogarithmicScale, + * MeasurementUnit, OrdinalScale, IntervalScale, CyclicRatioScale, LogarithmicScale, * SystemOfUnits * - standard library SI: * attribute 'ISO/IEC 80000 International System of Units' : SystemOfUnits diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_10-Primitive Data Types.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_10-Primitive Data Types.sysml index 9679256c6..79da45b81 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_10-Primitive Data Types.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/15-Properties-Values-Expressions/15_10-Primitive Data Types.sysml @@ -1,7 +1,7 @@ package '15.10-Primitive Data Types' { /* * Primitive data types are defined in normative model libraries. - * Any more specialized data types can be declared in user-defined + * Any more specialized data types can be declared in user-defined * model libraries or models as needed. */ private import ScalarValues::Integer { diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/18-Use Case/18-Use Case.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/18-Use Case/18-Use Case.sysml index 8eee106b3..9e0814247 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/18-Use Case/18-Use Case.sysml +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/18-Use Case/18-Use Case.sysml @@ -11,9 +11,9 @@ package '18-Use Case' { objective { doc /* - * Satisfy mission requirements to transport driver and passengers - * from starting location to ending location in conformance with - * the driving profile and meet the mission requirements for safety, + * Satisfy mission requirements to transport driver and passengers + * from starting location to ending location in conformance with + * the driving profile and meet the mission requirements for safety, * reliability, comfort, and affordability. */ diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ActionTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ActionTest.sysml new file mode 100644 index 000000000..b7c2227a9 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ActionTest.sysml @@ -0,0 +1,48 @@ +package ActionTest { + action def A { + in x; + } + action a: A { + first start; + action b { + in y = x; + } + bind x = b.y; + } + attribute def S; + action a1 { + first start; + then merge m; + then accept S; + then accept sig after 10[SI::s]; + then accept at new Time::Iso8601DateTime("2022-01-30T01:00:00Z"); + then send new S() to b; + then accept when b.f; + then decide; + if true then m; + else done; + } + action a2 { + in s: S; + action aa { + out part target; + } + flow aa.target to snd.receiver; + snd send { + in :>> payload = s; + } + snd2 send via this to aa.target; + bind s = snd2.payload; + } + action b { + attribute f: ScalarValues::Boolean; + ref action a: A; + } + action def c { + first start; + then action c1 { + terminate c1; + } + then terminate; + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AliasTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AliasTest.sysml new file mode 100644 index 000000000..9186123b8 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AliasTest.sysml @@ -0,0 +1,17 @@ +package AliasTest { + private import ISQSpaceTime::breadth; + attribute b :> breadth; + part def P1 { + port porig1; + alias po1 for porig1; + } + part p1: P1 { + port po1 :>> porig1; + } + part p2: P1 { + port pdest; + alias pd1 for pdest; + } + connect p1.po1 to p2.pdest; + connect p1.po1 to p2.pdest; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AllocationTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AllocationTest.sysml new file mode 100644 index 000000000..84fbce0d2 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AllocationTest.sysml @@ -0,0 +1,27 @@ +package AllocationTest { + part def Logical { + part component; + } + part def Physical { + part assembly { + part element; + } + } + part l: Logical { + part :>> component; + } + part p: Physical { + part :>> assembly { + part :>> element; + } + allocate l.component to assembly.element; + } + allocation def A; + allocation def Logical_to_Physical :> A { + end ref logical: Logical; + end ref physical: Physical; + } + allocation allocation1: Logical_to_Physical allocate l to p; + allocation allocation2: Logical_to_Physical allocate logical ::> l to physical ::> p; + allocate l.component to p.assembly.element; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AnalysisTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AnalysisTest.sysml new file mode 100644 index 000000000..915ce5208 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AnalysisTest.sysml @@ -0,0 +1,31 @@ +package AnalysisTest { + part def V { + ref m; + } + part vv: V; + requirement def AnalysisObjective { + doc + /* ... */ + } + analysis def AnalysisCase { + subject v: V; + objective obj: AnalysisObjective { + subject = Requirements::RequirementConstraintCheck::result; + } + v.m } + analysis def AnalysisPlan { + subject v: V; + objective { + doc + /* ... */ + } + analysis analysisCase: AnalysisCase { + return mass; + } + } + part analysisContext { + analysis analysisPlan: AnalysisPlan { + subject v = vv; + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AssignmentTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AssignmentTest.sysml new file mode 100644 index 000000000..a93a0f084 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/AssignmentTest.sysml @@ -0,0 +1,34 @@ +package AssignmentTest { + part def Counter { + attribute count: ScalarValues::Integer := 0; + action incr { + assign count := count + 1; + } + action decr { + assign count := count - 1; + } + } + attribute def Incr; + attribute def Decr; + state def Counting { + part counter: Counter; + entry assign counter.count := 0; + then state wait; + accept Incr then increment; + accept Decr then decrement; + state increment { + do assign counter.count := counter.count + 1; + } + then wait; + state decrement { + do assign counter.count := counter.count - 1; + } + then wait; + } + calc def Increment { in c: Counter; return : Counter; perform c.incr; c } + action a { + state counting: Counting; + assign counting.counter.count := counting.counter.count + 1; + assign counting.counter.count := Increment(counting.counter).count; + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/CalculationTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/CalculationTest.sysml new file mode 100644 index 000000000..4decaabff --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/CalculationTest.sysml @@ -0,0 +1,18 @@ +package CalculationExample { + private import ISQ::*; + private import NumericalFunctions::*; + part def VehiclePart { + attribute m: MassValue; + } + part def Vehicle :> VehiclePart; + part vehicle: Vehicle { + part eng: VehiclePart; + part trans: VehiclePart; + attribute ::> m = ms.totalMass; + } + calc def MassSum { in partMasses: MassValue[0..*]; return totalMass: MassValue = sum(partMasses); } + calc ms: MassSum { in partMasses = (vehicle.eng.m, vehicle.trans.m); return totalMass; } + part vehicles[*] = (vehicle, vehicle); + attribute masses1[*] = (vehicles as VehiclePart).m; + attribute masses2[*] = (vehicles as vehicle).m; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/CommentTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/CommentTest.sysml new file mode 100644 index 000000000..1fa79a8b0 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/CommentTest.sysml @@ -0,0 +1,42 @@ +/* AAA */ +package CommentTest { + /* + * AAA + * BBB*/ + /* + * + * + * AAA *** + * BBB + */ + /* + * AAAA + * BBBB */ + /* + * AAAA + * + * + * BBBB + * + * CCCC + */ + locale "en_US" /* + * AAAA + * BBBB + * CCC DDD + */ + /* comment inside a package */ + doc locale "en_US" + /* Documentation about Package */ + comment cmt /* Named Comment */ + comment cmt_cmt about cmt /* Comment about Comment */ + comment about C /* Documention Comment about Part Def */ + part def C { + doc + /* Documentation in Part Def */ + /* Comment in Part Def */ + comment about CommentTest locale "en_US" /* Comment about Package */ + } + /* abc */ + part def A; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ConjugationTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ConjugationTest.sysml new file mode 100644 index 000000000..7d1151497 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ConjugationTest.sysml @@ -0,0 +1,29 @@ +package ConjugationTest { + port def P; + part def B { + port p1: P; + port p2: ~P; + } + connection def A { + end port p1: P; + end port p2: ~P; + } + interface def I { + end p1: P; + end p2: ~P; + } + part def B1 { + part p { + port p1: P; + port p2: ~P; + } + connection a: A { + end port p3: P ::> p.p1; + end port p4: ~P ::> p.p2; + } + interface i: I { + end p3: P ::> p.p1; + end p4: ~P ::> p.p2; + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ConnectionTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ConnectionTest.sysml new file mode 100644 index 000000000..3c2c50fc5 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ConnectionTest.sysml @@ -0,0 +1,56 @@ +package ConnectionTest { + part p { + part x { + part x1; + } + } + part def P { + part y; + connect p to y; + part p1 :> p; + connect p1.x to y; + connect p1.x.x1 to y; + part a; + part b; + bind a = b; + binding ab bind a = b; + binding ab1: AB bind a = b; + first a then b; + succession s first a then b; + succession s1: AB first a then b; + } + abstract connection def C { + part p; + end ref end1; + end ref end2; + end ref end3; + } + part d1; + part d2; + part d3; + part d4; + connection bus: C connect (d1, d2, d3, d4); + connection : C { + end ref :>> end1 ::> d1; + end ref end2 ::> d2; + end ref end3 ::> d3; + } + connection { + part q; + end ref end1 ::> d1 :> q; + end ref end2 ::> d2; + } + abstract flow def F; + abstract message : F from p to p; + part def A { + ref b: B; + } + part def B; + connection def AB { + end[1] item a: A { + @ M; + } + end ref b: B; + } + metadata def M; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ConstraintTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ConstraintTest.sysml new file mode 100644 index 000000000..77aad3a04 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ConstraintTest.sysml @@ -0,0 +1,45 @@ +package ConstraintTest { + private import ISQ::MassValue; + private import SI::kg; + private import NumericalFunctions::sum; + constraint def MassAnalysis { attribute totalMass: MassValue; attribute componentMasses: MassValue[0..*]; totalMass == sum(componentMasses) } + part def Component { + attribute mass: MassValue; + } + part vehicle: Component { + part engine: Component; + part frontAxleAssembly: Component; + part rearAxleAssembly: Component; + } + part vehicle1a :> vehicle { + assert constraint massAnalysis: MassAnalysis { attribute :>> totalMass; attribute :>> componentMasses; } + bind massAnalysis.totalMass = mass; + bind massAnalysis.componentMasses = engine.mass; + bind massAnalysis.componentMasses = frontAxleAssembly.mass; + bind massAnalysis.componentMasses = rearAxleAssembly.mass; + } + part vehicle1b :> vehicle { + assert constraint massAnalysis: MassAnalysis { attribute :>> totalMass = mass; attribute :>> componentMasses = (engine.mass, frontAxleAssembly.mass, rearAxleAssembly.mass); } + } + constraint def MassAnalysis2 { in totalMass: MassValue; in componentMasses: MassValue[0..*]; totalMass == sum(componentMasses) } + part vehicle2a :> vehicle { + assert constraint massConstraint: MassAnalysis2; + bind massConstraint.totalMass = mass; + bind massConstraint.componentMasses = engine.mass; + bind massConstraint.componentMasses = frontAxleAssembly.mass; + bind massConstraint.componentMasses = rearAxleAssembly.mass; + } + part vehicle2b :> vehicle { + assert constraint massAnalysis2: MassAnalysis2 { in totalMass = mass; in componentMasses = (engine.mass, frontAxleAssembly.mass, rearAxleAssembly.mass); } + } + constraint def MassAnalysis3 { in totalMass: MassValue; in componentMasses: MassValue[0..*]; } + constraint massAnalysis3: MassAnalysis3 { in totalMass: MassValue; in componentMasses: MassValue[0..*]; totalMass == sum(componentMasses) } + part vehicle3 :> vehicle { + assert massAnalysis3 { in totalMass = mass; in componentMasses = (engine.mass, frontAxleAssembly.mass, rearAxleAssembly.mass); } + } + part vehicle4 :> vehicle { + assert constraint { mass == (engine.mass + frontAxleAssembly.mass) + rearAxleAssembly.mass } + } + constraint massLimitation { ref mass: MassValue; ref massLimit: MassValue; mass < massLimit } + assert not massLimitation { :>> mass = vehicle3.mass; :>> massLimit = vehicle4.mass; } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ControlNodeTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ControlNodeTest.sysml new file mode 100644 index 000000000..b6463b6a0 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ControlNodeTest.sysml @@ -0,0 +1,28 @@ +action def ControlNodeTest { + action A1; + then J; + action A2 { + out a; + } + then J; + flow A2.a to F.a; + join J; + then fork F { + in a; + out b1; + out b2; + } + then B1; + then B2; + flow F.b1 to B1.b; + flow F.b2 to B2.b; + action B1 { + in b; + } + then M; + action B2 { + in b; + } + then M; + merge M; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/DecisionTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/DecisionTest.sysml new file mode 100644 index 000000000..bda01509a --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/DecisionTest.sysml @@ -0,0 +1,16 @@ +action def DecisionTest { + attribute x = 1; + decide 'test x'; + if x == 1 then A1; + if x > 1 then A2; + else A3; + then decide D; + if true then A1; + if false then A2; + action A1; + action A2; + action A3; + succession S first A1 if x == 0 then A2; + first A3; + if x > 0 then 'test x'; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/DefaultValueTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/DefaultValueTest.sysml new file mode 100644 index 000000000..09232c0ed --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/DefaultValueTest.sysml @@ -0,0 +1,13 @@ +package DefaultValueTest { + part def V { + attribute m default = 10; + attribute n = 20; + } + part v1: V { + attribute :>> m = 20; + } + part def W :> V { + attribute :>> m default = n; + } + part v2 = new W(); +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/DependencyTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/DependencyTest.sysml new file mode 100644 index 000000000..3253d6e96 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/DependencyTest.sysml @@ -0,0 +1,14 @@ +package DependencyTest { + package System { + package 'Application Layer'; + package 'Service Layer'; + package 'Data Layer'; + } + private import System::*; + dependency Use from 'Application Layer' to 'Service Layer'; + dependency 'Service Layer' to 'Data Layer'; + attribute x; + attribute y; + attribute z; + dependency z to x, y; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/EnumerationTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/EnumerationTest.sysml new file mode 100644 index 000000000..249858771 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/EnumerationTest.sysml @@ -0,0 +1,52 @@ +package EnumerationTest { + attribute def Color { + attribute val: ScalarValues::Natural; + } + enum def ColorKind :> Color { + doc + /* + * An EnumerationDefinition can contain only EnumerationUsages. However, + * it can specialize an AttributeDefinition in order to inherit + * common features for its enumeration values. + */ + + enum red { + :>> val = 0; + } + enum blue { + :>> val = 1; + } + enum green { + :>> val = 2; + } + } + enum color: ColorKind; + enum color1 = ColorKind::blue; + attribute color2: ColorKind = color1; + enum def E1 { + enum a; + enum b; + enum c; + doc + /* + * The "enum" keyword is optional for EnumerationUsages used to define the + * enumerated values of an EnumerationDefinition. + */ + + } + enum def E2; + attribute def Size :> ScalarValues::Real { + doc + /* + * An EnumerationDefinition can also be used to restrict a supertype to + * specific values. + */ + + } + enum def SizeChoice :> Size { + enum = 60.0; + enum = 70.0; + enum = 80.0; + } + enum size: SizeChoice = 60.0; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/FeaturePathTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/FeaturePathTest.sysml new file mode 100644 index 000000000..e7c19aa50 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/FeaturePathTest.sysml @@ -0,0 +1,32 @@ +package Q { + part def F { + part a: A; + } + part f: F; + part def A { + part g = f.a; + } + part def B { + part f: F; + part a: A; + } + part def C { + part b: B { + connect f.a to a.g; + bind f.a = a.g; + } + part c :> b.f { + part aa :> a; + } + flow b.f.a to c.aa; + } + part e1 { + attribute x: E; + bind e1.x = E::e2; + } + enum def E { + enum e1; + enum e2; + } + part g = new A().g.g.g; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/IndividualTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/IndividualTest.sysml new file mode 100644 index 000000000..486d4cc5e --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/IndividualTest.sysml @@ -0,0 +1,33 @@ +package IndividualTest { + individual def IO1; + individual def IO2 { + individual io: IO1; + } + individual item def II1 { + individual item ii: II1; + } + item def I { + part i: I; + } + individual item def II2 :> I { + individual item :>> i : II2; + } + individual part def IP1 { + individual part p: IP1; + } + part def P { + part p: P; + } + individual part def IP2 :> P { + individual part :>> p : IP2; + } + individual action def AP1 { + individual action a: AP1; + } + action def A { + action a: A; + } + individual action def IA2 :> A { + individual action :>> a : IA2; + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ItemTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ItemTest.sysml new file mode 100644 index 000000000..1c209cfda --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ItemTest.sysml @@ -0,0 +1,17 @@ +package ItemTest { + item f: A; + item def A { + item b: B; + protected ref part c: C; + } + abstract item def B { + abstract part a: A; + } + private part def C { + private in y: A, B; + } + port def P { + in item a1: A; + out item a2: A; + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/MultiplicityTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/MultiplicityTest.sysml new file mode 100644 index 000000000..4310363ba --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/MultiplicityTest.sysml @@ -0,0 +1,15 @@ +package MultiplicityTest { + part def P; + attribute n: ScalarValues::Integer = 5; + part a[1]; + part b[0..2] : P; + part c: P[2..*]; + part d[*]; + part e[n]; + part f[n..*]; + part g[1..n]; + attribute def A { + attribute i: ScalarValues::Integer; + attribute x: A[i]; + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/OccurrenceTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/OccurrenceTest.sysml new file mode 100644 index 000000000..448e7081d --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/OccurrenceTest.sysml @@ -0,0 +1,28 @@ +package OccurrenceTest { + occurrence def Occ { + attribute a; + ref occurrence occ1: Occ; + occurrence occ2: Occ; + item x; + part y; + individual snapshot s: Ind; + timeslice t; + } + occurrence occ: Occ { + occurrence o1: Occ; + ref occurrence o2: Occ; + item z; + } + individual def Ind { + snapshot s2; + timeslice t2; + } + individual ind: Ind, Occ { + snapshot s3; + individual timeslice t3 :> ind; + individual snapshot s4: Ind; + } + occurrence o1 { + occurrence o2; + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ParameterTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ParameterTest.sysml new file mode 100644 index 000000000..94fd0da24 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ParameterTest.sysml @@ -0,0 +1,12 @@ +package ParameterTest { + attribute def A { + attribute x: ScalarValues::String; + attribute y: A; + } + attribute a: A; + calc def F { in p: A; in q: ScalarValues::Integer; return : ScalarValues::Integer; } + attribute f = F(a, 2); + attribute g = F(q = 1, p = a); + attribute b = new A(y = a, x = ""); + attribute c = new A("test2"); +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/PartTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/PartTest.sysml new file mode 100644 index 000000000..81f314bf0 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/PartTest.sysml @@ -0,0 +1,44 @@ +package PartTest { + part f: A; + part def A { + part <'1'> b: xx; + protected port c: C; + constant attribute x[0..2]; + derived constant attribute y :> x; + ref z: ScalarValues::Integer; + } + item def S; + abstract part def B { + abstract part a: A[1..2]; + abstract part b :> a; + abstract part c[0..1] :> a; + port x: ~C { + port p; + ref port q; + } + package P; + succession flow x.p to a1.aa.receiver; + action a1 { + accept S via x; + action aa accept S; + } + perform action a2; + state s1; + exhibit state s2; + } + private port def C { + private in y: A, xx { + part B_b :>> xx::b; + part B_c :>> xx::c; + port B_x :>> xx::x; + } + alias z1 for y; + alias z2 for y; + port c1: C; + ref port c2: C; + } + part p1 :> p2; + part p2 :> p3; + part p3 :> p1; + part p4 :> p4; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/RequirementTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/RequirementTest.sysml new file mode 100644 index 000000000..4f446b2f0 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/RequirementTest.sysml @@ -0,0 +1,29 @@ +package RequirementTest { + constraint def C; + constraint c: C; + private import q::**; + requirement def R { + assume constraint c1: C; + require c; + doc + /* */ + requirement; + requirement def <'1'> A { + doc + /* Text */ + subject s; + } + } + requirement def R1 { + require constraint c1 :>> c; + } + part p; + part q { + requirement r: R; + assert satisfy r by p; + assert satisfy r by q; + } + requirement r1: R1; + assert not satisfy r1 by p; + assert not satisfy r1 by q; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/RootPackageTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/RootPackageTest.sysml new file mode 100644 index 000000000..26875c4da --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/RootPackageTest.sysml @@ -0,0 +1,11 @@ +package P1 { + part def A; +} +package P2 { + private import P1::*; + part a: A; +} +private import P2::*; +package P3 { + part b :> a; +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/StateTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/StateTest.sysml new file mode 100644 index 000000000..5d93c1613 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/StateTest.sysml @@ -0,0 +1,47 @@ +package StateTest { + attribute def Sig { + ref x; + } + attribute def Exit; + part p; + action act; + state def S { + do action A; + entry; + then S1; + state S1; + accept s: Sig do action D then S2; + state S2 { + do send new Sig(T.s.x) to p; + state S3; + } + accept Exit then States::StateAction::done; + transition S1 accept s: Sig do action D then S2.S3; + transition T first S2.S3 accept s: Sig via p if true do send s to p then S1; + exit act; + state S3 { + state S3a; + } + transition S3.S3a then S1; + } + state s0 { + state s1 { + state s2; + } + state s3 { + state s4; + } + transition t1 first s1.s2 then s3.s4; + } + state s parallel { + state s1; + state s2; + } + state s4 { + do action a; + action c; + } + state s5 :> s4 { + do action b :>> c; + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/StructuredControlTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/StructuredControlTest.sysml new file mode 100644 index 000000000..2b6ebe155 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/StructuredControlTest.sysml @@ -0,0 +1,32 @@ +package StructuredControlTest { + action { + attribute i: ScalarValues::Integer := 0; + attribute b: ScalarValues::Boolean; + if i < 0 { + assign i := 0; + } + else in if i == 0 { + assign i := 1; + } + else { + assign i := i + 1; + } + if i > 0 { + assign i := i + 1; + } + then action aLoop while i > 0 { + assign i := i - 1; + } + until b; + then while i > 0 { + assign i := i - 1; + } + loop { + assign i := i - 1; + } + until b; + for n: ScalarValues::Integer in (1, 2, 3) { + assign i := i * n; + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/TextualRepresentationTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/TextualRepresentationTest.sysml new file mode 100644 index 000000000..e3291ff22 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/TextualRepresentationTest.sysml @@ -0,0 +1,15 @@ +package TextualRepresentationTest { + private import ScalarValues::Real; + item def C { + attribute x: Real; + assert constraint x_constraint { rep inOCL language "ocl"/* self.x > 0.0 */ } + } + action def setX { + in c: C; + in newX: Real; + language "alf"/* + * c.x = newX; + * WriteLine("Set new x"); + */ + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/TradeStudyTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/TradeStudyTest.sysml new file mode 100644 index 000000000..a82cf1a32 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/TradeStudyTest.sysml @@ -0,0 +1,13 @@ +package TradeStudyTest { + private import ScalarValues::Real; + private import TradeStudies::*; + part def Engine; + part engine1: Engine; + part engine2: Engine; + analysis engineTradeStudy: TradeStudy { + subject : Engine[1..*] = (engine1, engine2); + objective : MaximizeObjective; + calc :>> evaluationFunction { in part : Engine; return : Real; } + return part : Engine; + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/UseCaseTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/UseCaseTest.sysml new file mode 100644 index 000000000..3f695a5ea --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/UseCaseTest.sysml @@ -0,0 +1,32 @@ +package UseCaseTest { + part def System; + part def User; + use case def UseSystem { + subject system: System; + actor user: User; + objective { + /* Goal */ + } + include use case uc1: UC1; + include use case uc2 { + subject = system; + actor user = UseSystem::user; + } + } + use case def UC1; + part user: User; + use case uc2 { + subject; + actor :>> user; + } + use case u: UseSystem; + part system: System { + include uc2; + perform u; + use case uc1: UC1; + } + use case uc3 { + include u; + include system.uc1; + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/VariabilityTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/VariabilityTest.sysml new file mode 100644 index 000000000..bb2e294f5 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/VariabilityTest.sysml @@ -0,0 +1,32 @@ +package VariabilityTest { + part def P { + attribute a; + } + part def Q :> P; + attribute def B; + variation part def V :> P { + variant part x: Q { + attribute b: B :>> a; + } + } + part q: Q; + variation part v: P { + variant q { + attribute b: B :>> a; + } + } + part y: P = v::q; + variation action def A { + variant action a1; + variant action a2; + } + variation use case uc1 { + variant use case uc11; + variant use case uc12; + } + variation analysis a1; + variation verification v1; + variation requirement r { + variant requirement r1; + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/VerificationTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/VerificationTest.sysml new file mode 100644 index 000000000..31c9b02ac --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/VerificationTest.sysml @@ -0,0 +1,29 @@ +package VerificationTest { + part def V { + ref m: ScalarValues::Integer; + } + part vv: V; + requirement def R { + doc + /* ... */ + } + requirement r: R; + verification def VerificationCase { + subject v: V; + objective { + verify requirement : R; + } + VerificationCases::PassIf(v.m == 0) } + verification def VerificationPlan { + subject v: V; + objective { + verify r; + } + verification verificationCase: VerificationCase; + } + part verificationContext { + verification verificationPlan: VerificationPlan { + subject v = vv; + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ViewTest.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ViewTest.sysml new file mode 100644 index 000000000..f49edfc1b --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/Simple Tests/ViewTest.sysml @@ -0,0 +1,34 @@ +package ViewTest { + package P { + part p1; + private part p2; + } + part def S; + concern def C { + subject; + stakeholder s: S; + } + concern c: C { + subject; + stakeholder s1; + } + viewpoint def VP { + frame c; + } + rendering def R; + rendering r: R; + view def V { + viewpoint vp: VP { + frame concern c1; + concern c2; + } + render rendering r1: R[0..1]; + view v: V[0..*] { + expose P::*; + render r; + rendering r2; + alias vp1 for p1; + alias vp2 for p2; + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ActionTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ActionTest.sysmlx new file mode 100644 index 000000000..c6b13f152 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ActionTest.sysmlx @@ -0,0 +1,666 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AliasTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AliasTest.sysmlx new file mode 100644 index 000000000..fcc095b21 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AliasTest.sysmlx @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AllocationTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AllocationTest.sysmlx new file mode 100644 index 000000000..608ad0594 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AllocationTest.sysmlx @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AnalysisTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AnalysisTest.sysmlx new file mode 100644 index 000000000..62112d9f4 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AnalysisTest.sysmlx @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AssignmentTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AssignmentTest.sysmlx new file mode 100644 index 000000000..5796a4270 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/AssignmentTest.sysmlx @@ -0,0 +1,687 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/CalculationTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/CalculationTest.sysmlx new file mode 100644 index 000000000..7d3d73f33 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/CalculationTest.sysmlx @@ -0,0 +1,350 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/CommentTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/CommentTest.sysmlx new file mode 100644 index 000000000..6c191b336 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/CommentTest.sysmlx @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ConjugationTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ConjugationTest.sysmlx new file mode 100644 index 000000000..eebafb8f5 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ConjugationTest.sysmlx @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ConnectionTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ConnectionTest.sysmlx new file mode 100644 index 000000000..3f53ddae9 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ConnectionTest.sysmlx @@ -0,0 +1,459 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ConstraintTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ConstraintTest.sysmlx new file mode 100644 index 000000000..e5c0fc78c --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ConstraintTest.sysmlx @@ -0,0 +1,1188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ControlNodeTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ControlNodeTest.sysmlx new file mode 100644 index 000000000..27eba54c7 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ControlNodeTest.sysmlx @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/DecisionTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/DecisionTest.sysmlx new file mode 100644 index 000000000..97b3a4233 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/DecisionTest.sysmlx @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/DefaultValueTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/DefaultValueTest.sysmlx new file mode 100644 index 000000000..448287d57 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/DefaultValueTest.sysmlx @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/DependencyTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/DependencyTest.sysmlx new file mode 100644 index 000000000..523625a66 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/DependencyTest.sysmlx @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/EnumerationTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/EnumerationTest.sysmlx new file mode 100644 index 000000000..a0be2d43c --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/EnumerationTest.sysmlx @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/FeaturePathTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/FeaturePathTest.sysmlx new file mode 100644 index 000000000..92553ffe6 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/FeaturePathTest.sysmlx @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/IndividualTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/IndividualTest.sysmlx new file mode 100644 index 000000000..d3b0c0ef5 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/IndividualTest.sysmlx @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ItemTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ItemTest.sysmlx new file mode 100644 index 000000000..4742adc74 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ItemTest.sysmlx @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/MultiplicityTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/MultiplicityTest.sysmlx new file mode 100644 index 000000000..323fd1a56 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/MultiplicityTest.sysmlx @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/OccurrenceTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/OccurrenceTest.sysmlx new file mode 100644 index 000000000..0adeb5879 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/OccurrenceTest.sysmlx @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ParameterTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ParameterTest.sysmlx new file mode 100644 index 000000000..909ab4865 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ParameterTest.sysmlx @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/PartTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/PartTest.sysmlx new file mode 100644 index 000000000..1b86bfc38 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/PartTest.sysmlx @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/RequirementTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/RequirementTest.sysmlx new file mode 100644 index 000000000..0c0def70d --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/RequirementTest.sysmlx @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/RootPackageTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/RootPackageTest.sysmlx new file mode 100644 index 000000000..26d813f57 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/RootPackageTest.sysmlx @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/StateTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/StateTest.sysmlx new file mode 100644 index 000000000..286ac22e9 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/StateTest.sysmlx @@ -0,0 +1,493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/StructuredControlTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/StructuredControlTest.sysmlx new file mode 100644 index 000000000..c7d4196b1 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/StructuredControlTest.sysmlx @@ -0,0 +1,656 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/TextualRepresentationTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/TextualRepresentationTest.sysmlx new file mode 100644 index 000000000..12df4035f --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/TextualRepresentationTest.sysmlx @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/TradeStudyTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/TradeStudyTest.sysmlx new file mode 100644 index 000000000..ec353cf63 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/TradeStudyTest.sysmlx @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/UseCaseTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/UseCaseTest.sysmlx new file mode 100644 index 000000000..1dc7a10c8 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/UseCaseTest.sysmlx @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/VariabilityTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/VariabilityTest.sysmlx new file mode 100644 index 000000000..ce9c98b83 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/VariabilityTest.sysmlx @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/VerificationTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/VerificationTest.sysmlx new file mode 100644 index 000000000..22f631de2 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/VerificationTest.sysmlx @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ViewTest.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ViewTest.sysmlx new file mode 100644 index 000000000..4444d39ed --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/Simple Tests/ViewTest.sysmlx @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Writers/CollectionCursorTestFixture.cs b/SysML2.NET.Serializer.TextualNotation.Tests/Writers/CollectionCursorTestFixture.cs index b74ee6a0a..c82c38ae7 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Writers/CollectionCursorTestFixture.cs +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Writers/CollectionCursorTestFixture.cs @@ -103,5 +103,117 @@ public void VerifyAssertAdvancedSince() Assert.That(() => cursor.AssertAdvancedSince(positionAtEnd, "CaseBodyItem"), Throws.TypeOf()); } + + /// + /// locates an element by role: a match at the + /// cursor advances it like , a match ahead is consumed out of + /// order and skipped by all later traversal, and the cursor otherwise stays put. + /// + [Test] + public void VerifyTryTake() + { + var emptyCursor = new CollectionCursor([]); + + Assert.That(() => emptyCursor.TryTake(null, out _), Throws.TypeOf()); + Assert.That(emptyCursor.TryTake(element => true, out _), Is.False); + + var cursor = new CollectionCursor(["alpha", "beta", "gamma", "delta"]); + + // No match leaves the cursor untouched and yields no value. + var missed = cursor.TryTake(element => element == "omega", out var missedValue); + + using (Assert.EnterMultipleScope()) + { + Assert.That(missed, Is.False); + Assert.That(missedValue, Is.Null); + Assert.That(cursor.Position, Is.EqualTo(0)); + } + + // A match AT the cursor consumes it exactly like Move(1) — real forward progress. + var positionBeforeTakeAtCursor = cursor.Position; + var takenAtCursor = cursor.TryTake(element => element == "alpha", out var valueAtCursor); + + using (Assert.EnterMultipleScope()) + { + Assert.That(takenAtCursor, Is.True); + Assert.That(valueAtCursor, Is.EqualTo("alpha")); + Assert.That(cursor.Position, Is.EqualTo(1)); + Assert.That(cursor.Current, Is.EqualTo("beta")); + } + + Assert.That(() => cursor.AssertAdvancedSince(positionBeforeTakeAtCursor, "TransitionUsage"), Throws.Nothing); + + // A match AHEAD of the cursor is taken without advancing it; the consumed slot disappears + // from Current / GetNext / Move traversal. + var takenAhead = cursor.TryTake(element => element == "gamma", out var valueAhead); + + using (Assert.EnterMultipleScope()) + { + Assert.That(takenAhead, Is.True); + Assert.That(valueAhead, Is.EqualTo("gamma")); + Assert.That(cursor.Position, Is.EqualTo(1)); + Assert.That(cursor.Current, Is.EqualTo("beta")); + Assert.That(cursor.GetNext(1), Is.EqualTo("delta")); + } + + cursor.Move(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(cursor.Current, Is.EqualTo("delta")); + Assert.That(cursor.Position, Is.EqualTo(3)); + } + + // A taken element can never be taken again. + Assert.That(cursor.TryTake(element => element == "gamma", out _), Is.False); + + // The type test participates in the match: a TDerived mismatch is not taken. + var mixedCursor = new CollectionCursor(["epsilon", new object()]); + mixedCursor.Move(); + + Assert.That(mixedCursor.TryTake(element => true, out _), Is.False); + } + + /// + /// answers whether an unconsumed element + /// matching the predicate exists at or ahead of the cursor, without consuming anything — the + /// guard-condition companion of . + /// + [Test] + public void VerifyContains() + { + var emptyCursor = new CollectionCursor([]); + + Assert.That(() => emptyCursor.Contains(null), Throws.TypeOf()); + Assert.That(emptyCursor.Contains(element => true), Is.False); + + var cursor = new CollectionCursor(["alpha", "beta", "gamma"]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(cursor.Contains(element => element == "alpha"), Is.True); + Assert.That(cursor.Contains(element => element == "gamma"), Is.True); + Assert.That(cursor.Contains(element => element == "omega"), Is.False); + + // Non-consuming: the probe left the cursor exactly where it was. + Assert.That(cursor.Position, Is.EqualTo(0)); + Assert.That(cursor.Current, Is.EqualTo("alpha")); + } + + // Elements behind the cursor are out of reach. + cursor.Move(); + + Assert.That(cursor.Contains(element => element == "alpha"), Is.False); + + // Elements consumed out of order by TryTake are out of reach too. + cursor.TryTake(element => element == "gamma", out _); + + Assert.That(cursor.Contains(element => element == "gamma"), Is.False); + + // The type test participates in the match: a TDerived mismatch does not count. + var mixedCursor = new CollectionCursor([new object()]); + + Assert.That(mixedCursor.Contains(element => true), Is.False); + } } } diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs b/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs index 76f49c4d2..4071f116e 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs @@ -138,6 +138,38 @@ public void OneTimeTearDown() [TestCase("17-Sequence Modeling", "17a-Sequence-Modeling.sysmlx")] [TestCase("17-Sequence Modeling", "17b-Sequence-Modeling.sysmlx")] [TestCase("18-Use Case", "18-Use Case.sysmlx")] + [TestCase("Simple Tests", "ConnectionTest.sysmlx")] + [TestCase("Simple Tests", "EnumerationTest.sysmlx")] + [TestCase("Simple Tests", "StructuredControlTest.sysmlx")] + [TestCase("Simple Tests", "AliasTest.sysmlx")] + [TestCase("Simple Tests", "FeaturePathTest.sysmlx")] + [TestCase("Simple Tests", "AnalysisTest.sysmlx")] + [TestCase("Simple Tests", "StateTest.sysmlx")] + [TestCase("Simple Tests", "RequirementTest.sysmlx")] + [TestCase("Simple Tests", "ViewTest.sysmlx")] + [TestCase("Simple Tests", "DecisionTest.sysmlx")] + [TestCase("Simple Tests", "PartTest.sysmlx")] + [TestCase("Simple Tests", "ActionTest.sysmlx")] + [TestCase("Simple Tests", "AssignmentTest.sysmlx")] + [TestCase("Simple Tests", "CommentTest.sysmlx")] + [TestCase("Simple Tests", "ConstraintTest.sysmlx")] + [TestCase("Simple Tests", "TextualRepresentationTest.sysmlx")] + [TestCase("Simple Tests", "VerificationTest.sysmlx")] + [TestCase("Simple Tests", "AllocationTest.sysmlx")] + [TestCase("Simple Tests", "CalculationTest.sysmlx")] + [TestCase("Simple Tests", "ConjugationTest.sysmlx")] + [TestCase("Simple Tests", "DependencyTest.sysmlx")] + [TestCase("Simple Tests", "IndividualTest.sysmlx")] + [TestCase("Simple Tests", "ItemTest.sysmlx")] + [TestCase("Simple Tests", "OccurrenceTest.sysmlx")] + [TestCase("Simple Tests", "RootPackageTest.sysmlx")] + [TestCase("Simple Tests", "TradeStudyTest.sysmlx")] + [TestCase("Simple Tests", "ControlNodeTest.sysmlx")] + [TestCase("Simple Tests", "DefaultValueTest.sysmlx")] + [TestCase("Simple Tests", "MultiplicityTest.sysmlx")] + [TestCase("Simple Tests", "ParameterTest.sysmlx")] + [TestCase("Simple Tests", "UseCaseTest.sysmlx")] + [TestCase("Simple Tests", "VariabilityTest.sysmlx")] public async Task VerifyValidationTextualNotationXmi(string folderName, string fileName) { var loggerFactory = LoggerFactory.Create(builder => @@ -186,7 +218,20 @@ public async Task VerifyValidationTextualNotationXmi(string folderName, string f TestContext.WriteLine("=== Textual Notation Output ==="); TestContext.WriteLine(textualNotation); TestContext.WriteLine("=== End ==="); - + + // The console logger strips trailing whitespace from every line it captures, so the log is + // not a faithful copy of the emitted text. Re-baselining and any round-trip check must read + // the bytes from here instead. + var emittedDirectory = Environment.GetEnvironmentVariable("SYSML2_TN_EMIT_DIR"); + + if (!string.IsNullOrWhiteSpace(emittedDirectory)) + { + var emittedFilePath = Path.Combine(emittedDirectory, folderName, fileName.Replace(".sysmlx", ".sysml")); + Directory.CreateDirectory(Path.GetDirectoryName(emittedFilePath)); + await File.WriteAllTextAsync(emittedFilePath, textualNotation); + } + + var expectedFilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, "Expected", folderName, fileName.Replace(".sysmlx", ".sysml")); var expectedContent = await File.ReadAllTextAsync(expectedFilePath); diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/BindingLayer.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/BindingLayer.cs new file mode 100644 index 000000000..4229cd65a --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/BindingLayer.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + /// + /// The source a Membership enters a Namespace through. KerML §8.2.3.5.3: the local resolution of a + /// Namespace covers its owned, imported and (for a Type) inherited Memberships. + /// + internal enum BindingLayer + { + /// Bound by an owned Membership of the scope. + Owned, + + /// Bound by an inherited Membership (including through implied Specializations). + Inherited, + + /// Bound through an owned Import of the scope. + Imported, + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/ContainmentPaths.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/ContainmentPaths.cs new file mode 100644 index 000000000..5025f9d67 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/ContainmentPaths.cs @@ -0,0 +1,226 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System.Collections.Generic; + using System.Linq; + + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + + /// + /// Containment walks over the ownership tree: the Namespace an Element sits in, the chain up to its + /// root, and the qualified path that chain spells (KerML §7.2.5.3). + /// + internal static class ContainmentPaths + { + /// + /// Deterministic ordering for facade candidates at the same scope depth: shorter name first, then + /// ordinal alphabetical. + /// + /// First candidate. + /// Second candidate. + /// Negative if sorts first, positive if right, zero if tied. + internal static int CompareFacades(INamespace left, INamespace right) + { + var leftName = SegmentNaming.QueryPreferredRawName(left) ?? string.Empty; + var rightName = SegmentNaming.QueryPreferredRawName(right) ?? string.Empty; + + var lengthCompare = leftName.Length.CompareTo(rightName.Length); + + return lengthCompare != 0 ? lengthCompare : string.CompareOrdinal(leftName, rightName); + } + + /// + /// Determines whether is a member element of one of + /// 's own memberships (declared, not imported or inherited). + /// + /// The namespace to test. + /// The candidate member element. + /// when the scope declares the element itself. + internal static bool IsDirectlyOwnedBy(INamespace scope, IElement element) + { + return scope.ownedMembership.Any(membership => ReferenceEquals(membership.MemberElement, element)); + } + + /// + /// Determines whether is declared by or by + /// one of its lexically enclosing namespaces — in which case an import may name it relatively + /// (import Usages::*, or a sibling import Definitions::*) without depending on any + /// import. + /// + /// The imported element. + /// The element owning the import declaration. + /// when the target is reachable by containment. + internal static bool IsReachableByContainment(IElement target, IElement importOwner) + { + var declaringNamespace = target?.owningNamespace; + + if (declaringNamespace == null) + { + return false; + } + + // Full resolution reaches any member of a containing Namespace (§8.2.3.5.4), so the test is + // whether the two containment chains intersect, not whether one contains the other. + for (var scope = importOwner; scope != null; scope = QueryEnclosingContainer(scope)) + { + for (var candidate = declaringNamespace; candidate != null; candidate = candidate.owningNamespace) + { + if (ReferenceEquals(scope, candidate)) + { + return true; + } + } + } + + return false; + } + + /// + /// Returns the element CONTAINING : the owning related element of a + /// Relationship — whose owner is null, since it is owned as a relationship rather than as a + /// member — and the owner of anything else. + /// + /// The element to climb from; may be . + /// The container, or at the top of the containment tree. + private static IElement QueryContainer(IElement element) + { + return element switch + { + null => null, + IRelationship { OwningRelatedElement: { } owningRelatedElement } => owningRelatedElement, + _ => element.owner + }; + } + + /// + /// Returns the namespace enclosing , continuing through the CONTAINMENT + /// tree when owningNamespace is null. + /// + /// The element to climb from; may be . + /// The enclosing namespace, or at a true root. + /// + /// A FilterPackage is an ownedRelatedElement of its Import + /// ( + /// + /// NamespaceImport = … | importedNamespace = FilterPackage { ownedRelatedElement += + /// importedNamespace } + /// + /// ), and an Import is not a Membership — so the FilterPackage + /// has no owningMembership and therefore no owningNamespace, even though it is plainly + /// nested in the model, so climbing by owningNamespace alone dead-ends there. + /// + /// KerML §7.2.5.3 defines a root namespace as one with no OWNER, which a FilterPackage has; + /// a true root has neither and still terminates the climb. + /// + /// + private static INamespace QueryEnclosingContainer(IElement element) + { + if (element?.owningNamespace is { } owningNamespace) + { + return owningNamespace; + } + + var visited = new HashSet(); + + for (var container = element?.owner; container != null && visited.Add(container); container = container.owner) + { + if (container is INamespace enclosingNamespace) + { + return enclosingNamespace; + } + } + + return null; + } + + /// + /// Returns the nearest containing , by + /// CONTAINMENT rather than by the derived owningNamespace — which is null for a Relationship. + /// + /// The element to climb from; may be . + /// The containing namespace, or at the top of the containment tree. + internal static INamespace QueryParentNamespace(IElement element) + { + var visited = new HashSet(); + + for (var current = QueryContainer(element); current != null && visited.Add(current); current = QueryContainer(current)) + { + if (current is INamespace containingNamespace) + { + return containingNamespace; + } + } + + return null; + } + + /// + /// As , but stops the walk at the first namespace that + /// also encloses . + /// + /// + /// A namespace that encloses the reference site is already reached by full resolution + /// (KerML §8.2.3.5.4), so naming it explicitly is redundant self-prefixing. An element in a + /// separate resource shares no enclosing namespace with the source, so its path stays qualified — + /// SI::kg is unaffected. + /// + /// The leaf to qualify; must be non-null. + /// The reference site, or to always walk to the root. + /// The short-form qualified name, relative to the nearest shared enclosing namespace. + internal static string QueryShortQualifiedName(IElement element, IElement sourcePoco = null) + { + var enclosingScopes = new List(); + + var origin = sourcePoco is IImport { OwningRelatedElement: { } importOwner } ? importOwner : sourcePoco; + + for (var scope = origin; scope != null; scope = scope.owningNamespace) + { + enclosingScopes.Add(scope); + } + + var segments = new Stack(); + var current = element; + + while (current != null) + { + var preferred = SegmentNaming.QueryPreferredRawName(current); + + if (string.IsNullOrWhiteSpace(preferred)) + { + break; + } + + segments.Push(SegmentNaming.Escape(preferred)); + + current = current.owningNamespace; + + if (current != null && enclosingScopes.Any(scope => ReferenceEquals(scope, current))) + { + break; + } + } + + return string.Join("::", segments); + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/ImportExpansion.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/ImportExpansion.cs new file mode 100644 index 000000000..3a18f5b88 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/ImportExpansion.cs @@ -0,0 +1,418 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using SysML2.NET.Core.POCO.Core.Types; + using SysML2.NET.Core.POCO.Kernel.Packages; + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Core.Root.Namespaces; + + /// + /// Expands a Namespace's Imports into the Memberships they contribute, applying the visibility, + /// recursion, filtering and distinguishability rules of KerML §7.2.5.4 and §8.3.2.4. + /// + internal static class ImportExpansion + { + /// + /// Applies the filterConditions of to + /// (SysML 2.0 §7.5.4); a namespace that declares none is transparent. + /// + /// The namespace whose filter conditions apply. + /// The candidate memberships. + /// The memberships that satisfy every condition. + private static IEnumerable ApplyFilterConditions(INamespace filteringNamespace, IEnumerable memberships) + { + if (filteringNamespace is not IPackage filteringPackage) + { + return memberships; + } + + return filteringPackage.filterCondition.Count == 0 + ? memberships + : SelectIncludedMemberships(filteringPackage, memberships); + } + + /// + /// Determines whether binds any of . + /// + /// The membership to test. + /// The names to test against. + /// when at least one bound name is in the set. + private static bool BindsAnyName(IMembership membership, HashSet names) + { + if (membership is not { MemberElement: { } target }) + { + return false; + } + + var (shortName, longName) = SegmentNaming.QueryMembershipNames(membership, target); + + return (!string.IsNullOrWhiteSpace(shortName) && names.Contains(shortName)) + || (!string.IsNullOrWhiteSpace(longName) && names.Contains(longName)); + } + + /// + /// Determines whether an IMPORTED membership has a distinguishability collision with an owned + /// membership of the importing scope, which Namespace::importedMemberships excludes. Without + /// this the homonym competes with the owned member and forces a needlessly qualified reference. + /// + /// Delegates to ; the name check is only a + /// pre-filter, since differing names always imply distinguishable. + /// + /// + /// The candidate imported membership. + /// Names bound by the scope's owned memberships, used to pre-filter. + /// The scope's owned memberships, tested on a name hit. + /// when the imported membership must be excluded. + private static bool CollidesWithOwnedMembership(IMembership membership, HashSet ownedNames, List ownedMemberships) + { + if (ownedNames.Count == 0 || membership is not { MemberElement: { } target }) + { + return false; + } + + var (shortName, longName) = SegmentNaming.QueryMembershipNames(membership, target); + + var sharesName = (!string.IsNullOrWhiteSpace(shortName) && ownedNames.Contains(shortName)) + || (!string.IsNullOrWhiteSpace(longName) && ownedNames.Contains(longName)); + + return sharesName && ownedMemberships.Any(owned => !membership.IsDistinguishableFrom(owned)); + } + + /// + /// Enumerates the unfiltered visible memberships of . + /// + /// The namespace named by the import. + /// The triggering import's isImportAll. + /// The triggering import's isRecursive. + /// Whether the importing scope is reached through the global namespace. + /// Namespaces already visited on this import chain. + /// The memberships, before filter conditions. + private static IEnumerable EnumerateVisibleMemberships(INamespace importedNamespace, bool includeAll, bool isRecursive, bool isGlobal, HashSet excluded) + { + if (importedNamespace == null || !excluded.Add(importedNamespace)) + { + yield break; + } + + var publicOnly = !includeAll || isGlobal; + + List ownedMemberships = [..importedNamespace.ownedMembership.Where(ownedMember => !publicOnly || ownedMember.Visibility == VisibilityKind.Public)]; + + foreach (var ownedMembership in ownedMemberships) + { + yield return ownedMembership; + } + + List reExports = [..importedNamespace.ownedImport.Where(ownedImport => ownedImport.Visibility == VisibilityKind.Public)]; + + foreach (var reExport in reExports) + { + switch (reExport) + { + case IMembershipImport { ImportedMembership: { } reExportedMembership }: + yield return reExportedMembership; + + break; + + case INamespaceImport { ImportedNamespace: { } reExportedNamespace } reExportedImport: + + foreach (var reExportedMembership in QueryVisibleMemberships(reExportedNamespace, reExportedImport.IsImportAll, reExportedImport.IsRecursive, isGlobal, excluded)) + { + yield return reExportedMembership; + } + + break; + } + } + + if (!isRecursive) + { + yield break; + } + + foreach (var ownedSubNamespace in ownedMemberships.Select(ownedMembership => ownedMembership.MemberElement).OfType()) + { + foreach (var descendantMembership in QueryVisibleMemberships(ownedSubNamespace, includeAll, true, isGlobal, excluded)) + { + yield return descendantMembership; + } + } + } + + /// + /// When is set, admits only PUBLIC memberships and imports — the + /// filter both the global namespace and visible resolution apply. The global namespace contains + /// only the visible memberships of other roots, and every segment of a qualified name after the + /// first resolves against the visible memberships of the preceding one (KerML §8.2.3.5.2–.3), so a + /// name bound privately there would not re-parse. Within a local scope everything is visible. + /// + /// The or considered. + /// Whether only bindings visible OUTSIDE the owning scope are admitted. + /// when the relationship may contribute a binding. + internal static bool PassesVisibilityFilter(IRelationship relationship, bool publicOnly) + { + if (!publicOnly) + { + return true; + } + + return relationship switch + { + IMembership membership => membership.Visibility == VisibilityKind.Public, + IImport import => import.Visibility == VisibilityKind.Public, + _ => true + }; + } + + /// + /// Returns every name bound by . + /// + /// The memberships to collect names from. + /// The bound names. + private static HashSet QueryBoundNames(List memberships) + { + var names = new HashSet(StringComparer.Ordinal); + + foreach (var membership in memberships.Where(membership => membership?.MemberElement != null)) + { + var (shortName, longName) = SegmentNaming.QueryMembershipNames(membership, membership.MemberElement); + + foreach (var boundName in new[] { shortName, longName }.Where(name => !string.IsNullOrWhiteSpace(name))) + { + names.Add(boundName); + } + } + + return names; + } + + /// + /// Returns the names bound by more than one imported to DIFFERENT member + /// elements. KerML §7.2.5.4 hides every membership involved in such a conflict, so none of them can + /// be named without qualification. + /// + /// The candidate imported contributions. + /// The hidden names, possibly empty. + private static HashSet QueryHiddenImportedNames(List<(IImport Import, IMembership Membership)> contributions) + { + var boundElements = new Dictionary(StringComparer.Ordinal); + var hiddenNames = new HashSet(StringComparer.Ordinal); + + foreach (var (_, membership) in contributions.Where(contribution => contribution.Membership?.MemberElement != null)) + { + var target = membership.MemberElement; + var (shortName, longName) = SegmentNaming.QueryMembershipNames(membership, target); + + foreach (var boundName in new[] { shortName, longName }.Where(name => !string.IsNullOrWhiteSpace(name))) + { + if (!boundElements.TryGetValue(boundName, out var alreadyBound)) + { + boundElements[boundName] = target; + } + else if (!ReferenceEquals(alreadyBound, target)) + { + hiddenNames.Add(boundName); + } + } + } + + return hiddenNames; + } + + /// + /// Enumerates the memberships one ownedImport contributes to its importing namespace, per + /// MembershipImport::importedMemberships() and NamespaceImport::importedMemberships() + /// (KerML §8.3.2.4.4, §8.3.2.4.6). + /// + /// The import to expand. + /// Whether the importing scope is reached through the global namespace. + /// Namespaces already visited on this import chain. + /// The memberships the import contributes. + private static IEnumerable QueryImportContribution(IImport ownedImport, bool isGlobal, HashSet excluded) + { + switch (ownedImport) + { + case IMembershipImport { ImportedMembership: { } importedMembership } membershipImport: + + yield return importedMembership; + + if (membershipImport.IsRecursive && importedMembership.MemberElement is INamespace importedElement) + { + foreach (var descendantMembership in QueryVisibleMemberships(importedElement, membershipImport.IsImportAll, true, isGlobal, excluded)) + { + yield return descendantMembership; + } + } + + break; + + case INamespaceImport { ImportedNamespace: { } importedNamespace } namespaceImport: + + foreach (var importedMember in QueryVisibleMemberships(importedNamespace, namespaceImport.IsImportAll, namespaceImport.IsRecursive, isGlobal, excluded)) + { + yield return importedMember; + } + + break; + } + } + + /// + /// Returns what a scope's ownedImports contribute to its importedMemberships, less the + /// two exclusions of KerML §7.2.5.4: a membership colliding with an owned member, and one colliding + /// with a visible membership from another imported namespace. + /// + /// The importing namespace. + /// Whether only publicly visible imports are considered. + /// Whether the importing scope is reached through the global namespace. + /// Names bound by the scope's owned memberships. + /// The scope's owned memberships. + /// The surviving contributions, each paired with the import that produced it. + internal static List<(IImport Import, IMembership Membership)> QueryImportedContributions(INamespace scope, bool visibleOnly, bool isGlobal, HashSet ownedNames, List ownedMemberships) + { + var contributions = new List<(IImport Import, IMembership Membership)>(); + + foreach (var ownedImport in scope.ownedImport.Where(ownedImport => PassesVisibilityFilter(ownedImport, visibleOnly))) + { + contributions.AddRange(QueryImportContribution(ownedImport, isGlobal, [scope]) + .Where(membership => !CollidesWithOwnedMembership(membership, ownedNames, ownedMemberships)) + .Select(membership => (ownedImport, membership))); + } + + // A Package's own filterConditions filter ALL of its imports (SysML 2.0 §7.5.4). + var included = new HashSet(ApplyFilterConditions(scope, contributions.Select(contribution => contribution.Membership))); + + contributions = [..contributions.Where(contribution => included.Contains(contribution.Membership))]; + + var hiddenNames = QueryHiddenImportedNames(contributions); + + return hiddenNames.Count == 0 + ? contributions + : [..contributions.Where(contribution => !BindsAnyName(contribution.Membership, hiddenNames))]; + } + + /// + /// Returns the Memberships 's own Imports contribute, mirroring + /// minus its collision filter — a colliding import + /// cannot be the INDEPENDENT binding anyway, since the owned member it collides with is. + /// + /// The importing scope; must be non-null. + /// Whether to keep only PUBLIC imports, the ones that re-export. + /// The imported memberships, possibly empty. + internal static List QueryImportedMembershipsSafe(INamespace scope, bool visibleOnly) + { + var imported = new List(); + + var ownedMemberships = scope.ownedMembership; + var ownedNames = QueryBoundNames(ownedMemberships); + + imported.AddRange(QueryImportedContributions(scope, visibleOnly, false, ownedNames, ownedMemberships) + .Select(contribution => contribution.Membership)); + + return imported; + } + + /// + /// Enumerates the memberships an import ns::* contributes, following RE-EXPORTS + /// transitively per NamespaceImport::importedMemberships()visibleMemberships(): + /// the owned memberships plus, recursively, whatever the namespace's own PUBLIC imports bring in. + /// terminates import cycles. + /// + /// The visibility filter is driven by the import's isImportAll, not by the importing + /// scope; each re-exported import contributes under its own. + /// + /// + /// The namespace named by the import. + /// The triggering import's isImportAll: admits non-public memberships. + /// The triggering import's isRecursive: also descends into visible owned sub-namespaces. + /// + /// Whether the importing scope is reached through the global namespace, which admits only visible memberships regardless of + /// (KerML §8.2.3.5.2). + /// + /// Namespaces already visited on this import chain. + /// The memberships contributed to the importing scope. + private static IEnumerable QueryVisibleMemberships(INamespace importedNamespace, bool includeAll, bool isRecursive, bool isGlobal, HashSet excluded) + { + return ApplyFilterConditions(importedNamespace, EnumerateVisibleMemberships(importedNamespace, includeAll, isRecursive, isGlobal, excluded)); + } + + /// + /// Keeps the memberships whose member element satisfies every filterCondition of + /// . + /// + /// The package whose conditions apply. + /// The candidate memberships. + /// The surviving memberships. + /// + /// A condition whose expression cannot be evaluated leaves the membership neither known to bind nor + /// known to be absent, so it is kept: dropping it would hide a name that may legitimately bind here. + /// + private static IEnumerable SelectIncludedMemberships(IPackage filteringPackage, IEnumerable memberships) + { + foreach (var membership in memberships.Where(membership => membership?.MemberElement != null)) + { + bool isIncluded; + + try + { + isIncluded = filteringPackage.IncludeAsMember(membership.MemberElement); + } + catch (NotSupportedException) + { + isIncluded = true; + } + + if (isIncluded) + { + yield return membership; + } + } + } + + /// + /// Returns 's inheritedMembership, or an empty list when a filter + /// condition on a supertype's imports cannot be evaluated. + /// + /// The Type to query. + /// The inherited Memberships, possibly empty. + /// + /// The derivation reaches Package::importedMemberships, which evaluates + /// filterConditions; a condition whose Expression has no model-level evaluation throws. + /// + internal static List QueryInheritedMemberships(IType type) + { + try + { + return type.inheritedMembership; + } + catch (NotSupportedException) + { + return []; + } + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/LayeredOutcomeKind.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/LayeredOutcomeKind.cs new file mode 100644 index 000000000..2cb84e073 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/LayeredOutcomeKind.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + /// + /// The outcome kind of probing one name in one scope. + /// + internal enum LayeredOutcomeKind + { + /// The scope does not bind the name; the outward walk may continue. + NotBound, + + /// The scope binds the name to exactly one election winner. + Bound, + + /// The scope binds the name to several peers; a reader cannot pick one. + Ambiguous + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/LocalScopeResolver.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/LocalScopeResolver.cs new file mode 100644 index 000000000..f77265bd1 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/LocalScopeResolver.cs @@ -0,0 +1,502 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using SysML2.NET.Core.POCO.Core.Features; + using SysML2.NET.Core.POCO.Core.Types; + using SysML2.NET.Core.POCO.Kernel.Behaviors; + using SysML2.NET.Core.POCO.Kernel.Connectors; + using SysML2.NET.Core.POCO.Kernel.Expressions; + using SysML2.NET.Core.POCO.Kernel.FeatureValues; + using SysML2.NET.Core.POCO.Kernel.Interactions; + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Core.POCO.Systems.Actions; + + /// + /// Determines the local Namespace a reference resolves against, by the kind of Relationship the + /// reference is the target of (KerML §8.2.3.5.2). + /// + internal static class LocalScopeResolver + { + /// + /// Materialises the owningNamespace chain from up to the root, and + /// locates in it. + /// + /// The starting namespace. + /// The innermost scope a match may come from (may be ). + /// The chain. + internal static SourceScopeChain BuildChain(INamespace start, INamespace matchFloorScope) + { + var scopes = new List(); + var current = start; + + while (current != null) + { + scopes.Add(current); + current = current.owningNamespace; + } + + return new SourceScopeChain(scopes, QueryMatchFloorDepth(scopes, matchFloorScope)); + } + + /// + /// Resolves the local scope of : the first + /// reached by climbing OwningRelatedElement, owningNamespace, then owner. + /// An anonymous nested namespace (no upward chain of its own) is skipped via owner so the + /// reference site's real enclosing scope is found. Falls back to . + /// + /// The source POCO; may be . + /// The scope returned when no enclosing namespace is reached. + /// The local scope or . + internal static INamespace GetSourceLocalScope(IElement sourcePoco, INamespace rootNamespace) + { + if (sourcePoco == null) + { + return rootNamespace; + } + + if (QueryContextRelationshipLocalScope(sourcePoco) is { } contextScope) + { + return contextScope; + } + + var visited = new HashSet(); + var current = sourcePoco; + + while (current != null && visited.Add(current)) + { + if (current is IRelationship { OwningRelatedElement: not null } relationship) + { + current = relationship.OwningRelatedElement; + continue; + } + + if (current is INamespace asNamespace) + { + if (asNamespace.owningNamespace != null || ReferenceEquals(asNamespace, rootNamespace)) + { + return asNamespace; + } + + var namespaceOwner = asNamespace.owner; + + if (namespaceOwner == null) + { + return asNamespace; + } + + current = namespaceOwner; + continue; + } + + var owningNamespace = current.owningNamespace; + + if (owningNamespace != null) + { + return owningNamespace; + } + + current = current.owner; + } + + return rootNamespace; + } + + /// + /// Determines whether is the right-hand side of a chain accessor — + /// a reference the parser resolves against the preceding segment's type instead of the lexical + /// scope, so the bare simple name is the correct emission. + /// + /// The source POCO at the reference site. + /// when the source is a chain accessor. + internal static bool IsChainAccessor(IElement sourcePoco) + { + if (sourcePoco is IMembership { OwningRelatedElement: { } membershipOwner } and not IParameterMembership + && EstablishesRelativeNamespace(membershipOwner)) + { + return true; + } + + if (IsFlowFeatureAccessor(sourcePoco)) + { + return true; + } + + if (sourcePoco is not IFeatureChaining { OwningRelatedElement: IFeature chainOwner } chaining) + { + return false; + } + + var siblings = chainOwner.OwnedRelationship.OfType().ToList(); + var index = siblings.IndexOf(chaining); + + if (index > 0) + { + return true; + } + + return chainOwner.OwningRelationship is IMembership { OwningRelatedElement: { } chainMemberOwner } and not IParameterMembership + && EstablishesRelativeNamespace(chainMemberOwner); + } + + /// + /// Returns the Namespace a chain accessor resolves against: the result of the argument + /// Expression for a member, or the preceding + /// chainingFeature for a later (KerML §8.2.3.5.2). + /// + /// The source POCO at the reference site. + /// The relative Namespace, or when the site has none. + internal static INamespace QueryRelativeNamespace(IElement sourcePoco) + { + switch (sourcePoco) + { + case IMembership { OwningRelatedElement: IFeatureChainExpression chainExpression }: + + try + { + // argument derives through instantiatedType, which resolves a qualified name and so + // can evaluate a filterCondition whose Expression has no model-level evaluation. + return chainExpression.argument.Count == 0 ? null : chainExpression.argument[0].result; + } + catch (NotSupportedException) + { + return null; + } + + case IFeatureChaining { OwningRelatedElement: IFeature chainOwner } chaining: + { + var siblings = chainOwner.OwnedRelationship.OfType().ToList(); + var index = siblings.IndexOf(chaining); + + return index > 0 ? siblings[index - 1].ChainingFeature : null; + } + + default: + return null; + } + } + + internal static SelfBinding QuerySelfBinding(IElement sourcePoco) + { + if (sourcePoco is IMembershipImport { ImportedMembership: { } importedMembership, OwningRelatedElement: INamespace importScope }) + { + // Only when the import introduces the binding: a membership the scope already owns binds anyway. + return OwnsMembership(importScope, importedMembership) + ? null + : new SelfBinding(importScope, importedMembership); + } + + return sourcePoco is IMembership membership and not IOwningMembership + && string.IsNullOrWhiteSpace(membership.MemberName) + && string.IsNullOrWhiteSpace(membership.MemberShortName) + && membership.OwningRelatedElement is INamespace bindingScope + ? new SelfBinding(bindingScope, membership) + : null; + } + + /// + /// Returns the innermost scope of a reference's chain that may produce a MATCH, or + /// when every scope of the chain may. + /// + /// The source POCO at the reference site. + /// The floor scope, or . + /// + /// KerML §8.2.3.5.2 anchors a inside a + /// at the "non-invocation Namespace" — the nearest + /// containing Namespace that is neither an expression nor a parameter of one. For a + /// that is the value-carrying Feature itself, while a stricter reading + /// steps one scope further out. The clause admits both, so the floor bars the disputed scopes from + /// producing a match while keeping them as shadow sources. + /// + internal static INamespace QueryValueExpressionMatchFloor(IElement sourcePoco) + { + if (sourcePoco is not IMembership sourceMembership) + { + return null; + } + + var subject = sourceMembership; + var scope = QueryExpressionScope(subject); + var visited = new HashSet(); + var isFirstStep = true; + + while (scope != null && StepsOutOfInvocation(subject, scope, isFirstStep)) + { + subject = scope.owningMembership; + isFirstStep = false; + + if (subject == null || !visited.Add(subject)) + { + break; + } + + scope = QueryExpressionScope(subject); + } + + return scope; + } + + /// + /// Determines whether the local referencer's DECLARED name equals the target's effective name — in + /// which case the bare simple name would re-resolve to the local member and the qualified form is + /// required. An anonymous referencer (no declared names) can never collide. + /// + /// Declared names only. An anonymous redefiner takes its effective name from the feature it + /// redefines, so testing the effective name would qualify every such redefinition unnecessarily. + /// + /// + /// The redefining/referencing feature; must be non-null. + /// The referenced target. + /// on a collision. + internal static bool RedefinerDeclaredNameCollidesWith(IFeature localRedefiner, IElement target) + { + var declaredNames = new[] { localRedefiner.DeclaredName, localRedefiner.DeclaredShortName } + .Where(declared => !string.IsNullOrWhiteSpace(declared)); + + return declaredNames.Any(declared => + string.Equals(declared, target.name, StringComparison.Ordinal) + || string.Equals(declared, target.shortName, StringComparison.Ordinal)); + } + + /// + /// Determines whether also REFERENCES a different element that + /// shares a simple name with — the exhibit X :>> Y shape, where the + /// reference and the redefinition name distinct elements under one name. Qualifying the redefinition + /// keeps them distinct under either reading of the §8.2.3.5.1 exception. + /// + /// The feature owning the redefinition. + /// The redefined feature being named. + /// when the qualified form is required to keep the two distinct. + internal static bool ReferencedFeatureSharesSimpleName(IFeature redefiningFeature, IElement target) + { + var referencedFeature = redefiningFeature.OwnedRelationship + .OfType() + .Select(referenceSubsetting => referenceSubsetting.ReferencedFeature) + .FirstOrDefault(referenced => referenced != null && !ReferenceEquals(referenced, target)); + + if (referencedFeature == null) + { + return false; + } + + var referencedNames = new[] { referencedFeature.name, referencedFeature.shortName } + .Where(candidate => !string.IsNullOrWhiteSpace(candidate)); + + return referencedNames.Any(candidate => + string.Equals(candidate, target.name, StringComparison.Ordinal) + || string.Equals(candidate, target.shortName, StringComparison.Ordinal)); + } + + /// + /// Determines whether establishes a RELATIVE namespace — a scope taken + /// from a preceding expression's result rather than lexical containment (KerML §8.2.3.5.2): + /// always, and only when + /// it carries a target binding. + /// + /// The element owning the membership at the reference site. + /// when the owner establishes a relative namespace. + private static bool EstablishesRelativeNamespace(IElement owner) + { + return owner switch + { + IFeatureChainExpression => true, + IAssignmentActionUsage assignmentActionUsage => assignmentActionUsage.targetArgument != null, + _ => false + }; + } + + /// + /// Determines whether is the first ownedFeatureChaining of its + /// featureChained. + /// + /// The chaining to test. + /// when it is the first. + private static bool IsFirstOwnedChaining(IFeatureChaining chaining) + { + return chaining.OwningRelatedElement is IFeature chainOwner + && ReferenceEquals(chainOwner.OwnedRelationship.OfType().FirstOrDefault(), chaining); + } + + /// + /// Determines whether is the flow-feature reference of a + /// that carries a subsetting prefix — the feature then resolves against the + /// prefix's type, like a chain accessor. Without the prefix it keeps lexical resolution. + /// + /// The source POCO at the reference site. + /// when the source is a prefixed flow-feature accessor. + private static bool IsFlowFeatureAccessor(IElement sourcePoco) + { + if (sourcePoco is not IRedefinition { OwningRelatedElement: IFeature flowFeature }) + { + return false; + } + + return flowFeature.OwningRelationship is IFeatureMembership { OwningRelatedElement: IFlowEnd flowEnd } + && flowEnd.OwnedRelationship.OfType().Any(); + } + + /// + /// Determines whether is one of the four kinds the non-invocation + /// Namespace excludes (KerML §8.2.3.5.2). + /// + /// The candidate scope. + /// when the scope is excluded. + private static bool IsInvocationScope(INamespace scope) + { + return scope switch + { + IFeatureReferenceExpression or IInstantiationExpression => true, + IFeature { owningType: IInstantiationExpression } => true, + IFeature { owningType: IFeature { owningType: IConstructorExpression constructor } constructorResult } + => ReferenceEquals(constructor.result, constructorResult), + _ => false + }; + } + + /// + /// Determines the local from the KIND of context relationship, per + /// KerML §8.2.3.5.2. Only the kinds whose local scope is NOT simply the nearest enclosing namespace + /// are handled here; everything else falls back to the containment climb. + /// + /// For a the spec anchors resolution at the + /// owningNamespace of the owningType — one level OUT from the owning feature — so the + /// owning feature's own and inherited members are NOT in scope. + /// + /// + /// A whose referencingFeature is an end feature of a + /// anchors at the connector's owning namespace, which reaches the referenced + /// name only through implied Specializations. See TranslateToResolutionGraph. + /// + /// + /// The context relationship at the reference site. + /// The local scope, or when the generic climb applies. + private static INamespace QueryContextRelationshipLocalScope(IElement sourcePoco) + { + return sourcePoco switch + { + // The FIRST ownedFeatureChaining anchors where its featureChained's owning Relationship + // anchors; a later one resolves against the preceding chainingFeature and is written bare. + IFeatureChaining chaining when IsFirstOwnedChaining(chaining) + => QueryContextRelationshipLocalScope((chaining.OwningRelatedElement as IFeature)?.OwningRelationship), + + // Must precede ISpecialization: a ReferenceSubsetting is one, and would otherwise be + // re-anchored by the rule below. + IReferenceSubsetting { referencingFeature: { IsEnd: true, owningType: IConnector connector } } => connector.owningNamespace, + ISpecialization specialization => specialization.owningType != null ? specialization.owningType.owningNamespace : specialization.owningNamespace, + IConjugation conjugation => conjugation.owningType != null ? conjugation.owningType.owningNamespace : conjugation.owningNamespace, + _ => null + }; + } + + /// + /// Returns the namespace containing , except for a + /// on a parameter of an , whose + /// value expression is resolved against the invocation rather than against the parameter. + /// + /// The membership whose containing scope is requested. + /// The scope, or when the membership has none. + private static INamespace QueryExpressionScope(IMembership membership) + { + var scope = ContainmentPaths.QueryParentNamespace(membership); + + if (scope == null) + { + return null; + } + + return membership is IFeatureValue && scope.owningNamespace is IInstantiationExpression invocation + ? invocation + : scope; + } + + /// + /// Returns the depth in at which a match becomes admissible. + /// + /// The chain, innermost first. + /// The floor scope, or for no floor. + /// The depth; 0 admits the whole chain. + /// + /// The floor is reached by a CONTAINMENT climb while the chain is materialised through + /// owningNamespace, so the floor is not guaranteed to sit on the chain — the invocation + /// redirect of can elect a Namespace the chain does not pass + /// through. Falling back to depth 0 there would silently re-admit the very scopes the floor exists + /// to exclude, so the floor's own CONTAINERS are tried next: the innermost of them that IS on the + /// chain sits at or outside the floor, which keeps the constraint at least as strict as intended. + /// Depth 0 is reached only when the two are genuinely unrelated. + /// + private static int QueryMatchFloorDepth(List scopes, INamespace matchFloorScope) + { + var visited = new HashSet(); + + for (var candidate = matchFloorScope; candidate != null && visited.Add(candidate); candidate = ContainmentPaths.QueryParentNamespace(candidate)) + { + var depth = scopes.IndexOf(candidate); + + if (depth >= 0) + { + return depth; + } + } + + return 0; + } + + /// + /// Determines whether the walk towards the non-invocation Namespace must step out of + /// (KerML §8.2.3.5.2). + /// + /// The membership whose scope is under test. + /// The candidate scope. + /// Whether this is the original membership rather than an enclosing one. + /// when the scope cannot be the non-invocation Namespace. + /// + /// The clause gates the entry on the Membership NOT being a + /// ; that gate gets the original Membership only, since the walk + /// past the first scope is governed by the definition of the non-invocation Namespace alone. + /// + private static bool StepsOutOfInvocation(IMembership subject, INamespace scope, bool isFirstStep) + { + if (subject is IFeatureValue || scope is IFeatureReferenceExpression) + { + return true; + } + + return (!isFirstStep || subject is not IFeatureMembership) && IsInvocationScope(scope); + } + + /// + /// Determines whether declares as one of + /// its own owned memberships — in which case the binding exists independently of any import of it. + /// + /// The namespace to inspect. + /// The membership to look for. + /// when the scope owns the membership. + private static bool OwnsMembership(INamespace scope, IMembership membership) + { + return scope.ownedMembership.Any(owned => ReferenceEquals(owned, membership)); + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/NameCandidate.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/NameCandidate.cs new file mode 100644 index 000000000..d90657a79 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/NameCandidate.cs @@ -0,0 +1,59 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System.Collections.Generic; + + using SysML2.NET.Core.POCO.Root.Elements; + + /// + /// One candidate spelling to verify: raw segments for probing, escaped segments for emission, and + /// the element the FIRST segment is expected to resolve to (the target itself for a single + /// segment, an anchor namespace for a qualified form). + /// + internal readonly struct NameCandidate + { + /// + /// Initializes a new instance of the struct. + /// + /// The raw segment names, outermost first. + /// The escaped segment forms, outermost first. + /// The element the first segment must resolve to. + internal NameCandidate(IReadOnlyList rawSegments, IReadOnlyList escapedSegments, IElement anchor) + { + this.RawSegments = rawSegments; + this.EscapedSegments = escapedSegments; + this.Anchor = anchor; + } + + /// Gets the raw segment names, outermost first. + internal IReadOnlyList RawSegments { get; } + + /// Gets the escaped segment forms, outermost first. + private IReadOnlyList EscapedSegments { get; } + + /// Gets the element the first segment must resolve to. + internal IElement Anchor { get; } + + /// Gets the emitted text. + internal string Text => string.Join("::", this.EscapedSegments); + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/NameResolutionCache.LayeredResolution.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/NameResolutionCache.LayeredResolution.cs new file mode 100644 index 000000000..17e817d35 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/NameResolutionCache.LayeredResolution.cs @@ -0,0 +1,385 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System.Collections.Generic; + using System.Linq; + + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + + /// + /// Resolves the textual form of a reference by generating candidate qualified names and accepting the + /// first that re-resolves to the target under KerML §8.2.3.5 basic name resolution. + /// + /// Bindings are held per Namespace with the source they enter through — owned, inherited or + /// imported (§8.2.3.5.3) — because visibility and the choice between competing bindings depend on + /// that source. + /// + /// + public sealed partial class NameResolutionCache + { + /// + /// Verifies one candidate spelling exactly the way a reader resolves it: the first segment walks + /// the site's scope chain inward-out (an inner binding SHADOWS — a hit that is not the expected + /// element fails the candidate rather than continuing outward), the remaining segments descend + /// name-entered namespaces seeing public bindings only, and the final element must be the target. + /// The site's match floor applies to where the first segment may match, exactly as before. + /// + /// The reference site. + /// The candidate spelling. + /// The referenced element. + /// when the candidate re-resolves to the target. + private bool TryVerifyCandidate(ReferenceSite site, NameCandidate candidate, IElement target) + { + var expectedFirst = candidate.Anchor ?? target; + + var referenceOwnMembership = site.SourcePoco as IMembership; + + for (var depth = 0; depth < site.Chain.Scopes.Count; depth++) + { + var scope = site.Chain.Scopes[depth]; + + var excluded = site.SelfBinding != null && ReferenceEquals(scope, site.SelfBinding.Scope) + ? site.SelfBinding.Membership + : referenceOwnMembership; + + var outcome = this.index.ResolveNameInScope(scope, candidate.RawSegments[0], excluded, site.LocalRedefiner, true); + + switch (outcome.Kind) + { + case LayeredOutcomeKind.NotBound: + continue; + + case LayeredOutcomeKind.Ambiguous: + return false; + + // Scopes below the floor are shadow-only: a match there neither accepts nor stops the walk. + case LayeredOutcomeKind.Bound when depth < site.Chain.MatchFloor: + continue; + + default: + // §8.2.3.5.1 step 3: the qualification part need only resolve to a Namespace the last segment + // reaches the target from. + if (candidate.RawSegments.Count == 1) + { + return ReferenceEquals(outcome.Element, expectedFirst); + } + + return this.TryDescendSegments(outcome.Element, candidate, target); + } + } + + // Full resolution from a root Namespace continues in the global Namespace (§8.2.3.5.4). + foreach (var globalScope in this.index.EnumerateGlobalScopes()) + { + var outcome = this.index.ResolveNameInScope(globalScope, candidate.RawSegments[0], null, null, false); + + if (outcome.Kind == LayeredOutcomeKind.Bound && ReferenceEquals(outcome.Element, expectedFirst)) + { + return this.TryDescendSegments(outcome.Element, candidate, target); + } + } + + return false; + } + + /// + /// Descends a candidate's remaining segments from the resolved first element, each hop probing the + /// current namespace from OUTSIDE (public bindings only). + /// + /// The element the first segment resolved to. + /// The candidate being verified. + /// The referenced element the last segment must yield. + /// when the descent ends on the target. + private bool TryDescendSegments(IElement first, NameCandidate candidate, IElement target) + { + var current = first; + + for (var segmentIndex = 1; segmentIndex < candidate.RawSegments.Count; segmentIndex++) + { + if (current is not INamespace currentNamespace) + { + return false; + } + + var step = this.index.ResolveNameInScope(currentNamespace, candidate.RawSegments[segmentIndex], null, null, false); + + if (step.Kind != LayeredOutcomeKind.Bound) + { + return false; + } + + current = step.Element; + } + + return ReferenceEquals(current, target); + } + + /// + /// Builds the global-scope-qualified form $::A::B::C for , or + /// when the containment path is unnameable or does not resolve. + /// + /// The referenced element. + /// The qualified name, or . + /// + /// KerML §8.2.3.5.1 step 2 resolves the segment after $ in the global Namespace, and step 3 + /// then descends by visible resolution — so this form reaches any element a root Namespace exposes, + /// whatever the reference site shadows. It is the last resort, tried only once every relative + /// candidate has failed. + /// + private string TryBuildGlobalQualifiedName(IElement target) + { + var containmentChain = new List(); + var current = target; + var visited = new HashSet(); + + while (current != null && visited.Add(current)) + { + containmentChain.Add(current); + current = current.owningNamespace; + } + + containmentChain.Reverse(); + + // A root Namespace is unnamed and contributes no segment (KerML §7.2.5.3). + if (containmentChain.Count != 0 && containmentChain[0].owningNamespace == null) + { + containmentChain.RemoveAt(0); + } + + if (containmentChain.Count == 0) + { + return null; + } + + var rawSegments = new List(); + var escapedSegments = new List(); + + foreach (var rawName in containmentChain.Select(SegmentNaming.QueryPreferredRawName)) + { + if (string.IsNullOrWhiteSpace(rawName)) + { + return null; + } + + rawSegments.Add(rawName); + escapedSegments.Add(SegmentNaming.Escape(rawName)); + } + + var topLevelElement = containmentChain[0]; + var candidate = new NameCandidate(rawSegments, escapedSegments, topLevelElement); + + foreach (var globalScope in this.index.EnumerateGlobalScopes()) + { + var head = this.index.ResolveNameInScope(globalScope, rawSegments[0], null, null, false); + + if (head.Kind == LayeredOutcomeKind.Bound + && ReferenceEquals(head.Element, topLevelElement) + && this.TryDescendSegments(topLevelElement, candidate, target)) + { + return $"$::{candidate.Text}"; + } + } + + return null; + } + + /// + /// Generates the candidate spellings for a target at a site, in the established preference order: + /// bare short name, bare name, alias names visible on the chain, facade-qualified forms, then + /// ancestor-anchored suffixes from the nearest ancestor outward. Every candidate is only EMITTED + /// after proves it re-resolves to the target. + /// + /// The referenced element. + /// The reference site. + /// The candidates, most preferred first. + private IEnumerable GenerateCandidates(IElement target, ReferenceSite site) + { + var rawShortName = target.shortName; + + if (!string.IsNullOrWhiteSpace(rawShortName)) + { + yield return new NameCandidate([rawShortName], [SegmentNaming.Escape(rawShortName)], null); + } + + var rawName = target.name; + + if (!string.IsNullOrWhiteSpace(rawName)) + { + yield return new NameCandidate([rawName], [SegmentNaming.Escape(rawName)], null); + } + + foreach (var scope in site.Chain.Scopes) + { + if (!this.index.TryGetAliases(scope, target, out var aliasNames)) + { + continue; + } + + foreach (var aliasName in aliasNames) + { + yield return new NameCandidate([aliasName], [SegmentNaming.Escape(aliasName)], null); + } + } + + // A Namespace publicly importing the owner re-exports it (§8.2.3.5.3) and can anchor the + // qualification part. The specification ranks neither spelling, so the shorter name wins. + if (target.owningNamespace is { } owner) + { + var targetSegmentRaw = SegmentNaming.QueryPreferredRawName(target); + var targetSegment = SegmentNaming.QueryPreferredEscapedSegment(target); + var ownerRaw = SegmentNaming.QueryPreferredRawName(owner); + var ownerEscaped = SegmentNaming.QueryPreferredEscapedSegment(owner); + var meaningfulShorterMax = (int)((ownerEscaped?.Length ?? int.MaxValue) * 0.7); + + if (!string.IsNullOrWhiteSpace(targetSegmentRaw)) + { + var facades = this.index.QueryFacades(owner) + .OrderBy(facade => facade, Comparer.Create(ContainmentPaths.CompareFacades)) + .ToList(); + + foreach (var facade in facades.Where(facade => (SegmentNaming.QueryPreferredEscapedSegment(facade)?.Length ?? int.MaxValue) <= meaningfulShorterMax)) + { + var facadeRaw = SegmentNaming.QueryPreferredRawName(facade); + + if (!string.IsNullOrWhiteSpace(facadeRaw)) + { + yield return new NameCandidate([facadeRaw, targetSegmentRaw], [SegmentNaming.QueryPreferredEscapedSegment(facade), targetSegment], facade); + } + } + + if (!string.IsNullOrWhiteSpace(ownerRaw) && !string.IsNullOrWhiteSpace(ownerEscaped)) + { + yield return new NameCandidate([ownerRaw, targetSegmentRaw], [ownerEscaped, targetSegment], owner); + } + + foreach (var facade in facades.Where(facade => (SegmentNaming.QueryPreferredEscapedSegment(facade)?.Length ?? int.MaxValue) > meaningfulShorterMax)) + { + var facadeRaw = SegmentNaming.QueryPreferredRawName(facade); + + if (!string.IsNullOrWhiteSpace(facadeRaw)) + { + yield return new NameCandidate([facadeRaw, targetSegmentRaw], [SegmentNaming.QueryPreferredEscapedSegment(facade), targetSegment], facade); + } + } + } + } + + var rawPathDown = new List(); + var escapedPathDown = new List(); + + var pathRaw = SegmentNaming.QueryPreferredRawName(target); + var pathEscaped = SegmentNaming.QueryPreferredEscapedSegment(target); + + if (string.IsNullOrWhiteSpace(pathRaw)) + { + yield break; + } + + rawPathDown.Add(pathRaw); + escapedPathDown.Add(pathEscaped); + + var ancestor = (IElement)target.owningNamespace; + var visitedAncestors = new HashSet(); + + if (ancestor != null && visitedAncestors.Add(ancestor)) + { + var ownerWalkRaw = SegmentNaming.QueryPreferredRawName(ancestor); + var ownerWalkEscaped = SegmentNaming.QueryPreferredEscapedSegment(ancestor); + + if (string.IsNullOrWhiteSpace(ownerWalkRaw) || string.IsNullOrWhiteSpace(ownerWalkEscaped)) + { + yield break; + } + + rawPathDown.Add(ownerWalkRaw); + escapedPathDown.Add(ownerWalkEscaped); + ancestor = ancestor.owningNamespace; + } + + while (ancestor != null && visitedAncestors.Add(ancestor)) + { + var ancestorRaw = SegmentNaming.QueryPreferredRawName(ancestor); + var ancestorEscaped = SegmentNaming.QueryPreferredEscapedSegment(ancestor); + + if (string.IsNullOrWhiteSpace(ancestorRaw) || string.IsNullOrWhiteSpace(ancestorEscaped)) + { + yield break; + } + + var rawSegments = new List { ancestorRaw }; + rawSegments.AddRange(Enumerable.Reverse(rawPathDown)); + + var escapedSegments = new List { ancestorEscaped }; + escapedSegments.AddRange(Enumerable.Reverse(escapedPathDown)); + + yield return new NameCandidate(rawSegments, escapedSegments, ancestor); + + rawPathDown.Add(ancestorRaw); + escapedPathDown.Add(ancestorEscaped); + + ancestor = ancestor.owningNamespace; + } + } + + /// + /// Resolves the shortest spelling that a parser-faithful reader resolves back to + /// at , falling back to the qualified forms — + /// never empty for a non-null target. + /// + /// The referenced element. + /// The reference site. + /// The pre-computed escaped name of the target. + /// The spelling to emit. + private string ResolveViaLayeredScopes(IElement target, ReferenceSite site, string escapedName) + { + var verifiedText = this.GenerateCandidates(target, site) + .Where(candidate => this.TryVerifyCandidate(site, candidate, target)) + .Select(candidate => candidate.Text) + .FirstOrDefault(); + + if (!string.IsNullOrWhiteSpace(verifiedText)) + { + return verifiedText; + } + + if (this.TryBuildGlobalQualifiedName(target) is { } globalQualifiedName) + { + return globalQualifiedName; + } + + var shortQualifiedName = ContainmentPaths.QueryShortQualifiedName(target); + + if (!string.IsNullOrWhiteSpace(shortQualifiedName)) + { + return shortQualifiedName; + } + + if (!string.IsNullOrWhiteSpace(target.qualifiedName)) + { + return target.qualifiedName; + } + + return escapedName ?? string.Empty; + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/NameResolutionCache.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/NameResolutionCache.cs new file mode 100644 index 000000000..710b81893 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/NameResolutionCache.cs @@ -0,0 +1,760 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using SysML2.NET.Core.POCO.Core.Features; + using SysML2.NET.Core.POCO.Core.Types; + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Core.Root.Namespaces; + using SysML2.NET.Semantics.Implied; + + /// + /// Resolves the shortest unambiguous textual name for a reference, mirroring KerML §8.2.3.5. + /// Holds an eager per-namespace simple-name index built on construction, plus lazy caches for + /// source scope chains and resolved emissions keyed by (target, sourceLocalScope). + /// + public sealed partial class NameResolutionCache + { + /// + /// Supplies the implied Relationships (KerML §8.4.2) that a model exported without them + /// omits, so a name reachable only through one can still be shortened. + /// + private readonly IImpliedRelationshipProvider impliedRelationshipProvider; + + /// + /// The bindings every reachable Namespace offers, built once on construction. + /// + private readonly NamespaceBindingIndex index; + + /// + /// Lazy cache: (target.Id, sourceLocalScope.Id, matchFloorScope.Id) → emitted string. + /// + private readonly Dictionary<(Guid TargetId, Guid SourceScopeId, Guid MatchFloorId), string> resolvedReferences + = new(); + + /// + /// Lazy cache: source-POCO id → its upward containment chain of namespaces. + /// + private readonly Dictionary sourceScopeChains + = new(); + + /// + /// Initializes the cache and eagerly indexes every namespace reachable from + /// . + /// + /// The root being serialized. + /// + /// The other loaded root namespaces (model libraries), forming the global namespace per + /// KerML §8.2.3.5.2. Optional — without them resolution falls back to longer, equally valid names. + /// + /// + /// The provider supplying the implied Relationships a model exported without them omits. + /// Optional — when absent, resolution sees only the declared Specializations, so a name + /// reachable ONLY through an implied one degrades to a longer, equally valid form. + /// + public NameResolutionCache(INamespace rootNamespace, IEnumerable globalNamespaces = null, IImpliedRelationshipProvider impliedRelationshipProvider = null) + { + if (rootNamespace == null) + { + throw new ArgumentNullException(nameof(rootNamespace)); + } + + var otherRootNamespaces = globalNamespaces? + .Where(candidate => candidate != null && !ReferenceEquals(candidate, rootNamespace)) + .Distinct() + .ToList() ?? []; + + this.impliedRelationshipProvider = impliedRelationshipProvider ?? NullImpliedRelationshipProvider.Instance; + + this.index = new NamespaceBindingIndexBuilder(rootNamespace, otherRootNamespaces, this.impliedRelationshipProvider).Build(); + } + + /// + /// Gets the root — the fallback local scope when a source POCO has no + /// resolvable enclosing namespace. + /// + private INamespace RootNamespace => this.index.RootNamespace; + + /// + /// Resolves the textual notation for a reference to at the site of + /// . Results are memoised per (target, sourceLocalScope). + /// + /// The referenced ; may be . + /// The POCO at whose syntactic position the reference appears. + /// The string to emit; empty when is . + public string Resolve(IElement target, IElement sourcePoco) + { + switch (target) + { + case null: + return string.Empty; + + case IMembership membership: + if (membership.MemberElement == null) + { + return string.Empty; + } + + if (sourcePoco is IMembershipImport) + { + // §8.2.3.5.1: a MembershipImport's qualified name identifies the MEMBERSHIP itself, + // the one case where it is not the memberElement. The two coincide for an owning + // Membership, but an alias binds the element under its own name in its own + // Namespace, so naming the element would import a DIFFERENT Membership and bind a + // different name into the importing Namespace. + var aliasPath = this.ResolveAliasMembership(membership, sourcePoco); + + if (!string.IsNullOrWhiteSpace(aliasPath)) + { + return aliasPath; + } + + target = membership.MemberElement; + break; + } + + var membershipPath = ContainmentPaths.QueryShortQualifiedName(membership.MemberElement, sourcePoco); + + if (!string.IsNullOrWhiteSpace(membershipPath)) + { + return membershipPath; + } + + // An unnamed member element yields no path here; resolving it as an ordinary target + // reaches its effective name, and a QualifiedName segment can never be empty. + target = membership.MemberElement; + break; + } + + // A NamespaceImport keeps a self-contained path unless the target is reachable by + // containment, so the name cannot depend on a sibling import. A MembershipImport names one + // element and is exempt. + if (sourcePoco is IImport { OwningRelatedElement: { } importOwner } and not IMembershipImport + && !ContainmentPaths.IsReachableByContainment(target, importOwner)) + { + return this.QueryImportPath(target); + } + + var escapedName = target.EscapedName(); + + if (string.IsNullOrWhiteSpace(escapedName)) + { + var impliedName = this.QueryImpliedEffectiveName(target); + + escapedName = string.IsNullOrWhiteSpace(impliedName) ? null : SegmentNaming.Escape(impliedName); + } + + if (string.IsNullOrWhiteSpace(escapedName)) + { + return target.qualifiedName ?? string.Empty; + } + + if (LocalScopeResolver.IsChainAccessor(sourcePoco)) + { + return this.ResolveChainAccessor(target, sourcePoco, escapedName); + } + + var sourceLocalScope = LocalScopeResolver.GetSourceLocalScope(sourcePoco, this.RootNamespace); + + // Inside a FeatureValue expression the innermost scopes are shadow-only, since the readings + // of §8.2.3.5.2 disagree about them. See LocalScopeResolver.QueryValueExpressionMatchFloor. + var matchFloorScope = LocalScopeResolver.QueryValueExpressionMatchFloor(sourcePoco) ?? sourceLocalScope; + + // §8.2.3.5.1: a Redefinition's redefinedFeature resolves against the generals of the owning + // Type, not the local namespace. Skipped when the redefiner declares that same name, where the + // bare form would read as a self-reference. + if (sourcePoco is IRedefinition { RedefiningFeature: { } redefiningFeature } redefinitionContext + && ReferenceEquals(target, redefinitionContext.RedefinedFeature) + && !LocalScopeResolver.RedefinerDeclaredNameCollidesWith(redefiningFeature, target) + && !LocalScopeResolver.ReferencedFeatureSharesSimpleName(redefiningFeature, target) + && this.QueryRedefinedFeatureScope(redefinitionContext, target) is { } redefinitionScope) + { + sourceLocalScope = redefinitionScope; + + matchFloorScope = redefinitionScope; + } + + // A redefining or referencing feature takes its effective name from the target, so it is + // bound under the name being resolved and must not shadow it (§8.2.3.5). + var localReferencer = sourcePoco switch + { + IRedefinition redefinition when ReferenceEquals(target, redefinition.RedefinedFeature) => redefinition.RedefiningFeature, + IReferenceSubsetting referenceSubsetting when ReferenceEquals(target, referenceSubsetting.ReferencedFeature) => referenceSubsetting.referencingFeature, + _ => null + }; + + if (localReferencer != null && !LocalScopeResolver.RedefinerDeclaredNameCollidesWith(localReferencer, target)) + { + return this.ResolveFresh(target, this.BuildReferenceSite(sourcePoco, sourceLocalScope, matchFloorScope, localReferencer), escapedName); + } + + var referenceSite = this.BuildReferenceSite(sourcePoco, sourceLocalScope, matchFloorScope, null); + + // The memo key does not capture the self binding, so a self-binding site must not share a + // memo entry with an ordinary one at the same scope. + if (referenceSite.SelfBinding != null) + { + return this.ResolveFresh(target, referenceSite, escapedName); + } + + var cacheKey = (target.Id, sourceLocalScope?.Id ?? Guid.Empty, matchFloorScope?.Id ?? Guid.Empty); + + if (this.resolvedReferences.TryGetValue(cacheKey, out var cached)) + { + return cached; + } + + var resolved = this.ResolveFresh(target, referenceSite, escapedName); + this.resolvedReferences[cacheKey] = resolved; + return resolved; + } + + /// + /// Returns a SELF-CONTAINED path to , anchored at the outermost named + /// ancestor that binds it directly, so the path never depends on names introduced by imports of the + /// importing namespace. + /// + /// Intermediate owner segments the anchor re-exports through a public import are collapsed, + /// since the anchor's visible resolution already reaches the target (KerML §8.2.3.5.3). + /// + /// + /// The imported ; must be non-null. + /// The import path, or the target's qualifiedName when no anchor collapses. + private string QueryImportPath(IElement target) + { + var namedAncestors = new List(); + + for (var ancestor = target.owningNamespace; ancestor != null; ancestor = ancestor.owningNamespace) + { + if (string.IsNullOrWhiteSpace(SegmentNaming.QueryPreferredEscapedSegment(ancestor))) + { + break; + } + + namedAncestors.Add(ancestor); + } + + var targetSegment = SegmentNaming.QueryPreferredEscapedSegment(target); + + if (targetSegment == null) + { + return target.qualifiedName ?? string.Empty; + } + + namedAncestors.Reverse(); + + for (var anchorIndex = 0; anchorIndex < namedAncestors.Count; anchorIndex++) + { + if (namedAncestors[anchorIndex] is not INamespace anchor + || !this.BindsDirectly(anchor, target, targetSegment) + || !this.IsSuffixVisible(namedAncestors.Take(anchorIndex + 1).Append(target))) + { + continue; + } + + var segments = namedAncestors + .Take(anchorIndex + 1) + .Select(SegmentNaming.QueryPreferredEscapedSegment) + .Append(targetSegment); + + return string.Join("::", segments); + } + + return namedAncestors.Count == 0 + ? targetSegment + : target.qualifiedName ?? string.Empty; + } + + /// + /// Determines whether 's index binds uniquely + /// to , VISIBLY — i.e. the target is nameable from outside that scope, + /// which is what a qualified path through it requires (KerML §8.2.3.5.3). + /// + /// The candidate anchor namespace. + /// The element being named. + /// The target's escaped simple-name segment. + /// when the scope binds the segment to exactly the target. + private bool BindsDirectly(INamespace scope, IElement target, string segment) + { + var rawName = SegmentNaming.QueryPreferredRawName(target); + + return !string.IsNullOrWhiteSpace(rawName) + && this.index.GetSimpleNameIndex(scope).TryGetValue(rawName, out var bucket) + && bucket.Count == 1 + && bucket.Contains(target) + && !string.IsNullOrWhiteSpace(segment) + && this.BindsVisibly(scope, rawName, target); + } + + /// + /// Assembles the for one reference: its scope chain plus the + /// exclusions every probe of it must honour. + /// + /// The POCO bearing the reference. + /// The pre-computed local scope (may be ). + /// The innermost scope a match may come from (may be ). + /// Feature to exclude from every bucket, or . + /// The reference site. + private ReferenceSite BuildReferenceSite(IElement sourcePoco, INamespace sourceLocalScope, INamespace matchFloorScope, IFeature localRedefiner) + { + return new ReferenceSite( + sourcePoco, + this.GetSourceScopeChain(sourcePoco, sourceLocalScope, matchFloorScope), + localRedefiner, + LocalScopeResolver.QuerySelfBinding(sourcePoco)); + } + + /// + /// First-time resolution, delegated to the layered engine: candidate spellings are generated + /// shortest-first and the first that re-resolves to the target (KerML §8.2.3.5.1) is emitted. + /// + /// The referenced element. + /// The reference site: its scope chain and the exclusions that apply to every probe. + /// The target's escaped raw name. + /// The resolved emission string. + private string ResolveFresh(IElement target, ReferenceSite site, string escapedName) + { + return this.ResolveViaLayeredScopes(target, site, escapedName); + } + + /// + /// Determines whether every hop WITHIN resolves visibly — the + /// hop from the anchor into the path is checked separately, against whatever the anchor segment + /// actually resolves to. + /// + /// The suffix elements, outermost first. + /// when the suffix re-resolves segment by segment. + private bool IsSuffixVisible(IEnumerable pathDownToTarget) + { + IElement predecessor = null; + + foreach (var segmentElement in pathDownToTarget) + { + if (predecessor != null) + { + var rawName = SegmentNaming.QueryPreferredRawName(segmentElement); + + if (predecessor is not INamespace predecessorScope + || string.IsNullOrWhiteSpace(rawName) + || !this.BindsVisibly(predecessorScope, rawName, segmentElement)) + { + return false; + } + } + + predecessor = segmentElement; + } + + return true; + } + + /// + /// Returns the effective name of a Feature that carries none of its own, taken from the Feature it + /// redefines through an implied Redefinition (KerML §8.4.2). + /// + /// The element to name. + /// The implied effective name, or when none applies. + private string QueryImpliedEffectiveName(IElement element) + { + if (element is not IFeature feature || !string.IsNullOrWhiteSpace(feature.name) || !string.IsNullOrWhiteSpace(feature.shortName)) + { + return null; + } + + return this.impliedRelationshipProvider + .GetImpliedRedefinitions(feature) + .Select(redefinition => redefinition.RedefinedFeature) + .Where(redefined => redefined != null && !ReferenceEquals(redefined, feature)) + .Select(SegmentNaming.QueryPreferredRawName) + .FirstOrDefault(name => !string.IsNullOrWhiteSpace(name)); + } + + /// + /// Resolves a chain accessor's simple name: the target's name first, then shortName, + /// then qualifiedName as a last resort. + /// + /// The chain-accessor target element. + /// The source + /// The pre-computed escaped name form. + /// The escaped simple name. + private string ResolveChainAccessor(IElement target, IElement sourcePoco, string escapedName) + { + var rawName = SegmentNaming.QueryPreferredRawName(target); + + if (string.IsNullOrWhiteSpace(rawName)) + { + rawName = this.QueryImpliedEffectiveName(target); + } + + if (string.IsNullOrWhiteSpace(rawName)) + { + return target.qualifiedName ?? string.Empty; + } + + if (this.BindsRelativeNameElsewhere(sourcePoco, rawName, target)) + { + // The bare name is proven to bind elsewhere, so it must not be emitted while any qualified + // form is available; a segment can never be empty, so the bare name remains the floor. + return SegmentNaming.FirstNonBlank(this.TryBuildGlobalQualifiedName(target), ContainmentPaths.QueryShortQualifiedName(target), target.qualifiedName) + ?? SegmentNaming.Escape(rawName); + } + + return string.IsNullOrWhiteSpace(target.name) ? SegmentNaming.Escape(rawName) : escapedName; + } + + /// + /// Determines whether the relative Namespace a chain accessor resolves against binds + /// to something other than , which makes the + /// bare simple name unusable (KerML §8.2.3.5.2). + /// + /// The source POCO at the reference site. + /// The simple name that would be emitted. + /// The referenced element. + /// only on a definitive mismatch. + /// + /// A name the relative Namespace does not bind at all is not a mismatch: the index reaches only what + /// the model carries, so absence is inconclusive and the bare name stands. + /// + private bool BindsRelativeNameElsewhere(IElement sourcePoco, string rawName, IElement target) + { + if (LocalScopeResolver.QueryRelativeNamespace(sourcePoco) is not { } relativeNamespace) + { + return false; + } + + var outcome = this.index.ResolveNameInScope(relativeNamespace, rawName, null, null, false); + + return outcome.Kind switch + { + LayeredOutcomeKind.Bound => !ReferenceEquals(outcome.Element, target), + LayeredOutcomeKind.Ambiguous => true, + _ => false + }; + } + + /// + /// Returns the cached upward-walk chain for , building it on first + /// encounter from . + /// + /// The source POCO bearing the reference; may be . + /// The pre-computed local scope (may be ). + /// The innermost scope a match may come from (may be ). + /// The cached chain. + private SourceScopeChain GetSourceScopeChain(IElement sourcePoco, INamespace sourceLocalScope, INamespace matchFloorScope) + { + if (sourcePoco == null) + { + return LocalScopeResolver.BuildChain(sourceLocalScope ?? this.RootNamespace, matchFloorScope); + } + + if (this.sourceScopeChains.TryGetValue(sourcePoco.Id, out var cached)) + { + return cached; + } + + var chain = LocalScopeResolver.BuildChain(sourceLocalScope ?? this.RootNamespace, matchFloorScope); + this.sourceScopeChains[sourcePoco.Id] = chain; + + return chain; + } + + /// + /// Names an ALIAS through the alias itself, as + /// {owning namespace}::{memberName}. + /// + /// + /// Returns for an owning Membership, where the Membership and its + /// memberElement denote the same binding and the ordinary element path already names it. + /// + /// The Membership being imported. + /// The POCO at whose syntactic position the reference appears. + /// The alias path, or when the Membership is not an alias. + private string ResolveAliasMembership(IMembership membership, IElement sourcePoco) + { + if (ReferenceEquals(membership, membership.MemberElement.owningMembership)) + { + return null; + } + + var aliasName = !string.IsNullOrWhiteSpace(membership.MemberName) + ? membership.MemberName + : membership.MemberShortName; + + if (string.IsNullOrWhiteSpace(aliasName) || membership.membershipOwningNamespace == null) + { + return null; + } + + var namespacePath = this.Resolve(membership.membershipOwningNamespace, sourcePoco); + + return string.IsNullOrWhiteSpace(namespacePath) + ? SegmentNaming.Escape(aliasName) + : $"{namespacePath}::{SegmentNaming.Escape(aliasName)}"; + } + + /// + /// Returns the scope in which a 's redefinedFeature is resolved per + /// KerML §8.2.3.5.1: the general Type of each ownedSpecialization of the owning feature's + /// owningType, tried in turn until one binds the name. Falls back to the first such general type + /// so the qualified form is still anchored correctly. + /// + /// The redefinition at the reference site. + /// The redefined feature being named. + /// The scope, or when the exception does not apply. + private INamespace QueryRedefinedFeatureScope(IRedefinition redefinition, IElement target) + { + var owningType = redefinition.RedefiningFeature?.owningType; + + if (owningType == null) + { + return null; + } + + List generalScopes; + + generalScopes = + [ + ..owningType.ownedSpecialization + .Select(specialization => specialization.General) + .OfType() + .Where(general => !ReferenceEquals(general, owningType)) + ]; + + // A model exported without implied Relationships (§8.4.2) omits Specializations the abstract + // syntax requires. Appended last so declared supertypes keep priority. + generalScopes.AddRange(this.impliedRelationshipProvider.GetImpliedSpecializations(owningType) + .Select(specialization => this.index.ResolutionGraph.TranslateToResolutionGraph(specialization.General)) + .OfType() + .Where(general => !ReferenceEquals(general, owningType) && !generalScopes.Contains(general))); + + if (generalScopes.Count == 0) + { + return null; + } + + var rawName = SegmentNaming.QueryPreferredRawName(target); + + if (string.IsNullOrWhiteSpace(rawName)) + { + return generalScopes[0]; + } + + // §8.2.3.5.1 stops at the first general where "a resolution is found" — a resolution being one + // whose Element also has the proper type for the context, here Redefinition::redefinedFeature, + // so any Feature qualifies and a non-Feature binding does not stop the walk. Selecting the + // first scope binding the name to the TARGET instead would skip an earlier general that + // shadows it, and a bare name emitted on that basis re-reads as the shadowing feature. + return generalScopes.FirstOrDefault(scope => + this.ResolveSimpleNameInScope(scope, target, rawName, null, null, static element => element is IFeature) + == SimpleNameResolution.Matched); + } + + /// + /// Probes 's index for . Mirrors the parser's + /// local resolution: the local referencer and the self-binding entry are excluded, candidates are + /// reduced to redefinition leaves (KerML §8.2.3.5.3 — at most one Membership per name), and the + /// name matches only when that leaf set is exactly the target. + /// + /// The scope whose index is inspected. + /// The element to look up. + /// The simple-name lexical form to probe; must be non-blank. + /// Feature to exclude from the bucket, or . + /// The binding the reference itself is, or . + /// Predicate deciding whether a bound element counts as the target, or for reference identity. + /// The resolution state. + private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement target, string rawName, IFeature localRedefiner, SelfBinding selfBinding, Func accept = null) + { + var simpleNameIndex = this.index.GetSimpleNameIndex(scope); + + if (!simpleNameIndex.TryGetValue(rawName, out var elements)) + { + return SimpleNameResolution.NotBound; + } + + accept ??= candidate => ReferenceEquals(candidate, target); + + if (selfBinding != null && ReferenceEquals(scope, selfBinding.Scope)) + { + var isBoundToOtherElement = elements.Any(element => + !accept(element) && !ReferenceEquals(element, localRedefiner)); + + if (isBoundToOtherElement) + { + return SimpleNameResolution.Shadowed; + } + + return this.BindsTargetIndependently(scope, rawName, target, selfBinding.Membership) + ? SimpleNameResolution.Matched + : SimpleNameResolution.NotBound; + } + + var candidates = elements.Where(element => !ReferenceEquals(element, localRedefiner)).ToList(); + + if (candidates.Count == 0) + { + return SimpleNameResolution.NotBound; + } + + if (candidates.Count == 1) + { + return accept(candidates[0]) + ? SimpleNameResolution.Matched + : SimpleNameResolution.Shadowed; + } + + candidates = NamespaceBindingIndex.PreferDirectlyOwnedOverInherited(scope, candidates); + + var shadowed = new HashSet(); + + foreach (var candidate in candidates.OfType()) + { + foreach (var redefined in candidate.AllRedefinedFeatures().Where(redefined => !ReferenceEquals(redefined, candidate))) + { + shadowed.Add(redefined); + } + } + + IElement onlyLeaf = null; + var leafCount = 0; + + foreach (var element in candidates.Where(element => element is not IFeature feature || !shadowed.Contains(feature))) + { + leafCount++; + + if (leafCount > 1) + { + return SimpleNameResolution.Shadowed; + } + + onlyLeaf = element; + } + + return leafCount == 1 && accept(onlyLeaf) + ? SimpleNameResolution.Matched + : SimpleNameResolution.Shadowed; + } + + /// + /// Determines whether binds to + /// through a Membership OTHER than , + /// the reference being emitted. + /// + /// The scope to inspect. + /// The simple-name lexical form. + /// The referenced element. + /// The Membership that IS the reference. + /// when the binding survives without the reference itself. + /// + /// A live query rather than index provenance; a source it fails to cover costs a longer name, never + /// an invalid one. + /// + private bool BindsTargetIndependently(INamespace scope, string rawName, IElement target, IMembership selfMembership) + { + return this.QueryBindingMemberships(scope, false) + .Any(membership => !ReferenceEquals(membership, selfMembership) && BindsName(membership, rawName, target)); + } + + /// + /// Determines whether binds to + /// among its VISIBLE Memberships — the test the parser applies to every + /// segment of a qualified name after the first. + /// + /// The scope named by the preceding segment. + /// The segment's simple-name lexical form. + /// The element the segment must name. + /// when the segment resolves visibly to the target. + /// + /// Namespace::resolve resolves the FIRST segment with resolveLocal — the outward climb + /// over owned, imported and inherited Memberships of ANY visibility — but every following segment + /// with resolveVisible, i.e. visibleMemberships(Set{}, false, false), which is public + /// only (KerML §8.2.3.5.3). The simple-name index cannot answer this: it is built with the + /// visibility filter of the path that REACHED the scope, and within the model that admits + /// everything. Emitting A::b for a b that is private in A would produce a name + /// no conformant parser resolves. + /// + private bool BindsVisibly(INamespace scope, string rawName, IElement target) + { + return this.QueryBindingMemberships(scope, true) + .Any(membership => BindsName(membership, rawName, target)); + } + + /// + /// Enumerates the Memberships that give its name bindings: owned, + /// imported, inherited, and those contributed by implied generals. + /// + /// The scope whose bindings are collected. + /// Whether to keep only the bindings visible OUTSIDE the scope. + /// The Memberships, with duplicates possible. + private List QueryBindingMemberships(INamespace scope, bool visibleOnly) + { + var memberships = new List(scope.ownedMembership.Where(ownedMember => ImportExpansion.PassesVisibilityFilter(ownedMember, visibleOnly))); + + memberships.AddRange(ImportExpansion.QueryImportedMembershipsSafe(scope, visibleOnly)); + + if (scope is not IType type) + { + return memberships; + } + + memberships.AddRange(ImportExpansion.QueryInheritedMemberships(type).Where(inheritedMember => ImportExpansion.PassesVisibilityFilter(inheritedMember, visibleOnly))); + + var declaredSupertypes = type.AllSupertypes() + .Where(supertype => !ReferenceEquals(supertype, type)) + .ToList(); + + foreach (var impliedGeneral in this.index.ResolutionGraph.QueryImpliedGeneralClosure(type, declaredSupertypes)) + { + memberships.AddRange(impliedGeneral.ownedMembership + .Where(ownedMember => ownedMember.Visibility != VisibilityKind.Private && ImportExpansion.PassesVisibilityFilter(ownedMember, visibleOnly))); + + memberships.AddRange(ImportExpansion.QueryInheritedMemberships(impliedGeneral).Where(inheritedMember => ImportExpansion.PassesVisibilityFilter(inheritedMember, visibleOnly))); + } + + return memberships; + } + + /// + /// Determines whether binds to + /// , under either lexical form. + /// + /// The Membership to test. + /// The simple-name lexical form. + /// The referenced element. + /// when the membership binds the name to the target. + private static bool BindsName(IMembership membership, string rawName, IElement target) + { + if (membership?.MemberElement == null || !ReferenceEquals(membership.MemberElement, target)) + { + return false; + } + + var (shortName, longName) = SegmentNaming.QueryMembershipNames(membership, target); + + return string.Equals(shortName, rawName, StringComparison.Ordinal) + || string.Equals(longName, rawName, StringComparison.Ordinal); + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/NamespaceBindingIndex.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/NamespaceBindingIndex.cs new file mode 100644 index 000000000..a28a4118b --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/NamespaceBindingIndex.cs @@ -0,0 +1,313 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using SysML2.NET.Core.POCO.Core.Features; + using SysML2.NET.Core.POCO.Core.Types; + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Core.Root.Namespaces; + + /// + /// The bindings every indexed Namespace offers, and the lookups that answer "what does this name bind + /// to in this scope" (KerML §8.2.3.5.3). + /// + internal sealed class NamespaceBindingIndex + { + /// + /// Shared empty index returned for unknown or null Namespaces. + /// + private static readonly IReadOnlyDictionary> EmptyIndex + = new Dictionary>(StringComparer.Ordinal); + + /// + /// The layer probing order: owned shadows inherited shadows imported. + /// + private static readonly BindingLayer[] LayerPrecedence = [BindingLayer.Owned, BindingLayer.Inherited, BindingLayer.Imported]; + + /// + /// Reverse index of alias bindings: scope to (aliased element to alias names). + /// + private readonly Dictionary>> aliasIndex; + + /// + /// Reverse index: owning Namespace to the Namespaces re-exporting it through a public import. + /// + private readonly Dictionary> directFacadeIndex; + + /// + /// The other root Namespaces forming the global Namespace. + /// + private readonly List globalNamespaces; + + /// + /// The layered binding tables, one per indexed scope. + /// + private readonly Dictionary layeredBindings; + + /// + /// Namespace to (simple name to member set), populated on first probe of each scope. + /// + private readonly Dictionary>> simpleNameIndices = new(); + + /// + /// Builds a scope's bindings on its first probe. + /// + private readonly NamespaceBindingIndexBuilder scopeIndexBuilder; + + /// + /// Initializes a new instance of the class. + /// + /// The indexed root Namespace. + /// The other root Namespaces forming the global Namespace. + /// The builder invoked to index a scope on its first probe. + /// Namespace to layered binding table, filled by . + /// Owning Namespace to its re-exporting Namespaces. + /// Scope to its alias bindings, filled by . + /// Maps implied generals onto the graph being written. + internal NamespaceBindingIndex( + INamespace rootNamespace, + List globalNamespaces, + NamespaceBindingIndexBuilder scopeIndexBuilder, + Dictionary layeredBindings, + Dictionary> directFacadeIndex, + Dictionary>> aliasIndex, + ResolutionGraph resolutionGraph) + { + this.RootNamespace = rootNamespace; + this.globalNamespaces = globalNamespaces; + this.scopeIndexBuilder = scopeIndexBuilder; + this.layeredBindings = layeredBindings; + this.directFacadeIndex = directFacadeIndex; + this.aliasIndex = aliasIndex; + this.ResolutionGraph = resolutionGraph; + } + + /// + /// Gets the indexed root Namespace. + /// + internal INamespace RootNamespace { get; } + + /// + /// Gets the map from implied generals onto the graph being written. + /// + internal ResolutionGraph ResolutionGraph { get; } + + /// + /// Returns the alias names binds under. + /// + /// The scope declaring the aliases. + /// The aliased element. + /// The alias names, when any. + /// when the scope aliases the target. + internal bool TryGetAliases(INamespace scope, IElement target, out List aliasNames) + { + aliasNames = null; + this.EnsureIndexed(scope); + + return this.aliasIndex.TryGetValue(scope, out var scopeAliases) && scopeAliases.TryGetValue(target, out aliasNames); + } + + /// + /// Indexes on its first probe. + /// + /// The scope being probed; may be . + private void EnsureIndexed(INamespace scope) + { + if (scope == null || this.simpleNameIndices.ContainsKey(scope)) + { + return; + } + + this.simpleNameIndices[scope] = this.scopeIndexBuilder.BuildScopeIndex(scope); + } + + /// + /// Returns the Namespaces that re-export through a public import. + /// + /// The owning Namespace. + /// The re-exporting Namespaces; empty when there are none. + internal IEnumerable QueryFacades(INamespace owner) + { + return this.directFacadeIndex.TryGetValue(owner, out var facades) ? facades : []; + } + + /// + /// Returns the simple-name index for , building it on first probe, or + /// for . + /// + /// The whose index is requested. + /// The simple-name → member-set lookup. + internal IReadOnlyDictionary> GetSimpleNameIndex(INamespace scope) + { + this.EnsureIndexed(scope); + + return scope == null ? EmptyIndex : this.simpleNameIndices.GetValueOrDefault(scope, EmptyIndex); + } + + /// + /// Probes one raw simple name in one scope, layer by layer in shadowing order, electing the + /// redefinition leaf among a layer's peers. + /// + /// The scope to probe. + /// The raw simple name. + /// The reference's own Membership, absent at parse time; may be . + /// The redefining feature excluded from every probe; may be . + /// Whether the scope is seen from INSIDE (containment), which admits non-public bindings. + /// The outcome kind and, when bound, the elected element. + internal (LayeredOutcomeKind Kind, IElement Element) ResolveNameInScope(INamespace scope, string rawName, IMembership excludedMembership, IFeature localRedefiner, bool insideView) + { + this.EnsureIndexed(scope); + + if (scope == null + || !this.layeredBindings.TryGetValue(scope, out var table) + || !table.ByName.TryGetValue(rawName, out var bindings)) + { + return (LayeredOutcomeKind.NotBound, null); + } + + foreach (var layer in LayerPrecedence) + { + var candidates = bindings + .Where(binding => binding.Layer == layer + && !ReferenceEquals(binding.Membership, excludedMembership) + && !ReferenceEquals(binding.Element, localRedefiner) + && (insideView || IsExternallyVisible(binding))) + .Select(binding => binding.Element) + .Distinct() + .ToList(); + + if (candidates.Count == 0) + { + continue; + } + + var elected = ElectRedefinitionLeaf(scope, candidates); + + return elected == null ? (LayeredOutcomeKind.Ambiguous, null) : (LayeredOutcomeKind.Bound, elected); + } + + return (LayeredOutcomeKind.NotBound, null); + } + + internal IEnumerable EnumerateGlobalScopes() + { + yield return this.RootNamespace; + + foreach (var globalNamespace in this.globalNamespaces) + { + yield return globalNamespace; + } + } + + /// + /// Narrows to the directly-owned ones when every other candidate is + /// only inherited into — an owned feature shadows a same-named inherited + /// one even when the redefinition is IMPLIED and absent from the XMI (SysML v2 spec, Clause 7.17.2). + /// Applied at resolve time so a redefined feature stays nameable from its redefining declaration. + /// + /// The namespace whose index produced . + /// The candidates bound to the name being resolved. + /// The owned candidates when the preference applies, otherwise . + internal static List PreferDirectlyOwnedOverInherited(INamespace scope, List candidates) + { + if (scope is not IType scopeAsType) + { + return candidates; + } + + var supertypes = scopeAsType.AllSupertypes(); + var owned = candidates.Where(candidate => ContainmentPaths.IsDirectlyOwnedBy(scope, candidate)).ToList(); + + if (owned.Count == 0 || owned.Count == candidates.Count) + { + return candidates; + } + + var inheritedOnly = candidates + .Where(candidate => !owned.Contains(candidate)) + .All(candidate => candidate is IFeature { owningType: { } declaringType } && supertypes.Contains(declaringType)); + + return inheritedOnly ? owned : candidates; + } + + /// + /// Elects the single redefinition leaf among same-name peers: directly-owned members win over + /// inherited homonyms, then every feature transitively redefined by another candidate is dropped. + /// Returns when more than one leaf survives — a reader cannot pick one, so + /// no spelling relying on this scope is safe. + /// + /// The scope the candidates were found in. + /// The distinct candidate elements. + /// The elected element, or . + private static IElement ElectRedefinitionLeaf(INamespace scope, List candidates) + { + if (candidates.Count == 1) + { + return candidates[0]; + } + + candidates = PreferDirectlyOwnedOverInherited(scope, candidates); + + if (candidates.Count == 1) + { + return candidates[0]; + } + + var shadowed = new HashSet(); + + foreach (var candidate in candidates.OfType()) + { + foreach (var redefined in candidate.AllRedefinedFeatures().Where(redefined => !ReferenceEquals(redefined, candidate))) + { + shadowed.Add(redefined); + } + } + + IElement onlyLeaf = null; + var leafCount = 0; + + foreach (var element in candidates.Where(element => element is not IFeature feature || !shadowed.Contains(feature))) + { + leafCount++; + + if (leafCount > 1) + { + return null; + } + + onlyLeaf = element; + } + + return leafCount == 1 ? onlyLeaf : null; + } + + private static bool IsExternallyVisible(ScopeBinding binding) + { + return binding.Membership is { Visibility: VisibilityKind.Public } + && (binding.Layer != BindingLayer.Imported || binding.ViaPublicImport); + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/NamespaceBindingIndexBuilder.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/NamespaceBindingIndexBuilder.cs new file mode 100644 index 000000000..c3cea15ec --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/NamespaceBindingIndexBuilder.cs @@ -0,0 +1,481 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using SysML2.NET.Core.POCO.Core.Types; + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Core.Root.Namespaces; + using SysML2.NET.Semantics.Implied; + + /// + /// Builds the immutable for a root Namespace, walking containment, + /// imports and inheritance once and recording every binding each scope offers. + /// + internal sealed class NamespaceBindingIndexBuilder + { + /// + /// Reverse index of alias bindings: scope to (aliased element to alias names). + /// + private readonly Dictionary>> aliasIndex = new(); + + /// + /// Reverse index: owning Namespace to the Namespaces re-exporting it through a public import. + /// + private readonly Dictionary> directFacadeIndex = new(); + + /// + /// The other root Namespaces forming the global Namespace (KerML §8.2.3.5.2). + /// + private readonly List globalNamespaces; + + /// + /// The layered binding tables, one per indexed scope. + /// + private readonly Dictionary layeredBindings = new(); + + /// + /// Maps implied generals onto the graph being written. + /// + private readonly ResolutionGraph resolutionGraph; + + /// + /// The root Namespace being indexed. + /// + private readonly INamespace rootNamespace; + + /// + /// Whether the import whose contributions are being recorded is public. + /// + private bool currentImportIsPublic; + + /// + /// The layer the entries currently being recorded belong to. + /// + private BindingLayer currentRecordingLayer; + + /// + /// The table receiving bindings for the scope currently being indexed. + /// + private ScopeBindingTable currentScopeBindings; + + /// + /// Initializes a new instance of the class. + /// + /// The root Namespace to index. + /// The other root Namespaces forming the global Namespace. + /// The provider supplying implied Relationships. + internal NamespaceBindingIndexBuilder(INamespace rootNamespace, List globalNamespaces, IImpliedRelationshipProvider impliedRelationshipProvider) + { + this.rootNamespace = rootNamespace; + this.globalNamespaces = globalNamespaces; + this.resolutionGraph = new ResolutionGraph(rootNamespace, globalNamespaces, impliedRelationshipProvider); + } + + /// + /// Records the facade edges eagerly, then returns an index that builds each scope's bindings on + /// first probe through . + /// + /// The lazily-populated index. + /// + /// Per-scope bindings are independent of every other scope's, so they can be built on demand — and + /// resolution only ever probes the scopes on reference-site chains and candidate descents, a tiny + /// fraction of the reachable graph. The facade index is the one cross-scope product: an edge is + /// discovered on the IMPORTING namespace but queried by the imported one, so it cannot be found + /// on demand and is recorded up front from raw containment. + /// + internal NamespaceBindingIndex Build() + { + this.BuildFacadeIndex(); + + return new NamespaceBindingIndex(this.rootNamespace, this.globalNamespaces, this, this.layeredBindings, this.directFacadeIndex, this.aliasIndex, this.resolutionGraph); + } + + /// + /// Builds the simple-name index of one scope, recording its layered bindings and aliases as a side + /// effect. + /// + /// The to index. + /// The simple-name → member-set lookup for . + internal IReadOnlyDictionary> BuildScopeIndex(INamespace scope) + { + var index = new Dictionary>(StringComparer.Ordinal); + var discardedQueue = new Queue<(INamespace Scope, bool IsGlobal)>(); + var isGlobal = this.IsGlobalScope(scope); + + this.currentScopeBindings = new ScopeBindingTable(); + this.layeredBindings[scope] = this.currentScopeBindings; + + this.BuildOwnedAndImportedEntries(scope, index, discardedQueue, isGlobal); + + if (scope is IType type) + { + this.currentRecordingLayer = BindingLayer.Inherited; + this.BuildInheritedEntries(type, index, discardedQueue, isGlobal); + } + + this.currentScopeBindings = null; + + return index; + } + + /// + /// Records every facade edge — a public re-exporting its imported + /// namespace — by walking raw containment from the root and global Namespaces. + /// + /// + /// The walk reads only the OwnedRelationship / OwnedRelatedElement storage lists; + /// derived properties are never touched, which keeps this pass proportional to element count + /// rather than to inheritance depth. + /// + private void BuildFacadeIndex() + { + var pending = new Stack(); + var visited = new HashSet(); + + foreach (var seed in this.globalNamespaces.Prepend(this.rootNamespace).Where(seed => seed != null)) + { + pending.Push(seed); + } + + while (pending.Count != 0) + { + var element = pending.Pop(); + + if (!visited.Add(element)) + { + continue; + } + + foreach (var relationship in element.OwnedRelationship) + { + if (element is INamespace importOwner + && relationship is INamespaceImport { Visibility: VisibilityKind.Public, ImportedNamespace: { } imported }) + { + this.RecordDirectFacade(imported, importOwner); + } + + foreach (var ownedElement in relationship.OwnedRelatedElement) + { + pending.Push(ownedElement); + } + } + } + } + + /// + /// Asserts whether is reached through the global Namespace: its + /// containment root is not the root Namespace being serialized (KerML §8.2.3.5.2). + /// + /// The scope about to be indexed. + /// when only VISIBLE members of may be indexed. + private bool IsGlobalScope(INamespace scope) + { + var visited = new HashSet(); + IElement current = scope; + + while (current.owningNamespace != null && visited.Add(current)) + { + current = current.owningNamespace; + } + + return !ReferenceEquals(current, this.rootNamespace); + } + + /// + /// Indexes, for lookup only, the members an implied general contributes. + /// + /// The Type that reaches the general through an implied Specialization. + /// The Type reached through an implied Specialization. + /// The destination index. + /// Whether the owning scope is reached through the global namespace. + /// + /// Filtered through Type::removeRedefinedFeatures: KerML §8.3.3.1.10 derives + /// inheritedMemberships as removeRedefinedFeatures(inheritableMemberships(…)). Only this + /// path needs the filter, since an implied Specialization bypasses inheritedMembership. + /// + private void AddImpliedLookupEntries(IType inheritingType, IType impliedGeneral, Dictionary> index, bool isGlobal) + { + if (impliedGeneral == null) + { + return; + } + + // An implied general has no authored relationship, so its private members stay hidden. + var contributed = new List(impliedGeneral.ownedMembership + .Where(ownedMember => ownedMember.Visibility != VisibilityKind.Private)); + + contributed.AddRange(ImportExpansion.QueryInheritedMemberships(impliedGeneral)); + + contributed = inheritingType.RemoveRedefinedFeatures(contributed); + + foreach (var member in contributed.Where(member => ImportExpansion.PassesVisibilityFilter(member, isGlobal))) + { + this.AddLookupOnlyEntry(index, member); + } + } + + /// + /// Adds to under + /// when the name is non-blank. + /// + /// The destination index. + /// The simple name to use as the index key. + /// The element to record. + private static void AddIndexEntry(Dictionary> index, string simpleName, IElement element) + { + if (string.IsNullOrWhiteSpace(simpleName) || element == null) + { + return; + } + + if (!index.TryGetValue(simpleName, out var bucket)) + { + bucket = []; + index[simpleName] = bucket; + } + + bucket.Add(element); + } + + /// + /// Indexes a Membership for name lookup WITHOUT extending the namespace traversal. + /// + /// The destination index. + /// The Membership to index. + /// + /// The counterpart of , minus its pending enqueue. Used for + /// members reached through an IMPLIED Specialization: they must be resolvable by name, but the + /// library Types they come from must not be pulled into the walk that produces output. + /// + private void AddLookupOnlyEntry(Dictionary> index, IMembership membership) + { + if (membership is not { MemberElement: { } target }) + { + return; + } + + var (shortName, longName) = SegmentNaming.QueryMembershipNames(membership, target); + + AddIndexEntry(index, shortName, target); + AddIndexEntry(index, longName, target); + this.RecordLayeredBinding(membership, target); + } + + /// + /// Indexes 's member element under both lexical forms — the + /// membership's explicit name overrides when present, else the element's own names — and enqueues + /// the element when it is itself a namespace. + /// + /// The destination index. + /// The membership whose target is indexed. + /// Queue of namespaces yet to be indexed. + /// Whether the owning scope is reached through the global namespace. + private void AddMembershipEntry(Dictionary> index, IMembership membership, Queue<(INamespace Scope, bool IsGlobal)> pending, bool isGlobal) + { + if (membership is not { MemberElement: { } target }) + { + return; + } + + var (shortName, longName) = SegmentNaming.QueryMembershipNames(membership, target); + + AddIndexEntry(index, shortName, target); + AddIndexEntry(index, longName, target); + this.RecordLayeredBinding(membership, target); + + if (target is INamespace targetAsNamespace) + { + pending.Enqueue((targetAsNamespace, isGlobal)); + } + } + + /// + /// Indexes the entries inherited from 's transitive supertypes; namespace + /// supertypes are enqueued as scopes in their own right. Delegates to + /// Type::inheritedMembership (KerML §8.3.3.1.10) rather than re-deriving it. + /// + /// The type whose inherited memberships are indexed. + /// The destination index. + /// Queue of namespaces yet to be indexed. + /// Whether the owning scope is reached through the global namespace. + private void BuildInheritedEntries(IType type, Dictionary> index, Queue<(INamespace Scope, bool IsGlobal)> pending, bool isGlobal) + { + var inheritableSupertypes = type.AllSupertypes() + .Where(candidate => !ReferenceEquals(candidate, type)) + .ToList(); + + foreach (var supertypeAsNamespace in inheritableSupertypes.OfType()) + { + pending.Enqueue((supertypeAsNamespace, isGlobal)); + } + + var inheritedMemberships = ImportExpansion.QueryInheritedMemberships(type); + + foreach (var inheritedMember in inheritedMemberships + .Where(inheritedMember => ImportExpansion.PassesVisibilityFilter(inheritedMember, isGlobal))) + { + this.AddMembershipEntry(index, inheritedMember, pending, isGlobal); + } + + foreach (var impliedGeneral in this.resolutionGraph.QueryImpliedGeneralClosure(type, inheritableSupertypes)) + { + pending.Enqueue((impliedGeneral, isGlobal)); + + this.AddImpliedLookupEntries(type, impliedGeneral, index, isGlobal); + } + } + + /// + /// Populates with 's owned memberships and + /// imports; imported namespaces are enqueued for their own indexing. When + /// is set only VISIBLE entries are admitted (KerML §8.2.3.5.2). + /// + /// The namespace whose entries are populated. + /// The destination index. + /// Queue of namespaces yet to be indexed. + /// Whether the scope is reached through the global namespace. + private void BuildOwnedAndImportedEntries(INamespace scope, Dictionary> index, Queue<(INamespace Scope, bool IsGlobal)> pending, bool isGlobal) + { + var ownedMemberships = new List(); + this.currentRecordingLayer = BindingLayer.Owned; + + foreach (var ownedMember in scope.ownedMembership.Where(ownedMember => ImportExpansion.PassesVisibilityFilter(ownedMember, isGlobal))) + { + this.AddMembershipEntry(index, ownedMember, pending, isGlobal); + this.RecordAliasIfDeclared(scope, ownedMember); + ownedMemberships.Add(ownedMember); + } + + var ownedNames = new HashSet(index.Keys, StringComparer.Ordinal); + this.currentRecordingLayer = BindingLayer.Imported; + + foreach (var namespaceImport in scope.ownedImport + .Where(ownedImport => ImportExpansion.PassesVisibilityFilter(ownedImport, isGlobal)) + .OfType() + .Where(namespaceImport => namespaceImport.ImportedNamespace != null)) + { + pending.Enqueue((namespaceImport.ImportedNamespace, isGlobal)); + + // Only a PUBLIC import re-exports what it brings in, so only a public one makes + // the importing namespace a usable facade for it (KerML §8.2.3.5.3). + if (namespaceImport.Visibility == VisibilityKind.Public) + { + this.RecordDirectFacade(namespaceImport.ImportedNamespace, scope); + } + } + + foreach (var (ownedImport, importedMember) in ImportExpansion.QueryImportedContributions(scope, isGlobal, isGlobal, ownedNames, ownedMemberships)) + { + this.currentImportIsPublic = ownedImport.Visibility == VisibilityKind.Public; + + this.AddMembershipEntry(index, importedMember, pending, isGlobal); + this.RecordAliasIfDeclared(scope, importedMember); + + if (ReferenceEquals(importedMember, (ownedImport as IMembershipImport)?.ImportedMembership) + && importedMember?.MemberElement?.owningNamespace is { } memberOwner) + { + pending.Enqueue((memberOwner, isGlobal)); + } + } + } + + /// + /// Records an alias X for Y; binding — a membership whose explicit + /// / differs from + /// the member element's own names. + /// + /// The declaring the membership. + /// The candidate alias ; may be . + private void RecordAliasIfDeclared(INamespace scope, IMembership membership) + { + if (membership is not { MemberElement: { } target }) + { + return; + } + + var aliasNames = new[] { membership.MemberName, membership.MemberShortName } + .Where(aliasName => !string.IsNullOrWhiteSpace(aliasName) + && !string.Equals(aliasName, target.name, StringComparison.Ordinal) + && !string.Equals(aliasName, target.shortName, StringComparison.Ordinal)) + .ToList(); + + if (aliasNames.Count == 0) + { + return; + } + + if (!this.aliasIndex.TryGetValue(scope, out var scopeAliases)) + { + scopeAliases = []; + this.aliasIndex[scope] = scopeAliases; + } + + if (!scopeAliases.TryGetValue(target, out var existingAliases)) + { + existingAliases = []; + scopeAliases[target] = existingAliases; + } + + existingAliases.AddRange(aliasNames.Where(aliasName => !existingAliases.Contains(aliasName))); + } + + /// + /// Records as a direct (single-hop) re-exporter of + /// . + /// + /// The namespace being directly imported. + /// The namespace importing it. + private void RecordDirectFacade(INamespace canonicalOwner, INamespace facade) + { + if (!this.directFacadeIndex.TryGetValue(canonicalOwner, out var facades)) + { + facades = []; + this.directFacadeIndex[canonicalOwner] = facades; + } + + facades.Add(facade); + } + + /// + /// Records one binding with the ambient provenance; called wherever the flat index gains an entry. + /// + /// The Membership introducing the binding. + /// The bound element. + private void RecordLayeredBinding(IMembership membership, IElement target) + { + if (this.currentScopeBindings == null) + { + return; + } + + var (shortName, longName) = SegmentNaming.QueryMembershipNames(membership, target); + var binding = new ScopeBinding(membership, target, this.currentRecordingLayer, this.currentImportIsPublic); + + this.currentScopeBindings.Add(shortName, binding); + this.currentScopeBindings.Add(longName, binding); + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/ReferenceSite.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/ReferenceSite.cs new file mode 100644 index 000000000..533be9ddf --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/ReferenceSite.cs @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using SysML2.NET.Core.POCO.Core.Features; + using SysML2.NET.Core.POCO.Root.Elements; + + /// + /// What stays fixed while one reference is resolved: the scopes to probe and the exclusions that + /// apply to every probe of it. + /// + internal sealed class ReferenceSite + { + /// + /// Initializes a new instance of the class. + /// + /// The POCO bearing the reference. + /// The scopes to probe, innermost first, with the match floor. + /// Feature to exclude from every bucket, or . + /// The binding the reference itself is, or . + internal ReferenceSite(IElement sourcePoco, SourceScopeChain chain, IFeature localRedefiner, SelfBinding selfBinding) + { + this.SourcePoco = sourcePoco; + this.Chain = chain; + this.LocalRedefiner = localRedefiner; + this.SelfBinding = selfBinding; + } + + /// + /// Gets the POCO bearing the reference. + /// + internal IElement SourcePoco { get; } + + /// + /// Gets the scopes to probe, innermost first, with the depth at which a match is admissible. + /// + internal SourceScopeChain Chain { get; } + + /// + /// Gets the Feature excluded from every bucket — the reference's own redefining or + /// referencing Feature, which must not shadow its own target. + /// + internal IFeature LocalRedefiner { get; } + + /// + /// Gets the binding the reference itself is, whose entry does not exist at parse time. + /// + internal SelfBinding SelfBinding { get; } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/ResolutionGraph.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/ResolutionGraph.cs new file mode 100644 index 000000000..1a3345684 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/ResolutionGraph.cs @@ -0,0 +1,189 @@ + // ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using SysML2.NET.Core.POCO.Core.Types; + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Semantics.Implied; + + /// + /// Maps an implied general (KerML §8.4.2) onto the equivalent Element of the graph being written, so a + /// name reachable only through an implied Specialization resolves against an indexed Namespace. + /// + internal sealed class ResolutionGraph + { + /// + /// The other root Namespaces forming the global Namespace. + /// + private readonly List globalNamespaces; + + /// + /// Supplies the implied Relationships an exported model omits. + /// + private readonly IImpliedRelationshipProvider impliedRelationshipProvider; + + /// + /// The root Namespace being written. + /// + private readonly INamespace rootNamespace; + + /// + /// Elements of the resolution graph keyed by Id, built on first use. + /// + private Dictionary resolutionGraphElementsById; + + /// + /// Initializes a new instance of the class. + /// + /// The root Namespace being written. + /// The other root Namespaces forming the global Namespace. + /// The provider supplying implied Relationships. + internal ResolutionGraph(INamespace rootNamespace, List globalNamespaces, IImpliedRelationshipProvider impliedRelationshipProvider) + { + this.rootNamespace = rootNamespace; + this.globalNamespaces = globalNamespaces; + this.impliedRelationshipProvider = impliedRelationshipProvider; + } + + /// + /// Translates a Type produced by the implied-relationship layer into this cache's OWN object graph. + /// + /// The general of an implied Specialization, possibly from a foreign graph. + /// The same-Id Type of the resolution graph, or null when the graph does not carry it. + /// + /// The implied layer may be wired against a SEPARATE library load — a full, model-independent one — + /// so the generals it returns can be different POCO instances than the ones this cache resolves + /// against, even for the same library element (same Id). Indexing a foreign instance is + /// worse than useless: it can never equal a resolution target by reference, so it answers + /// Shadowed and STOPS the outward walk that would otherwise have found the local instance. + /// Translating by Id keeps reference equality authoritative everywhere else. A general the + /// resolution graph does not carry is dropped: its members can never be targets here. + /// + internal IType TranslateToResolutionGraph(IType impliedGeneral) + { + if (impliedGeneral == null) + { + return null; + } + + this.resolutionGraphElementsById ??= this.BuildResolutionGraphIndex(); + + if (this.resolutionGraphElementsById.TryGetValue(impliedGeneral.Id, out var local)) + { + return local as IType; + } + + return this.IsInResolutionGraph(impliedGeneral) ? impliedGeneral : null; + } + + /// + /// Builds the by-Id index of every Element reachable from the global namespaces. + /// + /// The index. + private Dictionary BuildResolutionGraphIndex() + { + var elementsById = new Dictionary(); + var pendingElements = new Queue(); + + foreach (var globalNamespace in this.globalNamespaces) + { + pendingElements.Enqueue(globalNamespace); + } + + while (pendingElements.Count > 0) + { + var current = pendingElements.Dequeue(); + + if (!elementsById.TryAdd(current.Id, current)) + { + continue; + } + + foreach (var owned in current.OwnedRelationship.SelectMany(relationship => relationship.OwnedRelatedElement)) + { + pendingElements.Enqueue(owned); + } + } + + return elementsById; + } + + /// + /// Asserts whether an Element belongs to this cache's own graph, by walking its owners to a known root. + /// + /// The Element to test. + /// True when an owner chain reaches the root or a global namespace. + private bool IsInResolutionGraph(IElement element) + { + for (var current = element; current != null; current = current.owner) + { + if (ReferenceEquals(current, this.rootNamespace) || this.globalNamespaces.Contains(current)) + { + return true; + } + } + + return false; + } + + /// + /// Returns every Type reachable from a Type or its declared supertypes through implied + /// Specializations, transitively. + /// + /// The Type whose implied generals are collected. + /// The declared supertypes, which carry implied Specializations of their own. + /// The implied generals, without duplicates. + internal List QueryImpliedGeneralClosure(IType type, List declaredSupertypes) + { + var visited = new HashSet(); + var pendingTypes = new Queue(); + + pendingTypes.Enqueue(type); + + foreach (var declaredSupertype in declaredSupertypes) + { + pendingTypes.Enqueue(declaredSupertype); + } + + var impliedGenerals = new List(); + + while (pendingTypes.Count > 0) + { + var current = pendingTypes.Dequeue(); + + foreach (var general in this.impliedRelationshipProvider.GetImpliedSpecializations(current) + .Select(specialization => this.TranslateToResolutionGraph(specialization.General)) + .Where(general => general != null && visited.Add(general))) + { + impliedGenerals.Add(general); + pendingTypes.Enqueue(general); + } + } + + return impliedGenerals; + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/ScopeBinding.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/ScopeBinding.cs new file mode 100644 index 000000000..ec13416b9 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/ScopeBinding.cs @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + + /// + /// One name binding with full provenance. + /// + internal readonly struct ScopeBinding + { + /// + /// Initializes a new instance of the struct. + /// + /// The Membership that introduces the binding. + /// The bound element. + /// The layer the binding entered through. + /// Whether an imported binding arrived through a PUBLIC import. + internal ScopeBinding(IMembership membership, IElement element, BindingLayer layer, bool viaPublicImport) + { + this.Membership = membership; + this.Element = element; + this.Layer = layer; + this.ViaPublicImport = viaPublicImport; + } + + /// Gets the Membership that introduces the binding. + internal IMembership Membership { get; } + + /// Gets the bound element. + internal IElement Element { get; } + + /// Gets the layer the binding entered through. + internal BindingLayer Layer { get; } + + /// Gets a value indicating whether an imported binding arrived through a PUBLIC import. + internal bool ViaPublicImport { get; } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/ScopeBindingTable.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/ScopeBindingTable.cs new file mode 100644 index 000000000..c837ce9ee --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/ScopeBindingTable.cs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// The per-scope binding table: raw simple name → bindings with provenance. + /// + internal sealed class ScopeBindingTable + { + /// Gets the bindings keyed by raw simple name. + internal Dictionary> ByName { get; } = new(StringComparer.Ordinal); + + /// + /// Adds a binding under , ignoring blank names and exact duplicates. + /// + /// The raw simple name the binding answers to. + /// The binding. + internal void Add(string rawName, ScopeBinding binding) + { + if (string.IsNullOrWhiteSpace(rawName)) + { + return; + } + + if (!this.ByName.TryGetValue(rawName, out var bindings)) + { + this.ByName[rawName] = bindings = []; + } + + if (!bindings.Any(existing => ReferenceEquals(existing.Membership, binding.Membership) + && ReferenceEquals(existing.Element, binding.Element) + && existing.Layer == binding.Layer)) + { + bindings.Add(binding); + } + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/SegmentNaming.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/SegmentNaming.cs new file mode 100644 index 000000000..dc3a79f54 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/SegmentNaming.cs @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System.Linq; + + using SysML2.NET.Core.POCO.Root.Elements; + using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.Extensions; + + /// + /// The lexical form of one qualified-name segment: which of an Element's names a segment prefers, and + /// how that name is escaped when it is not a basic NAME (KerML §8.2.2.3). + /// + internal static class SegmentNaming + { + /// + /// Escapes per KEBNF: unchanged when it is a basic name, otherwise + /// quoted as an unrestricted name. + /// + /// The raw name; must be non-blank. + /// The escaped form. + internal static string Escape(string rawName) + { + return rawName.QueryIsValidBasicName() ? rawName : rawName.ToUnrestrictedName(); + } + + /// + /// Returns the first non-blank candidate, or when every one is blank. + /// + /// The candidate forms, most preferred first. + /// The first non-blank candidate, or . + internal static string FirstNonBlank(params string[] candidates) + { + return candidates.FirstOrDefault(candidate => !string.IsNullOrWhiteSpace(candidate)); + } + + /// + /// Returns the two lexical forms a membership binds: the membership's explicit name overrides when + /// present, else the member element's own names. + /// + /// The membership doing the binding. + /// The membership's member element. + /// The short and long lexical forms; either may be . + internal static (string ShortName, string LongName) QueryMembershipNames(IMembership membership, IElement target) + { + return (!string.IsNullOrWhiteSpace(membership.MemberShortName) ? membership.MemberShortName : target.shortName, + !string.IsNullOrWhiteSpace(membership.MemberName) ? membership.MemberName : target.name); + } + + /// + /// Returns the element's shortest escaped name segment (shortName preferred), or + /// when neither lexical form is available. + /// + /// The element to name; must be non-null. + /// The escaped segment, or . + internal static string QueryPreferredEscapedSegment(IElement element) + { + var preferred = QueryPreferredRawName(element); + + return string.IsNullOrWhiteSpace(preferred) ? null : Escape(preferred); + } + + /// + /// Returns the element's preferred raw simple name: shortName when non-blank, otherwise + /// name. May be or blank. + /// + /// The element to name; must be non-null. + /// The preferred raw name. + internal static string QueryPreferredRawName(IElement element) + { + return !string.IsNullOrWhiteSpace(element.shortName) ? element.shortName : element.name; + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/SelfBinding.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/SelfBinding.cs new file mode 100644 index 000000000..50c76722a --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/SelfBinding.cs @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using SysML2.NET.Core.POCO.Root.Namespaces; + + /// + /// The binding a reference site declares itself, which does not exist while that declaration is + /// being written. + /// + internal sealed class SelfBinding + { + /// + /// Initializes a new instance of the class. + /// + /// The scope the membership binds in. + /// The Membership that IS the reference. + internal SelfBinding(INamespace scope, IMembership membership) + { + this.Scope = scope; + this.Membership = membership; + } + + /// + /// Gets the scope the membership binds in. + /// + internal INamespace Scope { get; } + + /// + /// Gets the Membership that IS the reference, and whose binding therefore does not exist yet + /// at parse time. + /// + internal IMembership Membership { get; } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/SimpleNameResolution.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/SimpleNameResolution.cs new file mode 100644 index 000000000..bda445b2c --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/SimpleNameResolution.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + /// + /// Tri-state result of probing one scope for one lexical form. + /// + internal enum SimpleNameResolution + { + /// Name not bound in this scope — keep walking outward. + NotBound, + + /// Name bound uniquely to the target — emit the simple name. + Matched, + + /// Name bound to something else — stop; the qualified form is required. + Shadowed + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/NameResolution/SourceScopeChain.cs b/SysML2.NET.Serializer.TextualNotation/NameResolution/SourceScopeChain.cs new file mode 100644 index 000000000..8d3bbd1b0 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation/NameResolution/SourceScopeChain.cs @@ -0,0 +1,54 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright (C) 2022-2026 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.Serializer.TextualNotation.NameResolution +{ + using System.Collections.Generic; + + using SysML2.NET.Core.POCO.Root.Namespaces; + + /// + /// The scopes a reference probes, innermost first, with the depth below which a match is barred. + /// + internal sealed class SourceScopeChain + { + /// + /// Initializes a new instance of the class. + /// + /// The scopes, innermost first. + /// The index of the innermost scope a match may come from. + internal SourceScopeChain(IReadOnlyList scopes, int matchFloor) + { + this.Scopes = scopes; + this.MatchFloor = matchFloor; + } + + /// + /// Gets the scopes of the chain, innermost first. + /// + internal IReadOnlyList Scopes { get; } + + /// + /// Gets the index of the innermost scope a match may come from; scopes below it are consulted + /// for shadowing only. + /// + internal int MatchFloor { get; } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/AcceptActionUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/AcceptActionUsageTextualNotationBuilder.cs index 1fd1f8b10..cd6da6999 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/AcceptActionUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/AcceptActionUsageTextualNotationBuilder.cs @@ -79,30 +79,18 @@ public static void BuildAcceptParameterPart(SysML2.NET.Core.POCO.Systems.Actions { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildPayloadParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildPayloadParameterMember(elementAsParameterMembership0, writerContext, stringBuilder); } - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { stringBuilder.Append("via "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildNodeParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildNodeParameterMember(elementAsParameterMembership1, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ActionUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ActionUsageTextualNotationBuilder.cs index 39f5cf023..20adcef11 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ActionUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ActionUsageTextualNotationBuilder.cs @@ -148,15 +148,9 @@ public static void BuildAssignmentNodeDeclaration(SysML2.NET.Core.POCO.Systems.A stringBuilder.Append("assign "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildAssignmentTargetMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildAssignmentTargetMember(elementAsParameterMembership0, writerContext, stringBuilder); } if (ownedRelationshipCursor.Current != null) @@ -171,15 +165,9 @@ public static void BuildAssignmentNodeDeclaration(SysML2.NET.Core.POCO.Systems.A } stringBuilder.Append(":= "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildNodeParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildNodeParameterMember(elementAsParameterMembership1, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/BindingConnectorAsUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/BindingConnectorAsUsageTextualNotationBuilder.cs index 0cf7effb4..1bc2e3494 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/BindingConnectorAsUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/BindingConnectorAsUsageTextualNotationBuilder.cs @@ -54,27 +54,15 @@ public static void BuildBindingConnectorAsUsage(SysML2.NET.Core.POCO.Systems.Con stringBuilder.Append("bind "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append("= "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership1, writerContext, stringBuilder); } UsageTextualNotationBuilder.BuildUsageBody(poco, writerContext, stringBuilder); diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ClassifierTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ClassifierTextualNotationBuilder.cs index 1383483fd..30dd194a4 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ClassifierTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ClassifierTextualNotationBuilder.cs @@ -93,24 +93,18 @@ public static void BuildClassifierDeclaration(SysML2.NET.Core.POCO.Core.Classifi ElementTextualNotationBuilder.BuildIdentification(poco, writerContext, stringBuilder); - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildOwnedMultiplicity(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildOwnedMultiplicity(elementAsOwningMembership0, writerContext, stringBuilder); } stringBuilder.Append(' '); } BuildClassifierDeclarationHandCoded(poco, writerContext, stringBuilder); - while (ownedRelationshipCursor.Current != null) + while (ownedRelationshipCursor.Current is (SysML2.NET.Core.POCO.Core.Types.IDisjoining or SysML2.NET.Core.POCO.Core.Types.IUnioning or SysML2.NET.Core.POCO.Core.Types.IIntersecting or SysML2.NET.Core.POCO.Core.Types.IDifferencing)) { var positionBeforeItem0 = ownedRelationshipCursor.Position; TypeTextualNotationBuilder.BuildTypeRelationshipPart(poco, writerContext, stringBuilder); diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/CommentTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/CommentTextualNotationBuilder.cs index c3af16799..e632bd1e5 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/CommentTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/CommentTextualNotationBuilder.cs @@ -44,7 +44,7 @@ public static void BuildComment(SysML2.NET.Core.POCO.Root.Annotations.IComment p { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (!string.IsNullOrWhiteSpace(poco.DeclaredShortName) || !string.IsNullOrWhiteSpace(poco.DeclaredName)) + if (!string.IsNullOrWhiteSpace(poco.DeclaredShortName) || !string.IsNullOrWhiteSpace(poco.DeclaredName) || ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Annotations.IAnnotation) { stringBuilder.Append("comment "); ElementTextualNotationBuilder.BuildIdentification(poco, writerContext, stringBuilder); diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConnectionUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConnectionUsageTextualNotationBuilder.cs index 1994214c6..0192d4e10 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConnectionUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConnectionUsageTextualNotationBuilder.cs @@ -65,27 +65,15 @@ public static void BuildBinaryConnectorPart(SysML2.NET.Core.POCO.Systems.Connect { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append("to "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership1, writerContext, stringBuilder); } } @@ -102,27 +90,15 @@ public static void BuildNaryConnectorPart(SysML2.NET.Core.POCO.Systems.Connectio var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); stringBuilder.Append("("); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append(", "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership1, writerContext, stringBuilder); } while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership endFeatureMembershipGuard && endFeatureMembershipGuard.OwnedRelatedElement.OfType().Any()) diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConnectorTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConnectorTextualNotationBuilder.cs index c3d796dae..217b22abd 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConnectorTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConnectorTextualNotationBuilder.cs @@ -66,27 +66,15 @@ public static void BuildBinaryConnectorDeclaration(SysML2.NET.Core.POCO.Kernel.C var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); BuildBinaryConnectorDeclarationHandCoded(poco, writerContext, stringBuilder); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append("to "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership1, writerContext, stringBuilder); } } @@ -109,27 +97,15 @@ public static void BuildNaryConnectorDeclaration(SysML2.NET.Core.POCO.Kernel.Con } stringBuilder.Append("("); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append(", "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership1, writerContext, stringBuilder); } while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership endFeatureMembershipGuard && endFeatureMembershipGuard.OwnedRelatedElement.OfType().Any()) diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConstructorExpressionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConstructorExpressionTextualNotationBuilder.cs index c9edc4f96..87a26837e 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConstructorExpressionTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ConstructorExpressionTextualNotationBuilder.cs @@ -56,15 +56,9 @@ public static void BuildConstructorExpression(SysML2.NET.Core.POCO.Kernel.Expres } } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsReturnParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IReturnParameterMembership elementAsReturnParameterMembership) - { - ReturnParameterMembershipTextualNotationBuilder.BuildConstructorResultMember(elementAsReturnParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ReturnParameterMembershipTextualNotationBuilder.BuildConstructorResultMember(elementAsReturnParameterMembership0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ElementTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ElementTextualNotationBuilder.cs index bc374dd2f..51c243828 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ElementTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ElementTextualNotationBuilder.cs @@ -140,12 +140,12 @@ public static void BuildDefinitionElement(SysML2.NET.Core.POCO.Root.Elements.IEl case SysML2.NET.Core.POCO.Systems.Attributes.IAttributeDefinition pocoAttributeDefinition: AttributeDefinitionTextualNotationBuilder.BuildAttributeDefinition(pocoAttributeDefinition, writerContext, stringBuilder); break; + case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceDefinition pocoOccurrenceDefinitionOccurrenceDefinition when pocoOccurrenceDefinitionOccurrenceDefinition.IsValidForOccurrenceDefinition(writerContext): + OccurrenceDefinitionTextualNotationBuilder.BuildOccurrenceDefinition(pocoOccurrenceDefinitionOccurrenceDefinition, writerContext, stringBuilder); + break; case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceDefinition pocoOccurrenceDefinitionIndividualDefinition when pocoOccurrenceDefinitionIndividualDefinition.IsIndividual: OccurrenceDefinitionTextualNotationBuilder.BuildIndividualDefinition(pocoOccurrenceDefinitionIndividualDefinition, writerContext, stringBuilder); break; - case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceDefinition pocoOccurrenceDefinition: - OccurrenceDefinitionTextualNotationBuilder.BuildOccurrenceDefinition(pocoOccurrenceDefinition, writerContext, stringBuilder); - break; case SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IDefinition pocoDefinition: DefinitionTextualNotationBuilder.BuildExtendedDefinition(pocoDefinition, writerContext, stringBuilder); break; diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EndFeatureMembershipTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EndFeatureMembershipTextualNotationBuilder.cs index c5cd52cbd..3062476f7 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EndFeatureMembershipTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EndFeatureMembershipTextualNotationBuilder.cs @@ -68,15 +68,9 @@ public static void BuildConnectorEndMember(SysML2.NET.Core.POCO.Core.Features.IE { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsReferenceUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IReferenceUsage elementAsReferenceUsage) - { - ReferenceUsageTextualNotationBuilder.BuildConnectorEnd(elementAsReferenceUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + ReferenceUsageTextualNotationBuilder.BuildConnectorEnd(elementAsReferenceUsage0, writerContext, stringBuilder); } } @@ -92,15 +86,9 @@ public static void BuildInterfaceEndMember(SysML2.NET.Core.POCO.Core.Features.IE { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsPortUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.Ports.IPortUsage elementAsPortUsage) - { - PortUsageTextualNotationBuilder.BuildInterfaceEnd(elementAsPortUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + PortUsageTextualNotationBuilder.BuildInterfaceEnd(elementAsPortUsage0, writerContext, stringBuilder); } } @@ -116,15 +104,9 @@ public static void BuildFlowEndMember(SysML2.NET.Core.POCO.Core.Features.IEndFea { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsFlowEnd0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Kernel.Interactions.IFlowEnd elementAsFlowEnd) - { - FlowEndTextualNotationBuilder.BuildFlowEnd(elementAsFlowEnd, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + FlowEndTextualNotationBuilder.BuildFlowEnd(elementAsFlowEnd0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EnumerationDefinitionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EnumerationDefinitionTextualNotationBuilder.cs index 8074ed86e..e5539a619 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EnumerationDefinitionTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EnumerationDefinitionTextualNotationBuilder.cs @@ -66,8 +66,7 @@ public static void BuildEnumerationBody(SysML2.NET.Core.POCO.Systems.Enumeration ownedRelationshipCursor.Move(); break; default: - ownedRelationshipCursor.Move(); - break; + throw new System.InvalidOperationException($"The textual notation writer cannot place the current element ({ownedRelationshipCursor.Current?.GetType().Name}) while building 'EnumerationDefinition' — no alternative of the rule claims it, so it would be silently dropped."); } ownedRelationshipCursor.AssertAdvancedSince(positionBeforeItem0, "EnumerationBody"); } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureMembershipTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureMembershipTextualNotationBuilder.cs index c7c83724a..b0621295d 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureMembershipTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureMembershipTextualNotationBuilder.cs @@ -145,15 +145,9 @@ public static void BuildSourceSuccessionMember(SysML2.NET.Core.POCO.Core.Types.I var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); stringBuilder.Append("then "); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsSuccessionAsUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.Connections.ISuccessionAsUsage elementAsSuccessionAsUsage) - { - SuccessionAsUsageTextualNotationBuilder.BuildSourceSuccession(elementAsSuccessionAsUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + SuccessionAsUsageTextualNotationBuilder.BuildSourceSuccession(elementAsSuccessionAsUsage0, writerContext, stringBuilder); } } @@ -243,15 +237,9 @@ public static void BuildFlowFeatureMember(SysML2.NET.Core.POCO.Core.Types.IFeatu { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsReferenceUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IReferenceUsage elementAsReferenceUsage) - { - ReferenceUsageTextualNotationBuilder.BuildFlowFeature(elementAsReferenceUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + ReferenceUsageTextualNotationBuilder.BuildFlowFeature(elementAsReferenceUsage0, writerContext, stringBuilder); } } @@ -355,15 +343,9 @@ public static void BuildGuardedSuccessionMember(SysML2.NET.Core.POCO.Core.Types. var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); MembershipTextualNotationBuilder.BuildMemberPrefix(poco, writerContext, stringBuilder); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => (candidate.OwnedRelationship.OfType().Any() && candidate.OwnedRelationship.OfType().Any() && candidate.OwnedRelationship.OfType().Any()), out var elementAsTransitionUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.States.ITransitionUsage elementAsTransitionUsage) - { - TransitionUsageTextualNotationBuilder.BuildGuardedSuccession(elementAsTransitionUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + TransitionUsageTextualNotationBuilder.BuildGuardedSuccession(elementAsTransitionUsage0, writerContext, stringBuilder); } } @@ -420,15 +402,9 @@ public static void BuildTransitionUsageMember(SysML2.NET.Core.POCO.Core.Types.IF var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); MembershipTextualNotationBuilder.BuildMemberPrefix(poco, writerContext, stringBuilder); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => (candidate.OwnedRelationship.OfType().Any() && candidate.OwnedRelationship.OfType().Any() && candidate.OwnedRelationship.OfType().Any()), out var elementAsTransitionUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.States.ITransitionUsage elementAsTransitionUsage) - { - TransitionUsageTextualNotationBuilder.BuildTransitionUsage(elementAsTransitionUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + TransitionUsageTextualNotationBuilder.BuildTransitionUsage(elementAsTransitionUsage0, writerContext, stringBuilder); } } @@ -445,15 +421,9 @@ public static void BuildTargetTransitionUsageMember(SysML2.NET.Core.POCO.Core.Ty var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); MembershipTextualNotationBuilder.BuildMemberPrefix(poco, writerContext, stringBuilder); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => (candidate.OwnedRelationship.OfType().Any() && candidate.OwnedRelationship.OfType().Any()), out var elementAsTransitionUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.States.ITransitionUsage elementAsTransitionUsage) - { - TransitionUsageTextualNotationBuilder.BuildTargetTransitionUsage(elementAsTransitionUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + TransitionUsageTextualNotationBuilder.BuildTargetTransitionUsage(elementAsTransitionUsage0, writerContext, stringBuilder); } } @@ -512,15 +482,9 @@ public static void BuildOwnedExpressionReferenceMember(SysML2.NET.Core.POCO.Core { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsFeatureReferenceExpression0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Expressions.IFeatureReferenceExpression elementAsFeatureReferenceExpression) - { - FeatureReferenceExpressionTextualNotationBuilder.BuildOwnedExpressionReference(elementAsFeatureReferenceExpression, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + FeatureReferenceExpressionTextualNotationBuilder.BuildOwnedExpressionReference(elementAsFeatureReferenceExpression0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureReferenceExpressionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureReferenceExpressionTextualNotationBuilder.cs index e6715fb2c..e27f0c8bb 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureReferenceExpressionTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureReferenceExpressionTextualNotationBuilder.cs @@ -127,15 +127,9 @@ public static void BuildFeatureReferenceExpression(SysML2.NET.Core.POCO.Kernel.E } } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsReturnParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IReturnParameterMembership elementAsReturnParameterMembership) - { - ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs index dd752e058..ad7952f84 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs @@ -44,15 +44,9 @@ public static void BuildValuePart(SysML2.NET.Core.POCO.Core.Features.IFeature po { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsFeatureValue0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue elementAsFeatureValue) - { - FeatureValueTextualNotationBuilder.BuildFeatureValue(elementAsFeatureValue, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + FeatureValueTextualNotationBuilder.BuildFeatureValue(elementAsFeatureValue0, writerContext, stringBuilder); } } @@ -377,15 +371,9 @@ public static void BuildOwnedCrossMultiplicity(SysML2.NET.Core.POCO.Core.Feature { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildOwnedMultiplicity(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildOwnedMultiplicity(elementAsOwningMembership0, writerContext, stringBuilder); } } @@ -467,15 +455,9 @@ public static void BuildTriggerValuePart(SysML2.NET.Core.POCO.Core.Features.IFea { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsFeatureValue0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue elementAsFeatureValue) - { - FeatureValueTextualNotationBuilder.BuildTriggerFeatureValue(elementAsFeatureValue, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + FeatureValueTextualNotationBuilder.BuildTriggerFeatureValue(elementAsFeatureValue0, writerContext, stringBuilder); } } @@ -515,15 +497,9 @@ public static void BuildArgumentExpression(SysML2.NET.Core.POCO.Core.Features.IF { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsFeatureValue0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue elementAsFeatureValue) - { - FeatureValueTextualNotationBuilder.BuildArgumentExpressionValue(elementAsFeatureValue, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + FeatureValueTextualNotationBuilder.BuildArgumentExpressionValue(elementAsFeatureValue0, writerContext, stringBuilder); } } @@ -542,29 +518,29 @@ public static void BuildFeatureElement(SysML2.NET.Core.POCO.Core.Features.IFeatu case SysML2.NET.Core.POCO.Kernel.Interactions.ISuccessionFlow pocoSuccessionFlow: SuccessionFlowTextualNotationBuilder.BuildSuccessionFlow(pocoSuccessionFlow, writerContext, stringBuilder); break; - case SysML2.NET.Core.POCO.Kernel.Functions.IInvariant pocoInvariantInvariant when (pocoInvariantInvariant.DeclaredShortName != null || pocoInvariantInvariant.DeclaredName != null): - InvariantTextualNotationBuilder.BuildInvariant(pocoInvariantInvariant, writerContext, stringBuilder); + case SysML2.NET.Core.POCO.Kernel.Functions.IInvariant pocoInvariant: + InvariantTextualNotationBuilder.BuildInvariant(pocoInvariant, writerContext, stringBuilder); break; case SysML2.NET.Core.POCO.Kernel.Interactions.IFlow pocoFlow: FlowTextualNotationBuilder.BuildFlow(pocoFlow, writerContext, stringBuilder); break; - case SysML2.NET.Core.POCO.Kernel.Functions.IBooleanExpression pocoBooleanExpressionBooleanExpression when (pocoBooleanExpressionBooleanExpression.DeclaredShortName != null || pocoBooleanExpressionBooleanExpression.DeclaredName != null): - BooleanExpressionTextualNotationBuilder.BuildBooleanExpression(pocoBooleanExpressionBooleanExpression, writerContext, stringBuilder); + case SysML2.NET.Core.POCO.Kernel.Functions.IBooleanExpression pocoBooleanExpression: + BooleanExpressionTextualNotationBuilder.BuildBooleanExpression(pocoBooleanExpression, writerContext, stringBuilder); break; - case SysML2.NET.Core.POCO.Kernel.Connectors.IBindingConnector pocoBindingConnectorBindingConnector when (pocoBindingConnectorBindingConnector.DeclaredShortName != null || pocoBindingConnectorBindingConnector.DeclaredName != null): - BindingConnectorTextualNotationBuilder.BuildBindingConnector(pocoBindingConnectorBindingConnector, writerContext, stringBuilder); + case SysML2.NET.Core.POCO.Kernel.Connectors.IBindingConnector pocoBindingConnector: + BindingConnectorTextualNotationBuilder.BuildBindingConnector(pocoBindingConnector, writerContext, stringBuilder); break; - case SysML2.NET.Core.POCO.Kernel.Connectors.ISuccession pocoSuccessionSuccession when (pocoSuccessionSuccession.DeclaredShortName != null || pocoSuccessionSuccession.DeclaredName != null): - SuccessionTextualNotationBuilder.BuildSuccession(pocoSuccessionSuccession, writerContext, stringBuilder); + case SysML2.NET.Core.POCO.Kernel.Connectors.ISuccession pocoSuccession: + SuccessionTextualNotationBuilder.BuildSuccession(pocoSuccession, writerContext, stringBuilder); break; - case SysML2.NET.Core.POCO.Kernel.Functions.IExpression pocoExpressionExpression when (pocoExpressionExpression.DeclaredShortName != null || pocoExpressionExpression.DeclaredName != null): - ExpressionTextualNotationBuilder.BuildExpression(pocoExpressionExpression, writerContext, stringBuilder); + case SysML2.NET.Core.POCO.Kernel.Functions.IExpression pocoExpression: + ExpressionTextualNotationBuilder.BuildExpression(pocoExpression, writerContext, stringBuilder); break; - case SysML2.NET.Core.POCO.Kernel.Connectors.IConnector pocoConnectorConnector when (pocoConnectorConnector.DeclaredShortName != null || pocoConnectorConnector.DeclaredName != null): - ConnectorTextualNotationBuilder.BuildConnector(pocoConnectorConnector, writerContext, stringBuilder); + case SysML2.NET.Core.POCO.Kernel.Connectors.IConnector pocoConnector: + ConnectorTextualNotationBuilder.BuildConnector(pocoConnector, writerContext, stringBuilder); break; - case SysML2.NET.Core.POCO.Kernel.Behaviors.IStep pocoStepStep when (pocoStepStep.DeclaredShortName != null || pocoStepStep.DeclaredName != null): - StepTextualNotationBuilder.BuildStep(pocoStepStep, writerContext, stringBuilder); + case SysML2.NET.Core.POCO.Kernel.Behaviors.IStep pocoStep: + StepTextualNotationBuilder.BuildStep(pocoStep, writerContext, stringBuilder); break; default: BuildFeature(poco, writerContext, stringBuilder); diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureValueTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureValueTextualNotationBuilder.cs index e40a6f0c2..4744e113e 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureValueTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureValueTextualNotationBuilder.cs @@ -44,15 +44,9 @@ public static void BuildTriggerFeatureValue(SysML2.NET.Core.POCO.Kernel.FeatureV { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsTriggerInvocationExpression0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.Actions.ITriggerInvocationExpression elementAsTriggerInvocationExpression) - { - TriggerInvocationExpressionTextualNotationBuilder.BuildTriggerExpression(elementAsTriggerInvocationExpression, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + TriggerInvocationExpressionTextualNotationBuilder.BuildTriggerExpression(elementAsTriggerInvocationExpression0, writerContext, stringBuilder); } } @@ -85,15 +79,9 @@ public static void BuildArgumentExpressionValue(SysML2.NET.Core.POCO.Kernel.Feat { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsFeatureReferenceExpression0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Kernel.Expressions.IFeatureReferenceExpression elementAsFeatureReferenceExpression) - { - FeatureReferenceExpressionTextualNotationBuilder.BuildOwnedExpressionReference(elementAsFeatureReferenceExpression, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + FeatureReferenceExpressionTextualNotationBuilder.BuildOwnedExpressionReference(elementAsFeatureReferenceExpression0, writerContext, stringBuilder); } } @@ -157,15 +145,9 @@ public static void BuildSatisfactionFeatureValue(SysML2.NET.Core.POCO.Kernel.Fea { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsFeatureReferenceExpression0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Kernel.Expressions.IFeatureReferenceExpression elementAsFeatureReferenceExpression) - { - FeatureReferenceExpressionTextualNotationBuilder.BuildSatisfactionReferenceExpression(elementAsFeatureReferenceExpression, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + FeatureReferenceExpressionTextualNotationBuilder.BuildSatisfactionReferenceExpression(elementAsFeatureReferenceExpression0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FlowEndTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FlowEndTextualNotationBuilder.cs index 9e4b5b2c4..b136f05df 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FlowEndTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FlowEndTextualNotationBuilder.cs @@ -44,7 +44,7 @@ public static void BuildFlowEnd(SysML2.NET.Core.POCO.Kernel.Interactions.IFlowEn { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting && ownedRelationshipCursor.GetNext(1) is SysML2.NET.Core.POCO.Core.Types.IFeatureMembership) + if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting && ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { if (ownedRelationshipCursor.Current != null) @@ -61,15 +61,9 @@ public static void BuildFlowEnd(SysML2.NET.Core.POCO.Kernel.Interactions.IFlowEn } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Types.IFeatureMembership elementAsFeatureMembership) - { - FeatureMembershipTextualNotationBuilder.BuildFlowFeatureMember(elementAsFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + FeatureMembershipTextualNotationBuilder.BuildFlowFeatureMember(elementAsFeatureMembership0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ForLoopActionUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ForLoopActionUsageTextualNotationBuilder.cs index 6f853911d..958f49aba 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ForLoopActionUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ForLoopActionUsageTextualNotationBuilder.cs @@ -46,38 +46,20 @@ public static void BuildForLoopNode(SysML2.NET.Core.POCO.Systems.Actions.IForLoo ActionUsageTextualNotationBuilder.BuildActionNodePrefix(poco, writerContext, stringBuilder); stringBuilder.Append("for "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Types.IFeatureMembership elementAsFeatureMembership) - { - FeatureMembershipTextualNotationBuilder.BuildForVariableDeclarationMember(elementAsFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + FeatureMembershipTextualNotationBuilder.BuildForVariableDeclarationMember(elementAsFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append("in "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildNodeParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildNodeParameterMember(elementAsParameterMembership1, writerContext, stringBuilder); } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership2)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildActionBodyParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildActionBodyParameterMember(elementAsParameterMembership2, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/IfActionUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/IfActionUsageTextualNotationBuilder.cs index b20c7f5ea..42a2ad2e0 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/IfActionUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/IfActionUsageTextualNotationBuilder.cs @@ -46,26 +46,14 @@ public static void BuildIfNode(SysML2.NET.Core.POCO.Systems.Actions.IIfActionUsa ActionUsageTextualNotationBuilder.BuildActionNodePrefix(poco, writerContext, stringBuilder); stringBuilder.Append("if "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildExpressionParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildExpressionParameterMember(elementAsParameterMembership0, writerContext, stringBuilder); } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildActionBodyParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildActionBodyParameterMember(elementAsParameterMembership1, writerContext, stringBuilder); } if (ownedRelationshipCursor.Current != null) diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/InterfaceUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/InterfaceUsageTextualNotationBuilder.cs index a55c91ba8..7e412d9a6 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/InterfaceUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/InterfaceUsageTextualNotationBuilder.cs @@ -77,27 +77,15 @@ public static void BuildBinaryInterfacePart(SysML2.NET.Core.POCO.Systems.Interfa { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildInterfaceEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildInterfaceEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append("to "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildInterfaceEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildInterfaceEndMember(elementAsEndFeatureMembership1, writerContext, stringBuilder); } } @@ -114,27 +102,15 @@ public static void BuildNaryInterfacePart(SysML2.NET.Core.POCO.Systems.Interface var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); stringBuilder.Append("("); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildInterfaceEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildInterfaceEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append(", "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildInterfaceEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildInterfaceEndMember(elementAsEndFeatureMembership1, writerContext, stringBuilder); } while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership endFeatureMembershipGuard && endFeatureMembershipGuard.OwnedRelatedElement.OfType().Any()) diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/InvocationExpressionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/InvocationExpressionTextualNotationBuilder.cs index 93ce81fa5..7e4f8c3f2 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/InvocationExpressionTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/InvocationExpressionTextualNotationBuilder.cs @@ -69,15 +69,9 @@ public static void BuildInvocationExpression(SysML2.NET.Core.POCO.Kernel.Express } FeatureTextualNotationBuilder.BuildArgumentList(poco, writerContext, stringBuilder); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsReturnParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IReturnParameterMembership elementAsReturnParameterMembership) - { - ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceDefinitionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceDefinitionTextualNotationBuilder.cs index 587a6e690..cf7c8180c 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceDefinitionTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceDefinitionTextualNotationBuilder.cs @@ -49,18 +49,12 @@ public static void BuildOccurrenceDefinitionPrefix(SysML2.NET.Core.POCO.Systems. SharedTextualNotationBuilder.BuildBasicDefinitionPrefix(poco, writerContext, stringBuilder); } - if (poco.IsIndividual && ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership) + if (poco.IsIndividual && ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildEmptyMultiplicityMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildEmptyMultiplicityMember(elementAsOwningMembership0, writerContext, stringBuilder); } stringBuilder.Append(" individual "); stringBuilder.Append(' '); @@ -87,15 +81,9 @@ public static void BuildIndividualDefinition(SysML2.NET.Core.POCO.Systems.Occurr { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildEmptyMultiplicityMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildEmptyMultiplicityMember(elementAsOwningMembership0, writerContext, stringBuilder); } if (poco.IsAbstract || poco.IsVariation) diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceUsageTextualNotationBuilder.cs index ecd151449..a9604fb16 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceUsageTextualNotationBuilder.cs @@ -35,14 +35,14 @@ public static partial class OccurrenceUsageTextualNotationBuilder { /// /// Builds the Textual Notation string for the rule OccurrenceUsagePrefix - /// OccurrenceUsagePrefix:OccurrenceUsage=BasicUsagePrefix(isIndividual?='individual')?(portionKind=PortionKind{isPortion=true})?UsageExtensionKeyword* + /// OccurrenceUsagePrefix:OccurrenceUsage=UnextendedUsagePrefix(isIndividual?='individual')?(portionKind=PortionKind{isPortion=true})?UsageExtensionKeyword* /// /// The from which the rule should be build /// The providing the serialization context for the current /// The that accumulates the entire textual notation with indentation public static void BuildOccurrenceUsagePrefix(SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder) { - UsageTextualNotationBuilder.BuildBasicUsagePrefix(poco, writerContext, stringBuilder); + UsageTextualNotationBuilder.BuildUnextendedUsagePrefix(poco, writerContext, stringBuilder); if (poco.IsIndividual) { diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OperatorExpressionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OperatorExpressionTextualNotationBuilder.cs index 68c8f0f51..b886b67d1 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OperatorExpressionTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OperatorExpressionTextualNotationBuilder.cs @@ -61,38 +61,20 @@ public static void BuildConditionalExpression(SysML2.NET.Core.POCO.Kernel.Expres } stringBuilder.Append("? "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildArgumentExpressionMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildArgumentExpressionMember(elementAsParameterMembership0, writerContext, stringBuilder); } stringBuilder.Append("else "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildArgumentExpressionMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildArgumentExpressionMember(elementAsParameterMembership1, writerContext, stringBuilder); } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsReturnParameterMembership2)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IReturnParameterMembership elementAsReturnParameterMembership) - { - ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership2, writerContext, stringBuilder); } } finally @@ -129,26 +111,14 @@ public static void BuildConditionalBinaryOperatorExpression(SysML2.NET.Core.POCO stringBuilder.Append(poco.Operator); stringBuilder.Append(' '); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildArgumentExpressionMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildArgumentExpressionMember(elementAsParameterMembership0, writerContext, stringBuilder); } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsReturnParameterMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IReturnParameterMembership elementAsReturnParameterMembership) - { - ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership1, writerContext, stringBuilder); } } finally @@ -196,15 +166,9 @@ public static void BuildBinaryOperatorExpression(SysML2.NET.Core.POCO.Kernel.Exp } } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsReturnParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IReturnParameterMembership elementAsReturnParameterMembership) - { - ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership0, writerContext, stringBuilder); } } finally @@ -241,15 +205,9 @@ public static void BuildUnaryOperatorExpression(SysML2.NET.Core.POCO.Kernel.Expr } } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsReturnParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IReturnParameterMembership elementAsReturnParameterMembership) - { - ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership0, writerContext, stringBuilder); } } finally @@ -292,15 +250,9 @@ public static void BuildClassificationExpression(SysML2.NET.Core.POCO.Kernel.Exp BuildClassificationExpressionHandCoded(poco, writerContext, stringBuilder); stringBuilder.Append(' '); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsReturnParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IReturnParameterMembership elementAsReturnParameterMembership) - { - ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership0, writerContext, stringBuilder); } } finally @@ -324,28 +276,16 @@ public static void BuildMetaclassificationExpression(SysML2.NET.Core.POCO.Kernel { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildMetadataArgumentMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildMetadataArgumentMember(elementAsParameterMembership0, writerContext, stringBuilder); } BuildMetaclassificationExpressionHandCoded(poco, writerContext, stringBuilder); stringBuilder.Append(' '); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsReturnParameterMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IReturnParameterMembership elementAsReturnParameterMembership) - { - ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ReturnParameterMembershipTextualNotationBuilder.BuildEmptyResultMember(elementAsReturnParameterMembership1, writerContext, stringBuilder); } } finally diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OwningMembershipTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OwningMembershipTextualNotationBuilder.cs index 8d4f03fbf..7fd9771ab 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OwningMembershipTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OwningMembershipTextualNotationBuilder.cs @@ -146,15 +146,9 @@ public static void BuildOwnedMultiplicity(SysML2.NET.Core.POCO.Root.Namespaces.I { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsMultiplicityRange0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Kernel.Multiplicities.IMultiplicityRange elementAsMultiplicityRange) - { - MultiplicityRangeTextualNotationBuilder.BuildMultiplicityRange(elementAsMultiplicityRange, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + MultiplicityRangeTextualNotationBuilder.BuildMultiplicityRange(elementAsMultiplicityRange0, writerContext, stringBuilder); } } @@ -218,15 +212,9 @@ public static void BuildConjugatedPortDefinitionMember(SysML2.NET.Core.POCO.Root { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsConjugatedPortDefinition0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.Ports.IConjugatedPortDefinition elementAsConjugatedPortDefinition) - { - ConjugatedPortDefinitionTextualNotationBuilder.BuildConjugatedPortDefinition(elementAsConjugatedPortDefinition, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + ConjugatedPortDefinitionTextualNotationBuilder.BuildConjugatedPortDefinition(elementAsConjugatedPortDefinition0, writerContext, stringBuilder); } } @@ -242,15 +230,9 @@ public static void BuildOwnedCrossMultiplicityMember(SysML2.NET.Core.POCO.Root.N { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsFeature0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Core.Features.IFeature elementAsFeature) - { - FeatureTextualNotationBuilder.BuildOwnedCrossMultiplicity(elementAsFeature, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + FeatureTextualNotationBuilder.BuildOwnedCrossMultiplicity(elementAsFeature0, writerContext, stringBuilder); } } @@ -266,15 +248,9 @@ public static void BuildOwnedFeatureChainMember(SysML2.NET.Core.POCO.Root.Namesp { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsFeature0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Core.Features.IFeature elementAsFeature) - { - FeatureTextualNotationBuilder.BuildOwnedFeatureChain(elementAsFeature, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + FeatureTextualNotationBuilder.BuildOwnedFeatureChain(elementAsFeature0, writerContext, stringBuilder); } } @@ -290,15 +266,9 @@ public static void BuildTransitionSuccessionMember(SysML2.NET.Core.POCO.Root.Nam { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsSuccession0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Kernel.Connectors.ISuccession elementAsSuccession) - { - SuccessionTextualNotationBuilder.BuildTransitionSuccession(elementAsSuccession, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + SuccessionTextualNotationBuilder.BuildTransitionSuccession(elementAsSuccession0, writerContext, stringBuilder); } } @@ -315,15 +285,9 @@ public static void BuildPrefixMetadataMember(SysML2.NET.Core.POCO.Root.Namespace var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); stringBuilder.Append("#"); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsMetadataUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.Metadata.IMetadataUsage elementAsMetadataUsage) - { - MetadataUsageTextualNotationBuilder.BuildPrefixMetadataUsage(elementAsMetadataUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + MetadataUsageTextualNotationBuilder.BuildPrefixMetadataUsage(elementAsMetadataUsage0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PackageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PackageTextualNotationBuilder.cs index b5d2a4564..b85f8cce7 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PackageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PackageTextualNotationBuilder.cs @@ -124,15 +124,9 @@ public static void BuildFilterPackage(SysML2.NET.Core.POCO.Kernel.Packages.IPack { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.Visibility == SysML2.NET.Core.Root.Namespaces.VisibilityKind.Public, out var elementAsImport0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IImport elementAsImport) - { - ImportTextualNotationBuilder.BuildFilterPackageImport(elementAsImport, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ImportTextualNotationBuilder.BuildFilterPackageImport(elementAsImport0, writerContext, stringBuilder); } while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Packages.IElementFilterMembership elementFilterMembershipGuard && elementFilterMembershipGuard.OwnedRelatedElement.OfType().Any()) diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ParameterMembershipTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ParameterMembershipTextualNotationBuilder.cs index 3cd6096ed..63559863b 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ParameterMembershipTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ParameterMembershipTextualNotationBuilder.cs @@ -44,15 +44,9 @@ public static void BuildMessageEventMember(SysML2.NET.Core.POCO.Kernel.Behaviors { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsEventOccurrenceUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.Occurrences.IEventOccurrenceUsage elementAsEventOccurrenceUsage) - { - EventOccurrenceUsageTextualNotationBuilder.BuildMessageEvent(elementAsEventOccurrenceUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + EventOccurrenceUsageTextualNotationBuilder.BuildMessageEvent(elementAsEventOccurrenceUsage0, writerContext, stringBuilder); } } @@ -109,15 +103,9 @@ public static void BuildArgumentExpressionMember(SysML2.NET.Core.POCO.Kernel.Beh { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsFeature0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Core.Features.IFeature elementAsFeature) - { - FeatureTextualNotationBuilder.BuildArgumentExpression(elementAsFeature, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + FeatureTextualNotationBuilder.BuildArgumentExpression(elementAsFeature0, writerContext, stringBuilder); } } @@ -133,15 +121,9 @@ public static void BuildNodeParameterMember(SysML2.NET.Core.POCO.Kernel.Behavior { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsReferenceUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IReferenceUsage elementAsReferenceUsage) - { - ReferenceUsageTextualNotationBuilder.BuildNodeParameter(elementAsReferenceUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + ReferenceUsageTextualNotationBuilder.BuildNodeParameter(elementAsReferenceUsage0, writerContext, stringBuilder); } } @@ -253,15 +235,9 @@ public static void BuildIfNodeParameterMember(SysML2.NET.Core.POCO.Kernel.Behavi { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsIfActionUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.Actions.IIfActionUsage elementAsIfActionUsage) - { - IfActionUsageTextualNotationBuilder.BuildIfNode(elementAsIfActionUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + IfActionUsageTextualNotationBuilder.BuildIfNode(elementAsIfActionUsage0, writerContext, stringBuilder); } } @@ -277,15 +253,9 @@ public static void BuildMetadataArgumentMember(SysML2.NET.Core.POCO.Kernel.Behav { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsFeature0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Core.Features.IFeature elementAsFeature) - { - FeatureTextualNotationBuilder.BuildMetadataArgument(elementAsFeature, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + FeatureTextualNotationBuilder.BuildMetadataArgument(elementAsFeature0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PortDefinitionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PortDefinitionTextualNotationBuilder.cs index f3ec4c5fe..178078a3b 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PortDefinitionTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PortDefinitionTextualNotationBuilder.cs @@ -48,15 +48,9 @@ public static void BuildPortDefinition(SysML2.NET.Core.POCO.Systems.Ports.IPortD stringBuilder.Append("def "); DefinitionTextualNotationBuilder.BuildDefinition(poco, writerContext, stringBuilder); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildConjugatedPortDefinitionMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildConjugatedPortDefinitionMember(elementAsOwningMembership0, writerContext, stringBuilder); } // NonParsing Assignment Element : conjugatedPortDefinition.ownedPortConjugator.originalPortDefinition = this => Does not have to be process diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PortUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PortUsageTextualNotationBuilder.cs index 80c517f91..af45da675 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PortUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PortUsageTextualNotationBuilder.cs @@ -61,18 +61,12 @@ public static void BuildInterfaceEnd(SysML2.NET.Core.POCO.Systems.Ports.IPortUsa { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership && ownedRelationshipCursor.GetNext(1) is SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildOwnedCrossMultiplicityMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildOwnedCrossMultiplicityMember(elementAsOwningMembership0, writerContext, stringBuilder); } stringBuilder.Append(' '); } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ReferenceUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ReferenceUsageTextualNotationBuilder.cs index 912dba56c..fc3da35b7 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ReferenceUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ReferenceUsageTextualNotationBuilder.cs @@ -82,7 +82,7 @@ public static void BuildVariantReference(SysML2.NET.Core.POCO.Systems.Definition } } - while (ownedRelationshipCursor.Current is not null and not SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage) + while (ownedRelationshipCursor.Current is (SysML2.NET.Core.POCO.Core.Features.IFeatureTyping or SysML2.NET.Core.POCO.Core.Features.ISubsetting or SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting or SysML2.NET.Core.POCO.Core.Features.ICrossSubsetting or SysML2.NET.Core.POCO.Core.Features.IRedefinition)) { var positionBeforeItem0 = ownedRelationshipCursor.Position; FeatureTextualNotationBuilder.BuildFeatureSpecialization(poco, writerContext, stringBuilder); @@ -104,18 +104,12 @@ public static void BuildSourceEnd(SysML2.NET.Core.POCO.Systems.DefinitionAndUsag { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildOwnedMultiplicity(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildOwnedMultiplicity(elementAsOwningMembership0, writerContext, stringBuilder); } } @@ -133,18 +127,12 @@ public static void BuildConnectorEnd(SysML2.NET.Core.POCO.Systems.DefinitionAndU { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership && ownedRelationshipCursor.GetNext(1) is SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildOwnedCrossMultiplicityMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildOwnedCrossMultiplicityMember(elementAsOwningMembership0, writerContext, stringBuilder); } stringBuilder.Append(' '); } @@ -218,15 +206,9 @@ public static void BuildNodeParameter(SysML2.NET.Core.POCO.Systems.DefinitionAnd { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsFeatureValue0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue elementAsFeatureValue) - { - FeatureValueTextualNotationBuilder.BuildFeatureBinding(elementAsFeatureValue, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + FeatureValueTextualNotationBuilder.BuildFeatureBinding(elementAsFeatureValue0, writerContext, stringBuilder); } } @@ -254,18 +236,12 @@ public static void BuildAssignmentTargetParameter(SysML2.NET.Core.POCO.Systems.D { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsFeatureValue0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue elementAsFeatureValue) - { - FeatureValueTextualNotationBuilder.BuildAssignmentTargetBinding(elementAsFeatureValue, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + FeatureValueTextualNotationBuilder.BuildAssignmentTargetBinding(elementAsFeatureValue0, writerContext, stringBuilder); } stringBuilder.Append("."); } @@ -331,15 +307,9 @@ public static void BuildSatisfactionParameter(SysML2.NET.Core.POCO.Systems.Defin { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsFeatureValue0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.FeatureValues.IFeatureValue elementAsFeatureValue) - { - FeatureValueTextualNotationBuilder.BuildSatisfactionFeatureValue(elementAsFeatureValue, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + FeatureValueTextualNotationBuilder.BuildSatisfactionFeatureValue(elementAsFeatureValue0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SatisfyRequirementUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SatisfyRequirementUsageTextualNotationBuilder.cs index a9f1fe3af..0768df745 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SatisfyRequirementUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SatisfyRequirementUsageTextualNotationBuilder.cs @@ -84,19 +84,13 @@ public static void BuildSatisfyRequirementUsage(SysML2.NET.Core.POCO.Systems.Req FeatureTextualNotationBuilder.BuildValuePart(poco, writerContext, stringBuilder); } - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { stringBuilder.Append("by "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsSubjectMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Systems.Requirements.ISubjectMembership elementAsSubjectMembership) - { - SubjectMembershipTextualNotationBuilder.BuildSatisfactionSubjectMember(elementAsSubjectMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + SubjectMembershipTextualNotationBuilder.BuildSatisfactionSubjectMember(elementAsSubjectMembership0, writerContext, stringBuilder); } stringBuilder.Append(' '); } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SendActionUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SendActionUsageTextualNotationBuilder.cs index b08008d54..1b849b308 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SendActionUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SendActionUsageTextualNotationBuilder.cs @@ -72,15 +72,9 @@ public static void BuildSendNodeDeclaration(SysML2.NET.Core.POCO.Systems.Actions } stringBuilder.Append("send "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildNodeParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildNodeParameterMember(elementAsParameterMembership0, writerContext, stringBuilder); } if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership || !string.IsNullOrWhiteSpace(poco.DeclaredShortName) || !string.IsNullOrWhiteSpace(poco.DeclaredName) || poco.Direction.HasValue || poco.IsDerived || poco.IsAbstract || poco.IsVariation || poco.IsConstant || poco.IsOrdered || poco.IsEnd || poco.isReference || poco.IsIndividual || poco.PortionKind.HasValue || poco.IsComposite || poco.IsPortion || poco.IsVariable || poco.IsSufficient) diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SubjectMembershipTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SubjectMembershipTextualNotationBuilder.cs index 7045f3d1d..f2ec8dd14 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SubjectMembershipTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SubjectMembershipTextualNotationBuilder.cs @@ -69,15 +69,9 @@ public static void BuildSatisfactionSubjectMember(SysML2.NET.Core.POCO.Systems.R { var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement); - if (ownedRelatedElementCursor.Current != null) + if (ownedRelatedElementCursor.TryTake(candidate => candidate.OwnedRelationship.OfType().Any(), out var elementAsReferenceUsage0)) { - - if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IReferenceUsage elementAsReferenceUsage) - { - ReferenceUsageTextualNotationBuilder.BuildSatisfactionParameter(elementAsReferenceUsage, writerContext, stringBuilder); - ownedRelatedElementCursor.Move(); - - } + ReferenceUsageTextualNotationBuilder.BuildSatisfactionParameter(elementAsReferenceUsage0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SuccessionAsUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SuccessionAsUsageTextualNotationBuilder.cs index dff409a8d..a27779673 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SuccessionAsUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SuccessionAsUsageTextualNotationBuilder.cs @@ -44,15 +44,9 @@ public static void BuildSourceSuccession(SysML2.NET.Core.POCO.Systems.Connection { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildSourceEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildSourceEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } } @@ -68,27 +62,15 @@ public static void BuildTargetSuccession(SysML2.NET.Core.POCO.Systems.Connection { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildSourceEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildSourceEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append("then "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership1, writerContext, stringBuilder); } } @@ -114,27 +96,15 @@ public static void BuildSuccessionAsUsage(SysML2.NET.Core.POCO.Systems.Connectio stringBuilder.Append("first "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append("then "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership1, writerContext, stringBuilder); } UsageTextualNotationBuilder.BuildUsageBody(poco, writerContext, stringBuilder); diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SuccessionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SuccessionTextualNotationBuilder.cs index 013c85d5d..ed8dcfffe 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SuccessionTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/SuccessionTextualNotationBuilder.cs @@ -44,26 +44,14 @@ public static void BuildTransitionSuccession(SysML2.NET.Core.POCO.Kernel.Connect { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildEmptyEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildEmptyEndMember(elementAsEndFeatureMembership0, writerContext, stringBuilder); } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsEndFeatureMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IEndFeatureMembership elementAsEndFeatureMembership) - { - EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + EndFeatureMembershipTextualNotationBuilder.BuildConnectorEndMember(elementAsEndFeatureMembership1, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TerminateActionUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TerminateActionUsageTextualNotationBuilder.cs index 937df88d9..92e2d22f6 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TerminateActionUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TerminateActionUsageTextualNotationBuilder.cs @@ -51,18 +51,12 @@ public static void BuildTerminateNode(SysML2.NET.Core.POCO.Systems.Actions.ITerm } stringBuilder.Append("terminate "); - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildNodeParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildNodeParameterMember(elementAsParameterMembership0, writerContext, stringBuilder); } stringBuilder.Append(' '); } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TransitionUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TransitionUsageTextualNotationBuilder.cs index d710a41bb..3b8fe3d47 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TransitionUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TransitionUsageTextualNotationBuilder.cs @@ -44,27 +44,15 @@ public static void BuildGuardedTargetSuccession(SysML2.NET.Core.POCO.Systems.Sta { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.Kind == SysML2.NET.Core.Systems.States.TransitionFeatureKind.Guard, out var elementAsTransitionFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership elementAsTransitionFeatureMembership) - { - TransitionFeatureMembershipTextualNotationBuilder.BuildGuardExpressionMember(elementAsTransitionFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + TransitionFeatureMembershipTextualNotationBuilder.BuildGuardExpressionMember(elementAsTransitionFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append("then "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildTransitionSuccessionMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildTransitionSuccessionMember(elementAsOwningMembership1, writerContext, stringBuilder); } } @@ -81,15 +69,9 @@ public static void BuildDefaultTargetSuccession(SysML2.NET.Core.POCO.Systems.Sta var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); stringBuilder.Append("else "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildTransitionSuccessionMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildTransitionSuccessionMember(elementAsOwningMembership0, writerContext, stringBuilder); } } @@ -125,27 +107,15 @@ public static void BuildGuardedSuccession(SysML2.NET.Core.POCO.Systems.States.IT } } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.Kind == SysML2.NET.Core.Systems.States.TransitionFeatureKind.Guard, out var elementAsTransitionFeatureMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership elementAsTransitionFeatureMembership) - { - TransitionFeatureMembershipTextualNotationBuilder.BuildGuardExpressionMember(elementAsTransitionFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + TransitionFeatureMembershipTextualNotationBuilder.BuildGuardExpressionMember(elementAsTransitionFeatureMembership0, writerContext, stringBuilder); } stringBuilder.Append("then "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildTransitionSuccessionMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildTransitionSuccessionMember(elementAsOwningMembership1, writerContext, stringBuilder); } UsageTextualNotationBuilder.BuildUsageBody(poco, writerContext, stringBuilder); @@ -162,28 +132,16 @@ public static void BuildTargetTransitionUsage(SysML2.NET.Core.POCO.Systems.State { var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildEmptyParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildEmptyParameterMember(elementAsParameterMembership0, writerContext, stringBuilder); } BuildTargetTransitionUsageHandCoded(poco, writerContext, stringBuilder); stringBuilder.Append("then "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildTransitionSuccessionMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildTransitionSuccessionMember(elementAsOwningMembership1, writerContext, stringBuilder); } TypeTextualNotationBuilder.BuildActionBody(poco, writerContext, stringBuilder); @@ -220,89 +178,53 @@ public static void BuildTransitionUsage(SysML2.NET.Core.POCO.Systems.States.ITra } } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildEmptyParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildEmptyParameterMember(elementAsParameterMembership0, writerContext, stringBuilder); } - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership && ownedRelationshipCursor.GetNext(1) is SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership { Kind: SysML2.NET.Core.Systems.States.TransitionFeatureKind.Trigger }) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any()) && ownedRelationshipCursor.Contains(candidate => candidate.Kind == SysML2.NET.Core.Systems.States.TransitionFeatureKind.Trigger)) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildEmptyParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildEmptyParameterMember(elementAsParameterMembership1, writerContext, stringBuilder); } - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.Kind == SysML2.NET.Core.Systems.States.TransitionFeatureKind.Trigger, out var elementAsTransitionFeatureMembership2)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership elementAsTransitionFeatureMembership) - { - TransitionFeatureMembershipTextualNotationBuilder.BuildTriggerActionMember(elementAsTransitionFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + TransitionFeatureMembershipTextualNotationBuilder.BuildTriggerActionMember(elementAsTransitionFeatureMembership2, writerContext, stringBuilder); } stringBuilder.Append(' '); } - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership { Kind: SysML2.NET.Core.Systems.States.TransitionFeatureKind.Guard }) + if (ownedRelationshipCursor.Contains(candidate => candidate.Kind == SysML2.NET.Core.Systems.States.TransitionFeatureKind.Guard)) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.Kind == SysML2.NET.Core.Systems.States.TransitionFeatureKind.Guard, out var elementAsTransitionFeatureMembership3)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership elementAsTransitionFeatureMembership) - { - TransitionFeatureMembershipTextualNotationBuilder.BuildGuardExpressionMember(elementAsTransitionFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + TransitionFeatureMembershipTextualNotationBuilder.BuildGuardExpressionMember(elementAsTransitionFeatureMembership3, writerContext, stringBuilder); } stringBuilder.Append(' '); } - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership { Kind: SysML2.NET.Core.Systems.States.TransitionFeatureKind.Effect } && ownedRelationshipCursor.GetNext(1) is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.Kind == SysML2.NET.Core.Systems.States.TransitionFeatureKind.Effect)) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.Kind == SysML2.NET.Core.Systems.States.TransitionFeatureKind.Effect, out var elementAsTransitionFeatureMembership4)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Systems.States.ITransitionFeatureMembership elementAsTransitionFeatureMembership) - { - TransitionFeatureMembershipTextualNotationBuilder.BuildEffectBehaviorMember(elementAsTransitionFeatureMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + TransitionFeatureMembershipTextualNotationBuilder.BuildEffectBehaviorMember(elementAsTransitionFeatureMembership4, writerContext, stringBuilder); } stringBuilder.Append(' '); } stringBuilder.Append("then "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership5)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildTransitionSuccessionMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildTransitionSuccessionMember(elementAsOwningMembership5, writerContext, stringBuilder); } TypeTextualNotationBuilder.BuildActionBody(poco, writerContext, stringBuilder); diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TypeTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TypeTextualNotationBuilder.cs index 5e61cf14b..f4532442b 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TypeTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TypeTextualNotationBuilder.cs @@ -230,18 +230,12 @@ public static void BuildCalculationBodyPart(SysML2.NET.Core.POCO.Core.Types.ITyp } - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsResultExpressionMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership elementAsResultExpressionMembership) - { - ResultExpressionMembershipTextualNotationBuilder.BuildResultExpressionMember(elementAsResultExpressionMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ResultExpressionMembershipTextualNotationBuilder.BuildResultExpressionMember(elementAsResultExpressionMembership0, writerContext, stringBuilder); } } @@ -375,18 +369,12 @@ public static void BuildCaseBody(SysML2.NET.Core.POCO.Core.Types.IType poco, Tex } - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsResultExpressionMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership elementAsResultExpressionMembership) - { - ResultExpressionMembershipTextualNotationBuilder.BuildResultExpressionMember(elementAsResultExpressionMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ResultExpressionMembershipTextualNotationBuilder.BuildResultExpressionMember(elementAsResultExpressionMembership0, writerContext, stringBuilder); } stringBuilder.Append(' '); } @@ -470,8 +458,7 @@ public static void BuildMetadataBody(SysML2.NET.Core.POCO.Core.Types.IType poco, ownedRelationshipCursor.Move(); break; default: - ownedRelationshipCursor.Move(); - break; + throw new System.InvalidOperationException($"The textual notation writer cannot place the current element ({ownedRelationshipCursor.Current?.GetType().Name}) while building 'Type' — no alternative of the rule claims it, so it would be silently dropped."); } ownedRelationshipCursor.AssertAdvancedSince(positionBeforeItem0, "MetadataBody"); } @@ -536,25 +523,19 @@ public static void BuildTypeDeclaration(SysML2.NET.Core.POCO.Core.Types.IType po ElementTextualNotationBuilder.BuildIdentification(poco, writerContext, stringBuilder); - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildOwnedMultiplicity(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildOwnedMultiplicity(elementAsOwningMembership0, writerContext, stringBuilder); } stringBuilder.Append(' '); } BuildTypeDeclarationHandCoded(poco, writerContext, stringBuilder); stringBuilder.Append(' '); - while (ownedRelationshipCursor.Current != null) + while (ownedRelationshipCursor.Current is (SysML2.NET.Core.POCO.Core.Types.IDisjoining or SysML2.NET.Core.POCO.Core.Types.IUnioning or SysML2.NET.Core.POCO.Core.Types.IIntersecting or SysML2.NET.Core.POCO.Core.Types.IDifferencing)) { var positionBeforeItem0 = ownedRelationshipCursor.Position; BuildTypeRelationshipPart(poco, writerContext, stringBuilder); @@ -953,18 +934,12 @@ public static void BuildFunctionBodyPart(SysML2.NET.Core.POCO.Core.Types.IType p } - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsResultExpressionMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Functions.IResultExpressionMembership elementAsResultExpressionMembership) - { - ResultExpressionMembershipTextualNotationBuilder.BuildResultExpressionMember(elementAsResultExpressionMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ResultExpressionMembershipTextualNotationBuilder.BuildResultExpressionMember(elementAsResultExpressionMembership0, writerContext, stringBuilder); } } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs index f7be10bd5..ce35e9bad 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs @@ -133,18 +133,12 @@ public static void BuildEndUsagePrefix(SysML2.NET.Core.POCO.Systems.DefinitionAn stringBuilder.Append(" end "); } - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsOwningMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership) - { - OwningMembershipTextualNotationBuilder.BuildOwnedCrossFeatureMember(elementAsOwningMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + OwningMembershipTextualNotationBuilder.BuildOwnedCrossFeatureMember(elementAsOwningMembership0, writerContext, stringBuilder); } } @@ -372,12 +366,12 @@ public static void BuildStructureUsageElement(SysML2.NET.Core.POCO.Systems.Defin case SysML2.NET.Core.POCO.Systems.Ports.IPortUsage pocoPortUsage: PortUsageTextualNotationBuilder.BuildPortUsage(pocoPortUsage, writerContext, stringBuilder); break; - case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage pocoOccurrenceUsageIndividualUsage when pocoOccurrenceUsageIndividualUsage.IsIndividual: - OccurrenceUsageTextualNotationBuilder.BuildIndividualUsage(pocoOccurrenceUsageIndividualUsage, writerContext, stringBuilder); - break; case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage pocoOccurrenceUsageOccurrenceUsage when pocoOccurrenceUsageOccurrenceUsage.IsValidForOccurrenceUsage(writerContext): OccurrenceUsageTextualNotationBuilder.BuildOccurrenceUsage(pocoOccurrenceUsageOccurrenceUsage, writerContext, stringBuilder); break; + case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage pocoOccurrenceUsageIndividualUsage when pocoOccurrenceUsageIndividualUsage.IsValidForIndividualUsage(writerContext): + OccurrenceUsageTextualNotationBuilder.BuildIndividualUsage(pocoOccurrenceUsageIndividualUsage, writerContext, stringBuilder); + break; case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage pocoOccurrenceUsage: OccurrenceUsageTextualNotationBuilder.BuildPortionUsage(pocoOccurrenceUsage, writerContext, stringBuilder); break; @@ -519,7 +513,7 @@ public static void BuildVariantUsageElement(SysML2.NET.Core.POCO.Systems.Definit case SysML2.NET.Core.POCO.Systems.Attributes.IAttributeUsage pocoAttributeUsage: AttributeUsageTextualNotationBuilder.BuildAttributeUsage(pocoAttributeUsage, writerContext, stringBuilder); break; - case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage pocoOccurrenceUsageIndividualUsage when pocoOccurrenceUsageIndividualUsage.IsIndividual: + case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage pocoOccurrenceUsageIndividualUsage when pocoOccurrenceUsageIndividualUsage.IsValidForIndividualUsage(writerContext): OccurrenceUsageTextualNotationBuilder.BuildIndividualUsage(pocoOccurrenceUsageIndividualUsage, writerContext, stringBuilder); break; case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage pocoOccurrenceUsageOccurrenceUsage when pocoOccurrenceUsageOccurrenceUsage.IsValidForOccurrenceUsage(writerContext): @@ -576,8 +570,8 @@ public static void BuildInterfaceOccurrenceUsageElement(SysML2.NET.Core.POCO.Sys { switch (poco) { - case SysML2.NET.Core.POCO.Systems.Ports.IPortUsage pocoPortUsage: - PortUsageTextualNotationBuilder.BuildDefaultInterfaceEnd(pocoPortUsage, writerContext, stringBuilder); + case SysML2.NET.Core.POCO.Systems.Ports.IPortUsage pocoPortUsageDefaultInterfaceEnd when pocoPortUsageDefaultInterfaceEnd.IsValidForDefaultInterfaceEnd(writerContext): + PortUsageTextualNotationBuilder.BuildDefaultInterfaceEnd(pocoPortUsageDefaultInterfaceEnd, writerContext, stringBuilder); break; case SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IUsage pocoUsageStructureUsageElement when pocoUsageStructureUsageElement.IsValidForStructureUsageElement(writerContext): BuildStructureUsageElement(pocoUsageStructureUsageElement, writerContext, stringBuilder); diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/WhileLoopActionUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/WhileLoopActionUsageTextualNotationBuilder.cs index b06222bdb..e68114ee9 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/WhileLoopActionUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/WhileLoopActionUsageTextualNotationBuilder.cs @@ -48,59 +48,35 @@ public static void BuildWhileLoopNode(SysML2.NET.Core.POCO.Systems.Actions.IWhil { stringBuilder.Append("while "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership0)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildExpressionParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildExpressionParameterMember(elementAsParameterMembership0, writerContext, stringBuilder); } } else if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership) { stringBuilder.Append("loop "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership1)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildEmptyParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildEmptyParameterMember(elementAsParameterMembership1, writerContext, stringBuilder); } } stringBuilder.Append(' '); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership2)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildActionBodyParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildActionBodyParameterMember(elementAsParameterMembership2, writerContext, stringBuilder); } - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership) + if (ownedRelationshipCursor.Contains(candidate => candidate.OwnedRelatedElement.OfType().Any())) { stringBuilder.Append("until "); - if (ownedRelationshipCursor.Current != null) + if (ownedRelationshipCursor.TryTake(candidate => candidate.OwnedRelatedElement.OfType().Any(), out var elementAsParameterMembership3)) { - - if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership elementAsParameterMembership) - { - ParameterMembershipTextualNotationBuilder.BuildExpressionParameterMember(elementAsParameterMembership, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - - } + ParameterMembershipTextualNotationBuilder.BuildExpressionParameterMember(elementAsParameterMembership3, writerContext, stringBuilder); } stringBuilder.AppendLine(";"); } diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/CollectionCursor.cs b/SysML2.NET.Serializer.TextualNotation/Writers/CollectionCursor.cs index 058965b0f..0fbcbe277 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/CollectionCursor.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/CollectionCursor.cs @@ -36,11 +36,17 @@ public class CollectionCursor where T : class /// private readonly IReadOnlyList elements; + /// + /// Gets the indices of elements consumed ahead of the cursor by ; + /// every traversal member skips them so a taken element is never visited twice. + /// + private readonly HashSet consumedOutOfOrder = []; + /// /// Gets the value of the current index. /// private int index; - + /// /// Initializes a new instance of the class. /// @@ -54,13 +60,14 @@ public CollectionCursor(IReadOnlyList elements) /// Gets the current element at the cursor position. /// Returns default if the cursor is out of range. /// - public T Current => this.GetCurrent(this.index); + public T Current => this.GetCurrent(this.SkipConsumed(this.index)); /// - /// Gets the cursor's current offset into the collection. Exposed so a caller iterating this cursor + /// Gets the cursor's current offset into the collection, skipping over elements already consumed + /// out of order by . Exposed so a caller iterating this cursor /// can prove its loop body made forward progress; see . /// - public int Position => this.index; + public int Position => this.SkipConsumed(this.index); /// /// Throws when the cursor has not moved past , i.e. the @@ -83,7 +90,7 @@ public CollectionCursor(IReadOnlyList elements) /// public void AssertAdvancedSince(int positionBeforeIteration, string ruleName) { - if (this.index != positionBeforeIteration) + if (this.Position != positionBeforeIteration) { return; } @@ -122,7 +129,91 @@ private T GetCurrent(int indexToUse) /// public T GetNext(int amount) { - return amount < 0 ? throw new ArgumentException("Not able to get previous element in the collection") : this.GetCurrent(this.index + amount); + if (amount < 0) + { + throw new ArgumentException("Not able to get previous element in the collection"); + } + + var probeIndex = this.SkipConsumed(this.index); + + for (var stepIndex = 0; stepIndex < amount; stepIndex++) + { + probeIndex = this.SkipConsumed(probeIndex + 1); + } + + return this.GetCurrent(probeIndex); + } + + /// + /// Determines whether an element the cursor has not yet consumed — at the cursor position or + /// anywhere ahead of it — is a satisfying + /// , without consuming anything. This is the non-consuming companion + /// of for use in guard conditions; both MUST be called with the + /// identical predicate so the condition and the consumption agree on the element they select. + /// + /// The role discriminator the sought element must satisfy. + /// Any class inheriting from . + /// true when a matching unconsumed element exists; false otherwise. + /// When is null. + public bool Contains(Func predicate) where TDerived : class, T + { + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + for (var probeIndex = this.SkipConsumed(this.index); probeIndex < this.elements.Count; probeIndex = this.SkipConsumed(probeIndex + 1)) + { + if (this.elements[probeIndex] is TDerived candidate && predicate(candidate)) + { + return true; + } + } + + return false; + } + + /// + /// Takes the first unconsumed element — at the cursor position or anywhere ahead of it — that is a + /// satisfying , locating the element by + /// its ROLE rather than by its position. A match at the cursor advances the cursor exactly like + /// ; a match ahead of it is recorded as consumed out of order and skipped by all + /// later traversal, while the cursor itself stays put. + /// + /// The role discriminator the sought element must satisfy. + /// The taken element, or default when nothing matched. + /// Any class inheriting from . + /// true when a matching element was taken; false otherwise. + /// When is null. + public bool TryTake(Func predicate, out TDerived value) where TDerived : class, T + { + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + var startIndex = this.SkipConsumed(this.index); + + for (var probeIndex = startIndex; probeIndex < this.elements.Count; probeIndex = this.SkipConsumed(probeIndex + 1)) + { + if (this.elements[probeIndex] is TDerived candidate && predicate(candidate)) + { + if (probeIndex == startIndex) + { + this.Move(); + } + else + { + this.consumedOutOfOrder.Add(probeIndex); + } + + value = candidate; + return true; + } + } + + value = default; + return false; } /// @@ -140,7 +231,29 @@ public void Move(int amount = 1) throw new ArgumentOutOfRangeException(nameof(amount)); } - this.index = Math.Min(this.index + amount, this.elements.Count); + for (var stepIndex = 0; stepIndex < amount; stepIndex++) + { + this.index = Math.Min(this.SkipConsumed(this.index) + 1, this.elements.Count); + } + } + + /// + /// Returns the first index at or after that has not been consumed out of + /// order by . Identity while nothing was taken out of order, so + /// every traversal member behaves exactly as it did before the primitive existed. + /// + /// The index to start probing at. + /// The first unconsumed index at or after . + private int SkipConsumed(int from) + { + var unconsumedIndex = from; + + while (this.consumedOutOfOrder.Contains(unconsumedIndex)) + { + unconsumedIndex++; + } + + return unconsumedIndex; } /// diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/ConnectionUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/ConnectionUsageTextualNotationBuilder.cs index 1cf9d4a0c..1f6400cc2 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/ConnectionUsageTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/ConnectionUsageTextualNotationBuilder.cs @@ -56,14 +56,15 @@ private static void BuildConnectionUsageHandCoded(IConnectionUsage poco, Textual || ownedRelationshipCursor.Current is ISpecialization || ownedRelationshipCursor.Current is IConjugation; - if (hasDeclaration) + var hasConnectorPart = poco.OwnedRelationship.OfType().Count() >= 2; + + if (hasDeclaration || !hasConnectorPart) { - // Alt 1: 'connection' UsageDeclaration ValuePart? ('connect' ConnectorPart)? stringBuilder.Append("connection "); UsageTextualNotationBuilder.BuildUsageDeclaration(poco, writerContext, stringBuilder); FeatureTextualNotationBuilder.BuildValuePart(poco, writerContext, stringBuilder); - if (poco.OwnedRelationship.OfType().Any()) + if (hasConnectorPart) { stringBuilder.Append("connect "); BuildConnectorPart(poco, writerContext, stringBuilder); diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/IndentedStringBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/IndentedStringBuilder.cs index 0436aff61..84f73735a 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/IndentedStringBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/IndentedStringBuilder.cs @@ -405,6 +405,26 @@ public IndentedStringBuilder AppendIndentedLiteral(string literal) return this; } + /// + /// Appends the default line terminator WITHOUT stripping the line's trailing white space, + /// unlike . + /// Use this only where trailing white space is content rather than layout — inside a + /// REGULAR_COMMENT body, which KerML §8.2.3.3.2 stores with all white space as entered. + /// + /// The current instance, to allow chaining. + public IndentedStringBuilder AppendLiteralLine() + { + if (this.inlineBlockDepth > 0) + { + this.AppendCharNormalized(' '); + return this; + } + + this.builder.AppendLine(); + this.atLineStart = true; + return this; + } + /// /// Converts the accumulated content of the underlying to /// a . diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs b/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs deleted file mode 100644 index b5c816768..000000000 --- a/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs +++ /dev/null @@ -1,2660 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// -// -// Copyright 2022-2026 Starion Group S.A. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// -// ------------------------------------------------------------------------------------------------ - -namespace SysML2.NET.Serializer.TextualNotation.Writers -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - - using SysML2.NET.Core.POCO.Core.Features; - using SysML2.NET.Core.POCO.Core.Types; - using SysML2.NET.Core.POCO.Kernel.Behaviors; - using SysML2.NET.Core.POCO.Kernel.Connectors; - using SysML2.NET.Core.POCO.Kernel.Expressions; - using SysML2.NET.Core.POCO.Kernel.FeatureValues; - using SysML2.NET.Core.POCO.Kernel.Interactions; - using SysML2.NET.Core.POCO.Root.Elements; - using SysML2.NET.Core.POCO.Root.Namespaces; - using SysML2.NET.Core.POCO.Systems.Actions; - using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage; - using SysML2.NET.Core.Root.Namespaces; - using SysML2.NET.Extensions; - using SysML2.NET.Semantics.Implied; - - /// - /// Resolves the shortest unambiguous textual name for a reference, mirroring KerML §8.2.3.5. - /// Holds an eager per-namespace simple-name index built on construction, plus lazy caches for - /// source scope chains and resolved emissions keyed by (target, sourceLocalScope). - /// - public sealed class NameResolutionCache - { - /// - /// Shared empty index returned for unknown / null namespaces. - /// - private static readonly IReadOnlyDictionary> EmptyIndex - = new Dictionary>(StringComparer.Ordinal); - - /// - /// Eager structural cache: namespace → (simple-name → member set). - /// - private readonly Dictionary>> simpleNameIndices; - - /// - /// Lazy cache: source-POCO id → its upward containment chain of namespaces. - /// - private readonly Dictionary sourceScopeChains - = new (); - - /// - /// Lazy cache: (target.Id, sourceLocalScope.Id, matchFloorScope.Id) → emitted string. - /// - private readonly Dictionary<(Guid TargetId, Guid SourceScopeId, Guid MatchFloorId), string> resolvedReferences - = new (); - - /// - /// Reverse index: canonical owning namespace → namespaces that DIRECTLY re-export it via - /// . Enables the SST facade idiom ISQ::mass over - /// ISQBase::mass (single hop only). - /// - private readonly Dictionary> directFacadeIndex = new(); - - /// - /// Reverse index of alias X for Y; bindings: scope → (aliased element → alias names). - /// Needed because probes only the target's own lexical forms. - /// - private readonly Dictionary>> aliasIndex = new(); - - /// - /// The other root namespaces forming the global namespace (KerML §8.2.3.5.2); only their - /// VISIBLE memberships are indexed. - /// - private readonly List globalNamespaces; - - /// - /// Supplies the implied Relationships (KerML §8.4.2) that a model exported without them - /// omits, so a name reachable only through one can still be shortened. - /// - private readonly IImpliedRelationshipProvider impliedRelationshipProvider; - - /// - /// Elements of the resolution graph keyed by Id, built lazily on the first implied-general - /// translation. Instance state, never shared: each writer context carries its own cache, so - /// parallel writers cannot observe or pollute one another. - /// - private Dictionary resolutionGraphElementsById; - - /// - /// Initializes the cache and eagerly indexes every namespace reachable from - /// . - /// - /// The root being serialized. - /// - /// The other loaded root namespaces (model libraries), forming the global namespace per - /// KerML §8.2.3.5.2. Optional — without them resolution falls back to longer, equally valid names. - /// - /// - /// The provider supplying the implied Relationships a model exported without them omits. - /// Optional — when absent, resolution sees only the declared Specializations, so a name - /// reachable ONLY through an implied one degrades to a longer, equally valid form. - /// - public NameResolutionCache(INamespace rootNamespace, IEnumerable globalNamespaces = null, IImpliedRelationshipProvider impliedRelationshipProvider = null) - { - this.RootNamespace = rootNamespace ?? throw new ArgumentNullException(nameof(rootNamespace)); - - this.globalNamespaces = globalNamespaces? - .Where(candidate => candidate != null && !ReferenceEquals(candidate, rootNamespace)) - .Distinct() - .ToList() ?? []; - - this.impliedRelationshipProvider = impliedRelationshipProvider ?? NullImpliedRelationshipProvider.Instance; - - this.simpleNameIndices = this.BuildSimpleNameIndices(rootNamespace); - } - - /// - /// Gets the root — the fallback local scope when a source POCO has no - /// resolvable enclosing namespace. - /// - public INamespace RootNamespace { get; } - - /// - /// Resolves the textual notation for a reference to at the site of - /// . Results are memoised per (target, sourceLocalScope). - /// - /// The referenced ; may be . - /// The POCO at whose syntactic position the reference appears. - /// The string to emit; empty when is . - public string Resolve(IElement target, IElement sourcePoco) - { - switch (target) - { - case null: - return string.Empty; - - case IMembership membership: - if (membership.MemberElement == null) - { - return string.Empty; - } - - // A MembershipImport names its target THROUGH a Membership, so resolving the - // Membership itself would never reach the simple-name index and every membership - // import was emitted ownership-relative — `import PartsTree::vehicle::**` where the - // enclosing namespace already imports `PartsTree::*` and `vehicle` resolves bare. - // Continue with the member element so the ordinary path applies; the import - // self-containment guard immediately below still runs, and the index still declines - // to shorten a name that is shadowed or ambiguous at the reference site. - // Narrowed to IMembershipImport deliberately, so this guard and the self-binding - // exclusion in QuerySelfBinding — which also keys on IMembershipImport — cannot drift - // apart. A rewrite here without the matching exclusion there would reintroduce exactly - // the self-referential match the exclusion exists to prevent. - if (sourcePoco is IMembershipImport) - { - target = membership.MemberElement; - break; - } - - // Elsewhere a Membership target keeps the full path, using the SHORTEST declared name - // per segment (`import SI::kg`, not `SI::kilogram` as qualifiedName would give). - return QueryShortQualifiedName(membership.MemberElement, sourcePoco); - } - - // A NAMESPACE import keeps a SELF-CONTAINED path unless the target is reachable by - // containment: shortest-name resolution would emit a name that only resolves while a - // SIBLING import of the same namespace remains (`import 'provide power'::*` rather than - // `import '3a-Function-based Behavior-1'::'provide power'::*`). - // A MEMBERSHIP import is deliberately exempt. It names ONE element, so the ordinary - // path can shorten it to whatever actually resolves at the reference site and declines - // wherever the name is shadowed or ambiguous — which is what keeps the colliding - // `Engine` / `Transmission` / `vehicle1_c1` qualified in `13a-Model Containment` while - // letting the unambiguous ones match the pilot. The self-referential match that shortening - // would otherwise expose is handled by QuerySelfBinding, not by this guard. - if (sourcePoco is IImport { OwningRelatedElement: { } importOwner } and not IMembershipImport - && !IsReachableByContainment(target, importOwner)) - { - return this.QueryImportPath(target); - } - - var escapedName = target.EscapedName(); - - if (string.IsNullOrWhiteSpace(escapedName)) - { - return target.qualifiedName ?? string.Empty; - } - - // Chain accessors resolve against the preceding segment's type, so the bare simple - // name is always correct. Not memoised: the decision depends on sourcePoco. - if (IsChainAccessor(sourcePoco)) - { - return ResolveChainAccessor(target, escapedName); - } - - var sourceLocalScope = this.GetSourceLocalScope(sourcePoco); - - // The innermost scopes of the chain are SHADOW-ONLY when the reference sits in a FeatureValue - // expression: the two readings of KerML §8.2.3.5.2 disagree about them, so a simple name matched - // there would not resolve to this target under both. See QueryValueExpressionMatchFloor. - var matchFloorScope = QueryValueExpressionMatchFloor(sourcePoco) ?? sourceLocalScope; - - // KerML §8.2.3.5.1: the ONE exception to basic resolution — a Redefinition's redefinedFeature is - // resolved against the general Type of each ownedSpecialization of the owningType, NOT against the - // reference site's local namespace. This is what keeps `:>> fuelCmdPort` short while the ordinary - // local scope has that inherited membership removed (§8.2.3.5.3). - // Skipped when the redefining feature DECLARES the redefined feature's name: the bare form is - // legal there (it resolves in the supertype, not locally) but reads as a self-reference, and the - // pilot always writes `mass :>> Vehicle::mass`. Qualifying is never wrong, so match it. - if (sourcePoco is IRedefinition { RedefiningFeature: { } redefiningFeature } redefinitionContext - && ReferenceEquals(target, redefinitionContext.RedefinedFeature) - && !RedefinerDeclaredNameCollidesWith(redefiningFeature, target) - && !ReferencedFeatureSharesSimpleName(redefiningFeature, target) - && this.QueryRedefinedFeatureScope(redefinitionContext, target) is { } redefinitionScope) - { - sourceLocalScope = redefinitionScope; - - // The exception REPLACES the local Namespace, so the elected general type is itself the - // innermost scope a match may come from — no scope below it to hold shadow-only. - matchFloorScope = redefinitionScope; - } - - // A redefinition's redefining feature — and equally a reference subsetting's referencing - // feature, whose effective name derives FROM the referenced target — is bound in scope - // under the very name being resolved and must not shadow its own target. Excluded from - // the lookup unless its DECLARED name collides, in which case the qualified form is - // required for the round-trip to resolve to the same element (KerML §8.2.3.5). - var localReferencer = sourcePoco switch - { - IRedefinition redefinition when ReferenceEquals(target, redefinition.RedefinedFeature) => redefinition.RedefiningFeature, - IReferenceSubsetting referenceSubsetting when ReferenceEquals(target, referenceSubsetting.ReferencedFeature) => referenceSubsetting.referencingFeature, - _ => null, - }; - - if (localReferencer != null && !RedefinerDeclaredNameCollidesWith(localReferencer, target)) - { - return this.ResolveFresh(target, this.BuildReferenceSite(sourcePoco, sourceLocalScope, matchFloorScope, localReferencer), escapedName); - } - - var referenceSite = this.BuildReferenceSite(sourcePoco, sourceLocalScope, matchFloorScope, localRedefiner: null); - - // The memo key is (target, localScope, matchFloor) — it does NOT capture the SELF BINDING, which - // is derived from sourcePoco. Two sites sharing that triple but differing in self-binding status - // resolve differently, so caching either one would answer for the other. That shape is ordinary: - // `import Foo::Bar;` and a `part p : Bar;` in the SAME package share the triple, yet the import - // must discount its own binding and the usage must not. Whichever ran first would win — emitting - // either a needlessly long name for the usage, or `import Bar;` that resolves only because of - // itself. Self-binding sites are just import declarations and bare re-export memberships, so - // resolving them fresh costs nothing measurable. - if (referenceSite.SelfBinding != null) - { - return this.ResolveFresh(target, referenceSite, escapedName); - } - - var cacheKey = (target.Id, sourceLocalScope?.Id ?? Guid.Empty, matchFloorScope?.Id ?? Guid.Empty); - - if (this.resolvedReferences.TryGetValue(cacheKey, out var cached)) - { - return cached; - } - - var resolved = this.ResolveFresh(target, referenceSite, escapedName); - this.resolvedReferences[cacheKey] = resolved; - return resolved; - } - - /// - /// Determines whether is declared by or by - /// one of its lexically enclosing namespaces — in which case an import may name it relatively - /// (import Usages::*, or a sibling import Definitions::*) without depending on any - /// import. - /// - /// The imported element. - /// The element owning the import declaration. - /// when the target is reachable by containment. - private static bool IsReachableByContainment(IElement target, IElement importOwner) - { - var declaringNamespace = QueryOwningContainer(target); - - if (declaringNamespace == null) - { - return false; - } - - // The declaring namespace need not be an ANCESTOR of the import owner. A SIBLING package under a - // shared enclosing namespace is equally reachable without any import, because containment scoping - // makes that enclosing namespace's members visible by simple name from within it — so the test is - // whether the two chains INTERSECT, not whether one contains the other. Comparing only against the - // import owner's own ancestors made every sibling fall through to the absolute self-contained path - // (`'10a-Analysis'::VehicleDesignModel::Vehicle` from inside `'10a-Analysis'`), which is redundant - // self-prefixing. Elements from a separate resource (a library) share no ancestor and still - // correctly return false. - for (var scope = importOwner; scope != null; scope = QueryEnclosingContainer(scope)) - { - for (var candidate = declaringNamespace; candidate != null; candidate = QueryOwningContainer(candidate)) - { - if (ReferenceEquals(scope, candidate)) - { - return true; - } - } - } - - return false; - } - - /// - /// Returns the namespace enclosing , continuing through the CONTAINMENT - /// tree when owningNamespace is null. - /// - /// The element to climb from; may be . - /// The enclosing namespace, or at a true root. - /// - /// A FilterPackage is an ownedRelatedElement of its Import - /// (NamespaceImport = … | importedNamespace = FilterPackage { ownedRelatedElement += - /// importedNamespace }), and an Import is not a Membership — so the FilterPackage - /// has no owningMembership and therefore no owningNamespace, even though it is plainly - /// nested in the model. Climbing by owningNamespace alone dead-ends there and makes every - /// name inside a filtered import look unreachable, forcing the fully root-qualified form. - /// KerML §7.2.5.3 (normative) defines a root namespace as one that has no OWNER, which the - /// FilterPackage has; the owningNamespace-based phrasing in §8.2.3.4.1 is informative. The - /// pilot climbs containment too (NamespaceUtil.getParentNamespaceOf walks - /// eContainer()). A true root still terminates the climb, because it has neither. - /// - private static INamespace QueryEnclosingContainer(IElement element) - { - if (QueryOwningContainer(element) is { } owningNamespace) - { - return owningNamespace; - } - - var visited = new HashSet(); - - for (var container = QueryOwnerSafe(element); container != null && visited.Add(container); container = QueryOwnerSafe(container)) - { - if (container is INamespace enclosingNamespace) - { - return enclosingNamespace; - } - } - - return null; - } - - /// - /// Returns a SELF-CONTAINED path to , anchored at the outermost named - /// ancestor that binds it directly, so the path never depends on names introduced by imports of the - /// importing namespace. - /// Intermediate owner segments the anchor re-exports are collapsed — - /// '3a-…-1'::Usages::'provide power' becomes '3a-…-1'::'provide power' because - /// '3a-…-1' publicly imports Usages::*. - /// - /// The imported ; must be non-null. - /// The import path, or the target's qualifiedName when no anchor collapses. - private string QueryImportPath(IElement target) - { - var namedAncestors = new List(); - - for (var ancestor = QueryOwningContainer(target); ancestor != null; ancestor = QueryOwningContainer(ancestor)) - { - if (string.IsNullOrWhiteSpace(QueryPreferredEscapedSegment(ancestor))) - { - break; - } - - namedAncestors.Add(ancestor); - } - - var targetSegment = QueryPreferredEscapedSegment(target); - - if (targetSegment == null) - { - return target.qualifiedName ?? string.Empty; - } - - // Outermost first: the widest anchor yields the shortest self-contained path. - namedAncestors.Reverse(); - - for (var anchorIndex = 0; anchorIndex < namedAncestors.Count; anchorIndex++) - { - if (namedAncestors[anchorIndex] is not INamespace anchor - || !this.BindsDirectly(anchor, target, targetSegment) - || !this.IsSuffixVisible(namedAncestors.Take(anchorIndex + 1).Append(target))) - { - continue; - } - - var segments = namedAncestors - .Take(anchorIndex + 1) - .Select(QueryPreferredEscapedSegment) - .Append(targetSegment); - - return string.Join("::", segments); - } - - return namedAncestors.Count == 0 - ? targetSegment - : target.qualifiedName ?? string.Empty; - } - - /// - /// Determines whether 's index binds uniquely - /// to , VISIBLY — i.e. the target is nameable from outside that scope, - /// which is what a qualified path through it requires (KerML §8.2.3.5.3). - /// - /// The candidate anchor namespace. - /// The element being named. - /// The target's escaped simple-name segment. - /// when the scope binds the segment to exactly the target. - private bool BindsDirectly(INamespace scope, IElement target, string segment) - { - var rawName = QueryPreferredRawName(target); - - return !string.IsNullOrWhiteSpace(rawName) - && this.GetSimpleNameIndex(scope).TryGetValue(rawName, out var bucket) - && bucket.Count == 1 - && bucket.Contains(target) - && !string.IsNullOrWhiteSpace(segment) - && this.BindsVisibly(scope, rawName, target); - } - - /// - /// Returns the binding that IS — a non-owning - /// without a name override is the reference being emitted, and its - /// binding does not exist yet at parse time. Its entry must be discounted in its own scope or - /// every such reference would resolve trivially at depth 0. - /// - /// The source POCO at the reference site. - /// The self binding, or when the source is not one. - private static SelfBinding QuerySelfBinding(IElement sourcePoco) - { - // A MembershipImport binds its imported Membership into the importing Namespace, and that - // binding likewise does not exist yet while the import declaration itself is being written. - // Without discounting it, `public import vehicle1_c1::interior::seatBelt;` resolves its own - // target trivially at depth 0 and emits `public import seatBelt;` — a name that resolves - // ONLY because of the very import it is the declaration of, so it does not re-parse. - if (sourcePoco is IMembershipImport { ImportedMembership: { } importedMembership, OwningRelatedElement: INamespace importScope }) - { - return new SelfBinding(importScope, importedMembership); - } - - return sourcePoco is IMembership membership and not IOwningMembership - && string.IsNullOrWhiteSpace(membership.MemberName) - && string.IsNullOrWhiteSpace(membership.MemberShortName) - && membership.OwningRelatedElement is INamespace bindingScope - ? new SelfBinding(bindingScope, membership) - : null; - } - - /// - /// Assembles the for one reference: its scope chain plus the - /// exclusions every probe of it must honour. - /// - /// The POCO bearing the reference. - /// The pre-computed local scope (may be ). - /// The innermost scope a match may come from (may be ). - /// Feature to exclude from every bucket, or . - /// The reference site. - private ReferenceSite BuildReferenceSite(IElement sourcePoco, INamespace sourceLocalScope, INamespace matchFloorScope, IFeature localRedefiner) - { - return new ReferenceSite( - sourcePoco, - this.GetSourceScopeChain(sourcePoco, sourceLocalScope, matchFloorScope), - localRedefiner, - QuerySelfBinding(sourcePoco)); - } - - /// - /// First-time resolution: probes the target's own simple names (short first, per the SST - /// convention), then aliases, then facade re-exports, then owner-chain ancestors as anchors for a - /// partially-qualified suffix, and finally falls back to . - /// - /// The referenced element. - /// The reference site: its scope chain and the exclusions that apply to every probe. - /// The target's escaped raw name. - /// The resolved emission string. - private string ResolveFresh(IElement target, ReferenceSite site, string escapedName) - { - var rawShortName = target.shortName; - string escapedShortName = null; - - if (!string.IsNullOrWhiteSpace(rawShortName)) - { - escapedShortName = Escape(rawShortName); - - if (this.TryResolveSimpleNameAcrossChain(site, target, rawShortName, escapedShortName, accept: null, out var matchedShort)) - { - return matchedShort; - } - } - - var rawName = target.name; - - if (!string.IsNullOrWhiteSpace(rawName) - && this.TryResolveSimpleNameAcrossChain(site, target, rawName, escapedName, accept: null, out var matchedLong)) - { - return matchedLong; - } - - // An alias binds the target under a name it does not carry itself, so the probes above - // can never find it. Preferred over facade/qualified forms — it is how the model names - // the element at this site. - if (this.TryResolveViaAlias(site, target, out var matchedAlias)) - { - return matchedAlias; - } - - // Facade re-export: `ISQ::mass` over `ISQBase::mass` — the SST canonical idiom - // (KerML §8.2.3.5.4 leaves the choice open; both forms parse to the same element). - if (this.TryResolveViaDirectFacade(site.Chain, target, escapedShortName, escapedName, out var matchedFacade)) - { - return matchedFacade; - } - - // Walk owner-chain ancestors outward; the first that resolves uniquely anchors a - // partially-qualified suffix down to the target. The suffix is kept as ELEMENTS: every segment - // of it is resolved by the parser with visible resolution, which has to be verified per hop. - var pathDownToTarget = new Stack(); - - pathDownToTarget.Push(target); - - var descendant = target; - var ancestor = (IElement)QueryOwningContainer(target); - var visitedAncestors = new HashSet(); - - while (ancestor != null && visitedAncestors.Add(ancestor)) - { - var ancestorSegment = QueryPreferredEscapedSegment(ancestor); - - if (string.IsNullOrWhiteSpace(ancestorSegment)) - { - // An unnamed namespace cannot appear inside a QualifiedName; stop and let the - // qualifiedName fallback take over. - break; - } - - var ancestorRawName = QueryPreferredRawName(ancestor); - - if (!string.IsNullOrWhiteSpace(ancestorRawName) - && this.IsSuffixVisible(pathDownToTarget) - && this.TryResolveSimpleNameAcrossChain(site, ancestor, ancestorRawName, ancestorSegment, this.BuildAnchorAcceptance(ancestor, descendant), out var matchedAnchor)) - { - var builder = new StringBuilder(matchedAnchor); - - foreach (var segment in pathDownToTarget) - { - builder.Append("::"); - builder.Append(QueryPreferredEscapedSegment(segment) ?? string.Empty); - } - - return builder.ToString(); - } - - pathDownToTarget.Push(ancestor); - - descendant = ancestor; - ancestor = QueryOwningContainer(ancestor); - } - - var shortQualifiedName = QueryShortQualifiedName(target); - - return string.IsNullOrWhiteSpace(shortQualifiedName) - ? target.qualifiedName ?? string.Empty - : shortQualifiedName; - } - - /// - /// Determines whether every hop WITHIN resolves visibly — the - /// hop from the anchor into the path is checked separately, against whatever the anchor segment - /// actually resolves to (see ). - /// - /// The suffix elements, outermost first. - /// when the suffix re-resolves segment by segment. - private bool IsSuffixVisible(IEnumerable pathDownToTarget) - { - IElement predecessor = null; - - foreach (var segmentElement in pathDownToTarget) - { - if (predecessor != null) - { - var rawName = QueryPreferredRawName(segmentElement); - - if (predecessor is not INamespace predecessorScope - || string.IsNullOrWhiteSpace(rawName) - || !this.BindsVisibly(predecessorScope, rawName, segmentElement)) - { - return false; - } - } - - predecessor = segmentElement; - } - - return true; - } - - /// - /// Returns 's owningNamespace, or when - /// unreachable or the derived property is not implemented. - /// - /// The element whose owner is requested; may be . - /// The owning namespace or . - private static INamespace QueryOwningContainer(IElement element) - { - if (element == null) - { - return null; - } - - try - { - return element.owningNamespace; - } - catch (NotSupportedException) - { - return null; - } - } - - /// - /// Returns 's owner, or when the derived - /// property is not implemented. - /// - /// The element whose owner is requested; must be non-null. - /// The owner or . - private static IElement QueryOwnerSafe(IElement element) - { - try - { - return element.owner; - } - catch (NotSupportedException) - { - return null; - } - } - - /// - /// Returns 's transitive supertypes, or an empty list when the operation is - /// not implemented. - /// - /// The type to query; must be non-null. - /// The supertypes, possibly empty. - private static List QueryAllSupertypesSafe(IType type) - { - try - { - return type.AllSupertypes(); - } - catch (NotSupportedException) - { - return []; - } - } - - /// - /// Escapes per KEBNF: unchanged when it is a basic name, otherwise - /// quoted as an unrestricted name. - /// - /// The raw name; must be non-blank. - /// The escaped form. - private static string Escape(string rawName) - { - return rawName.QueryIsValidBasicName() ? rawName : rawName.ToUnrestrictedName(); - } - - /// - /// Returns the element's preferred raw simple name: shortName when non-blank, otherwise - /// name. May be or blank. - /// - /// The element to name; must be non-null. - /// The preferred raw name. - private static string QueryPreferredRawName(IElement element) - { - return !string.IsNullOrWhiteSpace(element.shortName) ? element.shortName : element.name; - } - - /// - /// Determines whether the local referencer's DECLARED name equals the target's effective name — in - /// which case the bare simple name would re-resolve to the local member and the qualified form is - /// required. An anonymous referencer (no declared names) can never collide. - /// Deliberately DECLARED names only. Widening this to the effective name looks right — an - /// anonymous redefiner inherits its name from the feature it redefines — but the pilot writes the - /// bare form for exactly that shape (ref :>> driveshaft, port :>> fuelCmdPort), so - /// widening over-qualifies 2a / 2c / 3c-2 / 4a. - /// - /// The redefining/referencing feature; must be non-null. - /// The referenced target. - /// on a collision. - private static bool RedefinerDeclaredNameCollidesWith(IFeature localRedefiner, IElement target) - { - var declaredNames = new[] { localRedefiner.DeclaredName, localRedefiner.DeclaredShortName } - .Where(declared => !string.IsNullOrWhiteSpace(declared)); - - return declaredNames.Any(declared => - string.Equals(declared, target.name, StringComparison.Ordinal) - || string.Equals(declared, target.shortName, StringComparison.Ordinal)); - } - - /// - /// Attempts the facade form facade::simpleName, where the facade DIRECTLY re-exports the - /// target's owning namespace (single hop, matching the SST idiom ISQ::mass). Pass 1 prefers - /// a facade resolvable in the source scope chain (innermost first, then ); - /// pass 2 accepts any indexed facade whose name is meaningfully shorter than the owner's, since - /// global resolution (KerML §8.2.3.5.4) still reaches it. - /// - /// The source scope chain (innermost first). - /// The element being referenced. - /// Pre-escaped target shortName (may be ). - /// Pre-escaped target name. - /// On a hit, the emitted facade::simpleName string. - /// when a reachable facade was found. - private bool TryResolveViaDirectFacade(SourceScopeChain chain, IElement target, string escapedShortName, string escapedName, out string matched) - { - matched = null; - - var canonicalOwner = QueryOwningContainer(target); - - if (canonicalOwner == null || !this.directFacadeIndex.TryGetValue(canonicalOwner, out var facades) || facades.Count == 0) - { - return false; - } - - var targetSimpleName = !string.IsNullOrWhiteSpace(escapedShortName) ? escapedShortName : escapedName; - - if (string.IsNullOrWhiteSpace(targetSimpleName)) - { - return false; - } - - INamespace bestFacade = null; - var bestScopeDepth = int.MaxValue; - - // Starts at the match floor: a facade reachable only from a scope the pilot parser never - // consults would emit a name that does not re-resolve there. - for (var scopeDepth = chain.MatchFloor; scopeDepth < chain.Scopes.Count; scopeDepth++) - { - var scope = chain.Scopes[scopeDepth]; - var scopeIndex = this.GetSimpleNameIndex(scope); - - foreach (var facade in facades) - { - var facadeName = QueryPreferredRawName(facade); - - if (string.IsNullOrWhiteSpace(facadeName) || !scopeIndex.TryGetValue(facadeName, out var facadeBucket) || !facadeBucket.Contains(facade)) - { - continue; - } - - if (bestFacade == null - || scopeDepth < bestScopeDepth - || (scopeDepth == bestScopeDepth && CompareFacades(facade, bestFacade) < 0)) - { - bestFacade = facade; - bestScopeDepth = scopeDepth; - } - } - - if (bestFacade != null) - { - break; - } - } - - if (bestFacade == null) - { - // "Meaningfully shorter" = at most 70% of the owner's name length. ISQBase → ISQ - // qualifies; ScalarValues → Collections (a structural parent, not a user-facing - // facade) does not, and the canonical owner is preferred. - var canonicalNameForCompare = QueryPreferredEscapedSegment(canonicalOwner); - var canonicalLength = canonicalNameForCompare?.Length ?? int.MaxValue; - var meaningfulShorterMax = (int)(canonicalLength * 0.7); - - foreach (var facade in facades) - { - if (!this.simpleNameIndices.ContainsKey(facade)) - { - continue; - } - - var facadeNameForCompare = QueryPreferredEscapedSegment(facade); - - if (string.IsNullOrWhiteSpace(facadeNameForCompare) || facadeNameForCompare.Length > meaningfulShorterMax) - { - continue; - } - - if (bestFacade == null || CompareFacades(facade, bestFacade) < 0) - { - bestFacade = facade; - } - } - } - - if (bestFacade == null) - { - return false; - } - - var bestFacadeSegment = QueryPreferredEscapedSegment(bestFacade); - - if (string.IsNullOrWhiteSpace(bestFacadeSegment)) - { - return false; - } - - // The facade re-exports the owner, but the target still has to be VISIBLE through it: a - // non-public member, or one an `import all` pulled in without re-exporting, is not. - var targetRawName = QueryPreferredRawName(target); - - if (string.IsNullOrWhiteSpace(targetRawName) || !this.BindsVisibly(bestFacade, targetRawName, target)) - { - return false; - } - - matched = bestFacadeSegment + "::" + targetSimpleName; - return true; - } - - /// - /// Deterministic ordering for facade candidates at the same scope depth: shorter name first, then - /// ordinal alphabetical. - /// - /// First candidate. - /// Second candidate. - /// Negative if sorts first, positive if right, zero if tied. - private static int CompareFacades(INamespace left, INamespace right) - { - var leftName = QueryPreferredRawName(left) ?? string.Empty; - var rightName = QueryPreferredRawName(right) ?? string.Empty; - - var lengthCompare = leftName.Length.CompareTo(rightName.Length); - - return lengthCompare != 0 ? lengthCompare : string.CompareOrdinal(leftName, rightName); - } - - /// - /// Returns the element's shortest escaped name segment (shortName preferred), or - /// when neither lexical form is available. - /// - /// The element to name; must be non-null. - /// The escaped segment, or . - private static string QueryPreferredEscapedSegment(IElement element) - { - var preferred = QueryPreferredRawName(element); - - return string.IsNullOrWhiteSpace(preferred) ? null : Escape(preferred); - } - - /// - /// Walks 's scope chain innermost-out for a scope binding - /// uniquely to . A scope that binds the name - /// to anything else stops the walk — the parser's resolution would already have claimed the name - /// there. - /// Scopes BELOW the chain's match floor are consulted for shadowing only: a hit there is - /// skipped and the walk continues outward, because the pilot parser does not consult them (see - /// ) and the emitted name has to resolve to the same - /// element under both readings. - /// - /// The reference site: its scope chain and the exclusions that apply to every probe. - /// The referenced element. - /// The simple-name lexical form to probe (may be blank). - /// The escaped form to emit on a hit. - /// Predicate deciding whether a bound element counts as the target, or for reference identity. - /// On a hit, the simple-name string to emit. - /// when the name resolves uniquely to the target. - private bool TryResolveSimpleNameAcrossChain(ReferenceSite site, IElement target, string rawName, string escapedName, Func accept, out string matched) - { - matched = null; - - if (string.IsNullOrWhiteSpace(rawName)) - { - return false; - } - - for (var scopeDepth = 0; scopeDepth < site.Chain.Scopes.Count; scopeDepth++) - { - var resolution = this.ResolveSimpleNameInScope(site.Chain.Scopes[scopeDepth], target, rawName, site.LocalRedefiner, site.SelfBinding, accept); - - if (resolution == SimpleNameResolution.Shadowed) - { - return false; - } - - if (resolution == SimpleNameResolution.Matched && scopeDepth >= site.Chain.MatchFloor) - { - matched = escapedName; - return true; - } - } - - return false; - } - - /// - /// Resolves a chain accessor's simple name: the target's name first, then shortName, - /// then qualifiedName as a last resort. - /// - /// The chain-accessor target element. - /// The pre-computed escaped name form. - /// The escaped simple name. - private static string ResolveChainAccessor(IElement target, string escapedName) - { - if (!string.IsNullOrWhiteSpace(target.name)) - { - return escapedName; - } - - var rawShortName = target.shortName; - - if (!string.IsNullOrWhiteSpace(rawShortName)) - { - return Escape(rawShortName); - } - - return target.qualifiedName ?? string.Empty; - } - - /// - /// Returns the eagerly-built simple-name index for , or - /// when the scope was not reached. - /// - /// The whose index is requested. - /// The simple-name → member-set lookup. - private IReadOnlyDictionary> GetSimpleNameIndex(INamespace scope) - { - return scope == null ? EmptyIndex : this.simpleNameIndices.GetValueOrDefault(scope, EmptyIndex); - } - - /// - /// Returns the cached upward-walk chain for , building it on first - /// encounter from . - /// - /// The source POCO bearing the reference; may be . - /// The pre-computed local scope (may be ). - /// The innermost scope a match may come from (may be ). - /// The cached chain. - private SourceScopeChain GetSourceScopeChain(IElement sourcePoco, INamespace sourceLocalScope, INamespace matchFloorScope) - { - if (sourcePoco == null) - { - return BuildChain(sourceLocalScope ?? this.RootNamespace, matchFloorScope); - } - - if (this.sourceScopeChains.TryGetValue(sourcePoco.Id, out var cached)) - { - return cached; - } - - var chain = BuildChain(sourceLocalScope ?? this.RootNamespace, matchFloorScope); - this.sourceScopeChains[sourcePoco.Id] = chain; - - return chain; - } - - /// - /// Materialises the owningNamespace chain from up to the root, and - /// locates in it. - /// - /// The starting namespace. - /// The innermost scope a match may come from (may be ). - /// The chain. - private static SourceScopeChain BuildChain(INamespace start, INamespace matchFloorScope) - { - var scopes = new List(); - var current = start; - - while (current != null) - { - scopes.Add(current); - current = QueryOwningContainer(current); - } - - return new SourceScopeChain(scopes, QueryMatchFloorDepth(scopes, matchFloorScope)); - } - - /// - /// Returns the depth in at which a match becomes admissible. - /// - /// The chain, innermost first. - /// The floor scope, or for no floor. - /// The depth; 0 admits the whole chain. - /// - /// The floor is reached by a CONTAINMENT climb while the chain is materialised through - /// owningNamespace, so the floor is not guaranteed to sit on the chain — the invocation - /// redirect of can elect a Namespace the chain does not pass - /// through. Falling back to depth 0 there would silently re-admit the very scopes the floor exists - /// to exclude, so the floor's own CONTAINERS are tried next: the innermost of them that IS on the - /// chain sits at or outside the floor, which keeps the constraint at least as strict as intended. - /// Depth 0 is reached only when the two are genuinely unrelated. - /// - private static int QueryMatchFloorDepth(List scopes, INamespace matchFloorScope) - { - var visited = new HashSet(); - - for (var candidate = matchFloorScope; candidate != null && visited.Add(candidate); candidate = QueryParentNamespace(candidate)) - { - var depth = scopes.IndexOf(candidate); - - if (depth >= 0) - { - return depth; - } - } - - return 0; - } - - /// - /// Builds the qualified name of using the SHORTEST declared name per - /// segment, escaped for the parser. Used for import declarations, where the pilot emits short - /// forms (SI::kg) but qualifiedName yields long forms (SI::kilogram). - /// - /// The leaf to qualify; must be non-null. - /// The short-form qualified name, or empty when no segment carries a usable name. - private static string QueryShortQualifiedName(IElement element) - { - return QueryShortQualifiedName(element, sourcePoco: null); - } - - /// - /// As , but stops the walk at the first namespace that - /// also encloses . - /// - /// - /// A namespace that encloses the reference site is already in scope by containment, so naming it - /// explicitly is redundant self-prefixing: from inside '10a-Analysis' the pilot writes - /// import VehicleDesignModel::Vehicle, not - /// import '10a-Analysis'::VehicleDesignModel::Vehicle. An element in a separate resource (a - /// library) shares no enclosing namespace with the source, so its path stays fully qualified — - /// SI::kg is unaffected. - /// - /// The leaf to qualify; must be non-null. - /// The reference site, or to always walk to the root. - /// The short-form qualified name, relative to the nearest shared enclosing namespace. - private static string QueryShortQualifiedName(IElement element, IElement sourcePoco) - { - var enclosingScopes = new List(); - - // An Import is owned as a RELATIONSHIP, so its owningNamespace does not resolve — the chain has - // to be entered through OwningRelatedElement, exactly as IsReachableByContainment does. - var origin = sourcePoco is IImport { OwningRelatedElement: { } importOwner } ? importOwner : sourcePoco; - - for (var scope = origin; scope != null; scope = QueryOwningContainer(scope)) - { - enclosingScopes.Add(scope); - } - - var segments = new Stack(); - var current = element; - - while (current != null) - { - var preferred = QueryPreferredRawName(current); - - if (string.IsNullOrWhiteSpace(preferred)) - { - break; - } - - segments.Push(Escape(preferred)); - - current = QueryOwningContainer(current); - - if (current != null && enclosingScopes.Any(scope => ReferenceEquals(scope, current))) - { - break; - } - } - - return string.Join("::", segments); - } - - /// - /// Resolves the local scope of : the first - /// reached by climbing OwningRelatedElement, owningNamespace, then owner. - /// An anonymous nested namespace (no upward chain of its own) is skipped via owner so the - /// reference site's real enclosing scope is found. Falls back to . - /// - /// The source POCO; may be . - /// The local scope or . - private INamespace GetSourceLocalScope(IElement sourcePoco) - { - if (sourcePoco == null) - { - return this.RootNamespace; - } - - if (QueryContextRelationshipLocalScope(sourcePoco) is { } contextScope) - { - return contextScope; - } - - var visited = new HashSet(); - var current = sourcePoco; - - while (current != null && visited.Add(current)) - { - if (current is IRelationship { OwningRelatedElement: not null } relationship) - { - current = relationship.OwningRelatedElement; - continue; - } - - if (current is INamespace asNamespace) - { - if (QueryOwningContainer(asNamespace) != null || ReferenceEquals(asNamespace, this.RootNamespace)) - { - return asNamespace; - } - - var namespaceOwner = QueryOwnerSafe(asNamespace); - - if (namespaceOwner == null) - { - return asNamespace; - } - - current = namespaceOwner; - continue; - } - - var owningNamespace = QueryOwningContainer(current); - - if (owningNamespace != null) - { - return owningNamespace; - } - - current = QueryOwnerSafe(current); - } - - return this.RootNamespace; - } - - /// - /// Returns the innermost scope of a reference's chain that may produce a MATCH, or - /// when every scope of the chain may. - /// - /// The source POCO at the reference site. - /// The floor scope, or . - /// - /// KerML §8.2.3.5.2 anchors a inside a - /// at the "non-invocation Namespace" — the nearest - /// containing Namespace that is neither an expression nor a parameter of one. For a - /// that is the value-carrying Feature ITSELF, so its inherited members - /// are in scope. The pilot parser (NamespaceUtil.getNonExpressionNamespaceFor) steps one - /// scope FURTHER out whenever the climb passes a FeatureValue, so those inherited members are NOT. - /// The disagreement is observable: part :>> subsystemA = subsystem1; resolves under the - /// spec reading (the redefining Feature inherits the variation's variant Memberships) but is a name - /// resolution ERROR under the pilot's. This floor keeps such scopes in the chain as SHADOW sources - /// while barring them from producing a match, so every name emitted resolves to the same element - /// under both readings. - /// - private static INamespace QueryValueExpressionMatchFloor(IElement sourcePoco) - { - if (sourcePoco is not IMembership sourceMembership) - { - return null; - } - - var subject = sourceMembership; - var scope = QueryExpressionScope(subject); - var visited = new HashSet(); - - while (scope != null - && (subject is IFeatureValue || scope is IInstantiationExpression || scope is IFeatureReferenceExpression)) - { - subject = QueryOwningMembershipSafe(scope); - - if (subject == null || !visited.Add(subject)) - { - break; - } - - scope = QueryExpressionScope(subject); - } - - return scope; - } - - /// - /// Returns the namespace containing , except for a - /// on a parameter of an , whose - /// value expression is resolved against the invocation rather than against the parameter. - /// - /// The membership whose containing scope is requested. - /// The scope, or when the membership has none. - private static INamespace QueryExpressionScope(IMembership membership) - { - var scope = QueryParentNamespace(membership); - - if (scope == null) - { - return null; - } - - return membership is IFeatureValue && QueryOwningContainer(scope) is IInstantiationExpression invocation - ? invocation - : scope; - } - - /// - /// Returns the nearest containing , by - /// CONTAINMENT rather than by the derived owningNamespace — which is null for a Relationship. - /// - /// The element to climb from; may be . - /// The containing namespace, or at the top of the containment tree. - private static INamespace QueryParentNamespace(IElement element) - { - var visited = new HashSet(); - - for (var current = QueryContainer(element); current != null && visited.Add(current); current = QueryContainer(current)) - { - if (current is INamespace containingNamespace) - { - return containingNamespace; - } - } - - return null; - } - - /// - /// Returns the element CONTAINING : the owning related element of a - /// Relationship — whose owner is null, since it is owned as a relationship rather than as a - /// member — and the owner of anything else. - /// - /// The element to climb from; may be . - /// The container, or at the top of the containment tree. - private static IElement QueryContainer(IElement element) - { - return element switch - { - null => null, - IRelationship { OwningRelatedElement: { } owningRelatedElement } => owningRelatedElement, - _ => QueryOwnerSafe(element), - }; - } - - /// - /// Returns 's owningMembership, or when - /// unreachable or the derived property is not implemented. - /// - /// The element whose owning membership is requested; must be non-null. - /// The owning membership or . - private static IMembership QueryOwningMembershipSafe(IElement element) - { - try - { - return element.owningMembership; - } - catch (NotSupportedException) - { - return null; - } - } - - /// - /// Determines whether also REFERENCES a different element that - /// shares a simple name with — the exhibit X :>> Y shape, where the - /// reference and the redefinition name distinct elements under one name. - /// The reference is written bare, so leaving the redefinition bare emits the SAME token twice for - /// two different elements: they stay distinct only for a reader that applies the §8.2.3.5.1 exception. - /// Qualifying the redefinition is correct under either reading, and is what the pilot writes. - /// - /// The feature owning the redefinition. - /// The redefined feature being named. - /// when the qualified form is required to keep the two distinct. - private static bool ReferencedFeatureSharesSimpleName(IFeature redefiningFeature, IElement target) - { - var referencedFeature = redefiningFeature.OwnedRelationship - .OfType() - .Select(referenceSubsetting => referenceSubsetting.ReferencedFeature) - .FirstOrDefault(referenced => referenced != null && !ReferenceEquals(referenced, target)); - - if (referencedFeature == null) - { - return false; - } - - var referencedNames = new[] { referencedFeature.name, referencedFeature.shortName } - .Where(candidate => !string.IsNullOrWhiteSpace(candidate)); - - return referencedNames.Any(candidate => - string.Equals(candidate, target.name, StringComparison.Ordinal) - || string.Equals(candidate, target.shortName, StringComparison.Ordinal)); - } - - /// - /// Returns the scope in which a 's redefinedFeature is resolved per - /// KerML §8.2.3.5.1: the general Type of each ownedSpecialization of the owning feature's - /// owningType, tried in turn until one binds the name. Falls back to the first such general type - /// so the qualified form is still anchored correctly. - /// - /// The redefinition at the reference site. - /// The redefined feature being named. - /// The scope, or when the exception does not apply. - private INamespace QueryRedefinedFeatureScope(IRedefinition redefinition, IElement target) - { - var owningType = redefinition.RedefiningFeature?.owningType; - - if (owningType == null) - { - return null; - } - - List generalScopes; - - try - { - generalScopes = [..owningType.ownedSpecialization - .Select(specialization => specialization.General) - .OfType() - .Where(general => !ReferenceEquals(general, owningType))]; - } - catch (NotSupportedException) - { - return null; - } - - // A model exported without implied Relationships (KerML §8.4.2) omits Specializations the - // abstract syntax requires — a variant Usage specializing its owning variation, for one. Without - // them a redefinition of a member inherited THROUGH such a Specialization cannot shorten and - // degrades to a fully qualified name. Appended last so declared supertypes keep priority. - generalScopes.AddRange(this.impliedRelationshipProvider.GetImpliedSpecializations(owningType) - .Select(specialization => this.TranslateToResolutionGraph(specialization.General)) - .OfType() - .Where(general => !ReferenceEquals(general, owningType) && !generalScopes.Contains(general))); - - if (generalScopes.Count == 0) - { - return null; - } - - var rawName = QueryPreferredRawName(target); - - if (string.IsNullOrWhiteSpace(rawName)) - { - return generalScopes[0]; - } - - // Only a scope that actually binds the name can be the scope the redefinition's own binding - // would have occupied. Electing one that does not — which became reachable once implied - // Specializations joined the candidates — makes the caller treat the name as self-bound there - // and walk past the scope that really holds it, ending in a needlessly qualified name. - return generalScopes.FirstOrDefault(scope => - this.ResolveSimpleNameInScope(scope, target, rawName, localRedefiner: null, selfBinding: null) == SimpleNameResolution.Matched); - } - - /// - /// Determines the local from the KIND of context relationship, per - /// KerML §8.2.3.5.2. Only the kinds whose local scope is NOT simply the nearest enclosing namespace - /// are handled here; everything else falls back to the containment climb. - /// For a the spec anchors resolution at the - /// owningNamespace of the owningType — one level OUT from the owning feature — so the - /// owning feature's own and inherited members are NOT in scope. - /// A whose referencingFeature is an end feature of a - /// anchors at the CONNECTOR's owning namespace. That namespace inherits the - /// referenced name only through IMPLIED Specializations, so this anchoring works only because the - /// simple-name index folds implied generals in — translated into THIS graph first, since the implied - /// layer may be wired against a separate library load whose instances never satisfy reference - /// equality here. See TranslateToResolutionGraph; diagnosis in - /// .team-notes/start-overqualification-diagnosis.md. - /// - /// The context relationship at the reference site. - /// The local scope, or when the generic climb applies. - private static INamespace QueryContextRelationshipLocalScope(IElement sourcePoco) - { - return sourcePoco switch - { - // Connector ends keep the containment climb — see the remark above. This case must precede - // ISpecialization: a ReferenceSubsetting IS a Specialization and would otherwise be - // re-anchored by the general rule below. - IReferenceSubsetting { referencingFeature: { IsEnd: true, owningType: IConnector connector } } => QueryOwningContainer(connector), - ISpecialization specialization => specialization.owningType != null ? QueryOwningContainer(specialization.owningType) : QueryOwningContainer(specialization), - IConjugation conjugation => conjugation.owningType != null ? QueryOwningContainer(conjugation.owningType) : QueryOwningContainer(conjugation), - _ => null - }; - } - - /// - /// What stays fixed while one reference is resolved: the scopes to probe and the two exclusions - /// that apply to every probe of it. Only the name being looked up varies. - /// - private sealed class ReferenceSite - { - /// - /// Initializes a new instance of the class. - /// - /// The POCO bearing the reference. - /// The scopes to probe, innermost first, with the match floor. - /// Feature to exclude from every bucket, or . - /// The binding the reference itself is, or . - internal ReferenceSite(IElement sourcePoco, SourceScopeChain chain, IFeature localRedefiner, SelfBinding selfBinding) - { - this.SourcePoco = sourcePoco; - this.Chain = chain; - this.LocalRedefiner = localRedefiner; - this.SelfBinding = selfBinding; - } - - /// - /// Gets the POCO bearing the reference. - /// - internal IElement SourcePoco { get; } - - /// - /// Gets the scopes to probe, innermost first, with the depth at which a match is admissible. - /// - internal SourceScopeChain Chain { get; } - - /// - /// Gets the Feature excluded from every bucket — the reference's own redefining or - /// referencing Feature, which must not shadow its own target. - /// - internal IFeature LocalRedefiner { get; } - - /// - /// Gets the binding the reference itself is, whose entry does not exist at parse time. - /// - internal SelfBinding SelfBinding { get; } - } - - /// - /// The name binding a reference IS: the Membership being emitted and the scope it binds in. - /// - private sealed class SelfBinding - { - /// - /// Initializes a new instance of the class. - /// - /// The scope the membership binds in. - /// The Membership that IS the reference. - internal SelfBinding(INamespace scope, IMembership membership) - { - this.Scope = scope; - this.Membership = membership; - } - - /// - /// Gets the scope the membership binds in. - /// - internal INamespace Scope { get; } - - /// - /// Gets the Membership that IS the reference, and whose binding therefore does not exist yet - /// at parse time. - /// - internal IMembership Membership { get; } - } - - /// - /// A reference site's scope chain, innermost first, together with the depth at which a MATCH - /// becomes admissible. - /// - private sealed class SourceScopeChain - { - /// - /// Initializes a new instance of the class. - /// - /// The scopes, innermost first. - /// The index of the innermost scope a match may come from. - internal SourceScopeChain(IReadOnlyList scopes, int matchFloor) - { - this.Scopes = scopes; - this.MatchFloor = matchFloor; - } - - /// - /// Gets the scopes of the chain, innermost first. - /// - internal IReadOnlyList Scopes { get; } - - /// - /// Gets the index of the innermost scope a match may come from; scopes below it are consulted - /// for shadowing only (see ). - /// - internal int MatchFloor { get; } - } - - /// - /// Tri-state result of probing one scope for one lexical form. - /// - private enum SimpleNameResolution - { - /// Name not bound in this scope — keep walking outward. - NotBound, - - /// Name bound uniquely to the target — emit the simple name. - Matched, - - /// Name bound to something else — stop; the qualified form is required. - Shadowed, - } - - /// - /// Probes 's index for . Mirrors the parser's - /// local resolution: the local referencer and the self-binding entry are excluded, candidates are - /// reduced to redefinition leaves (KerML §8.2.3.5.3 — at most one Membership per name), and the - /// name matches only when that leaf set is exactly the target. - /// - /// The scope whose index is inspected. - /// The element to look up. - /// The simple-name lexical form to probe; must be non-blank. - /// Feature to exclude from the bucket, or . - /// The binding the reference itself is, or . - /// Predicate deciding whether a bound element counts as the target, or for reference identity. - /// The resolution state. - private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement target, string rawName, IFeature localRedefiner, SelfBinding selfBinding, Func accept = null) - { - var index = this.GetSimpleNameIndex(scope); - - if (!index.TryGetValue(rawName, out var elements)) - { - return SimpleNameResolution.NotBound; - } - - accept ??= candidate => ReferenceEquals(candidate, target); - - // The reference's own binding does not exist at parse time; only OTHER elements bound - // under the name in this scope shadow the target. - if (selfBinding != null && ReferenceEquals(scope, selfBinding.Scope)) - { - var isBoundToOtherElement = elements.Any(element => - !accept(element) && !ReferenceEquals(element, localRedefiner)); - - if (isBoundToOtherElement) - { - return SimpleNameResolution.Shadowed; - } - - // The index is keyed by ELEMENT, so the reference's own entry is indistinguishable from - // one the scope holds anyway — inherited, imported or aliased. Only the former is absent - // at parse time: when another Membership of this scope binds the same name to the same - // element, the parser resolves the simple name here and the bare form is correct. - return this.BindsTargetIndependently(scope, rawName, target, selfBinding.Membership) - ? SimpleNameResolution.Matched - : SimpleNameResolution.NotBound; - } - - var candidates = elements.Where(element => !ReferenceEquals(element, localRedefiner)).ToList(); - - if (candidates.Count == 0) - { - return SimpleNameResolution.NotBound; - } - - if (candidates.Count == 1) - { - return accept(candidates[0]) - ? SimpleNameResolution.Matched - : SimpleNameResolution.Shadowed; - } - - candidates = PreferDirectlyOwnedOverInherited(scope, candidates); - - // Reduce to redefinition leaves: drop every candidate transitively redefined by another. - var shadowed = new HashSet(); - - foreach (var candidate in candidates.OfType()) - { - foreach (var redefined in candidate.AllRedefinedFeatures().Where(redefined => !ReferenceEquals(redefined, candidate))) - { - shadowed.Add(redefined); - } - } - - IElement onlyLeaf = null; - var leafCount = 0; - - foreach (var element in candidates.Where(element => element is not IFeature feature || !shadowed.Contains(feature))) - { - leafCount++; - - if (leafCount > 1) - { - return SimpleNameResolution.Shadowed; - } - - onlyLeaf = element; - } - - return leafCount == 1 && accept(onlyLeaf) - ? SimpleNameResolution.Matched - : SimpleNameResolution.Shadowed; - } - - /// - /// Determines whether binds to - /// through a Membership OTHER than , - /// the reference being emitted. - /// - /// The scope to inspect. - /// The simple-name lexical form. - /// The referenced element. - /// The Membership that IS the reference. - /// when the binding survives without the reference itself. - /// - /// Answers the question the element-keyed index cannot: first start; declares a Membership - /// naming the library Actions::Action::start in a scope that ALREADY inherits that same - /// element through an implied Specialization, so the parser resolves the bare name — while - /// member Foo::bar; in a scope with no other binding for bar does not. - /// Deliberately a live query rather than index provenance: it runs only when the probed scope - /// is the reference's own AND the name is bound there to nothing else, which is rare. It mirrors - /// the sources of and - /// ; a source it fails to cover only costs a longer name, never - /// an invalid one. - /// - private bool BindsTargetIndependently(INamespace scope, string rawName, IElement target, IMembership selfMembership) - { - return this.QueryBindingMemberships(scope, visibleOnly: false) - .Any(membership => !ReferenceEquals(membership, selfMembership) && BindsName(membership, rawName, target)); - } - - /// - /// Determines whether binds to - /// among its VISIBLE Memberships — the test the parser applies to every - /// segment of a qualified name after the first. - /// - /// The scope named by the preceding segment. - /// The segment's simple-name lexical form. - /// The element the segment must name. - /// when the segment resolves visibly to the target. - /// - /// Namespace::resolve resolves the FIRST segment with resolveLocal — the outward climb - /// over owned, imported and inherited Memberships of ANY visibility — but every following segment - /// with resolveVisible, i.e. visibleMemberships(Set{}, false, false), which is public - /// only (KerML §8.2.3.5.3). The simple-name index cannot answer this: it is built with the - /// visibility filter of the path that REACHED the scope, and within the model that admits - /// everything. Emitting A::b for a b that is private in A would produce a name - /// no conformant parser resolves. - /// - private bool BindsVisibly(INamespace scope, string rawName, IElement target) - { - return this.QueryBindingMemberships(scope, visibleOnly: true) - .Any(membership => BindsName(membership, rawName, target)); - } - - /// - /// Enumerates the Memberships that give its name bindings: owned, - /// imported, inherited, and those contributed by implied generals. - /// - /// The scope whose bindings are collected. - /// Whether to keep only the bindings visible OUTSIDE the scope. - /// The Memberships, with duplicates possible. - private List QueryBindingMemberships(INamespace scope, bool visibleOnly) - { - var memberships = new List(QueryOwnedMembershipsSafe(scope).Where(ownedMember => PassesVisibilityFilter(ownedMember, visibleOnly))); - - memberships.AddRange(QueryImportedMembershipsSafe(scope, visibleOnly)); - - if (scope is not IType type) - { - return memberships; - } - - memberships.AddRange(QueryInheritedMembershipsSafe(type).Where(inheritedMember => PassesVisibilityFilter(inheritedMember, visibleOnly))); - - var declaredSupertypes = QueryAllSupertypesSafe(type) - .Where(supertype => !ReferenceEquals(supertype, type)) - .ToList(); - - foreach (var impliedGeneral in this.QueryImpliedGeneralClosure(type, declaredSupertypes)) - { - memberships.AddRange(QueryOwnedMembershipsSafe(impliedGeneral) - .Where(ownedMember => ownedMember.Visibility != VisibilityKind.Private && PassesVisibilityFilter(ownedMember, visibleOnly))); - - memberships.AddRange(QueryInheritedMembershipsSafe(impliedGeneral).Where(inheritedMember => PassesVisibilityFilter(inheritedMember, visibleOnly))); - } - - return memberships; - } - - /// - /// Determines whether binds to - /// , under either lexical form. - /// - /// The Membership to test. - /// The simple-name lexical form. - /// The referenced element. - /// when the membership binds the name to the target. - private static bool BindsName(IMembership membership, string rawName, IElement target) - { - if (membership?.MemberElement == null || !ReferenceEquals(membership.MemberElement, target)) - { - return false; - } - - var (shortName, longName) = QueryMembershipNames(membership, target); - - return string.Equals(shortName, rawName, StringComparison.Ordinal) - || string.Equals(longName, rawName, StringComparison.Ordinal); - } - - /// - /// Returns 's ownedMembership, or an empty list when the derived - /// property is not implemented. - /// - /// The scope to query; must be non-null. - /// The owned memberships, possibly empty. - private static List QueryOwnedMembershipsSafe(INamespace scope) - { - try - { - return scope.ownedMembership; - } - catch (NotSupportedException) - { - return []; - } - } - - /// - /// Returns 's inheritedMembership, or an empty list when the derived - /// property is not implemented. - /// - /// The type to query; must be non-null. - /// The inherited memberships, possibly empty. - private static List QueryInheritedMembershipsSafe(IType type) - { - try - { - return type.inheritedMembership; - } - catch (NotSupportedException) - { - return []; - } - } - - /// - /// Returns the Memberships 's own Imports contribute, mirroring - /// minus its collision filter — a colliding import - /// cannot be the INDEPENDENT binding anyway, since the owned member it collides with is. - /// - /// The importing scope; must be non-null. - /// Whether to keep only PUBLIC imports, the ones that re-export. - /// The imported memberships, possibly empty. - private static List QueryImportedMembershipsSafe(INamespace scope, bool visibleOnly) - { - var imported = new List(); - - try - { - foreach (var ownedImport in scope.ownedImport.Where(ownedImport => PassesVisibilityFilter(ownedImport, visibleOnly))) - { - switch (ownedImport) - { - case IMembershipImport { ImportedMembership: { } importedMembership }: - imported.Add(importedMembership); - break; - case INamespaceImport { ImportedNamespace: not null } namespaceImport: - imported.AddRange(QueryVisibleMemberships(namespaceImport.ImportedNamespace, namespaceImport.IsImportAll, false, [scope])); - break; - } - } - } - catch (NotSupportedException) - { - // ownedImport, or a derivation behind one of the imported namespaces, is not implemented. - } - - return imported; - } - - /// - /// Builds the acceptance predicate for an ancestor ANCHOR in a partially-qualified name: the anchor - /// segment need not resolve to itself, as long as what it resolves to - /// binds the next segment to the same . - /// - /// The owner-chain ancestor being probed as an anchor. - /// The element named by the segment immediately below the anchor. - /// The predicate. - /// - /// A feature that redefines a Type binds that Type's members by inheritance, so it anchors a path - /// through them exactly as the Type does — subsystemA::subsystem1 where subsystemA - /// resolves to the redefining part :>> subsystemA rather than to the variation it redefines. - /// Identity of the anchor is therefore too strong a test; what matters is that the WHOLE path still - /// resolves to the target, which is verified here one segment at a time. - /// - private Func BuildAnchorAcceptance(IElement ancestor, IElement descendant) - { - var descendantRawName = QueryPreferredRawName(descendant); - - // Whatever the anchor segment resolves to is what the parser applies visible resolution to for - // the next segment, so the check runs against the CANDIDATE — for the anchor itself as much as - // for a Feature that redefines it. - return candidate => candidate is INamespace candidateScope - && !string.IsNullOrWhiteSpace(descendantRawName) - && this.BindsVisibly(candidateScope, descendantRawName, descendant) - && (ReferenceEquals(candidate, ancestor) - || this.ResolveSimpleNameInScope(candidateScope, descendant, descendantRawName, localRedefiner: null, selfBinding: null) == SimpleNameResolution.Matched); - } - - /// - /// Narrows to the directly-owned ones when every other candidate is - /// only inherited into — an owned feature shadows a same-named inherited - /// one even when the redefinition is IMPLIED and absent from the XMI (SysML v2 spec, Clause 7.17.2). - /// Applied at resolve time so a redefined feature stays nameable from its redefining declaration. - /// - /// The namespace whose index produced . - /// The candidates bound to the name being resolved. - /// The owned candidates when the preference applies, otherwise . - private static List PreferDirectlyOwnedOverInherited(INamespace scope, List candidates) - { - if (scope is not IType scopeAsType) - { - return candidates; - } - - var supertypes = QueryAllSupertypesSafe(scopeAsType); - var owned = candidates.Where(candidate => IsDirectlyOwnedBy(scope, candidate)).ToList(); - - if (owned.Count == 0 || owned.Count == candidates.Count) - { - return candidates; - } - - var inheritedOnly = candidates - .Where(candidate => !owned.Contains(candidate)) - .All(candidate => candidate is IFeature { owningType: { } declaringType } && supertypes.Contains(declaringType)); - - return inheritedOnly ? owned : candidates; - } - - /// - /// Determines whether is a member element of one of - /// 's own memberships (declared, not imported or inherited). - /// - /// The namespace to test. - /// The candidate member element. - /// when the scope declares the element itself. - private static bool IsDirectlyOwnedBy(INamespace scope, IElement element) - { - try - { - return scope.ownedMembership.Any(membership => ReferenceEquals(membership.MemberElement, element)); - } - catch (NotSupportedException) - { - return false; - } - } - - /// - /// Determines whether is the right-hand side of a chain accessor — - /// a reference the parser resolves against the preceding segment's type instead of the lexical - /// scope, so the bare simple name is the correct emission. - /// - /// The source POCO at the reference site. - /// when the source is a chain accessor. - private static bool IsChainAccessor(IElement sourcePoco) - { - if (sourcePoco is IMembership { OwningRelatedElement: { } membershipOwner } and not IParameterMembership - && EstablishesRelativeNamespace(membershipOwner)) - { - return true; - } - - if (IsFlowFeatureAccessor(sourcePoco)) - { - return true; - } - - if (sourcePoco is not IFeatureChaining { OwningRelatedElement: IFeature chainOwner } chaining) - { - return false; - } - - var siblings = chainOwner.OwnedRelationship.OfType().ToList(); - var index = siblings.IndexOf(chaining); - - if (index > 0) - { - return true; - } - - // The FIRST segment is also a chain accessor when the owning chain Feature is the target - // member of a construct establishing a RELATIVE namespace (see EstablishesRelativeNamespace). - // A chain owned by a Specialization / ReferenceSubsetting keeps lexical resolution. - return chainOwner.OwningRelationship is IMembership { OwningRelatedElement: { } chainMemberOwner } and not IParameterMembership - && EstablishesRelativeNamespace(chainMemberOwner); - } - - /// - /// Determines whether establishes a RELATIVE namespace — a scope taken - /// from a preceding expression's result rather than lexical containment. Mirrors the pilot's - /// NamespaceUtil.getRelativeNamespaceFor: (always) and - /// (only with a target binding). The pilot's additional - /// !getArgument().isEmpty() guard on the expression arm is deliberately NOT mirrored: our - /// derived argument is empty for a FeatureChainExpression (its instantiated type is the - /// library function '.'), so the guard would reject every chain. - /// - /// The element owning the membership at the reference site. - /// when the owner establishes a relative namespace. - private static bool EstablishesRelativeNamespace(IElement owner) - { - return owner switch - { - IFeatureChainExpression => true, - IAssignmentActionUsage assignmentActionUsage => assignmentActionUsage.targetArgument != null, - _ => false, - }; - } - - /// - /// Determines whether is the flow-feature reference of a - /// that carries a subsetting prefix ('generate torque'.engineTorque) - /// — the parser resolves the feature against the prefix's type, like a chain accessor. Without the - /// prefix the flow feature keeps lexical resolution. - /// - /// The source POCO at the reference site. - /// when the source is a prefixed flow-feature accessor. - private static bool IsFlowFeatureAccessor(IElement sourcePoco) - { - if (sourcePoco is not IRedefinition { OwningRelatedElement: IFeature flowFeature }) - { - return false; - } - - return flowFeature.OwningRelationship is IFeatureMembership { OwningRelatedElement: IFlowEnd flowEnd } - && flowEnd.OwnedRelationship.OfType().Any(); - } - - /// - /// Eagerly indexes every namespace reachable from via - /// containment and imports, then the global namespaces (visible memberships only). - /// - /// The root namespace. - /// The full structural cache. - private Dictionary>> BuildSimpleNameIndices(INamespace rootNamespace) - { - var result = new Dictionary>>(); - var pending = new Queue<(INamespace Scope, bool IsGlobal)>(); - var visited = new HashSet(); - - pending.Enqueue((rootNamespace, false)); - - foreach (var globalNamespace in this.globalNamespaces) - { - pending.Enqueue((globalNamespace, true)); - } - - while (pending.Count != 0) - { - var (scope, isGlobal) = pending.Dequeue(); - - if (scope == null || !visited.Add(scope)) - { - continue; - } - - var index = new Dictionary>(StringComparer.Ordinal); - - this.BuildOwnedAndImportedEntries(scope, index, pending, isGlobal); - - if (scope is IType type) - { - BuildInheritedEntries(type, index, pending, isGlobal); - } - - result[scope] = index; - } - - return result; - } - - /// - /// Populates with 's owned memberships and - /// imports; imported namespaces are enqueued for their own indexing. When - /// is set only VISIBLE entries are admitted (KerML §8.2.3.5.2). - /// - /// The namespace whose entries are populated. - /// The destination index. - /// Queue of namespaces yet to be indexed. - /// Whether the scope is reached through the global namespace. - private void BuildOwnedAndImportedEntries(INamespace scope, Dictionary> index, Queue<(INamespace Scope, bool IsGlobal)> pending, bool isGlobal) - { - var ownedMemberships = new List(); - - try - { - foreach (var ownedMember in scope.ownedMembership.Where(ownedMember => PassesVisibilityFilter(ownedMember, isGlobal))) - { - AddMembershipEntry(index, ownedMember, pending, isGlobal); - this.RecordAliasIfDeclared(scope, ownedMember); - ownedMemberships.Add(ownedMember); - } - } - catch (NotSupportedException) - { - // ownedMembership may not be implemented; skip. - } - - // Per Namespace::importedMemberships, an imported membership that has a distinguishability - // collision with an OWNED membership is excluded. Snapshot the owned names first so a homonym - // pulled in by `import Other::*` cannot shadow — or appear to compete with — the owned member. - var ownedNames = new HashSet(index.Keys, StringComparer.Ordinal); - - try - { - foreach (var ownedImport in scope.ownedImport.Where(ownedImport => PassesVisibilityFilter(ownedImport, isGlobal))) - { - switch (ownedImport) - { - case IMembershipImport { ImportedMembership: { } importedMembership } - when !CollidesWithOwnedMembership(importedMembership, ownedNames, ownedMemberships): - - AddMembershipEntry(index, importedMembership, pending, isGlobal); - this.RecordAliasIfDeclared(scope, importedMembership); - - // Enqueue the member's owning namespace so its import chain (e.g. - // SI → ISQ → ISQBase) populates directFacadeIndex. - if (QueryOwningContainer(importedMembership.MemberElement) is { } memberOwner) - { - pending.Enqueue((memberOwner, isGlobal)); - } - - break; - case INamespaceImport { ImportedNamespace: not null } namespaceImport: - { - pending.Enqueue((namespaceImport.ImportedNamespace, isGlobal)); - - // Only a PUBLIC import re-exports what it brings in, so only a public one makes - // the importing namespace a usable facade for it (KerML §8.2.3.5.3). - if (namespaceImport.Visibility == VisibilityKind.Public) - { - this.RecordDirectFacade(namespaceImport.ImportedNamespace, scope); - } - - // Own try/catch so one broken imported namespace does not lose the - // remaining imports of this scope. - try - { - foreach (var importedMember in QueryVisibleMemberships(namespaceImport.ImportedNamespace, namespaceImport.IsImportAll, isGlobal, [scope]) - .Where(importedMember => !CollidesWithOwnedMembership(importedMember, ownedNames, ownedMemberships))) - { - AddMembershipEntry(index, importedMember, pending, isGlobal); - - // An imported alias is reachable by its alias name in the - // IMPORTING scope, so record it against `scope`. - this.RecordAliasIfDeclared(scope, importedMember); - } - } - catch (NotSupportedException) - { - // ownedMembership not implemented for this imported namespace; skip it. - } - - break; - } - } - } - } - catch (NotSupportedException) - { - // ownedImport may not be implemented; skip. - } - } - - /// - /// Enumerates the memberships an import ns::* contributes, following RE-EXPORTS - /// transitively per NamespaceImport::importedMemberships()visibleMemberships(): - /// the owned memberships plus, recursively, whatever the namespace's own PUBLIC imports bring in. - /// terminates import cycles. - /// Per visibleMemberships(excluded, isRecursive, includeAll) the visibility filter is - /// driven by the IMPORT's isImportAllmembershipsOfVisibility(public, …) unless it is - /// set — not by the importing scope. Each re-exported import contributes under its OWN - /// isImportAll. - /// - /// The namespace named by the import. - /// The triggering import's isImportAll: admits non-public memberships. - /// Whether the importing scope is reached through the global namespace, which admits only visible memberships regardless of (KerML §8.2.3.5.2). - /// Namespaces already visited on this import chain. - /// The memberships contributed to the importing scope. - private static IEnumerable QueryVisibleMemberships(INamespace importedNamespace, bool includeAll, bool isGlobal, HashSet excluded) - { - if (importedNamespace == null || !excluded.Add(importedNamespace)) - { - yield break; - } - - var publicOnly = !includeAll || isGlobal; - List ownedMemberships; - - try - { - ownedMemberships = [..importedNamespace.ownedMembership.Where(ownedMember => !publicOnly || ownedMember.Visibility == VisibilityKind.Public)]; - } - catch (NotSupportedException) - { - ownedMemberships = []; - } - - foreach (var ownedMembership in ownedMemberships) - { - yield return ownedMembership; - } - - List reExports; - - try - { - reExports = [..importedNamespace.ownedImport.Where(ownedImport => ownedImport.Visibility == VisibilityKind.Public)]; - } - catch (NotSupportedException) - { - reExports = []; - } - - foreach (var reExport in reExports) - { - switch (reExport) - { - case IMembershipImport { ImportedMembership: { } reExportedMembership }: - yield return reExportedMembership; - break; - - case INamespaceImport { ImportedNamespace: { } reExportedNamespace } reExportedImport: - - foreach (var reExportedMembership in QueryVisibleMemberships(reExportedNamespace, reExportedImport.IsImportAll, isGlobal, excluded)) - { - yield return reExportedMembership; - } - - break; - } - } - } - - /// - /// Records an alias X for Y; binding — a membership whose explicit - /// / differs from - /// the member element's own names. - /// - /// The declaring the membership. - /// The candidate alias ; may be . - private void RecordAliasIfDeclared(INamespace scope, IMembership membership) - { - if (membership is not { MemberElement: { } target }) - { - return; - } - - var aliasNames = new[] { membership.MemberName, membership.MemberShortName } - .Where(aliasName => !string.IsNullOrWhiteSpace(aliasName) - && !string.Equals(aliasName, target.name, StringComparison.Ordinal) - && !string.Equals(aliasName, target.shortName, StringComparison.Ordinal)) - .ToList(); - - if (aliasNames.Count == 0) - { - return; - } - - if (!this.aliasIndex.TryGetValue(scope, out var scopeAliases)) - { - scopeAliases = []; - this.aliasIndex[scope] = scopeAliases; - } - - if (!scopeAliases.TryGetValue(target, out var existingAliases)) - { - existingAliases = []; - scopeAliases[target] = existingAliases; - } - - existingAliases.AddRange(aliasNames.Where(aliasName => !existingAliases.Contains(aliasName))); - } - - /// - /// Attempts to emit an in-scope alias for . Each candidate is validated - /// through the same first-binding-wins scope walk as an ordinary simple name, so the emitted text - /// always round-trips to the same element. - /// - /// The reference site; its source POCO rejects the alias declaration itself. - /// The element being referenced. - /// On a hit, the escaped alias name to emit. - /// when an unambiguous in-scope alias was found. - private bool TryResolveViaAlias(ReferenceSite site, IElement target, out string matched) - { - matched = null; - - var candidateAliasNames = site.Chain.Scopes - .Where(scope => this.aliasIndex.ContainsKey(scope)) - .SelectMany(scope => this.aliasIndex[scope].TryGetValue(target, out var aliasNames) ? aliasNames : Enumerable.Empty()) - .Distinct(StringComparer.Ordinal) - .Where(aliasName => !DeclaresAlias(site.SourcePoco, target, aliasName)); - - foreach (var aliasName in candidateAliasNames) - { - if (this.TryResolveSimpleNameAcrossChain(site, target, aliasName, Escape(aliasName), accept: null, out matched)) - { - return true; - } - } - - return false; - } - - /// - /// Determines whether is the very declaration introducing - /// for — which must emit the target's own - /// name, not the circular alias Torque for Torque;. - /// - /// The reference site's source POCO. - /// The element being referenced. - /// The candidate alias name. - /// when the source is the declaration of this alias. - private static bool DeclaresAlias(IElement sourcePoco, IElement target, string aliasName) - { - return sourcePoco is IMembership sourceMembership - && ReferenceEquals(sourceMembership.MemberElement, target) - && (string.Equals(sourceMembership.MemberName, aliasName, StringComparison.Ordinal) - || string.Equals(sourceMembership.MemberShortName, aliasName, StringComparison.Ordinal)); - } - - /// - /// Records as a direct (single-hop) re-exporter of - /// . - /// - /// The namespace being directly imported. - /// The namespace importing it. - private void RecordDirectFacade(INamespace canonicalOwner, INamespace facade) - { - if (!this.directFacadeIndex.TryGetValue(canonicalOwner, out var facades)) - { - facades = []; - this.directFacadeIndex[canonicalOwner] = facades; - } - - facades.Add(facade); - } - - /// - /// When is set, admits only PUBLIC memberships and imports — the - /// filter both the global namespace and visible resolution apply. The global namespace contains - /// only the visible memberships of other roots, and every segment of a qualified name after the - /// first resolves against the visible memberships of the preceding one (KerML §8.2.3.5.2–.3), so a - /// name bound privately there would not re-parse. Within a local scope everything is visible. - /// - /// The or considered. - /// Whether only bindings visible OUTSIDE the owning scope are admitted. - /// when the relationship may contribute a binding. - private static bool PassesVisibilityFilter(IRelationship relationship, bool publicOnly) - { - if (!publicOnly) - { - return true; - } - - return relationship switch - { - IMembership membership => membership.Visibility == VisibilityKind.Public, - IImport import => import.Visibility == VisibilityKind.Public, - _ => true, - }; - } - - /// - /// Indexes the entries inherited from 's transitive supertypes; namespace - /// supertypes are enqueued as scopes in their own right. - /// - /// Membership indexing delegates to Type::inheritedMembership (KerML §8.3.3.1.10) rather than - /// re-deriving it. An earlier flattened walk applied removeRedefinedFeatures condition 2 at - /// the leaf type only — so a membership an intermediate supertype redefined away still reached the - /// index — and it admitted private supertype members while missing their - /// public/protected imports. Delegating is spec-faithful on all three counts. - /// - /// - /// The delegation had previously been backed out for COST, when inheritedMembership - /// recomputed the transitive closure on every access and took the validation fixture from 17 s to - /// over 4 minutes. The per-query memoisation since added to TypeExtensions removes that - /// blow-up: the same fixture now runs in 40 s against 18 s for the flattened walk, measured - /// back-to-back. That remaining ~2x is the price of spec fidelity, and the corpus output is - /// unchanged by the switch. - /// - /// - /// The type whose inherited memberships are indexed. - /// The destination index. - /// Queue of namespaces yet to be indexed. - /// Whether the owning scope is reached through the global namespace. - private void BuildInheritedEntries(IType type, Dictionary> index, Queue<(INamespace Scope, bool IsGlobal)> pending, bool isGlobal) - { - var inheritableSupertypes = QueryAllSupertypesSafe(type) - .OfType() - .Where(candidate => !ReferenceEquals(candidate, type)) - .ToList(); - - foreach (var supertypeAsNamespace in inheritableSupertypes.OfType()) - { - pending.Enqueue((supertypeAsNamespace, isGlobal)); - } - - List inheritedMemberships; - - try - { - inheritedMemberships = type.inheritedMembership; - } - catch (NotSupportedException) - { - // Resolving inheritance is atomic: a derivation that is unimplemented ANYWHERE in this - // Type's transitive supertype closure costs the whole closure, not just the branch that - // raised. Names that would have resolved through an unaffected supertype then fall back - // to a longer — never an invalid — form. - return; - } - - foreach (var inheritedMember in inheritedMemberships - .Where(inheritedMember => PassesVisibilityFilter(inheritedMember, isGlobal))) - { - AddMembershipEntry(index, inheritedMember, pending, isGlobal); - } - - // Implied Specializations are DETACHED — the layer computing them never touches - // ownedRelationship, so inheritedMembership above cannot see them and a name inherited ONLY - // through an implied general never reaches the index. Resolution then walks past the scope that - // really binds the name and emits a needlessly qualified form. - // - // These entries are LOOKUP-ONLY. The implied general is deliberately NOT enqueued as a scope: - // `pending` drives traversal into further namespaces, and an implied general is a library Type, - // so enqueueing it would drag the model libraries into the walk. Only the members it contributes - // are indexed, so nothing here can reach the writer. - foreach (var impliedGeneral in this.QueryImpliedGeneralClosure(type, inheritableSupertypes)) - { - // `pending` feeds INDEX construction only — it is not the traversal that emits output — so - // indexing the general as a scope in its own right keeps the fix lookup-only while making - // the names it owns resolvable. - pending.Enqueue((impliedGeneral, isGlobal)); - - AddImpliedLookupEntries(type, impliedGeneral, index, isGlobal); - } - } - - /// - /// Translates a Type produced by the implied-relationship layer into this cache's OWN object graph. - /// - /// The general of an implied Specialization, possibly from a foreign graph. - /// The same-Id Type of the resolution graph, or null when the graph does not carry it. - /// - /// The implied layer may be wired against a SEPARATE library load — a full, model-independent one — - /// so the generals it returns can be different POCO instances than the ones this cache resolves - /// against, even for the same library element (same Id). Indexing a foreign instance is - /// worse than useless: it can never equal a resolution target by reference, so it answers - /// Shadowed and STOPS the outward walk that would otherwise have found the local instance. - /// Translating by Id keeps reference equality authoritative everywhere else. A general the - /// resolution graph does not carry is dropped: its members can never be targets here. - private IType TranslateToResolutionGraph(IType impliedGeneral) - { - if (impliedGeneral == null) - { - return null; - } - - this.resolutionGraphElementsById ??= this.BuildResolutionGraphIndex(); - - if (this.resolutionGraphElementsById.TryGetValue(impliedGeneral.Id, out var local)) - { - return local as IType; - } - - // The general may belong to THIS graph already — a hand-coded rule computing against the model - // itself returns resolution-graph instances, which the containment walk below indexes only for - // library namespaces. - return this.IsInResolutionGraph(impliedGeneral) ? impliedGeneral : null; - } - - /// - /// Builds the by-Id index of every Element reachable from the global namespaces. - /// - /// The index. - private Dictionary BuildResolutionGraphIndex() - { - var elementsById = new Dictionary(); - var pendingElements = new Queue(); - - foreach (var globalNamespace in this.globalNamespaces) - { - pendingElements.Enqueue(globalNamespace); - } - - while (pendingElements.Count > 0) - { - var current = pendingElements.Dequeue(); - - // First-wins on a duplicate Id: distinct libraries carry unique Ids, so a collision only - // occurs when the same library is loaded twice, and the copies are then interchangeable. - if (!elementsById.TryAdd(current.Id, current)) - { - continue; - } - - foreach (var owned in current.OwnedRelationship.SelectMany(relationship => relationship.OwnedRelatedElement)) - { - pendingElements.Enqueue(owned); - } - } - - return elementsById; - } - - /// - /// Asserts whether an Element belongs to this cache's own graph, by walking its owners to a known root. - /// - /// The Element to test. - /// True when an owner chain reaches the root or a global namespace. - private bool IsInResolutionGraph(IElement element) - { - for (var current = element; current != null; current = current.owner) - { - if (ReferenceEquals(current, this.RootNamespace) || this.globalNamespaces.Contains(current)) - { - return true; - } - } - - return false; - } - - /// - /// Returns every Type reachable from a Type or its declared supertypes through implied - /// Specializations, transitively. - /// - /// The Type whose implied generals are collected. - /// The declared supertypes, which carry implied Specializations of their own. - /// The implied generals, without duplicates. - private List QueryImpliedGeneralClosure(IType type, List declaredSupertypes) - { - var visited = new HashSet(); - var pendingTypes = new Queue(); - - pendingTypes.Enqueue(type); - - foreach (var declaredSupertype in declaredSupertypes) - { - pendingTypes.Enqueue(declaredSupertype); - } - - var impliedGenerals = new List(); - - while (pendingTypes.Count > 0) - { - var current = pendingTypes.Dequeue(); - - foreach (var general in this.impliedRelationshipProvider.GetImpliedSpecializations(current) - .Select(specialization => this.TranslateToResolutionGraph(specialization.General)) - .Where(general => general != null && visited.Add(general))) - { - impliedGenerals.Add(general); - pendingTypes.Enqueue(general); - } - } - - return impliedGenerals; - } - - /// - /// Indexes, for lookup only, the members an implied general contributes. - /// - /// The Type that reaches the general through an implied Specialization. - /// The Type reached through an implied Specialization. - /// The destination index. - /// Whether the owning scope is reached through the global namespace. - /// - /// The contribution is filtered through Type::removeRedefinedFeatures, because a member the - /// inheriting Type REDEFINES is not one of that Type's Memberships. KerML §8.3.3.1.10 derives - /// inheritedMemberships as removeRedefinedFeatures(inheritableMemberships(…)), whose - /// second condition rejects a Membership one of whose redefined Features is a directly - /// redefinedFeature of an ownedFeature of the Type. - /// Without the filter the redefined library member and the local redefinition both land in the - /// same simple-name bucket, so the name reads as shadowed and a reference that should be bare is - /// emitted fully qualified — first 'provide transportation'::start for first start. - /// The declared-supertype path needs no such filter: it reads inheritedMembership, which - /// has already applied it. Only this path bypasses that derivation, because an implied Specialization - /// is detached and its members have to be gathered directly. - /// - private static void AddImpliedLookupEntries(IType inheritingType, IType impliedGeneral, Dictionary> index, bool isGlobal) - { - if (impliedGeneral == null) - { - return; - } - - // Stricter than the declared-supertype walk on purpose: an implied general is reached without - // any authored relationship, so its private internals are never exposed, even in a non-global - // scope where PassesVisibilityFilter alone would admit them. - var contributed = new List(impliedGeneral.ownedMembership - .Where(ownedMember => ownedMember.Visibility != VisibilityKind.Private)); - - try - { - contributed.AddRange(impliedGeneral.inheritedMembership); - } - catch (NotSupportedException) - { - // Same atomicity as above: an unimplemented derivation costs this general's contribution. - } - - try - { - contributed = inheritingType.RemoveRedefinedFeatures(contributed); - } - catch (NotSupportedException) - { - // Same atomicity again, and it fails safe: an unfiltered redefined member can only lengthen - // a name, never make one resolve to the wrong element. - } - - foreach (var member in contributed.Where(member => PassesVisibilityFilter(member, isGlobal))) - { - AddLookupOnlyEntry(index, member); - } - } - - /// - /// Indexes 's member element under both lexical forms — the - /// membership's explicit name overrides when present, else the element's own names — and enqueues - /// the element when it is itself a namespace. - /// - /// The destination index. - /// The membership whose target is indexed. - /// Queue of namespaces yet to be indexed. - /// Whether the owning scope is reached through the global namespace. - private static void AddMembershipEntry(Dictionary> index, IMembership membership, Queue<(INamespace Scope, bool IsGlobal)> pending, bool isGlobal) - { - if (membership is not { MemberElement: { } target }) - { - return; - } - - var (shortName, longName) = QueryMembershipNames(membership, target); - - AddIndexEntry(index, shortName, target); - AddIndexEntry(index, longName, target); - - if (target is INamespace targetAsNamespace) - { - pending.Enqueue((targetAsNamespace, isGlobal)); - } - } - - /// - /// Indexes a Membership for name lookup WITHOUT extending the namespace traversal. - /// - /// The destination index. - /// The Membership to index. - /// - /// The counterpart of , minus its pending enqueue. Used for - /// members reached through an IMPLIED Specialization: they must be resolvable by name, but the - /// library Types they come from must not be pulled into the walk that produces output. - /// - private static void AddLookupOnlyEntry(Dictionary> index, IMembership membership) - { - if (membership is not { MemberElement: { } target }) - { - return; - } - - var (shortName, longName) = QueryMembershipNames(membership, target); - - AddIndexEntry(index, shortName, target); - AddIndexEntry(index, longName, target); - } - - /// - /// Returns the two lexical forms a membership binds: the membership's explicit name overrides when - /// present, else the member element's own names. - /// - /// The membership doing the binding. - /// The membership's member element. - /// The short and long lexical forms; either may be . - private static (string ShortName, string LongName) QueryMembershipNames(IMembership membership, IElement target) - { - return (!string.IsNullOrWhiteSpace(membership.MemberShortName) ? membership.MemberShortName : target.shortName, - !string.IsNullOrWhiteSpace(membership.MemberName) ? membership.MemberName : target.name); - } - - /// - /// Determines whether an IMPORTED membership has a distinguishability collision with an owned - /// membership of the importing scope, which Namespace::importedMemberships excludes. Without - /// this the homonym competes with the owned member and forces a needlessly qualified reference - /// ('Model'::Definitions instead of Definitions). - /// Delegates to : a shared name is NOT - /// sufficient, since two memberships remain distinguishable when neither member element's metaclass - /// conforms to the other's. The name check is only a cheap pre-filter — differing names always imply - /// distinguishable, so it can never reject a genuine collision. - /// - /// The candidate imported membership. - /// Names bound by the scope's owned memberships, used to pre-filter. - /// The scope's owned memberships, tested on a name hit. - /// when the imported membership must be excluded. - private static bool CollidesWithOwnedMembership(IMembership membership, HashSet ownedNames, List ownedMemberships) - { - if (ownedNames.Count == 0 || membership is not { MemberElement: { } target }) - { - return false; - } - - var (shortName, longName) = QueryMembershipNames(membership, target); - - var sharesName = (!string.IsNullOrWhiteSpace(shortName) && ownedNames.Contains(shortName)) - || (!string.IsNullOrWhiteSpace(longName) && ownedNames.Contains(longName)); - - if (!sharesName) - { - return false; - } - - try - { - return ownedMemberships.Any(owned => !membership.IsDistinguishableFrom(owned)); - } - catch (NotSupportedException) - { - return false; - } - } - - /// - /// Adds to under - /// when the name is non-blank. - /// - /// The destination index. - /// The simple name to use as the index key. - /// The element to record. - private static void AddIndexEntry(Dictionary> index, string simpleName, IElement element) - { - if (string.IsNullOrWhiteSpace(simpleName) || element == null) - { - return; - } - - if (!index.TryGetValue(simpleName, out var bucket)) - { - bucket = []; - index[simpleName] = bucket; - } - - bucket.Add(element); - } - } -} diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs index 8788a416f..37579b8b3 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- // // // Copyright (C) 2022-2026 Starion Group S.A. @@ -20,6 +20,7 @@ namespace SysML2.NET.Serializer.TextualNotation.Writers { + using SysML2.NET.Serializer.TextualNotation.NameResolution; using System; using System.Linq; @@ -392,8 +393,8 @@ internal static void BuildFlowDeclarationHandCoded(IFlow poco, TextualNotationWr /// Dispatches on cursor.Current's runtime type and delegates to the corresponding /// membership builder: Import, VariantUsageMember, StructureUsageMember, /// NonOccurrenceUsageMember, DefinitionMember (for ), - /// AliasMember (for plain ). The cursor is always advanced — when - /// no alternative matches, the default branch simply moves it. + /// AliasMember (for plain ). An element no alternative claims is a + /// dispatch defect and throws instead of being silently dropped. /// Callers invoke this helper from the default: branch of their behavior-specific /// outer switch so that all behavior-specific cases (e.g. InitialNodeMember, /// ActionBehaviorMember, EntryActionMember) are matched first. @@ -438,8 +439,7 @@ internal static void BuildActionOrStateBodyItemNonBehaviorTailHandCoded(IType po break; default: - ownedRelationshipCursor.Move(); - break; + throw new System.InvalidOperationException($"The textual notation writer cannot place the current element ({ownedRelationshipCursor.Current?.GetType().Name}) in the body of '{poco.GetType().Name}' — no NonBehaviorBodyItem alternative claims it, so it would be silently dropped."); } } @@ -646,19 +646,19 @@ internal static void AppendRegularComment(IndentedStringBuilder stringBuilder, s stringBuilder.AppendLine(); } - if (string.IsNullOrWhiteSpace(body)) - { - stringBuilder.AppendLine("/* */"); - return; - } + var lines = (body ?? string.Empty).Replace("\r\n", "\n").Split('\n'); - var lines = body.Split('\n'); + // §8.2.3.3.2 strips the white-space run following `/*` on re-read, so a body that opens with + // white space cannot survive the one-line form and takes the block form below instead. The + // body itself is appended as a literal: the normalising path collapses its consecutive + // spaces, which are content here. + var opensWithWhiteSpace = lines[0].Length > 0 && char.IsWhiteSpace(lines[0][0]); - if (lines.Length == 1) + if (lines.Length == 1 && !opensWithWhiteSpace) { stringBuilder.Append("/* "); - stringBuilder.Append(lines[0].TrimEnd('\r')); - stringBuilder.AppendLine(" */"); + stringBuilder.AppendIndentedLiteral(lines[0] + "*/"); + stringBuilder.AppendLine(); return; } @@ -672,20 +672,26 @@ internal static void AppendRegularComment(IndentedStringBuilder stringBuilder, s { stringBuilder.AppendLine("/*"); - // Only the leading/trailing blank lines are dropped (the body of a block comment always - // ends with one). Interior blank lines are CONTENT and must survive — filtering every - // blank line collapses deliberate paragraph breaks in the comment. - var firstContentIndex = Array.FindIndex(lines, line => !string.IsNullOrWhiteSpace(line)); - var lastContentIndex = Array.FindLastIndex(lines, line => !string.IsNullOrWhiteSpace(line)); + // Every line is emitted verbatim behind a ` * ` prefix, which KerML §8.2.3.3.2 strips back + // off on re-read. Blank lines and trailing white space are body CONTENT — note 2 keeps + // "all line terminators and white space included as entered" — so neither may be trimmed. + // The closing delimiter carries the last line terminator: a body that ends with one puts + // `*/` on its own line, and a body that does not must close on the last content line. + var closesOnOwnLine = lines[^1].Length == 0; + var emittedLines = closesOnOwnLine ? lines[..^1] : lines; - foreach (var rawLine in lines[firstContentIndex..(lastContentIndex + 1)]) + for (var lineIndex = 0; lineIndex < emittedLines.Length; lineIndex++) { - var trimmedLine = rawLine.TrimEnd(); - stringBuilder.AppendIndentedLiteral(trimmedLine.Length == 0 ? " *" : " * " + trimmedLine); - stringBuilder.AppendLine(); + var line = emittedLines[lineIndex]; + stringBuilder.AppendIndentedLiteral(line.Length == 0 ? " *" : " * " + line); + + if (closesOnOwnLine || lineIndex < emittedLines.Length - 1) + { + stringBuilder.AppendLiteralLine(); + } } - stringBuilder.AppendIndentedLiteral(" */"); + stringBuilder.AppendIndentedLiteral(closesOnOwnLine ? " */" : "*/"); stringBuilder.AppendLine(); } finally diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs b/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs index 7fd2d4fc5..71c79087a 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs @@ -296,6 +296,25 @@ internal static bool IsValidForBinaryInterfacePart(this IInterfaceUsage interfac return interfaceUsage?.OwnedRelationship.OfType().Count() == 2; } + /// + /// Asserts that the is valid for the DefaultInterfaceEnd rule. + /// DefaultInterfaceEnd : PortUsage = isEnd ?= 'end' Usage — the interface-body end + /// form WITHOUT the 'port' keyword. The sibling StructureUsageElement alternative + /// reaches PortUsage = OccurrenceUsagePrefix 'port' Usage, whose EndUsagePrefix + /// carries the optional ownedRelationship += OwnedCrossFeatureMember slot. + /// Both forms round-trip to the same metaclass, so the choice is only constrained where one + /// form cannot express the model: OMG SysML v2 spec, Clause 7.14.2 states the port keyword + /// is optional on an interface end "if no owned cross feature is declared on the end". An end + /// carrying one therefore has to take the 'port' form. + /// + /// The + /// The active (unused for this guard) + /// True if the port usage is an end whose cross feature, if any, is not owned + internal static bool IsValidForDefaultInterfaceEnd(this IPortUsage portUsage, TextualNotationWriterContext writerContext) + { + return portUsage is { IsEnd: true } && portUsage.OwnedCrossFeature() == null; + } + /// /// Asserts that the is valid for the NonOccurrenceUsageElement rule. /// NonOccurrenceUsageElement : Usage = DefaultReferenceUsage | ReferenceUsage | @@ -557,10 +576,19 @@ internal static bool IsValidForGuardedTargetSuccession(this ITransitionUsage tra /// /// The /// The active (unused for this guard) - /// True if the membership owns an + /// True if the membership owns an that is not an + /// + /// An is excluded even though it is an : in the + /// else position of IfNode the two alternatives are + /// ( ActionBodyParameterMember | IfNodeParameterMember ), and only the second reproduces the + /// else if chain. Admitting an if-node here makes the first alternative always win, which + /// flattens the chain into a single else block. + /// internal static bool IsValidForActionBodyParameterMember(this IParameterMembership parameterMembership, TextualNotationWriterContext writerContext) { - return parameterMembership?.OwnedRelatedElement.OfType().Any() == true; + return parameterMembership?.OwnedRelatedElement + .OfType() + .Any(actionUsage => actionUsage is not IIfActionUsage) == true; } /// @@ -1132,7 +1160,9 @@ internal static bool IsValidForEntryTransitionMemberRule(this IFeatureMembership ITransitionUsage transitionUsage => transitionUsage.OwnedRelationship .OfType() .Any(transitionFeature => transitionFeature.Kind == SysML2.NET.Core.Systems.States.TransitionFeatureKind.Guard), - ISuccessionAsUsage => true, + // TargetSuccession names its target end and leads with an ANONYMOUS SourceEndMember; a named + // source end marks the standalone `first X then Y` form, whose `first` this rule cannot emit. + ISuccessionAsUsage succession => HasNamedTargetEnd(succession) && !HasNamedSourceEnd(succession), _ => false, }) == true; } @@ -1309,10 +1339,20 @@ internal static bool IsValidForDefinitionBodyItem(this IRelationship relationshi { IImport => true, IVariantMembership => true, + // An EmptyParameterMember is the grammar's placeholder for an omitted slot (see + // IsEmptyParameterMember) — it has no notation of its own, so a body containing only + // one must not open a block to render it as a spurious 'in;'. + IParameterMembership parameterMembership when parameterMembership.IsEmptyParameterMember() => false, + + // The content-free anonymous ReferenceUsage the pilot's transform attaches to every + // TransitionUsage / action node has no notation ANYWHERE — same exclusion as + // IsValidForActionBodyItem, or a target succession's UsageBody opens a block just to + // render it as a bare ';'. IFeatureMembership featureMembership => - featureMembership.IsValidForSourceSuccessionMember(writerContext) - || featureMembership.IsValidForOccurrenceUsageMember(writerContext) - || featureMembership.IsValidForNonOccurrenceUsageMember(writerContext), + !IsContentFreeAnonymousReferenceUsage(featureMembership) + && (featureMembership.IsValidForSourceSuccessionMember(writerContext) + || featureMembership.IsValidForOccurrenceUsageMember(writerContext) + || featureMembership.IsValidForNonOccurrenceUsageMember(writerContext)), IOwningMembership owningMembership => owningMembership.IsValidForDefinitionMember(writerContext), IMembership => true, _ => false, diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationWriterContext.cs b/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationWriterContext.cs index 3d6a54459..4129cc619 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationWriterContext.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationWriterContext.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------- // // // Copyright 2022-2026 Starion Group S.A. @@ -27,6 +27,7 @@ namespace SysML2.NET.Serializer.TextualNotation.Writers using SysML2.NET.Core.POCO.Root.Namespaces; using SysML2.NET.Extensions; using SysML2.NET.Semantics.Implied; + using SysML2.NET.Serializer.TextualNotation.NameResolution; /// /// Provides the serialization context for the textual notation builders. Carries the diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/TypeTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/TypeTextualNotationBuilder.cs index 2be5fbbe1..c524bbe0f 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/TypeTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/TypeTextualNotationBuilder.cs @@ -24,6 +24,7 @@ namespace SysML2.NET.Serializer.TextualNotation.Writers using SysML2.NET.Core.POCO.Core.Features; using SysML2.NET.Core.POCO.Core.Types; + using SysML2.NET.Core.POCO.Kernel.Behaviors; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; using SysML2.NET.Core.POCO.Systems.States; @@ -71,11 +72,7 @@ private static void BuildActionBodyItemHandCoded(IType poco, TextualNotationWrit // ( ownedRelationship += ActionTargetSuccessionMember )* belongs to THIS alternative, // so the trailing loop stays inside the item. - while (ownedRelationshipCursor.Current is IFeatureMembership targetSuccession && targetSuccession.IsValidForActionTargetSuccessionMember(writerContext)) - { - FeatureMembershipTextualNotationBuilder.BuildActionTargetSuccessionMember(targetSuccession, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - } + EmitActionTargetSuccessionRun(membershipForInitialNode.MemberElement as IFeature, ownedRelationshipCursor, writerContext, stringBuilder); break; } @@ -88,14 +85,11 @@ private static void BuildActionBodyItemHandCoded(IType poco, TextualNotationWrit { FeatureMembershipTextualNotationBuilder.BuildSourceSuccessionMember(featureMembershipForSuccession, writerContext, stringBuilder); ownedRelationshipCursor.Move(); - FeatureMembershipTextualNotationBuilder.BuildActionBehaviorMember((IFeatureMembership)ownedRelationshipCursor.Current, writerContext, stringBuilder); + var anchoredActionMember = (IFeatureMembership)ownedRelationshipCursor.Current; + FeatureMembershipTextualNotationBuilder.BuildActionBehaviorMember(anchoredActionMember, writerContext, stringBuilder); ownedRelationshipCursor.Move(); - while (ownedRelationshipCursor.Current is IFeatureMembership targetSuccession && targetSuccession.IsValidForActionTargetSuccessionMember(writerContext)) - { - FeatureMembershipTextualNotationBuilder.BuildActionTargetSuccessionMember(targetSuccession, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - } + EmitActionTargetSuccessionRun(anchoredActionMember.OwnedRelatedElement.OfType().FirstOrDefault(), ownedRelationshipCursor, writerContext, stringBuilder); } else if (nextElement is IFeatureMembership nextForStructure && nextForStructure.IsValidForStructureUsageMember(writerContext)) { @@ -112,25 +106,23 @@ private static void BuildActionBodyItemHandCoded(IType poco, TextualNotationWrit break; } + // Tested BEFORE ActionBehaviorMember: a guard-carrying TransitionUsage IS an + // IActionUsage, so the broader behavior guard would otherwise claim it. + case IFeatureMembership featureMembershipForGuarded when featureMembershipForGuarded.IsValidForGuardedSuccessionMember(writerContext): + FeatureMembershipTextualNotationBuilder.BuildGuardedSuccessionMember(featureMembershipForGuarded, writerContext, stringBuilder); + ownedRelationshipCursor.Move(); + break; + case IFeatureMembership featureMembershipForActionBehavior when featureMembershipForActionBehavior.IsValidForActionBehaviorMember(writerContext): { FeatureMembershipTextualNotationBuilder.BuildActionBehaviorMember(featureMembershipForActionBehavior, writerContext, stringBuilder); ownedRelationshipCursor.Move(); - while (ownedRelationshipCursor.Current is IFeatureMembership targetSuccession && targetSuccession.IsValidForActionTargetSuccessionMember(writerContext)) - { - FeatureMembershipTextualNotationBuilder.BuildActionTargetSuccessionMember(targetSuccession, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - } + EmitActionTargetSuccessionRun(featureMembershipForActionBehavior.OwnedRelatedElement.OfType().FirstOrDefault(), ownedRelationshipCursor, writerContext, stringBuilder); break; } - case IFeatureMembership featureMembershipForGuarded when featureMembershipForGuarded.IsValidForGuardedSuccessionMember(writerContext): - FeatureMembershipTextualNotationBuilder.BuildGuardedSuccessionMember(featureMembershipForGuarded, writerContext, stringBuilder); - ownedRelationshipCursor.Move(); - break; - // NonBehaviorBodyItem cases — shared with StateBodyItem; see Shared helper. default: SharedTextualNotationBuilder.BuildActionOrStateBodyItemNonBehaviorTailHandCoded(poco, writerContext, stringBuilder); @@ -383,6 +375,56 @@ private static void EmitTargetTransitionRun(IFeatureMembership anchorMember, Col } } + /// + /// Emits the ( ownedRelationship += ActionTargetSuccessionMember )* run that may follow an + /// InitialNodeMember or ActionBehaviorMember, using the shorthand forms + /// (if g then t; / else t; / then t;) whose source is implied by the anchor. + /// + /// The feature the run's implied sources must reference; may be . + /// The body cursor, positioned after the anchor member. + /// The for the current write. + /// The accumulating the notation. + /// + /// GuardedTargetSuccession and DefaultTargetSuccession have no notation for the + /// implied source membership NOR for the empty payload parameter, but the model records both, so + /// the transition's own cursor steps past them unemitted — the same Golden-Rule exception as + /// , conditional on + /// confirming position 0 is the implied source. A transition whose source is NOT the anchor must + /// keep the explicit GuardedSuccessionMember form (succession S first A1 if g then t;), + /// so it terminates the run and re-dispatches through BuildActionBodyItemHandCoded. + /// + private static void EmitActionTargetSuccessionRun(IFeature anchorFeature, CollectionCursor ownedRelationshipCursor, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder) + { + while (ownedRelationshipCursor.Current is IFeatureMembership targetSuccession + && targetSuccession.IsValidForActionTargetSuccessionMember(writerContext)) + { + var impliedTransition = QueryImpliedSourceTransition(targetSuccession, anchorFeature); + + if (impliedTransition == null && targetSuccession.OwnedRelatedElement.OfType().Any()) + { + // A guard-carrying transition re-dispatches to the explicit GuardedSuccessionMember + // form. A NON-guarded transition with a foreign source has no explicit production at + // ActionBodyItem level — the grammar cannot express it, so the re-dispatch renders it + // through the broader behavior arm as the least-wrong available notation. + break; + } + + if (impliedTransition != null) + { + var transitionCursor = writerContext.CursorCache.GetOrCreateCursor(impliedTransition.Id, OwnedRelationshipCollection, impliedTransition.OwnedRelationship); + transitionCursor.Move(); + + if (transitionCursor.Current is IParameterMembership parameterMembership && parameterMembership.IsEmptyParameterMember()) + { + transitionCursor.Move(); + } + } + + FeatureMembershipTextualNotationBuilder.BuildActionTargetSuccessionMember(targetSuccession, writerContext, stringBuilder); + ownedRelationshipCursor.Move(); + } + } + /// /// Returns the transition owned by when its source is exactly /// — the condition under which the source may be left implicit. diff --git a/SysML2.NET.Serializer.Xmi/DeSerializer.cs b/SysML2.NET.Serializer.Xmi/DeSerializer.cs index f42482d4c..fe2796cbc 100644 --- a/SysML2.NET.Serializer.Xmi/DeSerializer.cs +++ b/SysML2.NET.Serializer.Xmi/DeSerializer.cs @@ -278,6 +278,8 @@ private async Task ReadAsync(Stream stream, Uri fileLocation, bool i if (Guid.TryParse(namespaceId, out var guid) && this.cache.TryGetData(guid, out var foundData) && foundData is INamespace existingNamespace) { this.logger.LogInformation("Circular dependency spot, Namespace with id {NamespaceId} already exists", namespaceId); + this.cache.RegisterRootNamespace(fileLocation, existingNamespace); + stopWatch.Stop(); return existingNamespace; } @@ -285,6 +287,7 @@ private async Task ReadAsync(Stream stream, Uri fileLocation, bool i var readNamespace = (INamespace)await this.xmiDataReaderFacade.QueryXmiDataAsync(xmlReader, this.cache, fileLocation, this.externalReferenceService, this.loggerFactory, xmlReader.Name, elementOriginMap); elementOriginMap?.RegisterRootNamespace(fileLocation, readNamespace.Id); + this.cache.RegisterRootNamespace(fileLocation, readNamespace); stopWatch.Stop(); this.logger.LogTrace("finished to read xml {DocumentName} in {ElapsedMilliseconds}[ms]", fileInfo.Name, stopWatch.ElapsedMilliseconds); @@ -362,6 +365,8 @@ private INamespace Read(Stream stream, Uri fileLocation, bool isRoot, IXmiElemen if (Guid.TryParse(namespaceId, out var guid) && this.cache.TryGetData(guid, out var foundData) && foundData is INamespace existingNamespace) { this.logger.LogInformation("Circular dependency spot, Namespace with id {NamespaceId} already exists", namespaceId); + this.cache.RegisterRootNamespace(fileLocation, existingNamespace); + stopWatch.Stop(); return existingNamespace; } @@ -369,6 +374,7 @@ private INamespace Read(Stream stream, Uri fileLocation, bool isRoot, IXmiElemen var readNamespace = (INamespace)this.xmiDataReaderFacade.QueryXmiData(xmlReader, this.cache, fileLocation, this.externalReferenceService, this.loggerFactory, xmlReader.Name, elementOriginMap); elementOriginMap?.RegisterRootNamespace(fileLocation, readNamespace.Id); + this.cache.RegisterRootNamespace(fileLocation, readNamespace); stopWatch.Stop(); this.logger.LogTrace("finished to read xml {DocumentName} in {ElapsedMilliseconds}[ms]", fileInfo.Name, stopWatch.ElapsedMilliseconds); diff --git a/SysML2.NET.Serializer.Xmi/IXmiDataCache.cs b/SysML2.NET.Serializer.Xmi/IXmiDataCache.cs index ba3301bc2..debee7a19 100644 --- a/SysML2.NET.Serializer.Xmi/IXmiDataCache.cs +++ b/SysML2.NET.Serializer.Xmi/IXmiDataCache.cs @@ -72,7 +72,14 @@ public interface IXmiDataCache bool TryGetData(Guid dataId, out IData data); /// - /// Queries the cached root s — those without an owningNamespace. + /// Records the root of a resource that has been read. + /// + /// The of the resource, which keys the record so a resource re-entered through a circular reference is registered once + /// The root that resource yielded + void RegisterRootNamespace(Uri fileLocation, INamespace rootNamespace); + + /// + /// Queries the cached root s — one per resource read. /// A single de-serialization transitively reads every referenced resource (a model that /// references one library element pulls in that library and its own dependencies), so the cache /// holds the root of EVERY loaded resource, not only the one returned by diff --git a/SysML2.NET.Serializer.Xmi/XmiDataCache.cs b/SysML2.NET.Serializer.Xmi/XmiDataCache.cs index f86165a15..5a8369622 100644 --- a/SysML2.NET.Serializer.Xmi/XmiDataCache.cs +++ b/SysML2.NET.Serializer.Xmi/XmiDataCache.cs @@ -54,6 +54,11 @@ public class XmiDataCache : IXmiDataCache /// private readonly Dictionary> singleReferenceCache = []; + /// + /// Gets the root of each resource read, keyed by that resource's + /// + private readonly Dictionary rootNamespaces = []; + /// /// Gets the injected used to produce logs /// @@ -163,38 +168,36 @@ public bool TryGetData(Guid dataId, out IData data) } /// - /// Queries the cached root s — those without an owningNamespace. - /// Because a de-serialization transitively reads every referenced resource, this returns the - /// root of EVERY loaded resource, which per KerML 1.0 §8.2.3.5.2 is what constitutes the global - /// available to the model being read. + /// Records the root of a resource that has been read. /// - /// The cached root s; empty when nothing has been read - public IReadOnlyCollection QueryRootNamespaces() + /// The of the resource, which keys the record so a resource re-entered through a circular reference is registered once + /// The root that resource yielded + /// Thrown when or is null. + public void RegisterRootNamespace(Uri fileLocation, INamespace rootNamespace) { - return this.cache.Values - .OfType() - .Where(IsRootNamespace) - .ToList(); + if (fileLocation == null) + { + throw new ArgumentNullException(nameof(fileLocation)); + } + + if (rootNamespace == null) + { + throw new ArgumentNullException(nameof(rootNamespace)); + } + + this.rootNamespaces[fileLocation] = rootNamespace; } /// - /// Determines whether is a root , i.e. it has no - /// owningNamespace. The derived property is not implemented for every metaclass, so a - /// is treated as "cannot be established as a root" rather than - /// being allowed to escape a query over the whole cache. + /// Queries the cached root s — one per resource read. + /// Because a de-serialization transitively reads every referenced resource, this returns the + /// root of EVERY loaded resource, which per KerML 1.0 §8.2.3.5.2 is what constitutes the global + /// available to the model being read. /// - /// The to test - /// True when the candidate has no owning namespace - private static bool IsRootNamespace(INamespace candidate) + /// The cached root s; empty when nothing has been read + public IReadOnlyCollection QueryRootNamespaces() { - try - { - return candidate.owningNamespace == null; - } - catch (NotSupportedException) - { - return false; - } + return this.rootNamespaces.Values.ToList(); } ///