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..f2bc0e65f 100644
--- a/SysML2.NET/Extend/FunctionExtensions.cs
+++ b/SysML2.NET/Extend/FunctionExtensions.cs
@@ -30,9 +30,11 @@ 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;
+ using SysML2.NET.KernelFunctions;
///
/// The class provides extensions methods for
@@ -57,18 +59,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!--------
+// ------------------------------------------------------------------------------------------------