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
112 changes: 112 additions & 0 deletions SysML2.NET.CodeGenerator/Extensions/GrammarErrata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="GrammarErrata.cs" company="Starion Group S.A.">
//
// 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.
//
// </copyright>
// ------------------------------------------------------------------------------------------------

namespace SysML2.NET.CodeGenerator.Extensions
{
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Supplies the target metaclass for KEBNF rules whose name does not match the metaclass they build,
/// at generation time.
/// </summary>
/// <remarks>
/// The KEBNF files under <c>Resources/</c> are OMG source and are never edited, and the generated
/// output is never hand-edited either — so a rule that omits a target the generator cannot infer can
/// only be corrected here, on the way from the one to the other. The files reproduce the
/// textual-notation BNF of the KerML and SysML specifications verbatim, so a defect here is a
/// SPECIFICATION defect; OMG has confirmed this class of finding and routes the fix through the
/// Revision Task Forces (Systems-Modeling/SysML-v2-Release issue 124).
/// <para>The grammar writes an explicit target whenever the rule name differs from the metaclass
/// (<c>RequirementKind : RequirementConstraintMembership</c>, <c>SubjectMember : SubjectMembership</c>).
/// Every entry below is a rule where that annotation is missing, so the rule name resolves to no
/// metaclass at all and the generator falls back to inferring one from the assigned property names —
/// which silently selects an unrelated class that happens to declare the same property.</para>
/// <para>Scope is deliberately narrow: an entry corrects a rule the generator would otherwise bind to
/// the WRONG metaclass. A production that merely admits more than one valid spelling is NOT an
/// erratum — choosing between admissible spellings is the writer's business, not a correction to the
/// grammar.</para>
/// <para>These corrections are expected to become unnecessary as OMG publishes fixes. On a new KEBNF
/// release, run the generator and prune whatever <see cref="QueryUnappliedErrata" /> reports — an entry
/// that no longer matches has been fixed upstream.</para>
/// </remarks>
public static class GrammarErrata
{
/// <summary>
/// The targets supplied to rules that omit them, keyed by the exact rule name.
/// </summary>
private static readonly GrammarErratum[] Entries =
[
new("LiteralReal", "LiteralRational",
"KerML 8.2.2.24 writes 'LiteralReal = value = RealValue' with no target, but no metaclass named 'LiteralReal' exists — KerML 8.3.4.9 names it 'LiteralRational'. Its sibling literal rules (LiteralBoolean, LiteralString, LiteralInteger, LiteralInfinity) all match a metaclass by name, so only this one is left unresolved.")
];

/// <summary>
/// The corrections that have matched at least one rule during this generator run.
/// </summary>
private static readonly HashSet<string> AppliedRuleNames = [];

/// <summary>
/// Supplies the target metaclass for a rule when the grammar omits one and an erratum covers it.
/// </summary>
/// <param name="ruleName">The rule name read from the grammar.</param>
/// <param name="targetElementName">The target the grammar declares, which may be null.</param>
/// <returns>
/// The corrected target, or <paramref name="targetElementName" /> unchanged when nothing applies.
/// </returns>
/// <remarks>
/// A target the grammar states itself always wins: an erratum only fills a gap, so a rule that OMG
/// later annotates upstream stops being corrected here and surfaces via
/// <see cref="QueryUnappliedErrata" />.
/// </remarks>
public static string ApplyTarget(string ruleName, string targetElementName)
{
if (!string.IsNullOrWhiteSpace(targetElementName) || string.IsNullOrWhiteSpace(ruleName))
{
return targetElementName;
}

var erratum = Entries.SingleOrDefault(entry => string.Equals(entry.RuleName, ruleName, StringComparison.Ordinal));

if (erratum == null)
{
return targetElementName;
}

AppliedRuleNames.Add(erratum.RuleName);

return erratum.TargetElementName;
}

/// <summary>
/// Returns the corrections that matched no rule during this generator run.
/// </summary>
/// <returns>The stale entries, which should be pruned from <see cref="Entries" />.</returns>
/// <remarks>
/// Only meaningful once every rule has been read. A stale entry means the grammar no longer carries
/// the defect — either OMG annotated the rule, or the rule was renamed or removed.
/// </remarks>
public static IReadOnlyList<GrammarErratum> QueryUnappliedErrata()
{
return [..Entries.Where(erratum => !AppliedRuleNames.Contains(erratum.RuleName))];
}
}
}
74 changes: 74 additions & 0 deletions SysML2.NET.CodeGenerator/Extensions/GrammarErratum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="GrammarErratum.cs" company="Starion Group S.A.">
//
// 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.
//
// </copyright>
// ------------------------------------------------------------------------------------------------

namespace SysML2.NET.CodeGenerator.Extensions
{
using System;

/// <summary>
/// A single correction applied to a rule carried by the KEBNF grammar.
/// </summary>
public sealed class GrammarErratum
{
/// <summary>
/// Initializes a new instance of the <see cref="GrammarErratum" /> class.
/// </summary>
/// <param name="ruleName">The exact rule name the grammar carries.</param>
/// <param name="targetElementName">The metaclass the rule targets, which the grammar omits.</param>
/// <param name="justification">The evidence that the omission is a defect rather than intent.</param>
/// <exception cref="ArgumentException">Thrown when any argument is null or whitespace.</exception>
public GrammarErratum(string ruleName, string targetElementName, string justification)
{
if (string.IsNullOrWhiteSpace(ruleName))
{
throw new ArgumentException("The rule name is required.", nameof(ruleName));
}

if (string.IsNullOrWhiteSpace(targetElementName))
{
throw new ArgumentException("The target element name is required.", nameof(targetElementName));
}

if (string.IsNullOrWhiteSpace(justification))
{
throw new ArgumentException("A justification is required so the correction can be audited.", nameof(justification));
}

this.RuleName = ruleName;
this.TargetElementName = targetElementName;
this.Justification = justification;
}

/// <summary>
/// Gets the exact rule name the grammar carries.
/// </summary>
public string RuleName { get; }

/// <summary>
/// Gets the metaclass the rule targets.
/// </summary>
public string TargetElementName { get; }

/// <summary>
/// Gets the evidence that the omission is a defect rather than intent.
/// </summary>
public string Justification { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,17 @@
/// </returns>
public async Task GenerateAsync(XmiReaderResult xmiReaderResult, TextualNotationSpecification textualNotationSpecification, DirectoryInfo outputDirectory)
{
// Every rule has now been read, so a correction that matched nothing is stale: the grammar no
// longer carries the defect it repairs. Reported rather than thrown, since a fix upstream must
// not break generation.
foreach (var stale in GrammarErrata.QueryUnappliedErrata())
{
Console.WriteLine($"[GrammarErrata] STALE — {stale.RuleName} matched no grammar rule and should be pruned. Recorded reason: {stale.Justification}");
}

await this.GenerateBuilderClasses(xmiReaderResult, textualNotationSpecification, outputDirectory);
await this.GenerateSharedBuilder(xmiReaderResult, textualNotationSpecification, outputDirectory);
// await this.GenerateBuilderFacade(xmiReaderResult, outputDirectory);

Check warning on line 125 in SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCoreTextualNotationBuilderGenerator.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code.

Check warning on line 125 in SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCoreTextualNotationBuilderGenerator.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code.
}

/// <summary>
Expand Down Expand Up @@ -280,7 +288,7 @@
/// <param name="outputDirectory">The target <see cref="DirectoryInfo"/></param>
/// <exception cref="ArgumentNullException">If one of the given parameters is null</exception>
/// <returns>an awaitable <see cref="Task"/></returns>
private Task GenerateBuilderFacade(XmiReaderResult xmiReaderResult, DirectoryInfo outputDirectory)

Check warning on line 291 in SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCoreTextualNotationBuilderGenerator.cs

View workflow job for this annotation

GitHub Actions / Build

Remove the unused private method 'GenerateBuilderFacade'.
{
ArgumentNullException.ThrowIfNull(xmiReaderResult);
ArgumentNullException.ThrowIfNull(outputDirectory);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace SysML2.NET.CodeGenerator.Grammar
using System.Collections.Generic;
using System.Linq;

using SysML2.NET.CodeGenerator.Extensions;
using SysML2.NET.CodeGenerator.Grammar.Model;

/// <summary>
Expand Down Expand Up @@ -53,7 +54,11 @@ public override object VisitRule_definition(kebnfParser.Rule_definitionContext c
var rule = new TextualNotationRule()
{
RuleName = context.name.Text,
TargetElementName = context.target_ast?.Text,

// A rule whose name differs from the metaclass it builds normally states the target
// itself; where the grammar omits it, GrammarErrata supplies it, so every consumer of
// EffectiveTarget resolves the same metaclass the annotation would have named.
TargetElementName = GrammarErrata.ApplyTarget(context.name.Text, context.target_ast?.Text),
RawRule = context.GetText().Trim()
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@
}
else
{
var handCodedRuleName = groupElement.TextualNotationRule?.RuleName ?? "Unknown";

Check warning on line 232 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 @@ -263,7 +263,7 @@

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

Check warning on line 266 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 Down Expand Up @@ -534,6 +534,12 @@
break;
}
}
else if (string.Equals(targetProperty.Type?.Name, "Real", StringComparison.Ordinal))
{
// A Real maps to double, whose default ToString() is culture-sensitive and drops
// the '.' that every RealValue alternative requires — see AppendRealValue.
writer.WriteSafeString($"SharedTextualNotationBuilder.AppendRealValue(stringBuilder, poco.{targetPropertyName});");
}
else
{
writer.WriteSafeString($"stringBuilder.Append(poco.{targetPropertyName}.ToString());");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package '8-Requirements' {
private import ScalarValues::Real;
private import ISQ::*;
private import SI::*;
public import 'Vehicle Usages'::*;
public import 'Vehicle Requirements'::*;
package 'Vehicle Definitions' {
part def Vehicle {
attribute mass: MassValue;
attribute fuelLevel: Real;
attribute fuelTankCapacity: Real;
}
part def Engine {
port drivePwrPort: DrivePwrPort;
perform action 'generate torque': 'Generate Torque';
}
part def Transmission {
port clutchPort: ClutchPort;
}
port def DrivePwrPort;
port def ClutchPort;
interface def EngineToTransmissionInterface {
end drivePwrPort: DrivePwrPort;
end clutchPort: ClutchPort;
}
action def 'Generate Torque';
}
package 'Vehicle Usages' {
public import 'Vehicle Definitions'::*;
action 'provide power' {
action 'generate torque' {
/* ... */
}
}
part vehicle1_c1: Vehicle {
attribute :>> mass = 2000[kg];
perform 'provide power';
part engine_v1: Engine {
port :>> drivePwrPort;
perform 'provide power'.'generate torque' :>> 'generate torque';
}
part transmission: Transmission {
port :>> clutchPort;
}
interface engineToTransmission: EngineToTransmissionInterface connect engine_v1.drivePwrPort to transmission.clutchPort;
}
part vehicle1_c2: Vehicle {
attribute :>> mass = 2500[kg];
}
}
package 'Vehicle Requirements' {
public import 'Vehicle Definitions'::*;
requirement def <'1'> MassLimitationRequirement {
/*
* The optional requirement ID of this requirement ('1') is given after the keyword "id" (using name syntax).
* Every requirement is parameterized by a "subject". The "subject" of this requirement is implicitly "Anything".
*/
doc
/* The actual mass shall be less than or equal to the required mass. */
attribute massActual: MassValue;
attribute massReqd: MassValue;
require constraint { /*
* A constraint can be used to formalize a requirement.
*/
massActual <= massReqd }
}
requirement def <'2'> ReliabilityRequirement;
requirement <'1.1'> vehicleMass1: '1' {
doc
/* The vehicle mass shall be less than or equal to 2000 kg when the fuel tank is full. */
subject vehicle: Vehicle {
/*
* The subject of this requirement is redefined to be a "Vehicle".
*/
}
attribute :>> massActual : MassValue = vehicle.mass {
/*
* This redefinition binds the vehicle mass to the actual mass.
*/
}
attribute :>> massReqd = 2000[kg] {
/*
* This redefinition sets the required mass to 2000 kg.
*/
}
assume constraint fuelConstraint { /*
* A constraint can also be used to specify an assumption.
*/
doc /* full fuel tank */ vehicle.fuelLevel >= vehicle.fuelTankCapacity }
}
requirement <'2.1'> vehicleMass2: '1' {
doc
/* The vehicle mass shall be less than or equal to 2500 kg when the fuel tank is empty. */
subject vehicle: Vehicle;
attribute :>> massActual : MassValue = vehicle.mass;
attribute :>> massReqd = 2500[kg];
assume constraint fuelConstraint { doc /* empty fuel tank */ vehicle.fuelLevel == 0.0}
}
requirement <'2.2'> vehicleReliability2: '2' {
subject vehicle: Vehicle;
}
requirement <'3.1'> drivePowerInterface {
doc
/* The engine shall transfer its generated torque to the transmission via the clutch interface. */
subject drivePwrPort: DrivePwrPort;
}
requirement <'3.2'> torqueGeneration {
doc
/* The engine shall generate torque as a function of RPM as shown in Table 1. */
subject generateTorque: 'Generate Torque';
}
}
part 'vehicle1_c1 Specification Context' {
private import 'vehicle1-c1 Specification'::*;
private import 'engine-v1 Specification'::*;
requirement 'vehicle1-c1 Specification' {
doc
/*
* This models a "requirement group" as a requirement that references other requirements.
*/

subject vehicle: Vehicle;
requirement ::> '1.1' {
/*
* This is a reference to a requirement defined outside the group.
* By default, the subject of the requirement is bound to that of the group.
*/
}
}
requirement 'engine-v1 Specification' {
subject engine: Engine;
/*
* Here the subjects of the referenced requirements are defined to be specific properties of the
* subject of the group.
*/
require '3.2' {
in :>> generateTorque = engine.'generate torque';
}
require '3.1' {
in :>> drivePwrPort = engine.drivePwrPort;
}
}
assert satisfy 'vehicle1-c1 Specification' by vehicle1_c1 {
/*
* This asserts that if the assumptions of 'vehicle1-c1 Specification' are true with 'vehicle_c1' as
* the subject, then the required constraints are also true.
*/
}
assert satisfy 'engine-v1 Specification' by vehicle1_c1.engine_v1;
}
part 'vehicle1_c2 Specification Context' {
private import 'vehicle1-c2 Specification'::*;
requirement 'vehicle1-c2 Specification' {
subject vehicle: Vehicle;
require '2.1';
require '2.2';
}
assert satisfy 'vehicle1-c2 Specification' by vehicle1_c2;
}
}
Loading
Loading