From e91fe870b9f6e1da826f60fb4ac85e737ff19bb3 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Thu, 22 Oct 2026 21:50:32 +0200 Subject: [PATCH 1/5] test: reproduce issue 561 non-Boolean equality operator corruption Add four Standard/Light regressions for equality and inequality returning an interface, with both operand orders and three compiler flag modes. Compare operator side effects and interface dispatch with System.Compile. Guard the selected operator call before invoking on modern .NET so the known bad delegate fails the test instead of crashing the runner. Targeted tests fail on current upstream 3aefb5e5 and pass against PR558 ae4d9a55 on .NET8/.NET10, ARM64/x64. The isolated original repro crashes on upstream .NET10 x64 and Framework but succeeds with PR558; Framework is probed only in a child process because its dynamic-method decoder is unsupported. --- .../Issue561_NonBooleanEqualityOperator.cs | 98 +++++++++++++++++++ .../Program.cs | 2 + 2 files changed, 100 insertions(+) create mode 100644 test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs diff --git a/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs b/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs new file mode 100644 index 000000000..7a0be4e83 --- /dev/null +++ b/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs @@ -0,0 +1,98 @@ +// The safety guard uses the dynamic-method decoder supported on modern .NET. +// The fatal repro itself is also exercised in a separate child-process harness. +#if NET8_0_OR_GREATER +using System; +using System.Linq; +using System.Linq.Expressions; + +#if LIGHT_EXPRESSION +using FastExpressionCompiler.LightExpression.ILDecoder; +namespace FastExpressionCompiler.LightExpression.IssueTests; +#else +using FastExpressionCompiler.ILDecoder; +namespace FastExpressionCompiler.IssueTests; +#endif + +public struct Issue561_NonBooleanEqualityOperator : ITestX +{ + public void Run(TestRun t) + { + Equal_returns_the_interface_from_the_selected_operator(t); + NotEqual_returns_the_interface_from_the_selected_operator(t); + Reversed_equal_returns_the_interface_from_the_selected_operator(t); + Reversed_not_equal_returns_the_interface_from_the_selected_operator(t); + } + + public interface ICustomObject { void Ping(); } + + public sealed class CustomObject : ICustomObject + { + public static int OperatorCalls; + public static int PingCalls; + + public static ICustomObject operator ==(double left, CustomObject right) => Result(); + public static ICustomObject operator !=(double left, CustomObject right) => Result(); + public static ICustomObject operator ==(CustomObject left, double right) => Result(); + public static ICustomObject operator !=(CustomObject left, double right) => Result(); + private static ICustomObject Result() { ++OperatorCalls; return new CustomObject(); } + public void Ping() { ++PingCalls; } + public override bool Equals(object obj) => ReferenceEquals(this, obj); + public override int GetHashCode() => base.GetHashCode(); + } + + public void Equal_returns_the_interface_from_the_selected_operator(TestContext t) => + Check(t, () => 1 == new CustomObject()); + + public void NotEqual_returns_the_interface_from_the_selected_operator(TestContext t) => + Check(t, () => 1 != new CustomObject()); + + public void Reversed_equal_returns_the_interface_from_the_selected_operator(TestContext t) => + Check(t, () => new CustomObject() == 1); + + public void Reversed_not_equal_returns_the_interface_from_the_selected_operator(TestContext t) => + Check(t, () => new CustomObject() != 1); + + private static void Check(TestContext t, System.Linq.Expressions.Expression> expression) + { + var binary = (System.Linq.Expressions.BinaryExpression)expression.Body; + t.AreEqual(typeof(ICustomObject), binary.Type); + t.IsNotNull(binary.Method); + t.IsTrue(!binary.IsLifted); + + CustomObject.OperatorCalls = CustomObject.PingCalls = 0; + var normal = expression.Compile()(); + t.AreEqual(1, CustomObject.OperatorCalls); + normal.Ping(); + t.AreEqual(1, CustomObject.PingCalls); + + foreach (var flags in new[] { CompilerFlags.Default, CompilerFlags.NoInvocationLambdaInlining, + CompilerFlags.EnableDelegateDebugInfo }) + { +#if LIGHT_EXPRESSION + var fast = expression.ToLightExpression().CompileFast(true, flags); +#else + var fast = expression.CompileFast(true, flags); +#endif + if (!t.IsNotNull(fast)) return; + + // On affected compilers the returned reference is corrupt. Check the + // selected call before invocation so this regression fails without + // taking down the whole runner. This does not assert an IL layout. + var instructions = fast.TryGetDebugInfo()?.ILInstructions ?? fast.Method.ReadAllInstructions(); + if (!instructions.OfType().Any(i => i.Method.Equals(binary.Method))) + { + t.Fail("Issue561: selected non-Boolean operator call is missing; corrupt-reference delegate was not invoked."); + return; + } + + CustomObject.OperatorCalls = CustomObject.PingCalls = 0; + var result = fast(); + t.AreEqual(1, CustomObject.OperatorCalls); + if (!t.IsNotNull(result)) return; + t.IsTrue(result is CustomObject); + result.Ping(); + t.AreEqual(1, CustomObject.PingCalls); + } + } +} +#endif diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index df16dce9e..0f60c49fd 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -53,6 +53,7 @@ public static void Main() var st = new TestRun(TestFlags.RethrowException); st.Run(new Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); + st.Run(new Issue561_NonBooleanEqualityOperator()); 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()); st.Run(new Issue480_CLR_detected_an_invalid_program_exception()); @@ -75,6 +76,7 @@ public static void Main() lt.Run(new LightExpression.IssueTests.Issue473_InvalidProgramException_when_using_Expression_Condition_with_converted_decimal_expression()); lt.Run(new LightExpression.IssueTests.Issue476_System_ExecutionEngineException_with_nullables_on_repeated_calls_to_ConcurrentDictionary()); lt.Run(new LightExpression.IssueTests.Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); + lt.Run(new LightExpression.IssueTests.Issue561_NonBooleanEqualityOperator()); RunAllTests(); } From f9746e899cc9b6dafe07eda0835d525ed5fd54a6 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Thu, 22 Oct 2026 22:31:32 +0200 Subject: [PATCH 2/5] test: cover selected comparison method semantics Cover discarded results, single left-to-right evaluation with nested exception regions, non-lifted nullable parameters, non-Boolean value results and constant Boolean methods with observable side effects. --- .../Issue561_NonBooleanEqualityOperator.cs | 152 ++++++++++++++++-- 1 file changed, 136 insertions(+), 16 deletions(-) diff --git a/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs b/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs index 7a0be4e83..c5392ca0f 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Linq.Expressions; +using System.Reflection; #if LIGHT_EXPRESSION using FastExpressionCompiler.LightExpression.ILDecoder; @@ -21,10 +22,17 @@ public void Run(TestRun t) NotEqual_returns_the_interface_from_the_selected_operator(t); Reversed_equal_returns_the_interface_from_the_selected_operator(t); Reversed_not_equal_returns_the_interface_from_the_selected_operator(t); + Ignored_result_still_calls_the_operator(t); + Operands_execute_once_in_order_with_nested_try(t); + Non_lifted_nullable_parameters_reach_the_selected_method(t); + Selected_methods_keep_non_boolean_value_results(t); + Boolean_method_is_not_replaced_by_constant_comparison(t); } public interface ICustomObject { void Ping(); } + private static string Trace; + public sealed class CustomObject : ICustomObject { public static int OperatorCalls; @@ -34,7 +42,7 @@ public sealed class CustomObject : ICustomObject public static ICustomObject operator !=(double left, CustomObject right) => Result(); public static ICustomObject operator ==(CustomObject left, double right) => Result(); public static ICustomObject operator !=(CustomObject left, double right) => Result(); - private static ICustomObject Result() { ++OperatorCalls; return new CustomObject(); } + private static ICustomObject Result() { ++OperatorCalls; Trace += "O"; return new CustomObject(); } public void Ping() { ++PingCalls; } public override bool Equals(object obj) => ReferenceEquals(this, obj); public override int GetHashCode() => base.GetHashCode(); @@ -68,22 +76,9 @@ private static void Check(TestContext t, System.Linq.Expressions.Expression().Any(i => i.Method.Equals(binary.Method))) - { - t.Fail("Issue561: selected non-Boolean operator call is missing; corrupt-reference delegate was not invoked."); - return; - } + if (!CheckSelectedCall(t, fast, binary.Method)) return; CustomObject.OperatorCalls = CustomObject.PingCalls = 0; var result = fast(); @@ -94,5 +89,130 @@ private static void Check(TestContext t, System.Linq.Expressions.Expression(System.Linq.Expressions.Expression expression, + CompilerFlags flags = CompilerFlags.Default) where TDelegate : Delegate => +#if LIGHT_EXPRESSION + expression.ToLightExpression().CompileFast(true, flags); +#else + expression.CompileFast(true, flags); +#endif + + private static bool CheckSelectedCall(TestContext t, Delegate fast, MethodInfo method) + { + // On affected compilers the returned reference is corrupt. Check the + // selected call before invocation so this regression fails without + // taking down the whole runner. This does not assert an IL layout. + var instructions = fast.TryGetDebugInfo()?.ILInstructions ?? fast.Method.ReadAllInstructions(); + if (instructions.OfType().Any(i => i.Method.Equals(method))) return true; + t.Fail("Issue561: selected operator call is missing; corrupt-reference delegate was not invoked."); + return false; + } + + public void Ignored_result_still_calls_the_operator(TestContext t) + { + System.Linq.Expressions.Expression> original = () => 1 == new CustomObject(); + var method = ((System.Linq.Expressions.BinaryExpression)original.Body).Method; + var expression = System.Linq.Expressions.Expression.Lambda( + System.Linq.Expressions.Expression.Block(original.Body, System.Linq.Expressions.Expression.Empty())); + var fast = Compile(expression); + if (!t.IsNotNull(fast) || !CheckSelectedCall(t, fast, method)) return; + CustomObject.OperatorCalls = 0; + fast(); + t.AreEqual(1, CustomObject.OperatorCalls); + } + + public static double ReadLeft() { Trace += "L"; return 1; } + public static int ReadArgument() { Trace += "A"; return 2; } + public static void FinishArgument() { Trace += "F"; } + public static CustomObject ReadRight(int value) { Trace += "R"; return new CustomObject(); } + + public void Operands_execute_once_in_order_with_nested_try(TestContext t) + { + var owner = typeof(Issue561_NonBooleanEqualityOperator); + var left = System.Linq.Expressions.Expression.Call(owner.GetMethod(nameof(ReadLeft))); + var right = System.Linq.Expressions.Expression.Call(owner.GetMethod(nameof(ReadRight)), + System.Linq.Expressions.Expression.TryFinally( + System.Linq.Expressions.Expression.Call(owner.GetMethod(nameof(ReadArgument))), + System.Linq.Expressions.Expression.Call(owner.GetMethod(nameof(FinishArgument))))); + var body = System.Linq.Expressions.Expression.Equal(left, right); + var expression = System.Linq.Expressions.Expression.Lambda>(body); + var system = expression.Compile(); + Trace = ""; system().Ping(); + var expected = Trace; + t.AreEqual("LAFRO", expected); + var fast = Compile(expression); + if (!t.IsNotNull(fast) || !CheckSelectedCall(t, fast, body.Method)) return; + Trace = ""; CustomObject.OperatorCalls = CustomObject.PingCalls = 0; + fast().Ping(); + t.AreEqual(expected, Trace); + t.AreEqual(1, CustomObject.OperatorCalls); + t.AreEqual(1, CustomObject.PingCalls); + } + + public static ICustomObject CompareNullable(double? left, CustomObject right) + { + Trace += left.HasValue ? "value" : "null"; + ++CustomObject.OperatorCalls; + return new CustomObject(); + } + + public void Non_lifted_nullable_parameters_reach_the_selected_method(TestContext t) + { + var left = System.Linq.Expressions.Expression.Parameter(typeof(double?)); + var body = System.Linq.Expressions.Expression.Equal(left, + System.Linq.Expressions.Expression.New(typeof(CustomObject)), false, + typeof(Issue561_NonBooleanEqualityOperator).GetMethod(nameof(CompareNullable))); + t.IsTrue(!body.IsLifted); + var expression = System.Linq.Expressions.Expression.Lambda>(body, left); + var system = expression.Compile(); + var fast = Compile(expression); + if (!t.IsNotNull(fast) || !CheckSelectedCall(t, fast, body.Method)) return; + foreach (var value in new double?[] { null, 2 }) + { + Trace = ""; system(value).Ping(); var expected = Trace; + Trace = ""; CustomObject.OperatorCalls = CustomObject.PingCalls = 0; + fast(value).Ping(); + t.AreEqual(expected, Trace); + t.AreEqual(1, CustomObject.OperatorCalls); + t.AreEqual(1, CustomObject.PingCalls); + } + } + + public static int CompareValue(int left, int right) { ++CustomObject.OperatorCalls; return left * 10 + right; } + public static bool CompareBoolean(int left, int right) { ++CustomObject.OperatorCalls; return left != right; } + + public void Selected_methods_keep_non_boolean_value_results(TestContext t) + { + var left = System.Linq.Expressions.Expression.Parameter(typeof(int)); + var method = typeof(Issue561_NonBooleanEqualityOperator).GetMethod(nameof(CompareValue)); + foreach (var kind in new[] { ExpressionType.Equal, ExpressionType.NotEqual, ExpressionType.LessThan, + ExpressionType.LessThanOrEqual, ExpressionType.GreaterThan, ExpressionType.GreaterThanOrEqual }) + { + var body = System.Linq.Expressions.Expression.MakeBinary(kind, left, + System.Linq.Expressions.Expression.Constant(2), false, method); + var expression = System.Linq.Expressions.Expression.Lambda>(body, left); + var fast = Compile(expression); + if (!t.IsNotNull(fast) || !CheckSelectedCall(t, fast, method)) return; + var expected = expression.Compile()(3); + CustomObject.OperatorCalls = 0; + t.AreEqual(expected, fast(3)); + t.AreEqual(1, CustomObject.OperatorCalls); + } + } + + public void Boolean_method_is_not_replaced_by_constant_comparison(TestContext t) + { + var method = typeof(Issue561_NonBooleanEqualityOperator).GetMethod(nameof(CompareBoolean)); + var body = System.Linq.Expressions.Expression.Equal(System.Linq.Expressions.Expression.Constant(1), + System.Linq.Expressions.Expression.Constant(2), false, method); + var expression = System.Linq.Expressions.Expression.Lambda>(body); + var fast = Compile(expression); + if (!t.IsNotNull(fast) || !CheckSelectedCall(t, fast, method)) return; + var expected = expression.Compile()(); + CustomObject.OperatorCalls = 0; + t.AreEqual(expected, fast()); + t.AreEqual(1, CustomObject.OperatorCalls); + } } #endif From a7deb9e8c0d1e15dfd1f1efa731786e9d3fb52f4 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Thu, 22 Oct 2026 22:34:12 +0200 Subject: [PATCH 3/5] fix: emit selected non-lifted comparison methods Call BinaryExpression.Method before primitive comparison handling and preserve its actual return type. Evaluate operands once in order, retaining the left value while nontrivial right operands may emit exception regions or jumps. Respect ignored results and keep lifted comparisons on their existing path. Decline Boolean constant interpretation for method-backed binary expressions so compile-time folding cannot discard the selected call or its side effects. Fixes the invalid reference returned by the issue 561 repro without rebuilding expression nodes or changing compiler context layout. --- .../FastExpressionCompiler.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 88031db0d..1ceee7333 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -2011,6 +2011,9 @@ public static bool TryEmit(Expression expr, ILGenerator il, ref CompilerContext case ExpressionType.Equal: case ExpressionType.NotEqual: { + var comparison = (BinaryExpression)expr; + if (comparison.Method != null && !comparison.IsLifted) + return TryEmitComparisonMethod(comparison, il, ref context, parent); if (exprType.IsPrimitive && Interpreter.TryInterpretBool(out var boolResult, expr, context.CompilerFlags)) { if ((parent & ParentFlags.IgnoreResult) == 0) @@ -5521,6 +5524,37 @@ private static MethodInfo FindBinaryOperandMethod( return null; } + private static bool TryEmitComparisonMethod(BinaryExpression expr, ILGenerator il, ref CompilerContext context, ParentFlags parent) + { + var method = expr.Method; + var parameters = method.GetParameters(); + if (!TryEmit(expr.Left, il, ref context, ParentFlags.Call, parameters[0].ParameterType.IsByRef ? 0 : -1)) + return false; + + // Keep the evaluation stack empty while a nontrivial right operand + // may emit exception regions or jumps, just as for call arguments. + var right = expr.Right; + var spillLeft = right.NodeType != ExpressionType.Parameter && + right.NodeType != ExpressionType.Constant && right.NodeType != ExpressionType.Default; +#if LIGHT_EXPRESSION + spillLeft |= right.IsIntrinsic; +#endif + var leftVar = spillLeft ? EmitStoreLocalVariable(il, parameters[0].ParameterType) : -1; + + if (!TryEmit(expr.Right, il, ref context, ParentFlags.Call, parameters[1].ParameterType.IsByRef ? 1 : -1)) + return false; + if (leftVar != -1) + { + var rightVar = EmitStoreLocalVariable(il, parameters[1].ParameterType); + EmitLoadLocalVariable(il, leftVar); + EmitLoadLocalVariable(il, rightVar); + } + if (!EmitMethodCall(il, method)) + return false; + context.LastEmitIsAddress = false; + return il.EmitPopIfIgnoreResult(parent); + } + private static bool TryEmitComparison( Expression left, Expression right, Type exprType, ExpressionType nodeType, ILGenerator il, ref CompilerContext context, ParentFlags parent) @@ -7310,6 +7344,8 @@ public static bool TryFindSwitchBranch(SwitchExpression switchExpr, CompilerFlag /// Tries to interpret the expression of the Primitive type of Constant, Convert, Logical, Comparison, Arithmetic. internal static bool TryInterpretBool(ref bool resultBool, Expression expr, ExpressionType nodeType) { + if (expr is BinaryExpression binary && binary.Method != null) + return false; // what operations are supported, to get the boolean result? // yes: not, logical, comparison, default // not: negate, arithmetic, convert to bool From f2c784e0bae8d7d01f003a61bf33fb5322b37b9a Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Sat, 24 Oct 2026 08:35:47 +0200 Subject: [PATCH 4/5] fix: avoid comparison operand spills through simple conversions --- .../FastExpressionCompiler.cs | 12 +++- .../Issue561_NonBooleanEqualityOperator.cs | 63 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 1ceee7333..42d84238a 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -5534,10 +5534,16 @@ private static bool TryEmitComparisonMethod(BinaryExpression expr, ILGenerator i // Keep the evaluation stack empty while a nontrivial right operand // may emit exception regions or jumps, just as for call arguments. var right = expr.Right; - var spillLeft = right.NodeType != ExpressionType.Parameter && - right.NodeType != ExpressionType.Constant && right.NodeType != ExpressionType.Default; + var spillLeft = !right.IsParamOrConstantOrDefault(); #if LIGHT_EXPRESSION - spillLeft |= right.IsIntrinsic; + // A conversion chain may contain a custom IL emitter. + for (var operand = right; !spillLeft;) + { + spillLeft = operand.IsIntrinsic; + if (operand is not UnaryExpression convert || convert.NodeType != ExpressionType.Convert) + break; + operand = convert.Operand; + } #endif var leftVar = spillLeft ? EmitStoreLocalVariable(il, parameters[0].ParameterType) : -1; diff --git a/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs b/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs index c5392ca0f..f4be1c2ff 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue561_NonBooleanEqualityOperator.cs @@ -8,6 +8,8 @@ #if LIGHT_EXPRESSION using FastExpressionCompiler.LightExpression.ILDecoder; +using FastExpressionCompiler.LightExpression.ImTools; +using static FastExpressionCompiler.LightExpression.ExpressionCompiler; namespace FastExpressionCompiler.LightExpression.IssueTests; #else using FastExpressionCompiler.ILDecoder; @@ -27,6 +29,10 @@ public void Run(TestRun t) Non_lifted_nullable_parameters_reach_the_selected_method(t); Selected_methods_keep_non_boolean_value_results(t); Boolean_method_is_not_replaced_by_constant_comparison(t); + Converted_simple_right_operands_reach_the_selected_method(t); +#if LIGHT_EXPRESSION + Converted_intrinsic_right_operand_can_emit_exception_regions(t); +#endif } public interface ICustomObject { void Ping(); } @@ -214,5 +220,62 @@ public void Boolean_method_is_not_replaced_by_constant_comparison(TestContext t) t.AreEqual(expected, fast()); t.AreEqual(1, CustomObject.OperatorCalls); } + public void Converted_simple_right_operands_reach_the_selected_method(TestContext t) + { + var parameter = System.Linq.Expressions.Expression.Parameter(typeof(int)); + var method = typeof(Issue561_NonBooleanEqualityOperator).GetMethod(nameof(CompareValue)); + foreach (var right in new System.Linq.Expressions.Expression[] { parameter, + System.Linq.Expressions.Expression.Constant(2), System.Linq.Expressions.Expression.Default(typeof(int)) }) + { + var converted = System.Linq.Expressions.Expression.Convert( + System.Linq.Expressions.Expression.Convert(right, typeof(long)), typeof(int)); + var body = System.Linq.Expressions.Expression.Equal( + System.Linq.Expressions.Expression.Constant(3), converted, false, method); + var expression = System.Linq.Expressions.Expression.Lambda>(body, parameter); + var fast = Compile(expression); + if (!t.IsNotNull(fast) || !CheckSelectedCall(t, fast, method)) return; + var expected = expression.Compile()(4); + CustomObject.OperatorCalls = 0; + t.AreEqual(expected, fast(4)); + t.AreEqual(1, CustomObject.OperatorCalls); + } + } + +#if LIGHT_EXPRESSION + private sealed class TryFinallyConversion : UnaryExpression + { + private readonly Expression _body; + public TryFinallyConversion(Expression operand, Expression body) : base(operand) => _body = body; + public override ExpressionType NodeType => ExpressionType.Convert; + public override bool IsIntrinsic => true; + public override Result TryCollectInfo(ref CompilerContext context, NestedLambdaInfo nestedLambda, + ref SmallList rootNestedLambdas) => + ExpressionCompiler.TryCollectInfo(ref context, _body, nestedLambda, ref rootNestedLambdas); + public override bool TryEmit(ref CompilerContext context, System.Reflection.Emit.ILGenerator il, + ParentFlags parent, int byRefIndex = -1) => + EmittingVisitor.TryEmit(_body, il, ref context, parent, byRefIndex); + } + + public void Converted_intrinsic_right_operand_can_emit_exception_regions(TestContext t) + { + var owner = typeof(Issue561_NonBooleanEqualityOperator); + var parameter = Expression.Parameter(typeof(int)); + var read = Expression.Call(owner.GetMethod(nameof(ReadArgument))); + var intrinsic = new TryFinallyConversion(parameter, + Expression.TryFinally(read, Expression.Call(owner.GetMethod(nameof(FinishArgument))))); + // The intrinsic is hidden inside two ordinary conversion wrappers. + var right = Expression.Convert(Expression.Convert(intrinsic, typeof(long)), typeof(int)); + var method = owner.GetMethod(nameof(CompareValue)); + var body = Expression.Equal(read, right, false, method); + var fast = Expression.Lambda>(body, parameter).CompileFast(true); + if (!t.IsNotNull(fast) || !CheckSelectedCall(t, fast, method)) return; + Trace = ""; + CustomObject.OperatorCalls = 0; + t.AreEqual(22, fast(4)); + t.AreEqual("AAF", Trace); + t.AreEqual(1, CustomObject.OperatorCalls); + } +#endif + } #endif From 3a0576b2b908ce496ea4a640d3d0a92d5c822787 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Sat, 24 Oct 2026 12:14:15 +0200 Subject: [PATCH 5/5] refactor: make intrinsic comparison spill decision explicit --- src/FastExpressionCompiler/FastExpressionCompiler.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 42d84238a..2ddea4df3 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -5539,7 +5539,11 @@ private static bool TryEmitComparisonMethod(BinaryExpression expr, ILGenerator i // A conversion chain may contain a custom IL emitter. for (var operand = right; !spillLeft;) { - spillLeft = operand.IsIntrinsic; + if (operand.IsIntrinsic) + { + spillLeft = true; + break; + } if (operand is not UnaryExpression convert || convert.NodeType != ExpressionType.Convert) break; operand = convert.Operand;