From 1c48c2dcbb30d31f7893749d58abb996cf1c4ca3 Mon Sep 17 00:00:00 2001 From: atheate Date: Mon, 31 Aug 2026 16:40:48 +0200 Subject: [PATCH 1/2] Fix #364 --- ...lEvaluableFunctionsGeneratorTestFixture.cs | 118 ++++++++ ...reModelLevelEvaluableFunctionsGenerator.cs | 285 ++++++++++++++++++ .../Library/KernelFunctionLibraryReader.cs | 118 ++++++++ .../SysML2.NET.CodeGenerator.csproj | 6 + ...del-level-evaluable-functions-template.hbs | 71 +++++ .../Extend/FunctionExtensionsTestFixture.cs | 55 +++- ...vocationExpressionExtensionsTestFixture.cs | 26 +- SysML2.NET/Extend/FunctionExtensions.cs | 18 +- .../ModelLevelEvaluableFunctions.cs | 104 +++++++ 9 files changed, 783 insertions(+), 18 deletions(-) create mode 100644 SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCoreModelLevelEvaluableFunctionsGeneratorTestFixture.cs create mode 100644 SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCoreModelLevelEvaluableFunctionsGenerator.cs create mode 100644 SysML2.NET.CodeGenerator/Library/KernelFunctionLibraryReader.cs create mode 100644 SysML2.NET.CodeGenerator/Templates/Uml/core-model-level-evaluable-functions-template.hbs create mode 100644 SysML2.NET/KernelFunctions/AutoGenKernelFunctions/ModelLevelEvaluableFunctions.cs diff --git a/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCoreModelLevelEvaluableFunctionsGeneratorTestFixture.cs b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCoreModelLevelEvaluableFunctionsGeneratorTestFixture.cs new file mode 100644 index 000000000..aa2a05613 --- /dev/null +++ b/SysML2.NET.CodeGenerator.Tests/Generators/UmlHandleBarsGenerators/UmlCoreModelLevelEvaluableFunctionsGeneratorTestFixture.cs @@ -0,0 +1,118 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// 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.CodeGenerator.Tests.Generators.UmlHandleBarsGenerators +{ + using System; + using System.IO; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + + using NUnit.Framework; + + using SysML2.NET.CodeGenerator.Generators.UmlHandleBarsGenerators; + using SysML2.NET.CodeGenerator.Grammar; + using SysML2.NET.CodeGenerator.Grammar.Model; + + [TestFixture] + public partial class UmlCoreModelLevelEvaluableFunctionsGeneratorTestFixture + { + private DirectoryInfo outputDirectoryInfo; + private UmlCoreModelLevelEvaluableFunctionsGenerator generator; + private TextualNotationSpecification textualNotationSpecification; + private TextualNotationSpecification driftedTextualNotationSpecification; + private string kernelFunctionLibraryPath; + + [OneTimeSetUp] + public void OneTimeSetup() + { + var directoryInfo = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); + + var path = Path.Combine("UML", "_SysML2.NET.Core.UmlCoreModelLevelEvaluableFunctionsGenerator"); + + this.outputDirectoryInfo = directoryInfo.CreateSubdirectory(path); + this.generator = new UmlCoreModelLevelEvaluableFunctionsGenerator(); + + var dataModelFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, "datamodel"); + + // The operator token rules (BinaryOperator, UnaryOperator, ClassificationTestOperator, …) are + // declared by the KerML grammar only — the SysML grammar does not restate them. + this.textualNotationSpecification = GrammarLoader.LoadTextualNotationSpecification(Path.Combine(dataModelFolder, "KerML-textual-bnf.kebnf")); + + this.kernelFunctionLibraryPath = Path.Combine(dataModelFolder, "Kernel Function Library"); + + // The SysML grammar restates none of the operator token rules, so it stands in for a drifted + // grammar: only the four operators defaulted by the UML model survive collection. + this.driftedTextualNotationSpecification = GrammarLoader.LoadTextualNotationSpecification(Path.Combine(dataModelFolder, "SysML-textual-bnf.kebnf")); + } + + [Test] + public async Task VerifyGenerateAsync() + { + Assert.That(() => this.generator.GenerateAsync(GeneratorSetupFixture.XmiReaderResult, this.outputDirectoryInfo), Throws.TypeOf()); + + await Assert.ThatAsync(() => this.generator.GenerateAsync(null, this.textualNotationSpecification, this.kernelFunctionLibraryPath, this.outputDirectoryInfo), Throws.TypeOf()); + + // A grammar that no longer carries the operator token rules must fail generation, not emit a + // silently truncated set. + await Assert.ThatAsync(() => this.generator.GenerateAsync(GeneratorSetupFixture.XmiReaderResult, this.driftedTextualNotationSpecification, this.kernelFunctionLibraryPath, this.outputDirectoryInfo), Throws.TypeOf()); + + // A Kernel Function Library that cannot be read must fail generation too. + await Assert.ThatAsync(() => this.generator.GenerateAsync(GeneratorSetupFixture.XmiReaderResult, this.textualNotationSpecification, Path.Combine(this.outputDirectoryInfo.FullName, "absent"), this.outputDirectoryInfo), Throws.TypeOf()); + + await Assert.ThatAsync(() => this.generator.GenerateAsync(GeneratorSetupFixture.XmiReaderResult, this.textualNotationSpecification, this.kernelFunctionLibraryPath, this.outputDirectoryInfo), Throws.Nothing); + + var generated = await File.ReadAllTextAsync(Path.Combine(this.outputDirectoryInfo.FullName, "ModelLevelEvaluableFunctions.cs")); + + // 39 operator symbols per KerML Table 5 and Table 7, less the 3 rows marked "No". + Assert.That(FunctionRegex().Matches(generated), Has.Count.EqualTo(36)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(generated, Does.Contain("\"BaseFunctions::==\"")); + Assert.That(generated, Does.Contain("\"BaseFunctions::as\"")); + Assert.That(generated, Does.Contain("\"BaseFunctions::#\"")); + Assert.That(generated, Does.Contain("\"ControlFunctions::select\"")); + Assert.That(generated, Does.Contain("\"ControlFunctions::.\"")); + Assert.That(generated, Does.Contain("\"DataFunctions::^\"")); + Assert.That(generated, Does.Contain("\"DataFunctions::**\"")); + } + + // '==' and '===' are declared by BaseFunctions AND DataFunctions; the probe order elects BaseFunctions. + using (Assert.EnterMultipleScope()) + { + Assert.That(generated, Does.Not.Contain("\"DataFunctions::==\"")); + Assert.That(generated, Does.Not.Contain("\"DataFunctions::===\"")); + } + + using (Assert.EnterMultipleScope()) + { + Assert.That(generated, Does.Not.Contain("\"BaseFunctions::all\"")); + Assert.That(generated, Does.Not.Contain("\"BaseFunctions::[\"")); + Assert.That(generated, Does.Not.Contain("\"DataFunctions::~\"")); + Assert.That(generated, Does.Not.Contain("\"DataFunctions::max\"")); + Assert.That(generated, Does.Not.Contain("\"ControlFunctions::reduce\"")); + } + } + + [GeneratedRegex("^\\s*\"[^\"]*::[^\"]*\",?$", RegexOptions.Multiline)] + private static partial Regex FunctionRegex(); + } +} diff --git a/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCoreModelLevelEvaluableFunctionsGenerator.cs b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCoreModelLevelEvaluableFunctionsGenerator.cs new file mode 100644 index 000000000..fc2709f03 --- /dev/null +++ b/SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/UmlCoreModelLevelEvaluableFunctionsGenerator.cs @@ -0,0 +1,285 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// 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.CodeGenerator.Generators.UmlHandleBarsGenerators +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + + using SysML2.NET.CodeGenerator.Grammar.Model; + using SysML2.NET.CodeGenerator.Library; + + using uml4net.Values; + using uml4net.xmi.Readers; + + /// + /// A UML Handlebars generator that produces the ModelLevelEvaluableFunctions membership set + /// backing Function::isModelLevelEvaluable, per KerML 1.0 Table 5 (§8.2.5.8.1) and Table 7 + /// (§8.2.5.8.2). The output lands in the KernelFunctions/AutoGenKernelFunctions folder of + /// the runtime project. + /// + /// + /// The two tables are reproduced rather than transcribed: the operator symbols come from the KEBNF + /// grammar and the UML model, and the owning library package comes from the Kernel Function Library + /// itself. Only the three rows the tables mark as not model-level evaluable are curated. + /// + public class UmlCoreModelLevelEvaluableFunctionsGenerator : UmlHandleBarsGenerator + { + /// + /// The name of the template for the ModelLevelEvaluableFunctions static class. + /// + private const string ModelLevelEvaluableFunctionsTemplateName = "core-model-level-evaluable-functions-template"; + + /// + /// The name of the OperatorExpression::operator property, both as a KEBNF assignment + /// target and as a UML owned attribute. + /// + private const string OperatorPropertyName = "operator"; + + /// + /// The number of distinct operator symbols the KerML specification maps to a library + /// Function — Table 5 contributes 33 and Table 7 contributes 6. + /// + private const int ExpectedOperatorSymbolCount = 39; + + /// + /// The library packages an OperatorExpression operator resolves against, in the probe + /// order mandated by the OperatorExpression::instantiatedType derivation. The order + /// disambiguates '==' and '===', which both packages declare. + /// + private static readonly string[] OperatorFunctionPackages = ["BaseFunctions", "DataFunctions", "ControlFunctions"]; + + /// + /// The only operators that KerML 1.0 Table 5 and Table 7 mark as NOT model-level evaluable. + /// Abstractness does not discriminate them — BaseFunctions::'==' is abstract and is + /// model-level evaluable — so they are curated rather than derived. + /// + private static readonly string[] NonModelLevelEvaluableFunctions = ["BaseFunctions::all", "BaseFunctions::[", "DataFunctions::~"]; + + /// + /// Register the custom helpers + /// + protected override void RegisterHelpers() + { + } + + /// + /// Register the code templates + /// + protected override void RegisterTemplates() + { + this.RegisterTemplate(ModelLevelEvaluableFunctionsTemplateName); + } + + /// + /// Not supported — this generator requires the KEBNF grammar and the Kernel Function Library. + /// Use . + /// + /// The + /// The target + /// nothing — always throws + /// Always thrown. + public override Task GenerateAsync(XmiReaderResult xmiReaderResult, DirectoryInfo outputDirectory) + { + throw new NotSupportedException("The generator needs TextualNotationSpecification and Kernel Function Library access"); + } + + /// + /// Generates the ModelLevelEvaluableFunctions membership set. + /// + /// The UML model supplying the defaulted operator values + /// The KerML grammar supplying the operator terminals + /// The path of the Kernel Function Library .kermlx folder + /// The target + /// an awaitable + /// + /// Thrown when the derived operator set drifts from the specification — an unexpected symbol + /// count, a symbol no library package declares, or a curated exclusion that is not derived. + /// + public async Task GenerateAsync(XmiReaderResult xmiReaderResult, TextualNotationSpecification textualNotationSpecification, string kernelFunctionLibraryPath, DirectoryInfo outputDirectory) + { + ArgumentNullException.ThrowIfNull(xmiReaderResult); + ArgumentNullException.ThrowIfNull(textualNotationSpecification); + ArgumentNullException.ThrowIfNull(outputDirectory); + + var operatorSymbols = CollectOperatorSymbols(xmiReaderResult, textualNotationSpecification); + + if (operatorSymbols.Count != ExpectedOperatorSymbolCount) + { + throw new InvalidOperationException($"Expected {ExpectedOperatorSymbolCount} operator symbols per KerML Table 5 and Table 7 but derived {operatorSymbols.Count}: {string.Join(", ", operatorSymbols)}"); + } + + var library = KernelFunctionLibraryReader.Read(kernelFunctionLibraryPath); + + var resolutions = operatorSymbols + .Select(symbol => (Symbol: symbol, QualifiedName: ResolveQualifiedName(symbol, library))) + .ToList(); + + var unresolvedSymbols = resolutions + .Where(resolution => resolution.QualifiedName == null) + .Select(resolution => resolution.Symbol) + .ToList(); + + if (unresolvedSymbols.Count != 0) + { + throw new InvalidOperationException($"No Kernel Function Library package declares a Function for the operator(s): {string.Join(", ", unresolvedSymbols)}"); + } + + var qualifiedNames = resolutions.Select(resolution => resolution.QualifiedName).ToList(); + + var undrivenExclusions = NonModelLevelEvaluableFunctions.Where(exclusion => !qualifiedNames.Contains(exclusion)).ToList(); + + if (undrivenExclusions.Count != 0) + { + throw new InvalidOperationException($"The curated exclusion(s) {string.Join(", ", undrivenExclusions)} are not present in the derived operator set"); + } + + var modelLevelEvaluableFunctions = qualifiedNames + .Where(qualifiedName => !NonModelLevelEvaluableFunctions.Contains(qualifiedName)) + .ToList(); + + var template = this.Templates[ModelLevelEvaluableFunctionsTemplateName]; + var generated = template(new { QualifiedNames = modelLevelEvaluableFunctions }); + generated = this.CodeCleanup(generated); + + await WriteAsync(generated, outputDirectory, "ModelLevelEvaluableFunctions.cs"); + } + + /// + /// Collects every operator symbol that the specification maps to a Kernel Function Library + /// Function, from the KEBNF operator = … assignments and from the UML classes that + /// default the operator attribute instead. + /// + /// The UML model + /// The KerML grammar + /// The ordinal-sorted set of distinct operator symbols + private static IReadOnlyList CollectOperatorSymbols(XmiReaderResult xmiReaderResult, TextualNotationSpecification textualNotationSpecification) + { + var symbols = new SortedSet(StringComparer.Ordinal); + + var operatorAssignments = textualNotationSpecification.Rules + .SelectMany(rule => rule.Alternatives) + .SelectMany(alternative => EnumerateAssignments(alternative.Elements)) + .Where(assignment => assignment.Property == OperatorPropertyName); + + foreach (var operatorAssignment in operatorAssignments) + { + symbols.UnionWith(ResolveAssignedSymbols(operatorAssignment, textualNotationSpecification)); + } + + symbols.UnionWith(CollectDefaultedOperatorSymbols(xmiReaderResult)); + + return [.. symbols]; + } + + /// + /// Collects the operator symbols carried as the default value of an operator owned + /// attribute, which is how IndexExpression, FeatureChainExpression, + /// CollectExpression and SelectExpression fix their operator. + /// + /// The UML model + /// The defaulted operator symbols + private static IEnumerable CollectDefaultedOperatorSymbols(XmiReaderResult xmiReaderResult) + { + return CreateHandlebarsPayload(xmiReaderResult).Classes + .SelectMany(@class => @class.OwnedAttribute) + .Where(ownedAttribute => ownedAttribute.Name == OperatorPropertyName) + .SelectMany(ownedAttribute => ownedAttribute.DefaultValue.OfType()) + .Select(literalString => literalString.Value) + .Where(value => !string.IsNullOrWhiteSpace(value)); + } + + /// + /// Flattens the assignments of a rule alternative, descending into grouped alternatives. + /// + /// The rule elements to flatten + /// Every reachable from the supplied elements + private static IEnumerable EnumerateAssignments(IEnumerable elements) + { + return elements.SelectMany(EnumerateAssignmentsOf); + } + + /// + /// Flattens the assignments of a single rule element, descending into grouped alternatives. + /// + /// The rule element to flatten + /// Every reachable from the supplied element + private static IEnumerable EnumerateAssignmentsOf(RuleElement element) + { + switch (element) + { + case AssignmentElement assignment: + return [assignment]; + + case GroupElement group: + return group.Alternatives.SelectMany(alternative => EnumerateAssignments(alternative.Elements)); + + default: + return []; + } + } + + /// + /// Resolves the operator symbols an operator = … assignment can take — either the + /// assigned terminal itself, or every terminal of the referenced token rule. + /// + /// The operator assignment + /// The KerML grammar + /// The operator symbols the assignment admits + private static IEnumerable ResolveAssignedSymbols(AssignmentElement assignment, TextualNotationSpecification textualNotationSpecification) + { + switch (assignment.Value) + { + case TerminalElement terminal: + return [terminal.Value]; + + case NonTerminalElement nonTerminal: + var referencedRule = textualNotationSpecification.Rules.FirstOrDefault(rule => rule.RuleName == nonTerminal.Name); + + return referencedRule == null + ? [] + : referencedRule.Alternatives + .SelectMany(alternative => alternative.Elements.OfType()) + .Select(terminalElement => terminalElement.Value); + + default: + return []; + } + } + + /// + /// Resolves the library Function an operator symbol denotes, by probing the operator + /// packages in specification order and taking the first that declares the symbol. + /// + /// The operator symbol + /// The Kernel Function Library, keyed by package name + /// The raw Package::Function name, or null when no package declares it + private static string ResolveQualifiedName(string symbol, IReadOnlyDictionary> library) + { + var declaringPackage = OperatorFunctionPackages + .FirstOrDefault(package => library.TryGetValue(package, out var functions) && functions.Contains(symbol)); + + return declaringPackage == null ? null : $"{declaringPackage}::{symbol}"; + } + } +} diff --git a/SysML2.NET.CodeGenerator/Library/KernelFunctionLibraryReader.cs b/SysML2.NET.CodeGenerator/Library/KernelFunctionLibraryReader.cs new file mode 100644 index 000000000..44142b6cd --- /dev/null +++ b/SysML2.NET.CodeGenerator/Library/KernelFunctionLibraryReader.cs @@ -0,0 +1,118 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// 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.CodeGenerator.Library +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Xml.Linq; + + /// + /// Reads the Kernel Function Library from its .kermlx XMI files and reports the + /// Function names that each library package declares. + /// + public static class KernelFunctionLibraryReader + { + /// + /// The search pattern matching the KerML XMI files of a library folder. + /// + private const string KerMlXmiSearchPattern = "*.kermlx"; + + /// + /// The unqualified xsi:type discriminator of a library package element. + /// + private const string LibraryPackageType = "LibraryPackage"; + + /// + /// The unqualified xsi:type discriminator of a function element. + /// + private const string FunctionType = "Function"; + + /// + /// The name of the attribute carrying the declared name of an element. + /// + private static readonly XName DeclaredNameAttribute = "declaredName"; + + /// + /// The name of the XML Schema instance type attribute that discriminates each element. + /// + private static readonly XName TypeAttribute = XName.Get("type", "http://www.w3.org/2001/XMLSchema-instance"); + + /// + /// Reads every library package in the supplied Kernel Function Library folder. + /// + /// The path of the folder holding the .kermlx files + /// The declared function names, keyed by the declaring library package name + /// Thrown when is not supplied. + /// Thrown when the folder does not exist. + /// Thrown when the folder carries no readable library package. + public static IReadOnlyDictionary> Read(string libraryDirectoryPath) + { + if (string.IsNullOrWhiteSpace(libraryDirectoryPath)) + { + throw new ArgumentException("The path of the Kernel Function Library folder is required", nameof(libraryDirectoryPath)); + } + + if (!Directory.Exists(libraryDirectoryPath)) + { + throw new DirectoryNotFoundException($"The Kernel Function Library folder '{libraryDirectoryPath}' was not found"); + } + + var packages = new Dictionary>(StringComparer.Ordinal); + + foreach (var filePath in Directory.EnumerateFiles(libraryDirectoryPath, KerMlXmiSearchPattern)) + { + var elements = XDocument.Load(filePath).Descendants().ToList(); + + var libraryPackage = elements.FirstOrDefault(element => IsOfType(element, LibraryPackageType)); + + if (libraryPackage?.Attribute(DeclaredNameAttribute) == null) + { + continue; + } + + packages[libraryPackage.Attribute(DeclaredNameAttribute).Value] = elements + .Where(element => IsOfType(element, FunctionType)) + .Select(element => element.Attribute(DeclaredNameAttribute)?.Value) + .Where(declaredName => !string.IsNullOrWhiteSpace(declaredName)) + .ToList(); + } + + return packages.Count == 0 + ? throw new InvalidOperationException($"The Kernel Function Library folder '{libraryDirectoryPath}' carries no readable library package") + : packages; + } + + /// + /// Asserts that an element carries the supplied xsi:type, ignoring the namespace prefix. + /// + /// The element to test + /// The unqualified type discriminator to match + /// true when the element is of the supplied type, false otherwise + private static bool IsOfType(XElement element, string unqualifiedType) + { + var type = element.Attribute(TypeAttribute)?.Value; + + return type != null && type[(type.IndexOf(':') + 1)..] == unqualifiedType; + } + } +} diff --git a/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj b/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj index 4f0fadf1f..4fc9f3835 100644 --- a/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj +++ b/SysML2.NET.CodeGenerator/SysML2.NET.CodeGenerator.csproj @@ -250,6 +250,9 @@ Always + + Always + @@ -280,5 +283,8 @@ datamodel\SysML-textual-bnf.kebnf Always + + Always + \ No newline at end of file diff --git a/SysML2.NET.CodeGenerator/Templates/Uml/core-model-level-evaluable-functions-template.hbs b/SysML2.NET.CodeGenerator/Templates/Uml/core-model-level-evaluable-functions-template.hbs new file mode 100644 index 000000000..31319689c --- /dev/null +++ b/SysML2.NET.CodeGenerator/Templates/Uml/core-model-level-evaluable-functions-template.hbs @@ -0,0 +1,71 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// 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. +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.KernelFunctions +{ + using System; + using System.Collections.Frozen; + using System.Collections.Generic; + + /// + /// Provides the Kernel Function Library membership set that decides + /// Function::isModelLevelEvaluable, derived from the operator terminals of the KerML + /// textual-notation KEBNF grammar, the defaulted operator attributes of the UML model, and + /// the Kernel Function Library itself. + /// + public static class ModelLevelEvaluableFunctions + { + /// + /// Provides the model-level evaluable library functions as raw Package::Function names. + /// + /// + /// The segments are declared names, NOT KerML-escaped names — BaseFunctions::==, not + /// BaseFunctions::'==' — so that membership can be tested without reproducing the + /// escaping rules of the textual notation. + /// + public static readonly FrozenSet QualifiedNames = new List + { + {{#each this.QualifiedNames as | qualifiedName | }} + "{{{qualifiedName}}}"{{#unless @last}},{{/unless}} + {{/each}} + }.ToFrozenSet(StringComparer.Ordinal); + + /// + /// Asserts that the named function of the named library package is model-level evaluable. + /// + /// The declared name of the library package owning the function + /// The declared name of the function + /// true when the function is model-level evaluable, false otherwise + public static bool Contains(string packageName, string functionName) + { + return !string.IsNullOrWhiteSpace(packageName) + && !string.IsNullOrWhiteSpace(functionName) + && QualifiedNames.Contains($"{packageName}::{functionName}"); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ diff --git a/SysML2.NET.Tests/Extend/FunctionExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/FunctionExtensionsTestFixture.cs index f7e2b69fb..826e6b410 100644 --- a/SysML2.NET.Tests/Extend/FunctionExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/FunctionExtensionsTestFixture.cs @@ -27,6 +27,7 @@ namespace SysML2.NET.Tests.Extend using SysML2.NET.Core.POCO.Core.Features; using SysML2.NET.Core.POCO.Core.Types; using SysML2.NET.Core.POCO.Kernel.Functions; + using SysML2.NET.Core.POCO.Root.Namespaces; using SysML2.NET.Extensions; [TestFixture] @@ -91,9 +92,57 @@ public void VerifyComputeExpression() [Test] public void VerifyComputeIsModelLevelEvaluable() { - // For later: deferred — Kernel Functions Library registry membership test (see GitHub #322). - var subject = new Function(); - Assert.That(subject.ComputeIsModelLevelEvaluable, Throws.TypeOf()); + // Null subject: + Assert.That(() => ((IFunction)null).ComputeIsModelLevelEvaluable(), Throws.TypeOf()); + + // Empty: no owning namespace and no name → not a library function. + Assert.That(new Function().ComputeIsModelLevelEvaluable(), Is.False); + + // Positive: the operators KerML Table 5 and Table 7 mark as model-level evaluable. + using (Assert.EnterMultipleScope()) + { + Assert.That(LibraryFunction("BaseFunctions", "==").ComputeIsModelLevelEvaluable(), Is.True); + Assert.That(LibraryFunction("BaseFunctions", "as").ComputeIsModelLevelEvaluable(), Is.True); + Assert.That(LibraryFunction("BaseFunctions", "#").ComputeIsModelLevelEvaluable(), Is.True); + Assert.That(LibraryFunction("DataFunctions", "^").ComputeIsModelLevelEvaluable(), Is.True); + Assert.That(LibraryFunction("DataFunctions", "**").ComputeIsModelLevelEvaluable(), Is.True); + Assert.That(LibraryFunction("ControlFunctions", "select").ComputeIsModelLevelEvaluable(), Is.True); + Assert.That(LibraryFunction("ControlFunctions", ".").ComputeIsModelLevelEvaluable(), Is.True); + } + + // Negative: the three operators the tables mark as NOT model-level evaluable. + using (Assert.EnterMultipleScope()) + { + Assert.That(LibraryFunction("BaseFunctions", "all").ComputeIsModelLevelEvaluable(), Is.False); + Assert.That(LibraryFunction("BaseFunctions", "[").ComputeIsModelLevelEvaluable(), Is.False); + Assert.That(LibraryFunction("DataFunctions", "~").ComputeIsModelLevelEvaluable(), Is.False); + } + + // Negative: library functions that no operator maps to, and a function outside the library. + using (Assert.EnterMultipleScope()) + { + Assert.That(LibraryFunction("DataFunctions", "max").ComputeIsModelLevelEvaluable(), Is.False); + Assert.That(LibraryFunction("ControlFunctions", "reduce").ComputeIsModelLevelEvaluable(), Is.False); + Assert.That(LibraryFunction("BaseFunctions", "ToString").ComputeIsModelLevelEvaluable(), Is.False); + Assert.That(LibraryFunction("SomePackage", "==").ComputeIsModelLevelEvaluable(), Is.False); + Assert.That(LibraryFunction("BaseFunctions", null).ComputeIsModelLevelEvaluable(), Is.False); + } + + // '==' and '===' are declared by BaseFunctions AND DataFunctions; the probe order elects BaseFunctions. + using (Assert.EnterMultipleScope()) + { + Assert.That(LibraryFunction("DataFunctions", "==").ComputeIsModelLevelEvaluable(), Is.False); + Assert.That(LibraryFunction("DataFunctions", "===").ComputeIsModelLevelEvaluable(), Is.False); + } + + static IFunction LibraryFunction(string packageName, string functionName) + { + var libraryPackage = new Namespace { DeclaredName = packageName }; + var function = new Function { DeclaredName = functionName }; + libraryPackage.AssignOwnership(new OwningMembership(), function); + + return function; + } } } } diff --git a/SysML2.NET.Tests/Extend/InvocationExpressionExtensionsTestFixture.cs b/SysML2.NET.Tests/Extend/InvocationExpressionExtensionsTestFixture.cs index 479314120..3ed9aa338 100644 --- a/SysML2.NET.Tests/Extend/InvocationExpressionExtensionsTestFixture.cs +++ b/SysML2.NET.Tests/Extend/InvocationExpressionExtensionsTestFixture.cs @@ -75,21 +75,25 @@ public void VerifyComputeRedefinedModelLevelEvaluableOperation() argumentFeature.AssignOwnership(new FeatureValue(), falseArgument); falseSubject.AssignOwnership(new FeatureMembership(), argumentFeature); - // #322 stub-blocker branch: empty arguments -> All(...) over an empty source -> true, so the - // && evaluates the right operand and reaches function.isModelLevelEvaluable. A FeatureTyping to - // a Function is wired so function resolves non-null (else the access would NRE, not reach the - // stub); the Function's ComputeIsModelLevelEvaluable is still a NotSupportedException stub. - var stubSubject = new InvocationExpression(); - stubSubject.AssignOwnership(new FeatureTyping { Type = new Function() }); + // Empty arguments -> All(...) over an empty source -> true, so the && evaluates the right + // operand and reaches function.isModelLevelEvaluable. A Function outside the Kernel Functions + // Library is not model-level evaluable. + var nonLibrarySubject = new InvocationExpression(); + nonLibrarySubject.AssignOwnership(new FeatureTyping { Type = new Function() }); + + // The same shape, but invoking BaseFunctions::'==' — model-level evaluable per KerML Table 5. + var libraryPackage = new Namespace { DeclaredName = "BaseFunctions" }; + var equalityFunction = new Function { DeclaredName = "==" }; + libraryPackage.AssignOwnership(new OwningMembership(), equalityFunction); + + var librarySubject = new InvocationExpression(); + librarySubject.AssignOwnership(new FeatureTyping { Type = equalityFunction }); using (Assert.EnterMultipleScope()) { Assert.That(falseSubject.ComputeRedefinedModelLevelEvaluableOperation([]), Is.False); - - // For later: reaches function.isModelLevelEvaluable, deferred (GitHub #322). - Assert.That( - () => stubSubject.ComputeRedefinedModelLevelEvaluableOperation([]), - Throws.TypeOf()); + Assert.That(nonLibrarySubject.ComputeRedefinedModelLevelEvaluableOperation([]), Is.False); + Assert.That(librarySubject.ComputeRedefinedModelLevelEvaluableOperation([]), Is.True); } } } diff --git a/SysML2.NET/Extend/FunctionExtensions.cs b/SysML2.NET/Extend/FunctionExtensions.cs index 871651d42..8abe1d6f3 100644 --- a/SysML2.NET/Extend/FunctionExtensions.cs +++ b/SysML2.NET/Extend/FunctionExtensions.cs @@ -33,6 +33,7 @@ namespace SysML2.NET.Core.POCO.Kernel.Functions using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces; + using SysML2.NET.KernelFunctions; /// /// The class provides extensions methods for @@ -57,18 +58,27 @@ internal static List ComputeExpression(this IFunction functionSubje } /// - /// Computes the derived property. + /// Computes whether this is one of the Kernel Functions Library + /// functions that may be invoked by a model-level evaluable . /// + /// + /// There is no OCL derivation: KerML 1.0 §8.3.4.7.4 makes this a library-membership test, and + /// the member set is enumerated by Table 5 (§8.2.5.8.1) and Table 7 (§8.2.5.8.2). + /// /// /// The subject /// /// - /// the computed result + /// true when the subject is a model-level evaluable library function, false otherwise /// - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + /// + /// Thrown when is null. + /// internal static bool ComputeIsModelLevelEvaluable(this IFunction functionSubject) { - throw new NotSupportedException("Create a GitHub issue when this method is required"); + return functionSubject == null + ? throw new ArgumentNullException(nameof(functionSubject)) + : ModelLevelEvaluableFunctions.Contains(functionSubject.owningNamespace?.name, functionSubject.name); } /// diff --git a/SysML2.NET/KernelFunctions/AutoGenKernelFunctions/ModelLevelEvaluableFunctions.cs b/SysML2.NET/KernelFunctions/AutoGenKernelFunctions/ModelLevelEvaluableFunctions.cs new file mode 100644 index 000000000..9f0613ed3 --- /dev/null +++ b/SysML2.NET/KernelFunctions/AutoGenKernelFunctions/ModelLevelEvaluableFunctions.cs @@ -0,0 +1,104 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// 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. +// +// +// ------------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ + +namespace SysML2.NET.KernelFunctions +{ + using System; + using System.Collections.Frozen; + using System.Collections.Generic; + + /// + /// Provides the Kernel Function Library membership set that decides + /// Function::isModelLevelEvaluable, derived from the operator terminals of the KerML + /// textual-notation KEBNF grammar, the defaulted operator attributes of the UML model, and + /// the Kernel Function Library itself. + /// + public static class ModelLevelEvaluableFunctions + { + /// + /// Provides the model-level evaluable library functions as raw Package::Function names. + /// + /// + /// The segments are declared names, NOT KerML-escaped names — BaseFunctions::==, not + /// BaseFunctions::'==' — so that membership can be tested without reproducing the + /// escaping rules of the textual notation. + /// + public static readonly FrozenSet QualifiedNames = new List + { + "BaseFunctions::!=", + "BaseFunctions::!==", + "BaseFunctions::#", + "DataFunctions::%", + "DataFunctions::&", + "DataFunctions::*", + "DataFunctions::**", + "DataFunctions::+", + "BaseFunctions::,", + "DataFunctions::-", + "ControlFunctions::.", + "DataFunctions::..", + "DataFunctions::/", + "DataFunctions::<", + "DataFunctions::<=", + "BaseFunctions::==", + "BaseFunctions::===", + "DataFunctions::>", + "DataFunctions::>=", + "ControlFunctions::??", + "BaseFunctions::@", + "BaseFunctions::@@", + "DataFunctions::^", + "ControlFunctions::and", + "BaseFunctions::as", + "ControlFunctions::collect", + "BaseFunctions::hastype", + "ControlFunctions::if", + "ControlFunctions::implies", + "BaseFunctions::istype", + "BaseFunctions::meta", + "DataFunctions::not", + "ControlFunctions::or", + "ControlFunctions::select", + "DataFunctions::xor", + "DataFunctions::|" + }.ToFrozenSet(StringComparer.Ordinal); + + /// + /// Asserts that the named function of the named library package is model-level evaluable. + /// + /// The declared name of the library package owning the function + /// The declared name of the function + /// true when the function is model-level evaluable, false otherwise + public static bool Contains(string packageName, string functionName) + { + return !string.IsNullOrWhiteSpace(packageName) + && !string.IsNullOrWhiteSpace(functionName) + && QualifiedNames.Contains($"{packageName}::{functionName}"); + } + } +} + +// ------------------------------------------------------------------------------------------------ +// --------THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN!-------- +// ------------------------------------------------------------------------------------------------ From 650e865f9c4be463536bbbfe4210ab3124c29763 Mon Sep 17 00:00:00 2001 From: atheate Date: Tue, 1 Sep 2026 11:01:28 +0200 Subject: [PATCH 2/2] SQ issue --- SysML2.NET/Extend/FunctionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/SysML2.NET/Extend/FunctionExtensions.cs b/SysML2.NET/Extend/FunctionExtensions.cs index 8abe1d6f3..f2bc0e65f 100644 --- a/SysML2.NET/Extend/FunctionExtensions.cs +++ b/SysML2.NET/Extend/FunctionExtensions.cs @@ -30,6 +30,7 @@ namespace SysML2.NET.Core.POCO.Kernel.Functions using SysML2.NET.Core.POCO.Core.Features; using SysML2.NET.Core.POCO.Core.Types; using SysML2.NET.Core.POCO.Kernel.Behaviors; + using SysML2.NET.Core.POCO.Kernel.Expressions; using SysML2.NET.Core.POCO.Root.Annotations; using SysML2.NET.Core.POCO.Root.Elements; using SysML2.NET.Core.POCO.Root.Namespaces;