From 1bac8a3b5b37ea3af277ccdd6e0049e7db9379e2 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Sun, 8 Nov 2026 08:40:33 +0100 Subject: [PATCH 1/2] fix: compile lifted user-defined conditional logical operators Add a nullable custom-operator path using existing locals, nullable accessors and method emission. Preserve null and decisive-left short circuits, selected bitwise methods, ignored effects and right-side finally cleanup. Keep ordinary Boolean and non-lifted operator paths in place. --- .../FastExpressionCompiler.cs | 59 ++++++++++++++ .../Program.cs | 2 + .../Program.cs | 2 + ...LiftedUserDefinedLogicalRegressionTests.cs | 80 +++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 test/FastExpressionCompiler.UnitTests/LiftedUserDefinedLogicalRegressionTests.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 068230d6..8204e013 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -2423,6 +2423,9 @@ 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); + if (!exprType.IsPrimitive && logical.IsLifted) + return TryEmitLiftedUserDefinedLogicalOperator(logical, nodeType, il, ref context, parent); + var logicalMethod = logical.Method; if (logicalMethod != null && !exprType.IsPrimitive) { @@ -7304,6 +7307,62 @@ private static bool TryEmitArithmeticOperation(Type leftType, Type rightType, Ex return true; } + private static bool TryEmitLiftedUserDefinedLogicalOperator(BinaryExpression expr, ExpressionType nodeType, + ILGenerator il, ref CompilerContext context, ParentFlags parent) + { + var type = expr.Left.Type; + var operandType = Nullable.GetUnderlyingType(type); + var method = expr.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 || method.ContainsGenericParameters) + return false; + var test = method.DeclaringType.GetMethod(nodeType == ExpressionType.AndAlso ? "op_False" : "op_True", + BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy, + null, new[] { operandType }, null); + if (test == null || test.ReturnType != typeof(bool)) + return false; + + var flags = (parent & ~(ParentFlags.IgnoreResult | ParentFlags.InstanceAccess | ParentFlags.MemberAccess | + ParentFlags.LambdaCall | ParentFlags.ReturnByRef)) | ParentFlags.AssignmentRightValue; + if (!TryEmit(expr.Left, il, ref context, flags)) + return false; + var left = EmitStoreLocalVariable(il, type); + var skipRight = il.DefineLabel(); + EmitLoadLocalVariableAddress(il, left); + EmitMethodCall(il, type.GetNullableHasValueGetterMethod()); + il.Demit(OpCodes.Brfalse, skipRight); + EmitLoadLocalVariableAddress(il, left); + il.Demit(OpCodes.Ldfld, type.GetNullableValueUnsafeAkaGetValueOrDefaultMethod()); + EmitMethodCall(il, test); + il.Demit(OpCodes.Brtrue, skipRight); + + // A null or decisive left operand skips the right. Otherwise evaluate it on an empty stack. + if (!TryEmit(expr.Right, il, ref context, flags)) + return false; + var right = EmitStoreLocalVariable(il, type); + var 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()); + EmitMethodCall(il, method); + il.Demit(OpCodes.Newobj, type.GetNullableConstructor()); + var done = il.DefineLabel(); + il.Demit(OpCodes.Br, done); + il.DmarkLabel(skipRight); + EmitLoadLocalVariable(il, left); + il.Demit(OpCodes.Br, done); + il.DmarkLabel(rightIsNull); + EmitDefault(il, type); + il.DmarkLabel(done); + 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..10ca3cf6 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -36,6 +36,7 @@ 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.VoidLabelValueRegressionTests()); t.Run(new LightExpression.UnitTests.ReferenceInstanceCallRegressionTests()); t.Run(new LightExpression.UnitTests.MultiRankArrayReceiverRegressionTests()); @@ -72,6 +73,7 @@ static void RunLightExpressionTests(object state) var fecTests = new TestRun(); fecTests.Run(new ReadonlyRefFieldRegressionTests()); + fecTests.Run(new LiftedUserDefinedLogicalRegressionTests()); 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..470b969f 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -65,6 +65,7 @@ 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.VoidLabelValueRegressionTests()); lt.Run(new LightExpression.UnitTests.ReferenceInstanceCallRegressionTests()); lt.Run(new LightExpression.UnitTests.MultiRankArrayReceiverRegressionTests()); @@ -98,6 +99,7 @@ public static void Main(string[] args) var stStopwatch = Stopwatch.StartNew(); st.Run(new ReadonlyRefFieldRegressionTests()); + st.Run(new LiftedUserDefinedLogicalRegressionTests()); 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" }; } + } +} From 394f8e856c6bf1dd10b0df7eba403f0f9d9474ca Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Sun, 8 Nov 2026 23:04:43 +0100 Subject: [PATCH 2/2] refactor: share custom logical operator emission across lifted paths Move non-lifted custom logical emission out of the main switch and share operand storage, truth testing, ordered right evaluation, method calls and branch merging. Keep nullable checks and labels conditional on lifting, preserving existing lookup and operand contexts. Add standard and Light regression coverage for reference and struct operators, explicit methods, ignored effects and exception regions. --- .../FastExpressionCompiler.cs | 133 ++++++++---------- .../Program.cs | 2 + .../Program.cs | 2 + .../UserDefinedLogicalRegressionTests.cs | 122 ++++++++++++++++ 4 files changed, 187 insertions(+), 72 deletions(-) create mode 100644 test/FastExpressionCompiler.UnitTests/UserDefinedLogicalRegressionTests.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 8204e013..0f12ead3 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -2423,48 +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); - if (!exprType.IsPrimitive && logical.IsLifted) - return TryEmitLiftedUserDefinedLogicalOperator(logical, nodeType, 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; } @@ -7307,59 +7270,85 @@ private static bool TryEmitArithmeticOperation(Type leftType, Type rightType, Ex return true; } - private static bool TryEmitLiftedUserDefinedLogicalOperator(BinaryExpression expr, ExpressionType nodeType, + private static bool TryEmitUserDefinedLogicalOperator(BinaryExpression expr, ExpressionType nodeType, bool isLifted, ILGenerator il, ref CompilerContext context, ParentFlags parent) { var type = expr.Left.Type; - var operandType = Nullable.GetUnderlyingType(type); - var method = expr.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 || method.ContainsGenericParameters) + 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 test = method.DeclaringType.GetMethod(nodeType == ExpressionType.AndAlso ? "op_False" : "op_True", - BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy, - null, new[] { operandType }, null); - if (test == null || test.ReturnType != typeof(bool)) + 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 = (parent & ~(ParentFlags.IgnoreResult | ParentFlags.InstanceAccess | ParentFlags.MemberAccess | - ParentFlags.LambdaCall | ParentFlags.ReturnByRef)) | ParentFlags.AssignmentRightValue; + 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(); - EmitLoadLocalVariableAddress(il, left); - EmitMethodCall(il, type.GetNullableHasValueGetterMethod()); - il.Demit(OpCodes.Brfalse, skipRight); - EmitLoadLocalVariableAddress(il, left); - il.Demit(OpCodes.Ldfld, type.GetNullableValueUnsafeAkaGetValueOrDefaultMethod()); + 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); - // A null or decisive left operand skips the right. Otherwise evaluate it on an empty stack. + // 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, type); - var 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()); + 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); - il.Demit(OpCodes.Newobj, type.GetNullableConstructor()); + if (isLifted) + il.Demit(OpCodes.Newobj, type.GetNullableConstructor()); var done = il.DefineLabel(); il.Demit(OpCodes.Br, done); il.DmarkLabel(skipRight); EmitLoadLocalVariable(il, left); - il.Demit(OpCodes.Br, done); - il.DmarkLabel(rightIsNull); - EmitDefault(il, type); + if (isLifted) + { + il.Demit(OpCodes.Br, done); + il.DmarkLabel(rightIsNull); + EmitDefault(il, type); + } il.DmarkLabel(done); - context.LastEmitIsAddress = false; + if (isLifted) + context.LastEmitIsAddress = false; return il.EmitPopIfIgnoreResult(parent); } diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index 10ca3cf6..684f4f78 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -37,6 +37,7 @@ static void RunLightExpressionTests(object 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()); @@ -74,6 +75,7 @@ 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 470b969f..579ff0d0 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -66,6 +66,7 @@ public static void Main(string[] args) 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()); @@ -100,6 +101,7 @@ 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/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); + } +}