JIT: don't keep a stale range when assertion tightening yields an empty range - #132898
Open
EgorBo wants to merge 3 commits into
Open
JIT: don't keep a stale range when assertion tightening yields an empty range#132898EgorBo wants to merge 3 commits into
EgorBo wants to merge 3 commits into
Conversation
…ty range RangeCheck::MergeEdgeAssertionsWorker tightens *pRange with each incoming assertion. When the tightened range came out empty it logged "invalid range after tightening" and returned, leaving the previously tightened range in place. That range has just been contradicted by the assertions, so the caller ends up holding a fact that was disproven. Keeping it would only be safe if the assertion set really held at that point (then the block is unreachable and any range is vacuously fine). It does not always hold: ComputeRangeForLocalDef merges the use block's bbAssertionIn into the range of a definition that lives in another block, and that set can contain mutually exclusive assertions. Since #129354 assertion propagation folds conditions using these ranges, so the stale range turns into wrong code. In #132879 the range of an enum local was computed as [5..7] while its value was 78, which folded the "op > 69" test guarding a switch to false and sent execution to the default case. Bail out to Unknown instead. Fixes #132879 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 71e21c42-60e7-47f1-9026-4bf2df7a8e51
Contributor
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes an unsound RangeCheck range-tightening behavior where encountering contradictory assertions could leave a previously-tightened (stale) range in place, enabling incorrect assertion-prop folding in optimized JIT/crossgen2 scenarios. Adds a targeted JIT regression test for the reported miscompile involving an enum-backed switch.
Changes:
- In
RangeCheck::MergeEdgeAssertionsWorker, when tightening yields an empty range, explicitly bail out by setting*pRangetoUnknowninstead of leaving the prior range intact. - Add a new JitBlue regression test (
Runtime_132879) exercising the miscompile shape underAggressiveOptimization. - Wire the new test into
Regression_ro_2.csproj.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/coreclr/jit/rangecheck.cpp | Makes contradictory assertion tightening conservative by resetting the output range to Unknown on empty intersections. |
| src/tests/JIT/Regression/JitBlue/Runtime_132879/Runtime_132879.cs | New xUnit regression test covering the switch/enum miscompile scenario. |
| src/tests/JIT/Regression/Regression_ro_2.csproj | Includes the new regression test in the merged test project. |
Comment on lines
1679
to
+1683
| JITDUMP("invalid range after tightening\n"); | ||
| // The tightened range is empty, i.e. the assertions contradict the range we computed. | ||
| // If the assertion set really did hold here the block would be unreachable, but we get | ||
| // here with sets that do not hold at this point (e.g. assertions of a use block merged | ||
| // into the range of a definition that lives in another block). Returning while leaving |
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/coreclr/jit/rangecheck.cpp:1686
assertedRange.IsValid()bail-out returns without resetting*pRange. SinceassertedRangeis accumulated across assertions in this loop, it can become invalid when assertions contradict each other; returning here can still leak a partially-tightened (stale) range to the caller, similar to the empty-range case fixed below. To keep range inference sound in the presence of self-contradictory assertion sets, set*pRangeto Unknown before returning (same pattern as thecopy.IsValid()failure path).
JITDUMP("invalid range after tightening\n");
// The tightened range is empty, i.e. the assertions contradict the range we computed.
// If the assertion set really did hold here the block would be unreachable, but we get
// here with sets that do not hold at this point (e.g. assertions of a use block merged
// into the range of a definition that lives in another block). Returning while leaving
// the previously tightened range in place lets a fact that has just been disproven
// escape to the caller, which then folds branches with it. Bail out to Unknown instead.
*pRange = Range(Limit(Limit::keUnknown));
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RangeCheck::MergeEdgeAssertionsWorkertightens*pRangewith each incoming assertion. When tightening yielded an empty range it returned early and left the previously tightened range in place -- a range the assertions had just contradicted. That is only safe if the assertion set truly holds (then the block is unreachable), butComputeRangeForLocalDefmerges the use block'sbbAssertionIninto the range of a definition in another block, and that set can be self-contradictory.Since #129354 assertion prop folds conditions using these ranges, so the stale range becomes wrong code. In #132879 an enum local's range was computed as
[5..7]while its value was78, folding theop > 69test guarding a switch tofalseand sending execution todefault:Fix: bail out to
Unknown..NET 11 regression (.NET 10 unaffected). Needs full opts without PGO, which is what crossgen2 does -- hence it only showed up on a
PublishReadyToRun=truepublish, and whyDOTNET_ReadyToRun=0didn't help.Same root cause as #132841, whose analysis points at this exact
return. #132849 fixes theComputeDoesOverflowpath instead: that resolves #132841's repro but not #132879 (verified). This change fixes both, so #132849 may be redundant.SPMI: replay clean on benchmarks.run + libraries.pmi; asmdiffs +48 bytes / 2 contexts (benchmarks.run), +3172 bytes / 46 contexts (libraries.pmi), 0.00% both.
Regression test uses
AggressiveOptimizationsince tier1 profile data hides the bad range.Fixes #132879