diff --git a/SysML2.NET.CodeGenerator/GRAMMAR.md b/SysML2.NET.CodeGenerator/GRAMMAR.md
index dbaf9e07..0cad5b8f 100644
--- a/SysML2.NET.CodeGenerator/GRAMMAR.md
+++ b/SysML2.NET.CodeGenerator/GRAMMAR.md
@@ -269,3 +269,39 @@ Consequence, in `TypeTextualNotationBuilder.EmitTargetTransitionRun`: the transi
exception to the `Move()` ↔ `+=` Golden Rule, valid because the elected production has no notation for
that element. It is conditional — it only runs after `QueryImpliedSourceTransition` has confirmed
position 0 is the source membership — so it cannot consume a real element.
+
+### `DefaultReferenceUsage` vs `ReferenceUsage` — the optional `ref` keyword
+
+```
+NonOccurrenceUsageElement : Usage = DefaultReferenceUsage | ReferenceUsage | AttributeUsage | …
+DefaultReferenceUsage : ReferenceUsage = RefPrefix Usage ← no 'ref'
+ReferenceUsage = ( EndUsagePrefix | RefPrefix ) 'ref' Usage
+RefPrefix : Usage = ( direction = … )? ( isDerived ?= … )? ( isAbstract ?= … | isVariation ?= … )? ( isConstant ?= … )?
+```
+
+`RefPrefix` contains no `ref` keyword, and the `'ref'` in `ReferenceUsage` is a BARE terminal — not
+`isReference ?= 'ref'` — so it sets no property. Both productions therefore round-trip to the same
+model, and nothing records which the author wrote.
+
+The spec settles only the OPTIONALITY, not the choice. SysML 2.0 §7.6.4 Reference Usages (p. 74,
+informative): "The declaration of a reference usage may, but is not required, to include the `ref`
+keyword. However, a reference usage is always, by definition, referential." So both forms are valid
+and the writer must pick one; nothing normative says which.
+
+What follows is therefore an EMPIRICAL convention fitted to the corpus, not a spec rule — and §7.6.4's
+own example contradicts its "named" half (`orderedContent ordered :>> content;` is named yet omits
+`ref`). It is kept because it reproduces the pilot on all validated files and is always valid output.
+
+The corpus is consistent once both halves of the condition are taken together. `06` writes
+`:>> mass = m;` (unnamed, empty `RefPrefix`), `3c-…-2` writes `abstract ref :>> trailerHitch[1];`
+(unnamed but `RefPrefix` carries `isAbstract`) and `5-…-1` writes `ref vehicle: VehicleA;` (named).
+Each condition alone is refuted by one of the three; the conjunction fits all of them:
+
+> omit `ref` when the usage is unnamed AND `RefPrefix` is empty — there is no declaration for the
+> keyword to qualify. Otherwise write it.
+
+`IsValidForDefaultReferenceUsage` implements exactly that, on top of the spec-mandated case
+(a directed usage is always referential, Clause 7.6.3, so the keyword is redundant there).
+
+`IsValidForDefaultReferenceUsage` still encodes the one spec-mandated case (`!IsEnd &&
+Direction.HasValue`): a directed usage is always referential, so the keyword is redundant there.
diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs
index 89f829b7..85a40e91 100644
--- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs
+++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.CollectionProcessing.cs
@@ -74,7 +74,7 @@ private void EmitCollectionNonTerminalLoop(EncodedTextWriter writer, IClass umlC
var perItemCall = ResolveBuilderCall(umlClass, nonTerminalElement, typeTarget, ruleGenerationContext);
- var whileTypeExclusion = ResolveCollectionWhileTypeCondition(cursorVariableName, umlClass, referencedRule, ruleGenerationContext);
+ var whileTypeExclusion = this.ResolveCollectionWhileTypeCondition(cursorVariableName, umlClass, referencedRule, propertyName, ruleGenerationContext);
string whileCondition;
@@ -174,7 +174,7 @@ private void EmitCollectionNonTerminalLoop(EncodedTextWriter writer, IClass umlC
///
/// Resolves the type condition for a collection while loop.
///
- private static string ResolveCollectionWhileTypeCondition(string cursorVariableName, IClass umlClass, TextualNotationRule collectionRule, RuleGenerationContext ruleGenerationContext)
+ private string ResolveCollectionWhileTypeCondition(string cursorVariableName, IClass umlClass, TextualNotationRule collectionRule, string outerPropertyName, RuleGenerationContext ruleGenerationContext)
{
var siblings = ruleGenerationContext.CurrentSiblingElements;
var currentIndex = ruleGenerationContext.CurrentElementIndex;
@@ -202,6 +202,19 @@ private static string ResolveCollectionWhileTypeCondition(string cursorVariableN
var itemRule = ruleGenerationContext.FindRule(assignmentNonTerminals[0].Name);
var itemTypeTarget = itemRule != null ? itemRule.EffectiveTarget : null;
+ // The item rule's own target is the WRAPPER type for a thin owning wrapper
+ // (X : OwningMembership = … ownedRelatedElement = Y), which every sibling wrapper on this
+ // cursor also satisfies — `individual def` consumed its own EmptyMultiplicityMember as a
+ // DefinitionExtensionKeyword and emitted a stray '#'. Prefer the wrapped-type guard when
+ // one is available; otherwise keep the coarse test rather than falling through to a
+ // weaker condition that could admit elements the loop body will not consume.
+ var wrappedTypeGuard = this.ResolveContentTypeGuard(cursorVariableName, collectionRule, outerPropertyName, umlClass, ruleGenerationContext);
+
+ if (!string.IsNullOrWhiteSpace(wrappedTypeGuard))
+ {
+ return wrappedTypeGuard;
+ }
+
if (itemTypeTarget != null)
{
var itemTargetClass = umlClass.Cache.Values.OfType()
diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs
index 59b6654a..dbf2b360 100644
--- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs
+++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs
@@ -90,6 +90,8 @@ internal void EmitAlternativeBody(EncodedTextWriter writer, IClass umlClass, Alt
/// Whether this is part of a multi-alternative context
private void EmitElements(EncodedTextWriter writer, IClass umlClass, List elements, RuleGenerationContext ruleGenerationContext, bool restoreCallerPerElement = false, bool isPartOfMultipleAlternative = false)
{
+ elements = HoistSingleNonNotationalConsumption(elements, ruleGenerationContext);
+
var previousSiblings = ruleGenerationContext.CurrentSiblingElements;
var previousIndex = ruleGenerationContext.CurrentElementIndex;
ruleGenerationContext.CurrentSiblingElements = elements;
@@ -110,6 +112,88 @@ private void EmitElements(EncodedTextWriter writer, IClass umlClass, List
+ /// Text-free members the pilot is KNOWN to store before the elements the production declares ahead of
+ /// them, verified against real pilot output.
+ /// Deliberately an allowlist, not a structural rule. Emitting no text does NOT imply the model
+ /// may store the element anywhere: storage order is a per-rule implementation detail and it goes BOTH
+ /// ways. EmptyMultiplicityMember is stored FIRST though declared last, while
+ /// EmptyResultMember / ReturnParameterMembership is stored LAST as declared (in the
+ /// OperatorExpression family, InvocationExpression and FeatureReferenceExpression).
+ /// Hoisting the latter would strand the cursor on it. Only add a name here after checking real output.
+ ///
+ private static readonly HashSet HoistableTextFreeMembers = new(StringComparer.Ordinal)
+ {
+ "EmptyMultiplicityMember",
+ };
+
+ ///
+ /// Moves a lone += element whose production emits NO text to the front of the alternative.
+ /// Such an element has no observable position in the notation, so the grammar cannot constrain
+ /// where the parser puts it in the collection — and the pilot does not always put it where the
+ /// production does. Consuming it first keeps the cursor aligned for the elements that DO emit text;
+ /// leaving it in place strands the cursor on it (e.g. IndividualDefinition declares
+ /// EmptyMultiplicityMember last but the model stores it first, hiding the Subclassification
+ /// that Definition must read).
+ /// Only a LONE such element is hoisted: when several appear (e.g. TransitionUsage's two
+ /// EmptyParameterMembers) their relative order decides which pairs with which sibling, so
+ /// moving them would change meaning.
+ ///
+ /// The alternative's elements in grammar order.
+ /// The current .
+ /// The elements, reordered when a lone text-free consumption is present.
+ private static List HoistSingleNonNotationalConsumption(List elements, RuleGenerationContext ruleGenerationContext)
+ {
+ var textFree = elements
+ .OfType()
+ .Where(assignment => assignment.Operator == "+="
+ && assignment.Value is NonTerminalElement nonTerminal
+ && HoistableTextFreeMembers.Contains(nonTerminal.Name)
+ && EmitsNoNotation(ruleGenerationContext.FindRule(nonTerminal.Name), ruleGenerationContext, []))
+ .ToList();
+
+ // Only a TRAILING text-free element is hoisted. Declared last, it has nothing after it whose
+ // position it could encode, so moving it is meaning-preserving; declared mid-sequence its order
+ // relative to the following elements is significant (TransitionUsage's EmptyParameterMember
+ // pairs with the TriggerActionMember that follows it).
+ if (textFree.Count != 1
+ || !ReferenceEquals(elements[^1], textFree[0])
+ || ReferenceEquals(elements[0], textFree[0]))
+ {
+ return elements;
+ }
+
+ var reordered = new List { textFree[0] };
+ reordered.AddRange(elements.Where(element => !ReferenceEquals(element, textFree[0])));
+
+ return reordered;
+ }
+
+ ///
+ /// Determines whether produces no textual notation at all — no terminal and
+ /// no value-bearing assignment, transitively. EmptyMultiplicity, EmptyUsage and their
+ /// wrappers are the canonical cases.
+ ///
+ /// The rule to inspect; may be .
+ /// The current .
+ /// Rules already inspected, guarding against recursive productions.
+ /// when the rule emits nothing.
+ private static bool EmitsNoNotation(TextualNotationRule rule, RuleGenerationContext ruleGenerationContext, HashSet visited)
+ {
+ if (rule == null || !visited.Add(rule.RuleName))
+ {
+ return false;
+ }
+
+ return rule.Alternatives.SelectMany(alternative => alternative.Elements).All(element => element switch
+ {
+ NonParsingAssignmentElement => true,
+ AssignmentElement { Value: NonTerminalElement nested } => EmitsNoNotation(ruleGenerationContext.FindRule(nested.Name), ruleGenerationContext, visited),
+ NonTerminalElement nonTerminal => EmitsNoNotation(ruleGenerationContext.FindRule(nonTerminal.Name), ruleGenerationContext, visited),
+ _ => false,
+ });
+ }
+
///
/// Declares cursor variables for all enumerable properties referenced by assignment elements in the given alternative.
///
diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Expected/06-Individual and Snapshots/6-Individual and Snapshots.sysml b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/06-Individual and Snapshots/6-Individual and Snapshots.sysml
new file mode 100644
index 00000000..21fe4904
--- /dev/null
+++ b/SysML2.NET.Serializer.TextualNotation.Tests/Expected/06-Individual and Snapshots/6-Individual and Snapshots.sysml
@@ -0,0 +1,135 @@
+package '6-Individual and Snapshots' {
+ private import ScalarValues::Real;
+ private import Time::DateTime;
+ private import ISQ::*;
+ package 'Part Definitions' {
+ part def 'Temporal-Spatial Reference' {
+ attribute referenceTime: DateTime;
+ attribute referenceCoordinateSystem;
+ }
+ /*
+ * Note that space and time coordinatization have not
+ * been fully specified yet.
+ */
+ part def VehicleRoadContext {
+ attribute t: DurationValue;
+ }
+ part def VehicleA {
+ attribute mass: MassValue;
+ attribute position: Real;
+ attribute velocity: Real;
+ attribute acceleration: Real;
+ exhibit state vehicleStates {
+ entry;
+ then on;
+ state on;
+ then off;
+ state off;
+ }
+ }
+ part def Road {
+ attribute angle: Real;
+ attribute surfaceFriction: Real;
+ }
+ }
+ package 'Individual Definitions' {
+ private import 'Part Definitions'::*;
+ /*
+ * An individual definition restricts the instances of a part def to
+ * those that are portions of the same life ("identity").
+ */
+ individual def 'Temporal-Spatial Reference_ID1' :> 'Temporal-Spatial Reference';
+ individual def VehicleRoadContext_ID1 :> VehicleRoadContext;
+ individual def VehicleA_ID1 :> VehicleA;
+ individual def Road_ID1 :> Road;
+ }
+ package Values {
+ attribute t0: DurationValue;
+ attribute t1: DurationValue;
+ attribute tn: DurationValue;
+ attribute m: MassValue;
+ attribute p0: Real;
+ attribute p1: Real;
+ attribute pn: Real;
+ attribute v0: Real;
+ attribute v1: Real;
+ attribute vn: Real;
+ attribute a0: Real;
+ attribute a1: Real;
+ attribute an: Real;
+ attribute theta0: Real;
+ attribute theta1: Real;
+ attribute thetan: Real;
+ attribute sf0: Real;
+ attribute sf1: Real;
+ attribute sfn: Real;
+ }
+ package 'Individuals and Snapshots' {
+ private import 'Individual Definitions'::*;
+ private import Values::*;
+ individual reference: 'Temporal-Spatial Reference_ID1' {
+ /*
+ * An individual usage must be typed by an individual definition,
+ * representing the condition of that individual during some or all
+ * of its life.
+ */
+ snapshot context_t0: VehicleRoadContext_ID1 {
+ :>> t = t0 {
+ /*
+ * This is a concise notation for showing the redefinition
+ * of a attribute property.
+ */
+ }
+ snapshot vehicle_ID1_t0: VehicleA_ID1 {
+ /*
+ * A snapshot is a kind of individual usage restricted to
+ * a single instant of time.
+ */
+ :>> mass = m;
+ :>> position = p0;
+ :>> velocity = v0;
+ :>> acceleration = a0;
+ exhibit vehicleStates.on {
+ /*
+ * This asserts that the snapshot exhibits the referenced
+ * state, which means that the vehicle must me in the state
+ * at the time of the snapshot.
+ */
+ }
+ }
+ snapshot road_ID1_t0: Road_ID1 {
+ :>> angle = theta0;
+ :>> surfaceFriction = sf0;
+ }
+ }
+ snapshot context_t1: VehicleRoadContext_ID1 {
+ :>> t = t1;
+ snapshot vehicle_ID1_t1: VehicleA_ID1 {
+ :>> mass = m;
+ :>> position = p1;
+ :>> velocity = v1;
+ :>> acceleration = a1;
+ exhibit vehicleStates.on;
+ }
+ snapshot road_ID1_t1: Road_ID1 {
+ :>> angle = theta1;
+ :>> surfaceFriction = sf1;
+ }
+ }
+ snapshot context_tn: VehicleRoadContext_ID1 {
+ :>> t = tn;
+ snapshot vehicle_ID1_tn: VehicleA_ID1 {
+ :>> mass = m;
+ :>> position = pn;
+ :>> velocity = vn;
+ :>> acceleration = an;
+ exhibit vehicleStates.off;
+ }
+ snapshot road_ID1_tn: Road_ID1 {
+ :>> angle = theta1;
+ :>> surfaceFriction = sfn;
+ }
+ }
+ }
+ }
+}
diff --git a/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs b/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs
index dd3ed5aa..1014fdab 100644
--- a/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs
+++ b/SysML2.NET.Serializer.TextualNotation.Tests/Writers/TextualNotationValidationTestFixture.cs
@@ -53,6 +53,7 @@ public class TextualNotationValidationTestFixture
[TestCase("05-State-based Behavior", "5-State-based Behavior-1a.sysmlx")]
[TestCase("05-State-based Behavior", "5-State-based Behavior-1.sysmlx")]
[TestCase("05-State-based Behavior", "5-State-based Behavior-2.sysmlx")]
+ [TestCase("06-Individual and Snapshots", "6-Individual and Snapshots.sysmlx")]
public async Task VerifyValidationTextualNotationXmi(string folderName, string fileName)
{
var loggerFactory = LoggerFactory.Create(builder =>
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/DefinitionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/DefinitionTextualNotationBuilder.cs
index 39236301..02ba0e9d 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/DefinitionTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/DefinitionTextualNotationBuilder.cs
@@ -114,7 +114,7 @@ public static void BuildExtendedDefinition(SysML2.NET.Core.POCO.Systems.Definiti
SharedTextualNotationBuilder.BuildBasicDefinitionPrefix(poco, writerContext, stringBuilder);
}
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
BuildDefinitionExtensionKeyword(poco, writerContext, stringBuilder);
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EnumerationDefinitionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EnumerationDefinitionTextualNotationBuilder.cs
index 2f25b8e9..09d78e48 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EnumerationDefinitionTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/EnumerationDefinitionTextualNotationBuilder.cs
@@ -82,7 +82,7 @@ public static void BuildEnumerationBody(SysML2.NET.Core.POCO.Systems.Enumeration
public static void BuildEnumerationDefinition(SysML2.NET.Core.POCO.Systems.Enumerations.IEnumerationDefinition poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder)
{
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
DefinitionTextualNotationBuilder.BuildDefinitionExtensionKeyword(poco, writerContext, stringBuilder);
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MetadataDefinitionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MetadataDefinitionTextualNotationBuilder.cs
index 31fdf4cb..d7ac4c98 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MetadataDefinitionTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MetadataDefinitionTextualNotationBuilder.cs
@@ -50,7 +50,7 @@ public static void BuildMetadataDefinition(SysML2.NET.Core.POCO.Systems.Metadata
}
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
DefinitionTextualNotationBuilder.BuildDefinitionExtensionKeyword(poco, writerContext, stringBuilder);
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MetadataUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MetadataUsageTextualNotationBuilder.cs
index 5d62415a..66d2503f 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MetadataUsageTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/MetadataUsageTextualNotationBuilder.cs
@@ -99,7 +99,7 @@ public static void BuildMetadataUsageDeclaration(SysML2.NET.Core.POCO.Systems.Me
public static void BuildMetadataUsage(SysML2.NET.Core.POCO.Systems.Metadata.IMetadataUsage poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder)
{
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
UsageTextualNotationBuilder.BuildUsageExtensionKeyword(poco, writerContext, stringBuilder);
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceDefinitionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceDefinitionTextualNotationBuilder.cs
index f74b5789..3d6ed891 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceDefinitionTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceDefinitionTextualNotationBuilder.cs
@@ -51,7 +51,6 @@ public static void BuildOccurrenceDefinitionPrefix(SysML2.NET.Core.POCO.Systems.
if (poco.IsIndividual && ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
{
- stringBuilder.Append(" individual ");
if (ownedRelationshipCursor.Current != null)
{
@@ -63,6 +62,7 @@ public static void BuildOccurrenceDefinitionPrefix(SysML2.NET.Core.POCO.Systems.
}
}
+ stringBuilder.Append(" individual ");
stringBuilder.Append(' ');
}
@@ -85,6 +85,17 @@ public static void BuildIndividualDefinition(SysML2.NET.Core.POCO.Systems.Occurr
{
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
+ if (ownedRelationshipCursor.Current != null)
+ {
+
+ if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership)
+ {
+ OwningMembershipTextualNotationBuilder.BuildEmptyMultiplicityMember(elementAsOwningMembership, writerContext, stringBuilder);
+ ownedRelationshipCursor.Move();
+
+ }
+ }
+
if (poco.IsAbstract || poco.IsVariation)
{
SharedTextualNotationBuilder.BuildBasicDefinitionPrefix(poco, writerContext, stringBuilder);
@@ -93,7 +104,7 @@ public static void BuildIndividualDefinition(SysML2.NET.Core.POCO.Systems.Occurr
{
stringBuilder.Append(" individual ");
}
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
DefinitionTextualNotationBuilder.BuildDefinitionExtensionKeyword(poco, writerContext, stringBuilder);
}
@@ -101,17 +112,6 @@ public static void BuildIndividualDefinition(SysML2.NET.Core.POCO.Systems.Occurr
stringBuilder.Append("def ");
DefinitionTextualNotationBuilder.BuildDefinition(poco, writerContext, stringBuilder);
- if (ownedRelationshipCursor.Current != null)
- {
-
- if (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership elementAsOwningMembership)
- {
- OwningMembershipTextualNotationBuilder.BuildEmptyMultiplicityMember(elementAsOwningMembership, writerContext, stringBuilder);
- ownedRelationshipCursor.Move();
-
- }
- }
-
}
///
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceUsageTextualNotationBuilder.cs
index 39eea7dd..455611f6 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceUsageTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/OccurrenceUsageTextualNotationBuilder.cs
@@ -83,7 +83,7 @@ public static void BuildIndividualUsage(SysML2.NET.Core.POCO.Systems.Occurrences
stringBuilder.Append(" individual ");
}
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
UsageTextualNotationBuilder.BuildUsageExtensionKeyword(poco, writerContext, stringBuilder);
}
@@ -112,7 +112,7 @@ public static void BuildPortionUsage(SysML2.NET.Core.POCO.Systems.Occurrences.IO
stringBuilder.Append(poco.PortionKind.ToString().ToLower());
stringBuilder.Append(' ');
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
UsageTextualNotationBuilder.BuildUsageExtensionKeyword(poco, writerContext, stringBuilder);
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PartUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PartUsageTextualNotationBuilder.cs
index cd09d230..9e3383b1 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PartUsageTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/PartUsageTextualNotationBuilder.cs
@@ -44,7 +44,7 @@ public static void BuildActorUsage(SysML2.NET.Core.POCO.Systems.Parts.IPartUsage
{
stringBuilder.Append("actor ");
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
UsageTextualNotationBuilder.BuildUsageExtensionKeyword(poco, writerContext, stringBuilder);
}
@@ -64,7 +64,7 @@ public static void BuildStakeholderUsage(SysML2.NET.Core.POCO.Systems.Parts.IPar
{
stringBuilder.Append("stakeholder ");
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
UsageTextualNotationBuilder.BuildUsageExtensionKeyword(poco, writerContext, stringBuilder);
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ReferenceUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ReferenceUsageTextualNotationBuilder.cs
index ba907d77..1101edac 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ReferenceUsageTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ReferenceUsageTextualNotationBuilder.cs
@@ -307,7 +307,7 @@ public static void BuildSubjectUsage(SysML2.NET.Core.POCO.Systems.DefinitionAndU
{
stringBuilder.Append("subject ");
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
UsageTextualNotationBuilder.BuildUsageExtensionKeyword(poco, writerContext, stringBuilder);
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/RequirementUsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/RequirementUsageTextualNotationBuilder.cs
index cb113edb..0d5b78e8 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/RequirementUsageTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/RequirementUsageTextualNotationBuilder.cs
@@ -43,7 +43,7 @@ public static partial class RequirementUsageTextualNotationBuilder
public static void BuildObjectiveRequirementUsage(SysML2.NET.Core.POCO.Systems.Requirements.IRequirementUsage poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder)
{
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
UsageTextualNotationBuilder.BuildUsageExtensionKeyword(poco, writerContext, stringBuilder);
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs
index a73a9aae..a700af45 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs
@@ -613,7 +613,7 @@ public static void BuildExtendedUsage(SysML2.NET.Core.POCO.Systems.DefinitionAnd
{
BuildUnextendedUsagePrefix(poco, writerContext, stringBuilder);
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
- while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
+ while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType().Any())
{
BuildUsageExtensionKeyword(poco, writerContext, stringBuilder);
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs b/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs
index caba14a0..136838a3 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs
@@ -330,7 +330,27 @@ internal static bool IsValidForNonOccurrenceUsageElement(this IUsage usage, Text
/// True if the reference usage renders through the 'ref'-less default form
internal static bool IsValidForDefaultReferenceUsage(this IReferenceUsage referenceUsage, TextualNotationWriterContext writerContext)
{
- return !referenceUsage.IsEnd && referenceUsage.Direction.HasValue;
+ if (referenceUsage.IsEnd)
+ {
+ return false;
+ }
+
+ // A directed usage is always referential (Clause 7.6.3), so the keyword adds nothing.
+ if (referenceUsage.Direction.HasValue)
+ {
+ return true;
+ }
+
+ // Otherwise the keyword adds nothing only when there is no declaration for it to qualify: no
+ // name and an empty RefPrefix, i.e. the whole usage is a bare redefinition such as
+ // `:>> mass = m`. The pilot bears this out — it writes `ref vehicle: VehicleA` (named) and
+ // `abstract ref :>> trailerHitch[1]` (RefPrefix carries `abstract`) but plain `:>> mass = m`.
+ return string.IsNullOrWhiteSpace(referenceUsage.DeclaredName)
+ && string.IsNullOrWhiteSpace(referenceUsage.DeclaredShortName)
+ && !referenceUsage.IsDerived
+ && !referenceUsage.IsAbstract
+ && !referenceUsage.IsVariation
+ && !referenceUsage.IsConstant;
}
///