From 50c66966df4193ca1be9ecf582839327d2be3f8f Mon Sep 17 00:00:00 2001 From: atheate Date: Mon, 10 Aug 2026 09:10:11 +0200 Subject: [PATCH 1/2] Validation of the 4a --- .../RuleProcessor.CollectionProcessing.cs | 69 +- .../RuleProcessor.ElementProcessing.cs | 144 +- .../RuleProcessor.PatternHandlers.cs | 294 +--- .../HandleBarHelpers/RuleProcessor.cs | 274 ++-- .../4a-Functional Allocation.sysml | 80 ++ ...ET.Serializer.TextualNotation.Tests.csproj | 6 + .../2a-Parts Interconnection.sysmlx | 1200 ++++++++--------- .../3a-Function-based Behavior-1.sysmlx | 732 +++++----- .../4a-Functional Allocation.sysmlx | 558 ++++++++ .../TextualNotationValidationTestFixture.cs | 1 + .../Writers/NameResolutionCache.cs | 1134 +++++++--------- .../Writers/SharedTextualNotationBuilder.cs | 5 +- SysML2.NET/Extend/FeatureExtensions.cs | 4 +- 13 files changed, 2283 insertions(+), 2218 deletions(-) create mode 100644 SysML2.NET.Serializer.TextualNotation.Tests/Expected/04-Functional Allocation/4a-Functional Allocation.sysml create mode 100644 SysML2.NET.Serializer.TextualNotation.Tests/Validation/04-Functional Allocation/4a-Functional Allocation.sysmlx diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs index 8a7b68b0..f90aaf35 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs @@ -145,16 +145,7 @@ private void EmitCollectionNonTerminalLoop(EncodedTextWriter writer, IClass umlC { writer.WriteSafeString($"while ({whileCondition}){Environment.NewLine}"); writer.WriteSafeString($"{{{Environment.NewLine}"); - - var previousCaller = ruleGenerationContext.CallerRule; - var previousName = ruleGenerationContext.CurrentVariableName; - ruleGenerationContext.CallerRule = nonTerminalElement; - - this.ProcessAlternatives(writer, umlClass, referencedRule.Alternatives, ruleGenerationContext); - - ruleGenerationContext.CallerRule = previousCaller; - ruleGenerationContext.CurrentVariableName = previousName; - + this.ProcessReferencedRuleAlternatives(writer, umlClass, nonTerminalElement, referencedRule, ruleGenerationContext); writer.WriteSafeString($"{Environment.NewLine}}}{Environment.NewLine}"); } @@ -370,15 +361,10 @@ private static string ResolveBuilderCall(IClass umlClass, NonTerminalElement non } /// - /// Generates an inline condition expression for an optional non-terminal reference. - /// For enumerable properties whose consumption type can be resolved from the referenced - /// rule's += assignments, emits a cursor-typed guard - /// ({prop}Cursor.Current is T) — declaring the cursor immediately into - /// when not already present in - /// . The cursor principle aligns the - /// caller's guard with the position the called rule will actually consume from. When the - /// type cannot be resolved or is not the top-level - /// poco, the legacy {var}.{Property}.Count != 0 form is preserved. + /// Generates the inline condition for an optional non-terminal reference: a cursor-typed guard + /// ({prop}Cursor.Current is T, declaring the cursor when needed) so the guard matches the + /// position the called rule will actually consume from; falls back to the legacy + /// .Count != 0 form when the type cannot be resolved. /// /// The used to emit any required cursor declarations /// The optional non-terminal's referenced rule @@ -419,17 +405,13 @@ private static string GenerateInlineOptionalCondition(EncodedTextWriter writer, } else if (referencedRule.QueryAllReferencedCollectionAssignments(propertyName, ruleGenerationContext.AllRules).Count > 0) { - // The referenced rule has += consumptions of this property but the cursor - // types could not be resolved — fall back to the legacy collection-level - // non-empty check rather than skip the clause entirely. + // += consumptions exist but their types could not be resolved — fall back to + // the legacy non-empty check rather than skip the clause. conditionParts.Add($"{variableName}.{umlPropertyName}.Count != 0"); } - // No += consumptions exist for this property anywhere in the referenced rule - // tree (the property name reached the result set via a scalar `=` reference, - // typically pulled in by a transitive non-terminal walk). Skipping the clause - // tightens the guard to reflect what the called rule will actually consume - // and avoids evaluating derived properties that the caller never reads. + // With no += consumptions at all (scalar `=` reach only), the clause is skipped so + // the guard reflects what the called rule actually consumes. } else { @@ -441,11 +423,9 @@ private static string GenerateInlineOptionalCondition(EncodedTextWriter writer, } /// - /// Resolves the runtime types the referenced rule's += assignments consume from the - /// supplied and, when all are resolvable AND - /// is poco, returns a cursor-typed boolean - /// expression (declaring or reusing a cursor as needed). Returns null to signal - /// that the caller should fall back to the legacy .Count != 0 form. + /// Returns a cursor-typed boolean expression for the referenced rule's += consumptions of + /// (declaring or reusing a cursor as needed), or null when the + /// caller should fall back to the legacy .Count != 0 form. /// /// The used to emit a cursor declaration when one is required /// The optional non-terminal's referenced rule @@ -469,11 +449,8 @@ private static string TryBuildCursorTypedCheck(EncodedTextWriter writer, Textual return null; } - // Each entry: (WrapperType, InnerType). InnerType is non-null when the assignment's - // referenced rule is a "thin owning wrapper" (target=OwningMembership wrapping a - // single ownedRelatedElement += T); the inner type T then provides the narrowing - // discriminator that distinguishes the wrapper from sibling OwningMembership - // subtypes (e.g. EndFeatureMembership) that share the cursor's collection. + // 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) @@ -507,13 +484,9 @@ private static string TryBuildCursorTypedCheck(EncodedTextWriter writer, Textual } /// - /// Builds a single cursor-typed boolean check. When is - /// supplied the check narrows from a bare cursor.Current is Wrapper to - /// (cursor.Current is Wrapper owningMembershipN && owningMembershipN.OwnedRelatedElement.OfType<Inner>().Any()) - /// so the discriminator is precise enough to distinguish a thin owning wrapper from - /// sibling subtypes of the same wrapper class. The pattern-variable suffix is drawn from - /// so multiple narrowed - /// checks in the same generated method do not collide on the C# scope (CS0136). + /// Builds a single cursor-typed check; with it narrows to the + /// wrapped element type. The pattern-variable suffix comes from + /// to avoid CS0136 collisions. /// /// The cursor variable name in scope at the emission site. /// The fully-qualified wrapper type name. @@ -534,12 +507,8 @@ private static string BuildTypeCheck(string cursorVariableName, string wrapperTy } /// - /// Returns the cursor variable name for , reusing an - /// already-declared cursor when one is present in - /// and emitting a new declaration into - /// otherwise. The new declaration is registered in - /// so subsequent host-rule elements - /// targeting the same property reuse it. + /// Returns the cursor variable name for , reusing an existing cursor + /// or emitting and registering a new declaration. /// /// The that receives the cursor declaration line when emitted /// The property whose cursor is needed diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs index db018394..1fe71838 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs @@ -105,13 +105,8 @@ internal void ProcessRuleElement(EncodedTextWriter writer, IClass umlClass, Rule } } - // A repeated group followed by a MANDATORY consumption of the same element type - // from the same cursor — the `( X )+ X` shape of e.g. - // FeatureChainPrefix = ( ownedRelationship += OwnedFeatureChaining '.' )+ - // ownedRelationship += OwnedFeatureChaining '.' - // — must leave one element for that trailing consumption. Without the - // reservation the loop eats every element and the mandatory 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, or the + // tail emits its terminals against an exhausted cursor (`a.b.` became `a.b..`). var reservationGuard = ResolveTrailingConsumptionReservation(cursorToUse, umlClass, ruleGenerationContext); if (groupTypeGuard.StartsWith("__FULL_GUARD__")) @@ -258,16 +253,10 @@ internal void ProcessRuleElement(EncodedTextWriter writer, IClass umlClass, Rule } /// - /// Builds the extra while clause that reserves one element for a MANDATORY consumption - /// following a repeated group, or an empty string when no reservation is needed. - /// KEBNF rules of the shape ( prop += X )+ prop += XFeatureChainPrefix being - /// the canonical case — consume from a single shared cursor. Emitted naively the loop is greedy: it - /// takes every element, and the mandatory trailing assignment then emits its terminals with nothing - /// left to consume, duplicating them. The guard cursor.GetNext(1) is T stops the loop one - /// element short, which is exactly the arity the grammar asks for. - /// Applies only when the element immediately following the group in the SAME alternative is a - /// non-optional += assignment drawing on the same cursor and the same sub-rule; any other - /// successor consumes different elements and needs no reservation. + /// Builds the extra while clause reserving one element for a MANDATORY trailing consumption + /// after a repeated group (the ( prop += X )+ prop += X shape, e.g. + /// FeatureChainPrefix) — without it the greedy loop exhausts the cursor and the tail + /// duplicates its terminals. Empty when the successor is not a same-cursor += assignment. /// /// The cursor the group consumes from. /// The class hosting the current rule (provides the UML cache). @@ -323,12 +312,9 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass var previousCaller = ruleGenerationContext.CallerRule; ruleGenerationContext.CallerRule = assignmentElement; - // Route the cursor Move() through PendingCursorMove so that ProcessNonTerminalElement - // emits it INSIDE the type-discrimination block — Move() then fires only when the - // runtime cast actually matches (cursor advances only on real += consumption, - // honouring the Move() ↔ += Golden Rule). When the assignment is inside a collection - // group `(...)*` / `(...)+`, the loop body's own emitter handles the move; when it is - // part of a multi-alternative dispatch, the dispatcher handles the move. + // 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 }; @@ -390,11 +376,8 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass { if (assignmentElement.Value is NonTerminalElement { Name: "REGULAR_COMMENT" }) { - // Documentation rule (`doc /* … */`) surrounds the comment with - // blank lines so doc blocks are visually separated from their - // owning members. Every other rule that assigns a REGULAR_COMMENT - // body (currently only `Comment`) renders adjacent to its - // neighbouring statements per the SST convention. + // Only the Documentation rule separates its comment with blank lines; + // Comment renders adjacent per the SST convention. var surroundWithBlankLines = string.Equals(ruleGenerationContext.NamedElementToGenerate?.Name, "Documentation", StringComparison.Ordinal); writer.WriteSafeString($"SharedTextualNotationBuilder.AppendRegularComment(stringBuilder, poco.{targetPropertyName}, surroundWithBlankLines: {(surroundWithBlankLines ? "true" : "false")});"); } @@ -404,10 +387,8 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass } else if (string.Equals(targetPropertyName, "Operator", StringComparison.Ordinal)) { - // Operator tokens (binary, unary, conditional) need a trailing - // space to separate them from the next operand. Matches the - // convention used by the operator-switch dispatch path - // (RuleProcessor.PatternHandlers.cs:94-95). + // Operator tokens need a trailing space before the next operand, matching + // the operator-switch dispatch path in RuleProcessor.PatternHandlers.cs. writer.WriteSafeString($"stringBuilder.Append(poco.{targetPropertyName});{Environment.NewLine}"); writer.WriteSafeString("stringBuilder.Append(' ');"); } @@ -422,13 +403,9 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass { if (!isPartOfMultipleAlternative && assignmentElement.Container is not GroupElement { IsOptional: true }) { - // KEBNF `Prop ?= 'literal'` — emit the literal when the runtime - // value is truthy, but suppress it for concrete subtypes whose - // metamodel default already equals the literal-trigger value. - // For those subtypes the keyword is structurally redundant and - // the canonical idiomatic source omits it (see e.g. SysML - // `attribute X` rather than `ref attribute X` because - // AttributeUsage's `isReference` default is `true`). + // `Prop ?= 'literal'` — suppress the keyword for subtypes whose metamodel + // default already equals the trigger value (e.g. `attribute X`, not + // `ref attribute X`). var exclusionTypes = targetProperty.QuerySubclassesWithMatchingDefault(umlClass, "true"); var exclusionClause = exclusionTypes.Count == 0 ? string.Empty @@ -463,13 +440,9 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass var previousCaller = ruleGenerationContext.CallerRule; ruleGenerationContext.CallerRule = nonTerminalElement; - // Thin `[QualifiedName]` wrapper inlining: when the referenced rule's body is - // just `[QualifiedName]` (e.g. FeatureReference, InstantiatedTypeReference) the - // generated `Build{Wrapper}` method receives the target POCO as both target AND - // source for name resolution, which loses the reference site. Inline the - // AppendQualifiedName call here with the OUTER `poco` as the source context so - // imports declared in the source's enclosing namespace can resolve to the - // short / unqualified name. + // Thin `[QualifiedName]` wrappers are inlined with the OUTER `poco` as the + // resolution source — the generated Build{Wrapper} would lose the reference + // site (see IsThinQualifiedNameWrapperRule). var referencedRule = ruleGenerationContext.FindRule(nonTerminalElement.Name); if (IsThinQualifiedNameWrapperRule(referencedRule)) @@ -482,15 +455,9 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass break; } - // Polymorphic `ownedMemberFeature` access on IFeatureMembership: the runtime - // POCO may also be an IParameterMembership (the form used to model operands - // of every InvocationExpression / OperatorExpression per KerML §8.2.5.8.2 - // Notes 1-2 — see Resources/KerML-textual-bnf.kebnf:1176-1178). In that - // shape the operand expression lives under ownedMemberFeature → FeatureValue - // → value rather than directly under ownedMemberFeature. Route the access - // through SharedTextualNotationBuilder.QueryEffectiveOwnedMemberFeature - // which transparently normalises both runtime shapes into a single feature - // reference downstream code can type-test as before. + // On an IParameterMembership the operand lives under ownedMemberFeature → + // FeatureValue → value (KerML §8.2.5.8.2 Notes 1-2), so route the access + // through QueryEffectiveOwnedMemberFeature, which normalises both shapes. if (string.Equals(targetProperty.Name, "ownedMemberFeature", StringComparison.Ordinal) && QueryIsAssignableToFeatureMembership(umlClass)) { @@ -548,11 +515,8 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass } else { - // The grammar's assignment property does not resolve against the target - // metamodel class (e.g. the OMG kebnf carries a one-off `ownedFeatureMember` - // vs. metamodel `ownedMemberFeature` typo for `OwnedExpressionMember`). - // Delegate to the HandCoded sibling per the documented convention rather - // than emitting a name-collision-prone `Build{Property}(poco, …)` call. + // The grammar's property does not resolve against the metamodel class (e.g. the kebnf's + // `ownedFeatureMember` typo) — delegate to the HandCoded sibling. var handCodedRuleName = assignmentElement.TextualNotationRule?.RuleName ?? "Unknown"; EmitHandCodedFallback(writer, handCodedRuleName, ruleGenerationContext); } @@ -649,13 +613,7 @@ internal void ProcessNonTerminalElement(EncodedTextWriter writer, IClass umlClas } else { - var previousCaller = ruleGenerationContext.CallerRule; - ruleGenerationContext.CallerRule = nonTerminalElement; - var previousName = ruleGenerationContext.CurrentVariableName; - - this.ProcessAlternatives(writer, umlClass, referencedRule?.Alternatives, ruleGenerationContext, isPartOfMultipleAlternative); - ruleGenerationContext.CallerRule = previousCaller; - ruleGenerationContext.CurrentVariableName = previousName; + this.ProcessReferencedRuleAlternatives(writer, umlClass, nonTerminalElement, referencedRule, ruleGenerationContext, isPartOfMultipleAlternative); } } else @@ -666,13 +624,7 @@ internal void ProcessNonTerminalElement(EncodedTextWriter writer, IClass umlClas } else { - var previousCaller = ruleGenerationContext.CallerRule; - ruleGenerationContext.CallerRule = nonTerminalElement; - var previousName = ruleGenerationContext.CurrentVariableName; - - this.ProcessAlternatives(writer, umlClass, referencedRule?.Alternatives, ruleGenerationContext, isPartOfMultipleAlternative); - ruleGenerationContext.CallerRule = previousCaller; - ruleGenerationContext.CurrentVariableName = previousName; + this.ProcessReferencedRuleAlternatives(writer, umlClass, nonTerminalElement, referencedRule, ruleGenerationContext, isPartOfMultipleAlternative); } } } @@ -702,6 +654,27 @@ internal void ProcessNonTerminalElement(EncodedTextWriter writer, IClass umlClas } } + /// + /// Recurses into 's alternatives with + /// as the caller rule, restoring the caller rule and current + /// variable name afterwards. + /// + /// The used to write output + /// The related + /// The referencing the rule + /// The referenced ; may be + /// The current + /// Whether this is part of a multi-alternative context + private void ProcessReferencedRuleAlternatives(EncodedTextWriter writer, IClass umlClass, NonTerminalElement callerElement, TextualNotationRule referencedRule, RuleGenerationContext ruleGenerationContext, bool isPartOfMultipleAlternative = false) + { + var previousCaller = ruleGenerationContext.CallerRule; + var previousName = ruleGenerationContext.CurrentVariableName; + ruleGenerationContext.CallerRule = callerElement; + this.ProcessAlternatives(writer, umlClass, referencedRule?.Alternatives, ruleGenerationContext, isPartOfMultipleAlternative); + ruleGenerationContext.CallerRule = previousCaller; + ruleGenerationContext.CurrentVariableName = previousName; + } + /// /// Declares a single cursor for an enumerable assignment property if not already declared. /// @@ -786,9 +759,7 @@ private static void EmitSharedNoTargetRuleCall(EncodedTextWriter writer, IClass /// /// Returns when IS-A - /// FeatureMembership — used to gate the polymorphic ownedMemberFeature - /// access path that normalises pure IFeatureMembership and - /// IParameterMembership runtime shapes. + /// FeatureMembership — gates the polymorphic ownedMemberFeature access path. /// /// The under test. /// if the class IS-A FeatureMembership. @@ -804,20 +775,11 @@ private static bool QueryIsAssignableToFeatureMembership(IClass umlClass) } /// - /// Returns when is a "thin - /// [QualifiedName] wrapper" — i.e. its body is a single alternative containing a - /// single element that is a [QualifiedName] resolution. Examples in the KerML - /// grammar (Resources/KerML-textual-bnf.kebnf): FeatureReference : Feature = - /// [QualifiedName] (line 1201) and InstantiatedTypeReference : Type = - /// [QualifiedName] (line 1229). - /// - /// When a caller-rule's assignment-element references such a wrapper as its value, the - /// generated Build{Wrapper} method receives the target as both target AND source - /// for name resolution, which loses the syntactic reference site (the caller's - /// poco) needed to honour imports declared in the source's enclosing scope chain. - /// The codegen inlines the AppendQualifiedName call at the caller's emission point - /// instead so the OUTER poco serves as the resolution source. - /// + /// Returns when 's body is a single + /// [QualifiedName] resolution (e.g. FeatureReference, + /// InstantiatedTypeReference). Such wrappers are inlined at the caller so the OUTER + /// poco serves as the name-resolution source — the generated Build{Wrapper} would + /// receive the target as both target and source, losing the reference site. /// /// The under test; may be . /// if the rule is a thin [QualifiedName] wrapper. diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs index ccd32558..d5ae8e67 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs @@ -222,14 +222,9 @@ private bool TryHandleEmptyVsNonEmptyMembership(EncodedTextWriter writer, IClass var typeName = emptyTarget.QueryFullyQualifiedTypeName(); var cursorVarName = cursor.CursorVariableName; - // An "Empty" wrapper rule usually still ASSIGNS its collection — e.g. - // EmptyParameterMember : ParameterMembership = ownedRelatedElement += EmptyUsage, where - // EmptyUsage : ReferenceUsage = {} contributes an element that simply emits nothing. For - // such rules the collection is never empty at runtime, so a Count-based discriminator can - // never select the empty branch (WhileLoopNode always emitted 'while', never 'loop'). - // Discriminate on the WRAPPED element type instead whenever the two branches wrap - // different classes: the non-empty branch is the one that actually carries its payload - // (an OwnedExpression), and the empty branch is the degenerate fallback. + // 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. var wrappedNonEmptyTypeName = QueryWrappedElementTypeName(nonEmptyBranch.NonTerminal, umlClass, ruleGenerationContext); var wrappedEmptyTypeName = QueryWrappedElementTypeName(emptyBranch.NonTerminal, umlClass, ruleGenerationContext); @@ -263,11 +258,9 @@ private bool TryHandleEmptyVsNonEmptyMembership(EncodedTextWriter writer, IClass } /// - /// Resolves the fully-qualified runtime type name of the element that a single-assignment - /// wrapper rule (e.g. ExpressionParameterMember : ParameterMembership = ownedRelatedElement += OwnedExpression) - /// puts into its collection — here IExpression. Returns when the - /// referenced rule does not have exactly one += assignment of a non-terminal, or when the - /// wrapped rule's target class cannot be resolved. + /// Resolves the runtime type a single-assignment wrapper rule puts into its collection + /// (e.g. IExpression for ExpressionParameterMember), or + /// when the rule does not have exactly one resolvable += non-terminal assignment. /// /// The naming the wrapper rule. /// The class hosting the current rule (provides the UML cache). @@ -694,25 +687,10 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer, } } - // Subtype-overlap guard synthesis. - // - // After the existing IsValidFor pass, every duplicate group still has at most - // one unguarded member that becomes the bare `case I{Target}:` fall-through. - // That fall-through is only safe when no SIBLING alternative may dispatch a - // subtype of I{Target} — otherwise the unguarded case greedily swallows the - // subtype before it can reach the dispatcher that handles it (e.g. the - // OperatorExpression group's unguarded ExtentExpression case swallowing - // FeatureChainExpression before it can reach the sibling PrimaryExpression - // alternative). - // - // Detection: the group has subtype overlap if any other alternative in the - // dispatch targets a class that is a SUPERTYPE of this group's target — that - // sibling's dispatcher may then handle subtypes of this group's target inside - // its own switch. - // - // When detected, synthesise a `when` guard for the would-be-default member - // from the rule's parsed body (only parsed assignments contribute; non-parsing - // `{ … }` is ignored per GRAMMAR.md). + // 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. foreach (var duplicateGroup in duplicateClasses) { var stillUnguarded = duplicateGroup.Value @@ -754,35 +732,12 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer, } } - // Self-default inheritance-ambiguity guard synthesis. - // - // The criterion: emit guards only when one of the switch's alternatives - // has the same UML class as the rule's `NamedElementToGenerate`. That is - // the "self-default" shape — the rule uses its own target class as one - // alternative and that alternative becomes the default catch-all arm - // for inline/non-named subclass forms. The canonical example is - // FeatureElement: `FeatureElement : Feature = Feature | Step | Expression | …` - // where `Feature` is both an alternative and the rule's target. An - // inline IOperatorExpression (subclass of IExpression with no declared - // name) reaching the switch would match `case IExpression` and be - // mis-rendered; a property-derived guard like `DeclaredName != null` - // makes that arm decline the match and fall through to `BuildFeature`. - // - // When the rule does NOT exhibit this self-default pattern - // (BuildAnnotatingElement, BuildDefinitionElement, BuildNonFeatureElement - // — `NamedElementToGenerate` is `Element`/`AnnotatingElement` but no - // alternative targets that class), the most-derived-first ordering - // alone is sufficient: every legitimate runtime metaclass maps to its - // own arm. No guards are emitted, regardless of any speculative - // metamodel inheritance overlap. - // - // For each non-default alternative under a self-default rule, run the - // depth-aware synthesizer on the called rule's parsed body. Every - // synthesized clause (with property-mismatch and scalar-on-`+=` filters - // already applied) becomes part of the arm's `when` predicate, joined - // by `&&`. Shared clauses across siblings are harmless — they filter - // out subclass forms uniformly while runtime metaclass + most-derived - // ordering still routes named instances to the correct concrete arm. + // 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. var generatingClassForSelfDefault = ruleGenerationContext.NamedElementToGenerate as IClass; var isSelfDefault = generatingClassForSelfDefault != null && mappedNonTerminalElements.Any(element => element.UmlClass == generatingClassForSelfDefault); @@ -1123,23 +1078,16 @@ private static void EmitCompoundPocoTypeBranch(EncodedTextWriter writer, IClass } /// - /// Synthesises a when-clause guard template for a duplicate-group member that - /// would otherwise be emitted as the unguarded case I{Target}: fall-through, by - /// walking the rule's parsed body and emitting one predicate per - /// . Non-parsing { prop = X } assignments - /// () are intentionally ignored — they are - /// write-only side effects of parsing per SysML2.NET.CodeGenerator/GRAMMAR.md - /// and must not influence dispatch. + /// Synthesises a when-guard template (with {0} as the case-variable placeholder) + /// from the rule's parsed body, one predicate per . Non-parsing + /// { prop = X } assignments are ignored per GRAMMAR.md. Returns null when the body + /// carries no usable parsed assignments. /// /// The whose body to inspect /// The duplicate group's target /// The used to resolve referenced rule targets /// All available rules for NonTerminal resolution - /// - /// A template (with {0} as the - /// case variable name placeholder) ready for insertion into whenGuards, or - /// null when the rule body carries no usable parsed assignments. - /// + /// The guard template, or null. private static string SynthesiseGuardFromRuleBody(TextualNotationRule rule, IClass targetClass, IXmiElementCache cache, IReadOnlyList allRules, int maxDepth = 0) { if (rule == null || targetClass == null) @@ -1153,24 +1101,10 @@ private static string SynthesiseGuardFromRuleBody(TextualNotationRule rule, ICla } /// - /// Determines whether is itself a type-dispatcher — i.e. - /// a rule whose body is exclusively a union of single-NonTerminal alternatives - /// (Subclass1 | Subclass2 | …) that delegate to per-subclass builders. - /// - /// Used by the inheritance-ambiguity pass to suppress guard emission on a switch - /// arm whose called rule already routes subclasses internally. For example, the - /// LiteralExpression rule body is - /// LiteralBoolean | LiteralInteger | LiteralRational | LiteralString | LiteralInfinity - /// — any subclass that reaches a - /// case ILiteralExpression arm is correctly dispatched to its concrete - /// builder by BuildLiteralExpression's inner switch. Contrast with the - /// Expression rule body - /// FeaturePrefix 'expr' FeatureDeclaration ValuePart? FunctionBody: a - /// concrete construction rule that only renders the base form, so a subclass - /// (e.g. ) reaching a case IExpression - /// arm would be mis-rendered as a standalone Expression declaration and DOES - /// need a guard. - /// + /// Determines whether 's body is exclusively a union of + /// single-NonTerminal alternatives (Subclass1 | Subclass2 | …). Such a rule routes + /// subclasses internally (e.g. LiteralExpression), so its switch arm needs no guard — + /// unlike a concrete construction rule that renders only the base form. /// /// The rule to test, or null /// true when is a multi-alternative type-dispatcher @@ -1187,17 +1121,15 @@ private static bool IsTypeDispatcherRule(TextualNotationRule rule) } /// - /// Runs the structural-predicate walk for and returns the raw list - /// of synthesised clauses, before they are joined with &&. Centralises the - /// per-walk seeding of (visited-rules) and the cursor-state - /// bookkeeping. + /// Runs the structural-predicate walk for and returns the raw clause + /// list, seeding the per-walk visited-rules set and cursor-state bookkeeping. /// /// The entry to walk. - /// The dispatch-time target (the alternative's UML class). + /// The dispatch-time target . /// The for resolving NonTerminal RHS targets. /// All available rules for NonTerminal resolution. - /// Recursion budget: 0 = shallow (current/legacy behavior), N>0 = recurse up to NonTerminal references deep. - /// The per-clause list (unjoined) in source-order. + /// Recursion budget: 0 = shallow, N>0 = recurse N references deep. + /// The per-clause list (unjoined) in source order. private static List CollectGuardClausesForRule(TextualNotationRule rule, IClass targetClass, IXmiElementCache cache, IReadOnlyList allRules, int maxDepth) { var targetProperties = targetClass.QueryAllProperties(); @@ -1228,18 +1160,9 @@ private static List CollectGuardClausesForRule(TextualNotationRule rule, } /// - /// Combines per-grammar-alternative clause lists into a single OR-disjunction - /// string, respecting grammar semantics: a rule body of the shape - /// alt1 | alt2 | … means "exactly one of these paths is taken at parse - /// time" → the corresponding dispatch-time predicate is the disjunction of the - /// per-alternative AND-conjunctions. - /// - /// Each non-empty alternative's clause list is joined by && - /// (wrapped in parens when it has 2+ clauses to keep operator precedence - /// explicit). Empty alternatives are dropped. Identical alternatives collapse - /// to a single clause. When 2+ distinct alternatives remain, the overall result - /// is wrapped in parens and joined by ||. - /// + /// 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. /// /// One list of synthesized clauses per grammar alternative. /// A single combined predicate string, or null when every alternative is empty. @@ -1277,24 +1200,8 @@ private static string CombineAlternativesAsOr(List> perAlternativeC /// The used to resolve referenced rule targets /// All available rules for NonTerminal resolution /// Accumulator into which non-null clauses are appended in source order - /// - /// Tracks whether a cursor predicate has already been emitted for an - /// ownedRelationship += assignment in this rule. Only the first such - /// assignment yields a cursor clause; subsequent ones run after the cursor has - /// advanced and cannot be expressed at dispatch time. - /// - /// - /// Tracks whether any element walked so far may have advanced the dispatch cursor - /// (the ownedRelationship cursor) at runtime — either via a direct - /// ownedRelationship += earlier in the body or via a NonTerminalElement - /// reference whose target rule may consume ownedRelationship internally. When - /// this becomes true, no subsequent ownedRelationship += … can yield a cursor - /// predicate at dispatch time because cursor position 0 no longer corresponds to that - /// assignment's target. Rules whose first += sits behind a - /// NonTerminal?/NonTerminal* prefix (e.g. IndividualDefinition's - /// trailing ownedRelationship += EmptyMultiplicityMember) therefore correctly - /// produce no cursor clause and fall back to leaving the rule as the unguarded default. - /// + /// Whether an ownedRelationship cursor predicate was already emitted — only the first assignment can be expressed at dispatch time + /// Whether a prior element may have advanced the dispatch cursor, invalidating position-0 predicates for the rest of the walk private static void CollectGuardClauses(IEnumerable elements, IEnumerable targetProperties, IXmiElementCache cache, IReadOnlyList allRules, List clauses, ref bool firstCursorEmitted, ref bool cursorMayHaveAdvanced, HashSet visitedRules, int remainingDepth) { foreach (var element in elements) @@ -1303,11 +1210,8 @@ private static void CollectGuardClauses(IEnumerable elements, IEnum { case AssignmentElement assignment: { - // Skip optional assignments (`?` / `*` suffix on the assignment - // itself). Their target property may legitimately be unset on a - // valid runtime POCO, so an emitted `&&`-joined predicate would - // wrongly reject those instances at dispatch time. Only mandatory - // assignments (no suffix or `+` suffix) yield guard clauses. + // Optional assignments may legitimately be unset on a valid POCO — only + // mandatory ones yield guard clauses. if (assignment.IsOptional) { break; @@ -1331,18 +1235,9 @@ private static void CollectGuardClauses(IEnumerable elements, IEnum case NonTerminalElement nonTerminal: { - // Deep walk: if there is still depth budget and the referenced rule - // has not yet been visited on this walk, descend into its body so - // structural predicates further down the rule chain (e.g. - // FeatureDeclaration → FeatureIdentification → declaredName = NAME) - // are surfaced as additional clauses. Per-alternative OR-combine: a - // referenced rule with multiple alternatives means exactly one path - // fires at parse-time, so the dispatch-time predicate must be the - // disjunction of the per-alternative AND-conjunctions (not their - // conjunction). Once any NonTerminal is walked, the dispatch cursor - // may have advanced (we cannot statically replay the entire builder), - // so cursor-predicate emission is suppressed for the rest of this - // walk via cursorMayHaveAdvanced = true. + // Deep walk while depth budget lasts: descend into unvisited referenced rules, + // OR-combining per-alternative clauses. Any walked NonTerminal may advance the + // dispatch cursor, so later cursor predicates are suppressed. if (remainingDepth > 0) { var referencedRule = allRules.SingleOrDefault(rule => rule.RuleName == nonTerminal.Name); @@ -1371,36 +1266,23 @@ private static void CollectGuardClauses(IEnumerable elements, IEnum } } - // Shallow fallback: depth budget exhausted, rule not found, or - // already visited. A NonTerminal reference may advance the dispatch - // cursor at runtime, so conservatively suppress any later cursor - // predicates. + // Depth exhausted, rule not found, or already visited — conservatively + // suppress later cursor predicates. cursorMayHaveAdvanced = true; break; } case GroupElement group: { - // Skip optional groups (`?` and `*` suffix). Assignments inside an - // optional group are not guaranteed to fire at parse-time and - // therefore the associated property may legitimately be unset on a - // valid runtime POCO. Emitting them as mandatory `&&` clauses would - // wrongly reject those POCOs at dispatch time. Only walk groups - // that are guaranteed to execute: a group with no suffix or with - // `+` (one-or-more — at least one iteration is mandatory). + // Optional groups may not fire at parse time — only walk groups guaranteed to + // execute (no suffix or `+`). if (group.IsOptional) { break; } - // Per-alternative walk + OR-combine: each `group.Alternatives` - // entry represents an "or-branch" of the group (`(a | b | c)`). - // At runtime exactly one branch is taken, so the dispatch-time - // predicate is the disjunction of per-branch AND-conjunctions. - // Cursor-state (`firstCursorEmitted`, `cursorMayHaveAdvanced`) is - // shared across branches because any branch may consume the - // dispatch cursor; once that happens for any branch we must - // conservatively suppress subsequent cursor predicates. + // OR-combine per branch; cursor state is shared across branches since any + // branch may consume the dispatch cursor. var perGroupAlternativeClauses = new List>(); foreach (var groupAlternative in group.Alternatives) @@ -1453,24 +1335,13 @@ private static string TryBuildClauseForAssignment(AssignmentElement assignment, if (matchingProperty == null) { - // The assignment targets a property that does not exist on the outer - // alternative's class. This can happen when the depth-aware walk descends - // into a NonTerminal whose target metaclass is unrelated to the outer's - // class (e.g. walking AnnotatingElement's switch dispatches into Comment - // / Documentation rules that assign `body`, `locale`, `language` which - // are NOT on the outer IAnnotatingElement). Emitting `{0}.{Prop}` would - // produce a compile error since the case variable lacks that member. - // Skip the clause; the outer guard remains correct without it. + // The deep walk can cross into rules whose properties do not exist on the OUTER class + // (e.g. Comment's `body`) — emitting `{0}.{Prop}` would not compile. Skip the clause. return null; } - // Drop `+=` clauses whose target property is not actually a collection on the - // outer class. The grammar's `+=` annotation tells the parser to append to a - // collection in the called rule's target metaclass — but when the depth-aware - // walk crosses a NonTerminal boundary, the OUTER class may expose the same - // property name as a scalar (e.g. `ISubsetting.Specific` is a single IFeature, - // not an IFeature collection). Emitting `{0}.Specific.OfType<…>().Any()` would - // fail to compile against the scalar property. + // Likewise a `+=` property may be a SCALAR on the outer class (e.g. ISubsetting.Specific); + // an OfType<…>().Any() clause would not compile there. if (assignment.Operator == "+=" && !matchingProperty.QueryIsEnumerable()) { return null; @@ -1490,14 +1361,8 @@ private static string TryBuildClauseForAssignment(AssignmentElement assignment, } /// - /// Determines whether can hold null in C#. True for - /// reference-typed properties (POCO interfaces, ) and nullable - /// value-typed properties (e.g. int?, VisibilityKind?); false for - /// non-nullable value types (mandatory enums or primitives with multiplicity [1]). - /// - /// Used by to suppress tautological - /// != null guards on properties that the runtime POCO cannot leave unset. - /// + /// Determines whether can hold null in C# — used to suppress + /// tautological != null guards on non-nullable value types. /// /// The resolved against the outer alternative class. /// true if a runtime instance of the property could be null. @@ -1509,17 +1374,9 @@ private static bool IsPropertyNullableInCSharp(IProperty property) } /// - /// Translates a parsed scalar = assignment into its corresponding - /// when-clause predicate. Terminal-literal RHS becomes an equality check; - /// [QualifiedName] RHS becomes a non-null check; NonTerminal RHS narrows to - /// the referenced rule's target metaclass when known. - /// - /// When the matching property is a non-nullable value type (mandatory enum / - /// primitive — multiplicity [1] in UML), != null and is I… - /// predicates would be a tautology (always-true) or a compile error (value type - /// vs reference type), so they are silently dropped. Only the terminal-equality - /// branch ({0}.{Prop} == "literal") survives for non-nullable scalars. - /// + /// Translates a scalar = assignment into a when predicate: literal RHS → equality, + /// [QualifiedName] → non-null, NonTerminal → type narrowing. Predicates that would be + /// tautological or uncompilable on non-nullable value types are dropped. /// /// The with = /// The on the outer alternative class that this assignment binds — consulted for C# nullability. @@ -1532,13 +1389,9 @@ private static string BuildScalarAssignmentClause(AssignmentElement assignment, switch (assignment.Value) { case TerminalElement terminal when !string.IsNullOrEmpty(terminal.Value): - // Terminal-literal equality is meaningful for any property type. return $"{{0}}.{propertyAccessor} == \"{terminal.Value}\""; case ValueLiteralElement valueLiteral when valueLiteral.QueryIsQualifiedName(): - // [QualifiedName] RHS → non-null check; only meaningful when the C# - // property can be null. Non-nullable value types (multiplicity [1] - // enums / primitives) would yield a tautology — drop the clause. return IsPropertyNullableInCSharp(matchingProperty) ? $"{{0}}.{propertyAccessor} != null" : null; @@ -1549,17 +1402,12 @@ private static string BuildScalarAssignmentClause(AssignmentElement assignment, if (rhsTargetClass != null) { - // `is I{Rhs}` requires a reference-typed property. For non-nullable - // value-typed properties (mandatory enums / primitives) the cast - // would not compile, so drop the clause. + // `is I{Rhs}` needs a reference-typed property to compile. return matchingProperty.QueryIsReferenceType() ? $"{{0}}.{propertyAccessor} is {rhsTargetClass.QueryFullyQualifiedTypeName()}" : null; } - // Fallback when the NonTerminal does not resolve to an IClass (e.g. - // its target is an enum literal or unresolved name) — emit `!= null` - // only when the property can actually hold null. return IsPropertyNullableInCSharp(matchingProperty) ? $"{{0}}.{propertyAccessor} != null" : null; @@ -1571,32 +1419,16 @@ private static string BuildScalarAssignmentClause(AssignmentElement assignment, } /// - /// Translates a parsed collection += assignment into a cursor-based - /// when-clause predicate. The clause inspects the first element of the - /// target collection via the project's standard cursor pattern - /// (writerContext.CursorCache.GetOrCreateCursor(…).Current is …) — this - /// keeps the dispatch-time guards consistent with how the rest of the textual - /// notation builders consume containment collections. - /// - /// For ownedRelationship += the predicate is additionally gated by the - /// outer walk's tracker: once any - /// prior ownedRelationship += or a NonTerminal reference may have - /// advanced the dispatch cursor past position 0, no further cursor predicate - /// is emitted (it would check a stale position). For other collections that - /// concern does not apply — each collection has its own independent cursor. - /// + /// Translates a collection += assignment into a cursor-based when predicate + /// (GetOrCreateCursor(…).Current is …). For ownedRelationship the predicate is + /// suppressed once the dispatch cursor may have advanced past position 0. /// - /// The with += - /// The C# collection-property name resolved via + /// The with operator += + /// The resolved C# collection-property name /// The for resolving NonTerminal RHS targets /// All available rules for NonTerminal resolution - /// Tracks whether the first ownedRelationship cursor predicate has been emitted yet for this walk. - /// - /// When true, the dispatch (ownedRelationship) cursor may have advanced - /// past position 0 at runtime due to a prior ownedRelationship += or a - /// preceding NonTerminal reference whose target rule may consume the - /// cursor. Suppresses ownedRelationship cursor predicate emission. - /// + /// Whether the first ownedRelationship cursor predicate was already emitted + /// When true, suppresses ownedRelationship cursor predicates (stale position) /// A template clause, or null when no useful clause can be synthesised. private static string BuildCollectionAssignmentClause(AssignmentElement assignment, string propertyAccessor, IXmiElementCache cache, IReadOnlyList allRules, ref bool firstCursorEmitted, bool cursorMayHaveAdvanced) { diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs index e2215b7c..af7a9ee8 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs @@ -73,15 +73,36 @@ internal void ProcessAlternatives(EncodedTextWriter writer, IClass umlClass, IRe /// The whose elements are emitted /// The current internal void EmitAlternativeBody(EncodedTextWriter writer, IClass umlClass, Alternatives alternative, RuleGenerationContext ruleGenerationContext) + { + this.EmitElements(writer, umlClass, alternative.Elements, ruleGenerationContext); + } + + /// + /// Emits in order while maintaining the sibling/index context, + /// optionally restoring after each element. + /// + /// The used to write output + /// The current + /// The elements to emit + /// The current + /// Whether to restore the caller rule after each element + /// Whether this is part of a multi-alternative context + private void EmitElements(EncodedTextWriter writer, IClass umlClass, IReadOnlyList elements, RuleGenerationContext ruleGenerationContext, bool restoreCallerPerElement = false, bool isPartOfMultipleAlternative = false) { var previousSiblings = ruleGenerationContext.CurrentSiblingElements; var previousIndex = ruleGenerationContext.CurrentElementIndex; - ruleGenerationContext.CurrentSiblingElements = alternative.Elements; + ruleGenerationContext.CurrentSiblingElements = elements; - for (var elementIndex = 0; elementIndex < alternative.Elements.Count; elementIndex++) + for (var elementIndex = 0; elementIndex < elements.Count; elementIndex++) { ruleGenerationContext.CurrentElementIndex = elementIndex; - this.ProcessRuleElement(writer, umlClass, alternative.Elements[elementIndex], ruleGenerationContext); + var previousCaller = ruleGenerationContext.CallerRule; + this.ProcessRuleElement(writer, umlClass, elements[elementIndex], ruleGenerationContext, isPartOfMultipleAlternative); + + if (restoreCallerPerElement) + { + ruleGenerationContext.CallerRule = previousCaller; + } } ruleGenerationContext.CurrentSiblingElements = previousSiblings; @@ -116,17 +137,13 @@ internal void DeclareAllRequiredCursors(EncodedTextWriter writer, IClass umlClas } /// - /// Emits a Build{ruleName}HandCoded(variable, writerContext, stringBuilder); fallback call. - /// When is true, the call is only emitted if it has not - /// already been emitted for the same rule name in the current generation scope. + /// Emits a Build{ruleName}HandCoded(…) fallback call, optionally deduplicated per + /// generation scope. /// /// The used to write output /// The grammar rule name used to form the method name /// The current - /// - /// When true, suppress duplicate emissions via - /// - /// + /// When true, suppress duplicate emissions for the same rule name private static void EmitHandCodedFallback(EncodedTextWriter writer, string ruleName, RuleGenerationContext ruleGenerationContext, bool deduplicate = false) { if (deduplicate && !ruleGenerationContext.EmittedHandCodedCalls.Add(ruleName)) @@ -138,21 +155,10 @@ private static void EmitHandCodedFallback(EncodedTextWriter writer, string ruleN } /// - /// Collects, in the order they will be consumed at runtime, the += - /// items that target . - /// The collected items are: first the += assignments declared inside the optional - /// group itself (), then the += - /// assignments declared by the parent alternative AFTER - /// (). - /// - /// The tail walk stops as soon as it encounters a sibling whose own contribution to the - /// target cursor is not statically determinable (an optional or collection - /// that nests a += on the same property, or a - /// that could indirectly consume the cursor). When the - /// tail offset is uncertain, only the optional group's own consumptions are returned — - /// callers can then emit a guard that type-checks the optional positions without making - /// claims about the tail. - /// + /// Collects, in runtime consumption order, the += assignments targeting + /// : the optional group's own, then the parent + /// alternative's tail. The tail walk stops at the first sibling whose cursor contribution is + /// not statically determinable, so callers never guard positions they cannot prove. /// /// The optional group's own elements /// The parent alternative's elements @@ -209,10 +215,8 @@ when string.Equals(cursorAssignment.Property, targetPropertyName, StringComparis } /// - /// Determines whether the supplied (recursively) contains - /// any += targeting . - /// Used by to decide whether a sibling group - /// could shift the runtime offset of subsequent cursor consumptions. + /// Determines recursively whether contains a += assignment + /// targeting (i.e. could shift the runtime cursor offset). /// /// The to inspect /// The property name whose consumption is detected @@ -239,11 +243,8 @@ when string.Equals(cursorAssignment.Property, targetPropertyName, StringComparis } /// - /// Resolves the fully-qualified runtime type name (e.g. - /// SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership) that an - /// 's consumed cursor element is expected to satisfy. - /// Returns null when the assignment's value is not a - /// or when the referenced rule has no resolvable target class. + /// Resolves the fully-qualified runtime type the assignment's consumed cursor element must + /// satisfy, or null when the referenced rule has no resolvable target class. /// /// The += assignment to resolve the target type of /// The class hosting the current rule (provides the UML cache) @@ -269,17 +270,10 @@ private static string ResolveAssignmentTargetTypeName(AssignmentElement assignme } /// - /// Resolves the fully-qualified runtime type name of the inner element a "thin owning - /// wrapper" rule wraps. A thin owning wrapper is a rule whose target is - /// OwningMembership and whose body is a single - /// ownedRelatedElement += SomeNonTerminal assignment (e.g. - /// OwnedMultiplicity : OwningMembership = ownedRelatedElement += MultiplicityRange). - /// In such cases the wrapper type (IOwningMembership) is too coarse a discriminator - /// because every OwningMembership subtype (e.g. EndFeatureMembership) also - /// satisfies it; narrowing the check to the wrapped inner element type - /// (IMultiplicityRange) gives the precision the optional-group guard needs. - /// Returns when the assignment's referenced rule does not match - /// the thin-wrapper shape. + /// Resolves the inner element type of a "thin owning wrapper" rule + /// (X : OwningMembership = ownedRelatedElement += Y). The wrapper type is too coarse a + /// discriminator — every OwningMembership subtype satisfies it — so the guard narrows to + /// the wrapped type. Returns when the rule is not a thin wrapper. /// /// The += assignment whose referenced rule is inspected. /// The class hosting the current rule (provides the UML cache). @@ -387,12 +381,9 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass, } else { - // Guard on the TYPE the assignment consumes, not merely on the cursor being - // non-empty. An optional group is entered only when the element it would - // consume is actually present; a bare non-null test also passes for the next - // UNRELATED relationship, emitting the group's terminals spuriously — e.g. - // AcceptParameterPart's ( 'via' ownedRelationship += NodeParameterMember )? - // emitted `via` whenever the accept action merely had a body to follow. + // 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 singleTypeName = ResolveAssignmentTargetTypeName(assigment, umlClass, ruleGenerationContext); ifStatementContent.Add(singleTypeName == null @@ -405,12 +396,9 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass, { var condition = property.QueryIfStatementContentForNonEmpty("poco"); - // For `Prop ?= 'literal'` keyword assignments inside an optional `(...)?` - // group, exclude concrete subtypes whose metamodel default for the - // assigned property already equals the literal-trigger value — the - // keyword is structurally redundant for those subtypes and the - // canonical source omits it (e.g. `attribute X` rather than - // `ref attribute X` because AttributeUsage::isReference defaults to true). + // For `Prop ?= 'literal'` in an optional group, exclude subtypes whose metamodel + // default already equals the trigger value — the keyword is redundant there + // (e.g. `attribute X`, not `ref attribute X`). if (property.QueryIsBool()) { var exclusionTypes = property.QuerySubclassesWithMatchingDefault(umlClass, "true"); @@ -420,16 +408,9 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass, condition += $" && poco is not ({string.Join(" or ", exclusionTypes.Select(c => c.QueryFullyQualifiedTypeName()))})"; } - // A DERIVED property cannot record whether the keyword was written: its runtime - // value is COMPUTED from other state, so it is routinely true for reasons that - // have nothing to do with the notation. `Usage::isReference` derives as - // `not isComposite`, and every context in which the metamodel FORCES a Usage to - // be referential (validateUsageIsReferential — directed, end feature, or no - // featuringType) makes it true while the canonical source carries no `ref` at - // all. The type-level exclusion above cannot see that: the redundancy is - // per-INSTANCE, not per-subclass. Delegate the instance-level decision to a - // hand-coded guard companion, reusing the `IsValidFor…` convention that the - // rule-alternative dispatch already relies on. + // A DERIVED property cannot record whether the keyword was written (e.g. + // Usage::isReference = not isComposite is true in contexts with no `ref` at all), + // and the redundancy is per-INSTANCE — delegate to a hand-coded IsValidFor… guard. if (property.IsDerived || property.IsDerivedUnion) { condition += $" && poco.IsValidFor{ruleGenerationContext.NamedElementToGenerate?.Name}{property.Name.CapitalizeFirstLetter()}(writerContext)"; @@ -444,20 +425,7 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass, writer.WriteSafeString($"){Environment.NewLine}"); writer.WriteSafeString($"{{{Environment.NewLine}"); - var previousSiblings = ruleGenerationContext.CurrentSiblingElements; - var previousIndex = ruleGenerationContext.CurrentElementIndex; - ruleGenerationContext.CurrentSiblingElements = elements; - - for (var elementIndex = 0; elementIndex < elements.Count; elementIndex++) - { - ruleGenerationContext.CurrentElementIndex = elementIndex; - var previousCaller = ruleGenerationContext.CallerRule; - this.ProcessRuleElement(writer, umlClass, elements[elementIndex], ruleGenerationContext); - ruleGenerationContext.CallerRule = previousCaller; - } - - ruleGenerationContext.CurrentSiblingElements = previousSiblings; - ruleGenerationContext.CurrentElementIndex = previousIndex; + this.EmitElements(writer, umlClass, elements, ruleGenerationContext, restoreCallerPerElement: true); } else { @@ -490,18 +458,7 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass, writer.WriteSafeString($"{{{Environment.NewLine}"); - var previousSiblings = ruleGenerationContext.CurrentSiblingElements; - var previousIndex = ruleGenerationContext.CurrentElementIndex; - ruleGenerationContext.CurrentSiblingElements = elements; - - for (var elementIndex = 0; elementIndex < elements.Count; elementIndex++) - { - ruleGenerationContext.CurrentElementIndex = elementIndex; - this.ProcessRuleElement(writer, umlClass, elements[elementIndex], ruleGenerationContext); - } - - ruleGenerationContext.CurrentSiblingElements = previousSiblings; - ruleGenerationContext.CurrentElementIndex = previousIndex; + this.EmitElements(writer, umlClass, elements, ruleGenerationContext); } if (!ruleGenerationContext.IsNextElementNewLineTerminal() && !ruleGenerationContext.IsLastElement()) @@ -513,20 +470,7 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass, } else { - var previousSiblings = ruleGenerationContext.CurrentSiblingElements; - var previousIndex = ruleGenerationContext.CurrentElementIndex; - ruleGenerationContext.CurrentSiblingElements = elements; - - for (var elementIndex = 0; elementIndex < elements.Count; elementIndex++) - { - ruleGenerationContext.CurrentElementIndex = elementIndex; - var previousCaller = ruleGenerationContext.CallerRule; - this.ProcessRuleElement(writer, umlClass, elements[elementIndex], ruleGenerationContext, isPartOfMultipleAlternative); - ruleGenerationContext.CallerRule = previousCaller; - } - - ruleGenerationContext.CurrentSiblingElements = previousSiblings; - ruleGenerationContext.CurrentElementIndex = previousIndex; + this.EmitElements(writer, umlClass, elements, ruleGenerationContext, restoreCallerPerElement: true, isPartOfMultipleAlternative); } } @@ -742,12 +686,7 @@ private void EmitNonTerminalThenAssignmentDispatch(EncodedTextWriter writer, ICl } else { - var previousCaller = ruleGenerationContext.CallerRule; - var previousName = ruleGenerationContext.CurrentVariableName; - ruleGenerationContext.CallerRule = nonTerminalElement; - this.ProcessAlternatives(writer, umlClass, nonTerminalReferencedRule?.Alternatives, ruleGenerationContext); - ruleGenerationContext.CallerRule = previousCaller; - ruleGenerationContext.CurrentVariableName = previousName; + this.ProcessReferencedRuleAlternatives(writer, umlClass, nonTerminalElement, nonTerminalReferencedRule, ruleGenerationContext); } writer.WriteSafeString($"{Environment.NewLine}}}{Environment.NewLine}"); @@ -964,26 +903,10 @@ private static bool TryEmitQualifiedNameOrChainAlternatives(EncodedTextWriter wr writer.WriteSafeString($"{{{Environment.NewLine}"); writer.WriteSafeString($"SharedTextualNotationBuilder.AppendQualifiedName(stringBuilder,{variableName}.{resolvedPropertyName}, writerContext, poco);{Environment.NewLine}"); - // The two alternatives denote the SAME notational prefix, so the reference form must be - // terminated the same way the chain form is. When the chain rule ends in a terminal — as - // FeatureChainPrefix does with its trailing '.' — that terminal belongs to the prefix notation - // rather than to the chain, so the [QualifiedName] branch has to emit it too. - // - // Ground truth is the reference parser, whose FlowEndSubsetting spells the terminal out on BOTH - // alternatives (org.omg.sysml.xtext/src/org/omg/sysml/xtext/SysML.xtext and the identical KerML - // production in org.omg.kerml.xtext/src/org/omg/kerml/xtext/KerML.xtext): - // - // FlowEndSubsetting returns SysML::ReferenceSubsetting : - // referencedFeature = [SysML::Feature | QualifiedName] '.' - // | ownedRelatedElement += FeatureChainPrefix - // - // Resources/SysML-textual-bnf.kebnf omits that '.' on the reference alternative — the only - // genuine missing terminal found when diffing all 403 shared rules against the reference - // grammar. Since the kebnf files are OMG-owned and immutable, the terminal is recovered here - // instead. The inference is sound because the correlation holds for every rule of this shape: - // OwnedFeatureTyping, OwnedSubsetting, OwnedReferenceSubsetting, OwnedCrossSubsetting and - // OwnedRedefinition all pair with OwnedFeatureChain, which ends in a group and therefore keeps - // the plain space separator; FlowEndSubsetting is the sole rule pairing with FeatureChainPrefix. + // 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. var chainTrailingTerminal = QueryTrailingTerminal(referencedRule); if (chainTrailingTerminal == null) @@ -1002,10 +925,8 @@ private static bool TryEmitQualifiedNameOrChainAlternatives(EncodedTextWriter wr } /// - /// Returns the value of the that ends - /// with, or when the rule has more than one alternative or does not end - /// in a terminal. Used to keep alternative forms of the same notational construct terminated - /// identically — see . + /// Returns the terminal value ends with, or when + /// it has multiple alternatives or does not end in a terminal. /// /// The referenced ; may be . /// The trailing terminal's value, or . @@ -1024,17 +945,10 @@ private static string QueryTrailingTerminal(TextualNotationRule rule) } /// - /// Attempts to emit code for the subclass-rule dispatch pattern: - /// property = X | SubclassRule, where SubclassRule targets a strict - /// specialization of the current rule's target metaclass (e.g. - /// FeatureChainMember : Membership = memberElement = [QualifiedName] | OwnedFeatureChainMember - /// with OwnedFeatureChainMember : OwningMembership). The runtime subtype is the - /// discriminator: only an instance of the subclass can be the subclass-rule alternative, - /// so the emitted code dispatches on the POCO's runtime type FIRST and only falls back to - /// the assignment alternative for base-class instances. Emitting the alternatives in - /// grammar order instead would put a derived-property null check (e.g. - /// MemberElement != null, never null on an OwningMembership) in front, - /// rendering the subclass alternative unreachable. + /// Attempts the subclass-rule dispatch pattern property = X | SubclassRule, where + /// SubclassRule targets a strict specialization of the current metaclass (e.g. + /// FeatureChainMember). Dispatches on the runtime subtype FIRST — grammar order would put + /// a never-null derived-property check in front and make the subclass alternative unreachable. /// /// The used to write output /// The related @@ -1115,17 +1029,10 @@ private bool TryEmitSubclassRuleDispatchAlternatives(EncodedTextWriter writer, I } /// - /// Attempts to emit code for the single-element-or-same-class-rule pattern: - /// collection += X | SameClassRule, where SameClassRule targets the current - /// rule's own metaclass and re-consumes the same collection property (e.g. - /// ChainingPart : Feature = 'chains' (ownedRelationship += OwnedFeatureChaining | FeatureChain) - /// with FeatureChain : Feature = ownedRelationship += OwnedFeatureChaining ('.' ownedRelationship += OwnedFeatureChaining)+). - /// Because the same-class rule consumes two or more elements of the += value type, - /// the discriminator is the element count: exactly one matching element selects the single - /// += alternative (with its Golden-Rule Move()), otherwise the same-class - /// rule is delegated to and manages the shared cursor itself. A bare - /// cursor.Current != null discriminator would make the same-class alternative - /// unreachable and, without the Move(), stall the caller's dispatch loop. + /// Attempts the pattern collection += X | SameClassRule, where the same-class rule + /// re-consumes the same collection (e.g. ChainingPart vs FeatureChain). The + /// discriminator is the element COUNT: exactly one match selects the single += alternative + /// (with its Move()); otherwise the same-class rule manages the shared cursor itself. /// /// The used to write output /// The related @@ -1162,11 +1069,8 @@ private bool TryEmitSingleElementOrSameClassRuleAlternatives(EncodedTextWriter w return false; } - // The same-class rule must re-consume the same collection property THROUGH THE SAME - // sub-rule (e.g. FeatureChain re-consumes ownedRelationship += OwnedFeatureChaining): - // only then do the two alternatives compete for the same element type and need the - // count discriminator. Disjoint element types (e.g. CalculationBodyItem's - // ReturnParameterMember vs ActionBodyItem) stay with the plain type dispatch. + // Only when the same-class rule re-consumes the same property THROUGH THE SAME sub-rule do + // the alternatives compete for one element type and need the count discriminator. var elementValueNonTerminal = (NonTerminalElement)assignmentElement.Value; var reconsumesSameElements = referencedRule.Alternatives @@ -1346,14 +1250,9 @@ private void EmitTerminalVsBodyWithCollectionNonTerminals(EncodedTextWriter writ var propertyAccessName = targetProperty.QueryPropertyNameBasedOnUmlProperties(); - // KEBNF `XBody : Type = ';' | '{' XBodyItem* '}'` — both the choice and the `*` loop - // are bounded by "does the current cursor element match an XBodyItem alternative?". - // For body item rules whose dispatcher can encounter unrecognised elements legitimately - // belonging to a parent rule (notably PortDefinition's trailing - // ConjugatedPortDefinitionMember, which appears in OwnedRelationship but is NOT a - // DefinitionBodyItem alternative), the body rule must defer to an - // `IsValidFor{XBodyItem}` predicate. Other body rules retain the simple - // `cursor.Current != null` semantics; we promote rules into the guarded form by name. + // For body item rules that can encounter elements legitimately belonging to a parent rule + // (e.g. PortDefinition's trailing ConjugatedPortDefinitionMember), the `;` choice and the + // `*` loop must defer to an IsValidFor{XBodyItem} predicate instead of a bare non-null test. var requiresIsValidForGuard = IsGuardedBodyItemRule(collectionNonTerminals[0].Name); var guardCallSuffix = requiresIsValidForGuard ? $".IsValidFor{collectionNonTerminals[0].Name}(writerContext)" @@ -1407,12 +1306,7 @@ private void EmitTerminalVsBodyWithCollectionNonTerminals(EncodedTextWriter writ } else { - var previousCaller = ruleGenerationContext.CallerRule; - var previousName = ruleGenerationContext.CurrentVariableName; - ruleGenerationContext.CallerRule = collectionNonTerminal; - this.ProcessAlternatives(writer, umlClass, referencedRule?.Alternatives, ruleGenerationContext); - ruleGenerationContext.CallerRule = previousCaller; - ruleGenerationContext.CurrentVariableName = previousName; + this.ProcessReferencedRuleAlternatives(writer, umlClass, collectionNonTerminal, referencedRule, ruleGenerationContext); } writer.WriteSafeString($"{Environment.NewLine}}}{Environment.NewLine}"); @@ -1495,12 +1389,7 @@ private void EmitTerminalVsBodyWithSingleNonTerminal(EncodedTextWriter writer, I } else { - var previousCaller = ruleGenerationContext.CallerRule; - var previousName = ruleGenerationContext.CurrentVariableName; - ruleGenerationContext.CallerRule = singleNonTerminal; - this.ProcessAlternatives(writer, umlClass, referencedRule?.Alternatives, ruleGenerationContext); - ruleGenerationContext.CallerRule = previousCaller; - ruleGenerationContext.CurrentVariableName = previousName; + this.ProcessReferencedRuleAlternatives(writer, umlClass, singleNonTerminal, referencedRule, ruleGenerationContext); } writer.WriteSafeString($"{Environment.NewLine}}}{Environment.NewLine}"); @@ -1515,19 +1404,10 @@ private void EmitTerminalVsBodyWithSingleNonTerminal(EncodedTextWriter writer, I } /// - /// Returns true when the KEBNF body-item rule named can have - /// elements ahead of the cursor that legitimately belong to a parent rule (and therefore must - /// NOT be consumed by the body's * loop). For these rules, - /// emits the ; / { … } choice and the * loop as - /// IsValidFor{XBodyItem}-guarded code rather than a raw cursor.Current != null - /// check, faithfully implementing the KEBNF * quantifier semantics ("iterate while - /// the current element matches an alternative") for the affected rules. - /// Allowlist (rather than allow-all) keeps the codegen change scoped: only rules whose - /// dispatcher can encounter foreign elements need the guard. Currently this is the two - /// rules whose body item includes the DefinitionMember alternative — DefinitionBodyItem - /// (consumed by PortDefinition's trailing ConjugatedPortDefinitionMember that - /// the body must skip) and InterfaceBodyItem (same shape). All other body item rules - /// retain the existing cursor.Current != null semantics. + /// Returns true when the body-item rule can have cursor elements that legitimately belong to a + /// parent rule and must not be consumed by the body's * loop. Allowlisted by name to keep + /// the guarded form scoped: currently DefinitionBodyItem (PortDefinition's trailing + /// ConjugatedPortDefinitionMember) and InterfaceBodyItem. /// /// The KEBNF rule name of the body item (e.g. DefinitionBodyItem) /// true if the codegen should emit the guarded form 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 new file mode 100644 index 00000000..4789b46c --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/04-Functional Allocation/4a-Functional Allocation.sysml @@ -0,0 +1,80 @@ +package '4a-Functional Allocation' { + private import '2a-Parts Interconnection'::*; + private import '3a-Function-based Behavior-1'::*; + private import '3a-Function-based Behavior-1'::'provide power'::*; + part vehicle1_c1_functional_allocation :> vehicle1_c1 { + port :>> fuelCmdPort { + in fuelCmd: FuelCmd; + } + perform 'provide power' { + doc + /* + * This allocates the action '3a-Function-based Behavior-1'::'provide power' as an enacted + * performance of 'vehicle_c1_functional_allocation'. + */ + + in fuelCmd = fuelCmdPort.fuelCmd; + } + part :>> engine { + port :>> fuelCmdPort { + in fuelCmd: FuelCmd; + } + perform 'provide power'.'generate torque' { + /* + * 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; + } + port :>> drivePwrPort { + out engineTorque: Torque; + } + } + part :>> transmission { + port :>> clutchPort { + in attribute engineTorque: Torque; + } + perform 'provide power'.'amplify torque' { + in engineTorque = clutchPort.engineTorque; + out transmissionTorque = shaftPort_a.transmissionTorque; + } + port :>> shaftPort_a { + out transmissionTorque: Torque; + } + } + part :>> driveshaft { + port :>> shaftPort_b { + in transmissionTorque: Torque; + } + perform 'provide power'.'transfer torque' { + in transmissionTorque = shaftPort_b.transmissionTorque; + out driveshaftTorque = shaftPort_c.driveshaftTorque; + } + port :>> shaftPort_c { + out driveshaftTorque: Torque; + } + } + part :>> rearAxleAssembly { + port :>> shaftPort_d { + in driveshaftTorque: Torque; + } + perform 'provide power'.'distribute torque' { + in driveshaftTorque = shaftPort_d.driveshaftTorque; + out wheelTorque1 = rearAxle.leftHalfAxle.axleToWheelPort.wheelTorque; + out wheelTorque2 = rearAxle.rightHalfAxle.axleToWheelPort.wheelTorque; + } + part :>> rearAxle { + part :>> leftHalfAxle { + port :>> axleToWheelPort { + out wheelTorque: Torque; + } + } + part :>> rightHalfAxle { + port :>> axleToWheelPort { + out wheelTorque: Torque; + } + } + } + } + } +} diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/SysML2.NET.Serializer.TextualNotation.Tests.csproj b/SysML2.NET.Serializer.TextualNotation.Tests/SysML2.NET.Serializer.TextualNotation.Tests.csproj index 254f7640..52805693 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/SysML2.NET.Serializer.TextualNotation.Tests.csproj +++ b/SysML2.NET.Serializer.TextualNotation.Tests/SysML2.NET.Serializer.TextualNotation.Tests.csproj @@ -130,6 +130,12 @@ Always + + Always + + + Always + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/02-Parts Interconnection/2a-Parts Interconnection.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/02-Parts Interconnection/2a-Parts Interconnection.sysmlx index 5decf171..42188965 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/02-Parts Interconnection/2a-Parts Interconnection.sysmlx +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/02-Parts Interconnection/2a-Parts Interconnection.sysmlx @@ -1,374 +1,374 @@ - - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - + + + - - - - - - - + + + + + + + - - - - - + + + + + - - + + - - - - - - - - + + + + + + + + - - + + - - - + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - + + + + - - + + - - - - + + + + - - - - - + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - - + + + + + + - - + + - - + + - - - - - - - - + + + + + + + + - - + + - - - - - + + + + + @@ -378,609 +378,609 @@ - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - + + + + + + - - + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + - - + + - - - - - - + + + + + + - - + + - - + + - - - - - + + + + + - - - - - + + + + + - - + + - - - - - - - - - + + + + + + + + + - - + + - - - - - - + + + + + + - - + + - - - - - - - - - - - - + + + + + + + + + + + + - - + + - - - + + + - - + + - - - - - - - - - - + + + + + + + + + + - - - - - - + + + + + + - - + + - - - - - - - - + + + + + + + + - - + + - - - - - + + + + + - - - - - + + + + + - - + + - - - - - - - - + + + + + + + + - - + + - - - - - - - + + + + + + + - - + + - - + + - - - - - - - - + + + + + + + + - - + + - - - - - - - + + + + + + + - - + + - - - - - - - - + + + + + + + + - - - - - + + + + + - - + + - - - - - - - - - + + + + + + + + + - - + + - - - - - - + + + + + + - - + + - - - - - - - - - + + + + + + + + + - - + + - - - - - - + + + + + + - - + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - + + + + - - + + - - - - - + + + + + - - - - - + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + - - + + - - - - - + + + + + - - - - - + + + + + - - + + - - - - - - - - - + + + + + + + + + - - + + - - - - - - + + + + + + - - + + - - - - - - - - - + + + + + + + + + - - + + - - - - - - + + + + + + - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - + + + + - - + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + - - + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/03-Function-based Behavior/3a-Function-based Behavior-1.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/03-Function-based Behavior/3a-Function-based Behavior-1.sysmlx index 65bbc5da..bf3fd4d9 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/03-Function-based Behavior/3a-Function-based Behavior-1.sysmlx +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/03-Function-based Behavior/3a-Function-based Behavior-1.sysmlx @@ -1,127 +1,127 @@ - - - - - - - - - - - + + + + + + + + + + + - + - - + + - - + + - - + + - - + + - - + + - - - - - + + + + + - - - - + + + + - - - - - - + + + + + + - - - - + + + + - - - - - - + + + + + + - - - - + + + + - - - - - - + + + + + + - - - - + + + + - - - - + + + + - - - - - + + + + + - - - - + + + + - - - - + + + + @@ -129,480 +129,480 @@ - - - - - - - - + + + + + + + + - - - - + + + + - - - - + + + + - - - - - - - - + + + + + + + + - - + + - - - - - + + + + + - - + + - - - - - + + + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + - - + + - - - + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + - - - + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + - - - + + + - - - - - - - + + + + + + + - - - - - - + + + + + + - - + + - - - - - - - + + + + + + + - - - - - - + + + + + + - - + + - - - - - - + + + + + + - - + + - - - - - + + + + + - - + + - - - - + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - + + + + + - - + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/04-Functional Allocation/4a-Functional Allocation.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/04-Functional Allocation/4a-Functional Allocation.sysmlx new file mode 100644 index 00000000..9c092956 --- /dev/null +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/04-Functional Allocation/4a-Functional Allocation.sysmlx @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs b/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs index 919ea7cd..5ab64ae8 100644 --- a/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs +++ b/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs @@ -49,6 +49,7 @@ public class TextualNotationValidationTestFixture [TestCase("03-Function-based Behavior", "3c-Function-based Behavior-structure mod-3.sysmlx")] [TestCase("03-Function-based Behavior", "3d-Function-based Behavior-item.sysmlx")] [TestCase("03-Function-based Behavior", "3e-Function-based Behavior-item.sysmlx")] + [TestCase("04-Functional Allocation", "4a-Functional Allocation.sysmlx")] public async Task VerifyValidationTextualNotationXmi(string folderName, string fileName) { var loggerFactory = LoggerFactory.Create(builder => diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs b/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs index 057dfd3c..31e36fc6 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs @@ -37,28 +37,9 @@ namespace SysML2.NET.Serializer.TextualNotation.Writers using SysML2.NET.Extensions; /// - /// Performant single-place cache for textual-notation qualified-name resolution. - /// Owns three caches: - /// - /// Eager structural: per-namespace simple-name → member set, populated - /// by a single walk of the model on construction. Keys are the namespaces reachable - /// transitively from the root namespace via containment and imports. - /// Lazy source-scope chain: per-source-POCO upward walk result. The - /// chain is materialised on first encounter of the source and cached for subsequent - /// references rooted at the same source. - /// Lazy resolved emission: per-(target, sourceLocalScope) - /// pair, the final string to emit (bare simple name, escaped unrestricted name, or full - /// qualified name). Reused on every subsequent reference that hits the same pair. - /// - /// Resolution policy mirrors KerML §8.2.3.5 with two short-circuits: - /// - /// targets (import declarations) keep their full - /// qualified path — the path identifies WHAT is being imported. - /// Chain accessors (the .X of a feature-chain expression / multi-segment - /// feature chain) resolve X against the target's own owning namespace so the bare - /// simple name is emitted, since the parser establishes the resolution scope from the - /// preceding chain segment's type. - /// + /// 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 { @@ -69,80 +50,49 @@ private static readonly IReadOnlyDictionary> EmptyInde = new Dictionary>(StringComparer.Ordinal); /// - /// Eager structural cache: namespace → (simple-name → member set). Populated once on - /// construction by . + /// Eager structural cache: namespace → (simple-name → member set). /// private readonly Dictionary>> simpleNameIndices; /// - /// Lazy upward-walk cache: source-POCO id → containment-chain of s - /// terminated at the root namespace (or wherever the upward walk first hits an empty - /// owningNamespace). Populated by . + /// Lazy cache: source-POCO id → its upward containment chain of namespaces. /// private readonly Dictionary> sourceScopeChains = new (); /// - /// Lazy resolved-emission cache: (target.Id, sourceLocalScope.Id) → emitted string. + /// Lazy cache: (target.Id, sourceLocalScope.Id) → emitted string. /// private readonly Dictionary<(Guid TargetId, Guid SourceScopeId), string> resolvedReferences = new (); /// - /// Reverse index from a canonical-owning to the set of - /// "facade" namespaces that DIRECTLY re-export it via a . - /// Populated during the eager pass alongside the - /// per-scope simple-name indices. Single-hop only — no transitive walk. - /// Used by to shorten library references like - /// ISQBase::mass to the OMG SST idiomatic facade form ISQ::mass: when a - /// reference targets an element owned by ISQBase, and a namespace ISQ - /// directly imports ISQBase, AND ISQ is reachable from the source scope - /// chain, the writer emits ISQ::simpleName instead of ISQBase::simpleName. - /// The SST tutorial (Release 2026-03) uses the facade form 17:1 over the implementation - /// form (ISQ:: vs ISQBase::), establishing the canonical idiom. + /// 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 bindings: scope → (aliased element → the alias names declared for it - /// in that scope). Populated during the eager pass from every - /// that carries an explicit / - /// override differing from the target's own names — i.e. - /// an alias X for Y; declaration. - /// The forward index already maps "Torque" → TorqueValue, but - /// probes only the TARGET's own lexical forms, so an alias could never - /// be found. This reverse map lets a reference to TorqueValue emit the in-scope alias - /// Torque instead of the qualified ISQMechanics::TorqueValue, matching how the model - /// was written. + /// 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 root s forming the global (KerML 1.0 - /// §8.2.3.5.2), excluding itself. Indexed like any other scope, but - /// only their VISIBLE memberships are admitted — see . + /// The other root namespaces forming the global namespace (KerML §8.2.3.5.2); only their + /// VISIBLE memberships are indexed. /// private readonly List globalNamespaces; /// - /// Initializes a new rooted at - /// and eagerly populates the per-namespace simple-name - /// index for every namespace reachable from the root. + /// Initializes the cache and eagerly indexes every namespace reachable from + /// . /// /// The root being serialized. /// - /// The other root s available to — the - /// model libraries and any other loaded resource. Per KerML 1.0 §8.2.3.5.2 a root - /// has an implicit containing global - /// that "includes all the visible Memberships of all other root Namespaces that are available to - /// the first Namespace", and §8.2.3.5.4 makes resolution in that scope the final step once the - /// containment chain is exhausted. Supplying them lets the writer emit a name that resolves through - /// a library root which the model does not itself import (e.g. ISQ::TorqueValue for an - /// element owned by ISQMechanics and publicly re-exported by ISQ). - /// Optional: when or empty, resolution is limited to the containment - /// and import graph of , which is always safe — it can only yield - /// a longer, equally valid name. Obtain the roots from - /// IDeSerializer.QueryRootNamespaces(). + /// 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. /// public NameResolutionCache(INamespace rootNamespace, IEnumerable globalNamespaces = null) { @@ -157,16 +107,14 @@ public NameResolutionCache(INamespace rootNamespace, IEnumerable glo } /// - /// Gets the root the cache was rooted at — used as the fallback - /// local scope when a source POCO has no resolvable enclosing namespace. + /// 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 . The result is cached by - /// (target.Id, sourceLocalScope.Id) — repeat calls with the same pair are O(1) - /// dictionary lookups. + /// 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. @@ -178,20 +126,24 @@ public string Resolve(IElement target, IElement sourcePoco) case null: return string.Empty; - // Short-circuit 1 — IMembership targets: import declarations keep the full path, - // but use the SHORTEST declared name available at each owner-chain segment. - // IElement.qualifiedName walks via EscapedName() which prefers `name` over - // `shortName`; that is the inverse of what import declarations need. The SysML - // Textual Notation tutorial and the pilot implementation consistently emit - // imports using shortNames where declared (e.g. `import SI::kg` not - // `import SI::kilogram`). + // Membership imports keep the full path, using the SHORTEST declared name per + // segment (`import SI::kg`, not `SI::kilogram` as qualifiedName would give). case IMembership membership: return membership.MemberElement != null ? QueryShortQualifiedName(membership.MemberElement) : string.Empty; } - // Short-circuit 2 — no usable simple name on the target: emit the qualified name. + // 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'::*`). + if (sourcePoco is IImport { OwningRelatedElement: { } importOwner } + && !IsReachableByContainment(target, importOwner)) + { + return this.QueryImportPath(target); + } + var escapedName = target.EscapedName(); if (string.IsNullOrWhiteSpace(escapedName)) @@ -199,40 +151,30 @@ public string Resolve(IElement target, IElement sourcePoco) return target.qualifiedName ?? string.Empty; } - // Short-circuit 3 — chain accessor: the parser establishes the resolution scope - // from the preceding chain segment's type at parse time, so the simple name is - // sufficient. We do NOT cache this because the chain accessor's resolution is a - // function of (target, sourcePoco) — and sourcePoco changes per reference site. - // The work is cheap (one type test + EscapedName already computed above). + // 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); } - // Memoised path — look up by (target.Id, sourceLocalScope.Id). var sourceLocalScope = this.GetSourceLocalScope(sourcePoco); - // Redefinition-context: when the source is an OwnedRedefinition and the target is its - // RedefinedFeature, the LOCAL redefining feature must not shadow the redefined target - // during simple-name lookup. The parser resolves `:>> name` against the type's - // INHERITED members (the redefining feature isn't a member of the type yet — it's the - // very feature being defined), so the writer mirrors that by filtering the local - // redefiner out of every candidate bucket. Bypasses the memo because the redefinition - // context is per-call, not per (target, sourceLocalScope). - // EXCEPTION: when the local redefining feature has a DECLARED name equal to the - // target's name, emitting the bare simple-name form would re-resolve at parse time to - // the local redefiner itself (the post-parse local member shadows the inherited one), - // not to the redefined target. KerML §8.2.3.5 requires the qualified form in that - // case so the round-trip resolves to the SAME element. We detect this collision and - // fall through to the normal cached path, which produces the qualified form. - if (sourcePoco is IRedefinition redefinition && ReferenceEquals(target, redefinition.RedefinedFeature)) - { - var localRedefiner = redefinition.RedefiningFeature; - - if (localRedefiner != null && !RedefinerDeclaredNameCollidesWith(localRedefiner, target)) - { - return this.ResolveFresh(target, sourcePoco, sourceLocalScope, escapedName, localRedefiner, QuerySelfBindingScope(sourcePoco)); - } + // 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, sourcePoco, sourceLocalScope, escapedName, localReferencer, QuerySelfBindingScope(sourcePoco)); } var cacheKey = (target.Id, sourceLocalScope?.Id ?? Guid.Empty); @@ -248,23 +190,112 @@ public string Resolve(IElement target, IElement sourcePoco) } /// - /// Returns the in which is itself the - /// name binding for the element being referenced, or when the source is - /// not a referencing . - /// A non-owning under an expression (e.g. the - /// FeatureReferenceMember of a ) IS the reference - /// being emitted — the parser resolves the name against the ENCLOSING lexical scope and only then - /// creates this membership. Counting it as a binding in its own owner's scope is circular: the - /// target would always appear uniquely resolvable at depth 0, so a bare simple name is emitted even - /// when an intervening declaration shadows it (in fuelCmd = fuelCmd instead of - /// in fuelCmd = 'provide power'::fuelCmd, which would re-parse to the local parameter). - /// Restricted to memberships with NO explicit name override. Such a membership contributes - /// the target's OWN name to its scope's index, which is exactly the binding to ignore. A - /// membership that DOES carry an override is an alias X for Y; declaration: it contributes - /// only the alias name, so the scope's binding of the target's own name comes from a different - /// membership and stays valid (alias ThreeDVectorQuantityValue for '3dVectorQuantityValue'; - /// must not degrade to the qualified Quantities::'3dVectorQuantityValue'). The circular - /// alias-for-itself case is handled separately by . + /// 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; + } + + for (IElement scope = importOwner; scope != null; scope = QueryOwningContainer(scope)) + { + if (ReferenceEquals(scope, declaringNamespace)) + { + return true; + } + } + + return false; + } + + /// + /// 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)) + { + 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 — i.e. the target is nameable directly from that scope. + /// + /// 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); + } + + /// + /// Returns the scope in which is itself the name binding for the + /// target — 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 ignored in that + /// scope or every reference would resolve trivially at depth 0. /// /// The source POCO at the reference site. /// The scope whose binding for the target must be ignored, or . @@ -278,46 +309,27 @@ private static INamespace QuerySelfBindingScope(IElement sourcePoco) } /// - /// First-time resolution: emits the SHORTEST unambiguous qualified-name suffix for - /// at 's reference site. - /// Walks the target's owner chain outward (innermost ancestor first) and, at each - /// step, tries to resolve the anchor's simple name uniquely in the source-scope - /// chain. The first anchor that resolves uniquely produces the emission anchor; the - /// suffix from the anchor down to is then appended via - /// "::". Falls back to when no anchor in - /// the owner chain resolves. - /// - /// For the bare itself (anchor depth 0), the two lexical - /// forms (shortName and name) are tried in turn — short first per the - /// SST tutorial / pilot convention. Inner ancestors are tried using whichever single - /// lexical form is declared on them. - /// + /// 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's source POCO. - /// The previously-computed local scope (may be ). + /// The pre-computed local scope (may be ). /// The target's escaped raw name. - /// The local that acts like redefiner - /// - /// The whose binding of is contributed by the - /// reference being emitted itself and must therefore be ignored during the scope walk, or - /// when the source is not a self-binding membership. See - /// . - /// + /// Local feature to exclude from scope buckets, or . + /// Scope whose binding of the target must be ignored, or . /// The resolved emission string. private string ResolveFresh(IElement target, IElement sourcePoco, INamespace sourceLocalScope, string escapedName, IFeature localRedefiner, INamespace selfBindingScope) { var chain = this.GetSourceScopeChain(sourcePoco, sourceLocalScope); - // Depth 0 — try the target's own simple names. shortName first (the SST tutorial - // / pilot reference output consistently uses short forms for quantity literals - // like `[kg]` over `[kilogram]`), then long. var rawShortName = target.shortName; string escapedShortName = null; if (!string.IsNullOrWhiteSpace(rawShortName)) { - escapedShortName = rawShortName.QueryIsValidBasicName() ? rawShortName : rawShortName.ToUnrestrictedName(); + escapedShortName = Escape(rawShortName); if (this.TryResolveSimpleNameAcrossChain(chain, target, rawShortName, escapedShortName, localRedefiner, selfBindingScope, out var matchedShort)) { @@ -333,36 +345,28 @@ private string ResolveFresh(IElement target, IElement sourcePoco, INamespace sou return matchedLong; } - // Alias pass — an `alias X for Y;` declared in a reachable scope binds the target under a - // name it does not carry itself, so the target's own lexical forms above can never find it. - // Preferred over the facade / qualified forms because it is how the model names the element - // at this site (e.g. `Torque` rather than `ISQMechanics::TorqueValue`). + // 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(chain, target, sourcePoco, localRedefiner, selfBindingScope, out var matchedAlias)) { return matchedAlias; } - // Facade re-export pass — when the target's owningNamespace is DIRECTLY re-exported - // by another namespace via NamespaceImport AND that facade is reachable from the - // source scope chain, prefer the OMG SST canonical form `facade::simpleName` over - // the implementation-owning form `owner::simpleName`. The SST tutorial (Release - // 2026-03) uses the facade form 17:1 over the implementation form (e.g. - // `ISQ::mass` 17 times vs `ISQBase::mass` once), establishing this as the canonical - // textual idiom. KerML §8.2.3.5.4 leaves the choice between the two formally - // undetermined, so both forms parse to the same element. + // 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(chain, target, escapedShortName, escapedName, out var matchedFacade)) { return matchedFacade; } - // Depth ≥ 1 — walk owner-chain ancestors outward and look for the first one that - // itself resolves uniquely in the source-scope chain. Once found, emit it as the - // anchor followed by the owner-chain segments down to the target. + // Walk owner-chain ancestors outward; the first that resolves uniquely anchors a + // partially-qualified suffix down to the target. var segmentsDownToTarget = new Stack(); segmentsDownToTarget.Push(QueryPreferredEscapedSegment(target) ?? string.Empty); - var ancestor = QueryOwningContainer(target); + var ancestor = (IElement)QueryOwningContainer(target); var visitedAncestors = new HashSet(); while (ancestor != null && visitedAncestors.Add(ancestor)) @@ -371,17 +375,12 @@ private string ResolveFresh(IElement target, IElement sourcePoco, INamespace sou if (string.IsNullOrWhiteSpace(ancestorSegment)) { - // An unnamed namespace in the owner chain cannot appear inside a - // QualifiedName — emitting `::` while skipping the - // unnamed gap would produce an unparseable result. Stop the walk and let - // the fallback (target.qualifiedName) take over. + // An unnamed namespace cannot appear inside a QualifiedName; stop and let the + // qualifiedName fallback take over. break; } - var rawAncestorShort = ancestor.shortName; - var rawAncestorLong = ancestor.name; - - var ancestorRawName = !string.IsNullOrWhiteSpace(rawAncestorShort) ? rawAncestorShort : rawAncestorLong; + var ancestorRawName = QueryPreferredRawName(ancestor); if (!string.IsNullOrWhiteSpace(ancestorRawName) && this.TryResolveSimpleNameAcrossChain(chain, ancestor, ancestorRawName, ancestorSegment, localRedefiner, selfBindingScope, out var matchedAnchor)) @@ -402,19 +401,16 @@ private string ResolveFresh(IElement target, IElement sourcePoco, INamespace sou ancestor = QueryOwningContainer(ancestor); } - // Fall back to the fully-qualified name. return target.qualifiedName ?? string.Empty; } /// - /// Returns 's owningNamespace when reachable, - /// otherwise . Owner-chain traversal stops on - /// from unimplemented derived properties — the - /// same convention used by . + /// 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 IElement QueryOwningContainer(IElement element) + private static INamespace QueryOwningContainer(IElement element) { if (element == null) { @@ -432,73 +428,105 @@ private static IElement QueryOwningContainer(IElement element) } /// - /// Determines whether the 's DECLARED name (the - /// modeller-typed identifier, NOT the effective name derived from the redefinition - /// chain) equals the 's effective name. When this returns - /// , the writer must emit the redefined target as a qualified - /// name — emitting the bare simple-name form would re-resolve at parse time to the - /// local redefiner (because the local member, once parsed, shadows the inherited one) - /// instead of to the redefined target. Per KerML §8.2.3.5 the qualified form - /// guarantees that the textual round-trip resolves back to the SAME element. - /// An anonymous redefining feature (e.g. ref :>> driveshaft = …) has - /// both DeclaredName and DeclaredShortName empty — its effective name is - /// derived from the redefinition. Such a redefiner cannot collide and the writer can - /// safely emit the shortened form. + /// Returns 's owner, or when the derived + /// property is not implemented. /// - /// The redefining feature; must be non-null. - /// The redefined target. - /// when the declared simple-name of the redefiner equals the target's effective name. - private static bool RedefinerDeclaredNameCollidesWith(IFeature localRedefiner, IElement target) + /// The element whose owner is requested; must be non-null. + /// The owner or . + private static IElement QueryOwnerSafe(IElement element) { - var redefinerDeclaredName = localRedefiner.DeclaredName; - var redefinerDeclaredShortName = localRedefiner.DeclaredShortName; + try + { + return element.owner; + } + catch (NotSupportedException) + { + return null; + } + } - if (string.IsNullOrWhiteSpace(redefinerDeclaredName) && string.IsNullOrWhiteSpace(redefinerDeclaredShortName)) + /// + /// 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 false; + return type.AllSupertypes(); + } + catch (NotSupportedException) + { + return []; } + } - var targetName = target.name; - var targetShortName = target.shortName; + /// + /// 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; + } - return (!string.IsNullOrWhiteSpace(redefinerDeclaredName) && string.Equals(redefinerDeclaredName, targetName, StringComparison.Ordinal)) - || (!string.IsNullOrWhiteSpace(redefinerDeclaredName) && string.Equals(redefinerDeclaredName, targetShortName, StringComparison.Ordinal)) - || (!string.IsNullOrWhiteSpace(redefinerDeclaredShortName) && string.Equals(redefinerDeclaredShortName, targetName, StringComparison.Ordinal)) - || (!string.IsNullOrWhiteSpace(redefinerDeclaredShortName) && string.Equals(redefinerDeclaredShortName, targetShortName, StringComparison.Ordinal)); + /// + /// 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. + /// + /// 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 to emit a "facade re-export" form for — i.e. - /// facade::simpleName where facade is a namespace that DIRECTLY imports - /// the target's owning namespace via and is reachable - /// from the source scope . This matches the OMG SST canonical - /// idiom of ISQ::mass over ISQBase::mass (KerML §8.2.3.5.4 leaves the - /// choice formally undetermined; the SST tutorial uses the facade form 17:1). - /// Single-hop only — the SST does not use deep-chain facade names like - /// SI::mass (SI imports ISQ which imports ISQBase, two hops away). - /// Tie-break order when multiple facades are reachable: (1) facade whose simple - /// name resolves uniquely to ITSELF in the scope chain (i.e. a clean anchor); (2) - /// innermost scope-chain proximity (the facade reachable at the innermost scope wins); - /// (3) shorter facade name; (4) stable alphabetical. + /// 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 and the emission was assembled. + /// when a reachable facade was found. private bool TryResolveViaDirectFacade(IReadOnlyList chain, IElement target, string escapedShortName, string escapedName, out string matched) { matched = null; - var canonicalOwner = QueryOwningContainer(target) as INamespace; + var canonicalOwner = QueryOwningContainer(target); if (canonicalOwner == null || !this.directFacadeIndex.TryGetValue(canonicalOwner, out var facades) || facades.Count == 0) { return false; } - // Prefer the target's shortest emission form, mirroring the depth-0 walk above. var targetSimpleName = !string.IsNullOrWhiteSpace(escapedShortName) ? escapedShortName : escapedName; if (string.IsNullOrWhiteSpace(targetSimpleName)) @@ -509,9 +537,6 @@ private bool TryResolveViaDirectFacade(IReadOnlyList chain, IElement INamespace bestFacade = null; var bestScopeDepth = int.MaxValue; - // First pass — prefer facades reachable via a scope in the source chain (their - // simple name resolves directly in some chain scope's index). This is the - // strictest reachability and matches the lexical-resolution model. for (var scopeDepth = 0; scopeDepth < chain.Count; scopeDepth++) { var scope = chain[scopeDepth]; @@ -519,15 +544,13 @@ private bool TryResolveViaDirectFacade(IReadOnlyList chain, IElement foreach (var facade in facades) { - var facadeName = !string.IsNullOrWhiteSpace(facade.shortName) ? facade.shortName : facade.name; + var facadeName = QueryPreferredRawName(facade); if (string.IsNullOrWhiteSpace(facadeName) || !scopeIndex.TryGetValue(facadeName, out var facadeBucket) || !facadeBucket.Contains(facade)) { continue; } - // Innermost-scope win takes priority; within the same scope depth, prefer - // the shorter facade name, then stable alphabetical. if (bestFacade == null || scopeDepth < bestScopeDepth || (scopeDepth == bestScopeDepth && CompareFacades(facade, bestFacade) < 0)) @@ -543,20 +566,11 @@ private bool TryResolveViaDirectFacade(IReadOnlyList chain, IElement } } - // Second pass — KerML §8.2.3.5.4 says name resolution walks all the way out to - // the global namespace, which contains all loaded library root namespaces. A - // facade indexed by the cache (even one not lexically owned by a source-chain - // scope) is therefore reachable for the parser via the global resolution step, - // and `facade::simpleName` round-trips to the same target element. - // Restrict to facades whose name is MEANINGFULLY shorter than the canonical - // owner's — i.e. at most 70% of the owner's length. This matches the OMG SST - // convention: ISBase (7 chars) → ISQ (3 chars, 43% of ISBase) is a meaningful - // shortening; but ScalarValues (12 chars) → Collections (11 chars, 92%) is NOT - // — Collections is structurally a parent wrapper, not a user-facing facade for - // ScalarValues. Without semantic understanding the canonical owner is preferred - // in the latter case. 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); @@ -599,20 +613,16 @@ private bool TryResolveViaDirectFacade(IReadOnlyList chain, IElement } /// - /// Stable ordering for facade candidates at the SAME scope depth: shorter name first, - /// then ordinal alphabetical. Ensures the writer's output is deterministic across runs - /// when multiple facades re-export the same owning namespace from the same scope. + /// 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 = !string.IsNullOrWhiteSpace(left.shortName) ? left.shortName : left.name; - var rightName = !string.IsNullOrWhiteSpace(right.shortName) ? right.shortName : right.name; - - leftName ??= string.Empty; - rightName ??= string.Empty; + var leftName = QueryPreferredRawName(left) ?? string.Empty; + var rightName = QueryPreferredRawName(right) ?? string.Empty; var lengthCompare = leftName.Length.CompareTo(rightName.Length); @@ -620,46 +630,31 @@ private static int CompareFacades(INamespace left, INamespace right) } /// - /// Returns 's shortest escaped name segment — preferring - /// over , with KEBNF - /// unrestricted-name escaping when the chosen segment is not a basic name. Returns - /// when neither form is available. + /// 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 = !string.IsNullOrWhiteSpace(element.shortName) - ? element.shortName - : element.name; + var preferred = QueryPreferredRawName(element); - if (string.IsNullOrWhiteSpace(preferred)) - { - return null; - } - - return preferred.QueryIsValidBasicName() ? preferred : preferred.ToUnrestrictedName(); + return string.IsNullOrWhiteSpace(preferred) ? null : Escape(preferred); } /// - /// Walks from innermost to outermost looking for a scope - /// whose simple-name index binds uniquely to - /// . Stops the walk on the first scope that binds the name - /// to anything else — the parser's resolution would already have claimed the name in - /// that scope, so outer scopes are unreachable. + /// Walks 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. /// /// The pre-built source-scope chain (innermost first). /// The referenced element. - /// The simple-name lexical form to probe (may be / whitespace). + /// The simple-name lexical form to probe (may be blank). /// The escaped form to emit on a hit. - /// The local that acts as redefiner - /// - /// The whose binding of is contributed by the - /// reference being emitted itself and must therefore be skipped, or when the - /// source is not a self-binding membership. See . - /// - /// On a unique-binding hit, the simple-name string to emit. - /// when the simple name resolves uniquely to the target somewhere in the chain. + /// Local feature to exclude from scope buckets, or . + /// Scope whose binding of the target must be ignored, or . + /// On a hit, the simple-name string to emit. + /// when the name resolves uniquely to the target. private bool TryResolveSimpleNameAcrossChain(IReadOnlyList chain, IElement target, string rawName, string escapedName, IFeature localRedefiner, INamespace selfBindingScope, out string matched) { matched = null; @@ -687,13 +682,12 @@ private bool TryResolveSimpleNameAcrossChain(IReadOnlyList chain, IE } /// - /// Returns the resolved simple name for a chain accessor: tries the target's own - /// name first, then its shortName. Both go through the same KEBNF - /// basic-name / unrestricted-name escape as a non-chain reference. + /// 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 (long-form preference). - /// The escaped simple name, or target.qualifiedName as a last resort. + /// The pre-computed escaped name form. + /// The escaped simple name. private static string ResolveChainAccessor(IElement target, string escapedName) { if (!string.IsNullOrWhiteSpace(target.name)) @@ -705,15 +699,15 @@ private static string ResolveChainAccessor(IElement target, string escapedName) if (!string.IsNullOrWhiteSpace(rawShortName)) { - return rawShortName.QueryIsValidBasicName() ? rawShortName : rawShortName.ToUnrestrictedName(); + return Escape(rawShortName); } return target.qualifiedName ?? string.Empty; } /// - /// Returns the per-scope simple-name index that was built eagerly on construction. - /// Returns for namespaces the eager pass did not reach. + /// 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. @@ -723,9 +717,8 @@ private IReadOnlyDictionary> GetSimpleNameIndex(INames } /// - /// Returns the cached upward-walk chain for , building it - /// on first encounter. The chain starts at (the - /// reference's local scope) and walks owningNamespace up to the root. + /// 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 ). @@ -749,63 +742,31 @@ private IReadOnlyList GetSourceScopeChain(IElement sourcePoco, IName } /// - /// Walks up via owningNamespace and materialises the - /// chain. Stops at the first NotSupportedException or null. + /// Materialises the owningNamespace chain from up to the root. /// /// The starting namespace. /// The chain. private static IReadOnlyList BuildChain(INamespace start) { - if (start == null) - { - return []; - } - var chain = new List(); var current = start; while (current != null) { chain.Add(current); - - INamespace next; - - try - { - next = current.owningNamespace; - } - catch (NotSupportedException) - { - break; - } - - current = next; + current = QueryOwningContainer(current); } return chain; } /// - /// Walks up via owningNamespace and builds the - /// qualified name using the SHORTEST declared name at each segment — the - /// when non-blank, otherwise the - /// . Each segment is escaped through the KEBNF - /// unrestricted-name rules ('…' quoting for non-basic identifiers) so the - /// result is parser-roundtrip safe. - /// - /// Used by the short-circuit in for - /// import declarations, where the pilot implementation's reference text uses short - /// forms (e.g. SI::kg) but returns the - /// long form (SI::kilogram) because it goes through EscapedName() which - /// prefers name over shortName. - /// - /// Mirrors the cycle-and-null-safety pattern of : stops - /// on owningNamespace and swallows - /// from unimplemented derived properties. + /// 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 (e.g. "SI::kg"), or the empty string - /// when no segment carries a usable name. + /// The short-form qualified name, or empty when no segment carries a usable name. private static string QueryShortQualifiedName(IElement element) { var segments = new Stack(); @@ -813,44 +774,26 @@ private static string QueryShortQualifiedName(IElement element) while (current != null) { - var preferred = !string.IsNullOrWhiteSpace(current.shortName) - ? current.shortName - : current.name; + var preferred = QueryPreferredRawName(current); if (string.IsNullOrWhiteSpace(preferred)) { break; } - var escaped = preferred.QueryIsValidBasicName() - ? preferred - : preferred.ToUnrestrictedName(); - - segments.Push(escaped); - - INamespace next; - - try - { - next = current.owningNamespace; - } - catch (NotSupportedException) - { - break; - } + segments.Push(Escape(preferred)); - current = next; + current = QueryOwningContainer(current); } return string.Join("::", segments); } /// - /// Resolves the local scope of : the first - /// reached by climbing - /// , then - /// , then . Falls - /// back to when nothing is reachable. + /// 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 . @@ -874,135 +817,61 @@ private INamespace GetSourceLocalScope(IElement sourcePoco) if (current is INamespace asNamespace) { - // A Namespace is the local scope only when it has a proper upward - // owningNamespace chain. Anonymous nested namespaces (e.g. an - // OwnedFeatureChain Feature owned via Specialization rather than - // Membership) have a null `owningNamespace`; returning such a namespace - // here gives BuildChain a one-element chain that never reaches the - // reference site's enclosing scope, so name resolution falls through - // to qualifiedName. In that case keep walking via `owner` (which follows - // the owningRelationship → OwningRelatedElement path) to find the - // enclosing reference-site namespace. - INamespace asNamespaceUpward = null; - - try - { - asNamespaceUpward = asNamespace.owningNamespace; - } - catch (NotSupportedException) - { - // owningNamespace not implemented — treat as no upward chain. - } - - if (asNamespaceUpward != null || ReferenceEquals(asNamespace, this.RootNamespace)) + if (QueryOwningContainer(asNamespace) != null || ReferenceEquals(asNamespace, this.RootNamespace)) { return asNamespace; } - IElement asNamespaceOwner; - - try - { - asNamespaceOwner = asNamespace.owner; - } - catch (NotSupportedException) - { - asNamespaceOwner = null; - } + var namespaceOwner = QueryOwnerSafe(asNamespace); - if (asNamespaceOwner == null) + if (namespaceOwner == null) { return asNamespace; } - current = asNamespaceOwner; + current = namespaceOwner; continue; } - INamespace owningNs = null; + var owningNamespace = QueryOwningContainer(current); - try + if (owningNamespace != null) { - owningNs = current.owningNamespace; - } - catch (NotSupportedException) - { - // owningNamespace not implemented — fall through to owner walk. - } - - if (owningNs != null) - { - return owningNs; - } - - IElement nextOwner; - - try - { - nextOwner = current.owner; - } - catch (NotSupportedException) - { - nextOwner = null; + return owningNamespace; } - current = nextOwner; + current = QueryOwnerSafe(current); } return this.RootNamespace; } /// - /// Tri-state result of probing a single scope's simple-name index for a single lexical - /// form. Drives the walk: - /// stops the walk with a hit; stops the - /// walk without a hit (outer scopes are unreachable — the parser's resolution per - /// KerML §8.2.3.5 stops at the first scope binding the name, and that scope's binding - /// is not uniquely the target); continues the walk. + /// Tri-state result of probing one scope for one lexical form. /// private enum SimpleNameResolution { - /// The simple name is not present in this scope's index — keep walking. + /// Name not bound in this scope — keep walking outward. NotBound, - /// The simple name is bound uniquely to the target in this scope — emit the simple name. + /// Name bound uniquely to the target — emit the simple name. Matched, - /// The simple name is bound at this scope but not uniquely to the target — fall back to the qualified name. + /// Name bound to something else — stop; the qualified form is required. Shadowed, } /// - /// Returns the resolution state for in - /// 's simple-name index. The index intentionally indexes BOTH - /// the leaf inherited member AND its redefined ancestors (so a :> ancestor - /// reference can still reach them); the parser however applies the - /// RemoveRedefinedFeatures filter (KerML §8.2.3.5.3 — "in a well-formed - /// Namespace, there is at most one Membership for any given name") so its local - /// resolution sees only the LEAF (the most-derived feature in the redefinition - /// chain). We mirror that filter here: an index entry is reduced to its leaves - /// (elements not transitively redefined by any other element in the entry), and the - /// simple name is emitted only when that leaf set is exactly {target}. + /// 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. - /// - /// Optional feature to filter OUT of the scope's name bucket before leaf reduction — - /// used by the redefinition-resolution path so the local redefining feature does not - /// shadow the redefined target. Pass for the normal resolution - /// path. KerML §8.2.3.5: a redefining feature is not yet a resolvable member of its - /// owning Type at the redefinition site, so it must not participate in name resolution - /// when emitting :>> name. - /// - /// - /// Optional in which the reference being emitted is itself the - /// membership binding . In that scope the target's own entry is - /// excluded (it does not exist at parse time): when - /// nothing else binds the name there, when another - /// element does. Pass when the source is not a self-binding membership. - /// See . - /// + /// Feature to exclude from the bucket, or . + /// Scope whose binding of the target must be ignored, or . /// The resolution state. private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement target, string rawName, IFeature localRedefiner, INamespace selfBindingScope) { @@ -1013,13 +882,8 @@ private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement return SimpleNameResolution.NotBound; } - // In the scope where the reference itself is the binding, the target's entry must be - // ignored: per KerML §8.2.3.5.3 local resolution is membership-based, and the reference's - // own membership does not exist yet when the parser resolves the written name — honouring - // it would make every reference trivially resolvable at depth 0 and hide a shadowing - // declaration further out. Only the TARGET's binding is excluded (mirroring the - // localRedefiner filter): any other element bound under the same name in that scope still - // shadows and forces the qualified form. See QuerySelfBindingScope. + // 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 (selfBindingScope != null && ReferenceEquals(scope, selfBindingScope)) { var isBoundToOtherElement = elements.Any(element => @@ -1030,9 +894,6 @@ private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement : SimpleNameResolution.NotBound; } - // Filter out the local redefining feature so it doesn't shadow the redefined target - // it points to. When the bucket contains only the local redefiner, treat the name as - // unbound in this scope and continue the chain walk outward. var candidates = elements.Where(element => !ReferenceEquals(element, localRedefiner)).ToList(); if (candidates.Count == 0) @@ -1049,12 +910,7 @@ private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement candidates = PreferDirectlyOwnedOverInherited(scope, candidates); - // Reduce to the leaf set: drop any element that is transitively redefined by - // another element in `candidates`. The shadow set is the union of each candidate's - // `AllRedefinedFeatures()` closure (excluding the candidate itself, which the - // operation includes as the seed of the closure). The local redefiner — when - // present — is excluded from this computation entirely so it neither participates - // in shadow accumulation nor in the final leaf count. + // Reduce to redefinition leaves: drop every candidate transitively redefined by another. var shadowed = new HashSet(); foreach (var candidate in candidates.OfType()) @@ -1086,26 +942,10 @@ private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement } /// - /// Narrows to those declared DIRECTLY in - /// when every other candidate is reachable only by INHERITANCE - /// into that scope. - /// Per KerML §8.2.3.5.3 a well-formed has at most one - /// as the local resolution of a given name, and - /// Type::inheritedMembership is removeRedefinedFeatures(inheritableMemberships(…)). - /// So when a scope binds one name to both an owned and an inherited feature, the owned one - /// redefines the inherited one and the inherited membership is not in scope under that name. - /// That redefinition is frequently IMPLIED rather than materialised. The OMG SysML v2 - /// spec, Clause 7.17.2 states that "if the required redefinitions are not explicitly declared - /// for a parameter, then the parameter is considered to implicitly have redefinitions - /// sufficient to meet the stated requirements", and the pilot's XMI export does not write those - /// implied Relationships — so action 'provide power' : 'Provide Power' { in fuelCmd; … } - /// arrives with no and the explicit-redefinition leaf reduction - /// below cannot see the shadowing. Without this step the name looked ambiguous and degraded to - /// 'provide power'::fuelCmd where the canonical source writes fuelCmd. - /// Applied as a resolution-time preference rather than by dropping inherited entries from - /// the index: a redefined feature must stay nameable from the redefining declaration itself - /// (part frontAxleAssembly_c1 :>> frontAxleAssembly, port :>> pe = c1.pb), - /// where the two names differ so no collision arises and nothing may be shadowed. + /// 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. @@ -1117,17 +957,7 @@ private static List PreferDirectlyOwnedOverInherited(INamespace scope, return candidates; } - List supertypes; - - try - { - supertypes = scopeAsType.AllSupertypes(); - } - catch (NotSupportedException) - { - return candidates; - } - + var supertypes = QueryAllSupertypesSafe(scopeAsType); var owned = candidates.Where(candidate => IsDirectlyOwnedBy(scope, candidate)).ToList(); if (owned.Count == 0 || owned.Count == candidates.Count) @@ -1143,9 +973,8 @@ private static List PreferDirectlyOwnedOverInherited(INamespace scope, } /// - /// Determines whether is the member element of one of - /// 's own memberships — that is, declared in the scope rather than - /// imported into or inherited by it. + /// 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. @@ -1163,18 +992,9 @@ private static bool IsDirectlyOwnedBy(INamespace scope, IElement element) } /// - /// Determines whether is the right-hand side of a chain - /// accessor — see grammar rules FeatureChainExpression (KerML §8.2.4.X) and - /// FeatureChain (KerML §8.2.4.3.5). Three patterns match: - /// - /// An sitting as the chain-accessor RHS - /// of a . - /// An at any index after the FIRST - /// in its container's OwnedRelationship list. - /// The of a flow feature whose - /// carries a FlowEndSubsetting — see - /// . - /// + /// 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. @@ -1204,51 +1024,21 @@ private static bool IsChainAccessor(IElement sourcePoco) return true; } - // The FIRST chaining segment is also a chain accessor when the owned chain Feature is the - // target member of a construct that establishes a RELATIVE namespace from a preceding - // expression — the parser resolves even that first segment against the preceding result - // rather than the lexical scope, so the bare simple name is the correct emission. Two - // constructs do this, matching the reference implementation's - // NamespaceUtil.getRelativeNamespaceFor: - // - FeatureChainExpression, relative to its argument expression's result (`= a11.b11.c1`); - // - AssignmentActionUsage, relative to its targetArgument, per - // AssignmentTargetParameter = ( AssignmentTargetBinding '.' )? followed by - // FeatureChainMember (`assign trailer.trailerFrame.coupler.hitch := …`). The pilot - // guards on a non-null targetArgument, so an assignment WITHOUT a target binding - // (`assign a.b := …`) keeps lexical resolution for its first segment; that guard is - // mirrored here. - // A chain owned by a Specialization / ReferenceSubsetting (e.g. a connect end) is not - // relative and keeps lexical resolution for its first segment. + // 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 the result of a preceding expression instead of from lexical containment. A name resolved - /// against such a scope is written as a bare simple name. - /// This mirrors the reference implementation's single decision point, - /// NamespaceUtil.getRelativeNamespaceFor, which recognises exactly two constructs: - /// - /// — relative to the result of its - /// argument expression (a11.b11.c1). - /// — relative to the result of its - /// targetArgument, per AssignmentTargetParameter = ( AssignmentTargetBinding '.' )? - /// followed by FeatureChainMember (assign trailer.trailerFrame.coupler.hitch := …). - /// The reference implementation guards on a non-null target, so an assignment without a binding - /// (assign a.b := …) keeps lexical resolution. - /// - /// The reference implementation additionally guards each arm on the preceding expression being - /// present. The guard is reproduced. Its - /// counterpart (!getArgument().isEmpty()) is NOT: our - /// argument is derived by matching the instantiated type's inputs against redefining owned - /// features, and for a FeatureChainExpression the instantiated type is the library function - /// ControlFunctions::'.', for which that match yields an empty list — so the guard would - /// reject every feature-chain expression and regress a11.b11.c1 style output. The structural - /// test alone is safe here: a FeatureChainExpression exists only to chain onto a preceding operand. - /// Both forms of FeatureChainMember — the owned chain AND the plain - /// memberElement = [QualifiedName] reference — go through this test, matching the reference - /// implementation, which routes every Membership through the same relative-namespace lookup. + /// 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. @@ -1263,14 +1053,10 @@ private static bool EstablishesRelativeNamespace(IElement owner) } /// - /// Determines whether is the redefinedFeature reference of a - /// FlowFeatureRedefinition whose owning also carries a FlowEndSubsetting. - /// Per FlowEnd = ( ownedRelationship += FlowEndSubsetting )? ownedRelationship += FlowFeatureMember - /// (SysML 2.0 §8.2.2.16 Flows Textual Notation), the subsetting is the notational prefix — the flow - /// end reads 'generate torque'.engineTorque. The parser resolves the flow feature against the - /// prefix's type, exactly as for a feature-chain accessor, so the writer must emit the bare simple - /// name rather than a lexically-qualified one. When the optional subsetting is ABSENT the flow - /// feature stands alone and keeps ordinary lexical resolution. + /// 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. @@ -1286,11 +1072,8 @@ private static bool IsFlowFeatureAccessor(IElement sourcePoco) } /// - /// Eagerly walks the model rooted at and builds the - /// simple-name index for every reachable . Reachability follows - /// . for - /// nested namespaces, plus direct ownedImport targets (so imported namespaces are - /// also indexed once). + /// Eagerly indexes every namespace reachable from via + /// containment and imports, then the global namespaces (visible memberships only). /// /// The root namespace. /// The full structural cache. @@ -1302,9 +1085,6 @@ private Dictionary>> B pending.Enqueue((rootNamespace, false)); - // The other available root Namespaces form the global Namespace (KerML §8.2.3.5.2). They are - // indexed as ordinary scopes so their members can be named, and enqueued AFTER the model's own - // root so containment/import scopes are always visited first. foreach (var globalNamespace in this.globalNamespaces) { pending.Enqueue((globalNamespace, true)); @@ -1335,21 +1115,14 @@ private Dictionary>> B } /// - /// Populates with entries for 's - /// owned memberships and direct imports. Imported namespaces are enqueued onto - /// so they too are indexed in the eager pass. + /// 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, each paired with its global flag. - /// - /// when is reached through the global - /// rather than through the serialized model's own containment / import - /// graph. KerML §8.2.3.5.2 admits only the VISIBLE memberships of other root namespaces into the - /// global scope, so non-public entries are excluded there — naming an element bound only by a - /// private membership would emit text the parser cannot resolve. Scopes within the model itself are - /// resolved from the inside and therefore see all of their members. - /// + /// 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) { try @@ -1374,47 +1147,30 @@ private void BuildOwnedAndImportedEntries(INamespace scope, Dictionary IsVisibleWhenGlobal(importedMember, isGlobal))) + foreach (var importedMember in this.QueryVisibleMemberships(namespaceImport.ImportedNamespace, namespaceImport.IsImportAll, isGlobal, [scope])) { AddMembershipEntry(index, importedMember, pending, isGlobal); - // An imported `alias X for Y;` is reachable by its alias name in the - // IMPORTING scope too, so record it against `scope`, not the source. + // An imported alias is reachable by its alias name in the + // IMPORTING scope, so record it against `scope`. this.RecordAliasIfDeclared(scope, importedMember); } } @@ -1435,11 +1191,79 @@ private void BuildOwnedAndImportedEntries(INamespace scope, Dictionary - /// Records an alias X for Y; binding into . A membership declares an - /// alias when it carries an explicit / - /// that differs from the member element's own - /// / — a membership without an override - /// merely re-exposes the element under its own name and is not an alias. + /// 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 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 this.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 . @@ -1477,18 +1301,15 @@ private void RecordAliasIfDeclared(INamespace scope, IMembership membership) } /// - /// Attempts to emit an in-scope ALIAS for — the name introduced by an - /// alias X for Y; declaration reachable from the source scope . Each - /// candidate alias name is validated through the SAME first-binding-wins scope walk as an ordinary - /// simple name (, per KerML §8.2.3.5.4): a scope closer - /// to the reference site that binds the alias string to a DIFFERENT element shadows the alias, and - /// the candidate is rejected — so the emitted text always round-trips to the same element. + /// 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 source scope chain (innermost first). /// The element being referenced. - /// The reference site's source POCO — used to reject the alias DECLARATION itself. - /// Optional feature to exclude from the scope buckets. - /// Optional scope whose own binding of the target must be ignored. + /// The reference site's source POCO — used to reject the alias declaration itself. + /// Feature to exclude from the scope buckets, or . + /// Scope whose binding of the target must be ignored, or . /// On a hit, the escaped alias name to emit. /// when an unambiguous in-scope alias was found. private bool TryResolveViaAlias(IReadOnlyList chain, IElement target, IElement sourcePoco, IFeature localRedefiner, INamespace selfBindingScope, out string matched) @@ -1503,9 +1324,7 @@ private bool TryResolveViaAlias(IReadOnlyList chain, IElement target foreach (var aliasName in candidateAliasNames) { - var escapedAliasName = aliasName.QueryIsValidBasicName() ? aliasName : aliasName.ToUnrestrictedName(); - - if (this.TryResolveSimpleNameAcrossChain(chain, target, aliasName, escapedAliasName, localRedefiner, selfBindingScope, out matched)) + if (this.TryResolveSimpleNameAcrossChain(chain, target, aliasName, Escape(aliasName), localRedefiner, selfBindingScope, out matched)) { return true; } @@ -1515,12 +1334,9 @@ private bool TryResolveViaAlias(IReadOnlyList chain, IElement target } /// - /// Determines whether is the very alias declaration that - /// introduces for . Such a declaration must - /// emit the target's own (qualified) name — resolving it through its own alias would produce the - /// circular alias Torque for Torque;. The exclusion is needed in addition to - /// because an importing scope further out re-exports the same - /// alias, and that scope is not the declaration's own. + /// 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. @@ -1536,12 +1352,10 @@ private static bool DeclaresAlias(IElement sourcePoco, IElement target, string a /// /// Records as a direct (single-hop) re-exporter of - /// . Called once per - /// encountered in the eager build pass. + /// . /// /// The namespace being directly imported. - /// The namespace whose ownedImport contains the - /// targeting . + /// The namespace importing it. private void RecordDirectFacade(INamespace canonicalOwner, INamespace facade) { if (!this.directFacadeIndex.TryGetValue(canonicalOwner, out var facades)) @@ -1554,20 +1368,12 @@ private void RecordDirectFacade(INamespace canonicalOwner, INamespace facade) } /// - /// Determines whether may contribute a name binding to a scope that - /// is being indexed as part of the global . - /// KerML 1.0 §8.2.3.5.2 states that the global "includes all the - /// visible Memberships of all other root Namespaces", and §8.2.3.5.3 defines those as the - /// public owned memberships, the memberships imported through public Imports and — for a - /// — the public inherited memberships. A non-public membership of a library is - /// therefore NOT reachable from the model being written, so indexing it could shorten a reference to - /// a name the parser will not resolve. - /// Within the model's own containment / import graph resolution happens from the INSIDE, where - /// private and protected members are visible, so the filter applies only when - /// is . + /// When is set, admits only PUBLIC memberships and imports — the + /// global namespace contains only the visible memberships of other roots (KerML §8.2.3.5.2), so a + /// name bound privately there would not re-parse. Within the model itself everything is visible. /// - /// The or being considered. - /// Whether the owning scope is reached through the global . + /// The or considered. + /// Whether the owning scope is reached through the global namespace. /// when the relationship may contribute a binding. private static bool IsVisibleWhenGlobal(IRelationship relationship, bool isGlobal) { @@ -1585,38 +1391,21 @@ private static bool IsVisibleWhenGlobal(IRelationship relationship, bool isGloba } /// - /// Indexes the entries inherited from the transitive supertypes of - /// . Bypasses the RemoveRedefinedFeatures filter so - /// references such as :>> elements remain reachable. + /// Indexes the entries inherited from 's transitive supertypes. Deliberately + /// bypasses the RemoveRedefinedFeatures filter so :>> references stay reachable; + /// namespace supertypes are enqueued as scopes in their own right. /// /// The type whose inherited memberships are indexed. /// The destination index. - /// Queue of namespaces yet to be indexed (so supertypes that are - /// also namespaces get their own index built). - /// Whether the owning scope is reached through the global . + /// Queue of namespaces yet to be indexed. + /// Whether the owning scope is reached through the global namespace. private static void BuildInheritedEntries(IType type, Dictionary> index, Queue<(INamespace Scope, bool IsGlobal)> pending, bool isGlobal) { - List supertypes; - - try - { - supertypes = type.AllSupertypes(); - } - catch (NotSupportedException) - { - return; - } - - var inheritableSupertypes = supertypes + var inheritableSupertypes = QueryAllSupertypesSafe(type) .OfType() .Where(candidate => !ReferenceEquals(candidate, type)) .ToList(); - // Namespace supertypes are indexed as scopes in their own right, so a feature this type - // redefines stays nameable through a qualified name anchored on its declaring type. Selected - // with OfType rather than an `is` test inside the loop below: a failing type test admits - // "the value was null" as an explanation, which propagated a spurious null state onto the - // ownedMembership dereference. foreach (var supertypeAsNamespace in inheritableSupertypes.OfType()) { pending.Enqueue((supertypeAsNamespace, isGlobal)); @@ -1639,25 +1428,14 @@ private static void BuildInheritedEntries(IType type, Dictionary - /// Adds the of - /// to under both its short and long name and enqueues the - /// target onto if it is itself an - /// (so nested namespaces are indexed too). - /// - /// Per the metamodel, and - /// are explicit overrides of the member element's - /// declared names within the owning namespace. When the membership does not carry an - /// override (the pilot implementation's XMI never emits memberName / - /// memberShortName on Membership elements), fall back to the target's own - /// / so the simple-name - /// index remains reachable by simple name (e.g. kg, kilogram) for - /// references to imported library elements. - /// + /// 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. - /// Asserts if the target namespace is global of not + /// 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 }) @@ -1668,7 +1446,7 @@ private static void AddMembershipEntry(Dictionary> ind var shortName = !string.IsNullOrWhiteSpace(membership.MemberShortName) ? membership.MemberShortName : target.shortName; - + var longName = !string.IsNullOrWhiteSpace(membership.MemberName) ? membership.MemberName : target.name; @@ -1688,7 +1466,7 @@ private static void AddMembershipEntry(Dictionary> ind /// /// The destination index. /// The simple name to use as the index key. - /// The element to record under . + /// The element to record. private static void AddIndexEntry(Dictionary> index, string simpleName, IElement element) { if (string.IsNullOrWhiteSpace(simpleName) || element == null) diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs index 144e8526..dc520709 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs @@ -616,9 +616,8 @@ internal static void AppendRegularComment(IndentedStringBuilder stringBuilder, s foreach (var rawLine in lines.Where(l => !string.IsNullOrWhiteSpace(l))) { - var line = rawLine.TrimEnd('\r'); - stringBuilder.AppendIndentedLiteral(" * "); - stringBuilder.AppendLine(line); + stringBuilder.AppendIndentedLiteral(" * " + rawLine.TrimEnd()); + stringBuilder.AppendLine(); } stringBuilder.AppendIndentedLiteral(" */"); diff --git a/SysML2.NET/Extend/FeatureExtensions.cs b/SysML2.NET/Extend/FeatureExtensions.cs index 3bf6496d..fe797313 100644 --- a/SysML2.NET/Extend/FeatureExtensions.cs +++ b/SysML2.NET/Extend/FeatureExtensions.cs @@ -535,7 +535,7 @@ internal static string ComputeRedefinedEffectiveShortNameOperation(this IFeature return featureSubject.DeclaredShortName; } - var namingFeature = featureSubject.OwnedRelationship.OfType().FirstOrDefault()?.RedefinedFeature; + var namingFeature = featureSubject.NamingFeature(); return namingFeature?.EffectiveShortName(); } @@ -580,7 +580,7 @@ internal static string ComputeRedefinedEffectiveNameOperation(this IFeature feat return featureSubject.DeclaredName; } - var namingFeature = featureSubject.OwnedRelationship.OfType().FirstOrDefault()?.RedefinedFeature; + var namingFeature = featureSubject.NamingFeature(); return namingFeature?.EffectiveName(); } From aaef7a5628cc2c401a7c88ae421ab97f881c5864 Mon Sep 17 00:00:00 2001 From: atheate Date: Mon, 10 Aug 2026 10:35:50 +0200 Subject: [PATCH 2/2] SQ issues --- SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs | 2 +- .../Writers/NameResolutionCache.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs index af7a9ee8..5a61fe88 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs @@ -87,7 +87,7 @@ internal void EmitAlternativeBody(EncodedTextWriter writer, IClass umlClass, Alt /// The current /// Whether to restore the caller rule after each element /// Whether this is part of a multi-alternative context - private void EmitElements(EncodedTextWriter writer, IClass umlClass, IReadOnlyList elements, RuleGenerationContext ruleGenerationContext, bool restoreCallerPerElement = false, bool isPartOfMultipleAlternative = false) + private void EmitElements(EncodedTextWriter writer, IClass umlClass, List elements, RuleGenerationContext ruleGenerationContext, bool restoreCallerPerElement = false, bool isPartOfMultipleAlternative = false) { var previousSiblings = ruleGenerationContext.CurrentSiblingElements; var previousIndex = ruleGenerationContext.CurrentElementIndex; diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs b/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs index 31e36fc6..95d47598 100644 --- a/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs +++ b/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs @@ -1165,7 +1165,7 @@ private void BuildOwnedAndImportedEntries(INamespace scope, DictionaryWhether 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 IEnumerable QueryVisibleMemberships(INamespace importedNamespace, bool includeAll, bool isGlobal, HashSet excluded) + private static IEnumerable QueryVisibleMemberships(INamespace importedNamespace, bool includeAll, bool isGlobal, HashSet excluded) { if (importedNamespace == null || !excluded.Add(importedNamespace)) { @@ -1250,7 +1250,7 @@ private IEnumerable QueryVisibleMemberships(INamespace importedName case INamespaceImport { ImportedNamespace: { } reExportedNamespace } reExportedImport: - foreach (var reExportedMembership in this.QueryVisibleMemberships(reExportedNamespace, reExportedImport.IsImportAll, isGlobal, excluded)) + foreach (var reExportedMembership in QueryVisibleMemberships(reExportedNamespace, reExportedImport.IsImportAll, isGlobal, excluded)) { yield return reExportedMembership; }