diff --git a/Resources/Quantities.sysml b/Resources/Quantities.sysml
new file mode 100644
index 00000000..263e9ff9
--- /dev/null
+++ b/Resources/Quantities.sysml
@@ -0,0 +1,107 @@
+standard library package Quantities {
+ doc
+ /*
+ * This package defines the root representations for quantities and their values.
+ */
+
+ private import Collections::*;
+ private import ScalarValues::NumericalValue;
+ private import ScalarValues::Number;
+ private import ScalarValues::Real;
+ private import ScalarValues::Natural;
+ private import ScalarValues::Boolean;
+ private import ScalarValues::String;
+ private import VectorValues::NumericalVectorValue;
+ private import VectorValues::ThreeVectorValue;
+
+ abstract attribute def TensorQuantityValue :> Array {
+ doc
+ /*
+ * The value of a quantity is a tuple of one or more numbers (i.e. mathematical number values) and a reference to a measurement reference.
+ * The most general case is a multi-dimensional, tensor quantity of any order. In engineering, the majority of quantities used are
+ * scalar and vector quantities, that are tensor quantities of order 0 and 1 respectively.
+ * The measurement reference used to express a quantity value must have a type, dimensions and order that match the quantity, i.e.,
+ * a TensorQuantityValue must use a TensorMeasurementReference, a VectorQuantityValue a VectorMeasurementReference,
+ * and a ScalarQuantityValue a ScalarMeasurementReference. See package MeasurementReferences for details.
+ */
+
+ attribute isBound: Boolean;
+ attribute num: Number[1..*] ordered nonunique :>> elements;
+ attribute mRef: MeasurementReferences::TensorMeasurementReference;
+ attribute :>> dimensions = mRef.dimensions;
+ attribute order :>> rank;
+ attribute contravariantOrder: Natural;
+ attribute covariantOrder: Natural;
+
+ assert constraint orderSum { contravariantOrder + covariantOrder == order }
+ assert constraint boundMatch { (isBound == mRef.isBound) or (not isBound and mRef.isBound) }
+ }
+
+ abstract attribute def VectorQuantityValue :> TensorQuantityValue, NumericalVectorValue {
+ attribute :>> mRef: MeasurementReferences::VectorMeasurementReference;
+ }
+
+ abstract attribute def ScalarQuantityValue :> VectorQuantityValue, NumericalValue {
+ attribute :>> mRef: MeasurementReferences::ScalarMeasurementReference;
+ }
+
+ abstract attribute tensorQuantities: TensorQuantityValue[*] nonunique {
+ doc
+ /*
+ * Quantities are defined as self-standing features that can be used to consistently specify quantities as
+ * features of occurrences. Each single quantity feature is subsetting the root feature tensorQuantities.
+ * In other words, the codomain of a quantity feature is a suitable specialization of TensorQuantityValue.
+ */
+ }
+ abstract attribute vectorQuantities: VectorQuantityValue[*] nonunique :> tensorQuantities;
+ abstract attribute scalarQuantities: ScalarQuantityValue[*] nonunique :> vectorQuantities;
+
+ abstract attribute def '3dVectorQuantityValue' :> VectorQuantityValue, ThreeVectorValue {
+ doc
+ /*
+ * Most general representation of real 3-vector quantities
+ */
+
+ attribute :>> num: Real[3];
+ }
+ alias ThreeDVectorQuantityValue for '3dVectorQuantityValue';
+
+ /*
+ * Define generic aliases QuantityValue and quantities for the top level quantity attribute def and attribute.
+ */
+ alias QuantityValue for TensorQuantityValue;
+ alias quantities for tensorQuantities;
+
+ attribute def SystemOfQuantities {
+ doc
+ /*
+ * A SystemOfQuantities represents the essentials of [VIM] concept "system of quantities" (https://jcgm.bipm.org/vim/en/1.3.html), defined as a
+ * "set of quantities together with a set of noncontradictory equations relating those quantities".
+ * In order to establish such a set of noncontradictory equations a set of base quantities is selected. Subsequently the system of quantities is
+ * completed by adding derived quantities which are products of powers of the base quantities.
+ */
+
+ attribute baseQuantities: ScalarQuantityValue[*] ordered :> scalarQuantities;
+ }
+
+ attribute def QuantityPowerFactor {
+ doc
+ /*
+ * Representation of a quantity power factor, being the combination of a quantity and an exponent.
+ *
+ * A sequence of QuantityPowerFactors for the baseQuantities of a SystemOfQuantities define the QuantityDimension of a scalar quantity.
+ */
+
+ attribute quantity: ScalarQuantityValue[1];
+ attribute exponent: Real[1];
+ }
+
+ attribute def QuantityDimension {
+ doc
+ /*
+ * Representation of quantity dimension, which is the product of powers of the set of base quantities defined for a particular system of quantities, units and scales.
+ */
+
+ attribute quantityPowerFactors: QuantityPowerFactor[*] ordered;
+ }
+}
diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCoreTextualNotationBuilderGeneratorTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCoreTextualNotationBuilderGeneratorTestFixture.cs
index 373705c5..ff7487e5 100644
--- a/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCoreTextualNotationBuilderGeneratorTestFixture.cs
+++ b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCoreTextualNotationBuilderGeneratorTestFixture.cs
@@ -67,5 +67,41 @@ public async Task VerifyCanGenerateTextualNotation()
{
await Assert.ThatAsync(() => this.umlCoreTextualNotationBuilderGenerator.GenerateAsync(GeneratorSetupFixture.XmiReaderResult, this.textualNotationSpecification, this.umlPocoDirectoryInfo), Throws.Nothing);
}
+
+ ///
+ /// Regression for the subtype-overlap guard-synthesis fix. The OwnedExpression
+ /// dispatch in BuildOwnedExpression groups seven rules that target
+ /// OperatorExpression and pairs them with a sibling alternative
+ /// (PrimaryExpression) that targets a SUPERTYPE (Expression). Before the
+ /// fix, the would-be-default of the duplicate group (ExtentExpression) was emitted
+ /// as a bare case IOperatorExpression pocoOperatorExpression: which greedily
+ /// swallowed IFeatureChainExpression (an IOperatorExpression subtype)
+ /// before it could reach default → BuildPrimaryExpression. The fix synthesises a
+ /// when-clause from the rule's parsed assignments — operator = 'all' and
+ /// ownedRelationship += TypeReferenceMember — so the case becomes
+ /// case IOperatorExpression … when … .Operator == "all" && … .Current is IParameterMembership:
+ /// and FeatureChainExpression falls through to the correct dispatcher. This test
+ /// pins both halves of the synthesised guard so future regressions are caught.
+ ///
+ [Test]
+ public async Task Verify_that_ExtentExpression_case_carries_synthesised_guard()
+ {
+ await this.umlCoreTextualNotationBuilderGenerator.GenerateAsync(GeneratorSetupFixture.XmiReaderResult, this.textualNotationSpecification, this.umlPocoDirectoryInfo);
+
+ var generatedExpressionBuilderPath = Path.Combine(this.umlPocoDirectoryInfo.FullName, "ExpressionTextualNotationBuilder.cs");
+ Assert.That(File.Exists(generatedExpressionBuilderPath), Is.True, $"Expected generator to emit {generatedExpressionBuilderPath}");
+
+ var generatedSource = await File.ReadAllTextAsync(generatedExpressionBuilderPath);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(generatedSource, Does.Not.Contain("case SysML2.NET.Core.POCO.Kernel.Expressions.IOperatorExpression pocoOperatorExpression:" + System.Environment.NewLine + " OperatorExpressionTextualNotationBuilder.BuildExtentExpression"),
+ "ExtentExpression's case must not be emitted as the bare unguarded `case IOperatorExpression pocoOperatorExpression:` fall-through — it would swallow IFeatureChainExpression.");
+ Assert.That(generatedSource, Does.Contain(".Operator == \"all\""),
+ "Synthesised guard for ExtentExpression must include the parsed scalar literal `operator = 'all'` as `.Operator == \"all\"`.");
+ Assert.That(generatedSource, Does.Contain(".Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership"),
+ "Synthesised guard for ExtentExpression must include the parsed `ownedRelationship += TypeReferenceMember` cursor predicate.");
+ });
+ }
}
}
diff --git a/SysML2.NET.CodeGenerator/GRAMMAR.md b/SysML2.NET.CodeGenerator/GRAMMAR.md
index 26d35314..e75ca2cf 100644
--- a/SysML2.NET.CodeGenerator/GRAMMAR.md
+++ b/SysML2.NET.CodeGenerator/GRAMMAR.md
@@ -31,7 +31,7 @@ SysML2 grammar rules (in `Grammar/Resources/*.kebnf` and the `…`
| Scalar assignment | `prop = X` | Assign the parsed value of `X` to the property `prop` |
| Collection assignment | `prop += X` | Append one parsed `X` to the collection `prop` |
| Boolean assignment | `prop ?= 'keyword'` | Set `prop = true` when the terminal is present |
-| Non-parsing assignment | `{ prop = 'val' }` | Implicit side-effect in parse direction; in unparse direction it emits no output |
+| Non-parsing assignment | `{ prop = 'val' }` | Implicit side-effect in parse direction; in unparse direction it emits no output, and it does NOT participate in dispatch-guard synthesis — only parsed assignments (`prop = X`, `prop += X`, `prop ?= X`) do |
| QualifiedName value literal | `prop = [QualifiedName]` | Cross-reference by qualified name |
## Pipeline Overview
@@ -115,7 +115,15 @@ When multiple alternatives map to the same UML class (creating duplicate switch
1. **`?=` boolean guards** (primary) — e.g., `EndUsagePrefix` has `isEnd?='end'`, so it gets `when poco.IsEnd`
2. **`IsValidFor{RuleName}()` extension methods** (fallback) — hand-coded in `MembershipValidationExtensions.cs` or `TextualNotationValidationExtensions.cs`. Used when `?=` can't disambiguate
-3. **Type ordering** — more specific types (deeper inheritance) come first, fallback case (matching `NamedElementToGenerate`) goes last as `default:`
+3. **Synthesised structural guards** (subtype-overlap defence) — when a duplicate group's target class has subtypes routed by a sibling alternative (i.e. another alternative targets a SUPERTYPE of the group's target), the would-be-default member is NOT left as a bare `case I{Target}:`. Instead, `RuleProcessor.PatternHandlers.cs#SynthesiseGuardFromRuleBody` walks the rule body and AND-combines one predicate per parsed `AssignmentElement`:
+ - `prop = 'literal'` → `poco.{Prop} == "literal"`
+ - `prop = [QualifiedName]` → `poco.{Prop} != null`
+ - `prop = NonTerminal` → `poco.{Prop} is I{RHS-target}` (or `!= null` when the RHS target cannot be resolved)
+ - first `ownedRelationship += NonTerminal` → `cursor.Current is I{RHS-target}`
+ - non-cursor `prop += NonTerminal` → `poco.{Prop}.OfType().Any()`
+ - `prop ?= 'kw'` → produced by step 1, not re-synthesised here
+ - `{ prop = X }` non-parsing → ignored
+4. **Type ordering** — more specific types (deeper inheritance) come first, fallback case (matching `NamedElementToGenerate`) goes last as `default:`
## Patterns Handled by Code-Gen
diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs
index 4da9a51e..3fb1995c 100644
--- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs
+++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs
@@ -29,6 +29,8 @@ namespace SysML2.NET.CodeGenerator.HandleBarHelpers
using SysML2.NET.CodeGenerator.Extensions;
using SysML2.NET.CodeGenerator.Grammar.Model;
+ using uml4net;
+ using uml4net.Classification;
using uml4net.CommonStructure;
using uml4net.Extensions;
using uml4net.StructuredClassifiers;
@@ -632,6 +634,66 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer,
}
}
+ // Subtype-overlap guard synthesis.
+ //
+ // After the existing IsValidFor pass, every duplicate group still has at most
+ // one unguarded member that becomes the bare `case I{Target}:` fall-through.
+ // That fall-through is only safe when no SIBLING alternative may dispatch a
+ // subtype of I{Target} — otherwise the unguarded case greedily swallows the
+ // subtype before it can reach the dispatcher that handles it (e.g. the
+ // OperatorExpression group's unguarded ExtentExpression case swallowing
+ // FeatureChainExpression before it can reach the sibling PrimaryExpression
+ // alternative).
+ //
+ // Detection: the group has subtype overlap if any other alternative in the
+ // dispatch targets a class that is a SUPERTYPE of this group's target — that
+ // sibling's dispatcher may then handle subtypes of this group's target inside
+ // its own switch.
+ //
+ // When detected, synthesise a `when` guard for the would-be-default member
+ // from the rule's parsed body (only parsed assignments contribute; non-parsing
+ // `{ … }` is ignored per GRAMMAR.md).
+ foreach (var duplicateGroup in duplicateClasses)
+ {
+ var stillUnguarded = duplicateGroup.Value
+ .Where(element => !whenGuards.ContainsKey(element.RuleElement))
+ .ToList();
+
+ if (stillUnguarded.Count == 0)
+ {
+ continue;
+ }
+
+ var groupTargetClass = duplicateGroup.Key;
+
+ var hasSubtypeOverlap = mappedNonTerminalElements
+ .Where(other => other.UmlClass != groupTargetClass)
+ .Any(other => groupTargetClass.QueryAllGeneralClassifiers().Contains(other.UmlClass));
+
+ if (!hasSubtypeOverlap)
+ {
+ continue;
+ }
+
+ foreach (var unguarded in stillUnguarded.Select(x => x.RuleElement))
+ {
+ var referencedRule = ruleGenerationContext.AllRules
+ .SingleOrDefault(rule => rule.RuleName == unguarded.Name);
+
+ if (referencedRule == null)
+ {
+ continue;
+ }
+
+ var synthesisedGuard = SynthesiseGuardFromRuleBody(referencedRule, groupTargetClass, umlClass.Cache, ruleGenerationContext.AllRules);
+
+ if (!string.IsNullOrEmpty(synthesisedGuard))
+ {
+ whenGuards[unguarded] = synthesisedGuard;
+ }
+ }
+ }
+
var reorderedElements = new List<(NonTerminalElement RuleElement, IClass UmlClass)>();
var processedDuplicateClasses = new HashSet();
@@ -938,5 +1000,242 @@ private void EmitCompoundPocoTypeBranch(EncodedTextWriter writer, IClass umlClas
}
}
}
+
+ ///
+ /// Synthesises a when-clause guard template for a duplicate-group member that
+ /// would otherwise be emitted as the unguarded case I{Target}: fall-through, by
+ /// walking the rule's parsed body and emitting one predicate per
+ /// . Non-parsing { prop = X } assignments
+ /// () are intentionally ignored — they are
+ /// write-only side effects of parsing per SysML2.NET.CodeGenerator/GRAMMAR.md
+ /// and must not influence dispatch.
+ ///
+ /// The whose body to inspect
+ /// The duplicate group's target
+ /// The used to resolve referenced rule targets
+ /// All available rules for NonTerminal resolution
+ ///
+ /// A template (with {0} as the
+ /// case variable name placeholder) ready for insertion into whenGuards, or
+ /// null when the rule body carries no usable parsed assignments.
+ ///
+ private static string SynthesiseGuardFromRuleBody(TextualNotationRule rule, IClass targetClass, IXmiElementCache cache, IReadOnlyList allRules)
+ {
+ if (rule == null || targetClass == null)
+ {
+ return null;
+ }
+
+ var targetProperties = targetClass.QueryAllProperties();
+ var clauses = new List();
+ var firstCursorEmitted = false;
+ var cursorMayHaveAdvanced = false;
+
+ CollectGuardClauses(rule.Alternatives.SelectMany(alternative => alternative.Elements), targetProperties, cache, allRules, clauses, ref firstCursorEmitted, ref cursorMayHaveAdvanced);
+
+ return clauses.Count == 0 ? null : string.Join(" && ", clauses);
+ }
+
+ ///
+ /// Recursively walks a sequence of values produced by the rule
+ /// parser, accumulating one structural-predicate clause per parsed
+ /// via .
+ ///
+ /// The elements to walk
+ /// Properties of the duplicate group's target class
+ /// The used to resolve referenced rule targets
+ /// All available rules for NonTerminal resolution
+ /// Accumulator into which non-null clauses are appended in source order
+ ///
+ /// Tracks whether a cursor predicate has already been emitted for an
+ /// ownedRelationship += assignment in this rule. Only the first such
+ /// assignment yields a cursor clause; subsequent ones run after the cursor has
+ /// advanced and cannot be expressed at dispatch time.
+ ///
+ ///
+ /// Tracks whether any element walked so far may have advanced the dispatch cursor
+ /// (the ownedRelationship cursor) at runtime — either via a direct
+ /// ownedRelationship += earlier in the body or via a NonTerminalElement
+ /// reference whose target rule may consume ownedRelationship internally. When
+ /// this becomes true, no subsequent ownedRelationship += … can yield a cursor
+ /// predicate at dispatch time because cursor position 0 no longer corresponds to that
+ /// assignment's target. Rules whose first += sits behind a
+ /// NonTerminal?/NonTerminal* prefix (e.g. IndividualDefinition's
+ /// trailing ownedRelationship += EmptyMultiplicityMember) therefore correctly
+ /// produce no cursor clause and fall back to leaving the rule as the unguarded default.
+ ///
+ private static void CollectGuardClauses(IEnumerable elements, IEnumerable targetProperties, IXmiElementCache cache, IReadOnlyList allRules, List clauses, ref bool firstCursorEmitted, ref bool cursorMayHaveAdvanced)
+ {
+ foreach (var element in elements)
+ {
+ switch (element)
+ {
+ case AssignmentElement assignment:
+ {
+ var clause = TryBuildClauseForAssignment(assignment, targetProperties, cache, allRules, ref firstCursorEmitted, cursorMayHaveAdvanced);
+
+ if (!string.IsNullOrEmpty(clause))
+ {
+ clauses.Add(clause);
+ }
+
+ if (assignment.Operator == "+="
+ && string.Equals(assignment.Property, "ownedRelationship", StringComparison.OrdinalIgnoreCase))
+ {
+ cursorMayHaveAdvanced = true;
+ }
+
+ break;
+ }
+
+ case NonTerminalElement:
+ {
+ // A NonTerminal reference may advance the dispatch cursor at runtime
+ // because the referenced rule may consume `ownedRelationship +=`
+ // internally. Conservatively assume it does so that we don't synthesise
+ // a stale cursor-0 predicate for a later `ownedRelationship +=`.
+ cursorMayHaveAdvanced = true;
+ break;
+ }
+
+ case GroupElement group:
+ {
+ foreach (var groupAlternative in group.Alternatives)
+ {
+ CollectGuardClauses(groupAlternative.Elements, targetProperties, cache, allRules, clauses, ref firstCursorEmitted, ref cursorMayHaveAdvanced);
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Builds a single when-clause predicate for one ,
+ /// per the synthesis table in SysML2.NET.CodeGenerator/GRAMMAR.md:
+ ///
+ /// - prop = 'literal' → {0}.{Prop} == "literal"
+ /// - prop = [QualifiedName] → {0}.{Prop} != null
+ /// - prop = NonTerminal → {0}.{Prop} is I{RHS-target} (narrows when possible, else != null)
+ /// - ownedRelationship += NonTerminal (first occurrence) → cursor predicate
+ /// - prop += NonTerminal for any other collection → {0}.{Prop}.OfType<I{RHS-target}>().Any()
+ /// - prop ?= 'kw' → null (already handled by the boolean discriminator pass)
+ ///
+ ///
+ /// The to translate
+ /// Properties of the duplicate group's target class
+ /// The used to resolve referenced rule targets
+ /// All available rules for NonTerminal resolution
+ /// Tracks whether a cursor predicate has been emitted yet for this rule.
+ /// When true, suppresses any new cursor predicate because cursor position 0 no longer corresponds to the upcoming ownedRelationship +=.
+ /// A template clause, or null when no clause applies.
+ private static string TryBuildClauseForAssignment(AssignmentElement assignment, IEnumerable targetProperties, IXmiElementCache cache, IReadOnlyList allRules, ref bool firstCursorEmitted, bool cursorMayHaveAdvanced)
+ {
+ if (assignment?.Property == null || assignment.Operator == "?=")
+ {
+ return null;
+ }
+
+ var matchingProperty = targetProperties.FirstOrDefault(property => string.Equals(property.Name, assignment.Property, StringComparison.OrdinalIgnoreCase));
+
+ var propertyAccessor = matchingProperty != null
+ ? matchingProperty.QueryPropertyNameBasedOnUmlProperties()
+ : assignment.Property.CapitalizeFirstLetter();
+
+ switch (assignment.Operator)
+ {
+ case "=":
+ return BuildScalarAssignmentClause(assignment, propertyAccessor, cache, allRules);
+ case "+=":
+ return BuildCollectionAssignmentClause(assignment, propertyAccessor, cache, allRules, ref firstCursorEmitted, cursorMayHaveAdvanced);
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// Translates a parsed scalar = assignment into its corresponding
+ /// when-clause predicate. Terminal-literal RHS becomes an equality check;
+ /// [QualifiedName] RHS becomes a non-null check; NonTerminal RHS narrows to
+ /// the referenced rule's target metaclass when known.
+ ///
+ /// The with =
+ /// The C# property name resolved via
+ /// The for resolving NonTerminal RHS targets
+ /// All available rules for NonTerminal resolution
+ /// A template clause, or null when no useful clause can be synthesised.
+ private static string BuildScalarAssignmentClause(AssignmentElement assignment, string propertyAccessor, IXmiElementCache cache, IReadOnlyList allRules)
+ {
+ switch (assignment.Value)
+ {
+ case TerminalElement terminal when !string.IsNullOrEmpty(terminal.Value):
+ return $"{{0}}.{propertyAccessor} == \"{terminal.Value}\"";
+
+ case ValueLiteralElement valueLiteral when valueLiteral.QueryIsQualifiedName():
+ return $"{{0}}.{propertyAccessor} != null";
+
+ case NonTerminalElement nonTerminal:
+ {
+ var rhsTargetClass = RuleQueryUtilities.ResolveRuleTargetClass(nonTerminal, cache, allRules);
+ return rhsTargetClass != null
+ ? $"{{0}}.{propertyAccessor} is {rhsTargetClass.QueryFullyQualifiedTypeName()}"
+ : $"{{0}}.{propertyAccessor} != null";
+ }
+
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// Translates a parsed collection += assignment into its corresponding
+ /// when-clause predicate. For the first ownedRelationship += in the
+ /// rule body the predicate inspects the dispatch cursor's current element; for any
+ /// other collection it inspects the collection contents directly. Subsequent
+ /// ownedRelationship += assignments produce no clause because the cursor
+ /// will have advanced past them by the time their position is reached at runtime.
+ ///
+ /// The with +=
+ /// The C# collection-property name resolved via
+ /// The for resolving NonTerminal RHS targets
+ /// All available rules for NonTerminal resolution
+ /// Tracks whether a cursor predicate has been emitted yet for this rule.
+ ///
+ /// When true, the dispatch cursor may have advanced past position 0 at runtime due to a
+ /// prior ownedRelationship += or a preceding NonTerminal reference whose
+ /// target rule may consume the cursor. Suppresses cursor predicate emission so we don't
+ /// generate stale cursor.Current is … checks against the wrong position.
+ ///
+ /// A template clause, or null when no useful clause can be synthesised.
+ private static string BuildCollectionAssignmentClause(AssignmentElement assignment, string propertyAccessor, IXmiElementCache cache, IReadOnlyList allRules, ref bool firstCursorEmitted, bool cursorMayHaveAdvanced)
+ {
+ if (assignment.Value is not NonTerminalElement nonTerminal)
+ {
+ return null;
+ }
+
+ var rhsTargetClass = RuleQueryUtilities.ResolveRuleTargetClass(nonTerminal, cache, allRules);
+
+ if (rhsTargetClass == null)
+ {
+ return null;
+ }
+
+ var rhsTargetTypeName = rhsTargetClass.QueryFullyQualifiedTypeName();
+
+ if (string.Equals(assignment.Property, "ownedRelationship", StringComparison.OrdinalIgnoreCase))
+ {
+ if (firstCursorEmitted || cursorMayHaveAdvanced)
+ {
+ return null;
+ }
+
+ firstCursorEmitted = true;
+ return $"writerContext.CursorCache.GetOrCreateCursor({{0}}.Id, \"{assignment.Property}\", {{0}}.{propertyAccessor}).Current is {rhsTargetTypeName}";
+ }
+
+ return $"{{0}}.{propertyAccessor}.OfType<{rhsTargetTypeName}>().Any()";
+ }
}
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ExpressionTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ExpressionTextualNotationBuilder.cs
index 0b72aaee..5d354fe8 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ExpressionTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/ExpressionTextualNotationBuilder.cs
@@ -63,8 +63,8 @@ public static void BuildOwnedExpression(SysML2.NET.Core.POCO.Kernel.Functions.IE
case SysML2.NET.Core.POCO.Kernel.Expressions.IOperatorExpression pocoOperatorExpressionMetaclassificationExpression when pocoOperatorExpressionMetaclassificationExpression.IsValidForMetaclassificationExpression(writerContext):
OperatorExpressionTextualNotationBuilder.BuildMetaclassificationExpression(pocoOperatorExpressionMetaclassificationExpression, writerContext, stringBuilder);
break;
- case SysML2.NET.Core.POCO.Kernel.Expressions.IOperatorExpression pocoOperatorExpression:
- OperatorExpressionTextualNotationBuilder.BuildExtentExpression(pocoOperatorExpression, writerContext, stringBuilder);
+ case SysML2.NET.Core.POCO.Kernel.Expressions.IOperatorExpression pocoOperatorExpressionExtentExpression when pocoOperatorExpressionExtentExpression.Operator == "all" && writerContext.CursorCache.GetOrCreateCursor(pocoOperatorExpressionExtentExpression.Id, "ownedRelationship", pocoOperatorExpressionExtentExpression.OwnedRelationship).Current is SysML2.NET.Core.POCO.Kernel.Behaviors.IParameterMembership:
+ OperatorExpressionTextualNotationBuilder.BuildExtentExpression(pocoOperatorExpressionExtentExpression, writerContext, stringBuilder);
break;
default:
BuildPrimaryExpression(poco, writerContext, stringBuilder);
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs
index fc571c51..90805799 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/FeatureTextualNotationBuilder.cs
@@ -694,12 +694,12 @@ public static void BuildFeatureRelationshipPart(SysML2.NET.Core.POCO.Core.Featur
case SysML2.NET.Core.POCO.Core.Features.IFeature pocoFeatureInvertingPart when pocoFeatureInvertingPart.IsValidForInvertingPart(writerContext):
BuildInvertingPart(pocoFeatureInvertingPart, writerContext, stringBuilder);
break;
+ case SysML2.NET.Core.POCO.Core.Features.IFeature pocoFeatureTypeFeaturingPart when writerContext.CursorCache.GetOrCreateCursor(pocoFeatureTypeFeaturingPart.Id, "ownedRelationship", pocoFeatureTypeFeaturingPart.OwnedRelationship).Current is SysML2.NET.Core.POCO.Core.Features.ITypeFeaturing && pocoFeatureTypeFeaturingPart.ownedTypeFeaturing.OfType().Any():
+ BuildTypeFeaturingPart(pocoFeatureTypeFeaturingPart, writerContext, stringBuilder);
+ break;
case SysML2.NET.Core.POCO.Core.Types.IType pocoType:
TypeTextualNotationBuilder.BuildTypeRelationshipPart(pocoType, writerContext, stringBuilder);
break;
- default:
- BuildTypeFeaturingPart(poco, writerContext, stringBuilder);
- break;
}
}
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs
index d9ab679b..b4a63358 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/AutoGenTextualNotationBuilder/UsageTextualNotationBuilder.cs
@@ -498,8 +498,8 @@ public static void BuildVariantUsageElement(SysML2.NET.Core.POCO.Systems.Definit
case SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IReferenceUsage pocoReferenceUsageReferenceUsage when pocoReferenceUsageReferenceUsage.IsEnd:
ReferenceUsageTextualNotationBuilder.BuildReferenceUsage(pocoReferenceUsageReferenceUsage, writerContext, stringBuilder);
break;
- case SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IReferenceUsage pocoReferenceUsage:
- ReferenceUsageTextualNotationBuilder.BuildVariantReference(pocoReferenceUsage, writerContext, stringBuilder);
+ case SysML2.NET.Core.POCO.Systems.DefinitionAndUsage.IReferenceUsage pocoReferenceUsageVariantReference when writerContext.CursorCache.GetOrCreateCursor(pocoReferenceUsageVariantReference.Id, "ownedRelationship", pocoReferenceUsageVariantReference.OwnedRelationship).Current is SysML2.NET.Core.POCO.Core.Features.IReferenceSubsetting:
+ ReferenceUsageTextualNotationBuilder.BuildVariantReference(pocoReferenceUsageVariantReference, writerContext, stringBuilder);
break;
case SysML2.NET.Core.POCO.Systems.Attributes.IAttributeUsage pocoAttributeUsage:
AttributeUsageTextualNotationBuilder.BuildAttributeUsage(pocoAttributeUsage, writerContext, stringBuilder);
@@ -510,8 +510,8 @@ public static void BuildVariantUsageElement(SysML2.NET.Core.POCO.Systems.Definit
case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage pocoOccurrenceUsageOccurrenceUsage when pocoOccurrenceUsageOccurrenceUsage.IsValidForOccurrenceUsage(writerContext):
OccurrenceUsageTextualNotationBuilder.BuildOccurrenceUsage(pocoOccurrenceUsageOccurrenceUsage, writerContext, stringBuilder);
break;
- case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage pocoOccurrenceUsage:
- OccurrenceUsageTextualNotationBuilder.BuildPortionUsage(pocoOccurrenceUsage, writerContext, stringBuilder);
+ case SysML2.NET.Core.POCO.Systems.Occurrences.IOccurrenceUsage pocoOccurrenceUsagePortionUsage when pocoOccurrenceUsagePortionUsage.PortionKind != null:
+ OccurrenceUsageTextualNotationBuilder.BuildPortionUsage(pocoOccurrenceUsagePortionUsage, writerContext, stringBuilder);
break;
default:
BuildBehaviorUsageElement(poco, writerContext, stringBuilder);
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs b/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs
index 768e2eed..4f00d871 100644
--- a/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs
+++ b/SysML2.NET.Serializer.TextualNotation/Writers/TextualNotationValidationExtensions.cs
@@ -540,19 +540,34 @@ internal static bool IsValidForMetaclassificationExpression(this IOperatorExpres
///
/// Asserts that the is valid for the SequenceExpression rule.
- /// SequenceExpression = OwnedExpression ','? | SequenceOperatorExpression
- /// Acts as a catch-all in the PrimaryExpression switch: by the time a switch reaches this
- /// case, every more-specific PrimaryExpression variant (Classification, Metaclassification,
- /// Conditional*, BinaryOp, UnaryOp, FeatureReference, …) has already failed, so any remaining
- /// is valid as a SequenceExpression. No runtime property further
- /// disambiguates.
+ /// SequenceExpression : Expression = '(' SequenceExpressionList ')'
+ /// SequenceExpression is the wrapping (…) rule that applies to expressions
+ /// which are not one of the more specific BaseExpression variants
+ /// (NullExpression | LiteralExpression | FeatureReferenceExpression |
+ /// MetadataAccessExpression | InvocationExpression | ConstructorExpression | BodyExpression).
+ /// The grammar lists SequenceExpression before BaseExpression as an alternative, but at
+ /// unparse time we don't have the surface-text parens to discriminate, so this guard
+ /// must explicitly exclude the BaseExpression metaclasses to prevent it from swallowing
+ /// them.
///
/// The
/// The active (unused for this guard)
- /// True for any non-null expression
+ ///
+ /// True when the expression is not null and is not one of the runtime types handled
+ /// by BaseExpression's dispatch (,
+ /// , ,
+ /// , ,
+ /// ); false otherwise.
+ ///
internal static bool IsValidForSequenceExpression(this IExpression expression, TextualNotationWriterContext writerContext)
{
- return expression is not null;
+ return expression is not null
+ && expression is not INullExpression
+ && expression is not ILiteralExpression
+ && expression is not IFeatureReferenceExpression
+ && expression is not IMetadataAccessExpression
+ && expression is not IInvocationExpression
+ && expression is not IConstructorExpression;
}
///
diff --git a/SysML2.NET.Serializer.TextualNotation/Writers/UsageTextualNotationBuilder.cs b/SysML2.NET.Serializer.TextualNotation/Writers/UsageTextualNotationBuilder.cs
deleted file mode 100644
index eafa3648..00000000
--- a/SysML2.NET.Serializer.TextualNotation/Writers/UsageTextualNotationBuilder.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-// -------------------------------------------------------------------------------------------------
-//
-//
-// Copyright 2022-2026 Starion Group S.A.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-//
-// ------------------------------------------------------------------------------------------------
-
-namespace SysML2.NET.Serializer.TextualNotation.Writers
-{
- using System.Text;
-
- using SysML2.NET.Core.POCO.Systems.DefinitionAndUsage;
-
- ///
- /// Hand-coded part of the
- ///
- public static partial class UsageTextualNotationBuilder
- {
- }
-}
diff --git a/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs
index 74b662ec..f3bdca61 100644
--- a/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs
+++ b/SysML2.NET.Tests/Extend/FeatureValueExtensionsTestFixture.cs
@@ -1,44 +1,81 @@
-// -------------------------------------------------------------------------------------------------
+// -------------------------------------------------------------------------------------------------
//
-//
+//
// Copyright 2022-2026 Starion Group S.A.
-//
+//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
-//
+//
// http://www.apache.org/licenses/LICENSE-2.0
-//
+//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-//
+//
//
// ------------------------------------------------------------------------------------------------
namespace SysML2.NET.Tests.Extend
{
using System;
-
+
using NUnit.Framework;
-
+
+ using SysML2.NET.Core.POCO.Core.Features;
+ using SysML2.NET.Core.POCO.Kernel.Expressions;
using SysML2.NET.Core.POCO.Kernel.FeatureValues;
+ using SysML2.NET.Core.POCO.Root.Elements;
+ using SysML2.NET.Core.POCO.Root.Namespaces;
+ using SysML2.NET.Extensions;
[TestFixture]
public class FeatureValueExtensionsTestFixture
{
[Test]
- public void ComputeFeatureWithValue_ThrowsNotSupportedException()
+ public void VerifyComputeFeatureWithValue()
{
- Assert.That(() => ((IFeatureValue)null).ComputeFeatureWithValue(), Throws.TypeOf());
+ Assert.That(() => ((IFeatureValue)null).ComputeFeatureWithValue(), Throws.TypeOf());
+
+ var feature = new Feature();
+ var featureValue = new FeatureValue();
+ var literalBoolean = new LiteralBoolean();
+
+ feature.AssignOwnership(featureValue, literalBoolean);
+
+ Assert.That(featureValue.ComputeFeatureWithValue(), Is.SameAs(feature));
+
+ // Non-Feature owning namespace: direct field bypass — the cast must return null.
+ var nonFeatureFeatureValue = new FeatureValue();
+ var nonFeatureNamespace = new Namespace();
+
+ ((IContainedRelationship)nonFeatureFeatureValue).OwningRelatedElement = nonFeatureNamespace;
+
+ Assert.That(nonFeatureFeatureValue.ComputeFeatureWithValue(), Is.Null);
}
-
+
[Test]
- public void ComputeValue_ThrowsNotSupportedException()
+ public void VerifyComputeValue()
{
- Assert.That(() => ((IFeatureValue)null).ComputeValue(), Throws.TypeOf());
+ Assert.That(() => ((IFeatureValue)null).ComputeValue(), Throws.TypeOf());
+
+ var feature = new Feature();
+ var featureValue = new FeatureValue();
+ var literalBoolean = new LiteralBoolean();
+
+ feature.AssignOwnership(featureValue, literalBoolean);
+
+ Assert.That(featureValue.ComputeValue(), Is.SameAs(literalBoolean));
+
+ // Non-Expression owned member: direct field bypass — the cast must return null.
+ var nonExprFeatureValue = new FeatureValue();
+ var nonExprElement = new Namespace();
+
+ ((IContainedRelationship)nonExprFeatureValue).OwnedRelatedElement.Add(nonExprElement);
+
+ Assert.That(nonExprFeatureValue.ComputeValue(), Is.Null);
}
}
}
diff --git a/SysML2.NET/Extend/FeatureValueExtensions.cs b/SysML2.NET/Extend/FeatureValueExtensions.cs
index 467fb060..c43c10f9 100644
--- a/SysML2.NET/Extend/FeatureValueExtensions.cs
+++ b/SysML2.NET/Extend/FeatureValueExtensions.cs
@@ -45,10 +45,11 @@ internal static class FeatureValueExtensions
///
/// the computed result
///
- [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static IFeature ComputeFeatureWithValue(this IFeatureValue featureValueSubject)
{
- throw new NotSupportedException("Create a GitHub issue when this method is required");
+ return featureValueSubject == null
+ ? throw new ArgumentNullException(nameof(featureValueSubject))
+ : featureValueSubject.OwningRelatedElement as IFeature;
}
///
@@ -60,11 +61,16 @@ internal static IFeature ComputeFeatureWithValue(this IFeatureValue featureValue
///
/// the computed result
///
- [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static IExpression ComputeValue(this IFeatureValue featureValueSubject)
{
- throw new NotSupportedException("Create a GitHub issue when this method is required");
- }
+ if (featureValueSubject == null)
+ {
+ throw new ArgumentNullException(nameof(featureValueSubject));
+ }
+ return featureValueSubject.OwnedRelatedElement.Count == 1
+ ? featureValueSubject.OwnedRelatedElement[0] as IExpression
+ : null;
+ }
}
}