diff --git a/SysML2.NET.CodeGenerator/Extensions/PropertyExtension.cs b/SysML2.NET.CodeGenerator/Extensions/PropertyExtension.cs
index f91aeb062..7910d64f5 100644
--- a/SysML2.NET.CodeGenerator/Extensions/PropertyExtension.cs
+++ b/SysML2.NET.CodeGenerator/Extensions/PropertyExtension.cs
@@ -140,6 +140,62 @@ public static string QueryIfStatementContentForNonEmpty(this IProperty property,
return "THIS WILL PRODUCE COMPILE ERROR";
}
+ ///
+ /// Finds the minimal set of subclasses of (including
+ /// itself) that directly redefine with a <defaultValue>
+ /// equal to . Used by the textual-notation codegen to suppress
+ /// emission of a ?= 'literal' keyword when the property's runtime value matches the
+ /// metamodel default for the concrete subtype.
+ /// "Minimal" means: if a class C is in the set and any of C's ancestors is also in the set, C
+ /// is removed (the C# is check on the ancestor already matches C at runtime).
+ /// Comparison of to the property's defaultValue is
+ /// done via , which already normalises booleans
+ /// ("true" / "false"), enum literals, integers, and strings.
+ ///
+ /// The base property whose redefinitions are being scanned (e.g. Usage::isReference).
+ /// The metaclass under which to scan (typically the textual rule's effective target).
+ /// The default-value string that should trigger inclusion in the exclusion set (e.g. "true").
+ /// The minimal list of redefining classes; empty when no subclass redefines with the matching default.
+ public static IReadOnlyList QuerySubclassesWithMatchingDefault(this IProperty property, IClass rootClass, string literalTriggerValue)
+ {
+ ArgumentNullException.ThrowIfNull(property);
+ ArgumentNullException.ThrowIfNull(rootClass);
+ ArgumentException.ThrowIfNullOrWhiteSpace(literalTriggerValue);
+
+ var allClasses = rootClass.Cache.Values.OfType();
+ var introducers = new List();
+
+ foreach (var candidateClass in allClasses)
+ {
+ if (candidateClass != rootClass && !candidateClass.QueryAllGeneralClassifiers().Contains(rootClass))
+ {
+ continue;
+ }
+
+ foreach (var ownedAttribute in candidateClass.OwnedAttribute)
+ {
+ if (!string.Equals(ownedAttribute.Name, property.Name, StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ var defaultValueAsString = ownedAttribute.QueryDefaultValueAsString();
+
+ if (string.Equals(defaultValueAsString, literalTriggerValue, StringComparison.OrdinalIgnoreCase))
+ {
+ introducers.Add(candidateClass);
+ break;
+ }
+ }
+ }
+
+ // Reduce to the minimal set: drop any class whose ancestor is already present, because the
+ // C# `poco is not IAncestor` check at runtime already covers the descendant.
+ return introducers
+ .Where(candidate => !introducers.Any(other => other != candidate && candidate.QueryAllGeneralClassifiers().Contains(other)))
+ .ToList();
+ }
+
///
/// Returns every from the owning class's OwnedRule that
/// applies to the given derived . The XMI shipped with this
diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs
index d95050a20..a5293391b 100644
--- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs
+++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs
@@ -374,7 +374,19 @@ internal void ProcessAssignmentElement(EncodedTextWriter writer, IClass umlClass
{
if (!isPartOfMultipleAlternative && assignmentElement.Container is not GroupElement { IsOptional: true })
{
- writer.WriteSafeString($"if({targetProperty.QueryIfStatementContentForNonEmpty("poco")}){Environment.NewLine}");
+ // 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`).
+ var exclusionTypes = targetProperty.QuerySubclassesWithMatchingDefault(umlClass, "true");
+ var exclusionClause = exclusionTypes.Count == 0
+ ? string.Empty
+ : $" && poco is not ({string.Join(" or ", exclusionTypes.Select(c => c.QueryFullyQualifiedTypeName()))})";
+
+ writer.WriteSafeString($"if({targetProperty.QueryIfStatementContentForNonEmpty("poco")}{exclusionClause}){Environment.NewLine}");
writer.WriteSafeString($"{{{Environment.NewLine}");
writer.WriteSafeString($"stringBuilder.Append(\" {terminalElement.Value} \");{Environment.NewLine}");
writer.WriteSafeString('}');
diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs
index db10565e9..8496f721b 100644
--- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs
+++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs
@@ -393,7 +393,25 @@ private void ProcessSingleAlternative(EncodedTextWriter writer, IClass umlClass,
}
else
{
- ifStatementContent.Add(property.QueryIfStatementContentForNonEmpty("poco"));
+ 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).
+ if (property.QueryIsBool())
+ {
+ var exclusionTypes = property.QuerySubclassesWithMatchingDefault(umlClass, "true");
+
+ if (exclusionTypes.Count > 0)
+ {
+ condition += $" && poco is not ({string.Join(" or ", exclusionTypes.Select(c => c.QueryFullyQualifiedTypeName()))})";
+ }
+ }
+
+ ifStatementContent.Add(condition);
}
}
@@ -526,6 +544,16 @@ private void ProcessSingleElementAlternatives(EncodedTextWriter writer, IClass u
/// The distinct element types across all alternatives
private void ProcessMixedTypeSingleElementAlternatives(EncodedTextWriter writer, IClass umlClass, IReadOnlyCollection alternatives, RuleGenerationContext ruleGenerationContext, List types)
{
+ if (this.TryEmitSubclassRuleDispatchAlternatives(writer, umlClass, alternatives, ruleGenerationContext))
+ {
+ return;
+ }
+
+ if (this.TryEmitSingleElementOrSameClassRuleAlternatives(writer, umlClass, alternatives, ruleGenerationContext))
+ {
+ return;
+ }
+
if (types.SequenceEqual([typeof(AssignmentElement), typeof(NonTerminalElement)]))
{
foreach (var alternative in alternatives)
@@ -916,6 +944,206 @@ private static bool TryEmitQualifiedNameOrChainAlternatives(EncodedTextWriter wr
return true;
}
+ ///
+ /// 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.
+ ///
+ /// The used to write output
+ /// The related
+ /// The grammar alternatives to process
+ /// The current
+ /// true if the pattern matched and code was emitted; false otherwise
+ private bool TryEmitSubclassRuleDispatchAlternatives(EncodedTextWriter writer, IClass umlClass, IReadOnlyCollection alternatives, RuleGenerationContext ruleGenerationContext)
+ {
+ if (alternatives.Count != 2)
+ {
+ return false;
+ }
+
+ var assignmentAlt = alternatives.FirstOrDefault(alt =>
+ alt.Elements.Count == 1
+ && alt.Elements[0] is AssignmentElement { Operator: "=" });
+
+ var subclassRuleAlt = alternatives.FirstOrDefault(alt =>
+ alt.Elements.Count == 1
+ && alt.Elements[0] is NonTerminalElement);
+
+ if (assignmentAlt == null || subclassRuleAlt == null)
+ {
+ return false;
+ }
+
+ var assignmentElement = (AssignmentElement)assignmentAlt.Elements[0];
+ var nonTerminalElement = (NonTerminalElement)subclassRuleAlt.Elements[0];
+
+ var targetProperty = umlClass.QueryAllProperties().SingleOrDefault(x =>
+ string.Equals(x.Name, assignmentElement.Property, StringComparison.OrdinalIgnoreCase));
+
+ if (targetProperty == null || targetProperty.QueryIsEnumerable())
+ {
+ return false;
+ }
+
+ var referencedRule = ruleGenerationContext.FindRule(nonTerminalElement.Name);
+
+ if (referencedRule == null)
+ {
+ return false;
+ }
+
+ var subclass = RuleQueryUtilities.FindClass(umlClass.Cache, referencedRule.EffectiveTarget);
+
+ if (subclass == null || subclass == umlClass || !subclass.QueryAllGeneralClassifiers().Contains(umlClass))
+ {
+ return false;
+ }
+
+ var variableName = ruleGenerationContext.CurrentVariableName ?? "poco";
+ var subclassTypeName = subclass.QueryFullyQualifiedTypeName();
+ var patternVariableName = $"{subclass.Name.LowerCaseFirstLetter()}{ruleGenerationContext.NarrowedTypeCheckCounter}";
+ ruleGenerationContext.NarrowedTypeCheckCounter++;
+
+ writer.WriteSafeString($"if ({variableName} is {subclassTypeName} {patternVariableName}){Environment.NewLine}");
+ writer.WriteSafeString($"{{{Environment.NewLine}");
+ writer.WriteSafeString($"{subclass.Name}TextualNotationBuilder.Build{nonTerminalElement.Name}({patternVariableName}, writerContext, stringBuilder);{Environment.NewLine}");
+ writer.WriteSafeString($"}}{Environment.NewLine}");
+
+ // A NonTerminal-valued assignment emits its own null guard inside ProcessAssignmentElement;
+ // only a value-literal assignment (e.g. [QualifiedName]) needs the guard supplied here.
+ if (assignmentElement.Value is ValueLiteralElement)
+ {
+ writer.WriteSafeString($"else if ({targetProperty.QueryIfStatementContentForNonEmpty(variableName)}){Environment.NewLine}");
+ }
+ else
+ {
+ writer.WriteSafeString($"else{Environment.NewLine}");
+ }
+
+ writer.WriteSafeString($"{{{Environment.NewLine}");
+ this.ProcessAssignmentElement(writer, umlClass, ruleGenerationContext, assignmentElement, true);
+ writer.WriteSafeString($"{Environment.NewLine}}}{Environment.NewLine}");
+
+ return true;
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The used to write output
+ /// The related
+ /// The grammar alternatives to process
+ /// The current
+ /// true if the pattern matched and code was emitted; false otherwise
+ private bool TryEmitSingleElementOrSameClassRuleAlternatives(EncodedTextWriter writer, IClass umlClass, IReadOnlyCollection alternatives, RuleGenerationContext ruleGenerationContext)
+ {
+ if (alternatives.Count != 2)
+ {
+ return false;
+ }
+
+ var collectionAlt = alternatives.FirstOrDefault(alt =>
+ alt.Elements.Count == 1
+ && alt.Elements[0] is AssignmentElement { Operator: "+=", Value: NonTerminalElement });
+
+ var sameClassRuleAlt = alternatives.FirstOrDefault(alt =>
+ alt.Elements.Count == 1
+ && alt.Elements[0] is NonTerminalElement);
+
+ if (collectionAlt == null || sameClassRuleAlt == null)
+ {
+ return false;
+ }
+
+ var assignmentElement = (AssignmentElement)collectionAlt.Elements[0];
+ var nonTerminalElement = (NonTerminalElement)sameClassRuleAlt.Elements[0];
+
+ var referencedRule = ruleGenerationContext.FindRule(nonTerminalElement.Name);
+
+ if (referencedRule == null || !string.Equals(referencedRule.EffectiveTarget, umlClass.Name, StringComparison.Ordinal))
+ {
+ 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.
+ var elementValueNonTerminal = (NonTerminalElement)assignmentElement.Value;
+
+ var reconsumesSameElements = referencedRule.Alternatives
+ .SelectMany(alt => alt.Elements)
+ .OfType()
+ .Any(innerAssignment => innerAssignment is { Operator: "+=", Value: NonTerminalElement innerNonTerminal }
+ && string.Equals(innerAssignment.Property, assignmentElement.Property, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(innerNonTerminal.Name, elementValueNonTerminal.Name, StringComparison.Ordinal));
+
+ if (!reconsumesSameElements)
+ {
+ return false;
+ }
+
+ var targetProperty = umlClass.QueryAllProperties().SingleOrDefault(x =>
+ string.Equals(x.Name, assignmentElement.Property, StringComparison.OrdinalIgnoreCase));
+
+ if (targetProperty == null || !targetProperty.QueryIsEnumerable())
+ {
+ return false;
+ }
+
+ var elementTypeName = ResolveAssignmentTargetTypeName(assignmentElement, umlClass, ruleGenerationContext);
+ var elementRule = ruleGenerationContext.FindRule(elementValueNonTerminal.Name);
+ var elementTypeTarget = elementRule?.EffectiveTarget;
+ var sameClassRuleCall = ResolveBuilderCall(umlClass, nonTerminalElement, referencedRule.EffectiveTarget, ruleGenerationContext);
+
+ if (elementTypeName == null || elementTypeTarget == null || sameClassRuleCall == null)
+ {
+ return false;
+ }
+
+ this.DeclareAllRequiredCursors(writer, umlClass, collectionAlt, ruleGenerationContext);
+ var cursor = ruleGenerationContext.DefinedCursors.Single(x => x.ApplicableRuleElements.Contains(assignmentElement));
+
+ var variableName = ruleGenerationContext.CurrentVariableName ?? "poco";
+ var propertyAccessor = targetProperty.QueryPropertyNameBasedOnUmlProperties();
+ var elementVariableName = $"elementAs{elementTypeTarget}";
+
+ var singleElementBuilderCall = elementTypeTarget == ruleGenerationContext.NamedElementToGenerate.Name
+ ? $"Build{elementValueNonTerminal.Name}({elementVariableName}, writerContext, stringBuilder);"
+ : $"{elementTypeTarget}TextualNotationBuilder.Build{elementValueNonTerminal.Name}({elementVariableName}, writerContext, stringBuilder);";
+
+ writer.WriteSafeString($"if ({variableName}.{propertyAccessor}.OfType<{elementTypeName}>().Count() == 1 && {cursor.CursorVariableName}.Current is {elementTypeName} {elementVariableName}){Environment.NewLine}");
+ writer.WriteSafeString($"{{{Environment.NewLine}");
+ writer.WriteSafeString($"{singleElementBuilderCall}{Environment.NewLine}");
+ writer.WriteSafeString($"{cursor.CursorVariableName}.Move();{Environment.NewLine}");
+ writer.WriteSafeString($"}}{Environment.NewLine}");
+ writer.WriteSafeString($"else{Environment.NewLine}");
+ writer.WriteSafeString($"{{{Environment.NewLine}");
+ writer.WriteSafeString($"{sameClassRuleCall}{Environment.NewLine}");
+ writer.WriteSafeString($"}}{Environment.NewLine}");
+
+ return true;
+ }
+
///
/// Emits the terminal-vs-body pattern where the first alternative is a single terminal
/// (e.g., ;) and the second alternative is a body with collection assignments or
@@ -1039,7 +1267,27 @@ private void EmitTerminalVsBodyWithCollectionNonTerminals(EncodedTextWriter writ
var propertyAccessName = targetProperty.QueryPropertyNameBasedOnUmlProperties();
- writer.WriteSafeString($"if(writerContext.CursorCache.GetOrCreateCursor(poco.Id, \"{targetProperty.Name}\", poco.{propertyAccessName}).Current == null){Environment.NewLine}");
+ // 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.
+ var requiresIsValidForGuard = IsGuardedBodyItemRule(collectionNonTerminals[0].Name);
+ var guardCallSuffix = requiresIsValidForGuard
+ ? $".IsValidFor{collectionNonTerminals[0].Name}(writerContext)"
+ : string.Empty;
+
+ if (requiresIsValidForGuard)
+ {
+ writer.WriteSafeString($"if (writerContext.CursorCache.GetOrCreateCursor(poco.Id, \"{targetProperty.Name}\", poco.{propertyAccessName}).Current is not SysML2.NET.Core.POCO.Root.Elements.IRelationship emptyBodyCandidate || !emptyBodyCandidate{guardCallSuffix}){Environment.NewLine}");
+ }
+ else
+ {
+ writer.WriteSafeString($"if(writerContext.CursorCache.GetOrCreateCursor(poco.Id, \"{targetProperty.Name}\", poco.{propertyAccessName}).Current == null){Environment.NewLine}");
+ }
writer.WriteSafeString($"{{{Environment.NewLine}");
writer.WriteSafeString($"stringBuilder.AppendLine(\"{terminalValue}\");{Environment.NewLine}");
@@ -1063,7 +1311,15 @@ private void EmitTerminalVsBodyWithCollectionNonTerminals(EncodedTextWriter writ
var perItemCall = ResolveBuilderCall(umlClass, collectionNonTerminal, typeTarget, ruleGenerationContext);
- writer.WriteSafeString($"while ({cursorVarName}.Current != null){Environment.NewLine}");
+ if (requiresIsValidForGuard)
+ {
+ writer.WriteSafeString($"while ({cursorVarName}.Current is SysML2.NET.Core.POCO.Root.Elements.IRelationship loopBodyItem && loopBodyItem{guardCallSuffix}){Environment.NewLine}");
+ }
+ else
+ {
+ writer.WriteSafeString($"while ({cursorVarName}.Current != null){Environment.NewLine}");
+ }
+
writer.WriteSafeString($"{{{Environment.NewLine}");
if (perItemCall != null)
@@ -1178,5 +1434,28 @@ private void EmitTerminalVsBodyWithSingleNonTerminal(EncodedTextWriter writer, I
writer.WriteSafeString($"}}{Environment.NewLine}");
}
+
+ ///
+ /// 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.
+ ///
+ /// The KEBNF rule name of the body item (e.g. DefinitionBodyItem)
+ /// true if the codegen should emit the guarded form
+ private static bool IsGuardedBodyItemRule(string bodyItemRuleName)
+ {
+ return string.Equals(bodyItemRuleName, "DefinitionBodyItem", StringComparison.Ordinal)
+ || string.Equals(bodyItemRuleName, "InterfaceBodyItem", StringComparison.Ordinal);
+ }
}
}
diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1a-Parts Tree.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1a-Parts Tree.sysml
new file mode 100644
index 000000000..df22bd851
--- /dev/null
+++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1a-Parts Tree.sysml
@@ -0,0 +1,105 @@
+package '1a-Parts Tree' {
+ private import SI::kg;
+ package Definitions {
+ part def Vehicle {
+ attribute mass :> ISQ::mass {
+ doc
+ /*
+ * The 'mass' attribute property is declared here to be a
+ * specialization (subset) of the general 'mass' quantity
+ * from the 'ISQ' (International System of Quantities)
+ * library model.
+ */
+
+ }
+ }
+ part def AxleAssembly;
+ part def Axle {
+ attribute mass :> ISQ::mass;
+ }
+ part def FrontAxle :> Axle {
+ attribute steeringAngle: ScalarValues::Real;
+ }
+ part def Wheel;
+ }
+ package Usages {
+ private import Definitions::* {
+ /*
+ * A "private" private import makes the imported names private to the
+ * imported package.
+ */
+ }
+ ref part vehicle1: Vehicle {
+ /*
+ * 'vehicle1' is a package-owned part of type Vehicle.
+ */
+ attribute mass :>> Vehicle::mass = 1750[kg] {
+ /*
+ * This redefines the 'mass' attribute property from 'Vehicle' to
+ * give it a fixed attribute.
+ */
+ }
+ part frontAxleAssembly: AxleAssembly {
+ /*
+ * 'frontAxleAssembly' is a nested part of part 'vehicle1'.
+ * It is a composite part of the containing part.
+ * (And similarly for 'rearAxleAssembly'.)
+ */
+ part frontAxle: Axle;
+ part frontWheel: Wheel[2] ordered {
+ /*
+ * 'frontWheel' is a nested part of type 'Wheel' with
+ * multiplicity "2". This means that this axle assembly
+ * must have exactly two wheels. However, there is still
+ * only one 'frontWheel' part. The part is "ordered",
+ * so that the first wheel can be distinguished from the
+ * second.
+ */
+ }
+ }
+ part rearAxleAssembly: AxleAssembly {
+ part rearAxle: Axle;
+ part rearWheel: Wheel[2] ordered;
+ }
+ }
+ ref part vehicle1_c1: Vehicle {
+ /*
+ * 'vehicle1_c1' is a modified copy of 'vehicle1'. There is no
+ * connection between this copy and the original version in the
+ * model.
+ */
+ attribute mass :>> Vehicle::mass = 2000[kg] {
+ /*
+ * The mass attribute has been modified.
+ */
+ }
+ part frontAxleAssembly: AxleAssembly {
+ part frontAxle: FrontAxle {
+ /*
+ * The part 'frontAxle' has been modified to have type 'FrontAxle'.
+ */
+ }
+ part frontWheel: Wheel[2] ordered {
+ /*
+ * The parts 'frontWheel_1' and 'frontWheel_2' have been added
+ * as subsets of 'frontWheel'. These are separate parts from
+ * 'frontWheel', but essentially provide alternate names for
+ * each of the two wheels, as given by their defining expressions.
+ */
+ }
+ part frontWheel_1 :> frontWheel = frontWheel#(1);
+ part frontWheel_2 :> frontWheel = frontWheel#(2);
+ }
+ part rearAxleAssembly: AxleAssembly {
+ /*
+ * 'rearAxleAssembly' has also been modified to add subsetting parts
+ * for 'rearWheel'.
+ */
+ part rearAxle: Axle;
+ part rearWheel: Wheel[2] ordered;
+ part rearWheel_1 :> rearWheel = rearWheel#(1);
+ part rearWheel_2 :> rearWheel = rearWheel#(2);
+ }
+ }
+ }
+}
diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1c-Parts Tree Redefinition.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1c-Parts Tree Redefinition.sysml
new file mode 100644
index 000000000..f77508b30
--- /dev/null
+++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1c-Parts Tree Redefinition.sysml
@@ -0,0 +1,75 @@
+package '1c-Parts Tree Redefinition' {
+ private import SI::kg;
+ package Definitions {
+ part def Vehicle {
+ attribute mass :> ISQ::mass;
+ }
+ part def AxleAssembly;
+ part def Axle {
+ attribute mass :> ISQ::mass;
+ }
+ part def FrontAxle :> Axle {
+ attribute steeringAngle: ScalarValues::Real;
+ }
+ part def Wheel;
+ }
+ package Usages {
+ private import Definitions::*;
+ ref part vehicle1: Vehicle {
+ attribute mass :>> Vehicle::mass default = 1750[kg] {
+ doc
+ /*
+ * The mass attribute is redefined to give it a default value.
+ */
+
+ }
+ part frontAxleAssembly: AxleAssembly {
+ part frontAxle: Axle;
+ part frontWheel: Wheel[2] ordered;
+ }
+ part rearAxleAssembly: AxleAssembly {
+ part rearAxle: Axle;
+ part rearWheel: Wheel[2] ordered;
+ }
+ }
+ ref part vehicle1_c1 :> vehicle1 {
+ /*
+ * 'vehicle1_c1' is a specialization of 'vehicle1' (technically
+ * a subset). It inherits all the parts of 'vehicle1' and
+ * only needs to specify additional or redefined parts.
+ */
+ attribute mass :>> vehicle1::mass = 2000[kg] {
+ /*
+ * The mass is further redefined to override the default value
+ * with a bound value for 'vehicle_c1'.
+ */
+ }
+ part frontAxleAssembly_c1 :>> frontAxleAssembly {
+ part frontAxle_c1: FrontAxle :>> frontAxle {
+ /*
+ * 'frontAxle_c1' redefines 'frontAxleAssembly'::'frontAxle'
+ * to give it a new name and the specialized type
+ * 'FrontAxle'.
+ */
+ }
+ /*
+ * 'frontWheel' is inherited from 'vehicle1'::'frontAxleAssembly',
+ * allowing it to be used in the following part declarations.
+ */
+ part frontWheel_1 :> frontWheel = frontWheel#(1);
+ part frontWheel_2 :> frontWheel = frontWheel#(2);
+ }
+ part rearAxleAssembly_c1 :>> rearAxleAssembly {
+ part rearAxle_c1 :>> rearAxle {
+ /*
+ * 'rearAxle_c1' redefines 'rearAxleAssembly'::'rearAxle'
+ * to give it a new name. It inherits the type 'Axle'
+ * from the redefined part.
+ */
+ }
+ part rearWheel_1 :> rearWheel = rearWheel#(1);
+ part rearWheel_2 :> rearWheel = rearWheel#(2);
+ }
+ }
+ }
+}
diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1d-Parts Tree with Reference.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1d-Parts Tree with Reference.sysml
new file mode 100644
index 000000000..bf5ad2c62
--- /dev/null
+++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/01-Parts Tree/1d-Parts Tree with Reference.sysml
@@ -0,0 +1,44 @@
+package '1d-Parts Tree with Reference' {
+ package Definitions {
+ part def Vehicle;
+ part def Trailer;
+ part def TrailerHitch;
+ part def HitchBall;
+ part def TrailerCoupler;
+ }
+ package Usages {
+ private import Definitions::*;
+ ref part vehicle_trailer_system {
+ part vehicle1_c1: Vehicle {
+ ref hitchBall: HitchBall {
+ /*
+ * 'vehicle1_c1'::'hitchBall' is a reference property that
+ * references a hitch ball that is not part of this vehicle.
+ * If 'vehicle1_c1' is removed or destroyed, this does not
+ * effect the hitchBall referenced here.
+ */
+ }
+ }
+ ref bind vehicle1_c1.hitchBall = trailerHitch.hitchBall {
+ /*
+ * This is a binding connector between the 'hitchBall' in 'vehicle1_c1'
+ * and the 'hitchBall' in 'trailerHitch'.
+ */
+ }
+ part trailerHitch: TrailerHitch {
+ part hitchBall: HitchBall;
+ part trailerCoupler: TrailerCoupler;
+ }
+ part trailer1: Trailer {
+ ref trailerCoupler: TrailerCoupler = trailerHitch.trailerCoupler {
+ /*
+ * This is a shorthand for a binding connector between the
+ * 'trailerCoupler' here and the 'trailerCoupler' in 'trailerHitch'.
+ * The binding connector is now contained within the 'trailer1'
+ * part, though, rather than being at the system level.
+ */
+ }
+ }
+ }
+ }
+}
diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/02-Parts Interconnection/2a-Parts Interconnection.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/02-Parts Interconnection/2a-Parts Interconnection.sysml
new file mode 100644
index 000000000..755f93651
--- /dev/null
+++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/02-Parts Interconnection/2a-Parts Interconnection.sysml
@@ -0,0 +1,153 @@
+package '2a-Parts Interconnection' {
+ public import Definitions::*;
+ public import Usages::*;
+ package Definitions {
+ port def FuelCmdPort;
+ port def DrivePwrPort;
+ port def ClutchPort;
+ port def ShaftPort_a;
+ port def ShaftPort_b;
+ port def ShaftPort_c;
+ port def ShaftPort_d;
+ port def DiffPort;
+ port def AxlePort;
+ port def AxleToWheelPort;
+ port def WheelToAxlePort;
+ port def WheelToRoadPort;
+ port def VehicleToRoadPort {
+ /*
+ * A port definition can have nested ports.
+ */
+ port wheelToRoadPort: WheelToRoadPort[2];
+ }
+ part def VehicleA {
+ ref port fuelCmdPort: FuelCmdPort;
+ ref port vehicleToRoadPort: VehicleToRoadPort;
+ }
+ part def AxleAssembly;
+ part def RearAxleAssembly :> AxleAssembly {
+ ref port shaftPort_d: ShaftPort_d;
+ }
+ part def Axle;
+ part def RearAxle :> Axle;
+ part def HalfAxle {
+ ref port axleToDiffPort: AxlePort;
+ ref port axleToWheelPort: AxleToWheelPort;
+ }
+ part def Engine {
+ ref port fuelCmdPort: FuelCmdPort;
+ ref port drivePwrPort: DrivePwrPort;
+ }
+ part def Transmission {
+ ref port clutchPort: ClutchPort;
+ ref port shaftPort_a: ShaftPort_a;
+ }
+ part def Driveshaft {
+ ref port shaftPort_b: ShaftPort_b;
+ ref port shaftPort_c: ShaftPort_c;
+ }
+ part def Differential {
+ /*
+ * Ports do not have to be defined on part defs.
+ * They can be added directly to their usages.
+ */
+ }
+ part def Wheel;
+ interface def EngineToTransmissionInterface {
+ /*
+ * The ends of an interface definition are always ports.
+ */
+ end drivePwrPort: DrivePwrPort;
+ end clutchPort: ClutchPort;
+ }
+ interface def DriveshaftInterface {
+ end shaftPort_a: ShaftPort_a;
+ end shaftPort_d: ShaftPort_d;
+ ref driveshaft: Driveshaft {
+ /*
+ * 'driveshaft' is a reference to the driveshaft that will
+ * act as the "interface medium" for this interface.
+ */
+ }
+ connect shaftPort_a to driveshaft.shaftPort_b {
+ /*
+ * The two ends of 'DriveShaftInterface' are always connected
+ * via the referenced 'driveshaft'.
+ */
+ }
+ connect driveshaft.shaftPort_c to shaftPort_d;
+ }
+ }
+ package Usages {
+ ref part vehicle1_c1: VehicleA {
+ ref bind fuelCmdPort = engine.fuelCmdPort;
+ part engine: Engine;
+ interface : EngineToTransmissionInterface connect engine.drivePwrPort to transmission.clutchPort {
+ /*
+ * A usage of an interface definition connects two ports relative to
+ * a containing context.
+ */
+ }
+ part transmission: Transmission;
+ part driveshaft: Driveshaft {
+ /*
+ * This 'driveshaft' is the part of 'vehicle1_c1' that will act as the
+ * interface medium in the following 'DriveshaftInterface' usage.
+ */
+ }
+ interface : DriveshaftInterface connect transmission.shaftPort_a to rearAxleAssembly.shaftPort_d {
+ ref :>> driveshaft = vehicle1_c1.driveshaft {
+ /*
+ * The reference property from 'DriveshaftInterface' is redefined
+ * in order to bind it to the appropriate part of 'vehicle1_c1'.
+ */
+ }
+ }
+ part rearAxleAssembly: RearAxleAssembly {
+ ref bind shaftPort_d = differential.shaftPort_d;
+ part differential: Differential {
+ ref port shaftPort_d: ShaftPort_d {
+ /*
+ * If the part def has no ports, then they can be defined directly in
+ * a usage of the part def.
+ */
+ }
+ ref port leftDiffPort: DiffPort;
+ ref port rightDiffPort: DiffPort;
+ }
+ interface differential.leftDiffPort to rearAxle.leftHalfAxle.axleToDiffPort {
+ /*
+ * A connection can be to a port that is arbitrarily deeply nested, on either end.
+ */
+ }
+ interface differential.rightDiffPort to rearAxle.rightHalfAxle.axleToDiffPort;
+ part rearAxle: RearAxle {
+ part leftHalfAxle: HalfAxle;
+ part rightHalfAxle: HalfAxle;
+ }
+ connect rearAxle.leftHalfAxle.axleToWheelPort to leftWheel.wheelToAxlePort;
+ connect rearAxle.rightHalfAxle.axleToWheelPort to rightWheel.wheelToAxlePort;
+ part rearWheel: Wheel[2] ordered;
+ /*
+ * The two rear wheels of 'rearAxleAssembly' must be given
+ * their own names in order to be referenced in connections.
+ * (":>" is a shorthand here for "subsets".)
+ */
+ part leftWheel :> rearWheel = rearWheel#(1) {
+ ref port wheelToAxlePort: WheelToAxlePort;
+ ref port wheelToRoadPort: WheelToRoadPort;
+ }
+ part rightWheel :> rearWheel = rearWheel#(2) {
+ ref port wheelToAxlePort: WheelToAxlePort;
+ ref port wheelToRoadPort: WheelToRoadPort;
+ }
+ }
+ ref bind rearAxleAssembly.leftWheel.wheelToRoadPort = vehicleToRoadPort.leftWheelToRoadPort;
+ ref bind rearAxleAssembly.rightWheel.wheelToRoadPort = vehicleToRoadPort.rightWheelToRoadPort;
+ ref port vehicleToRoadPort :>> VehicleA::vehicleToRoadPort {
+ port leftWheelToRoadPort :> wheelToRoadPort = wheelToRoadPort#(1);
+ port rightWheelToRoadPort :> wheelToRoadPort = wheelToRoadPort#(2);
+ }
+ }
+ }
+}
diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/02-Parts Interconnection/2c-Parts Interconnection-Multiple Decompositions.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/02-Parts Interconnection/2c-Parts Interconnection-Multiple Decompositions.sysml
new file mode 100644
index 000000000..7c60b604f
--- /dev/null
+++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/02-Parts Interconnection/2c-Parts Interconnection-Multiple Decompositions.sysml
@@ -0,0 +1,76 @@
+package '2c-Parts Interconnection-Multiple Decompositions' {
+ part def A1;
+ part def B11 {
+ ref port pe;
+ }
+ part def B12 {
+ ref port pf;
+ }
+ part def B21 {
+ ref port pg;
+ }
+ part def B22 {
+ ref port ph;
+ }
+ part def C1 {
+ ref port pa;
+ ref port pb;
+ }
+ part def C2 {
+ ref port pc;
+ }
+ part def C3 {
+ ref port pd;
+ }
+ part def C4;
+ ref part a11: A1 {
+ doc
+ /*
+ * Decomposition 1 - Subsystems b11, b12
+ */
+
+ part b11: B11 {
+ part c1: C1;
+ part c2: C2;
+ connect c1.pa to c2.pc;
+ ref port :>> pe = c1.pb {
+ doc
+ /*
+ * This combines the definition of a port with a binding
+ * connector. (It is the same notation used to bind a
+ * attribute to a attribute property or a reference to a reference
+ * property.)
+ */
+
+ }
+ }
+ part b12: B12 {
+ part c3: C3;
+ part c4: C4;
+ ref port :>> pf = c3.pd;
+ }
+ connect b11.pe to b12.pf;
+ }
+ ref part a12: A1 {
+ doc
+ /*
+ * Decomposition 2 - Assemblies b21, b22
+ */
+
+ part b21: B21 {
+ /*
+ * The c-level entities are already composite parts within
+ * a11, so they cannot also be composite parts within a12.
+ */
+ ref c1: C1 = a11.b11.c1;
+ ref c3: C3 = a11.b12.c3;
+ connect c1.pb to c3.pd;
+ ref port :>> pg = c1.pa;
+ }
+ part b22: B22 {
+ ref c2: C2 = a11.b11.c2;
+ ref c4: C4 = a11.b12.c4;
+ ref port :>> ph = c2.pc;
+ }
+ }
+}
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 d7644f6b8..f5a088659 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
@@ -61,6 +61,27 @@
Always
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ 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
new file mode 100644
index 000000000..5decf171d
--- /dev/null
+++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/02-Parts Interconnection/2a-Parts Interconnection.sysmlx
@@ -0,0 +1,997 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Validation/02-Parts Interconnection/2c-Parts Interconnection-Multiple Decompositions.sysmlx b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/02-Parts Interconnection/2c-Parts Interconnection-Multiple Decompositions.sysmlx
new file mode 100644
index 000000000..5ae0dd743
--- /dev/null
+++ b/SysML2.NET.Serializer.TextualNotation.Tests/Validation/02-Parts Interconnection/2c-Parts Interconnection-Multiple Decompositions.sysmlx
@@ -0,0 +1,517 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs b/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs
index 3768e5f03..dba1e5439 100644
--- a/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs
+++ b/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs
@@ -22,8 +22,6 @@ namespace SysML2.NET.Serializer.TextualNotation.Tests.Writers
{
using System;
using System.IO;
- using System.Linq;
- using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -41,6 +39,8 @@ public class TextualNotationValidationTestFixture
[TestCase("01-Parts Tree", "1a-Parts Tree.sysmlx")]
[TestCase("01-Parts Tree", "1c-Parts Tree Redefinition.sysmlx")]
[TestCase("01-Parts Tree", "1d-Parts Tree with Reference.sysmlx")]
+ [TestCase("02-Parts Interconnection", "2a-Parts Interconnection.sysmlx")]
+ [TestCase("02-Parts Interconnection", "2c-Parts Interconnection-Multiple Decompositions.sysmlx")]
public async Task VerifyValidationTextualNotationXmi(string folderName, string fileName)
{
var loggerFactory = LoggerFactory.Create(builder =>
@@ -80,6 +80,11 @@ public async Task VerifyValidationTextualNotationXmi(string folderName, string f
TestContext.WriteLine("=== Textual Notation Output ===");
TestContext.WriteLine(textualNotation);
TestContext.WriteLine("=== End ===");
+
+ var expectedFilePath = Path.Combine(TestContext.CurrentContext.TestDirectory, "Expected", folderName, fileName.Replace(".sysmlx", ".sysml"));
+
+ var expectedContent = await File.ReadAllTextAsync(expectedFilePath);
+ Assert.That(expectedContent, Is.EqualTo(textualNotation));
}
}
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ClassifierTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ClassifierTextualNotationBuilder.cs
index 42ee426a6..1f62924e5 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ClassifierTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ClassifierTextualNotationBuilder.cs
@@ -85,7 +85,7 @@ public static void BuildClassifierDeclaration(SysML2.NET.Core.POCO.Core.Classifi
{
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- if (poco.IsSufficient)
+ if (poco.IsSufficient && poco is not (SysML2.NET.Core.POCO.Systems.Connections.IConnectionDefinition))
{
stringBuilder.Append(" all ");
stringBuilder.Append(' ');
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs
index 36132f046..594623e39 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs
@@ -714,23 +714,17 @@ public static void BuildChainingPart(SysML2.NET.Core.POCO.Core.Features.IFeature
{
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
stringBuilder.Append("chains ");
- if (ownedRelationshipCursor.Current != null)
+ if (poco.OwnedRelationship.OfType().Count() == 1 && ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IFeatureChaining elementAsFeatureChaining)
{
-
- if (ownedRelationshipCursor.Current != null)
- {
-
- if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Core.Features.IFeatureChaining elementAsFeatureChaining)
- {
- FeatureChainingTextualNotationBuilder.BuildOwnedFeatureChaining(elementAsFeatureChaining, writerContext, stringBuilder);
- }
- }
+ FeatureChainingTextualNotationBuilder.BuildOwnedFeatureChaining(elementAsFeatureChaining, writerContext, stringBuilder);
+ ownedRelationshipCursor.Move();
}
else
{
BuildFeatureChain(poco, writerContext, stringBuilder);
}
+
}
///
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ImportTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ImportTextualNotationBuilder.cs
index 0274943e0..1a927443b 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ImportTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ImportTextualNotationBuilder.cs
@@ -67,7 +67,7 @@ public static void BuildImport(SysML2.NET.Core.POCO.Root.Namespaces.IImport poco
stringBuilder.Append(' ');
stringBuilder.Append("import ");
- if (poco.IsImportAll)
+ if (poco.IsImportAll && poco is not (SysML2.NET.Core.POCO.Systems.Views.IExpose))
{
stringBuilder.Append(" all ");
stringBuilder.Append(' ');
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MembershipTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MembershipTextualNotationBuilder.cs
index 2df466889..c5b5e01f4 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MembershipTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MembershipTextualNotationBuilder.cs
@@ -99,28 +99,16 @@ public static void BuildAliasMember(SysML2.NET.Core.POCO.Root.Namespaces.IMember
/// The that accumulates the entire textual notation with indentation
public static void BuildFeatureChainMember(SysML2.NET.Core.POCO.Root.Namespaces.IMembership poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder)
{
-
- if (poco.MemberElement != null)
+ if (poco is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembership0)
+ {
+ OwningMembershipTextualNotationBuilder.BuildOwnedFeatureChainMember(owningMembership0, writerContext, stringBuilder);
+ }
+ else if (poco.MemberElement != null)
{
SharedTextualNotationBuilder.AppendQualifiedName(stringBuilder, poco.MemberElement, writerContext, poco);
stringBuilder.Append(' ');
}
- else
- {
- var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement);
- if (ownedRelatedElementCursor.Current != null)
- {
-
- if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Core.Features.IFeature elementAsFeature)
- {
- FeatureTextualNotationBuilder.BuildOwnedFeatureChain(elementAsFeature, writerContext, stringBuilder);
- ownedRelatedElementCursor.Move();
-
- }
- }
-
- }
}
///
@@ -169,8 +157,11 @@ public static void BuildElementReferenceMember(SysML2.NET.Core.POCO.Root.Namespa
/// The that accumulates the entire textual notation with indentation
public static void BuildInstantiatedTypeMember(SysML2.NET.Core.POCO.Root.Namespaces.IMembership poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder)
{
-
- if (poco.MemberElement != null)
+ if (poco is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembership0)
+ {
+ OwningMembershipTextualNotationBuilder.BuildOwnedFeatureChainMember(owningMembership0, writerContext, stringBuilder);
+ }
+ else
{
if (poco.MemberElement != null)
@@ -180,22 +171,7 @@ public static void BuildInstantiatedTypeMember(SysML2.NET.Core.POCO.Root.Namespa
}
}
- else
- {
- var ownedRelatedElementCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelatedElement", poco.OwnedRelatedElement);
- if (ownedRelatedElementCursor.Current != null)
- {
-
- if (ownedRelatedElementCursor.Current is SysML2.NET.Core.POCO.Core.Features.IFeature elementAsFeature)
- {
- FeatureTextualNotationBuilder.BuildOwnedFeatureChain(elementAsFeature, writerContext, stringBuilder);
- ownedRelatedElementCursor.Move();
-
- }
- }
-
- }
}
///
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TypeTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TypeTextualNotationBuilder.cs
index 2bd68f70f..13235b390 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TypeTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/TypeTextualNotationBuilder.cs
@@ -42,7 +42,7 @@ public static partial class TypeTextualNotationBuilder
/// The that accumulates the entire textual notation with indentation
public static void BuildDefinitionBody(SysML2.NET.Core.POCO.Core.Types.IType poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder)
{
- if (writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship).Current == null)
+ if (writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship).Current is not SysML2.NET.Core.POCO.Root.Elements.IRelationship emptyBodyCandidate || !emptyBodyCandidate.IsValidForDefinitionBodyItem(writerContext))
{
stringBuilder.AppendLine(";");
}
@@ -52,7 +52,7 @@ public static void BuildDefinitionBody(SysML2.NET.Core.POCO.Core.Types.IType poc
stringBuilder.AppendLine("{");
stringBuilder.IncreaseIndent();
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current != null)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Elements.IRelationship loopBodyItem && loopBodyItem.IsValidForDefinitionBodyItem(writerContext))
{
BuildDefinitionBodyItem(poco, writerContext, stringBuilder);
}
@@ -83,7 +83,7 @@ public static void BuildDefinitionBodyItem(SysML2.NET.Core.POCO.Core.Types.IType
/// The that accumulates the entire textual notation with indentation
public static void BuildInterfaceBody(SysML2.NET.Core.POCO.Core.Types.IType poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder)
{
- if (writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship).Current == null)
+ if (writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship).Current is not SysML2.NET.Core.POCO.Root.Elements.IRelationship emptyBodyCandidate || !emptyBodyCandidate.IsValidForInterfaceBodyItem(writerContext))
{
stringBuilder.AppendLine(";");
}
@@ -93,7 +93,7 @@ public static void BuildInterfaceBody(SysML2.NET.Core.POCO.Core.Types.IType poco
stringBuilder.AppendLine("{");
stringBuilder.IncreaseIndent();
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current != null)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Elements.IRelationship loopBodyItem && loopBodyItem.IsValidForInterfaceBodyItem(writerContext))
{
BuildInterfaceBodyItem(poco, writerContext, stringBuilder);
}
@@ -508,7 +508,7 @@ public static void BuildTypeDeclaration(SysML2.NET.Core.POCO.Core.Types.IType po
{
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- if (poco.IsSufficient)
+ if (poco.IsSufficient && poco is not (SysML2.NET.Core.POCO.Systems.Connections.IConnectionDefinition))
{
stringBuilder.Append(" all ");
stringBuilder.Append(' ');
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs
index 7294471aa..31bc473fe 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs
@@ -107,7 +107,7 @@ public static void BuildBasicUsagePrefix(SysML2.NET.Core.POCO.Systems.Definition
{
BuildRefPrefix(poco, writerContext, stringBuilder);
- if (poco.isReference)
+ if (poco.isReference && poco is not (SysML2.NET.Core.POCO.Systems.Attributes.IAttributeUsage or SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IReferenceUsage or SysML2.NET.Core.POCO.Systems.Occurrences.IEventOccurrenceUsage))
{
stringBuilder.Append(" ref ");
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/IndentedStringBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/IndentedStringBuilder.cs
index eecccabfd..674c5c488 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/IndentedStringBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/IndentedStringBuilder.cs
@@ -339,6 +339,33 @@ public IndentedStringBuilder AppendLine(string value)
return this;
}
+ ///
+ /// Emits the current indent prefix (if positioned at the start of a new logical line)
+ /// and then appends verbatim — bypassing the leading-space
+ /// strip, the consecutive-space collapse, and the tight-token normalisation that
+ /// applies. The wrapper is left in mid-line state, so
+ /// subsequent / calls
+ /// continue to normalise their payloads as usual.
+ /// Use this for the rare case where the canonical SST form preserves leading
+ /// whitespace inside a line prefix — notably the alignment space inside a multi-line
+ /// regular comment (" * " aligns the body asterisks with the asterisk in the
+ /// opening /*). The normal path strips that
+ /// leading space at line start, which collapses " * " to "* ".
+ ///
+ /// The literal text to append after the indent prefix; null or empty is a no-op.
+ /// The current instance, to allow chaining.
+ public IndentedStringBuilder AppendIndentedLiteral(string literal)
+ {
+ if (string.IsNullOrEmpty(literal))
+ {
+ return this;
+ }
+
+ this.EmitIndentIfNeeded();
+ this.builder.Append(literal);
+ return this;
+ }
+
///
/// Converts the accumulated content of the underlying to
/// a .
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs b/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs
index fbd42610e..c304cd6f3 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/NameResolutionCache.cs
@@ -85,6 +85,21 @@ private readonly Dictionary> sourceScopeChains
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.
+ ///
+ private readonly Dictionary> directFacadeIndex = new();
+
///
/// Initializes a new rooted at
/// and eagerly populates the per-namespace simple-name
@@ -94,7 +109,7 @@ private readonly Dictionary> sourceScopeChains
public NameResolutionCache(INamespace rootNamespace)
{
this.RootNamespace = rootNamespace ?? throw new ArgumentNullException(nameof(rootNamespace));
- this.simpleNameIndices = BuildSimpleNameIndices(rootNamespace);
+ this.simpleNameIndices = this.BuildSimpleNameIndices(rootNamespace);
}
///
@@ -152,6 +167,30 @@ public string Resolve(IElement target, IElement sourcePoco)
// 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);
+ }
+ }
+
var cacheKey = (target.Id, sourceLocalScope?.Id ?? Guid.Empty);
if (this.resolvedReferences.TryGetValue(cacheKey, out var cached))
@@ -159,7 +198,7 @@ public string Resolve(IElement target, IElement sourcePoco)
return cached;
}
- var resolved = this.ResolveFresh(target, sourcePoco, sourceLocalScope, escapedName);
+ var resolved = this.ResolveFresh(target, sourcePoco, sourceLocalScope, escapedName, localRedefiner: null);
this.resolvedReferences[cacheKey] = resolved;
return resolved;
}
@@ -184,8 +223,9 @@ public string Resolve(IElement target, IElement sourcePoco)
/// The reference site's source POCO.
/// The previously-computed local scope (may be ).
/// The target's escaped raw name.
+ /// The local that acts like redefiner
/// The resolved emission string.
- private string ResolveFresh(IElement target, IElement sourcePoco, INamespace sourceLocalScope, string escapedName)
+ private string ResolveFresh(IElement target, IElement sourcePoco, INamespace sourceLocalScope, string escapedName, IFeature localRedefiner)
{
var chain = this.GetSourceScopeChain(sourcePoco, sourceLocalScope);
@@ -199,7 +239,7 @@ private string ResolveFresh(IElement target, IElement sourcePoco, INamespace sou
{
escapedShortName = rawShortName.QueryIsValidBasicName() ? rawShortName : rawShortName.ToUnrestrictedName();
- if (this.TryResolveSimpleNameAcrossChain(chain, target, rawShortName, escapedShortName, out var matchedShort))
+ if (this.TryResolveSimpleNameAcrossChain(chain, target, rawShortName, escapedShortName, localRedefiner, out var matchedShort))
{
return matchedShort;
}
@@ -208,11 +248,24 @@ private string ResolveFresh(IElement target, IElement sourcePoco, INamespace sou
var rawName = target.name;
if (!string.IsNullOrWhiteSpace(rawName)
- && this.TryResolveSimpleNameAcrossChain(chain, target, rawName, escapedName, out var matchedLong))
+ && this.TryResolveSimpleNameAcrossChain(chain, target, rawName, escapedName, localRedefiner, out var matchedLong))
{
return matchedLong;
}
+ // 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.
+ 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.
@@ -242,7 +295,7 @@ private string ResolveFresh(IElement target, IElement sourcePoco, INamespace sou
var ancestorRawName = !string.IsNullOrWhiteSpace(rawAncestorShort) ? rawAncestorShort : rawAncestorLong;
if (!string.IsNullOrWhiteSpace(ancestorRawName)
- && this.TryResolveSimpleNameAcrossChain(chain, ancestor, ancestorRawName, ancestorSegment, out var matchedAnchor))
+ && this.TryResolveSimpleNameAcrossChain(chain, ancestor, ancestorRawName, ancestorSegment, localRedefiner, out var matchedAnchor))
{
var builder = new StringBuilder(matchedAnchor);
@@ -289,6 +342,194 @@ 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.
+ ///
+ /// 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)
+ {
+ var redefinerDeclaredName = localRedefiner.DeclaredName;
+ var redefinerDeclaredShortName = localRedefiner.DeclaredShortName;
+
+ if (string.IsNullOrWhiteSpace(redefinerDeclaredName) && string.IsNullOrWhiteSpace(redefinerDeclaredShortName))
+ {
+ return false;
+ }
+
+ var targetName = target.name;
+ var targetShortName = target.shortName;
+
+ 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));
+ }
+
+ ///
+ /// 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.
+ ///
+ /// 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.
+ private bool TryResolveViaDirectFacade(IReadOnlyList chain, IElement target, string escapedShortName, string escapedName, out string matched)
+ {
+ matched = null;
+
+ var canonicalOwner = QueryOwningContainer(target) as INamespace;
+
+ 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))
+ {
+ return false;
+ }
+
+ 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];
+ var scopeIndex = this.GetSimpleNameIndex(scope);
+
+ foreach (var facade in facades)
+ {
+ var facadeName = !string.IsNullOrWhiteSpace(facade.shortName) ? facade.shortName : facade.name;
+
+ 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))
+ {
+ bestFacade = facade;
+ bestScopeDepth = scopeDepth;
+ }
+ }
+
+ if (bestFacade != null)
+ {
+ break;
+ }
+ }
+
+ // 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)
+ {
+ var canonicalNameForCompare = QueryPreferredEscapedSegment(canonicalOwner);
+ var canonicalLength = canonicalNameForCompare?.Length ?? int.MaxValue;
+ var meaningfulShorterMax = (int)(canonicalLength * 0.7);
+
+ foreach (var facade in facades)
+ {
+ if (!this.simpleNameIndices.ContainsKey(facade))
+ {
+ continue;
+ }
+
+ var facadeNameForCompare = QueryPreferredEscapedSegment(facade);
+
+ if (string.IsNullOrWhiteSpace(facadeNameForCompare) || facadeNameForCompare.Length > meaningfulShorterMax)
+ {
+ continue;
+ }
+
+ if (bestFacade == null || CompareFacades(facade, bestFacade) < 0)
+ {
+ bestFacade = facade;
+ }
+ }
+ }
+
+ if (bestFacade == null)
+ {
+ return false;
+ }
+
+ var bestFacadeSegment = QueryPreferredEscapedSegment(bestFacade);
+
+ if (string.IsNullOrWhiteSpace(bestFacadeSegment))
+ {
+ return false;
+ }
+
+ matched = bestFacadeSegment + "::" + targetSimpleName;
+ return true;
+ }
+
+ ///
+ /// 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.
+ ///
+ /// 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 lengthCompare = leftName.Length.CompareTo(rightName.Length);
+
+ return lengthCompare != 0 ? lengthCompare : string.CompareOrdinal(leftName, rightName);
+ }
+
///
/// Returns 's shortest escaped name segment — preferring
/// over , with KEBNF
@@ -322,9 +563,10 @@ private static string QueryPreferredEscapedSegment(IElement element)
/// The referenced element.
/// The simple-name lexical form to probe (may be / whitespace).
/// The escaped form to emit on a hit.
+ /// The local that acts as redefiner
/// On a unique-binding hit, the simple-name string to emit.
/// when the simple name resolves uniquely to the target somewhere in the chain.
- private bool TryResolveSimpleNameAcrossChain(IReadOnlyList chain, IElement target, string rawName, string escapedName, out string matched)
+ private bool TryResolveSimpleNameAcrossChain(IReadOnlyList chain, IElement target, string rawName, string escapedName, IFeature localRedefiner, out string matched)
{
matched = null;
@@ -335,7 +577,7 @@ private bool TryResolveSimpleNameAcrossChain(IReadOnlyList chain, IE
foreach (var scope in chain)
{
- var resolution = this.ResolveSimpleNameInScope(scope, target, rawName);
+ var resolution = this.ResolveSimpleNameInScope(scope, target, rawName, localRedefiner);
switch (resolution)
{
@@ -651,8 +893,16 @@ private enum SimpleNameResolution
/// 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.
+ ///
/// The resolution state.
- private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement target, string rawName)
+ private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement target, string rawName, IFeature localRedefiner)
{
var index = this.GetSimpleNameIndex(scope);
@@ -661,9 +911,24 @@ private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement
return SimpleNameResolution.NotBound;
}
- if (elements.Count == 1)
+ // 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 hasLocalRedefiner = localRedefiner != null && elements.Contains(localRedefiner);
+ var effectiveCount = hasLocalRedefiner ? elements.Count - 1 : elements.Count;
+
+ if (effectiveCount == 0)
+ {
+ return SimpleNameResolution.NotBound;
+ }
+
+ if (effectiveCount == 1)
{
- return elements.Contains(target)
+ var only = hasLocalRedefiner
+ ? elements.First(e => !ReferenceEquals(e, localRedefiner))
+ : elements.First();
+
+ return ReferenceEquals(only, target)
? SimpleNameResolution.Matched
: SimpleNameResolution.Shadowed;
}
@@ -671,10 +936,12 @@ private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement
// Reduce to the leaf set: drop any element that is transitively redefined by
// another element in `elements`. 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).
+ // 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.
var shadowed = new HashSet();
- foreach (var candidate in elements.OfType())
+ foreach (var candidate in elements.OfType().Where(candidate => !ReferenceEquals(candidate, localRedefiner)))
{
foreach (var redefined in candidate.AllRedefinedFeatures().Where(redefined => !ReferenceEquals(redefined, candidate)))
{
@@ -685,7 +952,7 @@ private SimpleNameResolution ResolveSimpleNameInScope(INamespace scope, IElement
IElement onlyLeaf = null;
var leafCount = 0;
- foreach (var element in elements.Where(element => element is not IFeature feature || !shadowed.Contains(feature)))
+ foreach (var element in elements.Where(element => !ReferenceEquals(element, localRedefiner) && (element is not IFeature feature || !shadowed.Contains(feature))))
{
leafCount++;
@@ -729,7 +996,19 @@ private static bool IsChainAccessor(IElement sourcePoco)
var siblings = chainOwner.OwnedRelationship.OfType().ToList();
var index = siblings.IndexOf(chaining);
- return index > 0;
+
+ if (index > 0)
+ {
+ return true;
+ }
+
+ // The FIRST chaining segment is also a chain accessor when the owned chain Feature
+ // itself is the target member of a FeatureChainExpression (grammar rule
+ // OwnedFeatureChainMember): the parser resolves even the first segment against the
+ // argument expression's result, not the lexical scope, so the bare simple name is
+ // the correct emission. A chain owned by a Specialization / ReferenceSubsetting
+ // (e.g. a connect end) keeps lexical resolution for its first segment.
+ return chainOwner.OwningRelationship is IMembership { OwningRelatedElement: IFeatureChainExpression } and not IParameterMembership;
}
///
@@ -741,7 +1020,7 @@ private static bool IsChainAccessor(IElement sourcePoco)
///
/// The root namespace.
/// The full structural cache.
- private static Dictionary>> BuildSimpleNameIndices(INamespace rootNamespace)
+ private Dictionary>> BuildSimpleNameIndices(INamespace rootNamespace)
{
var result = new Dictionary>>();
var pending = new Queue();
@@ -760,7 +1039,7 @@ private static Dictionary>(StringComparer.Ordinal);
- BuildOwnedAndImportedEntries(scope, index, pending);
+ this.BuildOwnedAndImportedEntries(scope, index, pending);
if (scope is IType type)
{
@@ -781,7 +1060,7 @@ private static DictionaryThe namespace whose entries are populated.
/// The destination index.
/// Queue of namespaces yet to be indexed.
- private static void BuildOwnedAndImportedEntries(INamespace scope, Dictionary> index, Queue pending)
+ private void BuildOwnedAndImportedEntries(INamespace scope, Dictionary> index, Queue pending)
{
try
{
@@ -803,11 +1082,36 @@ private static void BuildOwnedAndImportedEntries(INamespace scope, Dictionary
+ /// 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 .
+ private void RecordDirectFacade(INamespace canonicalOwner, INamespace facade)
+ {
+ if (!this.directFacadeIndex.TryGetValue(canonicalOwner, out var facades))
+ {
+ facades = [];
+ this.directFacadeIndex[canonicalOwner] = facades;
+ }
+
+ facades.Add(facade);
+ }
+
///
/// Indexes the entries inherited from the transitive supertypes of
/// . Bypasses the RemoveRedefinedFeatures filter so
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs
index 5f38e9b20..38049374a 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/SharedTextualNotationBuilder.cs
@@ -468,24 +468,29 @@ internal static void BuildDefinitionOrInterfaceBodyItemHandCoded(
ownedRelationshipCursor.Move();
break;
- case IOwningMembership owningMembership:
+ case IOwningMembership owningMembership when owningMembership.IsValidForDefinitionMember(writerContext):
OwningMembershipTextualNotationBuilder.BuildDefinitionMember(owningMembership, writerContext, stringBuilder);
ownedRelationshipCursor.Move();
break;
- case IMembership membership:
- MembershipTextualNotationBuilder.BuildAliasMember(membership, writerContext, stringBuilder);
- ownedRelationshipCursor.Move();
- break;
-
case IImport import:
ImportTextualNotationBuilder.BuildImport(import, writerContext, stringBuilder);
ownedRelationshipCursor.Move();
break;
- default:
+ case IMembership membership when membership is not IOwningMembership and not IFeatureMembership:
+ MembershipTextualNotationBuilder.BuildAliasMember(membership, writerContext, stringBuilder);
ownedRelationshipCursor.Move();
break;
+
+ default:
+ // KEBNF DefinitionBodyItem* / InterfaceBodyItem* semantics: terminate the body
+ // loop when the cursor's current element matches no alternative — this leaves
+ // the element for the parent rule to consume (e.g. PortDefinition's trailing
+ // ownedRelationship += ConjugatedPortDefinitionMember). The outer body loop is
+ // also guarded by IsValidForDefinitionBodyItem / IsValidForInterfaceBodyItem,
+ // so no caller reaches the dispatcher with an unrecognised element.
+ return;
}
}
}
@@ -612,11 +617,12 @@ internal static void AppendRegularComment(IndentedStringBuilder stringBuilder, s
foreach (var rawLine in lines.Where(l => !string.IsNullOrWhiteSpace(l)))
{
var line = rawLine.TrimEnd('\r');
- stringBuilder.Append(" * ");
+ stringBuilder.AppendIndentedLiteral(" * ");
stringBuilder.AppendLine(line);
}
- stringBuilder.AppendLine(" */");
+ stringBuilder.AppendIndentedLiteral(" */");
+ stringBuilder.AppendLine();
if (surroundWithBlankLines)
{
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs b/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs
index 56e5f1cc4..6f80ca136 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs
@@ -31,6 +31,9 @@ namespace SysML2.NET.Serializer.TextualNotation.Writers
using SysML2.NET.Core.POCO.Kernel.Expressions;
using SysML2.NET.Core.POCO.Kernel.Functions;
using SysML2.NET.Core.POCO.Kernel.Interactions;
+ using SysML2.NET.Core.POCO.Kernel.Packages;
+ using SysML2.NET.Core.POCO.Root.Annotations;
+ using SysML2.NET.Core.POCO.Root.Dependencies;
using SysML2.NET.Core.POCO.Root.Elements;
using SysML2.NET.Core.POCO.Root.Namespaces;
using SysML2.NET.Core.POCO.Systems.Actions;
@@ -866,26 +869,135 @@ internal static bool IsValidForNonOccurrenceUsageMember(this IFeatureMembership
}
///
- /// Asserts that the has valid element types for StructureUsageMember
- /// inside the collection
+ /// Asserts that the is valid for the StructureUsageMember rule.
+ /// StructureUsageMember : FeatureMembership = MemberPrefix ownedRelatedElement += StructureUsageElement
+ /// StructureUsageElement : Usage =
+ /// OccurrenceUsage | IndividualUsage | PortionUsage | EventOccurrenceUsage
+ /// | ItemUsage | PartUsage | ViewUsage | RenderingUsage | PortUsage
+ /// | ConnectionUsage | InterfaceUsage | AllocationUsage | Message
+ /// | FlowUsage | SuccessionFlowUsage
+ /// Encoded as the disjunction of the StructureUsageElement union expressed via
+ /// the corresponding metamodel interfaces. Because the metamodel inheritance chain has
+ /// IFlowUsage : IActionUsage (a FlowUsage is structurally an action), but the
+ /// KEBNF places FlowUsage under StructureUsageElement and ActionUsage
+ /// under BehaviorUsageElement, the simple supertype check
+ /// e is IOccurrenceUsage is paired with two exclusion clauses:
+ ///
+ /// - !(e is IActionUsage and not IFlowUsage) — every
+ /// that is NOT an belongs to
+ /// BehaviorUsageElement (ActionUsage, CalculationUsage, StateUsage, CaseUsage,
+ /// AnalysisCaseUsage, VerificationCaseUsage, UseCaseUsage).
+ /// - !(e is IConstraintUsage) —
+ /// and its descendants (RequirementUsage, ConcernUsage) belong to
+ /// BehaviorUsageElement.
+ ///
+ /// Interfaces (not concrete POCO classes) are used so the guard is robust to
+ /// alternate implementations (extensions, test doubles)
+ /// — the POCO classes in this model do not inherit from each other (only via interface
+ /// chains), so an OfType{ConcreteClass} check would silently miss any instance
+ /// supplied through a different concrete class but the same interface.
///
/// The
/// The active (unused for this guard)
- /// True if contains any of the required element types
+ /// True if any matches the StructureUsageElement union
internal static bool IsValidForStructureUsageMember(this IFeatureMembership featureMembership, TextualNotationWriterContext writerContext)
{
- return featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any()
- || featureMembership.OwnedRelatedElement.OfType().Any();
+ return featureMembership.OwnedRelatedElement.Any(element =>
+ element is IOccurrenceUsage
+ && !(element is IActionUsage && element is not IFlowUsage)
+ && element is not IConstraintUsage);
+ }
+
+ ///
+ /// Asserts that the is valid for the DefinitionMember rule.
+ /// DefinitionMember : OwningMembership = MemberPrefix ownedRelatedElement += DefinitionElement
+ /// DefinitionElement : Element = Package | LibraryPackage | AnnotatingElement | Dependency
+ /// | AttributeDefinition | EnumerationDefinition | OccurrenceDefinition | IndividualDefinition
+ /// | ItemDefinition | PartDefinition | ConnectionDefinition | FlowDefinition | InterfaceDefinition
+ /// | PortDefinition | ActionDefinition | CalculationDefinition | StateDefinition | ConstraintDefinition
+ /// | RequirementDefinition | ConcernDefinition | CaseDefinition | AnalysisCaseDefinition
+ /// | VerificationCaseDefinition | UseCaseDefinition | ViewDefinition | ViewpointDefinition
+ /// | RenderingDefinition | MetadataDefinition | ExtendedDefinition
+ /// The four covering supertypes of the union are , ,
+ /// , .
+ /// IS-A but is NOT in the DefinitionElement union — it is only ever the
+ /// inner element of a ConjugatedPortDefinitionMember consumed by the parent PortDefinition
+ /// rule and is therefore excluded explicitly.
+ ///
+ /// The
+ /// The active (unused for this guard)
+ /// True if at least one is a DefinitionElement
+ internal static bool IsValidForDefinitionMember(this IOwningMembership owningMembership, TextualNotationWriterContext writerContext)
+ {
+ foreach (var ownedRelatedElement in owningMembership.OwnedRelatedElement)
+ {
+ switch (ownedRelatedElement)
+ {
+ case IConjugatedPortDefinition:
+ continue;
+ case IDefinition or IPackage or IAnnotatingElement or IDependency:
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Asserts that the currently positioned by the cursor matches any
+ /// alternative of the DefinitionBodyItem rule.
+ /// DefinitionBodyItem : Type =
+ /// ownedRelationship += DefinitionMember
+ /// | ownedRelationship += VariantUsageMember
+ /// | ownedRelationship += NonOccurrenceUsageMember
+ /// | ( ownedRelationship += SourceSuccessionMember )? ownedRelationship += OccurrenceUsageMember
+ /// | ownedRelationship += AliasMember
+ /// | ownedRelationship += Import
+ /// Used by BuildDefinitionBody to bound the KEBNF * quantifier and the
+ /// ';' | '{' DefinitionBodyItem* '}' choice. Returns false for relationships that the body
+ /// must not consume — notably the synthetic ConjugatedPortDefinitionMember (which carries an
+ /// and is consumed by the parent PortDefinition rule).
+ ///
+ /// The at the cursor
+ /// The active
+ /// True if the relationship matches a DefinitionBodyItem alternative
+ internal static bool IsValidForDefinitionBodyItem(this IRelationship relationship, TextualNotationWriterContext writerContext)
+ {
+ return relationship switch
+ {
+ IImport => true,
+ IVariantMembership => true,
+ IFeatureMembership featureMembership =>
+ featureMembership.IsValidForSourceSuccessionMember(writerContext)
+ || featureMembership.IsValidForOccurrenceUsageMember(writerContext)
+ || featureMembership.IsValidForNonOccurrenceUsageMember(writerContext),
+ IOwningMembership owningMembership => owningMembership.IsValidForDefinitionMember(writerContext),
+ IMembership => true,
+ _ => false,
+ };
+ }
+
+ ///
+ /// Asserts that the currently positioned by the cursor matches any
+ /// alternative of the InterfaceBodyItem rule.
+ /// InterfaceBodyItem : Type =
+ /// ownedRelationship += DefinitionMember
+ /// | ownedRelationship += VariantUsageMember
+ /// | ownedRelationship += InterfaceNonOccurrenceUsageMember
+ /// | ( ownedRelationship += SourceSuccessionMember )? ownedRelationship += InterfaceOccurrenceUsageMember
+ /// | ownedRelationship += AliasMember
+ /// | ownedRelationship += Import
+ /// The shape is identical to DefinitionBodyItem except for the
+ /// InterfaceOccurrenceUsageMember / InterfaceNonOccurrenceUsageMember specialisations
+ /// — for the boolean-only guard, they share the same underlying
+ /// IsValidForOccurrenceUsageMember / IsValidForNonOccurrenceUsageMember predicates.
+ ///
+ /// The at the cursor
+ /// The active
+ /// True if the relationship matches an InterfaceBodyItem alternative
+ internal static bool IsValidForInterfaceBodyItem(this IRelationship relationship, TextualNotationWriterContext writerContext)
+ {
+ return relationship.IsValidForDefinitionBodyItem(writerContext);
}
}
}