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
36 changes: 36 additions & 0 deletions SysML2.NET.CodeGenerator/GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,39 @@ Consequence, in `TypeTextualNotationBuilder.EmitTargetTransitionRun`: the transi
exception to the `Move()` ↔ `+=` Golden Rule, valid because the elected production has no notation for
that element. It is conditional — it only runs after `QueryImpliedSourceTransition` has confirmed
position 0 is the source membership — so it cannot consume a real element.

### `DefaultReferenceUsage` vs `ReferenceUsage` — the optional `ref` keyword

```
NonOccurrenceUsageElement : Usage = DefaultReferenceUsage | ReferenceUsage | AttributeUsage | …
DefaultReferenceUsage : ReferenceUsage = RefPrefix Usage ← no 'ref'
ReferenceUsage = ( EndUsagePrefix | RefPrefix ) 'ref' Usage
RefPrefix : Usage = ( direction = … )? ( isDerived ?= … )? ( isAbstract ?= … | isVariation ?= … )? ( isConstant ?= … )?
```

`RefPrefix` contains no `ref` keyword, and the `'ref'` in `ReferenceUsage` is a BARE terminal — not
`isReference ?= 'ref'` — so it sets no property. Both productions therefore round-trip to the same
model, and nothing records which the author wrote.

The spec settles only the OPTIONALITY, not the choice. SysML 2.0 §7.6.4 Reference Usages (p. 74,
informative): "The declaration of a reference usage may, but is not required, to include the `ref`
keyword. However, a reference usage is always, by definition, referential." So both forms are valid
and the writer must pick one; nothing normative says which.

What follows is therefore an EMPIRICAL convention fitted to the corpus, not a spec rule — and §7.6.4's
own example contradicts its "named" half (`orderedContent ordered :>> content;` is named yet omits
`ref`). It is kept because it reproduces the pilot on all validated files and is always valid output.

The corpus is consistent once both halves of the condition are taken together. `06` writes
`:>> mass = m;` (unnamed, empty `RefPrefix`), `3c-…-2` writes `abstract ref :>> trailerHitch[1];`
(unnamed but `RefPrefix` carries `isAbstract`) and `5-…-1` writes `ref vehicle: VehicleA;` (named).
Each condition alone is refuted by one of the three; the conjunction fits all of them:

> omit `ref` when the usage is unnamed AND `RefPrefix` is empty — there is no declaration for the
> keyword to qualify. Otherwise write it.

`IsValidForDefaultReferenceUsage` implements exactly that, on top of the spec-mandated case
(a directed usage is always referential, Clause 7.6.3, so the keyword is redundant there).

`IsValidForDefaultReferenceUsage` still encodes the one spec-mandated case (`!IsEnd &&
Direction.HasValue`): a directed usage is always referential, so the keyword is redundant there.
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private void EmitCollectionNonTerminalLoop(EncodedTextWriter writer, IClass umlC

var perItemCall = ResolveBuilderCall(umlClass, nonTerminalElement, typeTarget, ruleGenerationContext);

var whileTypeExclusion = ResolveCollectionWhileTypeCondition(cursorVariableName, umlClass, referencedRule, ruleGenerationContext);
var whileTypeExclusion = this.ResolveCollectionWhileTypeCondition(cursorVariableName, umlClass, referencedRule, propertyName, ruleGenerationContext);

string whileCondition;

Expand Down Expand Up @@ -174,7 +174,7 @@ private void EmitCollectionNonTerminalLoop(EncodedTextWriter writer, IClass umlC
/// <summary>
/// Resolves the type condition for a collection while loop.
/// </summary>
private static string ResolveCollectionWhileTypeCondition(string cursorVariableName, IClass umlClass, TextualNotationRule collectionRule, RuleGenerationContext ruleGenerationContext)
private string ResolveCollectionWhileTypeCondition(string cursorVariableName, IClass umlClass, TextualNotationRule collectionRule, string outerPropertyName, RuleGenerationContext ruleGenerationContext)
{
var siblings = ruleGenerationContext.CurrentSiblingElements;
var currentIndex = ruleGenerationContext.CurrentElementIndex;
Expand Down Expand Up @@ -202,6 +202,19 @@ private static string ResolveCollectionWhileTypeCondition(string cursorVariableN
var itemRule = ruleGenerationContext.FindRule(assignmentNonTerminals[0].Name);
var itemTypeTarget = itemRule != null ? itemRule.EffectiveTarget : null;

// The item rule's own target is the WRAPPER type for a thin owning wrapper
// (X : OwningMembership = … ownedRelatedElement = Y), which every sibling wrapper on this
// cursor also satisfies — `individual def` consumed its own EmptyMultiplicityMember as a
// DefinitionExtensionKeyword and emitted a stray '#'. Prefer the wrapped-type guard when
// one is available; otherwise keep the coarse test rather than falling through to a
// weaker condition that could admit elements the loop body will not consume.
var wrappedTypeGuard = this.ResolveContentTypeGuard(cursorVariableName, collectionRule, outerPropertyName, umlClass, ruleGenerationContext);

if (!string.IsNullOrWhiteSpace(wrappedTypeGuard))
{
return wrappedTypeGuard;
}

if (itemTypeTarget != null)
{
var itemTargetClass = umlClass.Cache.Values.OfType<INamedElement>()
Expand Down
84 changes: 84 additions & 0 deletions SysML2.NET.CodeGenerator/HandleBarHelpers/RuleProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@
/// <param name="isPartOfMultipleAlternative">Whether this is part of a multi-alternative context</param>
private void EmitElements(EncodedTextWriter writer, IClass umlClass, List<RuleElement> elements, RuleGenerationContext ruleGenerationContext, bool restoreCallerPerElement = false, bool isPartOfMultipleAlternative = false)
{
elements = HoistSingleNonNotationalConsumption(elements, ruleGenerationContext);

var previousSiblings = ruleGenerationContext.CurrentSiblingElements;
var previousIndex = ruleGenerationContext.CurrentElementIndex;
ruleGenerationContext.CurrentSiblingElements = elements;
Expand All @@ -110,6 +112,88 @@
ruleGenerationContext.CurrentElementIndex = previousIndex;
}

/// <summary>
/// Text-free members the pilot is KNOWN to store before the elements the production declares ahead of
/// them, verified against real pilot output.
/// <para>Deliberately an allowlist, not a structural rule. Emitting no text does NOT imply the model
/// may store the element anywhere: storage order is a per-rule implementation detail and it goes BOTH
/// ways. <c>EmptyMultiplicityMember</c> is stored FIRST though declared last, while
/// <c>EmptyResultMember</c> / <c>ReturnParameterMembership</c> is stored LAST as declared (in the
/// <c>OperatorExpression</c> family, <c>InvocationExpression</c> and <c>FeatureReferenceExpression</c>).
/// Hoisting the latter would strand the cursor on it. Only add a name here after checking real output.</para>
/// </summary>
private static readonly HashSet<string> HoistableTextFreeMembers = new(StringComparer.Ordinal)
{
"EmptyMultiplicityMember",
};

/// <summary>
/// Moves a lone <c>+=</c> element whose production emits NO text to the front of the alternative.
/// <para>Such an element has no observable position in the notation, so the grammar cannot constrain
/// where the parser puts it in the collection — and the pilot does not always put it where the
/// production does. Consuming it first keeps the cursor aligned for the elements that DO emit text;
/// leaving it in place strands the cursor on it (e.g. <c>IndividualDefinition</c> declares
/// <c>EmptyMultiplicityMember</c> last but the model stores it first, hiding the Subclassification
/// that <c>Definition</c> must read).</para>
/// <para>Only a LONE such element is hoisted: when several appear (e.g. <c>TransitionUsage</c>'s two
/// <c>EmptyParameterMember</c>s) their relative order decides which pairs with which sibling, so
/// moving them would change meaning.</para>
/// </summary>
/// <param name="elements">The alternative's elements in grammar order.</param>
/// <param name="ruleGenerationContext">The current <see cref="RuleGenerationContext" />.</param>
/// <returns>The elements, reordered when a lone text-free consumption is present.</returns>
private static List<RuleElement> HoistSingleNonNotationalConsumption(List<RuleElement> elements, RuleGenerationContext ruleGenerationContext)
{
var textFree = elements
.OfType<AssignmentElement>()
.Where(assignment => assignment.Operator == "+="
&& assignment.Value is NonTerminalElement nonTerminal
&& HoistableTextFreeMembers.Contains(nonTerminal.Name)
&& EmitsNoNotation(ruleGenerationContext.FindRule(nonTerminal.Name), ruleGenerationContext, []))
.ToList();

// Only a TRAILING text-free element is hoisted. Declared last, it has nothing after it whose
// position it could encode, so moving it is meaning-preserving; declared mid-sequence its order
// relative to the following elements is significant (TransitionUsage's EmptyParameterMember
// pairs with the TriggerActionMember that follows it).
if (textFree.Count != 1
|| !ReferenceEquals(elements[^1], textFree[0])
|| ReferenceEquals(elements[0], textFree[0]))
{
return elements;
}

var reordered = new List<RuleElement> { textFree[0] };
reordered.AddRange(elements.Where(element => !ReferenceEquals(element, textFree[0])));

return reordered;
}

/// <summary>
/// Determines whether <paramref name="rule" /> produces no textual notation at all — no terminal and
/// no value-bearing assignment, transitively. <c>EmptyMultiplicity</c>, <c>EmptyUsage</c> and their
/// wrappers are the canonical cases.
/// </summary>
/// <param name="rule">The rule to inspect; may be <see langword="null" />.</param>
/// <param name="ruleGenerationContext">The current <see cref="RuleGenerationContext" />.</param>
/// <param name="visited">Rules already inspected, guarding against recursive productions.</param>
/// <returns><see langword="true" /> when the rule emits nothing.</returns>
private static bool EmitsNoNotation(TextualNotationRule rule, RuleGenerationContext ruleGenerationContext, HashSet<string> visited)
{
if (rule == null || !visited.Add(rule.RuleName))
{
return false;
}

return rule.Alternatives.SelectMany(alternative => alternative.Elements).All(element => element switch
{
NonParsingAssignmentElement => true,
AssignmentElement { Value: NonTerminalElement nested } => EmitsNoNotation(ruleGenerationContext.FindRule(nested.Name), ruleGenerationContext, visited),
NonTerminalElement nonTerminal => EmitsNoNotation(ruleGenerationContext.FindRule(nonTerminal.Name), ruleGenerationContext, visited),
_ => false,
});
}

/// <summary>
/// Declares cursor variables for all enumerable properties referenced by assignment elements in the given alternative.
/// </summary>
Expand Down Expand Up @@ -1109,7 +1193,7 @@
writer.WriteSafeString($"{subclass.Name}TextualNotationBuilder.Build{nonTerminalElement.Name}({patternVariableName}, writerContext, stringBuilder);{Environment.NewLine}");
writer.WriteSafeString($"}}{Environment.NewLine}");

// A NonTerminal-valued assignment emits its own null guard inside ProcessAssignmentElement;

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

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code.

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

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code.
// only a value-literal assignment (e.g. [QualifiedName]) needs the guard supplied here.
if (assignmentElement.Value is ValueLiteralElement)
{
Expand Down Expand Up @@ -1374,7 +1458,7 @@

foreach (var element in secondAlt.Elements)
{
if (element is NonTerminalElement { IsCollection: true })

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

View workflow job for this annotation

GitHub Actions / Build

Remove this redundant cast.
{
var cursorVarName = $"{targetProperty.Name.LowerCaseFirstLetter()}Cursor";
writer.WriteSafeString($"var {cursorVarName} = writerContext.CursorCache.GetOrCreateCursor(poco.Id, \"{targetProperty.Name}\", poco.{propertyAccessName});{Environment.NewLine}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package '6-Individual and Snapshots' {
private import ScalarValues::Real;
private import Time::DateTime;
private import ISQ::*;
package 'Part Definitions' {
part def 'Temporal-Spatial Reference' {
attribute referenceTime: DateTime;
attribute referenceCoordinateSystem;
}
/*
* Note that space and time coordinatization have not
* been fully specified yet.
*/
part def VehicleRoadContext {
attribute t: DurationValue;
}
part def VehicleA {
attribute mass: MassValue;
attribute position: Real;
attribute velocity: Real;
attribute acceleration: Real;
exhibit state vehicleStates {
entry;
then on;
state on;
then off;
state off;
}
}
part def Road {
attribute angle: Real;
attribute surfaceFriction: Real;
}
}
package 'Individual Definitions' {
private import 'Part Definitions'::*;
/*
* An individual definition restricts the instances of a part def to
* those that are portions of the same life ("identity").
*/
individual def 'Temporal-Spatial Reference_ID1' :> 'Temporal-Spatial Reference';
individual def VehicleRoadContext_ID1 :> VehicleRoadContext;
individual def VehicleA_ID1 :> VehicleA;
individual def Road_ID1 :> Road;
}
package Values {
attribute t0: DurationValue;
attribute t1: DurationValue;
attribute tn: DurationValue;
attribute m: MassValue;
attribute p0: Real;
attribute p1: Real;
attribute pn: Real;
attribute v0: Real;
attribute v1: Real;
attribute vn: Real;
attribute a0: Real;
attribute a1: Real;
attribute an: Real;
attribute theta0: Real;
attribute theta1: Real;
attribute thetan: Real;
attribute sf0: Real;
attribute sf1: Real;
attribute sfn: Real;
}
package 'Individuals and Snapshots' {
private import 'Individual Definitions'::*;
private import Values::*;
individual reference: 'Temporal-Spatial Reference_ID1' {
/*
* An individual usage must be typed by an individual definition,
* representing the condition of that individual during some or all
* of its life.
*/
snapshot context_t0: VehicleRoadContext_ID1 {
:>> t = t0 {
/*
* This is a concise notation for showing the redefinition
* of a attribute property.
*/
}
snapshot vehicle_ID1_t0: VehicleA_ID1 {
/*
* A snapshot is a kind of individual usage restricted to
* a single instant of time.
*/
:>> mass = m;
:>> position = p0;
:>> velocity = v0;
:>> acceleration = a0;
exhibit vehicleStates.on {
/*
* This asserts that the snapshot exhibits the referenced
* state, which means that the vehicle must me in the state
* at the time of the snapshot.
*/
}
}
snapshot road_ID1_t0: Road_ID1 {
:>> angle = theta0;
:>> surfaceFriction = sf0;
}
}
snapshot context_t1: VehicleRoadContext_ID1 {
:>> t = t1;
snapshot vehicle_ID1_t1: VehicleA_ID1 {
:>> mass = m;
:>> position = p1;
:>> velocity = v1;
:>> acceleration = a1;
exhibit vehicleStates.on;
}
snapshot road_ID1_t1: Road_ID1 {
:>> angle = theta1;
:>> surfaceFriction = sf1;
}
}
snapshot context_tn: VehicleRoadContext_ID1 {
:>> t = tn;
snapshot vehicle_ID1_tn: VehicleA_ID1 {
:>> mass = m;
:>> position = pn;
:>> velocity = vn;
:>> acceleration = an;
exhibit vehicleStates.off;
}
snapshot road_ID1_tn: Road_ID1 {
:>> angle = theta1;
:>> surfaceFriction = sfn;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class TextualNotationValidationTestFixture
[TestCase("05-State-based Behavior", "5-State-based Behavior-1a.sysmlx")]
[TestCase("05-State-based Behavior", "5-State-based Behavior-1.sysmlx")]
[TestCase("05-State-based Behavior", "5-State-based Behavior-2.sysmlx")]
[TestCase("06-Individual and Snapshots", "6-Individual and Snapshots.sysmlx")]
public async Task VerifyValidationTextualNotationXmi(string folderName, string fileName)
{
var loggerFactory = LoggerFactory.Create(builder =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public static void BuildExtendedDefinition(SysML2.NET.Core.POCO.Systems.Definiti
SharedTextualNotationBuilder.BuildBasicDefinitionPrefix(poco, writerContext, stringBuilder);
}
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType<SysML2.NET.Core.POCO.Systems.Metadata.IMetadataUsage>().Any())
{
BuildDefinitionExtensionKeyword(poco, writerContext, stringBuilder);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public static void BuildEnumerationBody(SysML2.NET.Core.POCO.Systems.Enumeration
public static void BuildEnumerationDefinition(SysML2.NET.Core.POCO.Systems.Enumerations.IEnumerationDefinition poco, TextualNotationWriterContext writerContext, IndentedStringBuilder stringBuilder)
{
var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType<SysML2.NET.Core.POCO.Systems.Metadata.IMetadataUsage>().Any())
{
DefinitionTextualNotationBuilder.BuildDefinitionExtensionKeyword(poco, writerContext, stringBuilder);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static void BuildMetadataDefinition(SysML2.NET.Core.POCO.Systems.Metadata
}

var ownedRelationshipCursor = writerContext.CursorCache.GetOrCreateCursor(poco.Id, "ownedRelationship", poco.OwnedRelationship);
while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership)
while (ownedRelationshipCursor.Current is SysML2.NET.Core.POCO.Root.Namespaces.IOwningMembership owningMembershipGuard && owningMembershipGuard.OwnedRelatedElement.OfType<SysML2.NET.Core.POCO.Systems.Metadata.IMetadataUsage>().Any())
{
DefinitionTextualNotationBuilder.BuildDefinitionExtensionKeyword(poco, writerContext, stringBuilder);
}
Expand Down
Loading
Loading