JIT: (bug) `optRedundantDominatingBranch` relop simplification strengthens a compare in a block that has other predecessors
- 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
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