Skip to content

Commit a94e93c

Browse files
committed
lifted support - some of it
1 parent 81c863c commit a94e93c

2 files changed

Lines changed: 168 additions & 17 deletions

File tree

‎src/FastExpressionCompiler/FastExpressionCompiler.cs‎

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,12 +2018,50 @@ public static bool TryEmit(Expression expr, ILGenerator il, ref CompilerContext
20182018
case ExpressionType.NotEqual:
20192019
{
20202020
var comparison = (BinaryExpression)expr;
2021-
if (comparison.Method != null && !comparison.IsLifted)
2021+
var cmpMethod = comparison.Method;
2022+
if (cmpMethod != null && !comparison.IsLifted)
20222023
return TryEmitComparisonMethod(comparison, il, ref context, parent);
2023-
if (exprType != typeof(bool) && !(exprType == typeof(bool?) && comparison.Method == null &&
2024-
comparison.Left.Type == comparison.Right.Type && comparison.Left.Type.IsNullable() &&
2025-
!IsNullContainingExpression(comparison.Left) && !IsNullContainingExpression(comparison.Right)))
2026-
return false; // todo: @feat other kinds of lifting are not supported yet
2024+
2025+
var cmpLeft = comparison.Left;
2026+
var cmpLeftType = cmpLeft.Type;
2027+
var cmpRight = comparison.Right;
2028+
var cmpRightType = cmpRight.Type;
2029+
var exprTypeIsBool = exprType == typeof(bool);
2030+
if (!exprTypeIsBool)
2031+
{
2032+
if (exprType != typeof(bool?) ||
2033+
!IsStandardComparisonMethod(cmpMethod, nodeType) ||
2034+
cmpLeftType != cmpRightType ||
2035+
!cmpLeftType.IsNullable())
2036+
return false; // todo: @feat other kinds of lifting are not supported yet
2037+
}
2038+
2039+
// A `null` operand of the nullable type: the relational comparison is always `false` (`default(bool?)` when lifted to null),
2040+
// the other operand is still evaluated for its side effects
2041+
if (IsStandardComparisonMethod(cmpMethod, nodeType) &&
2042+
cmpLeftType == cmpRightType &&
2043+
cmpLeftType.IsNullable() &&
2044+
(!exprTypeIsBool || nodeType != ExpressionType.Equal & nodeType != ExpressionType.NotEqual))
2045+
{
2046+
var leftIsNull = IsNullContainingExpression(cmpLeft);
2047+
var rightIsNull = IsNullContainingExpression(cmpRight);
2048+
if (leftIsNull | rightIsNull)
2049+
{
2050+
if (!leftIsNull && !TryEmit(cmpLeft, il, ref context, parent | ParentFlags.IgnoreResult))
2051+
return false;
2052+
if (!rightIsNull && !TryEmit(cmpRight, il, ref context, parent | ParentFlags.IgnoreResult))
2053+
return false;
2054+
if ((parent & ParentFlags.IgnoreResult) == 0)
2055+
{
2056+
if (exprTypeIsBool)
2057+
il.Demit(OpCodes.Ldc_I4_0);
2058+
else
2059+
EmitLoadLocalVariable(il, InitValueTypeVariable(il, typeof(bool?)));
2060+
}
2061+
context.LastEmitIsAddress = false;
2062+
return true;
2063+
}
2064+
}
20272065
#if LIGHT_EXPRESSION
20282066
if (comparison is LogicalBinaryExpression { IsReferenceComparison: true })
20292067
return TryEmitReferenceComparison(comparison, nodeType == ExpressionType.NotEqual, il, ref context, parent);
@@ -3861,7 +3899,6 @@ private static int InitValueTypeVariable(ILGenerator il, Type exprType, int valu
38613899
return valueVarIndex;
38623900
}
38633901

3864-
// todo: @perf merge with EmitLoadLocalVariable
38653902
private static int InitValueTypeVariable(ILGenerator il, Type exprType) =>
38663903
InitValueTypeVariable(il, exprType, il.GetNextLocalVarIndex(exprType));
38673904

@@ -5889,14 +5926,7 @@ private static bool TryEmitComparison(
58895926

58905927
if (!leftType.IsPrimitive && !leftType.IsEnum)
58915928
{
5892-
var methodName
5893-
= nodeType == ExpressionType.Equal ? "op_Equality"
5894-
: nodeType == ExpressionType.NotEqual ? "op_Inequality"
5895-
: nodeType == ExpressionType.GreaterThan ? "op_GreaterThan"
5896-
: nodeType == ExpressionType.GreaterThanOrEqual ? "op_GreaterThanOrEqual"
5897-
: nodeType == ExpressionType.LessThan ? "op_LessThan"
5898-
: nodeType == ExpressionType.LessThanOrEqual ? "op_LessThanOrEqual"
5899-
: null;
5929+
var methodName = GetStandardComparisonMethodNameOrNull(nodeType);
59005930
if (methodName == null)
59015931
return false;
59025932

@@ -5935,10 +5965,10 @@ var methodName
59355965
EmitEqualToZeroOrNull(il);
59365966
break;
59375967
case ExpressionType.LessThan:
5938-
il.Demit(OpCodes.Clt);
5968+
il.Demit(leftType.IsUnsigned() && rightType.IsUnsigned() ? OpCodes.Clt_Un : OpCodes.Clt);
59395969
break;
59405970
case ExpressionType.GreaterThan:
5941-
il.Demit(OpCodes.Cgt);
5971+
il.Demit(leftType.IsUnsigned() && rightType.IsUnsigned() ? OpCodes.Cgt_Un : OpCodes.Cgt);
59425972
break;
59435973
case ExpressionType.GreaterThanOrEqual:
59445974
// simplifying by using the LessThen (Clt) and comparing with negative outcome (Ceq 0)
@@ -6398,6 +6428,19 @@ private static bool TryEmitLogicalOperator(BinaryExpression expr, ExpressionType
63986428
return true;
63996429
}
64006430

6431+
[MethodImpl((MethodImplOptions)256)]
6432+
private static string GetStandardComparisonMethodNameOrNull(ExpressionType nodeType) =>
6433+
nodeType == ExpressionType.Equal ? "op_Equality"
6434+
: nodeType == ExpressionType.NotEqual ? "op_Inequality"
6435+
: nodeType == ExpressionType.GreaterThan ? "op_GreaterThan"
6436+
: nodeType == ExpressionType.GreaterThanOrEqual ? "op_GreaterThanOrEqual"
6437+
: nodeType == ExpressionType.LessThan ? "op_LessThan"
6438+
: nodeType == ExpressionType.LessThanOrEqual ? "op_LessThanOrEqual"
6439+
: null;
6440+
6441+
private static bool IsStandardComparisonMethod(MethodInfo method, ExpressionType nodeType) =>
6442+
method == null || method.IsSpecialName && method.IsStatic && method.Name == GetStandardComparisonMethodNameOrNull(nodeType);
6443+
64016444
private static bool IsNullContainingExpression(Expression expr) =>
64026445
expr is DefaultExpression ld && (ld.Type.IsClass || ld.Type.IsNullable()) ||
64036446
expr is ConstantExpression lc && lc.Value == null;

‎test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs‎

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public int Run()
7474
Can_round_trip_more_expression_kinds_through_system_expressions();
7575
Can_compile_type_is_and_type_equal_the_same_as_system();
7676
IsLifted_and_IsLiftedToNull_are_the_same_as_in_system_expressions();
77+
Lifted_comparisons_with_null_operand_and_other_nullable_types_are_the_same_as_system();
7778
Lifted_comparisons_have_the_same_type_and_result_as_system();
7879
And_AndAlso_OrElse_take_the_type_from_the_left_operand();
7980
Can_compile_nullable_boolean_and_also();
@@ -87,7 +88,7 @@ public int Run()
8788
Can_print_reference_comparison();
8889
Can_round_trip_new_expression_kinds_through_system_expressions();
8990

90-
return 68;
91+
return 69;
9192
}
9293

9394
private static int _sideEffectCalls;
@@ -767,6 +768,113 @@ public void And_AndAlso_OrElse_take_the_type_from_the_left_operand()
767768
Asserts.AreEqual(typeof(bool?), OrElse(nb, nb).Type);
768769
}
769770

771+
public struct LiftMoney
772+
{
773+
public int V;
774+
public LiftMoney(int v) { V = v; }
775+
public static bool operator ==(LiftMoney a, LiftMoney b) => a.V == b.V;
776+
public static bool operator !=(LiftMoney a, LiftMoney b) => a.V != b.V;
777+
public static bool operator <(LiftMoney a, LiftMoney b) => a.V < b.V;
778+
public static bool operator >(LiftMoney a, LiftMoney b) => a.V > b.V;
779+
public static bool operator <=(LiftMoney a, LiftMoney b) => a.V <= b.V;
780+
public static bool operator >=(LiftMoney a, LiftMoney b) => a.V >= b.V;
781+
public override bool Equals(object o) => o is LiftMoney m && m.V == V;
782+
public override int GetHashCode() => V;
783+
}
784+
public enum LiftEnum { A, B, C }
785+
786+
public void Lifted_comparisons_with_null_operand_and_other_nullable_types_are_the_same_as_system()
787+
{
788+
var names = new[] { "Equal", "NotEqual", "GreaterThan", "GreaterThanOrEqual", "LessThan", "LessThanOrEqual" };
789+
var cases = new (Type, object, object)[]
790+
{
791+
(typeof(int?), 1, 2), (typeof(long?), 1L, 2L), (typeof(double?), 1.0, 2.0), (typeof(uint?), 1u, 2u),
792+
(typeof(char?), 'a', 'b'), (typeof(LiftEnum?), LiftEnum.A, LiftEnum.B), (typeof(bool?), true, false),
793+
(typeof(decimal?), 1m, 2m), (typeof(DateTime?), new DateTime(2000, 1, 1), new DateTime(2001, 1, 1)),
794+
(typeof(LiftMoney?), new LiftMoney(1), new LiftMoney(2)),
795+
};
796+
var sig = new[] { typeof(SysExpr), typeof(SysExpr), typeof(bool), typeof(MethodInfo) };
797+
var lsig = new[] { typeof(Expression), typeof(Expression), typeof(bool), typeof(MethodInfo) };
798+
var checkedCount = 0;
799+
foreach (var (t, v1, v2) in cases)
800+
foreach (var name in names)
801+
foreach (var lift in new[] { false, true })
802+
foreach (var nullSide in new[] { 0, 1, 2 }) // none, left, right
803+
{
804+
var what = $"{t.Name} {name} lift={lift} nullSide={nullSide}";
805+
Delegate sysD;
806+
try
807+
{
808+
var sa = SysExpr.Parameter(t, "a"); var sb = SysExpr.Parameter(t, "b");
809+
var sl = nullSide == 1 ? SysExpr.Constant(null, t) : (SysExpr)sa;
810+
var sr = nullSide == 2 ? SysExpr.Constant(null, t) : (SysExpr)sb;
811+
var se = (SysExpr)typeof(SysExpr).GetMethod(name, sig).Invoke(null, new object[] { sl, sr, lift, null });
812+
sysD = SysExpr.Lambda(typeof(Func<,,>).MakeGenericType(t, t, se.Type), se, sa, sb).Compile();
813+
}
814+
catch (System.Reflection.TargetInvocationException) { continue; } // the operator is not defined for the type
815+
816+
var la = Parameter(t, "a"); var lb = Parameter(t, "b");
817+
var ll = nullSide == 1 ? Constant(null, t) : (Expression)la;
818+
var lr = nullSide == 2 ? Constant(null, t) : (Expression)lb;
819+
var le = (Expression)typeof(Expression).GetMethod(name, lsig).Invoke(null, new object[] { ll, lr, lift, null });
820+
var fastD = Lambda(typeof(Func<,,>).MakeGenericType(t, t, le.Type), le, la, lb).CompileFast(true);
821+
Asserts.IsNotNull(fastD, what);
822+
++checkedCount;
823+
824+
foreach (var x in new[] { null, v1, v2 })
825+
foreach (var y in new[] { null, v1, v2 })
826+
Asserts.AreEqual(sysD.DynamicInvoke(x, y), fastD.DynamicInvoke(x, y), $"{what} ({x}, {y})");
827+
}
828+
Asserts.IsTrue(checkedCount > 100);
829+
830+
// both operands are constants, as in the ported dotnet/runtime tests
831+
foreach (var (t, v1, v2) in cases)
832+
foreach (var name in names)
833+
foreach (var lift in new[] { false, true })
834+
foreach (var x in new[] { null, v1, v2 })
835+
foreach (var y in new[] { null, v1, v2 })
836+
{
837+
object expected;
838+
try
839+
{
840+
var se = (SysExpr)typeof(SysExpr).GetMethod(name, sig).Invoke(null, new object[] { SysExpr.Constant(x, t), SysExpr.Constant(y, t), lift, null });
841+
expected = SysExpr.Lambda(typeof(Func<>).MakeGenericType(se.Type), se).Compile().DynamicInvoke();
842+
}
843+
catch (System.Reflection.TargetInvocationException) { continue; }
844+
845+
var le = (Expression)typeof(Expression).GetMethod(name, lsig).Invoke(null, new object[] { Constant(x, t), Constant(y, t), lift, null });
846+
var fastD = Lambda(typeof(Func<>).MakeGenericType(le.Type), le).CompileFast(true);
847+
var what = $"{t.Name} {name} lift={lift} ({x}, {y})";
848+
Asserts.IsNotNull(fastD, what);
849+
Asserts.AreEqual(expected, fastD.DynamicInvoke(), what);
850+
}
851+
852+
// unsigned operands above the signed range
853+
var ua = Parameter(typeof(uint?), "ua"); var ub = Parameter(typeof(uint?), "ub");
854+
var ult = Lambda<Func<uint?, uint?, bool>>(LessThan(ua, ub), ua, ub).CompileFast(true);
855+
Asserts.AreEqual(true, ult(1u, uint.MaxValue));
856+
Asserts.AreEqual(false, ult(uint.MaxValue, 1u));
857+
var ugt = Lambda<Func<uint?, uint?, bool?>>(GreaterThan(ua, ub, true, null), ua, ub).CompileFast(true);
858+
Asserts.AreEqual(true, ugt(uint.MaxValue, 1u));
859+
Asserts.AreEqual(null, ugt(null, 1u));
860+
var pu = Parameter(typeof(ulong), "pu"); var pv = Parameter(typeof(ulong), "pv");
861+
Asserts.AreEqual(true, Lambda<Func<ulong, ulong, bool>>(GreaterThan(pu, pv), pu, pv).CompileFast(true)(ulong.MaxValue, 1UL));
862+
// the other operand is still evaluated when the result is known to be null
863+
var calls = 0;
864+
Func<int?> sideEffect = () => { ++calls; return 1; };
865+
var lt = LessThan(Invoke(Constant(sideEffect)), Constant(null, typeof(int?)), true, null);
866+
Asserts.AreEqual(typeof(bool?), lt.Type);
867+
Asserts.AreEqual(null, Lambda<Func<bool?>>(lt).CompileFast(true)());
868+
var gt = GreaterThan(Constant(null, typeof(int?)), Invoke(Constant(sideEffect)), true, null);
869+
Asserts.AreEqual(null, Lambda<Func<bool?>>(gt).CompileFast(true)());
870+
Asserts.AreEqual(2, calls);
871+
872+
// and the result is usable as a nested operand
873+
var nb = Parameter(typeof(bool?), "nb");
874+
var withOr = Lambda<Func<bool?, bool?>>(Or(nb, lt), nb).CompileFast(true);
875+
Asserts.AreEqual(true, withOr(true));
876+
Asserts.AreEqual(null, withOr(false));
877+
}
770878
public void Lifted_comparisons_have_the_same_type_and_result_as_system()
771879
{
772880
var ip = Parameter(typeof(int), "a"); var iq = Parameter(typeof(int), "b");

0 commit comments

Comments
 (0)