dotnet / dotnet/runtime

JIT: (bug) `optRedundantDominatingBranch` relop simplification strengthens a compare in a block that has other predecessors

Open
#133,980 1 comment 0 reactions 0 assignees View on GitHub
area-CodeGen-coreclr
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

The AND-simplification fallback in `Compiler::optRedundantDominatingBranch` rewrites the *dominated* block's relop in place, but never checks that the dominating block is the only way into that block. When the dominated block has other predecessors, the strengthened condition is applied on paths where the dominating predicate does not hold.

### Minimal Repro

```csharp
using System;
using System.Runtime.CompilerServices;

public static class Program
{
static int s_counter;

[MethodImpl(MethodImplOptions.NoInlining)]
static void SideEffect() => s_counter++;

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
public static int Test(int x, int y)
{
if (x != y)
{
goto B;
}
X:
SideEffect();
if (s_counter > 1000)
{
return -1;
}
B:
if (x <= y)
{
return 1;
}
goto X;
}

public static int Main()
{
Console.WriteLine(Test(5, 5));
s_counter = 0;
Console.WriteLine(Test(3, 7));
s_counter = 0;
Console.WriteLine(Test(9, 2));
return 100;
}
}
```

### Expected

```
1
1
-1
```

`Test(5, 5)`: `5 != 5` is false, so control falls into `X`, calls `SideEffect()` once, then `5 <= 5` is true -> `return 1`. This is what MinOpts produces.

### Actual

```
-1
1
-1
```

`Test(5, 5)` spins in the `X`/`B` loop until the `s_counter > 1000` guard fires. In the codegen the entry test became `jl` (`x < y`) and the loop test `jge`, so `x == y` never reaches `return 1`.

### Notes

Deterministic. Does NOT reproduce on released .NET 10.0.12 — a `main`-only regression.

Root cause: `optRelopImpliesRelop` cannot prove `LE ==> NE`, so the pass falls into the simplification fallback, derives `LE(x,y) AND NE(x,y) ==> LT(x,y)`, folds the dominator's `JTRUE` and then does `tree->SetOper(newRelop)` on the *dominated* block's relop. Merging the two conditions is only sound if every execution of that block comes through the dominator; here the shared successor also flows back into it with `x == y`. The classic (non-simplifying) path is unaffected because it only bashes the dominator's relop to a constant. Introduced by #127181 (`19f4dfc70e7`).

Contributor guide

Open the contributing guide

Research direction

Start at Compiler::optRedundantDominatingBranch and reproduce the supplied C# program, comparing its output with the expected values. Trace the AND-simplification fallback and the dominated block's relop update; done means the shared-predecessor case preserves the expected output without regressing the unaffected path.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.