From 402fd812c48e9f76751ce4baf8d76aac62cdfa7f Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Wed, 4 Nov 2026 00:05:01 +0100 Subject: [PATCH] fix: preserve explicit Not methods during simplification Do not interpret a method-based Not as built-in negation or rewrite it through equality during conditional simplification. Preserve the selected method and its observable effects using the existing unary emitter. Standard and Light regressions cover constant and parameter operands, conditional branches, ignored results and nullable method arguments, with interpreter optimization enabled and disabled. Focused tests pass on net8.0, net10.0 and net472; the unfixed compiler changes results and omits calls. Full solution builds, legacy suites and Framework runners pass. Existing master MultiplyChecked parity failures reproduce identically on net8.0/net10.0. --- .../FastExpressionCompiler.cs | 4 +- .../Program.cs | 2 + .../Program.cs | 2 + .../MethodBasedNotRegressionTests.cs | 87 +++++++++++++++++++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 test/FastExpressionCompiler.UnitTests/MethodBasedNotRegressionTests.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 8ee51c68..5909c94c 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -8507,6 +8507,8 @@ internal static bool TryInterpretBool(ref bool resultBool, Expression expr, Expr // not: negate, arithmetic, convert to bool if (nodeType == ExpressionType.Not) { + if (((UnaryExpression)expr).Method != null) + return false; var operandExpr = ((UnaryExpression)expr).Operand; if (operandExpr.NodeType == ExpressionType.Conditional) operandExpr = Tools.TryReduceConditional((ConditionalExpression)operandExpr); @@ -9528,7 +9530,7 @@ public static T GetFirst(this IEnumerable source) public static Expression TryReduceConditionalTest(Expression testExpr) { // removing Not by turning Equal -> NotEqual, NotEqual -> Equal - if (testExpr.NodeType == ExpressionType.Not) + if (testExpr.NodeType == ExpressionType.Not && ((UnaryExpression)testExpr).Method == null) { // simplify the not `==` -> `!=`, `!=` -> `==` var op = TryReduceConditionalTest(((UnaryExpression)testExpr).Operand); diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index eaeefdf4..e81e7f1a 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -44,6 +44,7 @@ static void RunLightExpressionTests(object state) t.Run(new LightExpression.IssueTests.Issue498_InvalidProgramException_when_using_loop()); t.Run(new LightExpression.IssueTests.Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); t.Run(new LightExpression.UnitTests.RuntimeTypeConstantRegressionTests()); + t.Run(new LightExpression.UnitTests.MethodBasedNotRegressionTests()); t.Run(new LightExpression.UnitTests.ParityTests()); Console.WriteLine($"Just LightExpression tests are passing in {justLightTestsStopwatch.ElapsedMilliseconds} ms."); @@ -66,6 +67,7 @@ static void RunLightExpressionTests(object state) fecTests.Run(new Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); fecTests.Run(new ReductionProgressRegressionTests()); fecTests.Run(new RuntimeTypeConstantRegressionTests()); + fecTests.Run(new MethodBasedNotRegressionTests()); fecTests.Run(new ParityTests()); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 7a867a16..379b3f3e 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -76,6 +76,7 @@ public static void Main(string[] args) lt.Run(new LightExpression.IssueTests.Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); lt.Run(new LightExpression.IssueTests.Issue561_NonBooleanEqualityOperator()); lt.Run(new LightExpression.UnitTests.RuntimeTypeConstantRegressionTests()); + lt.Run(new LightExpression.UnitTests.MethodBasedNotRegressionTests()); lt.Run(new LightExpression.UnitTests.ParityTests()); ltTime = ltStopwatch.ElapsedMilliseconds; }); @@ -85,6 +86,7 @@ public static void Main(string[] args) st.Run(new Issue561_NonBooleanEqualityOperator()); st.Run(new ReductionProgressRegressionTests()); st.Run(new RuntimeTypeConstantRegressionTests()); + st.Run(new MethodBasedNotRegressionTests()); st.Run(new ParityTests()); st.Run(new Issue498_InvalidProgramException_when_using_loop()); st.Run(new Issue495_Incomplete_pattern_detection_for_NotSupported_1007_Return_goto_from_TryCatch_with_Assign_generates_invalid_IL()); diff --git a/test/FastExpressionCompiler.UnitTests/MethodBasedNotRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/MethodBasedNotRegressionTests.cs new file mode 100644 index 00000000..c4462db4 --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/MethodBasedNotRegressionTests.cs @@ -0,0 +1,87 @@ +using System; +using SysExpr = System.Linq.Expressions.Expression; + +#if LIGHT_EXPRESSION +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +namespace FastExpressionCompiler.UnitTests; +#endif + +public struct MethodBasedNotRegressionTests : ITestX +{ + public void Run(TestRun t) + { + Explicit_not_method_controls_result(t); + Conditional_preserves_custom_not_semantics(t); + Ignored_result_preserves_custom_not_effects(t); + Nullable_method_receives_null(t); + } + + public void Explicit_not_method_controls_result(TestContext t) + { + var parameter = SysExpr.Parameter(typeof(bool)); + var method = typeof(MethodBasedNotRegressionTests).GetMethod(nameof(Identity)); + foreach (var flags in new[] { CompilerFlags.Default, CompilerFlags.DisableInterpreter }) + foreach (var value in new[] { false, true }) + { + var constant = SysExpr.Lambda>(SysExpr.Not(SysExpr.Constant(value), method)); + var fastConstant = constant.FromSysExpression().CompileFast(true, flags); + if (!t.IsNotNull(fastConstant)) return; + t.AreEqual(value, fastConstant()); + var expression = SysExpr.Lambda>(SysExpr.Not(parameter, method), parameter); + var fast = expression.FromSysExpression().CompileFast(true, flags); + if (!t.IsNotNull(fast)) return; + t.AreEqual(value, fast(value)); + } + } + + public void Conditional_preserves_custom_not_semantics(TestContext t) + { + var parameter = SysExpr.Parameter(typeof(bool)); + var method = typeof(MethodBasedNotRegressionTests).GetMethod(nameof(Identity)); + foreach (var flags in new[] { CompilerFlags.Default, CompilerFlags.DisableInterpreter }) + foreach (var value in new[] { false, true }) + foreach (var shape in new[] { 0, 1, 2 }) + { + System.Linq.Expressions.Expression operand = shape == 0 ? SysExpr.Constant(value) + : shape == 1 ? parameter : SysExpr.Equal(parameter, SysExpr.Constant(true)); + var body = SysExpr.Condition(SysExpr.Not(operand, method), SysExpr.Constant(42), SysExpr.Constant(7)); + var expression = SysExpr.Lambda>(body, parameter); + var fast = expression.FromSysExpression().CompileFast(true, flags); + if (!t.IsNotNull(fast)) return; + Calls = 0; + t.AreEqual(expression.Compile()(value), fast(value)); + t.AreEqual(2, Calls); + } + } + + public void Ignored_result_preserves_custom_not_effects(TestContext t) + { + var method = typeof(MethodBasedNotRegressionTests).GetMethod(nameof(Identity)); + var expression = SysExpr.Lambda>(SysExpr.Block( + SysExpr.Not(SysExpr.Constant(true), method), SysExpr.Constant(7))); + foreach (var flags in new[] { CompilerFlags.Default, CompilerFlags.DisableInterpreter }) + { + var fast = expression.FromSysExpression().CompileFast(true, flags); + if (!t.IsNotNull(fast)) return; + Calls = 0; + t.AreEqual(7, fast()); + t.AreEqual(1, Calls); + } + } + + public void Nullable_method_receives_null(TestContext t) + { + var parameter = SysExpr.Parameter(typeof(bool?)); + var method = typeof(MethodBasedNotRegressionTests).GetMethod(nameof(NullableIdentity)); + var expression = SysExpr.Lambda>(SysExpr.Not(parameter, method), parameter); + var fast = expression.FromSysExpression().CompileFast(true); + if (!t.IsNotNull(fast)) return; + foreach (var value in new bool?[] { null, false, true }) + t.AreEqual(expression.Compile()(value), fast(value)); + } + + public static int Calls; + public static bool Identity(bool value) { ++Calls; return value; } + public static bool? NullableIdentity(bool? value) { ++Calls; return value ?? true; } +}