Skip to content

Fix non-Boolean comparison operator results (#561) - #562

Merged
dadhi merged 5 commits into
dadhi:masterfrom
jogibear9988:test/issue-561-non-bool-equality
Oct 24, 2026
Merged

dadhi merged 5 commits into
dadhi:masterfrom
jogibear9988:test/issue-561-non-bool-equality

Conversation

@jogibear9988

@jogibear9988 jogibear9988 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #561.

A comparison operator returning an interface currently emits ceq (or an invalid object.Equals call) instead of its selected method. The integer result is returned as an object reference, causing the reported fatal CLR error. Emit the exact non-lifted BinaryExpression.Method, preserve its return type and operand evaluation order, and discard ignored results correctly. Prevent Boolean constant interpretation from dropping method-backed binary calls and their side effects.

The production change is 46 added lines in one file. It uses existing emission/local-storage helpers, without constructing replacement expression nodes or changing compiler context layout. Lifted comparison handling stays on its existing path. This is based directly on current master (3aefb5e5) and has no dependency on #558.

Regression coverage includes ==/!=, both operand orders, interface and value results, discarded results, nested try/finally evaluation order, nullable-accepting non-lifted methods, and constant Boolean methods. The modern native fixture checks for the selected call before invoking a potentially corrupt delegate, without asserting an exact IL layout.

Validation at a7deb9e8:

  • Production Standard/Light builds pass across their configured targets.

  • Full .NET 10 ARM64 and .NET 8 x64 native suites pass (1,714/1,713 legacy cases, plus registered regressions).

  • Framework native suites pass: 97 TestX and 1,597 legacy cases.

  • All nine targeted regression methods pass for Standard/Light on .NET 8/10, ARM64/x64; the baseline fails in all eight configurations. All 50 isolated raw repro variants pass with the fix, including Framework and debug mode. Strict FEC compilation is used throughout.

Tests ran on Windows 11 ARM64; x64 uses the installed x64 host under emulation. The full .NET 8 ARM64 suite hits the pre-existing by-ref float post-increment crash in Issue346. Isolated clean-master and fixed-source probes reproduce the same crash with identical IL on ARM64; both probes pass on x64 using the same 8.0.28 runtime patch. No test is skipped or changed to hide it. Framework's dynamic-method decoder is unavailable, so its issue-specific raw repros run in child processes rather than the guarded modern fixture.

Review follow-up at f2c784e0: reuse IsParamOrConstantOrDefault to avoid unnecessary spills through ordinary conversions, while retaining intrinsic-emitter handling throughout LightExpression conversion chains. Added converted parameter/constant/default coverage and a nested intrinsic try/finally regression. All 10 Standard / 11 Light targeted methods pass on .NET 8/10, ARM64/x64; full .NET 10 ARM64, .NET 8 x64, and Framework suites pass again. The validation above at a7deb9e8 remains the original baseline/fix and isolated repro evidence.

Readability follow-up at 3a0576b2: make the intrinsic early exit explicit. All targeted methods pass again across the same eight Standard/Light, .NET 8/10, ARM64/x64 configurations.

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 3aefb5e and pass against PR558 ae4d9a5 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.
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.
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.
@dadhi

dadhi commented Oct 22, 2026

Copy link
Copy Markdown
Owner

generally looks good. will double check tomorrow. thank you!

Comment thread src/FastExpressionCompiler/FastExpressionCompiler.cs Outdated
// A conversion chain may contain a custom IL emitter.
for (var operand = right; !spillLeft;)
{
spillLeft = operand.IsIntrinsic;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this code doing, spillLeft will be overwriten by the next operand, until no more conversion. So what this comment assumes is not true - it overriten by the last operand anyway. // A conversion chain may contain a custom IL emitter.

Also you need to test such special case with Intrinsic.

The current code as whole may can be replaced by:

                // 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 operand = right.StripConvertRecursively();
                var spillLeft = operand.NodeType == ExpressionType.Parameter
                    | operand.NodeType == ExpressionType.Constant
                    | operand.NodeType == ExpressionType.Default;
#if LIGHT_EXPRESSION
                spillLeft |= operand.IsIntrinsic;
#endif

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made the early exit explicit in 3a0576b:

if (operand.IsIntrinsic)
{
    spillLeft = true;
    break;
}

The previous loop also stopped once spillLeft became true because its condition was !spillLeft, so later operands could not overwrite that decision. The explicit break makes this easier to see.

The reason for inspecting the conversion chain is that a conversion node itself can be intrinsic. StripConvertRecursively() would skip that node and could leave just its ordinary parameter operand, losing the information that custom IL will be emitted. Also, the simple-node predicate in the suggested replacement needs negation: simple operands should avoid spilling; nontrivial operands need the empty evaluation stack.

The special-case test was added in the previous commit: Converted_intrinsic_right_operand_can_emit_exception_regions. Its intrinsic conversion declares a parameter operand but emits a try/finally expression, and is wrapped in two ordinary conversions. It checks the result, evaluation trace, and operator call count.

Rebuilt Standard/Light for .NET 8, .NET 10, and Framework. Reran all 10 Standard / 11 Light targeted regression methods on .NET 8/10 with ARM64/x64 hosts: all pass, including the intrinsic test. This commit changes only the loop's readability; the full-suite results from f2c784e remain documented separately.

@dadhi
dadhi marked this pull request as ready for review October 24, 2026 10:36
@dadhi

dadhi commented Oct 24, 2026

Copy link
Copy Markdown
Owner

thank you, merging

@dadhi
dadhi merged commit 5473b42 into dadhi:master Oct 24, 2026
2 checks passed
@jogibear9988
jogibear9988 deleted the test/issue-561-non-bool-equality branch October 24, 2026 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Internal CLR error. (0x80131506) from CompileFast when a custom equality operator's returns non bool value

2 participants