Skip to content

Commit 05baf72

Browse files
Fix #296
1 parent 1d0f29d commit 05baf72

15 files changed

Lines changed: 625 additions & 40 deletions

File tree

SysML2.NET.CodeGenerator/Extensions/GrammarErrata.cs

Lines changed: 97 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,30 @@ namespace SysML2.NET.CodeGenerator.Extensions
2525
using System.Linq;
2626

2727
/// <summary>
28-
/// Supplies the target metaclass for KEBNF rules whose name does not match the metaclass they build,
29-
/// at generation time.
28+
/// Corrects known defects in the KEBNF grammar carried under <c>Resources/</c>, at generation time.
3029
/// </summary>
3130
/// <remarks>
32-
/// The KEBNF files under <c>Resources/</c> are OMG source and are never edited, and the generated
33-
/// output is never hand-edited either — so a rule that omits a target the generator cannot infer can
34-
/// only be corrected here, on the way from the one to the other. The files reproduce the
35-
/// textual-notation BNF of the KerML and SysML specifications verbatim, so a defect here is a
36-
/// SPECIFICATION defect; OMG has confirmed this class of finding and routes the fix through the
37-
/// Revision Task Forces (Systems-Modeling/SysML-v2-Release issue 124).
38-
/// <para>The grammar writes an explicit target whenever the rule name differs from the metaclass
39-
/// (<c>RequirementKind : RequirementConstraintMembership</c>, <c>SubjectMember : SubjectMembership</c>).
40-
/// Every entry below is a rule where that annotation is missing, so the rule name resolves to no
41-
/// metaclass at all and the generator falls back to inferring one from the assigned property names —
42-
/// which silently selects an unrelated class that happens to declare the same property.</para>
43-
/// <para>Scope is deliberately narrow: an entry corrects a rule the generator would otherwise bind to
44-
/// the WRONG metaclass. A production that merely admits more than one valid spelling is NOT an
45-
/// erratum — choosing between admissible spellings is the writer's business, not a correction to the
46-
/// grammar.</para>
31+
/// The KEBNF files are OMG source and are never edited, and the generated output is never hand-edited
32+
/// either — so a defect can only be corrected here, on the way from the one to the other. The files are
33+
/// mechanically extracted from the specification document, while the pilot implementation's parser is a
34+
/// separately hand-maintained Xtext grammar; the two drift, and a defect here is a SPECIFICATION defect.
35+
/// OMG has confirmed this class of finding and routes the fix through the Revision Task Forces
36+
/// (Systems-Modeling/SysML-v2-Release issue 124).
37+
/// <para>Two kinds of correction, because the defects differ in kind:</para>
38+
/// <para><see cref="Entries" /> — a rule whose name does not match the metaclass it builds and which
39+
/// omits the explicit target the grammar normally writes in that case
40+
/// (<c>RequirementKind : RequirementConstraintMembership</c>). Without it the rule name resolves to no
41+
/// metaclass, and the generator falls back to inferring one from the assigned property names, silently
42+
/// selecting an unrelated class that happens to declare the same property.</para>
43+
/// <para><see cref="ProductionEntries" /> — a production whose token sequence cannot derive notation the
44+
/// metamodel and the specification's own normative examples require. Applied to the grammar TEXT before
45+
/// it is parsed, so the corrected production flows through the normal pipeline and nothing downstream
46+
/// needs to special-case the rule.</para>
47+
/// <para>Scope is deliberately narrow, and the bar for both kinds is the same: the generator would
48+
/// otherwise produce output that is WRONG, not merely different. A production that admits more than one
49+
/// valid spelling is NOT an erratum — choosing between admissible spellings is the writer's business.
50+
/// The reference test is whether the pilot's Xtext grammar accepts what we emit: where it does, any
51+
/// difference is a style choice; where it cannot, the grammar is genuinely deficient.</para>
4752
/// <para>These corrections are expected to become unnecessary as OMG publishes fixes. On a new KEBNF
4853
/// release, run the generator and prune whatever <see cref="QueryUnappliedErrata" /> reports — an entry
4954
/// that no longer matches has been fixed upstream.</para>
@@ -59,6 +64,34 @@ public static class GrammarErrata
5964
"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.")
6065
];
6166

67+
/// <summary>
68+
/// The production-text corrections applied to the grammar before it is parsed.
69+
/// </summary>
70+
/// <remarks>
71+
/// An entry belongs here only when the grammar cannot derive the notation at all. A production that
72+
/// merely admits a spelling we do not emit is NOT an erratum — see the class remarks.
73+
/// </remarks>
74+
private static readonly GrammarProductionErratum[] ProductionEntries =
75+
[
76+
new("CaseBodyItem",
77+
"CaseBodyItem : Type =\r\n ActionBodyItem",
78+
"CaseBodyItem : Type =\r\n CalculationBodyItem",
79+
"SysML 8.2.2.22.1 gives CaseBodyItem the alternative 'ActionBodyItem', which reaches no " +
80+
"ReturnParameterMember, so 'return' cannot be written in a case body. Three independent " +
81+
"sources say it must be: (1) the pilot implementation's own grammar uses " +
82+
"'CalculationBodyItem' here (org.omg.sysml.xtext SysML.xtext, rule CaseBodyItem), and " +
83+
"CalculationBodyItem = ActionBodyItem | ReturnParameterMember; (2) the metamodel permits it " +
84+
"— constraint validateReturnParameterMembershipOwningType requires the owningType of a " +
85+
"ReturnParameterMembership to be a Function or Expression, and VerificationCaseUsage " +
86+
"specializes CaseUsage specializes CalculationUsage specializes Expression; (3) the " +
87+
"normative example in SysML 7.24.2 writes 'return verdict : VerdictKind = " +
88+
"evaluateData.verdict;' inside a 'verification def' body. There is no admissible " +
89+
"alternative spelling: rendering the ReturnParameterMembership through the generic " +
90+
"parameter path emits 'out verdict', which re-parses as a plain FeatureMembership with " +
91+
"direction out and so loses the metaclass. CalculationBodyItem is already declared in the " +
92+
"same file, so the replacement resolves without any further correction.")
93+
];
94+
6295
/// <summary>
6396
/// The corrections that have matched at least one rule during this generator run.
6497
/// </summary>
@@ -97,16 +130,57 @@ public static string ApplyTarget(string ruleName, string targetElementName)
97130
}
98131

99132
/// <summary>
100-
/// Returns the corrections that matched no rule during this generator run.
133+
/// Applies every known production correction to the raw text of a KEBNF file.
134+
/// </summary>
135+
/// <param name="kebnfSource">The grammar text as read from disk.</param>
136+
/// <returns>
137+
/// The corrected grammar text, or <paramref name="kebnfSource" /> unchanged when nothing applies.
138+
/// </returns>
139+
/// <remarks>
140+
/// Correcting the text rather than the parsed rule keeps the correction in the grammar's own
141+
/// language: the entry reads as the production OMG should have written, and every consumer parses
142+
/// it exactly as it parses the rest of the file. Each <c>Original</c> is matched verbatim, so a
143+
/// correction cannot partially match, and re-applying it to already-corrected text is a no-op.
144+
/// Both KEBNF files are passed through this, so an entry only fires against the file that carries
145+
/// its production.
146+
/// </remarks>
147+
public static string ApplyProductions(string kebnfSource)
148+
{
149+
if (string.IsNullOrWhiteSpace(kebnfSource))
150+
{
151+
return kebnfSource;
152+
}
153+
154+
return ProductionEntries
155+
.Where(erratum => kebnfSource.Contains(erratum.Original, StringComparison.Ordinal))
156+
.Aggregate(kebnfSource, (corrected, erratum) =>
157+
{
158+
AppliedRuleNames.Add(erratum.RuleName);
159+
160+
return corrected.Replace(erratum.Original, erratum.Replacement);
161+
});
162+
}
163+
164+
/// <summary>
165+
/// Returns the corrections that matched nothing during this generator run.
101166
/// </summary>
102-
/// <returns>The stale entries, which should be pruned from <see cref="Entries" />.</returns>
167+
/// <returns>The stale entries, which should be pruned.</returns>
103168
/// <remarks>
104-
/// Only meaningful once every rule has been read. A stale entry means the grammar no longer carries
105-
/// the defect — either OMG annotated the rule, or the rule was renamed or removed.
169+
/// Only meaningful once every grammar file has been loaded and every rule read. A stale entry means
170+
/// the grammar no longer carries the defect — either OMG corrected it, or the rule was renamed or
171+
/// removed.
106172
/// </remarks>
107-
public static IReadOnlyList<GrammarErratum> QueryUnappliedErrata()
173+
public static IReadOnlyList<(string RuleName, string Justification)> QueryUnappliedErrata()
108174
{
109-
return [..Entries.Where(erratum => !AppliedRuleNames.Contains(erratum.RuleName))];
175+
return
176+
[
177+
..Entries
178+
.Where(erratum => !AppliedRuleNames.Contains(erratum.RuleName))
179+
.Select(erratum => (erratum.RuleName, erratum.Justification)),
180+
..ProductionEntries
181+
.Where(erratum => !AppliedRuleNames.Contains(erratum.RuleName))
182+
.Select(erratum => (erratum.RuleName, erratum.Justification))
183+
];
110184
}
111185
}
112186
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// -------------------------------------------------------------------------------------------------
2+
// <copyright file="GrammarProductionErratum.cs" company="Starion Group S.A.">
3+
//
4+
// Copyright 2022-2026 Starion Group S.A.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// </copyright>
19+
// ------------------------------------------------------------------------------------------------
20+
21+
namespace SysML2.NET.CodeGenerator.Extensions
22+
{
23+
using System;
24+
25+
/// <summary>
26+
/// A single correction applied to the text of a KEBNF production before it is parsed.
27+
/// </summary>
28+
public sealed class GrammarProductionErratum
29+
{
30+
/// <summary>
31+
/// Initializes a new instance of the <see cref="GrammarProductionErratum" /> class.
32+
/// </summary>
33+
/// <param name="ruleName">The rule the correction belongs to, used when reporting staleness.</param>
34+
/// <param name="original">The exact production text the grammar carries.</param>
35+
/// <param name="replacement">The text it is corrected to.</param>
36+
/// <param name="justification">The evidence that the original is a defect rather than intent.</param>
37+
/// <exception cref="ArgumentException">Thrown when any argument is null or whitespace.</exception>
38+
public GrammarProductionErratum(string ruleName, string original, string replacement, string justification)
39+
{
40+
if (string.IsNullOrWhiteSpace(ruleName))
41+
{
42+
throw new ArgumentException("The rule name is required.", nameof(ruleName));
43+
}
44+
45+
if (string.IsNullOrWhiteSpace(original))
46+
{
47+
throw new ArgumentException("The original production text is required.", nameof(original));
48+
}
49+
50+
if (string.IsNullOrWhiteSpace(replacement))
51+
{
52+
throw new ArgumentException("The replacement production text is required.", nameof(replacement));
53+
}
54+
55+
if (string.IsNullOrWhiteSpace(justification))
56+
{
57+
throw new ArgumentException("A justification is required so the correction can be audited.", nameof(justification));
58+
}
59+
60+
this.RuleName = ruleName;
61+
this.Original = original;
62+
this.Replacement = replacement;
63+
this.Justification = justification;
64+
}
65+
66+
/// <summary>
67+
/// Gets the rule the correction belongs to.
68+
/// </summary>
69+
public string RuleName { get; }
70+
71+
/// <summary>
72+
/// Gets the exact production text the grammar carries.
73+
/// </summary>
74+
public string Original { get; }
75+
76+
/// <summary>
77+
/// Gets the text it is corrected to.
78+
/// </summary>
79+
public string Replacement { get; }
80+
81+
/// <summary>
82+
/// Gets the evidence that the original is a defect rather than intent.
83+
/// </summary>
84+
public string Justification { get; }
85+
}
86+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// -------------------------------------------------------------------------------------------------
2+
// <copyright file="NotationInvariant.cs" company="Starion Group S.A.">
3+
//
4+
// Copyright 2022-2026 Starion Group S.A.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// </copyright>
19+
// ------------------------------------------------------------------------------------------------
20+
21+
namespace SysML2.NET.CodeGenerator.Extensions
22+
{
23+
using System;
24+
25+
/// <summary>
26+
/// A single notation invariant: a rule the writer must honour that neither the KEBNF nor the metamodel
27+
/// states machine-readably, anchored to the OMG name it depends on.
28+
/// </summary>
29+
public sealed class NotationInvariant
30+
{
31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="NotationInvariant" /> class.
33+
/// </summary>
34+
/// <param name="name">The stable key the generator refers to the invariant by.</param>
35+
/// <param name="metamodelName">The OMG metaclass or property name the invariant depends on.</param>
36+
/// <param name="justification">Why the invariant holds, and what breaks without it.</param>
37+
/// <exception cref="ArgumentException">Thrown when any argument is null or whitespace.</exception>
38+
public NotationInvariant(string name, string metamodelName, string justification)
39+
{
40+
if (string.IsNullOrWhiteSpace(name))
41+
{
42+
throw new ArgumentException("The invariant name is required.", nameof(name));
43+
}
44+
45+
if (string.IsNullOrWhiteSpace(metamodelName))
46+
{
47+
throw new ArgumentException("The metamodel name is required.", nameof(metamodelName));
48+
}
49+
50+
if (string.IsNullOrWhiteSpace(justification))
51+
{
52+
throw new ArgumentException("A justification is required so the invariant can be audited.", nameof(justification));
53+
}
54+
55+
this.Name = name;
56+
this.MetamodelName = metamodelName;
57+
this.Justification = justification;
58+
}
59+
60+
/// <summary>
61+
/// Gets the stable key the generator refers to the invariant by.
62+
/// </summary>
63+
/// <remarks>
64+
/// Deliberately independent of <see cref="MetamodelName" />: the generator names the CONCEPT, so an
65+
/// OMG rename is a single edit here rather than a hunt through the emission code.
66+
/// </remarks>
67+
public string Name { get; }
68+
69+
/// <summary>
70+
/// Gets the OMG metaclass or property name the invariant depends on.
71+
/// </summary>
72+
public string MetamodelName { get; }
73+
74+
/// <summary>
75+
/// Gets the reason the invariant holds, and what breaks without it.
76+
/// </summary>
77+
public string Justification { get; }
78+
}
79+
}

0 commit comments

Comments
 (0)