Skip to content

Commit 7efc00b

Browse files
authored
Merge branch 'develop' into AnnotatePopulation-fixes
2 parents b4f8c8d + ab2b04c commit 7efc00b

File tree

148 files changed

+3938
-824
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

148 files changed

+3938
-824
lines changed

Cql-Sdk.slnf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
"Cql\\Cql.CqlToElm\\Cql.CqlToElm.csproj",
1010
"Cql\\Cql.Firely\\Cql.Firely.csproj",
1111
"Cql\\Cql.Grammar\\Cql.Grammar.csproj",
12+
"Cql\\Cql.Invocation\\Cql.Invocation.csproj",
1213
"Cql\\Cql.Model\\Cql.Model.csproj",
1314
"Cql\\Cql.Packaging\\Cql.Packaging.csproj",
1415
"Cql\\Cql.Runtime\\Cql.Runtime.csproj",
1516
"Cql\\CqlToElmTests\\CqlToElmTests.csproj",
16-
"Cql\\Cql.Invocation\\Cql.Invocation.csproj",
1717
"Cql\\Cql\\Cql.csproj",
1818
"Cql\\Elm\\Elm.csproj",
1919
"Cql\\Iso8601\\Iso8601.csproj",

Cql/Cql.Compiler/CqlOperatorsBinder.Specific.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
using Hl7.Cql.Compiler.Expressions;
10+
using Hl7.Cql.Compiler.Infrastructure;
1011
using Hl7.Cql.Operators;
1112
using Hl7.Cql.Primitives;
1213
using Hl7.Cql.ValueSets;
@@ -72,6 +73,21 @@ private Expression Union(
7273
}
7374
}
7475

76+
// Check if we have compatible list types with different element types
77+
var leftListElementType = _typeResolver.GetListElementType(left.Type);
78+
var rightListElementType = _typeResolver.GetListElementType(right.Type);
79+
if (leftListElementType != null && rightListElementType != null && leftListElementType != rightListElementType)
80+
{
81+
// Check if the element types are structurally compatible for union
82+
if (ElmTupleTypeUtility.AreCompatibleForUnionOperation(leftListElementType, rightListElementType))
83+
{
84+
// Cast both to IEnumerable<object> to allow union
85+
var leftAsObjectEnumerable = left.NewTypeAsExpression<IEnumerable<object>>();
86+
var rightAsObjectEnumerable = right.NewTypeAsExpression<IEnumerable<object>>();
87+
return BindToBestMethodOverload(nameof(ICqlOperators.Union), [leftAsObjectEnumerable, rightAsObjectEnumerable], [])!;
88+
}
89+
}
90+
7591
return BindToBestMethodOverload(nameof(ICqlOperators.Union), [left, right], [])!;
7692
}
7793

Cql/Cql.Compiler/CqlOperatorsBinder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Type[] typeArgs
6969
("Flatten" ,0 , >=1) => Flatten(args[0]),
7070
("InList" ,0 , >=2) => InList(args[0], args[1]),
7171
("LateBoundProperty",0 , >=3) => LateBoundProperty(args[0], args[1], args[2]),
72+
("Union" ,0 , >=2) => Union(args[0], args[1]),
7273
("ListUnion" ,0 , >=2) => Union(args[0], args[1]),
7374
("ResolveValueSet" ,0 , >=1) => ResolveValueSet(args[0]),
7475
("Retrieve" ,0 , >=3) => Retrieve(args[0], args[1], args[2], args[3]),

Cql/Cql.Compiler/ExpressionBuilderContext.LibraryDefs.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ 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+
101107
var expressionKey = $"{_libraryContext.LibraryVersionedIdentifier}.{expressionDefName}";
102108
Type[] parameterTypes = [];
103109
ParameterExpression[] parameters = [CqlExpressions.ParameterExpression];

Cql/Cql.Compiler/ExpressionBuilderContext.TypeFor.cs

Lines changed: 3 additions & 2 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
@@ -244,7 +244,8 @@ private Type TupleTypeFor((string name, TypeSpecifier elementType)[] elements, F
244244
type = changeType(type);
245245

246246
return (type, el.name);
247-
});
247+
})
248+
.ToList();
248249

249250
return _tupleBuilderCache.CreateOrGetTupleTypeFor(tupleFields);
250251
}

Cql/Cql.Compiler/ExpressionBuilderContext.cs

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, NCQA and contributors
2+
* Copyright (c) 2023, Firely, NCQA and contributors
33
* See the file CONTRIBUTORS for details.
44
*
55
* This file is licensed under the BSD 3-Clause license
@@ -127,6 +127,20 @@ private Expression TranslateElement(Element element) =>
127127
{
128128
using (PushElement(element))
129129
{
130+
/*
131+
This code is useful for setting breakpoints to inspect the expression tree at a specific element.
132+
The ELM json must be modified to add an annotation tags with a debug counter first.
133+
134+
var debugCounter = element.annotation
135+
?.OfType<Annotation>()
136+
.FirstOrDefault()?.t.FirstOrDefault(t => t.name == "debug")
137+
?.value;
138+
if (debugCounter == "42") // Identify the correct debug counter from the ELM file
139+
{
140+
; // Set a breakpoint here
141+
}
142+
*/
143+
130144
Expression? expression = element switch
131145
{
132146
//@formatter:off
@@ -325,12 +339,12 @@ Expression ConvertToResultType()
325339
{
326340
if (_typeResolver.GetListElementType(left.Type, throwError: false) is { } leftListElemType
327341
&& _typeResolver.GetListElementType(right.Type, throwError: false) is { } rightListElemType
328-
&& leftListElemType == rightListElemType)
342+
&& ElmTupleTypeUtility.AreCompatibleForUnionOperation(leftListElemType, rightListElemType))
329343
return [left, right];
330344

331345
if (left.Type.IsCqlInterval(out var leftPointType)
332346
&& right.Type.IsCqlInterval(out var rightPointType)
333-
&& leftPointType == rightPointType)
347+
&& ElmTupleTypeUtility.AreCompatibleForUnionOperation(leftPointType, rightPointType))
334348
return [left, right];
335349
}
336350

@@ -442,8 +456,7 @@ protected Expression List(List list)
442456
array = Expression.NewArrayBounds(elementType, Expression.Constant(0));
443457
}
444458

445-
var asEnumerable = array.NewTypeAsExpression(typeof(IEnumerable<>).MakeGenericType(elementType));
446-
return asEnumerable;
459+
return array;
447460
}
448461

449462
throw this.NewExpressionBuildingException($"List is the wrong type");
@@ -1918,7 +1931,7 @@ private void QueryDumpDebugInfoToLog(Query query)
19181931
Type valueTupleType = _typeResolver.GetListElementType(funcResultType, true)!;
19191932
FieldInfo[] valueTupleFields = valueTupleType.GetFields(bfPublicInstance | BindingFlags.GetField);
19201933

1921-
Type cqlTupleType = _tupleBuilderCache.CreateOrGetTupleTypeFor(sourceListElementTypes.Zip(aliases));
1934+
Type cqlTupleType = _tupleBuilderCache.CreateOrGetTupleTypeFor(sourceListElementTypes.Zip(aliases).ToList());
19221935
PropertyInfo[] cqlTupleProperties = cqlTupleType.GetProperties(bfPublicInstance | BindingFlags.SetProperty);
19231936

19241937
Debug.Assert(valueTupleFields.Length > 0);
@@ -2354,6 +2367,55 @@ void throwCannotCastIfNoMatch(TypeConversion result)
23542367
throw this.NewExpressionBuildingException($"Cannot convert {input.Type} to {outputType}.");
23552368
}
23562369
}
2370+
2371+
/// <summary>
2372+
/// Pre-processes an expression tree to fix missing resultTypeSpecifier on AliasRef elements
2373+
/// by copying the type information from the source elements that define the aliases.
2374+
/// </summary>
2375+
/// <param name="elmExpression">The root ELM expression to process</param>
2376+
private void FixMissingAliasRefTypeSpecifiers(Elm.Expression elmExpression)
2377+
{
2378+
// First pass: Build dictionary of alias names to their source elements (with resultTypeSpecifier)
2379+
var aliasSources = new Dictionary<string, (Element sourceElement, TypeSpecifier sourceResultTypeSpecifier)>();
2380+
2381+
var aliasCollector = new ElmTreeWalker(node =>
2382+
{
2383+
switch (node)
2384+
{
2385+
case AliasedQuerySource aqs when !string.IsNullOrEmpty(aqs.alias) && aqs.expression?.resultTypeSpecifier != null:
2386+
aliasSources[aqs.alias] = (aqs, aqs.expression.resultTypeSpecifier);
2387+
break;
2388+
2389+
case LetClause let when !string.IsNullOrEmpty(let.identifier) && let.expression?.resultTypeSpecifier != null:
2390+
aliasSources[let.identifier] = (let, let.expression.resultTypeSpecifier);
2391+
break;
2392+
}
2393+
return true; // Continue walking children
2394+
});
2395+
2396+
aliasCollector.Start(elmExpression);
2397+
2398+
// Second pass: Find AliasRef elements without resultTypeSpecifier and copy it from the dictionary
2399+
var aliasRefFixer = new ElmTreeWalker(node =>
2400+
{
2401+
if (node is AliasRef aliasRef
2402+
&& !string.IsNullOrEmpty(aliasRef.name)
2403+
&& aliasRef.resultTypeSpecifier == null
2404+
&& aliasSources.TryGetValue(aliasRef.name, out var source))
2405+
{
2406+
_logger.LogDebug(
2407+
"Fixing missing resultTypeSpecifier for AliasRef named '{alias}' @ {aliasLocator}, originating from {sourceType} @ {sourceLocator}. {expressionBuilderContext}",
2408+
aliasRef.name, aliasRef.locator,
2409+
source.sourceElement.GetType().Name,
2410+
source.sourceElement.locator,
2411+
DebuggerView);
2412+
aliasRef.resultTypeSpecifier = source.sourceResultTypeSpecifier;
2413+
}
2414+
return true; // Continue walking children
2415+
});
2416+
2417+
aliasRefFixer.Start(elmExpression);
2418+
}
23572419
}
23582420

23592421
#endregion
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (c) 2025, Firely, 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.Primitives;
10+
using Hl7.Fhir.Model;
11+
12+
namespace Hl7.Cql.Compiler.Infrastructure;
13+
14+
internal static class ElmTupleTypeUtility
15+
{
16+
/// <summary>
17+
/// Determines whether two types are compatible for Union operations in ELM to LINQ conversion.
18+
/// This includes exact equality, assignability, and structural equivalence for tuple types.
19+
/// </summary>
20+
/// <param name="leftType">The left operand type.</param>
21+
/// <param name="rightType">The right operand type.</param>
22+
/// <returns><c>true</c> if the types are compatible for Union operations; otherwise, <c>false</c>.</returns>
23+
public static bool AreCompatibleForUnionOperation(Type leftType, Type rightType)
24+
{
25+
// First check for exact equality
26+
if (leftType == rightType)
27+
return true;
28+
29+
// Check if one type is assignable from the other (for polymorphic cases)
30+
if (leftType.IsAssignableFrom(rightType) || rightType.IsAssignableFrom(leftType))
31+
return true;
32+
33+
// Check for structural equivalence of tuple types
34+
if (AreElmTupleTypesStructurallyEquivalent(leftType, rightType))
35+
return true;
36+
37+
return false;
38+
}
39+
40+
/// <summary>
41+
/// Determines whether two types are structurally equivalent ELM tuple types.
42+
/// ELM tuple types are considered structurally equivalent if they have the same properties
43+
/// in the same order with compatible types.
44+
/// </summary>
45+
/// <param name="leftType">The left tuple type.</param>
46+
/// <param name="rightType">The right tuple type.</param>
47+
/// <returns><c>true</c> if both types are ELM tuple types and are structurally equivalent; otherwise, <c>false</c>.</returns>
48+
private static bool AreElmTupleTypesStructurallyEquivalent(Type leftType, Type rightType)
49+
{
50+
// Check if both types are tuple-like (derive from TupleBaseType or have tuple-like properties)
51+
if (!leftType.IsTupleBaseType() || !rightType.IsTupleBaseType())
52+
return false;
53+
54+
var leftProps = leftType.GetProperties();
55+
var rightProps = rightType.GetProperties();
56+
57+
// Check if they have the same number of properties
58+
if (leftProps.Length != rightProps.Length)
59+
return false;
60+
61+
// Check if each property has the same name and compatible type (order matters for tuples)
62+
for (int i = 0; i < leftProps.Length; i++)
63+
{
64+
var leftProp = leftProps[i];
65+
var rightProp = rightProps[i];
66+
67+
// Property names must match
68+
if (leftProp.Name != rightProp.Name)
69+
return false;
70+
71+
// For property types, check if they are the same or convertible
72+
if (!AreElmPropertyTypesCompatible(leftProp.PropertyType, rightProp.PropertyType))
73+
return false;
74+
}
75+
76+
return true;
77+
78+
// Determines whether two property types are compatible in the context of ELM tuple operations.
79+
// This includes exact matches, assignability, and known CQL/FHIR type conversions.
80+
static bool AreElmPropertyTypesCompatible(Type leftPropType, Type rightPropType)
81+
{
82+
// Exact match
83+
if (leftPropType == rightPropType)
84+
return true;
85+
86+
// Check assignability in both directions
87+
if (leftPropType.IsAssignableFrom(rightPropType) || rightPropType.IsAssignableFrom(leftPropType))
88+
return true;
89+
90+
// Special cases for known compatible CQL types
91+
// CqlDateTime and FhirDateTime are convertible
92+
if ((leftPropType == typeof(CqlDateTime) && rightPropType == typeof(FhirDateTime)||
93+
(rightPropType == typeof(CqlDateTime) && leftPropType == typeof(FhirDateTime))))
94+
return true;
95+
96+
// Add other known convertible pairs as needed
97+
98+
return false;
99+
}
100+
}
101+
}

Cql/Cql.Compiler/TupleBuilderCache.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,18 @@ public void AddTupleType(Type type)
3333
}
3434

3535
public bool TryFindTupleType(
36-
IEnumerable<(Type propType, string propName, string cqlName)> tupleProps3,
36+
IReadOnlyCollection<(Type propType, string propName, string cqlName)> tupleProps3,
3737
[NotNullWhen(true)]out Type? type)
3838
{
3939
type = _tupleTypeList
4040
.FirstOrDefault(tupleType =>
4141
{
42-
var isMatch = tupleProps3.All(
43-
tf => tupleType.GetProperty(tf.propName) is { PropertyType: { } tuplePropertyType }
44-
&& tuplePropertyType == tf.propType);
42+
var isMatch =
43+
tupleProps3.Count == tupleType.GetProperties().Length
44+
&&
45+
tupleProps3.All(tf =>
46+
tupleType.GetProperty(tf.propName) is { PropertyType: { } tuplePropertyType }
47+
&& tuplePropertyType == tf.propType);
4548
return isMatch;
4649
});
4750
return type != null;
@@ -70,7 +73,7 @@ public void Dispose()
7073
/// </summary>
7174
/// <param name="tupleProps">A readonly collection of property names with their corresponding types.</param>
7275
/// <returns>Gets the type that matches the properties.</returns>
73-
public Type CreateOrGetTupleTypeFor(IEnumerable<(Type propType, string cqlName)> tupleProps)
76+
public Type CreateOrGetTupleTypeFor(IReadOnlyCollection<(Type propType, string cqlName)> tupleProps)
7477
{
7578
HashSet<string> propNameDuplicates = new();
7679
List<(Type propType, string propName, string cqlName)> tupleProps3 =
@@ -83,7 +86,6 @@ public Type CreateOrGetTupleTypeFor(IEnumerable<(Type propType, string cqlName)>
8386
return (tupleProp.propType, propName, tupleProp.cqlName);
8487
})
8588
.ToList();
86-
8789
if (!_tupleTypeCache.TryFindTupleType(tupleProps3, out var tupleType))
8890
{
8991
tupleType = DefineType(tupleProps3);

0 commit comments

Comments
 (0)