Skip to content

Commit a7cb276

Browse files
authored
Merge pull request #54 from EFNext/fix/issue-53
Add enum relational comparison support
2 parents 9061489 + faf1a25 commit a7cb276

6 files changed

Lines changed: 315 additions & 2 deletions

File tree

src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,15 @@ private string EmitBinary(IBinaryOperation binary)
881881
var leftVar = EmitOperation(binary.LeftOperand);
882882
var rightVar = EmitOperation(binary.RightOperand);
883883

884+
// MakeBinary rejects raw enum operands for relational operators.
885+
if (binary.OperatorMethod is null
886+
&& IsRelationalOperator(binary.OperatorKind)
887+
&& TryGetEnumComparisonUnderlyingFqn(binary.LeftOperand.Type, out var underlyingFqn))
888+
{
889+
leftVar = EmitConvert(leftVar, underlyingFqn);
890+
rightVar = EmitConvert(rightVar, underlyingFqn);
891+
}
892+
884893
if (binary.OperatorMethod is not null)
885894
{
886895
var methodField = _fieldCache.EnsureMethodInfo(binary.OperatorMethod);
@@ -894,6 +903,42 @@ private string EmitBinary(IBinaryOperation binary)
894903
return resultVar;
895904
}
896905

906+
private static bool IsRelationalOperator(BinaryOperatorKind kind) =>
907+
kind is BinaryOperatorKind.LessThan
908+
or BinaryOperatorKind.LessThanOrEqual
909+
or BinaryOperatorKind.GreaterThan
910+
or BinaryOperatorKind.GreaterThanOrEqual;
911+
912+
private static bool TryGetEnumComparisonUnderlyingFqn(ITypeSymbol? type, out string underlyingFqn)
913+
{
914+
underlyingFqn = "";
915+
916+
if (type is INamedTypeSymbol { IsGenericType: true } nullable
917+
&& nullable.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T
918+
&& nullable.TypeArguments[0] is INamedTypeSymbol { TypeKind: TypeKind.Enum } innerEnum
919+
&& innerEnum.EnumUnderlyingType is { } innerUnderlying)
920+
{
921+
underlyingFqn = $"global::System.Nullable<{innerUnderlying.ToDisplayString(_fqnFormat)}>";
922+
return true;
923+
}
924+
925+
if (type is INamedTypeSymbol { TypeKind: TypeKind.Enum } enumType
926+
&& enumType.EnumUnderlyingType is { } underlying)
927+
{
928+
underlyingFqn = underlying.ToDisplayString(_fqnFormat);
929+
return true;
930+
}
931+
932+
return false;
933+
}
934+
935+
private string EmitConvert(string operandVar, string typeFqn)
936+
{
937+
var convertVar = NextVar();
938+
AppendLine($"var {convertVar} = {Expr}.Convert({operandVar}, typeof({typeFqn}));");
939+
return convertVar;
940+
}
941+
897942
private string EmitStringConcatenation(IBinaryOperation binary)
898943
{
899944
var resultVar = NextVar();
@@ -1532,7 +1577,7 @@ private string EmitPattern(IPatternOperation pattern, string operandVar, ITypeSy
15321577
IConstantPatternOperation constant => EmitConstantPattern(constant, operandVar),
15331578
ITypePatternOperation typePattern => EmitTypePattern(typePattern, operandVar),
15341579
IDeclarationPatternOperation declaration => EmitDeclarationPattern(declaration, operandVar),
1535-
IRelationalPatternOperation relational => EmitRelationalPattern(relational, operandVar),
1580+
IRelationalPatternOperation relational => EmitRelationalPattern(relational, operandVar, operandType),
15361581
INegatedPatternOperation negated => EmitNegatedPattern(negated, operandVar, operandType),
15371582
IBinaryPatternOperation binaryPattern => EmitBinaryPattern(binaryPattern, operandVar, operandType),
15381583
IDiscardPatternOperation => EmitDiscardPattern(),
@@ -1567,7 +1612,7 @@ private string EmitDeclarationPattern(IDeclarationPatternOperation declaration,
15671612
return resultVar;
15681613
}
15691614

1570-
private string EmitRelationalPattern(IRelationalPatternOperation relational, string operandVar)
1615+
private string EmitRelationalPattern(IRelationalPatternOperation relational, string operandVar, ITypeSymbol? operandType)
15711616
{
15721617
var resultVar = NextVar();
15731618
var valueVar = EmitOperation(relational.Value);
@@ -1590,6 +1635,12 @@ private string EmitRelationalPattern(IRelationalPatternOperation relational, str
15901635
return resultVar;
15911636
}
15921637

1638+
if (TryGetEnumComparisonUnderlyingFqn(operandType, out var underlyingFqn))
1639+
{
1640+
operandVar = EmitConvert(operandVar, underlyingFqn);
1641+
valueVar = EmitConvert(valueVar, underlyingFqn);
1642+
}
1643+
15931644
AppendLine($"var {resultVar} = {Expr}.MakeBinary(global::System.Linq.Expressions.ExpressionType.{exprType}, {operandVar}, {valueVar});");
15941645
return resultVar;
15951646
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// <auto-generated/>
2+
#nullable disable
3+
4+
using Foo;
5+
6+
namespace ExpressiveSharp.Generated
7+
{
8+
static partial class Foo_Entity
9+
{
10+
// [Expressive]
11+
// public string Tier => Value <= Bucket.Low ? "low" : Value <= Bucket.Mid ? "mid" : "high";
12+
static global::System.Linq.Expressions.Expression<global::System.Func<global::Foo.Entity, string>> Tier_Expression()
13+
{
14+
var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this");
15+
var expr_2 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value
16+
var expr_3 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.Bucket).GetField("Low", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // Bucket.Low
17+
var expr_4 = global::System.Linq.Expressions.Expression.Convert(expr_2, typeof(int));
18+
var expr_5 = global::System.Linq.Expressions.Expression.Convert(expr_3, typeof(int));
19+
var expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.LessThanOrEqual, expr_4, expr_5);
20+
var expr_6 = global::System.Linq.Expressions.Expression.Constant("low", typeof(string)); // "low"
21+
var expr_9 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value
22+
var expr_10 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.Bucket).GetField("Mid", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // Bucket.Mid
23+
var expr_11 = global::System.Linq.Expressions.Expression.Convert(expr_9, typeof(int));
24+
var expr_12 = global::System.Linq.Expressions.Expression.Convert(expr_10, typeof(int));
25+
var expr_8 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.LessThanOrEqual, expr_11, expr_12);
26+
var expr_13 = global::System.Linq.Expressions.Expression.Constant("mid", typeof(string)); // "mid"
27+
var expr_14 = global::System.Linq.Expressions.Expression.Constant("high", typeof(string)); // "high"
28+
var expr_7 = global::System.Linq.Expressions.Expression.Condition(expr_8, expr_13, expr_14, typeof(string));
29+
var expr_0 = global::System.Linq.Expressions.Expression.Condition(expr_1, expr_6, expr_7, typeof(string));
30+
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.Entity, string>>(expr_0, p__this);
31+
}
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// <auto-generated/>
2+
#nullable disable
3+
4+
using Foo;
5+
6+
namespace ExpressiveSharp.Generated
7+
{
8+
static partial class Foo_Entity
9+
{
10+
// [Expressive]
11+
// public bool IsLowOrMid => Value <= Bucket.Mid;
12+
static global::System.Linq.Expressions.Expression<global::System.Func<global::Foo.Entity, bool>> IsLowOrMid_Expression()
13+
{
14+
var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this");
15+
var expr_1 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value
16+
var expr_3 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.Bucket).GetField("Mid", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // Bucket.Mid
17+
var expr_2 = global::System.Linq.Expressions.Expression.Convert(expr_3, typeof(global::Foo.Bucket?));
18+
var expr_4 = global::System.Linq.Expressions.Expression.Convert(expr_1, typeof(global::System.Nullable<int>));
19+
var expr_5 = global::System.Linq.Expressions.Expression.Convert(expr_2, typeof(global::System.Nullable<int>));
20+
var expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.LessThanOrEqual, expr_4, expr_5);
21+
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.Entity, bool>>(expr_0, p__this);
22+
}
23+
}
24+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// <auto-generated/>
2+
#nullable disable
3+
4+
using Foo;
5+
6+
namespace ExpressiveSharp.Generated
7+
{
8+
static partial class Foo_Entity
9+
{
10+
// [Expressive]
11+
// public bool IsLowOrMid => Value is <= Bucket.Mid;
12+
static global::System.Linq.Expressions.Expression<global::System.Func<global::Foo.Entity, bool>> IsLowOrMid_Expression()
13+
{
14+
var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Entity), "@this");
15+
var expr_0 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Entity).GetProperty("Value", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // Value
16+
var expr_2 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::Foo.Bucket).GetField("Mid", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // Bucket.Mid
17+
var expr_3 = global::System.Linq.Expressions.Expression.Convert(expr_0, typeof(int));
18+
var expr_4 = global::System.Linq.Expressions.Expression.Convert(expr_2, typeof(int));
19+
var expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.LessThanOrEqual, expr_3, expr_4);
20+
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.Entity, bool>>(expr_1, p__this);
21+
}
22+
}
23+
}

tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/EnumTests.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,82 @@ public record Entity
255255

256256
return Verifier.Verify(result.GeneratedTrees[0].ToString());
257257
}
258+
259+
[TestMethod]
260+
public Task EnumRelationalComparison()
261+
{
262+
var compilation = CreateCompilation(
263+
"""
264+
namespace Foo {
265+
public enum Bucket { Low, Mid, High }
266+
267+
public record Entity
268+
{
269+
public Bucket Value { get; set; }
270+
271+
[Expressive]
272+
public string Tier =>
273+
Value <= Bucket.Low ? "low" :
274+
Value <= Bucket.Mid ? "mid" :
275+
"high";
276+
}
277+
}
278+
""");
279+
var result = RunExpressiveGenerator(compilation);
280+
281+
Assert.AreEqual(0, result.Diagnostics.Length);
282+
Assert.AreEqual(1, result.GeneratedTrees.Length);
283+
284+
return Verifier.Verify(result.GeneratedTrees[0].ToString());
285+
}
286+
287+
[TestMethod]
288+
public Task EnumRelationalComparisonWithNullableEnum()
289+
{
290+
var compilation = CreateCompilation(
291+
"""
292+
namespace Foo {
293+
public enum Bucket { Low, Mid, High }
294+
295+
public record Entity
296+
{
297+
public Bucket? Value { get; set; }
298+
299+
[Expressive]
300+
public bool IsLowOrMid => Value <= Bucket.Mid;
301+
}
302+
}
303+
""");
304+
var result = RunExpressiveGenerator(compilation);
305+
306+
Assert.AreEqual(0, result.Diagnostics.Length);
307+
Assert.AreEqual(1, result.GeneratedTrees.Length);
308+
309+
return Verifier.Verify(result.GeneratedTrees[0].ToString());
310+
}
311+
312+
[TestMethod]
313+
public Task EnumRelationalPattern()
314+
{
315+
var compilation = CreateCompilation(
316+
"""
317+
namespace Foo {
318+
public enum Bucket { Low, Mid, High }
319+
320+
public record Entity
321+
{
322+
public Bucket Value { get; set; }
323+
324+
[Expressive]
325+
public bool IsLowOrMid => Value is <= Bucket.Mid;
326+
}
327+
}
328+
""");
329+
var result = RunExpressiveGenerator(compilation);
330+
331+
Assert.AreEqual(0, result.Diagnostics.Length);
332+
Assert.AreEqual(1, result.GeneratedTrees.Length);
333+
334+
return Verifier.Verify(result.GeneratedTrees[0].ToString());
335+
}
258336
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System.Linq.Expressions;
2+
using ExpressiveSharp.Mapping;
3+
using Microsoft.VisualStudio.TestTools.UnitTesting;
4+
5+
namespace ExpressiveSharp.IntegrationTests.Tests;
6+
7+
[TestClass]
8+
public class EnumComparisonTests
9+
{
10+
[TestMethod]
11+
public void EnumLessThanOrEqual_ExpandExpressives_MaterializesAndEvaluates()
12+
{
13+
var source = new List<EnumComparisonEntity>
14+
{
15+
new() { Value = Bucket.Low },
16+
new() { Value = Bucket.Mid },
17+
new() { Value = Bucket.High },
18+
}.AsQueryable();
19+
20+
Expression<Func<EnumComparisonEntity, string>> expr = e => e.Tier;
21+
var expanded = (Expression<Func<EnumComparisonEntity, string>>)expr.ExpandExpressives();
22+
23+
var tiers = source.Select(expanded.Compile()).ToList();
24+
25+
CollectionAssert.AreEqual(new[] { "low", "mid", "high" }, tiers);
26+
}
27+
28+
[TestMethod]
29+
public void EnumLessThan_ExpandExpressives_MaterializesAndEvaluates()
30+
{
31+
var source = new List<EnumComparisonEntity>
32+
{
33+
new() { Value = Bucket.Low },
34+
new() { Value = Bucket.Mid },
35+
new() { Value = Bucket.High },
36+
}.AsQueryable();
37+
38+
Expression<Func<EnumComparisonEntity, bool>> expr = e => e.IsBelowMid;
39+
var expanded = (Expression<Func<EnumComparisonEntity, bool>>)expr.ExpandExpressives();
40+
41+
var results = source.Select(expanded.Compile()).ToList();
42+
43+
CollectionAssert.AreEqual(new[] { true, false, false }, results);
44+
}
45+
46+
[TestMethod]
47+
public void NullableEnumComparison_ExpandExpressives_MaterializesAndEvaluates()
48+
{
49+
var source = new List<EnumComparisonEntity>
50+
{
51+
new() { NullableValue = Bucket.Low },
52+
new() { NullableValue = Bucket.Mid },
53+
new() { NullableValue = Bucket.High },
54+
new() { NullableValue = null },
55+
}.AsQueryable();
56+
57+
Expression<Func<EnumComparisonEntity, bool>> expr = e => e.IsLowOrMid;
58+
var expanded = (Expression<Func<EnumComparisonEntity, bool>>)expr.ExpandExpressives();
59+
60+
var results = source.Select(expanded.Compile()).ToList();
61+
62+
CollectionAssert.AreEqual(new[] { true, true, false, false }, results);
63+
}
64+
65+
[TestMethod]
66+
public void ExpressivePropertyOnEnumComparison_RegistryResolves()
67+
{
68+
var registered = ExpressiveSharp.Generated.ExpressionRegistry.TryGet(
69+
typeof(EnumComparisonProperty).GetProperty(nameof(EnumComparisonProperty.Tier))!);
70+
71+
Assert.IsNotNull(registered);
72+
}
73+
}
74+
75+
public class EnumComparisonEntity
76+
{
77+
public Bucket Value { get; init; }
78+
public Bucket? NullableValue { get; init; }
79+
80+
[Expressive]
81+
public string Tier =>
82+
Value <= Bucket.Low ? "low" :
83+
Value <= Bucket.Mid ? "mid" :
84+
"high";
85+
86+
[Expressive]
87+
public bool IsBelowMid => Value < Bucket.Mid;
88+
89+
[Expressive]
90+
public bool IsLowOrMid => NullableValue <= Bucket.Mid;
91+
}
92+
93+
public enum Bucket { Low, Mid, High }
94+
95+
public partial class EnumComparisonProperty
96+
{
97+
public Bucket Value { get; init; }
98+
99+
[ExpressiveProperty("Tier")]
100+
private string TierExpr =>
101+
Value <= Bucket.Low ? "low" :
102+
Value <= Bucket.Mid ? "mid" :
103+
"high";
104+
}

0 commit comments

Comments
 (0)