diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 068230d6..0f12ead3 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -2423,45 +2423,11 @@ public static bool TryEmit(Expression expr, ILGenerator il, ref CompilerContext logical.Left.Type == typeof(bool?) && logical.Right.Type == typeof(bool?)) return TryEmitNullableBooleanShortCircuit(logical.Left, logical.Right, nodeType == ExpressionType.AndAlso, il, ref context, parent); - var logicalMethod = logical.Method; - if (logicalMethod != null && !exprType.IsPrimitive) - { - // User-defined `&&` and `||`: `op_False(left) ? left : method(left, right)`, and `op_True` for the `||`. - // The right is emitted with the empty stack on purpose, so it may contain the try or the other non-stack-safe expression. - var leftExpr = logical.Left; - var isAndAlso = nodeType == ExpressionType.AndAlso; - var leftType = leftExpr.Type; - var shortCircuitMethod = leftType.GetMethod(isAndAlso ? "op_False" : "op_True", BindingFlags.Public | BindingFlags.Static, null, new[] { leftType }, null); - if (shortCircuitMethod == null) - return false; - - var operandParent = parent & ~ParentFlags.IgnoreResult; - if (!TryEmit(leftExpr, il, ref context, operandParent)) - return false; - var leftVar = EmitStoreLocalVariable(il, leftType); - EmitLoadLocalVariable(il, leftVar); - if (!EmitMethodCall(il, shortCircuitMethod)) - return false; - var skipRightLabel = il.DefineLabel(); - il.Demit(OpCodes.Brtrue, skipRightLabel); - - if (!TryEmit(logical.Right, il, ref context, operandParent)) - return false; - var rightVar = EmitStoreLocalVariable(il, logical.Right.Type); - EmitLoadLocalVariable(il, leftVar); - EmitLoadLocalVariable(il, rightVar); - if (!EmitMethodCall(il, logicalMethod)) - return false; - var doneLabel = il.DefineLabel(); - il.Demit(OpCodes.Br, doneLabel); - - il.DmarkLabel(skipRightLabel); - EmitLoadLocalVariable(il, leftVar); - il.DmarkLabel(doneLabel); - return il.EmitPopIfIgnoreResult(parent); - } if (!exprType.IsPrimitive) { + var isLifted = logical.IsLifted; + if (isLifted || logical.Method != null) + return TryEmitUserDefinedLogicalOperator(logical, nodeType, isLifted, il, ref context, parent); Debug.WriteLine("Unsupported: non-primitive result in || or && (other than `bool?` operands without a method), see #480: " + expr); return false; } @@ -7304,6 +7270,88 @@ private static bool TryEmitArithmeticOperation(Type leftType, Type rightType, Ex return true; } + private static bool TryEmitUserDefinedLogicalOperator(BinaryExpression expr, ExpressionType nodeType, bool isLifted, + ILGenerator il, ref CompilerContext context, ParentFlags parent) + { + var type = expr.Left.Type; + var operandType = isLifted ? Nullable.GetUnderlyingType(type) : type; + var method = expr.Method; + if (isLifted && method == null) + method = FindBinaryOperandMethod( + nodeType == ExpressionType.AndAlso ? "op_BitwiseAnd" : "op_BitwiseOr", operandType, operandType, + Nullable.GetUnderlyingType(expr.Right.Type) ?? expr.Right.Type, operandType); + if (operandType == null || method == null || isLifted && method.ContainsGenericParameters) + return false; + var testType = isLifted ? method.DeclaringType : type; + var bindingFlags = BindingFlags.Public | BindingFlags.Static; + if (isLifted) + bindingFlags |= BindingFlags.FlattenHierarchy; + var test = testType.GetMethod(nodeType == ExpressionType.AndAlso ? "op_False" : "op_True", + bindingFlags, null, new[] { operandType }, null); + if (test == null || isLifted && test.ReturnType != typeof(bool)) + return false; + + var flags = isLifted + ? (parent & ~(ParentFlags.IgnoreResult | ParentFlags.InstanceAccess | ParentFlags.MemberAccess | + ParentFlags.LambdaCall | ParentFlags.ReturnByRef)) | ParentFlags.AssignmentRightValue + : parent & ~ParentFlags.IgnoreResult; + if (!TryEmit(expr.Left, il, ref context, flags)) + return false; + var left = EmitStoreLocalVariable(il, type); + var skipRight = il.DefineLabel(); + if (isLifted) + { + EmitLoadLocalVariableAddress(il, left); + EmitMethodCall(il, type.GetNullableHasValueGetterMethod()); + il.Demit(OpCodes.Brfalse, skipRight); + EmitLoadLocalVariableAddress(il, left); + il.Demit(OpCodes.Ldfld, type.GetNullableValueUnsafeAkaGetValueOrDefaultMethod()); + } + else + EmitLoadLocalVariable(il, left); + EmitMethodCall(il, test); + il.Demit(OpCodes.Brtrue, skipRight); + + // Both paths evaluate the right on an empty stack, including exception regions. + if (!TryEmit(expr.Right, il, ref context, flags)) + return false; + var right = EmitStoreLocalVariable(il, expr.Right.Type); + var rightIsNull = default(Label); + if (isLifted) + { + rightIsNull = il.DefineLabel(); + EmitLoadLocalVariableAddress(il, right); + EmitMethodCall(il, type.GetNullableHasValueGetterMethod()); + il.Demit(OpCodes.Brfalse, rightIsNull); + EmitLoadLocalVariableAddress(il, left); + il.Demit(OpCodes.Ldfld, type.GetNullableValueUnsafeAkaGetValueOrDefaultMethod()); + EmitLoadLocalVariableAddress(il, right); + il.Demit(OpCodes.Ldfld, type.GetNullableValueUnsafeAkaGetValueOrDefaultMethod()); + } + else + { + EmitLoadLocalVariable(il, left); + EmitLoadLocalVariable(il, right); + } + EmitMethodCall(il, method); + if (isLifted) + il.Demit(OpCodes.Newobj, type.GetNullableConstructor()); + var done = il.DefineLabel(); + il.Demit(OpCodes.Br, done); + il.DmarkLabel(skipRight); + EmitLoadLocalVariable(il, left); + if (isLifted) + { + il.Demit(OpCodes.Br, done); + il.DmarkLabel(rightIsNull); + EmitDefault(il, type); + } + il.DmarkLabel(done); + if (isLifted) + context.LastEmitIsAddress = false; + return il.EmitPopIfIgnoreResult(parent); + } + private static bool TryEmitLogicalOperator(BinaryExpression expr, ExpressionType nodeType, ILGenerator il, ref CompilerContext context, ParentFlags parent) { diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index c13f9c5c..684f4f78 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -36,6 +36,8 @@ static void RunLightExpressionTests(object state) var t = (LightExpression.TestRun)state; t.Run(new LightExpression.UnitTests.VoidSwitchConversionRegressionTests()); t.Run(new LightExpression.UnitTests.ReadonlyRefFieldRegressionTests()); + t.Run(new LightExpression.UnitTests.LiftedUserDefinedLogicalRegressionTests()); + t.Run(new LightExpression.UnitTests.UserDefinedLogicalRegressionTests()); t.Run(new LightExpression.UnitTests.VoidLabelValueRegressionTests()); t.Run(new LightExpression.UnitTests.ReferenceInstanceCallRegressionTests()); t.Run(new LightExpression.UnitTests.MultiRankArrayReceiverRegressionTests()); @@ -72,6 +74,8 @@ static void RunLightExpressionTests(object state) var fecTests = new TestRun(); fecTests.Run(new ReadonlyRefFieldRegressionTests()); + fecTests.Run(new LiftedUserDefinedLogicalRegressionTests()); + fecTests.Run(new UserDefinedLogicalRegressionTests()); fecTests.Run(new VoidLabelValueRegressionTests()); fecTests.Run(new ReferenceInstanceCallRegressionTests()); fecTests.Run(new MultiRankArrayReceiverRegressionTests()); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index cf7fa328..579ff0d0 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -65,6 +65,8 @@ public static void Main(string[] args) var ltStopwatch = Stopwatch.StartNew(); lt.Run(new LightExpression.UnitTests.VoidSwitchConversionRegressionTests()); lt.Run(new LightExpression.UnitTests.ReadonlyRefFieldRegressionTests()); + lt.Run(new LightExpression.UnitTests.LiftedUserDefinedLogicalRegressionTests()); + lt.Run(new LightExpression.UnitTests.UserDefinedLogicalRegressionTests()); lt.Run(new LightExpression.UnitTests.VoidLabelValueRegressionTests()); lt.Run(new LightExpression.UnitTests.ReferenceInstanceCallRegressionTests()); lt.Run(new LightExpression.UnitTests.MultiRankArrayReceiverRegressionTests()); @@ -98,6 +100,8 @@ public static void Main(string[] args) var stStopwatch = Stopwatch.StartNew(); st.Run(new ReadonlyRefFieldRegressionTests()); + st.Run(new LiftedUserDefinedLogicalRegressionTests()); + st.Run(new UserDefinedLogicalRegressionTests()); st.Run(new VoidLabelValueRegressionTests()); st.Run(new ReferenceInstanceCallRegressionTests()); st.Run(new MultiRankArrayReceiverRegressionTests()); diff --git a/test/FastExpressionCompiler.UnitTests/LiftedUserDefinedLogicalRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/LiftedUserDefinedLogicalRegressionTests.cs new file mode 100644 index 00000000..c2cab257 --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/LiftedUserDefinedLogicalRegressionTests.cs @@ -0,0 +1,80 @@ +using System; +using SysExpr = System.Linq.Expressions.Expression; +#if LIGHT_EXPRESSION +using Expr = FastExpressionCompiler.LightExpression.Expression; +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +using Expr = System.Linq.Expressions.Expression; +namespace FastExpressionCompiler.UnitTests; +#endif +public struct LiftedUserDefinedLogicalRegressionTests : ITestX +{ + public void Run(TestRun t) + { + Lifted_conditional_operators_match_values_and_effects(t); + Lifted_explicit_methods_match_values_and_effects(t); + Ignored_lifted_conditional_operators_preserve_effects(t); + Lifted_right_operand_may_contain_finally(t); + Direct_factory_discovers_lifted_operator(t); + } + public void Lifted_conditional_operators_match_values_and_effects(TestContext t) => Check(t, false, false, false); + public void Lifted_explicit_methods_match_values_and_effects(TestContext t) => Check(t, true, false, false); + public void Ignored_lifted_conditional_operators_preserve_effects(TestContext t) => Check(t, false, true, false); + public void Lifted_right_operand_may_contain_finally(TestContext t) => Check(t, false, false, true); + private static void Check(TestContext t, bool explicitMethod, bool ignored, bool finallyEffect) + { + foreach (var and in new[] { false, true }) + { + var left = SysExpr.Parameter(typeof(Func)); + var right = SysExpr.Parameter(typeof(Func)); + var method = explicitMethod ? typeof(Logical).GetMethod(and ? nameof(Logical.And) : nameof(Logical.Or)) : null; + SysExpr r = SysExpr.Invoke(right); + if (finallyEffect) r = SysExpr.TryFinally(r, SysExpr.Call(typeof(LiftedUserDefinedLogicalRegressionTests).GetMethod(nameof(Finally)))); + var operation = and ? SysExpr.AndAlso(SysExpr.Invoke(left), r, method) : SysExpr.OrElse(SysExpr.Invoke(left), r, method); + var expression = ignored ? SysExpr.Lambda(SysExpr.Block(operation, SysExpr.Empty()), left, right) : SysExpr.Lambda(operation, left, right); + var fast = expression.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + var system = expression.Compile(); + foreach (var a in new Logical?[] { null, new Logical { Value = 0 }, new Logical { Value = 3 } }) + foreach (var b in new Logical?[] { null, new Logical { Value = 0 }, new Logical { Value = 3 } }) + { + Func l = () => { Trace += "L;"; return a; }; + Func rr = () => { Trace += "R;"; return b; }; + Trace = ""; + var expected = system.DynamicInvoke(l, rr); + var expectedTrace = Trace; + Trace = ""; + t.AreEqual(expected, fast.DynamicInvoke(l, rr)); + t.AreEqual(expectedTrace, Trace); + } + } + } + public void Direct_factory_discovers_lifted_operator(TestContext t) + { + var l = Expr.Parameter(typeof(Logical?)); + var r = Expr.Parameter(typeof(Logical?)); + foreach (var and in new[] { false, true }) + { + var body = and ? Expr.AndAlso(l, r) : Expr.OrElse(l, r); + var fast = Expr.Lambda>(body, l, r).CompileFast(true); + if (!t.IsNotNull(fast)) return; + t.AreEqual((Logical?)null, fast(null, new Logical { Value = 3 })); + t.AreEqual((Logical?)null, fast(new Logical { Value = and ? 3 : 0 }, null)); + var expected = new Logical { Value = and ? 0 : 3 }; + t.AreEqual((Logical?)expected, fast(expected, null)); + } + } + public static string Trace; + public static void Finally() => Trace += "F;"; + public struct Logical + { + public int Value; + public string Tag; + public static bool operator true(Logical value) { Trace += "T;"; return value.Value != 0; } + public static bool operator false(Logical value) { Trace += "F?;"; return value.Value == 0; } + public static Logical operator &(Logical left, Logical right) { Trace += "&;"; return new Logical { Value = left.Value + right.Value, Tag = "and" }; } + public static Logical operator |(Logical left, Logical right) { Trace += "|;"; return new Logical { Value = left.Value + right.Value, Tag = "or" }; } + public static Logical And(Logical left, Logical right) { Trace += "And;"; return new Logical { Value = left.Value + right.Value, Tag = "explicit and" }; } + public static Logical Or(Logical left, Logical right) { Trace += "Or;"; return new Logical { Value = left.Value + right.Value, Tag = "explicit or" }; } + } +} diff --git a/test/FastExpressionCompiler.UnitTests/UserDefinedLogicalRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/UserDefinedLogicalRegressionTests.cs new file mode 100644 index 00000000..23dbabbe --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/UserDefinedLogicalRegressionTests.cs @@ -0,0 +1,122 @@ +using System; +using SysExpr = System.Linq.Expressions.Expression; + +#if LIGHT_EXPRESSION +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +namespace FastExpressionCompiler.UnitTests; +#endif + +public struct UserDefinedLogicalRegressionTests : ITestX +{ + public void Run(TestRun t) + { + Reference_operators_short_circuit_and_evaluate_once(t); + Explicit_methods_are_honored(t); + Struct_operators_and_ignored_results(t); + Right_operand_can_contain_try_catch(t); + } + + public void Reference_operators_short_circuit_and_evaluate_once(TestContext t) => CheckReference(t, false); + public void Explicit_methods_are_honored(TestContext t) => CheckReference(t, true); + + private static void CheckReference(TestContext t, bool explicitMethod) + { + foreach (var and in new[] { false, true }) + foreach (var value in new[] { 0, 5 }) + { + var left = new LogicalValue(value); + var right = new LogicalValue(3); + var leftCalls = 0; + var rightCalls = 0; + Func getLeft = () => { ++leftCalls; return left; }; + Func getRight = () => { ++rightCalls; return right; }; + var l = SysExpr.Parameter(typeof(Func)); + var r = SysExpr.Parameter(typeof(Func)); + var method = explicitMethod ? typeof(LogicalValue).GetMethod(and ? "And" : "Or") : null; + var body = and ? SysExpr.AndAlso(SysExpr.Invoke(l), SysExpr.Invoke(r), method) + : SysExpr.OrElse(SysExpr.Invoke(l), SysExpr.Invoke(r), method); + var fast = SysExpr.Lambda, Func, LogicalValue>>(body, l, r) + .FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + var result = fast(getLeft, getRight); + var shortCircuited = and ? value == 0 : value != 0; + t.AreEqual(shortCircuited ? value : and ? value & 3 : value | 3, result.Value); + t.AreEqual(1, leftCalls); + t.AreEqual(shortCircuited ? 0 : 1, rightCalls); + t.AreEqual(and ? 0 : 1, left.TrueCalls); + t.AreEqual(and ? 1 : 0, left.FalseCalls); + t.AreEqual(0, right.TrueCalls + right.FalseCalls); + t.AreEqual(!shortCircuited && explicitMethod ? 1 : 0, left.MethodCalls); + t.AreEqual(!shortCircuited && !explicitMethod ? 1 : 0, left.OperatorCalls); + if (shortCircuited) t.AreEqual(left, result); + } + } + + public void Struct_operators_and_ignored_results(TestContext t) + { + foreach (var and in new[] { false, true }) + { + var l = SysExpr.Parameter(typeof(LogicalStruct)); + var r = SysExpr.Parameter(typeof(Func)); + var body = and ? SysExpr.AndAlso(l, SysExpr.Invoke(r)) : SysExpr.OrElse(l, SysExpr.Invoke(r)); + var fast = SysExpr.Lambda, LogicalStruct>>(body, l, r) + .FromSysExpression().CompileFast(true); + var ignored = SysExpr.Lambda>>(SysExpr.Block(body, SysExpr.Empty()), l, r) + .FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast) || !t.IsNotNull(ignored)) return; + foreach (var value in new[] { 0, 5 }) + { + var calls = 0; + Func right = () => { ++calls; return new LogicalStruct(3); }; + var shortCircuited = and ? value == 0 : value != 0; + t.AreEqual(shortCircuited ? value : and ? value & 3 : value | 3, fast(new LogicalStruct(value), right).Value); + ignored(new LogicalStruct(value), right); + t.AreEqual(shortCircuited ? 0 : 2, calls); + } + } + } + + public void Right_operand_can_contain_try_catch(TestContext t) + { + foreach (var and in new[] { false, true }) + { + var l = SysExpr.Parameter(typeof(LogicalStruct)); + var right = SysExpr.TryCatch(SysExpr.Throw(SysExpr.New(typeof(InvalidOperationException)), typeof(LogicalStruct)), + SysExpr.Catch(typeof(InvalidOperationException), SysExpr.Constant(new LogicalStruct(3)))); + var body = and ? SysExpr.AndAlso(l, right) : SysExpr.OrElse(l, right); + var expression = SysExpr.Lambda>(body, l); + var fast = expression.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + foreach (var value in new[] { 0, 5 }) + t.AreEqual(and ? value == 0 ? value : value & 3 : value != 0 ? value : value | 3, fast(new LogicalStruct(value)).Value); + } + } + + public sealed class LogicalValue + { + public readonly int Value; + public int TrueCalls, FalseCalls, OperatorCalls, MethodCalls; + public LogicalValue(int value) => Value = value; + public static bool operator true(LogicalValue value) { ++value.TrueCalls; return value.Value != 0; } + public static bool operator false(LogicalValue value) { ++value.FalseCalls; return value.Value == 0; } + public static LogicalValue operator &(LogicalValue left, LogicalValue right) + { ++left.OperatorCalls; return new LogicalValue(left.Value & right.Value); } + public static LogicalValue operator |(LogicalValue left, LogicalValue right) + { ++left.OperatorCalls; return new LogicalValue(left.Value | right.Value); } + public static LogicalValue And(LogicalValue left, LogicalValue right) + { ++left.MethodCalls; return new LogicalValue(left.Value & right.Value); } + public static LogicalValue Or(LogicalValue left, LogicalValue right) + { ++left.MethodCalls; return new LogicalValue(left.Value | right.Value); } + } + + public struct LogicalStruct + { + public readonly int Value; + public LogicalStruct(int value) => Value = value; + public static bool operator true(LogicalStruct value) => value.Value != 0; + public static bool operator false(LogicalStruct value) => value.Value == 0; + public static LogicalStruct operator &(LogicalStruct left, LogicalStruct right) => new(left.Value & right.Value); + public static LogicalStruct operator |(LogicalStruct left, LogicalStruct right) => new(left.Value | right.Value); + } +}