Skip to content

Commit 7b12482

Browse files
authored
Merge pull request #1026 from FirelyTeam/1008-fix-qicore-2025-bugs-revisit-aliasrefresulttypespecifier-resolver-logic
Refactor library preprocessing with new classes
2 parents ce34113 + beb7cfc commit 7b12482

31 files changed

+647
-499
lines changed

Cql/CodeGeneration.NET/Toolkit/Internal/ElmToolkitServices.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
using Hl7.Cql.Abstractions;
1010
using Hl7.Cql.Compiler;
11+
using Hl7.Cql.Compiler.Preprocessing;
1112
using Hl7.Cql.Fhir;
1213
using Hl7.Cql.Runtime.Hosting;
1314
using Hl7.Cql.Runtime.Logging;
@@ -70,6 +71,7 @@ public static IServiceCollection AddCqlCompilerServices(
7071
return converter;
7172
});
7273

74+
services.TryAddSingleton<LibraryPreprocessorBuilder>();
7375
services.TryAddSingleton<CqlOperatorsBinder>();
7476
services.TryAddSingleton<CqlContextBinder>();
7577
services.TryAddSingleton(_ => expressionBuilderSettings);

Cql/CoreTests/ElmPreprocessorTest.cs renamed to Cql/CoreTests/LibraryPreprocessorTest.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
*/
88

99
using Hl7.Cql.Compiler;
10+
using Hl7.Cql.Compiler.Preprocessing;
1011
using Hl7.Cql.Elm;
12+
using Microsoft.Extensions.Logging.Abstractions;
1113

1214
namespace CoreTests
1315
{
1416
[TestClass]
15-
public class ElmPreprocessorTest
17+
public class LibraryPreprocessorTest
1618
{
1719
private Library FHIRHelpers() =>
1820
Library.LoadFromJson(new FileInfo(Path.Combine("..", "..", "..", "..", "..", "LibrarySets", "Demo", "Elm", "FHIRHelpers.json")));
@@ -30,8 +32,8 @@ public void MATGlobal_Add_ResultTypeSpecifier()
3032
rtsChecker.Nodes.Should().NotBeEmpty();
3133

3234
var ls = new LibrarySet("", FHIRHelpers(), lib);
33-
var pp = new ElmPreprocessor(ls);
34-
pp.Preprocess(lib);
35+
var pp = new LibraryPreprocessor(ls, NullLoggerFactory.Instance);
36+
pp.PreprocessLibrary(lib);
3537
rtsChecker = new ResultTypeSpecifierChecker();
3638
rtsChecker.Start(lib);
3739
// after pre-processing the library, there should be no such nodes remaining.

Cql/Cql.Compiler/AmbiguousOverloadCorrector.cs

Lines changed: 0 additions & 34 deletions
This file was deleted.

Cql/Cql.Compiler/ElmPreprocessor.cs

Lines changed: 0 additions & 27 deletions
This file was deleted.

Cql/Cql.Compiler/ExpressionBuilderContext.LibraryDefs.cs

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024, NCQA and contributors
2+
* Copyright (c) 2024, Firely, NCQA and contributors
33
* See the file CONTRIBUTORS for details.
44
*
55
* This file is licensed under the BSD 3-Clause license
@@ -98,12 +98,6 @@ public void ProcessExpressionDef(
9898
null);
9999
}
100100

101-
// Pre-process expression to fix missing resultTypeSpecifier on AliasRef elements
102-
if (expressionDef.expression != null)
103-
{
104-
FixMissingAliasRefTypeSpecifiers(expressionDef.expression);
105-
}
106-
107101
var expressionKey = $"{_libraryContext.LibraryVersionedIdentifier}.{expressionDefName}";
108102
Type[] parameterTypes = [];
109103
ParameterExpression[] parameters = [CqlExpressions.ParameterExpression];
@@ -137,7 +131,16 @@ public void ProcessExpressionDef(
137131
}
138132
}
139133

140-
CreateAndAddDefinition(expressionDefName, parameters, parameterTypes, originalParameterNames);
134+
if (expressionDef.expression is { } expressionDefExpression)
135+
{
136+
var bodyExpression = TranslateArg(expressionDefExpression);
137+
var lambda = Expression.Lambda(bodyExpression, parameters);
138+
var tags = BuildTags();
139+
var def = expressionDef is FunctionDef
140+
? new CqlFunctionDefinition(lambda, expressionDefName, originalParameterNames, tags)
141+
: new CqlExpressionDefinition(lambda, expressionDefName, tags);
142+
_libraryContext.LibraryDefinitions.AddDefinition(_libraryContext.LibraryVersionedIdentifier, new(expressionDefName, parameterTypes), def);
143+
}
141144
}
142145
});
143146

@@ -212,21 +215,6 @@ void HandleExternalFunction(
212215

213216
return (parameterTypes, originalNames);
214217
}
215-
216-
void CreateAndAddDefinition(
217-
string expressionDefName,
218-
ParameterExpression[] parameters,
219-
Type[] parameterTypes,
220-
IReadOnlyDictionary<string, string> originalParameterNames)
221-
{
222-
var bodyExpression = TranslateArg(expressionDef.expression);
223-
var lambda = Expression.Lambda(bodyExpression, parameters);
224-
var tags = BuildTags();
225-
var def = expressionDef is FunctionDef
226-
? new CqlFunctionDefinition(lambda, expressionDefName, originalParameterNames, tags)
227-
: new CqlExpressionDefinition(lambda, expressionDefName, tags);
228-
_libraryContext.LibraryDefinitions.AddDefinition(_libraryContext.LibraryVersionedIdentifier, new(expressionDefName, parameterTypes), def);
229-
}
230218
}
231219

232220
public void ProcessIncludes(

Cql/Cql.Compiler/LibraryExpressionBuilder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE
77
*/
88

9+
using Hl7.Cql.Compiler.Preprocessing;
910
using Hl7.Cql.Elm;
1011

1112
namespace Hl7.Cql.Compiler;
@@ -15,7 +16,8 @@ namespace Hl7.Cql.Compiler;
1516
/// </summary>
1617
internal class LibraryExpressionBuilder(
1718
ILogger<LibraryExpressionBuilder> logger,
18-
ExpressionBuilder expressionBuilder)
19+
ExpressionBuilder expressionBuilder,
20+
LibraryPreprocessorBuilder libraryPreprocessorBuilder)
1921
{
2022
public CqlDefinitionDictionary ProcessLibrary(
2123
Library library,
@@ -28,7 +30,7 @@ public LibraryExpressionBuilderContext NewLibraryExpressionBuilderContext(
2830
Library library,
2931
CqlDefinitionDictionary? libraryDefinitions = null,
3032
LibrarySetExpressionBuilderContext? libsCtx = null) =>
31-
new(logger, expressionBuilder, library, libraryDefinitions ?? new(), libsCtx);
33+
new(logger, expressionBuilder, libraryPreprocessorBuilder, library, libraryDefinitions ?? new(), libsCtx);
3234

3335
public ExpressionBuilderContext NewExpressionBuilderContext(
3436
Library library,

Cql/Cql.Compiler/LibraryExpressionBuilderContext.cs

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE
77
*/
88

9+
using Hl7.Cql.Compiler.Preprocessing;
910
using Hl7.Cql.Elm;
1011

1112
namespace Hl7.Cql.Compiler;
@@ -16,10 +17,12 @@ internal partial class LibraryExpressionBuilderContext
1617
private readonly ExpressionBuilder _expressionBuilder;
1718
private readonly CqlDefinitionDictionary _libraryDefinitions;
1819
private readonly LibrarySetExpressionBuilderContext? _libsCtx;
20+
private readonly LibraryPreprocessor _preprocessor;
1921

2022
public LibraryExpressionBuilderContext(
2123
ILogger<LibraryExpressionBuilder> logger,
2224
ExpressionBuilder expressionBuilder,
25+
LibraryPreprocessorBuilder libraryPreprocessorBuilder,
2326
Library library,
2427
CqlDefinitionDictionary libraryDefinitions,
2528
LibrarySetExpressionBuilderContext? libsCtx = null)
@@ -30,10 +33,11 @@ public LibraryExpressionBuilderContext(
3033
_expressionBuilder = expressionBuilder;
3134
Library = library;
3235
LibraryVersionedIdentifier = Library.VersionedLibraryIdentifier;
36+
_preprocessor =
37+
LibrarySetContext?.Preprocessor
38+
?? libraryPreprocessorBuilder.Build(new LibrarySet(LibraryVersionedIdentifier, Library));
3339
}
3440

35-
private static readonly AmbiguousOverloadCorrector AmbiguousOverloadCorrector = new AmbiguousOverloadCorrector();
36-
3741
/// <summary>
3842
/// Gets the library associated with the expression builder context.
3943
/// </summary>
@@ -48,20 +52,10 @@ public LibraryExpressionBuilderContext(
4852
public CqlDefinitionDictionary ProcessLibrary() =>
4953
this.CatchRethrowExpressionBuildingException(_ =>
5054
{
51-
// Make sure all overloads in the library are unique.
52-
// This is a fix for QICore-based CQL, where the functions only differ by profiles on the same resource.
53-
// We should remove this when the compiler is fixed.
54-
// See https://github.com/FirelyTeam/firely-cql-sdk/issues/438.
55-
_logger.LogDebug("Preprocessing library '{library}' - AmbiguousOverloadCorrector", LibraryVersionedIdentifier);
56-
AmbiguousOverloadCorrector.Fix(Library);
57-
58-
_logger.LogDebug("Preprocessing library '{library}' - ElmPreprocessor", LibraryVersionedIdentifier);
59-
var ls = LibrarySetContext?.LibrarySet ?? new LibrarySet(LibraryVersionedIdentifier, Library);
60-
var processor = new ElmPreprocessor(ls);
61-
processor.Preprocess(Library);
62-
6355
_logger.LogDebug("Building expressions for '{library}'", LibraryVersionedIdentifier);
6456

57+
_preprocessor.PreprocessLibrary(Library);
58+
6559
if (Library.includes is { Length: > 0 } includeDefs)
6660
{
6761
foreach (var includeDef in includeDefs)

Cql/Cql.Compiler/LibrarySetExpressionBuilder.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
* available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE
77
*/
88

9+
using Hl7.Cql.Compiler.Preprocessing;
910
using Hl7.Cql.Elm;
1011
using Hl7.Cql.Runtime;
1112

1213
namespace Hl7.Cql.Compiler;
1314

14-
internal class LibrarySetExpressionBuilder(LibraryExpressionBuilder libraryExpressionBuilder)
15+
internal class LibrarySetExpressionBuilder(
16+
LibraryExpressionBuilder libraryExpressionBuilder,
17+
LibraryPreprocessorBuilder libraryPreprocessorBuilder)
1518
{
1619
public IEnumerable<(Library library, CqlDefinitionDictionary libraryDefinitions)> BuildEachLibraryDefinitions(
1720
LibrarySet librarySet,
@@ -24,5 +27,5 @@ internal class LibrarySetExpressionBuilder(LibraryExpressionBuilder libraryExpre
2427
private LibrarySetExpressionBuilderContext NewLibrarySetExpressionBuilderContext(
2528
LibrarySet librarySet,
2629
CqlDefinitionDictionary librarySetDefinitions) =>
27-
new(libraryExpressionBuilder, librarySet, librarySetDefinitions);
30+
new(libraryExpressionBuilder, libraryPreprocessorBuilder, librarySet, librarySetDefinitions);
2831
}

Cql/Cql.Compiler/LibrarySetExpressionBuilderContext.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE
77
*/
88

9+
using Hl7.Cql.Compiler.Preprocessing;
910
using Hl7.Cql.Runtime;
1011
using Library = Hl7.Cql.Elm.Library;
1112

@@ -17,12 +18,14 @@ internal partial class LibrarySetExpressionBuilderContext
1718

1819
public LibrarySetExpressionBuilderContext(
1920
LibraryExpressionBuilder libraryExpressionBuilder,
21+
LibraryPreprocessorBuilder libraryPreprocessorBuilder,
2022
LibrarySet librarySet,
2123
CqlDefinitionDictionary librarySetDefinitions)
2224
{
2325
_libraryExpressionBuilder = libraryExpressionBuilder;
2426
LibrarySetDefinitions = librarySetDefinitions;
2527
LibrarySet = librarySet;
28+
Preprocessor = libraryPreprocessorBuilder.Build(librarySet);
2629
DebuggerInfo = new BuilderContextDebuggerInfo("LibrarySet", Name: LibrarySet!.Name!);
2730
}
2831

@@ -36,6 +39,8 @@ public LibrarySetExpressionBuilderContext(
3639
/// </summary>
3740
public LibrarySet LibrarySet { get; }
3841

42+
public LibraryPreprocessor Preprocessor { get; }
43+
3944
public IEnumerable<(Library library, CqlDefinitionDictionary libraryDefinitions)> BuildEachLibraryDefinitions(
4045
BatchProcessExceptionHandlingStrategyBuilder<Library>? buildExceptionHandlingStrategy = null,
4146
Action<Library>? prebuildLibraryHandler = null) =>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) 2024, NCQA and contributors
3+
* See the file CONTRIBUTORS for details.
4+
*
5+
* This file is licensed under the BSD 3-Clause license
6+
* available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE
7+
*/
8+
9+
using Hl7.Cql.Elm;
10+
11+
namespace Hl7.Cql.Compiler.Preprocessing;
12+
13+
/// <summary>
14+
/// Ensures that there are no overloads with exactly the same signature.
15+
/// </summary>
16+
/// <remarks>This happens currently in QICore-based CQL, where the functions only differ
17+
/// by profiles on the same resource, which results in the same signature after the QICore
18+
/// profile names are erased by the compiler. This is a temporary fix that should be
19+
/// corrected in the compiler. Until that moment, we'll just pick the first overload.
20+
/// See https://github.com/FirelyTeam/firely-cql-sdk/issues/438 for more info.
21+
/// </remarks>
22+
internal class AmbiguousOverloadCorrector(ILogger<AmbiguousOverloadCorrector> logger)
23+
{
24+
public void Fix(Library library)
25+
{
26+
// Make sure all overloads in the library are unique.
27+
// This is a fix for QICore-based CQL, where the functions only differ by profiles on the same resource.
28+
// We should remove this when the compiler is fixed.
29+
// See https://github.com/FirelyTeam/firely-cql-sdk/issues/438.
30+
logger.LogDebug("Preprocessing library '{library}'", library.VersionedLibraryIdentifier);
31+
32+
if (library.statements is { Length: > 0 } expressionDefs)
33+
{
34+
const LogLevel logLevel = LogLevel.Information;
35+
36+
if (logger.IsEnabled(logLevel))
37+
{
38+
// Use grouping to find duplicates, so we can log them.
39+
var grouped = expressionDefs.GroupBy(def => def, ExpressionSignatureComparer.Instance).ToList();
40+
var duplicates = grouped
41+
.Where(g => g.Count() > 1)
42+
.Select(g => g.Key.Name)
43+
.ToList();
44+
if (duplicates.Count > 0)
45+
{
46+
logger.Log(logLevel, "Duplicate overloads found in library '{Library}': {Duplicates}", library.VersionedLibraryIdentifier, string.Join(", ", duplicates));
47+
library.statements = grouped.Select(g => (ExpressionDef)g.Key).ToArray(); // Since we just only put ExpressionDefs into the HashSet, we can safely cast them back.
48+
}
49+
}
50+
else
51+
{
52+
var statementSet = new HashSet<IDefinitionElement>(expressionDefs, ExpressionSignatureComparer.Instance);
53+
if (statementSet.Count < expressionDefs.Length)
54+
{
55+
library.statements = statementSet.Cast<ExpressionDef>().ToArray(); // Since we just only put ExpressionDefs into the HashSet, we can safely cast them back.
56+
}
57+
}
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)