Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Resources/KerML-textual-bnf.kebnf
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ SuccessionDeclaration : Succession =
FeatureDeclaration
( 'first' ownedRelationship += ConnectorEndMember
'then' ownedRelationship += ConnectorEndMember )?
| ( s.isSufficient ?= 'all' )?
| ( isSufficient ?= 'all' )?
( 'first'? ownedRelationship += ConnectorEndMember
'then' ownedRelationship += ConnectorEndMember )?

Expand Down Expand Up @@ -993,7 +993,7 @@ CastOperator =

MetaclassificationExpression : OperatorExpression =
ownedRelationship += MetadataArgumentMember
( operator = ClassificationTestOperator
( operator = MetaclassificationTestOperator
ownedRelationship += TypeReferenceMember
| operator = MetaCastOperator
ownedRelationship += TypeResultMember
Expand Down Expand Up @@ -1133,7 +1133,7 @@ SelectExpression =

FunctionOperationExpression : InvocationExpression =
ownedRelationship += PrimaryArgumentMember '->'
ownedRelationship += InvocationTypeMember
ownedRelationship += InstantiatedTypeMember
( ownedRelationship += BodyArgumentMember
| ownedRelationship += FunctionReferenceArgumentMember
| ArgumentList )
Expand Down Expand Up @@ -1233,8 +1233,8 @@ ArgumentList : Feature =
'(' ( PositionalArgumentList | NamedArgumentList )? ')'

PositionalArgumentList : Feature =
e.ownedRelationship += ArgumentMember
( ',' e.ownedRelationship += ArgumentMember )*
ownedRelationship += ArgumentMember
( ',' ownedRelationship += ArgumentMember )*

NamedArgumentList : Feature =
ownedRelationship += NamedArgumentMember
Expand Down Expand Up @@ -1326,7 +1326,7 @@ PayloadFeature =
| ownedRelationship += OwnedFeatureTyping
( ownedRelationship += OwnedMultiplicity )?
| ownedRelationship += OwnedMultiplicity
( ownedRelationship += OwnedFeatureTyping )?
ownedRelationship += OwnedFeatureTyping

PayloadFeatureSpecializationPart : Feature =
FeatureSpecialization+ MultiplicityPart?
Expand Down
20 changes: 12 additions & 8 deletions Resources/SysML-textual-bnf.kebnf
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ RESERVED_KEYWORD =
| 'def' | 'default' | 'defined' | 'dependency' | 'derived' | 'do' | 'doc' | 'else' | 'end' | 'entry' | 'enum'
| 'event' | 'exhibit' | 'exit' | 'expose' | 'false' | 'filter' | 'first' | 'flow' | 'for' | 'fork' | 'frame' | 'from'
| 'hastype' | 'if' | 'implies' | 'import' | 'in' | 'include' | 'individual' | 'inout' | 'interface' | 'istype'
| 'item' | 'join' | 'language' | 'library' | 'locale' | 'loop' | 'merge' | 'message' | 'meta' | 'metadata'
| 'item' | 'join' | 'language' | 'library' | 'locale' | 'loop' | 'merge' | 'message' | 'meta' | 'metadata' | 'new'
| 'nonunique' | 'not' | 'null' | 'objective' | 'occurrence' | 'of' | 'or' | 'ordered' | 'out' | 'package' | 'parallel'
| 'part' | 'perform' | 'port' | 'private' | 'protected' | 'public' | 'redefines' | 'ref' | 'references' | 'render'
| 'rendering' | 'rep' | 'require' | 'requirement' | 'return' | 'satisfy' | 'send' | 'snapshot' | 'specializes'
Expand Down Expand Up @@ -69,7 +69,7 @@ OwnedAnnotation : Annotation =
ownedRelatedElement += AnnotatingElement

AnnotatingMember : OwningMembership =
ownedRelatedElement += AnnotatingElement
MemberPrefix ownedRelatedElement += AnnotatingElement

AnnotatingElement =
Comment
Expand Down Expand Up @@ -169,6 +169,10 @@ FilterPackage : Package =
ownedRelationship += FilterPackageImport
( ownedRelationship += FilterPackageMember )+

FilterPackageImport : Import =
ImportDeclaration
{ visibility = 'public' }

FilterPackageMember : ElementFilterMembership =
'[' ownedRelatedElement += OwnedExpression ']'

Expand Down Expand Up @@ -709,8 +713,8 @@ BindingConnectorAsUsage =

SuccessionAsUsage =
UsagePrefix ( 'succession' UsageDeclaration )?
'first' s.ownedRelationship += ConnectorEndMember
'then' s.ownedRelationship += ConnectorEndMember
'first' ownedRelationship += ConnectorEndMember
'then' ownedRelationship += ConnectorEndMember
UsageBody

// Clause 8.2.2.14 Interfaces Textual Notation
Expand Down Expand Up @@ -853,7 +857,7 @@ PayloadFeature : Feature =
ownedRelationship += OwnedFeatureTyping

PayloadFeatureSpecializationPart : Feature =
( FeatureSpecialization )+ MultiplicityPart?
FeatureSpecialization+ MultiplicityPart?
FeatureSpecialization*
| MultiplicityPart FeatureSpecialization+

Expand Down Expand Up @@ -1089,7 +1093,7 @@ AssignmentNode : AssignmentActionUsage =
OccurrenceUsagePrefix
AssignmentNodeDeclaration ActionBody

AssignmentNodeDeclaration: ActionUsage =
AssignmentNodeDeclaration : ActionUsage =
( ActionNodeUsageDeclaration )? 'assign'
ownedRelationship += AssignmentTargetMember
ownedRelationship += FeatureChainMember ':='
Expand Down Expand Up @@ -1440,10 +1444,10 @@ FramedConcernMember : FramedConcernMembership =

FramedConcernUsage : ConcernUsage =
ownedRelationship += OwnedReferenceSubsetting
FeatureSpecializationPart? CalculationBody
FeatureSpecializationPart? RequirementBody
| ( UsageExtensionKeyword* 'concern'
| UsageExtensionKeyword+ )
CalculationUsageDeclaration CalculationBody
ConstraintUsageDeclaration RequirementBody

ActorMember : ActorMembership =
MemberPrefix ownedRelatedElement += ActorUsage
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// -------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------
// <copyright file="RuleProcessor.ElementProcessing.cs" company="Starion Group S.A.">
//
// Copyright 2022-2026 Starion Group S.A.
Expand Down Expand Up @@ -105,14 +105,23 @@
}
}

// A repeated group followed by a MANDATORY consumption of the same element type
// from the same cursor — the `( X )+ X` shape of e.g.
// FeatureChainPrefix = ( ownedRelationship += OwnedFeatureChaining '.' )+
// ownedRelationship += OwnedFeatureChaining '.'
// — must leave one element for that trailing consumption. Without the
// reservation the loop eats every element and the mandatory tail emits its
// terminals against an exhausted cursor (`a.b.` became `a.b..`).
var reservationGuard = ResolveTrailingConsumptionReservation(cursorToUse, umlClass, ruleGenerationContext);

if (groupTypeGuard.StartsWith("__FULL_GUARD__"))
{
var fullGuard = groupTypeGuard.Substring("__FULL_GUARD__".Length);
writer.WriteSafeString($"{Environment.NewLine}while({fullGuard}){Environment.NewLine}");
writer.WriteSafeString($"{Environment.NewLine}while({fullGuard}{reservationGuard}){Environment.NewLine}");
}
else
{
writer.WriteSafeString($"{Environment.NewLine}while({cursorToUse.CursorVariableName}.Current != null{groupTypeGuard}){Environment.NewLine}");
writer.WriteSafeString($"{Environment.NewLine}while({cursorToUse.CursorVariableName}.Current != null{groupTypeGuard}{reservationGuard}){Environment.NewLine}");
}
}

Expand Down Expand Up @@ -197,7 +206,7 @@
}
else
{
var handCodedRuleName = groupElement.TextualNotationRule?.RuleName ?? "Unknown";

Check warning on line 209 in SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'Unknown' 6 times.
EmitHandCodedFallback(writer, handCodedRuleName, ruleGenerationContext);
}
}
Expand Down Expand Up @@ -231,7 +240,7 @@

if (!ruleGenerationContext.IsNextElementNewLineTerminal())
{
writer.WriteSafeString("stringBuilder.Append(' ');");

Check warning on line 243 in SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.ElementProcessing.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'stringBuilder.Append(' ');' 5 times.
}
}
else
Expand All @@ -248,6 +257,45 @@
writer.WriteSafeString(Environment.NewLine);
}

/// <summary>
/// Builds the extra <c>while</c> clause that reserves one element for a MANDATORY consumption
/// following a repeated group, or an empty string when no reservation is needed.
/// <para>KEBNF rules of the shape <c>( prop += X )+ prop += X</c> — <c>FeatureChainPrefix</c> being
/// the canonical case — consume from a single shared cursor. Emitted naively the loop is greedy: it
/// takes every element, and the mandatory trailing assignment then emits its terminals with nothing
/// left to consume, duplicating them. The guard <c>cursor.GetNext(1) is T</c> stops the loop one
/// element short, which is exactly the arity the grammar asks for.</para>
/// <para>Applies only when the element immediately following the group in the SAME alternative is a
/// non-optional <c>+=</c> assignment drawing on the same cursor and the same sub-rule; any other
/// successor consumes different elements and needs no reservation.</para>
/// </summary>
/// <param name="cursorDefinition">The cursor the group consumes from.</param>
/// <param name="umlClass">The class hosting the current rule (provides the UML cache).</param>
/// <param name="ruleGenerationContext">The current <see cref="RuleGenerationContext" />.</param>
/// <returns>The additional guard clause, or an empty string.</returns>
private static string ResolveTrailingConsumptionReservation(CursorDefinition cursorDefinition, IClass umlClass, RuleGenerationContext ruleGenerationContext)
{
var siblings = ruleGenerationContext.CurrentSiblingElements;

if (siblings == null || ruleGenerationContext.CurrentElementIndex + 1 >= siblings.Count)
{
return string.Empty;
}

if (siblings[ruleGenerationContext.CurrentElementIndex + 1] is not AssignmentElement { Operator: "+=", IsOptional: false, IsCollection: false } trailingAssignment
|| !cursorDefinition.IsCursorValidForProperty(cursorDefinition.DefinedForProperty)
|| !string.Equals(trailingAssignment.Property, cursorDefinition.DefinedForProperty.Name, StringComparison.OrdinalIgnoreCase))
{
return string.Empty;
}

var trailingTypeName = ResolveAssignmentTargetTypeName(trailingAssignment, umlClass, ruleGenerationContext);

return trailingTypeName == null
? string.Empty
: $" && {cursorDefinition.CursorVariableName}.GetNext(1) is {trailingTypeName}";
}

/// <summary>
/// Processes an <see cref="AssignmentElement" />
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,34 @@ private bool TryHandleEmptyVsNonEmptyMembership(EncodedTextWriter writer, IClass
var typeName = emptyTarget.QueryFullyQualifiedTypeName();
var cursorVarName = cursor.CursorVariableName;

// An "Empty" wrapper rule usually still ASSIGNS its collection — e.g.
// EmptyParameterMember : ParameterMembership = ownedRelatedElement += EmptyUsage, where
// EmptyUsage : ReferenceUsage = {} contributes an element that simply emits nothing. For
// such rules the collection is never empty at runtime, so a Count-based discriminator can
// never select the empty branch (WhileLoopNode always emitted 'while', never 'loop').
// Discriminate on the WRAPPED element type instead whenever the two branches wrap
// different classes: the non-empty branch is the one that actually carries its payload
// (an OwnedExpression), and the empty branch is the degenerate fallback.
var wrappedNonEmptyTypeName = QueryWrappedElementTypeName(nonEmptyBranch.NonTerminal, umlClass, ruleGenerationContext);
var wrappedEmptyTypeName = QueryWrappedElementTypeName(emptyBranch.NonTerminal, umlClass, ruleGenerationContext);

if (wrappedNonEmptyTypeName != null && wrappedEmptyTypeName != null && wrappedNonEmptyTypeName != wrappedEmptyTypeName)
{
var payloadVarName = $"{emptyTarget.Name.LowerCaseFirstLetter()}Payload{ruleGenerationContext.NarrowedTypeCheckCounter}";
ruleGenerationContext.NarrowedTypeCheckCounter++;

writer.WriteSafeString($"if ({cursorVarName}.Current is {typeName} {payloadVarName} && {payloadVarName}.{discriminatorPropertyName}.OfType<{wrappedNonEmptyTypeName}>().Any()){Environment.NewLine}");
writer.WriteSafeString($"{{{Environment.NewLine}");
this.EmitAlternativeBody(writer, umlClass, nonEmptyBranch.Alternative, ruleGenerationContext);
writer.WriteSafeString($"}}{Environment.NewLine}");
writer.WriteSafeString($"else if ({cursorVarName}.Current is {typeName}){Environment.NewLine}");
writer.WriteSafeString($"{{{Environment.NewLine}");
this.EmitAlternativeBody(writer, umlClass, emptyBranch.Alternative, ruleGenerationContext);
writer.WriteSafeString($"}}{Environment.NewLine}");

return true;
}

writer.WriteSafeString($"if ({cursorVarName}.Current is {typeName} {{ {discriminatorPropertyName}.Count: 0 }}){Environment.NewLine}");
writer.WriteSafeString($"{{{Environment.NewLine}");
this.EmitAlternativeBody(writer, umlClass, emptyBranch.Alternative, ruleGenerationContext);
Expand All @@ -234,6 +262,38 @@ private bool TryHandleEmptyVsNonEmptyMembership(EncodedTextWriter writer, IClass
return true;
}

/// <summary>
/// Resolves the fully-qualified runtime type name of the element that a single-assignment
/// wrapper rule (e.g. <c>ExpressionParameterMember : ParameterMembership = ownedRelatedElement += OwnedExpression</c>)
/// puts into its collection — here <c>IExpression</c>. Returns <see langword="null" /> when the
/// referenced rule does not have exactly one <c>+=</c> assignment of a non-terminal, or when the
/// wrapped rule's target class cannot be resolved.
/// </summary>
/// <param name="wrapperNonTerminal">The <see cref="NonTerminalElement" /> naming the wrapper rule.</param>
/// <param name="umlClass">The class hosting the current rule (provides the UML cache).</param>
/// <param name="ruleGenerationContext">The current <see cref="RuleGenerationContext" />.</param>
/// <returns>The wrapped element's fully-qualified type name, or <see langword="null" />.</returns>
private static string QueryWrappedElementTypeName(NonTerminalElement wrapperNonTerminal, IClass umlClass, RuleGenerationContext ruleGenerationContext)
{
var wrapperRule = ruleGenerationContext.FindRule(wrapperNonTerminal.Name);

var wrappedAssignments = wrapperRule?.Alternatives
.SelectMany(alternative => alternative.Elements)
.OfType<AssignmentElement>()
.Where(assignment => assignment is { Operator: "+=", Value: NonTerminalElement })
.ToList();

if (wrappedAssignments == null || wrappedAssignments.Count != 1)
{
return null;
}

var wrappedNonTerminal = (NonTerminalElement)wrappedAssignments[0].Value;
var wrappedClass = RuleQueryUtilities.ResolveRuleTargetClass(wrappedNonTerminal, umlClass.Cache, ruleGenerationContext.AllRules);

return wrappedClass?.QueryFullyQualifiedTypeName();
}

/// <summary>
/// Pattern C: detects poco-runtime-type dispatch with compound alternatives.
/// </summary>
Expand Down
Loading
Loading