diff --git a/Cql/CodeGeneration.NET/CSharpLibrarySetToStreamsWriter.cs b/Cql/CodeGeneration.NET/CSharpLibrarySetToStreamsWriter.cs index dd336da2f..c1ab69598 100644 --- a/Cql/CodeGeneration.NET/CSharpLibrarySetToStreamsWriter.cs +++ b/Cql/CodeGeneration.NET/CSharpLibrarySetToStreamsWriter.cs @@ -261,7 +261,7 @@ private void WriteCqlTupleMetadataProperties( foreach (var (propertyName, signature) in tupleMetadataBuilder.GetAllTupleMetadataPropertySignatures()) { var types = string.Join(", ", signature.Select(t => $"typeof({_typeToCSharpConverter.ToCSharp(t.Type)})")); - var names = string.Join(", ", signature.Select(t => t.Name.QuoteString())); + var names = string.Join(", ", signature.Select(t => t.PropName.QuoteString())); writer.WriteLine(indentLevel, $"private static CqlTupleMetadata {propertyName} = new("); writer.WriteLine(indentLevel+1, $"[{types}],"); writer.WriteLine(indentLevel+1, $"[{names}]);"); diff --git a/Cql/CodeGeneration.NET/ExpressionToCSharpConverter.cs b/Cql/CodeGeneration.NET/ExpressionToCSharpConverter.cs index f3b065a31..d15039135 100644 --- a/Cql/CodeGeneration.NET/ExpressionToCSharpConverter.cs +++ b/Cql/CodeGeneration.NET/ExpressionToCSharpConverter.cs @@ -50,7 +50,7 @@ public Context WithOverride(Func? indentFn = null, Func? u public Context WithDoUseIndent() => WithOverride(useIndentFn: _ => true); - public string GetTupleMetadataPropertyName(IReadOnlyCollection<(string Name, Type Type)> tupleProperties) => + public string GetTupleMetadataPropertyName(IReadOnlyCollection<(Type Type, string Name)> tupleProperties) => _tupleMetadataBuilder.GetTupleMetadataPropertyName(tupleProperties); } diff --git a/Cql/CodeGeneration.NET/TupleMetadataBuilder.cs b/Cql/CodeGeneration.NET/TupleMetadataBuilder.cs index 9276d37ec..cea3f6e2b 100644 --- a/Cql/CodeGeneration.NET/TupleMetadataBuilder.cs +++ b/Cql/CodeGeneration.NET/TupleMetadataBuilder.cs @@ -8,23 +8,22 @@ using System; using System.Collections.Generic; using Hl7.Cql.Abstractions.Infrastructure; -using Hl7.Cql.Runtime; +using Hl7.Cql.Primitives; namespace Hl7.Cql.CodeGeneration.NET; internal class TupleMetadataBuilder { - private readonly Dictionary> _signaturesByHash = new(); + private readonly Dictionary> _signaturesByHash = new(); public string GetTupleMetadataPropertyName( - IReadOnlyCollection<(string Name, Type Type)> tupleItemsSignature) + IReadOnlyCollection<(Type Type, string PropName)> tupleProps) { - var propName = CqlTupleMetadata.BuildSignatureHashString(tupleItemsSignature); - _signaturesByHash[propName] = tupleItemsSignature; + var propName = CqlTupleMetadata.BuildSignatureHashString(tupleProps, CqlTupleMetadata.PropertyPrefix); + _signaturesByHash[propName] = tupleProps; return propName; } - public IReadOnlyCollection<(string PropertyName, IReadOnlyCollection<(string Name, Type Type)> Signature)> GetAllTupleMetadataPropertySignatures() => - _signaturesByHash - .SelectToArray(kv => (kv.Key, kv.Value)); + public IReadOnlyCollection<(string PropertyName, IReadOnlyCollection<(Type Type, string PropName)> Signature)> GetAllTupleMetadataPropertySignatures() => + _signaturesByHash.SelectToArray(kv => (kv.Key, kv.Value)); } \ No newline at end of file diff --git a/Cql/CodeGeneration.NET/TypeToCSharpConverter.cs b/Cql/CodeGeneration.NET/TypeToCSharpConverter.cs index 2f09965fc..cd7528297 100644 --- a/Cql/CodeGeneration.NET/TypeToCSharpConverter.cs +++ b/Cql/CodeGeneration.NET/TypeToCSharpConverter.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using Hl7.Cql.Abstractions.Infrastructure; +using Hl7.Cql.Primitives; namespace Hl7.Cql.CodeGeneration.NET; @@ -35,18 +36,14 @@ private TextWriterFormattableString FormatTypeNameAsTuple(ITypeNameCSharpFormatC return formatTypeNameAsTuple; } - public IEnumerable<(string Name, Type Type)> GetTupleProperties(Type type) + public IEnumerable<(Type Type, string Name)> GetTupleProperties(Type type) { - var length = type.GetProperties().Length; - for (var i = 0; i < length; i++) - { - var prop = type.GetProperties()[i]; - yield return (prop.Name, prop.PropertyType); - } + var properties = type.GetProperties(); + return properties.Select(p => (p.PropertyType, p.Name)); } public bool ShouldUseTupleType(Type type) => - _useCSharpValueTuples && type.Name.StartsWith("Tuple_"); // REVIEW: This is a heuristic, and may not be correct in all cases. + _useCSharpValueTuples && type.IsTupleBaseType(); public string ToCSharp(Type type) { diff --git a/Cql/CoreTests/Infrastructure/CSharpFormatterTests.cs b/Cql/CoreTests/Abstractions/CSharpFormatterTests.cs similarity index 93% rename from Cql/CoreTests/Infrastructure/CSharpFormatterTests.cs rename to Cql/CoreTests/Abstractions/CSharpFormatterTests.cs index 6d94cf44d..944e6e5df 100644 --- a/Cql/CoreTests/Infrastructure/CSharpFormatterTests.cs +++ b/Cql/CoreTests/Abstractions/CSharpFormatterTests.cs @@ -10,7 +10,7 @@ using Hl7.Cql.Compiler.Infrastructure; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace CoreTests.Infrastructure; +namespace CoreTests.Abstractions; [TestClass] [TestCategory("UnitTest")] @@ -83,7 +83,7 @@ public void TypeToCSharpString_ShouldReturnCorrectResults() Assert.AreEqual( expected: "System.Collections.Generic.IDictionary<, >", - actual: typeof(IDictionary<,>).ToCSharpString(typeFormatterOptions: new(NoGenericTypeParameterNames:true))); + actual: typeof(IDictionary<,>).ToCSharpString(typeFormatterOptions: new(NoGenericTypeParameterNames: true))); Assert.AreEqual( expected: "IDictionary", @@ -98,13 +98,13 @@ public void TypeToCSharpString_ShouldReturnCorrectResults() typeFormatterOptions: new( NoNamespaces: false, UseKeywords: true, - GenericArgumentTokens: CSharpTokens.GenericArguments with { Separator = ","}))); + GenericArgumentTokens: CSharpTokens.GenericArguments with { Separator = "," }))); Assert.AreEqual( - expected: "CoreTests.Infrastructure.EmptyStruct+Nested1+Nested2", + expected: "CoreTests.Abstractions.EmptyStruct+Nested1+Nested2", actual: typeof(EmptyStruct.Nested1.Nested2).ToCSharpString( typeFormatterOptions: new( - NestedTypeSeparator:"+"))); + NestedTypeSeparator: "+"))); Assert.AreEqual( expected: "System.Nullable", @@ -123,7 +123,7 @@ public void TypeToCSharpString_ShouldReturnCorrectResults() [TestMethod] public void MethodToCSharpString_GenericGenericDefinition_ShouldReturnCorrectResults() { - MethodInfo m = ReflectionUtility.GenericMethodDefinitionOf(fnToMethodCall: () => default(TypeExtensionsTests.INonGenericInterface)!.GenericMethod(default!, default!, default!)); + MethodInfo m = ReflectionUtility.GenericMethodDefinitionOf(fnToMethodCall: () => default(TypeExtensionsTests.INonGenericInterface)!.GenericMethod(default!, default!, default!)); Assert.AreEqual( expected: "IList GenericMethod(T1 a, T2[] b, IEnumerable[] c)", @@ -153,12 +153,12 @@ public void MethodToCSharpString_NonGeneric_ShouldReturnCorrectResults() tw = new TestTextWriter(new StringWriter()); var methodCSharpFormat = new MethodCSharpFormat( MethodFormat: method => $"function {method.Name}{method.GenericArguments}{method.Parameters}: {method.ReturnType};", - ParameterFormat: new ( + ParameterFormat: new( ParameterFormat: parameter => $"{parameter.Name}: {parameter.Type}", TypeFormat: new( - UseKeywords:true, - NoNamespaces:true)), - ParameterTokens: CSharpTokens.Parameters with { Separator = "; "} + UseKeywords: true, + NoNamespaces: true)), + ParameterTokens: CSharpTokens.Parameters with { Separator = "; " } ); methodCSharpFormat.WriteTo(m, tw); Assert.AreEqual( diff --git a/Cql/CoreTests/Infrastructure/TypeExtensionsTests.cs b/Cql/CoreTests/Abstractions/TypeExtensionsTests.cs similarity index 97% rename from Cql/CoreTests/Infrastructure/TypeExtensionsTests.cs rename to Cql/CoreTests/Abstractions/TypeExtensionsTests.cs index 8f489a71b..e99ffd459 100644 --- a/Cql/CoreTests/Infrastructure/TypeExtensionsTests.cs +++ b/Cql/CoreTests/Abstractions/TypeExtensionsTests.cs @@ -5,7 +5,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; #pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type -namespace CoreTests.Infrastructure; +namespace CoreTests.Abstractions; [TestClass] @@ -329,12 +329,12 @@ IList GenericMethod( T1 a, T2[] b, IEnumerable[] c) - where T1: struct, IComparable - where T2: notnull, new() - where T3: class, new(); + where T1 : struct, IComparable + where T2 : notnull, new() + where T3 : class, new(); } - public abstract class MyGenericClassBase : IGenericInterface + public abstract class MyGenericClassBase : IGenericInterface { public void Method(T value) { @@ -359,9 +359,9 @@ public readonly record struct Nested1 { public readonly record struct Nested2 { } - public readonly record struct GenericNested2 { } + public readonly record struct GenericNested2 { } public delegate TOut NestedFunc(TIn input) - where TIn: notnull; + where TIn : notnull; }; } \ No newline at end of file diff --git a/Cql/CoreTests/Fhir/DataSourceTests.cs b/Cql/CoreTests/Fhir/DataSourceTests.cs index 292bb9420..db2212cc5 100644 --- a/Cql/CoreTests/Fhir/DataSourceTests.cs +++ b/Cql/CoreTests/Fhir/DataSourceTests.cs @@ -46,7 +46,6 @@ public void FiltersOnDefaultProp() } [TestMethod] - [Ignore("Will be fixed in PR 614")] public void FiltersOnSpecificProp() { var dr = buildDataSource(); diff --git a/Cql/CoreTests/Primitives/TypeExtensionsTests.cs b/Cql/CoreTests/Primitives/TypeExtensionsTests.cs new file mode 100644 index 000000000..0684ed304 --- /dev/null +++ b/Cql/CoreTests/Primitives/TypeExtensionsTests.cs @@ -0,0 +1,65 @@ +using System; +using Hl7.Cql.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CoreTests.Primitives; + +[TestClass] +public class TypeExtensionsTests +{ + [TestMethod] + public void IsCqlInterval_ShouldReturnTrueAndSetElementType_WhenTypeIsCqlInterval() + { + // Arrange + Type type = typeof(CqlInterval); + Type elementType; + + // Act + bool result = type.IsCqlInterval(out elementType); + + // Assert + Assert.IsTrue(result); + Assert.AreEqual(typeof(int), elementType); + } + + [TestMethod] + public void IsCqlInterval_ShouldReturnFalseAndSetElementTypeToNull_WhenTypeIsNotCqlInterval() + { + // Arrange + Type type = typeof(string); + Type elementType; + + // Act + bool result = type.IsCqlInterval(out elementType); + + // Assert + Assert.IsFalse(result); + Assert.IsNull(elementType); + } + + [TestMethod] + public void IsCqlValueTuple_ShouldReturnTrue_WhenTypeIsCqlValueTuple() + { + // Arrange + Type type = typeof(ValueTuple); + + // Act + bool result = type.IsCqlValueTuple(); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + public void IsCqlValueTuple_ShouldReturnFalse_WhenTypeIsNotCqlValueTuple() + { + // Arrange + Type type = typeof(string); + + // Act + bool result = type.IsCqlValueTuple(); + + // Assert + Assert.IsFalse(result); + } +} diff --git a/Cql/CoreTests/Tuples/CqlTupleTests.cs b/Cql/CoreTests/Tuples/CqlTupleTests.cs index 7d3a3b5b9..2dac74ca1 100644 --- a/Cql/CoreTests/Tuples/CqlTupleTests.cs +++ b/Cql/CoreTests/Tuples/CqlTupleTests.cs @@ -3,11 +3,11 @@ using System.Text.Json; using Microsoft.VisualStudio.TestTools.UnitTesting; using Hl7.Cql.Fhir; -using Hl7.Cql.Runtime; using Hl7.Cql.Runtime.Serialization; using System.IO; using System.Runtime.Loader; using Hl7.Cql.Packaging; +using Hl7.Cql.Primitives; namespace CoreTests.Tuples; diff --git a/Cql/Cql.Abstractions/Cql.Abstractions.csproj b/Cql/Cql.Abstractions/Cql.Abstractions.csproj index de36a10a4..29dcf980e 100644 --- a/Cql/Cql.Abstractions/Cql.Abstractions.csproj +++ b/Cql/Cql.Abstractions/Cql.Abstractions.csproj @@ -11,13 +11,17 @@ - - + + + + + + diff --git a/Cql/Cql.Abstractions/Infrastructure/TypeExtensions.cs b/Cql/Cql.Abstractions/Infrastructure/TypeExtensions.cs index 042840bd7..533b96b57 100644 --- a/Cql/Cql.Abstractions/Infrastructure/TypeExtensions.cs +++ b/Cql/Cql.Abstractions/Infrastructure/TypeExtensions.cs @@ -7,7 +7,6 @@ */ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; diff --git a/Cql/Cql.Comparers/CqlComparers.cs b/Cql/Cql.Comparers/CqlComparers.cs index dc4faba8c..26ae2cee3 100644 --- a/Cql/Cql.Comparers/CqlComparers.cs +++ b/Cql/Cql.Comparers/CqlComparers.cs @@ -14,7 +14,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -using System.Xml.Schema; using JetBrains.Annotations; namespace Hl7.Cql.Comparers @@ -24,7 +23,6 @@ namespace Hl7.Cql.Comparers /// internal sealed class CqlComparers : ICqlComparer { - /* * * Equivalence : https://cql.hl7.org/04-logicalspecification.html#equivalent @@ -42,8 +40,8 @@ internal sealed class CqlComparers : ICqlComparer (left, right) switch { (null, null) => true, - (null, _) => false, - (_, null) => false, + (null, _) => false, + (_, null) => false, _ => null }; @@ -87,8 +85,9 @@ public CqlComparers() Comparers.TryAdd(typeof(CqlDate), new InterfaceCqlComparer()); Comparers.TryAdd(typeof(CqlTime), new InterfaceCqlComparer()); Comparers.TryAdd(typeof(CqlDateTime), new InterfaceCqlComparer()); - Comparers.TryAdd(typeof(TupleBaseType), new TupleBaseTypeComparer(this)); - Comparers.TryAdd(typeof(ITuple), new TupleComparer(this)); + + Comparers.TryAdd(typeof(TupleBaseType), new TupleBaseTypeComparer(this)); // Legacy, will be removed! + Comparers.TryAdd(typeof(ITuple), new CqlTupleTypeComparer(this)); ComparerFactories.TryAdd(typeof(Nullable<>), (type, @this) => { @@ -100,7 +99,7 @@ public CqlComparers() { var genericArguments = type.GetGenericArguments(); var genericType = typeof(KeyValuePairComparer<,>).MakeGenericType(genericArguments); - var cqlComparer = (ICqlComparer)Activator.CreateInstance(genericType)!; + var cqlComparer = (ICqlComparer)Activator.CreateInstance(genericType, args: [this])!; return cqlComparer; }); } @@ -192,18 +191,16 @@ public CqlComparers Unregister(Type type) // if x or y is null it must return null and if both are null then it's a match // if we return 1 or -1 when only 1 side is null then we hit a lot of issues with Stratification: Race - Two or More Races on a lot of measures // because it expects null/false but gets true because 1 was returned (x null, y = 2) so 2 > null => return 1 - if (x == null) + switch (x, y) { - if (y == null) - return 0; - else return null; + case (null, null): return 0; + case (not null, not null): break; + default: return null; } - else if (y == null) - return null; bool xySwapped = false; - var xType = x.GetType(); - var yType = y.GetType(); + var xType = GetKeyTypeForComparers(x); + var yType = GetKeyTypeForComparers(y); if (xType != yType) { // if x and y are not the same type, we prioritize them based on the following order: @@ -217,10 +214,6 @@ public CqlComparers Unregister(Type type) } ICqlComparer ? comparer = null; - - if (x is ITuple) // Should cover all value types - xType = typeof(ITuple); - if (Comparers.TryGetValue(xType, out ICqlComparer? c)) { comparer = c; @@ -239,14 +232,11 @@ public CqlComparers Unregister(Type type) comparer = enumerableComparer; } } - else if (x is TupleBaseType && Comparers.TryGetValue(typeof(TupleBaseType), out ICqlComparer? tupleComparer)) - { - comparer = tupleComparer; - } - else if (x is IEnumerable && Comparers.TryGetValue(typeof(IEnumerable), out ICqlComparer? listComarper)) + else if (x is IEnumerable && Comparers.TryGetValue(typeof(IEnumerable), out ICqlComparer? listComparer)) { - comparer = listComarper; + comparer = listComparer; } + if (comparer != null) { var result = comparer.Compare(x, y, precision); @@ -260,24 +250,15 @@ public CqlComparers Unregister(Type type) /// public bool Equivalent(object? x, object? y, string? precision) { - if (x == null) - { - if (y == null) - return true; - else return false; - } - else if (y == null) - return false; + if (EquivalentOnNullsOnly(x, y) is { } r) + return r; + + var xType = GetKeyTypeForComparers(x); - var xType = x.GetType(); if (Comparers.TryGetValue(xType, out ICqlComparer? comparer)) { return comparer.Equivalent(x, y, precision); } - else if (x is TupleBaseType tuple && Comparers.TryGetValue(typeof(TupleBaseType), out ICqlComparer? tupleComparer)) - { - return tupleComparer.Equivalent(x, y, precision); - } else { if (xType.IsGenericType) @@ -294,20 +275,32 @@ public bool Equivalent(object? x, object? y, string? precision) throw new ArgumentException($"Cannot check equivalence for type {xType.Name}"); } + /// + /// Collapses derived types to their bases, since this makes it easier to find the comparer by the exact type. + /// + private static Type GetKeyTypeForComparers(object? x) + { + var type = x switch + { + TupleBaseType => typeof(TupleBaseType), // Tuple types generated in the LINQ expressions by the TupleBuilderCache + ITuple => typeof(ITuple), // .NET tuples (e.g. System.ValueTuple<...>) used in generated libraries + _ => x!.GetType() + }; + return type; + } + /// public int GetHashCode(object? x) { if (x == null) return typeof(object).GetHashCode(); - var xType = x.GetType(); + + var xType = GetKeyTypeForComparers(x); + if (Comparers.TryGetValue(xType, out ICqlComparer? comparer)) { return comparer.GetHashCode(x); } - else if (x is TupleBaseType && Comparers.TryGetValue(typeof(TupleBaseType), out ICqlComparer? tupleComparer)) - { - return tupleComparer.GetHashCode(x); - } else if (x is IEnumerable enumerable) { int hash = typeof(IEnumerable).GetHashCode(); diff --git a/Cql/Cql.Comparers/CqlConceptCqlComparer.cs b/Cql/Cql.Comparers/CqlConceptCqlComparer.cs index e4373fecc..98c942c54 100644 --- a/Cql/Cql.Comparers/CqlConceptCqlComparer.cs +++ b/Cql/Cql.Comparers/CqlConceptCqlComparer.cs @@ -7,7 +7,6 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE */ -using System; using System.Collections.Generic; using Hl7.Cql.Abstractions; using Hl7.Cql.Primitives; diff --git a/Cql/Cql.Comparers/CqlTupleTypeComparer.cs b/Cql/Cql.Comparers/CqlTupleTypeComparer.cs new file mode 100644 index 000000000..7a7cfa951 --- /dev/null +++ b/Cql/Cql.Comparers/CqlTupleTypeComparer.cs @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023, NCQA and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE + */ + + +using Hl7.Cql.Abstractions; +using System.Runtime.CompilerServices; +using Hl7.Cql.Primitives; + +namespace Hl7.Cql.Comparers +{ + internal class CqlTupleTypeComparer(ICqlComparer memberComparer) : ICqlComparer, ICqlComparer + { + private static readonly int FallbackHashCode = typeof(ITuple).GetHashCode() ^ 098174506; + + public int? Compare(ITuple? x, ITuple? y, string? precision = null) + { + if (x == null || y == null) + return null; + + // Check the "type" via the metadata + if (x.Length == 0 || x.Length != y.Length) + return null; + + var xMetadata = x[0] as CqlTupleMetadata; + var yMetadata = y[0] as CqlTupleMetadata; + if (xMetadata == null || xMetadata != yMetadata) + return null; + + // Compare the items on the tuple + for (int i = 1; i < x.Length; i++) + { + var compare = memberComparer.Compare(x[i], y[i], precision); + if (compare is null or not 0) + return compare; + } + + return 0; + } + + public int GetHashCode(ITuple? obj) => + obj?.GetHashCode() ?? FallbackHashCode; + + public bool? Equals(ITuple? x, ITuple? y, string? precision = null) => + Compare(x, y, precision) == 0; + + public bool Equivalent(ITuple? x, ITuple? y, string? precision = null) + { + if (CqlComparers.EquivalentOnNullsOnly(x, y) is { } r) + return r; + + // Check the "type" via the metadata + if (x!.Length == 0 || x.Length != y!.Length) + return false; + + var xMetadata = x[0] as CqlTupleMetadata; + var yMetadata = y[0] as CqlTupleMetadata; + if (xMetadata == null || xMetadata != yMetadata) + return false; + + // Compare the items on the tuple + for (int i = 1; i < x.Length; i++) + { + var equivalent = memberComparer.Equivalent(x[i], y[i], precision); + if (!equivalent) + return false; + } + + return true; + } + + int? ICqlComparer.Compare(object? x, object? y, string? precision) => + Compare(x as ITuple, y as ITuple, precision); + + bool? ICqlComparer.Equals(object? x, object? y, string? precision) => + Equals(x as ITuple, y as ITuple, precision); + + bool IEquivalenceComparer.Equivalent(object? x, object? y, string? precision) => + Equivalent(x as ITuple, y as ITuple, precision); + + int ICqlComparer.GetHashCode(object? obj) => + GetHashCode(obj as ITuple); + } +} + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member diff --git a/Cql/Cql.Comparers/DefaultCqlComparer.cs b/Cql/Cql.Comparers/DefaultCqlComparer.cs index b2b288f75..d7226f9ca 100644 --- a/Cql/Cql.Comparers/DefaultCqlComparer.cs +++ b/Cql/Cql.Comparers/DefaultCqlComparer.cs @@ -7,7 +7,6 @@ */ using Hl7.Cql.Abstractions; -using System; using System.Collections.Generic; namespace Hl7.Cql.Comparers diff --git a/Cql/Cql.Comparers/KeyValuePairComparer.cs b/Cql/Cql.Comparers/KeyValuePairComparer.cs index c9b54b073..2a64722e2 100644 --- a/Cql/Cql.Comparers/KeyValuePairComparer.cs +++ b/Cql/Cql.Comparers/KeyValuePairComparer.cs @@ -12,7 +12,7 @@ namespace Hl7.Cql.Comparers; -internal class KeyValuePairComparer : ICqlComparer, ICqlComparer> +internal class KeyValuePairComparer(ICqlComparer cqlComparer) : ICqlComparer, ICqlComparer> { /// public int? Compare(object? x, object? y, string? precision = null) @@ -30,9 +30,9 @@ internal class KeyValuePairComparer : ICqlComparer, ICqlComparer x, KeyValuePair y, string? precision = null) => - Comparer.Default.Compare(x.Key, y.Key) switch + cqlComparer.Compare(x.Key, y.Key, precision) switch { - 0 => Comparer.Default.Compare(x.Value, y.Value), + 0 => cqlComparer.Compare(x.Value, y.Value, precision), var i => i }; @@ -45,8 +45,11 @@ internal class KeyValuePairComparer : ICqlComparer, ICqlComparer x, KeyValuePair y, string? precision = null) => - Comparer.Default.Compare(x.Key, y.Key) == 0 - && Comparer.Default.Compare(x.Value, y.Value) == 0; + cqlComparer.Equals(x.Key, y.Key, precision) switch + { + true => cqlComparer.Equals(x.Value, y.Value, precision), + var b => b + }; /// public bool Equivalent(object? x, object? y, string? precision = null) => @@ -59,7 +62,9 @@ public bool Equivalent(KeyValuePair x, KeyValuePair /// public int GetHashCode(KeyValuePair x) => - x.GetHashCode(); + HashCode.Combine( + cqlComparer.GetHashCode(x.Key), + cqlComparer.GetHashCode(x.Value)); /// public int GetHashCode(object? x) => diff --git a/Cql/Cql.Comparers/ListEqualComparer.cs b/Cql/Cql.Comparers/ListEqualComparer.cs index bf1ffba97..4432c16f2 100644 --- a/Cql/Cql.Comparers/ListEqualComparer.cs +++ b/Cql/Cql.Comparers/ListEqualComparer.cs @@ -10,7 +10,6 @@ using System; using Hl7.Cql.Abstractions; using System.Collections; -using System.Reflection.Metadata.Ecma335; namespace Hl7.Cql.Comparers { diff --git a/Cql/Cql.Comparers/TupleBaseTypeComparer.cs b/Cql/Cql.Comparers/TupleBaseTypeComparer.cs index 28abfd710..df0912b8b 100644 --- a/Cql/Cql.Comparers/TupleBaseTypeComparer.cs +++ b/Cql/Cql.Comparers/TupleBaseTypeComparer.cs @@ -1,10 +1,10 @@ /* - * Copyright (c) 2023, NCQA and contributors - * See the file CONTRIBUTORS for details. - * - * This file is licensed under the BSD 3-Clause license - * available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE - */ +* Copyright (c) 2023, NCQA and contributors +* See the file CONTRIBUTORS for details. +* +* This file is licensed under the BSD 3-Clause license +* available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE +*/ using Hl7.Cql.Abstractions; using Hl7.Cql.Primitives; diff --git a/Cql/Cql.Comparers/TupleComparer.cs b/Cql/Cql.Comparers/TupleComparer.cs deleted file mode 100644 index 1979b48d6..000000000 --- a/Cql/Cql.Comparers/TupleComparer.cs +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2024, NCQA and contributors - * See the file CONTRIBUTORS for details. - * - * This file is licensed under the BSD 3-Clause license - * available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE - */ - - -using System.Runtime.CompilerServices; -using Hl7.Cql.Abstractions; - -namespace Hl7.Cql.Comparers; - -internal class TupleComparer(ICqlComparer memberComparer) : ICqlComparer, ICqlComparer -{ - public int? Compare( - ITuple? x, - ITuple? y, - string? precision) - { - if (x == null || y == null) - return null; - - if (x.Length != y.Length) - return null; - - var xType = x.GetType(); - var yType = y.GetType(); - if (xType != yType) - return null; - - for (int i = 0; i < x.Length; i++) - { - var compare = memberComparer.Compare(x[i], y[i], precision); - if (compare is null or not 0) - return compare; - } - - return 0; - } - - public int GetHashCode(ITuple? obj) => - obj?.GetHashCode() ?? typeof(ITuple).GetHashCode() ^ 098174506; - - public bool? Equals(ITuple? x, ITuple? y, string? precision = null) => - Compare(x, y, null) == 0; - - public bool Equivalent( - ITuple? x, - ITuple? y, - string? precision) - { - if (CqlComparers.EquivalentOnNullsOnly(x, y) is { } r) - return r; - - if (x!.Length != y!.Length) - return false; - - var xType = x.GetType(); - var yType = y.GetType(); - if (xType != yType) - return false; - - for (int i = 0; i < x.Length; i++) - { - if (!memberComparer.Equivalent(x[i], y[i], precision)) - return false; - } - - return true; - } - - public bool Equivalent(object? x, object? y, string? precision) => - Equivalent(x as ITuple, y as ITuple, precision); - - public bool? Equals(object? x, object? y, string? precision) => - Equals(x as ITuple, y as ITuple, precision); - - public int? Compare(object? x, object? y, string? precision) => - Compare(x as ITuple, y as ITuple, precision); - - public int GetHashCode(object? x) => GetHashCode(x as ITuple); -} \ No newline at end of file diff --git a/Cql/Cql.Compiler/ExpressionBuilderContext.TypeFor.cs b/Cql/Cql.Compiler/ExpressionBuilderContext.TypeFor.cs index 6213600ab..37e27b449 100644 --- a/Cql/Cql.Compiler/ExpressionBuilderContext.TypeFor.cs +++ b/Cql/Cql.Compiler/ExpressionBuilderContext.TypeFor.cs @@ -235,22 +235,20 @@ private Type TupleTypeFor(Elm.Tuple tuple, Func? changeType = null) private Type TupleTypeFor((string name, TypeSpecifier elementType)[] elements, Func? changeType) { - Dictionary elementInfo = elements! - .ToDictionary( - el => el.name, - el => - { - if (el.elementType == null) - throw this.NewExpressionBuildingException( - $"Tuple element {el.name} has a null {nameof(el.elementType)} property. This property is required."); + var tupleFields = elements! + .Select(el => + { + if (el.elementType == null) + throw this.NewExpressionBuildingException( + $"Tuple element {el.name} has a null {nameof(el.elementType)} property. This property is required."); - var type = TypeFor(el.elementType)!; - if (changeType != null) - type = changeType(type); + var type = TypeFor(el.elementType)!; + if (changeType != null) + type = changeType(type); - return type; - }); + return (type, el.name); + }); - return _tupleBuilderCache.CreateOrGetTupleTypeFor(elementInfo); + return _tupleBuilderCache.CreateOrGetTupleTypeFor(tupleFields); } } \ No newline at end of file diff --git a/Cql/Cql.Compiler/ExpressionBuilderContext.cs b/Cql/Cql.Compiler/ExpressionBuilderContext.cs index 1b801e1d7..6d21dcd61 100644 --- a/Cql/Cql.Compiler/ExpressionBuilderContext.cs +++ b/Cql/Cql.Compiler/ExpressionBuilderContext.cs @@ -1866,10 +1866,6 @@ private void QueryDumpDebugInfoToLog(Query query) Type[] sourceListElementTypes = promotedSourceExpressions .SelectToArray(pse => _typeResolver.GetListElementType(pse.Type, true)!); - var aliasAndElementTypes = aliases - .Zip(sourceListElementTypes, (alias, elementType) => (alias, elementType)) - .ToDictionary(t => t.alias, t => t.elementType); - // IEnumerable<(A,B,C) var funcResultType = crossJoinedValueTupleResultsExpression.Type; @@ -1879,7 +1875,7 @@ private void QueryDumpDebugInfoToLog(Query query) Type valueTupleType = _typeResolver.GetListElementType(funcResultType, true)!; FieldInfo[] valueTupleFields = valueTupleType.GetFields(bfPublicInstance | BindingFlags.GetField); - Type cqlTupleType = _tupleBuilderCache.CreateOrGetTupleTypeFor(aliasAndElementTypes); + Type cqlTupleType = _tupleBuilderCache.CreateOrGetTupleTypeFor(sourceListElementTypes.Zip(aliases)); PropertyInfo[] cqlTupleProperties = cqlTupleType.GetProperties(bfPublicInstance | BindingFlags.SetProperty); Debug.Assert(valueTupleFields.Length > 0); diff --git a/Cql/Cql.Compiler/IBuilderContext.cs b/Cql/Cql.Compiler/IBuilderContext.cs index 075e5cda2..c31d99e09 100644 --- a/Cql/Cql.Compiler/IBuilderContext.cs +++ b/Cql/Cql.Compiler/IBuilderContext.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using Hl7.Cql.Abstractions; +using Hl7.Cql.Primitives; namespace Hl7.Cql.Compiler; diff --git a/Cql/Cql.Compiler/Infrastructure/TypeExtensions.cs b/Cql/Cql.Compiler/Infrastructure/TypeExtensions.cs deleted file mode 100644 index 794ec181d..000000000 --- a/Cql/Cql.Compiler/Infrastructure/TypeExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2024, NCQA and contributors - * See the file CONTRIBUTORS for details. - * - * This file is licensed under the BSD 3-Clause license - * available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE - */ -using System; -using System.Diagnostics.CodeAnalysis; -using Hl7.Cql.Primitives; - -namespace Hl7.Cql.Compiler.Infrastructure; - -internal static class TypeExtensions -{ - public static bool IsCqlInterval(this Type t, [NotNullWhen(true)] out Type? elementType) - { - if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(CqlInterval<>)) - { - elementType = t.GetGenericArguments()[0]; - return true; - } - elementType = null; - return false; - } -} \ No newline at end of file diff --git a/Cql/Cql.Compiler/TupleBuilderCache.cs b/Cql/Cql.Compiler/TupleBuilderCache.cs index 1775e957d..4ec077e86 100644 --- a/Cql/Cql.Compiler/TupleBuilderCache.cs +++ b/Cql/Cql.Compiler/TupleBuilderCache.cs @@ -8,6 +8,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -24,75 +26,93 @@ namespace Hl7.Cql.Compiler; internal class TupleBuilderCache : IDisposable { private readonly ILogger _logger; - private readonly List _tupleTypeList; + private readonly TupleTypeCache _tupleTypeCache; private readonly ModuleBuilder _moduleBuilder; - private readonly string _tupleBuilderCacheName; + private const string TupleBuilderCacheName = "TemporaryTupleAssembly"; + + private class TupleTypeCache + { + private readonly List _tupleTypeList = []; + + public void AddTupleType(Type type) + { + if (_tupleTypeList.Contains(type)) + throw new ArgumentException($"Type {type.Name} already exists", nameof(type)); + _tupleTypeList.Add(type); + } + + public bool TryFindTupleType( + IEnumerable<(Type propType, string propName, string cqlName)> tupleProps3, + [NotNullWhen(true)]out Type? type) + { + type = _tupleTypeList + .FirstOrDefault(tupleType => + { + var isMatch = tupleProps3.All( + tf => tupleType.GetProperty(tf.propName) is { PropertyType: { } tuplePropertyType } + && tuplePropertyType == tf.propType); + return isMatch; + }); + return type != null; + } + } /// public TupleBuilderCache( ILogger logger) { _logger = logger; - _tupleBuilderCacheName = $"Tuples{Guid.NewGuid():N}"; - _logger.LogInformation("Creating tuple type cache {name}", _tupleBuilderCacheName); + _logger.LogInformation("Creating scoped tuple builder cache {name}", TupleBuilderCacheName); - var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(_tupleBuilderCacheName), AssemblyBuilderAccess.RunAndCollect); - _tupleTypeList = []; - _moduleBuilder = assemblyBuilder.DefineDynamicModule(_tupleBuilderCacheName); + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(TupleBuilderCacheName), AssemblyBuilderAccess.RunAndCollect); + _tupleTypeCache = new(); + _moduleBuilder = assemblyBuilder.DefineDynamicModule(TupleBuilderCacheName); } public void Dispose() { - _logger.LogInformation("Disposing tuple type cache {name}", _tupleBuilderCacheName); + _logger.LogInformation("Disposing scoped tuple builder cache {name}", TupleBuilderCacheName); } /// /// Creates or gets from the cache, a tuple type for the specified property names and types. /// - /// A readonly collection of property names with their corresponding types. + /// A readonly collection of property names with their corresponding types. /// Gets the type that matches the properties. - public Type CreateOrGetTupleTypeFor(IReadOnlyDictionary propertyNamesAndTypes) + public Type CreateOrGetTupleTypeFor(IEnumerable<(Type propType, string cqlName)> tupleProps) { - var normalizedProperties = propertyNamesAndTypes - .SelectToArray(kvp => + HashSet propNameDuplicates = new(); + List<(Type propType, string propName, string cqlName)> tupleProps3 = + tupleProps + .Select(tupleProp => { - var propName = ExpressionBuilderContext.NormalizeIdentifier(kvp.Key); - var propType = kvp.Value; - return (propName, propType); - }); + var propName = ExpressionBuilderContext.NormalizeIdentifier(tupleProp.cqlName); + if (!propNameDuplicates.Add(propName)) + throw new ArgumentException($"Duplicate property name {propName} in tuple.", nameof(tupleProps)); + return (tupleProp.propType, propName, tupleProp.cqlName); + }) + .ToList(); - var matchedTupleType = _tupleTypeList - .FirstOrDefault(tupleType => - { - var isMatch = normalizedProperties - .All(prop => - tupleType.GetProperty(prop.propName) is { PropertyType: { } tuplePropertyType } - && tuplePropertyType == prop.propType); - return isMatch; - }); - if (matchedTupleType != null) - return matchedTupleType; - - var typeName = $"Tuples.{TupleTypeNameFor(propertyNamesAndTypes)}"; - - var myTypeBuilder = _moduleBuilder.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class, typeof(TupleBaseType)); - - foreach (var kvp in propertyNamesAndTypes) + if (!_tupleTypeCache.TryFindTupleType(tupleProps3, out var tupleType)) { - var name = ExpressionBuilderContext.NormalizeIdentifier(kvp.Key); - var type = kvp.Value; - DefineProperty(myTypeBuilder, name!, kvp.Key, type); + tupleType = DefineType(tupleProps3); + _tupleTypeCache.AddTupleType(tupleType); } - var typeInfo = myTypeBuilder.CreateTypeInfo(); - AddTupleType(typeInfo!); - return typeInfo!; + + return tupleType; } - private void AddTupleType(Type type) + private TypeInfo DefineType(IReadOnlyCollection<(Type propType, string propName, string cqlName)> tupleProps3) { - if (_tupleTypeList.Contains(type)) - throw new ArgumentException($"Type {type.Name} already exists", nameof(type)); - _tupleTypeList.Add(type); + var typeName = TupleTypeNameFor(tupleProps3.Select(tp => (tp.propType, tp.propName))); + + var myTypeBuilder = _moduleBuilder.DefineType(typeName, TypeAttributes.NotPublic | TypeAttributes.Class, typeof(TupleBaseType)); + + foreach (var t in tupleProps3) + DefineProperty(myTypeBuilder, t.propName, t.cqlName, t.propType); + + var typeInfo = myTypeBuilder.CreateTypeInfo(); + return typeInfo; } private static void DefineProperty(TypeBuilder myTypeBuilder, string normalizedName, string cqlName, Type type) @@ -127,20 +147,15 @@ private static void DefineProperty(TypeBuilder myTypeBuilder, string normalizedN } /// - /// Gets a unique tuple name given the elements (members) of the type. - /// This method must return the same value for equal values of . - /// Equality is determined by comparing using default string equality - /// and using default equality. + /// Gets a unique tuple name given the ordered members of the tuple type. + /// This method must return the same value for equal values of . /// - /// Key value pairs where key is the name of the element and the value is its type. + /// The property names and types in the tuple. /// The unique tuple type name. - private static string TupleTypeNameFor(IReadOnlyDictionary elementInfo) + private static string TupleTypeNameFor(IEnumerable<(Type propType, string propName)> tupleProps) { - var nameTypes = elementInfo - .OrderBy(k => k.Key) - .Select(kvp => $"{kvp.Key}:{kvp.Value.ToCSharpString()}"); - var hashInput = string.Join("+", nameTypes); - var tupleId = Hasher.Instance.Hash(hashInput); - return $"Tuple_{tupleId}"; + var orderedTupleProps = tupleProps.OrderBy(k => k.propName); + var name = $"Tuples.{CqlTupleMetadata.BuildSignatureHashString(orderedTupleProps, "Tuple_")}"; + return name; } } \ No newline at end of file diff --git a/Cql/Cql.Primitives/Cql.Primitives.csproj b/Cql/Cql.Primitives/Cql.Primitives.csproj index 2827a70b7..2e8cd4ef0 100644 --- a/Cql/Cql.Primitives/Cql.Primitives.csproj +++ b/Cql/Cql.Primitives/Cql.Primitives.csproj @@ -1,4 +1,4 @@ - + @@ -16,6 +16,13 @@ + + + + + + + true diff --git a/Cql/Cql.Runtime/CqlTupleMetadata.cs b/Cql/Cql.Primitives/CqlTupleMetadata.cs similarity index 64% rename from Cql/Cql.Runtime/CqlTupleMetadata.cs rename to Cql/Cql.Primitives/CqlTupleMetadata.cs index b30f5c2f2..ac854b54b 100644 --- a/Cql/Cql.Runtime/CqlTupleMetadata.cs +++ b/Cql/Cql.Primitives/CqlTupleMetadata.cs @@ -1,16 +1,20 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Hl7.Cql.Abstractions; using Hl7.Cql.Abstractions.Infrastructure; -namespace Hl7.Cql.Runtime; +namespace Hl7.Cql.Primitives; /// -/// Represents the metadata for a tuple. +/// Represents the metadata for a CQL value tuple. +/// CQL value tuples are represented as instances with the first element being a instance. /// public class CqlTupleMetadata : IEquatable { + internal const string? PropertyPrefix = "CqlTupleMetadata_"; + /// /// Represents the metadata for a tuple. /// @@ -25,15 +29,22 @@ public CqlTupleMetadata( if (ItemNames.Count != ItemTypes.Count) throw new ArgumentException("Item names and types must have the same number of elements."); - _signatureHashString = BuildSignatureHashString(ItemNames.Zip(ItemTypes).ToList()); _toString = $"[{string.Join(", ", ItemNames.Select(pn => $"\"{pn}\""))}]"; - _hashCode = _signatureHashString.GetHashCode(); + + // For some odd reason, if this is not lazy, the hash code is occasionally fails deep inside the Hasher with NullReferenceException, whenever a library is created. + _signatureHashStringLazy = new Lazy(() => BuildSignatureHashString(ItemTypes.Zip(ItemNames).ToList(), PropertyPrefix, _toString)); } - internal static string BuildSignatureHashString(IReadOnlyCollection<(string ItemName, Type ItemType)> signature) + internal static string BuildSignatureHashString( + IEnumerable<(Type propType, string propName)> tupleProps, + string? prepend = null, + string? toString = null) { - var signatureString = string.Join("+", signature.Select(t => $"{t.ItemName}:{t.ItemType.ToCSharpString()}")); - var signatureHashString = $"CqlTupleMetadata_{Hasher.Instance.Hash(signatureString)}"; + var hasher = Hasher.Instance; + var signatureString = string.Join( + "+", + tupleProps.Select(t => $"{t.propName}:{t.propType.ToCSharpString()}")); + var signatureHashString = $"{prepend}{hasher.Hash(signatureString)}"; return signatureHashString; } @@ -47,8 +58,7 @@ internal static string BuildSignatureHashString(IReadOnlyCollection<(string Item /// public IReadOnlyList ItemTypes { get; } - private readonly string _signatureHashString; - private readonly int _hashCode; + private readonly Lazy _signatureHashStringLazy; private readonly string _toString; /// @@ -69,7 +79,7 @@ public virtual bool Equals(CqlTupleMetadata? obj) } private bool EqualsImpl(CqlTupleMetadata other) => - _signatureHashString == other._signatureHashString + _signatureHashStringLazy.Value == other._signatureHashStringLazy.Value // && ItemNames.SequenceEqual(other.ItemNames) // This is redundant && ItemTypes.SequenceEqual(other.ItemTypes); @@ -84,7 +94,7 @@ private bool EqualsImpl(CqlTupleMetadata other) => public static bool operator!=(CqlTupleMetadata? left, CqlTupleMetadata? right) => !(left == right); /// - public override int GetHashCode() => _hashCode; + public override int GetHashCode() => _signatureHashStringLazy.Value.GetHashCode(); /// public override string ToString() => _toString; diff --git a/Cql/Cql.Primitives/TupleBaseType.cs b/Cql/Cql.Primitives/TupleBaseType.cs index b34dba9ae..c1bca8847 100644 --- a/Cql/Cql.Primitives/TupleBaseType.cs +++ b/Cql/Cql.Primitives/TupleBaseType.cs @@ -6,11 +6,18 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE */ +using System; +using System.Diagnostics.CodeAnalysis; + namespace Hl7.Cql.Primitives { /// - /// The base type for all generated Tuples. + /// The temporary base class for tuple types generated into the LINQ expressions. + /// This is not used during CQL runtime, because the C# compiler will replace this with value tuples instead. /// + /// + /// This type is for internal use by the SDK only. + /// [CqlPrimitiveType(CqlPrimitiveType.Tuple)] public abstract class TupleBaseType { diff --git a/Cql/Cql.Primitives/TypeExtensions.cs b/Cql/Cql.Primitives/TypeExtensions.cs new file mode 100644 index 000000000..0291cdfc2 --- /dev/null +++ b/Cql/Cql.Primitives/TypeExtensions.cs @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, NCQA and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE + */ + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Hl7.Cql.Primitives; + +/// +/// Provides extension methods for CQL-related . +/// +public static class TypeExtensions +{ + /// + /// Determines if the specified type is a CqlInterval and retrieves its element type. + /// + /// The type to check. + /// The element type of the CqlInterval, if it is a CqlInterval. + /// true if the type is a CqlInterval; otherwise, false. + public static bool IsCqlInterval(this Type type, [NotNullWhen(true)] out Type? elementType) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(CqlInterval<>)) + { + elementType = type.GetGenericArguments()[0]; + return true; + } + elementType = null; + return false; + } + + /// + /// Determines if the specified type is a CqlValueTuple. + /// + /// The type to check. + /// true if the type is a CqlValueTuple; otherwise, false. + public static bool IsCqlValueTuple(this Type type) + { + bool isCqlValueTuple = + type.IsGenericType + && type.IsAssignableTo(typeof(ITuple)) + && type.GenericTypeArguments.ElementAtOrDefault(0) == typeof(CqlTupleMetadata); + return isCqlValueTuple; + } + + internal static bool IsTupleBaseType(this Type type) + { + var isTupleBaseType = type.IsAssignableTo(typeof(TupleBaseType)); + return isTupleBaseType; + } +} diff --git a/Cql/Cql.Runtime/BaseTypeResolver.cs b/Cql/Cql.Runtime/BaseTypeResolver.cs index 707afe00d..2263461ed 100644 --- a/Cql/Cql.Runtime/BaseTypeResolver.cs +++ b/Cql/Cql.Runtime/BaseTypeResolver.cs @@ -103,8 +103,9 @@ pointType is not null /// internal sealed override PropertyInfo? GetProperty(Type type, string propertyName) { - if (typeof(TupleBaseType).IsAssignableFrom(type)) + if (type.IsTupleBaseType()) { + // This code only executes during the building of the LINQ expression tree, not at runtime var properties = type.GetProperties(); foreach (var prop in properties) { @@ -112,9 +113,15 @@ pointType is not null if (cqlDeclaration != null && cqlDeclaration.Name == propertyName) return prop; } - } + else if (type.IsCqlValueTuple()) + { + throw new NotSupportedException("It is not expected that the CQL runtime query the properties of a value type."); + // If it turns out that this is needed, we can implement it, but since the metadata + // is only available from the CqlTupleMetadata that is available from the first element + // of the tuple, it is not clear how to implement this given only the type. + } return GetPropertyCore(type, propertyName); } diff --git a/Cql/Cql.Runtime/PublicAPI.Unshipped.txt b/Cql/Cql.Runtime/PublicAPI.Unshipped.txt index ea4548ab5..c08de74c0 100644 --- a/Cql/Cql.Runtime/PublicAPI.Unshipped.txt +++ b/Cql/Cql.Runtime/PublicAPI.Unshipped.txt @@ -13,10 +13,6 @@ Hl7.Cql.Runtime.CqlContext.Operators.get -> Hl7.Cql.Operators.ICqlOperators! Hl7.Cql.Runtime.CqlContext.Parameters.get -> System.Collections.Generic.IDictionary! Hl7.Cql.Runtime.CqlContext.RaiseContextEvent(Hl7.Cql.Runtime.ContextEventArgs! eventData) -> Hl7.Cql.Runtime.CqlContext! Hl7.Cql.Runtime.CqlContext.ResolveParameter(string! libraryVersionedIdentifier, string! parameterName, object? defaultValue) -> object? -Hl7.Cql.Runtime.CqlTupleMetadata -Hl7.Cql.Runtime.CqlTupleMetadata.CqlTupleMetadata(System.Type![]? itemTypes = null, string![]? itemNames = null) -> void -Hl7.Cql.Runtime.CqlTupleMetadata.ItemNames.get -> System.Collections.Generic.IReadOnlyList! -Hl7.Cql.Runtime.CqlTupleMetadata.ItemTypes.get -> System.Collections.Generic.IReadOnlyList! Hl7.Cql.Runtime.DefinitionDictionary Hl7.Cql.Runtime.DefinitionDictionary.Add(string! libraryName, string! definition, System.Type![]! signature, T! expression) -> void Hl7.Cql.Runtime.DefinitionDictionary.Add(string! libraryName, string! definition, T! expression) -> void @@ -42,16 +38,10 @@ Hl7.Cql.Runtime.Serialization.CqlValueTupleJsonConverterFactory Hl7.Cql.Runtime.Serialization.CqlValueTupleJsonConverterFactory.CqlValueTupleJsonConverterFactory() -> void override Hl7.Cql.Runtime.BaseTypeResolver.GetListElementType(System.Type! type, bool throwError = false) -> System.Type? override Hl7.Cql.Runtime.BaseTypeResolver.ResolveType(string! typeSpecifier, bool throwError = true) -> System.Type? -override Hl7.Cql.Runtime.CqlTupleMetadata.Equals(object? obj) -> bool -override Hl7.Cql.Runtime.CqlTupleMetadata.GetHashCode() -> int -override Hl7.Cql.Runtime.CqlTupleMetadata.ToString() -> string! override Hl7.Cql.Runtime.Serialization.CqlValueTupleJsonConverter.Read(ref System.Text.Json.Utf8JsonReader reader, System.Type! typeToConvert, System.Text.Json.JsonSerializerOptions! options) -> System.Runtime.CompilerServices.ITuple! override Hl7.Cql.Runtime.Serialization.CqlValueTupleJsonConverter.Write(System.Text.Json.Utf8JsonWriter! writer, System.Runtime.CompilerServices.ITuple! value, System.Text.Json.JsonSerializerOptions! options) -> void override Hl7.Cql.Runtime.Serialization.CqlValueTupleJsonConverterFactory.CanConvert(System.Type! typeToConvert) -> bool override Hl7.Cql.Runtime.Serialization.CqlValueTupleJsonConverterFactory.CreateConverter(System.Type! typeToConvert, System.Text.Json.JsonSerializerOptions! options) -> System.Text.Json.Serialization.JsonConverter! readonly Hl7.Cql.Runtime.BaseTypeResolver.Types -> System.Collections.Generic.Dictionary! -static Hl7.Cql.Runtime.CqlTupleMetadata.operator !=(Hl7.Cql.Runtime.CqlTupleMetadata? left, Hl7.Cql.Runtime.CqlTupleMetadata? right) -> bool -static Hl7.Cql.Runtime.CqlTupleMetadata.operator ==(Hl7.Cql.Runtime.CqlTupleMetadata? left, Hl7.Cql.Runtime.CqlTupleMetadata? right) -> bool static Hl7.Cql.Runtime.Serialization.CqlValueTupleJsonConverter.Default.get -> Hl7.Cql.Runtime.Serialization.CqlValueTupleJsonConverter! virtual Hl7.Cql.Runtime.BaseTypeResolver.GetPropertyCore(System.Type! type, string! propertyName) -> System.Reflection.PropertyInfo? -virtual Hl7.Cql.Runtime.CqlTupleMetadata.Equals(Hl7.Cql.Runtime.CqlTupleMetadata? obj) -> bool diff --git a/Cql/Cql.Runtime/Serialization/CqlValueTupleJsonConverter.cs b/Cql/Cql.Runtime/Serialization/CqlValueTupleJsonConverter.cs index 24acb7a88..906dce15e 100644 --- a/Cql/Cql.Runtime/Serialization/CqlValueTupleJsonConverter.cs +++ b/Cql/Cql.Runtime/Serialization/CqlValueTupleJsonConverter.cs @@ -6,11 +6,11 @@ * available at https://raw.githubusercontent.com/FirelyTeam/cql-sdk/main/LICENSE */ #nullable enable -using Hl7; using System; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; +using Hl7.Cql.Primitives; namespace Hl7.Cql.Runtime.Serialization; diff --git a/Cql/Cql.Runtime/Serialization/CqlValueTupleJsonConverterFactory.cs b/Cql/Cql.Runtime/Serialization/CqlValueTupleJsonConverterFactory.cs index 8f159cdcc..cc7548a7a 100644 --- a/Cql/Cql.Runtime/Serialization/CqlValueTupleJsonConverterFactory.cs +++ b/Cql/Cql.Runtime/Serialization/CqlValueTupleJsonConverterFactory.cs @@ -11,6 +11,7 @@ using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; +using Hl7.Cql.Primitives; namespace Hl7.Cql.Runtime.Serialization; @@ -22,9 +23,7 @@ public class CqlValueTupleJsonConverterFactory : JsonConverterFactory /// public override bool CanConvert(Type typeToConvert) { - var canConvert = typeof(ITuple).IsAssignableFrom(typeToConvert) - && typeToConvert.IsGenericType - && typeToConvert.GenericTypeArguments[0] == typeof(CqlTupleMetadata); + var canConvert = typeToConvert.IsCqlValueTuple(); return canConvert; } diff --git a/Cql/CqlToElmTests/NotEqualTest.cs b/Cql/CqlToElmTests/NotEqualTest.cs index 09c6824b6..350ddf48d 100644 --- a/Cql/CqlToElmTests/NotEqualTest.cs +++ b/Cql/CqlToElmTests/NotEqualTest.cs @@ -1525,7 +1525,6 @@ public void Tuple_Equal_Tuple_Null_Equals_NotNull() } [TestMethod] - [Ignore("Will be fixed in PR 614")] public void Tuple_Equal_Tuple_Null_Equals_Null() { var lib = CreateLibraryForExpression("{ x: 1, y: null } = { x: 1, y: null }"); diff --git a/submodules/Firely.Cql.Sdk.Integration.Runner b/submodules/Firely.Cql.Sdk.Integration.Runner index 51c9cd77f..9c073cf08 160000 --- a/submodules/Firely.Cql.Sdk.Integration.Runner +++ b/submodules/Firely.Cql.Sdk.Integration.Runner @@ -1 +1 @@ -Subproject commit 51c9cd77faf2583f1546474436de47dd3baae68f +Subproject commit 9c073cf084aeb275df78512245d768f01282c142