From eac79a189f86cb12a157ff353d0652c6bf66afff Mon Sep 17 00:00:00 2001 From: atheate Date: Tue, 11 Aug 2026 15:59:10 +0200 Subject: [PATCH] Fix #293 --- SysML2.NET.CodeGenerator/GRAMMAR.md | 36 +++++ .../RuleProcessor.CollectionProcessing.cs | 17 ++- .../HandleBarHelpers/RuleProcessor.cs | 84 +++++++++++ .../6-Individual and Snapshots.sysml | 135 ++++++++++++++++++ .../TextualNotationValidationTestFixture.cs | 1 + .../DefinitionTextualNotationBuilder.cs | 2 +- ...erationDefinitionTextualNotationBuilder.cs | 2 +- ...etadataDefinitionTextualNotationBuilder.cs | 2 +- .../MetadataUsageTextualNotationBuilder.cs | 2 +- ...urrenceDefinitionTextualNotationBuilder.cs | 26 ++-- .../OccurrenceUsageTextualNotationBuilder.cs | 4 +- .../PartUsageTextualNotationBuilder.cs | 4 +- .../ReferenceUsageTextualNotationBuilder.cs | 2 +- .../RequirementUsageTextualNotationBuilder.cs | 2 +- .../UsageTextualNotationBuilder.cs | 2 +- .../TextualNotationValidationExtensions.cs | 22 ++- 16 files changed, 316 insertions(+), 27 deletions(-) create mode 100644 SysML2.NET.Serializer.TextualNotation.Tests/Expected/06-Individual and Snapshots/6-Individual and Snapshots.sysml 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; } ///