From c8dfb091f346441b3c7469bcee6d920c522951dd Mon Sep 17 00:00:00 2001 From: atheate Date: Wed, 13 May 2026 20:21:44 +0200 Subject: [PATCH 1/2] Fix #136 and improve textual notation --- Resources/Quantities.sysml | 107 +++++++ ...tualNotationBuilderGeneratorTestFixture.cs | 36 +++ SysML2.NET.CodeGenerator/GRAMMAR.md | 12 +- .../RuleProcessor.PatternHandlers.cs | 299 ++++++++++++++++++ .../ExpressionTextualNotationBuilder.cs | 4 +- .../FeatureTextualNotationBuilder.cs | 6 +- .../UsageTextualNotationBuilder.cs | 8 +- .../TextualNotationValidationExtensions.cs | 31 +- .../Writers/UsageTextualNotationBuilder.cs | 33 -- .../FeatureValueExtensionsTestFixture.cs | 63 +++- SysML2.NET/Extend/FeatureValueExtensions.cs | 12 +- 11 files changed, 542 insertions(+), 69 deletions(-) create mode 100644 Resources/Quantities.sysml delete mode 100644 SysML2.NET.Serializer.TextualNotation/Writers/UsageTextualNotationBuilder.cs 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..ab8482d2 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) + { + var referencedRule = ruleGenerationContext.AllRules + .SingleOrDefault(rule => rule.RuleName == unguarded.RuleElement.Name); + + if (referencedRule == null) + { + continue; + } + + var synthesisedGuard = this.SynthesiseGuardFromRuleBody(referencedRule, groupTargetClass, umlClass.Cache, ruleGenerationContext.AllRules); + + if (!string.IsNullOrEmpty(synthesisedGuard)) + { + whenGuards[unguarded.RuleElement] = 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 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..f81e27d5 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,10 +61,13 @@ 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"); + return featureValueSubject == null + ? throw new ArgumentNullException(nameof(featureValueSubject)) + : featureValueSubject.OwnedRelatedElement.Count == 1 + ? featureValueSubject.OwnedRelatedElement[0] as IExpression + : null; } } From 84836bf3b4fcab77180737db4e4c21358b5c8238 Mon Sep 17 00:00:00 2001 From: atheate Date: Fri, 15 May 2026 08:54:02 +0200 Subject: [PATCH 2/2] SQ issues --- .../HandleBarHelpers/RuleProcessor.PatternHandlers.cs | 10 +++++----- SysML2.NET/Extend/FeatureValueExtensions.cs | 10 ++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs index ab8482d2..3fb1995c 100644 --- a/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs +++ b/SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.PatternHandlers.cs @@ -675,21 +675,21 @@ private void ProcessUnitypedAlternativesWithOneElement(EncodedTextWriter writer, continue; } - foreach (var unguarded in stillUnguarded) + foreach (var unguarded in stillUnguarded.Select(x => x.RuleElement)) { var referencedRule = ruleGenerationContext.AllRules - .SingleOrDefault(rule => rule.RuleName == unguarded.RuleElement.Name); + .SingleOrDefault(rule => rule.RuleName == unguarded.Name); if (referencedRule == null) { continue; } - var synthesisedGuard = this.SynthesiseGuardFromRuleBody(referencedRule, groupTargetClass, umlClass.Cache, ruleGenerationContext.AllRules); + var synthesisedGuard = SynthesiseGuardFromRuleBody(referencedRule, groupTargetClass, umlClass.Cache, ruleGenerationContext.AllRules); if (!string.IsNullOrEmpty(synthesisedGuard)) { - whenGuards[unguarded.RuleElement] = synthesisedGuard; + whenGuards[unguarded] = synthesisedGuard; } } } @@ -1019,7 +1019,7 @@ private void EmitCompoundPocoTypeBranch(EncodedTextWriter writer, IClass umlClas /// case variable name placeholder) ready for insertion into whenGuards, or /// null when the rule body carries no usable parsed assignments. /// - private string SynthesiseGuardFromRuleBody(TextualNotationRule rule, IClass targetClass, IXmiElementCache cache, IReadOnlyList allRules) + private static string SynthesiseGuardFromRuleBody(TextualNotationRule rule, IClass targetClass, IXmiElementCache cache, IReadOnlyList allRules) { if (rule == null || targetClass == null) { diff --git a/SysML2.NET/Extend/FeatureValueExtensions.cs b/SysML2.NET/Extend/FeatureValueExtensions.cs index f81e27d5..c43c10f9 100644 --- a/SysML2.NET/Extend/FeatureValueExtensions.cs +++ b/SysML2.NET/Extend/FeatureValueExtensions.cs @@ -63,12 +63,14 @@ internal static IFeature ComputeFeatureWithValue(this IFeatureValue featureValue /// internal static IExpression ComputeValue(this IFeatureValue featureValueSubject) { - return featureValueSubject == null - ? throw new ArgumentNullException(nameof(featureValueSubject)) - : featureValueSubject.OwnedRelatedElement.Count == 1 + if (featureValueSubject == null) + { + throw new ArgumentNullException(nameof(featureValueSubject)); + } + + return featureValueSubject.OwnedRelatedElement.Count == 1 ? featureValueSubject.OwnedRelatedElement[0] as IExpression : null; } - } }