Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions src/coreclr/jit/assertionprop.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1525,7 +1525,10 @@ void Compiler::optDebugCheckAssertion(const AssertionDsc& assertion) const
case O2K_VN_ADD_CNS:
assert(!optLocalAssertionProp);
assert(assertion.GetOp1().KindIs(O1K_VN));
assert(assertion.IsRelop());
// Most O2K_VN_ADD_CNS assertions are ordered relops ("i <relop> bnd + cns"), but
// we also create equality assertions against a checked bound (e.g. "i != arr.Length")
// for use by RangeCheck.
assert(assertion.IsRelop() || assertion.CanPropEqualOrNotEqual());
break;

case O2K_ZEROOBJ:
Expand DownExpand Up@@ -1678,6 +1681,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
}

bool isUnsignedRelop;
bool isEqualityRelop = false;
if (relopFuncApp.FuncIs(VNF_LE, VNF_LT, VNF_GE, VNF_GT))
{
isUnsignedRelop = false;
Expand All@@ -1686,10 +1690,20 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
{
isUnsignedRelop = true;
}
else if (relopFuncApp.FuncIs(VNF_EQ, VNF_NE))
{
// Equality relops against a checked bound (e.g. "i != arr.Length") are
// useful for RangeCheck to tighten ranges on loop back-edges. They flow
// through the CheckedBound paths below; other equality assertions
// (against constants, locals, type handles, etc.) are handled in
// optAssertionGenJtrue.
isUnsignedRelop = false;
isEqualityRelop = true;
}
else
{
// Not a relop we're interested in.
// Assertions for NE/EQ are handled elsewhere.
// Assertions for EQ/NE not against a checked bound are handled elsewhere.
return NO_ASSERTION_INDEX;
}

Expand All@@ -1706,21 +1720,42 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
// "CheckedBnd <relop> X"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op1VN))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// For equality relops where the non-bound side is a constant (e.g. "len != 0"), the
// LCLVAR-based equality assertion created below by optAssertionGenJtrue is strictly more
// useful than a CompareCheckedBound form -- downstream consumers (folding bounds checks,
// proving "len > 0" after a "len != 0" test) only recognize the LCLVAR form. Skip the new
// assertion in that case and let the LCLVAR path produce it.
if (!(isEqualityRelop && vnStore->IsVNConstant(op2VN)))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// "X <relop> CheckedBnd"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op2VN))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// Symmetric guard: leave constant-vs-CheckedBound equality assertions to the LCLVAR path.
if (!(isEqualityRelop && vnStore->IsVNConstant(op1VN)))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// The remaining "(CheckedBnd + CNS) <relop> X" cases are only useful when the
// comparison is ordered (LT/LE/GT/GE). For equality relops we don't produce
// CheckedBoundAddConst-shaped assertions; the consumers (RangeCheck) only
// tighten ranges from equality assertions whose RHS is the bound itself.
if (isEqualityRelop)
{
return NO_ASSERTION_INDEX;
}

// "(CheckedBnd + CNS) <relop> X"
Expand Down
32 changes: 26 additions & 6 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -6848,9 +6848,20 @@ genTreeOps NaturalLoopIterInfo::TestOper()
bool NaturalLoopIterInfo::IsIncreasingLoop()
{
// Increasing loop is the one that has "+=" increment operation and "< or <=" limit check.
bool isLessThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_LT, GT_LE);
return (isLessThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0))));
// We also recognize "!=" against a limit when the IV step is exactly +1 (or -1 with GT_SUB):
// such loops visit indices [init, limit) provided that "init <= limit" holds on entry.
// Stride must be ±1 to avoid parity/overflow issues where the IV could skip past the limit
// (e.g. "for (i = 0; i != 5; i += 2)" never terminates and wraps around).
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_LT, GT_LE))
{
return (((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == 1)) || ((IterOper() == GT_SUB) && (IterConst() == -1));
}
return false;
}

//------------------------------------------------------------------------
Expand All@@ -6864,9 +6875,18 @@ bool NaturalLoopIterInfo::IsDecreasingLoop()
{
// Decreasing loop is the one that has "-=" decrement operation and "> or >=" limit check. If the operation is
// "+=", make sure the constant is negative to give an effect of decrementing the iterator.
bool isGreaterThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_GT, GT_GE);
return (isGreaterThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0))));
// As with IsIncreasingLoop, we also recognize "!=" against a limit when the IV step is exactly -1
// (or +1 with GT_SUB); the stride must be exactly +/-1 to avoid parity/overflow issues.
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_GT, GT_GE))
{
return (((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == -1)) || ((IterOper() == GT_SUB) && (IterConst() == 1));
}
return false;
}

//------------------------------------------------------------------------
Expand Down
81 changes: 74 additions & 7 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1189,10 +1189,11 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

NaturalLoopIterInfo* iterInfo = context->GetLoopIterInfo(loop->GetIndex());
// Note we see cases where the test oper is NE (array.Len) which we could handle
// with some extra care.
// Loop tests we can reason about for cloning: ordered relops (LT/LE/GT/GE) and
// NE limits (treated as LT/GT-equivalent when stride is exactly +/-1; see
// NaturalLoopIterInfo::IsIncreasingLoop/IsDecreasingLoop).
//
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE))
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE, GT_NE))
{
// We can't reason about how this loop iterates
return false;
Expand DownExpand Up@@ -1312,9 +1313,17 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
// adjusted for IsReversed and ExitedOnTrue. For GT_NE we substitute an
// ordered relop (LT for increasing, GT for decreasing) so that the
// runtime guard strictly orders init and limit. Using the raw "init !=
// limit" form would let a misordered init pass while the fast clone
// (with bounds checks removed) wraps the IV through the type.
genTreeOps zeroTripOp = iterInfo->TestOper();
if (zeroTripOp == GT_NE)
{
zeroTripOp = iterInfo->IsIncreasingLoop() ? GT_LT : GT_GT;
}
LC_Condition zeroTrip(zeroTripOp, LC_Expr(initIdent), LC_Expr(limitIdent), iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}
Expand DownExpand Up@@ -1427,20 +1436,30 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
// GT_LT loop test: (start < end) ==> (end <= arrLen)
// GT_LE loop test: (start <= end) ==> (end < arrLen)
//
// GT_NE loop test (stride = +/-1; see IsIncreasing/DecreasingLoop):
// For increasing: visited indices are [init..end-1] => guard end <= arrLen (same as LT).
// `ident` is the loop's end value, so the per-access condition is (end <= arrLen).
// For decreasing: visited indices are [end+1..init] => guard init < arrLen (same as GT).
// `ident` is the loop's init value (set in the init-conditions section above and not
// overwritten by the GT_NE limit branch), so the per-access condition is
// (init < arrLen). This is why the switch below maps decreasing GT_NE to GT_LT.
//
// Decreasing loops
// Always check if iter var is less than array length.
genTreeOps opLimitCondition;
switch (iterInfo->TestOper())
{
case GT_LT:

opLimitCondition = GT_LE;
break;
case GT_LE:
case GT_GE:
case GT_GT:
opLimitCondition = GT_LT;
break;
case GT_NE:
opLimitCondition = isIncreasingLoop ? GT_LE : GT_LT;
break;
default:
unreached();
}
Expand DownExpand Up@@ -1496,6 +1515,54 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
}

// For GT_NE loops (with stride exactly +/-1; see IsIncreasing/DecreasingLoop),
// the cloned fast path preserves the "i != limit" exit test. If the IV starts
// past the limit, the loop would wrap around the type and access arbitrary
// memory because the fast path has its bounds checks removed. Guard the fast
// path with an ordered "init RELOP limit" condition (init <= limit for
// increasing, init >= limit for decreasing) so that a misordered init falls
// back to the slow path with bounds checks.
if (iterInfo->TestOper() == GT_NE)
{
LC_Ident neInitIdent;
if (iterInfo->HasConstInit)
{
assert(iterInfo->ConstInitValue >= 0);
neInitIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
const unsigned initLcl = iterInfo->IterVar;
assert(genActualTypeIsInt(lvaGetDesc(initLcl)));
neInitIdent = LC_Ident::CreateVar(initLcl, iterInfo->Iterator()->TypeGet());
}

LC_Ident neLimitIdent;
if (iterInfo->HasConstLimit)
{
const int limit = iterInfo->ConstLimit();
assert(limit >= 0);
neLimitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
assert(genActualTypeIsInt(lvaGetDesc(limitLcl)));
neLimitIdent = LC_Ident::CreateVar(limitLcl, iterInfo->Limit()->TypeGet());
}
else
{
assert(iterInfo->HasArrayLengthLimit);
assert(limitArrIndex != nullptr);
neLimitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}

const genTreeOps cmpOp = isIncreasingLoop ? GT_LE : GT_GE;
LC_Condition initLimitCond(cmpOp, LC_Expr(neInitIdent), LC_Expr(neLimitIdent));
context->EnsureConditions(loop->GetIndex())->Push(initLimitCond);
JITDUMP("Added NE init-vs-limit cloning condition\n");
}

JITDUMP("Conditions: ");
DBEXEC(verbose, context->PrintConditions(loop->GetIndex()));
JITDUMP("\n");
Expand Down
42 changes: 42 additions & 0 deletions src/coreclr/jit/rangecheck.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1337,6 +1337,48 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp
}
}
}
// Current assertion is of the form "X != (BoundVN + 0)" or "X == (BoundVN + 0)"
// where BoundVN is a length-like checked bound (e.g. an array length). These are
// generated from loop exit tests written as "i != arr.Length"; we can tighten the
// induction variable's range when its current upper/lower limit equals the bound.
else if (canUseCheckedBounds && curAssertion.KindIs(Compiler::OAK_EQUAL, Compiler::OAK_NOT_EQUAL) &&
(curAssertion.GetOp1().GetVN() == normalLclVN) &&
curAssertion.GetOp2().KindIs(Compiler::O2K_VN_ADD_CNS) && (curAssertion.GetOp2().GetCns() == 0) &&
comp->vnStore->IsVNCheckedBound(curAssertion.GetOp2().GetVN()))
{
const ValueNum boundVN = curAssertion.GetOp2().GetVN();

if (curAssertion.KindIs(Compiler::OAK_EQUAL))
{
// X == bound: range tightens to exactly [bound, bound].
limit = Limit(Limit::keBinOpArray, boundVN, 0);
cmpOper = GT_EQ;
}
else
{
// X != bound: only useful when pRange's upper or lower limit already equals
// the bound. Tighten:
// [lo, bound] -> [lo, bound - 1]
// [bound, hi] -> [bound + 1, hi]
if (pRange->UpperLimit().IsBinOpArray() && (pRange->UpperLimit().vn == boundVN) &&
(pRange->UpperLimit().GetConstant() == 0))

@EgorBoEgorBoJun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

presumably we don't have to check pRange->UpperLimit().GetConstant() == 0 ?
e.g. [lo, bound - 1] -> [lo, bound - 2]
ah, actually nvm, it's not correct

{
limit = Limit(Limit::keBinOpArray, boundVN, -1);
cmpOper = GT_LE;
}
else if (pRange->LowerLimit().IsBinOpArray() && (pRange->LowerLimit().vn == boundVN) &&
(pRange->LowerLimit().GetConstant() == 0))
{
limit = Limit(Limit::keBinOpArray, boundVN, 1);
cmpOper = GT_GE;
}
else
{
// Nothing to deduce from this assertion at this site.
continue;
}
}
}
// Current assertion asserts a bounds check does not throw
else if (curAssertion.IsBoundsCheckNoThrow())
{
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions src/coreclr/jit/assertionprop.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1525,7 +1525,10 @@ void Compiler::optDebugCheckAssertion(const AssertionDsc& assertion) const
case O2K_VN_ADD_CNS:
assert(!optLocalAssertionProp);
assert(assertion.GetOp1().KindIs(O1K_VN));
assert(assertion.IsRelop());
// Most O2K_VN_ADD_CNS assertions are ordered relops ("i <relop> bnd + cns"), but
// we also create equality assertions against a checked bound (e.g. "i != arr.Length")
// for use by RangeCheck.
assert(assertion.IsRelop() || assertion.CanPropEqualOrNotEqual());
break;

case O2K_ZEROOBJ:
Expand DownExpand Up@@ -1678,6 +1681,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
}

bool isUnsignedRelop;
bool isEqualityRelop = false;
if (relopFuncApp.FuncIs(VNF_LE, VNF_LT, VNF_GE, VNF_GT))
{
isUnsignedRelop = false;
Expand All@@ -1686,10 +1690,20 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
{
isUnsignedRelop = true;
}
else if (relopFuncApp.FuncIs(VNF_EQ, VNF_NE))
{
// Equality relops against a checked bound (e.g. "i != arr.Length") are
// useful for RangeCheck to tighten ranges on loop back-edges. They flow
// through the CheckedBound paths below; other equality assertions
// (against constants, locals, type handles, etc.) are handled in
// optAssertionGenJtrue.
isUnsignedRelop = false;
isEqualityRelop = true;
}
else
{
// Not a relop we're interested in.
// Assertions for NE/EQ are handled elsewhere.
// Assertions for EQ/NE not against a checked bound are handled elsewhere.
return NO_ASSERTION_INDEX;
}

Expand All@@ -1706,21 +1720,42 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
// "CheckedBnd <relop> X"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op1VN))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// For equality relops where the non-bound side is a constant (e.g. "len != 0"), the
// LCLVAR-based equality assertion created below by optAssertionGenJtrue is strictly more
// useful than a CompareCheckedBound form -- downstream consumers (folding bounds checks,
// proving "len > 0" after a "len != 0" test) only recognize the LCLVAR form. Skip the new
// assertion in that case and let the LCLVAR path produce it.
if (!(isEqualityRelop && vnStore->IsVNConstant(op2VN)))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// "X <relop> CheckedBnd"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op2VN))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// Symmetric guard: leave constant-vs-CheckedBound equality assertions to the LCLVAR path.
if (!(isEqualityRelop && vnStore->IsVNConstant(op1VN)))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// The remaining "(CheckedBnd + CNS) <relop> X" cases are only useful when the
// comparison is ordered (LT/LE/GT/GE). For equality relops we don't produce
// CheckedBoundAddConst-shaped assertions; the consumers (RangeCheck) only
// tighten ranges from equality assertions whose RHS is the bound itself.
if (isEqualityRelop)
{
return NO_ASSERTION_INDEX;
}

// "(CheckedBnd + CNS) <relop> X"
Expand Down
32 changes: 26 additions & 6 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -6848,9 +6848,20 @@ genTreeOps NaturalLoopIterInfo::TestOper()
bool NaturalLoopIterInfo::IsIncreasingLoop()
{
// Increasing loop is the one that has "+=" increment operation and "< or <=" limit check.
bool isLessThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_LT, GT_LE);
return (isLessThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0))));
// We also recognize "!=" against a limit when the IV step is exactly +1 (or -1 with GT_SUB):
// such loops visit indices [init, limit) provided that "init <= limit" holds on entry.
// Stride must be ±1 to avoid parity/overflow issues where the IV could skip past the limit
// (e.g. "for (i = 0; i != 5; i += 2)" never terminates and wraps around).
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_LT, GT_LE))
{
return (((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == 1)) || ((IterOper() == GT_SUB) && (IterConst() == -1));
}
return false;
}

//------------------------------------------------------------------------
Expand All@@ -6864,9 +6875,18 @@ bool NaturalLoopIterInfo::IsDecreasingLoop()
{
// Decreasing loop is the one that has "-=" decrement operation and "> or >=" limit check. If the operation is
// "+=", make sure the constant is negative to give an effect of decrementing the iterator.
bool isGreaterThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_GT, GT_GE);
return (isGreaterThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0))));
// As with IsIncreasingLoop, we also recognize "!=" against a limit when the IV step is exactly -1
// (or +1 with GT_SUB); the stride must be exactly +/-1 to avoid parity/overflow issues.
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_GT, GT_GE))
{
return (((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == -1)) || ((IterOper() == GT_SUB) && (IterConst() == 1));
}
return false;
}

//------------------------------------------------------------------------
Expand Down
81 changes: 74 additions & 7 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1189,10 +1189,11 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

NaturalLoopIterInfo* iterInfo = context->GetLoopIterInfo(loop->GetIndex());
// Note we see cases where the test oper is NE (array.Len) which we could handle
// with some extra care.
// Loop tests we can reason about for cloning: ordered relops (LT/LE/GT/GE) and
// NE limits (treated as LT/GT-equivalent when stride is exactly +/-1; see
// NaturalLoopIterInfo::IsIncreasingLoop/IsDecreasingLoop).
//
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE))
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE, GT_NE))
{
// We can't reason about how this loop iterates
return false;
Expand DownExpand Up@@ -1312,9 +1313,17 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
// adjusted for IsReversed and ExitedOnTrue. For GT_NE we substitute an
// ordered relop (LT for increasing, GT for decreasing) so that the
// runtime guard strictly orders init and limit. Using the raw "init !=
// limit" form would let a misordered init pass while the fast clone
// (with bounds checks removed) wraps the IV through the type.
genTreeOps zeroTripOp = iterInfo->TestOper();
if (zeroTripOp == GT_NE)
{
zeroTripOp = iterInfo->IsIncreasingLoop() ? GT_LT : GT_GT;
}
LC_Condition zeroTrip(zeroTripOp, LC_Expr(initIdent), LC_Expr(limitIdent), iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}
Expand DownExpand Up@@ -1427,20 +1436,30 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
// GT_LT loop test: (start < end) ==> (end <= arrLen)
// GT_LE loop test: (start <= end) ==> (end < arrLen)
//
// GT_NE loop test (stride = +/-1; see IsIncreasing/DecreasingLoop):
// For increasing: visited indices are [init..end-1] => guard end <= arrLen (same as LT).
// `ident` is the loop's end value, so the per-access condition is (end <= arrLen).
// For decreasing: visited indices are [end+1..init] => guard init < arrLen (same as GT).
// `ident` is the loop's init value (set in the init-conditions section above and not
// overwritten by the GT_NE limit branch), so the per-access condition is
// (init < arrLen). This is why the switch below maps decreasing GT_NE to GT_LT.
//
// Decreasing loops
// Always check if iter var is less than array length.
genTreeOps opLimitCondition;
switch (iterInfo->TestOper())
{
case GT_LT:

opLimitCondition = GT_LE;
break;
case GT_LE:
case GT_GE:
case GT_GT:
opLimitCondition = GT_LT;
break;
case GT_NE:
opLimitCondition = isIncreasingLoop ? GT_LE : GT_LT;
break;
default:
unreached();
}
Expand DownExpand Up@@ -1496,6 +1515,54 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
}

// For GT_NE loops (with stride exactly +/-1; see IsIncreasing/DecreasingLoop),
// the cloned fast path preserves the "i != limit" exit test. If the IV starts
// past the limit, the loop would wrap around the type and access arbitrary
// memory because the fast path has its bounds checks removed. Guard the fast
// path with an ordered "init RELOP limit" condition (init <= limit for
// increasing, init >= limit for decreasing) so that a misordered init falls
// back to the slow path with bounds checks.
if (iterInfo->TestOper() == GT_NE)
{
LC_Ident neInitIdent;
if (iterInfo->HasConstInit)
{
assert(iterInfo->ConstInitValue >= 0);
neInitIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
const unsigned initLcl = iterInfo->IterVar;
assert(genActualTypeIsInt(lvaGetDesc(initLcl)));
neInitIdent = LC_Ident::CreateVar(initLcl, iterInfo->Iterator()->TypeGet());
}

LC_Ident neLimitIdent;
if (iterInfo->HasConstLimit)
{
const int limit = iterInfo->ConstLimit();
assert(limit >= 0);
neLimitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
assert(genActualTypeIsInt(lvaGetDesc(limitLcl)));
neLimitIdent = LC_Ident::CreateVar(limitLcl, iterInfo->Limit()->TypeGet());
}
else
{
assert(iterInfo->HasArrayLengthLimit);
assert(limitArrIndex != nullptr);
neLimitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}

const genTreeOps cmpOp = isIncreasingLoop ? GT_LE : GT_GE;
LC_Condition initLimitCond(cmpOp, LC_Expr(neInitIdent), LC_Expr(neLimitIdent));
context->EnsureConditions(loop->GetIndex())->Push(initLimitCond);
JITDUMP("Added NE init-vs-limit cloning condition\n");
}

JITDUMP("Conditions: ");
DBEXEC(verbose, context->PrintConditions(loop->GetIndex()));
JITDUMP("\n");
Expand Down
42 changes: 42 additions & 0 deletions src/coreclr/jit/rangecheck.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1337,6 +1337,48 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp
}
}
}
// Current assertion is of the form "X != (BoundVN + 0)" or "X == (BoundVN + 0)"
// where BoundVN is a length-like checked bound (e.g. an array length). These are
// generated from loop exit tests written as "i != arr.Length"; we can tighten the
// induction variable's range when its current upper/lower limit equals the bound.
else if (canUseCheckedBounds && curAssertion.KindIs(Compiler::OAK_EQUAL, Compiler::OAK_NOT_EQUAL) &&
(curAssertion.GetOp1().GetVN() == normalLclVN) &&
curAssertion.GetOp2().KindIs(Compiler::O2K_VN_ADD_CNS) && (curAssertion.GetOp2().GetCns() == 0) &&
comp->vnStore->IsVNCheckedBound(curAssertion.GetOp2().GetVN()))
{
const ValueNum boundVN = curAssertion.GetOp2().GetVN();

if (curAssertion.KindIs(Compiler::OAK_EQUAL))
{
// X == bound: range tightens to exactly [bound, bound].
limit = Limit(Limit::keBinOpArray, boundVN, 0);
cmpOper = GT_EQ;
}
else
{
// X != bound: only useful when pRange's upper or lower limit already equals
// the bound. Tighten:
// [lo, bound] -> [lo, bound - 1]
// [bound, hi] -> [bound + 1, hi]
if (pRange->UpperLimit().IsBinOpArray() && (pRange->UpperLimit().vn == boundVN) &&
(pRange->UpperLimit().GetConstant() == 0))

@EgorBoEgorBoJun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

presumably we don't have to check pRange->UpperLimit().GetConstant() == 0 ?
e.g. [lo, bound - 1] -> [lo, bound - 2]
ah, actually nvm, it's not correct

{
limit = Limit(Limit::keBinOpArray, boundVN, -1);
cmpOper = GT_LE;
}
else if (pRange->LowerLimit().IsBinOpArray() && (pRange->LowerLimit().vn == boundVN) &&
(pRange->LowerLimit().GetConstant() == 0))
{
limit = Limit(Limit::keBinOpArray, boundVN, 1);
cmpOper = GT_GE;
}
else
{
// Nothing to deduce from this assertion at this site.
continue;
}
}
}
// Current assertion asserts a bounds check does not throw
else if (curAssertion.IsBoundsCheckNoThrow())
{
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions src/coreclr/jit/assertionprop.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1525,7 +1525,10 @@ void Compiler::optDebugCheckAssertion(const AssertionDsc& assertion) const
case O2K_VN_ADD_CNS:
assert(!optLocalAssertionProp);
assert(assertion.GetOp1().KindIs(O1K_VN));
assert(assertion.IsRelop());
// Most O2K_VN_ADD_CNS assertions are ordered relops ("i <relop> bnd + cns"), but
// we also create equality assertions against a checked bound (e.g. "i != arr.Length")
// for use by RangeCheck.
assert(assertion.IsRelop() || assertion.CanPropEqualOrNotEqual());
break;

case O2K_ZEROOBJ:
Expand DownExpand Up@@ -1678,6 +1681,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
}

bool isUnsignedRelop;
bool isEqualityRelop = false;
if (relopFuncApp.FuncIs(VNF_LE, VNF_LT, VNF_GE, VNF_GT))
{
isUnsignedRelop = false;
Expand All@@ -1686,10 +1690,20 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
{
isUnsignedRelop = true;
}
else if (relopFuncApp.FuncIs(VNF_EQ, VNF_NE))
{
// Equality relops against a checked bound (e.g. "i != arr.Length") are
// useful for RangeCheck to tighten ranges on loop back-edges. They flow
// through the CheckedBound paths below; other equality assertions
// (against constants, locals, type handles, etc.) are handled in
// optAssertionGenJtrue.
isUnsignedRelop = false;
isEqualityRelop = true;
}
else
{
// Not a relop we're interested in.
// Assertions for NE/EQ are handled elsewhere.
// Assertions for EQ/NE not against a checked bound are handled elsewhere.
return NO_ASSERTION_INDEX;
}

Expand All@@ -1706,21 +1720,42 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
// "CheckedBnd <relop> X"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op1VN))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// For equality relops where the non-bound side is a constant (e.g. "len != 0"), the
// LCLVAR-based equality assertion created below by optAssertionGenJtrue is strictly more
// useful than a CompareCheckedBound form -- downstream consumers (folding bounds checks,
// proving "len > 0" after a "len != 0" test) only recognize the LCLVAR form. Skip the new
// assertion in that case and let the LCLVAR path produce it.
if (!(isEqualityRelop && vnStore->IsVNConstant(op2VN)))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// "X <relop> CheckedBnd"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op2VN))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// Symmetric guard: leave constant-vs-CheckedBound equality assertions to the LCLVAR path.
if (!(isEqualityRelop && vnStore->IsVNConstant(op1VN)))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// The remaining "(CheckedBnd + CNS) <relop> X" cases are only useful when the
// comparison is ordered (LT/LE/GT/GE). For equality relops we don't produce
// CheckedBoundAddConst-shaped assertions; the consumers (RangeCheck) only
// tighten ranges from equality assertions whose RHS is the bound itself.
if (isEqualityRelop)
{
return NO_ASSERTION_INDEX;
}

// "(CheckedBnd + CNS) <relop> X"
Expand Down
32 changes: 26 additions & 6 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -6848,9 +6848,20 @@ genTreeOps NaturalLoopIterInfo::TestOper()
bool NaturalLoopIterInfo::IsIncreasingLoop()
{
// Increasing loop is the one that has "+=" increment operation and "< or <=" limit check.
bool isLessThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_LT, GT_LE);
return (isLessThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0))));
// We also recognize "!=" against a limit when the IV step is exactly +1 (or -1 with GT_SUB):
// such loops visit indices [init, limit) provided that "init <= limit" holds on entry.
// Stride must be ±1 to avoid parity/overflow issues where the IV could skip past the limit
// (e.g. "for (i = 0; i != 5; i += 2)" never terminates and wraps around).
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_LT, GT_LE))
{
return (((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == 1)) || ((IterOper() == GT_SUB) && (IterConst() == -1));
}
return false;
}

//------------------------------------------------------------------------
Expand All@@ -6864,9 +6875,18 @@ bool NaturalLoopIterInfo::IsDecreasingLoop()
{
// Decreasing loop is the one that has "-=" decrement operation and "> or >=" limit check. If the operation is
// "+=", make sure the constant is negative to give an effect of decrementing the iterator.
bool isGreaterThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_GT, GT_GE);
return (isGreaterThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0))));
// As with IsIncreasingLoop, we also recognize "!=" against a limit when the IV step is exactly -1
// (or +1 with GT_SUB); the stride must be exactly +/-1 to avoid parity/overflow issues.
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_GT, GT_GE))
{
return (((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == -1)) || ((IterOper() == GT_SUB) && (IterConst() == 1));
}
return false;
}

//------------------------------------------------------------------------
Expand Down
81 changes: 74 additions & 7 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1189,10 +1189,11 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

NaturalLoopIterInfo* iterInfo = context->GetLoopIterInfo(loop->GetIndex());
// Note we see cases where the test oper is NE (array.Len) which we could handle
// with some extra care.
// Loop tests we can reason about for cloning: ordered relops (LT/LE/GT/GE) and
// NE limits (treated as LT/GT-equivalent when stride is exactly +/-1; see
// NaturalLoopIterInfo::IsIncreasingLoop/IsDecreasingLoop).
//
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE))
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE, GT_NE))
{
// We can't reason about how this loop iterates
return false;
Expand DownExpand Up@@ -1312,9 +1313,17 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
// adjusted for IsReversed and ExitedOnTrue. For GT_NE we substitute an
// ordered relop (LT for increasing, GT for decreasing) so that the
// runtime guard strictly orders init and limit. Using the raw "init !=
// limit" form would let a misordered init pass while the fast clone
// (with bounds checks removed) wraps the IV through the type.
genTreeOps zeroTripOp = iterInfo->TestOper();
if (zeroTripOp == GT_NE)
{
zeroTripOp = iterInfo->IsIncreasingLoop() ? GT_LT : GT_GT;
}
LC_Condition zeroTrip(zeroTripOp, LC_Expr(initIdent), LC_Expr(limitIdent), iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}
Expand DownExpand Up@@ -1427,20 +1436,30 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
// GT_LT loop test: (start < end) ==> (end <= arrLen)
// GT_LE loop test: (start <= end) ==> (end < arrLen)
//
// GT_NE loop test (stride = +/-1; see IsIncreasing/DecreasingLoop):
// For increasing: visited indices are [init..end-1] => guard end <= arrLen (same as LT).
// `ident` is the loop's end value, so the per-access condition is (end <= arrLen).
// For decreasing: visited indices are [end+1..init] => guard init < arrLen (same as GT).
// `ident` is the loop's init value (set in the init-conditions section above and not
// overwritten by the GT_NE limit branch), so the per-access condition is
// (init < arrLen). This is why the switch below maps decreasing GT_NE to GT_LT.
//
// Decreasing loops
// Always check if iter var is less than array length.
genTreeOps opLimitCondition;
switch (iterInfo->TestOper())
{
case GT_LT:

opLimitCondition = GT_LE;
break;
case GT_LE:
case GT_GE:
case GT_GT:
opLimitCondition = GT_LT;
break;
case GT_NE:
opLimitCondition = isIncreasingLoop ? GT_LE : GT_LT;
break;
default:
unreached();
}
Expand DownExpand Up@@ -1496,6 +1515,54 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
}

// For GT_NE loops (with stride exactly +/-1; see IsIncreasing/DecreasingLoop),
// the cloned fast path preserves the "i != limit" exit test. If the IV starts
// past the limit, the loop would wrap around the type and access arbitrary
// memory because the fast path has its bounds checks removed. Guard the fast
// path with an ordered "init RELOP limit" condition (init <= limit for
// increasing, init >= limit for decreasing) so that a misordered init falls
// back to the slow path with bounds checks.
if (iterInfo->TestOper() == GT_NE)
{
LC_Ident neInitIdent;
if (iterInfo->HasConstInit)
{
assert(iterInfo->ConstInitValue >= 0);
neInitIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
const unsigned initLcl = iterInfo->IterVar;
assert(genActualTypeIsInt(lvaGetDesc(initLcl)));
neInitIdent = LC_Ident::CreateVar(initLcl, iterInfo->Iterator()->TypeGet());
}

LC_Ident neLimitIdent;
if (iterInfo->HasConstLimit)
{
const int limit = iterInfo->ConstLimit();
assert(limit >= 0);
neLimitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
assert(genActualTypeIsInt(lvaGetDesc(limitLcl)));
neLimitIdent = LC_Ident::CreateVar(limitLcl, iterInfo->Limit()->TypeGet());
}
else
{
assert(iterInfo->HasArrayLengthLimit);
assert(limitArrIndex != nullptr);
neLimitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}

const genTreeOps cmpOp = isIncreasingLoop ? GT_LE : GT_GE;
LC_Condition initLimitCond(cmpOp, LC_Expr(neInitIdent), LC_Expr(neLimitIdent));
context->EnsureConditions(loop->GetIndex())->Push(initLimitCond);
JITDUMP("Added NE init-vs-limit cloning condition\n");
}

JITDUMP("Conditions: ");
DBEXEC(verbose, context->PrintConditions(loop->GetIndex()));
JITDUMP("\n");
Expand Down
42 changes: 42 additions & 0 deletions src/coreclr/jit/rangecheck.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1337,6 +1337,48 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp
}
}
}
// Current assertion is of the form "X != (BoundVN + 0)" or "X == (BoundVN + 0)"
// where BoundVN is a length-like checked bound (e.g. an array length). These are
// generated from loop exit tests written as "i != arr.Length"; we can tighten the
// induction variable's range when its current upper/lower limit equals the bound.
else if (canUseCheckedBounds && curAssertion.KindIs(Compiler::OAK_EQUAL, Compiler::OAK_NOT_EQUAL) &&
(curAssertion.GetOp1().GetVN() == normalLclVN) &&
curAssertion.GetOp2().KindIs(Compiler::O2K_VN_ADD_CNS) && (curAssertion.GetOp2().GetCns() == 0) &&
comp->vnStore->IsVNCheckedBound(curAssertion.GetOp2().GetVN()))
{
const ValueNum boundVN = curAssertion.GetOp2().GetVN();

if (curAssertion.KindIs(Compiler::OAK_EQUAL))
{
// X == bound: range tightens to exactly [bound, bound].
limit = Limit(Limit::keBinOpArray, boundVN, 0);
cmpOper = GT_EQ;
}
else
{
// X != bound: only useful when pRange's upper or lower limit already equals
// the bound. Tighten:
// [lo, bound] -> [lo, bound - 1]
// [bound, hi] -> [bound + 1, hi]
if (pRange->UpperLimit().IsBinOpArray() && (pRange->UpperLimit().vn == boundVN) &&
(pRange->UpperLimit().GetConstant() == 0))

@EgorBoEgorBoJun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

presumably we don't have to check pRange->UpperLimit().GetConstant() == 0 ?
e.g. [lo, bound - 1] -> [lo, bound - 2]
ah, actually nvm, it's not correct

{
limit = Limit(Limit::keBinOpArray, boundVN, -1);
cmpOper = GT_LE;
}
else if (pRange->LowerLimit().IsBinOpArray() && (pRange->LowerLimit().vn == boundVN) &&
(pRange->LowerLimit().GetConstant() == 0))
{
limit = Limit(Limit::keBinOpArray, boundVN, 1);
cmpOper = GT_GE;
}
else
{
// Nothing to deduce from this assertion at this site.
continue;
}
}
}
// Current assertion asserts a bounds check does not throw
else if (curAssertion.IsBoundsCheckNoThrow())
{
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions src/coreclr/jit/assertionprop.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1525,7 +1525,10 @@ void Compiler::optDebugCheckAssertion(const AssertionDsc& assertion) const
case O2K_VN_ADD_CNS:
assert(!optLocalAssertionProp);
assert(assertion.GetOp1().KindIs(O1K_VN));
assert(assertion.IsRelop());
// Most O2K_VN_ADD_CNS assertions are ordered relops ("i <relop> bnd + cns"), but
// we also create equality assertions against a checked bound (e.g. "i != arr.Length")
// for use by RangeCheck.
assert(assertion.IsRelop() || assertion.CanPropEqualOrNotEqual());
break;

case O2K_ZEROOBJ:
Expand DownExpand Up@@ -1678,6 +1681,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
}

bool isUnsignedRelop;
bool isEqualityRelop = false;
if (relopFuncApp.FuncIs(VNF_LE, VNF_LT, VNF_GE, VNF_GT))
{
isUnsignedRelop = false;
Expand All@@ -1686,10 +1690,20 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
{
isUnsignedRelop = true;
}
else if (relopFuncApp.FuncIs(VNF_EQ, VNF_NE))
{
// Equality relops against a checked bound (e.g. "i != arr.Length") are
// useful for RangeCheck to tighten ranges on loop back-edges. They flow
// through the CheckedBound paths below; other equality assertions
// (against constants, locals, type handles, etc.) are handled in
// optAssertionGenJtrue.
isUnsignedRelop = false;
isEqualityRelop = true;
}
else
{
// Not a relop we're interested in.
// Assertions for NE/EQ are handled elsewhere.
// Assertions for EQ/NE not against a checked bound are handled elsewhere.
return NO_ASSERTION_INDEX;
}

Expand All@@ -1706,21 +1720,42 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
// "CheckedBnd <relop> X"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op1VN))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// For equality relops where the non-bound side is a constant (e.g. "len != 0"), the
// LCLVAR-based equality assertion created below by optAssertionGenJtrue is strictly more
// useful than a CompareCheckedBound form -- downstream consumers (folding bounds checks,
// proving "len > 0" after a "len != 0" test) only recognize the LCLVAR form. Skip the new
// assertion in that case and let the LCLVAR path produce it.
if (!(isEqualityRelop && vnStore->IsVNConstant(op2VN)))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// "X <relop> CheckedBnd"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op2VN))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// Symmetric guard: leave constant-vs-CheckedBound equality assertions to the LCLVAR path.
if (!(isEqualityRelop && vnStore->IsVNConstant(op1VN)))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// The remaining "(CheckedBnd + CNS) <relop> X" cases are only useful when the
// comparison is ordered (LT/LE/GT/GE). For equality relops we don't produce
// CheckedBoundAddConst-shaped assertions; the consumers (RangeCheck) only
// tighten ranges from equality assertions whose RHS is the bound itself.
if (isEqualityRelop)
{
return NO_ASSERTION_INDEX;
}

// "(CheckedBnd + CNS) <relop> X"
Expand Down
32 changes: 26 additions & 6 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -6848,9 +6848,20 @@ genTreeOps NaturalLoopIterInfo::TestOper()
bool NaturalLoopIterInfo::IsIncreasingLoop()
{
// Increasing loop is the one that has "+=" increment operation and "< or <=" limit check.
bool isLessThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_LT, GT_LE);
return (isLessThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0))));
// We also recognize "!=" against a limit when the IV step is exactly +1 (or -1 with GT_SUB):
// such loops visit indices [init, limit) provided that "init <= limit" holds on entry.
// Stride must be ±1 to avoid parity/overflow issues where the IV could skip past the limit
// (e.g. "for (i = 0; i != 5; i += 2)" never terminates and wraps around).
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_LT, GT_LE))
{
return (((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == 1)) || ((IterOper() == GT_SUB) && (IterConst() == -1));
}
return false;
}

//------------------------------------------------------------------------
Expand All@@ -6864,9 +6875,18 @@ bool NaturalLoopIterInfo::IsDecreasingLoop()
{
// Decreasing loop is the one that has "-=" decrement operation and "> or >=" limit check. If the operation is
// "+=", make sure the constant is negative to give an effect of decrementing the iterator.
bool isGreaterThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_GT, GT_GE);
return (isGreaterThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0))));
// As with IsIncreasingLoop, we also recognize "!=" against a limit when the IV step is exactly -1
// (or +1 with GT_SUB); the stride must be exactly +/-1 to avoid parity/overflow issues.
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_GT, GT_GE))
{
return (((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == -1)) || ((IterOper() == GT_SUB) && (IterConst() == 1));
}
return false;
}

//------------------------------------------------------------------------
Expand Down
81 changes: 74 additions & 7 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1189,10 +1189,11 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

NaturalLoopIterInfo* iterInfo = context->GetLoopIterInfo(loop->GetIndex());
// Note we see cases where the test oper is NE (array.Len) which we could handle
// with some extra care.
// Loop tests we can reason about for cloning: ordered relops (LT/LE/GT/GE) and
// NE limits (treated as LT/GT-equivalent when stride is exactly +/-1; see
// NaturalLoopIterInfo::IsIncreasingLoop/IsDecreasingLoop).
//
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE))
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE, GT_NE))
{
// We can't reason about how this loop iterates
return false;
Expand DownExpand Up@@ -1312,9 +1313,17 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
// adjusted for IsReversed and ExitedOnTrue. For GT_NE we substitute an
// ordered relop (LT for increasing, GT for decreasing) so that the
// runtime guard strictly orders init and limit. Using the raw "init !=
// limit" form would let a misordered init pass while the fast clone
// (with bounds checks removed) wraps the IV through the type.
genTreeOps zeroTripOp = iterInfo->TestOper();
if (zeroTripOp == GT_NE)
{
zeroTripOp = iterInfo->IsIncreasingLoop() ? GT_LT : GT_GT;
}
LC_Condition zeroTrip(zeroTripOp, LC_Expr(initIdent), LC_Expr(limitIdent), iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}
Expand DownExpand Up@@ -1427,20 +1436,30 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
// GT_LT loop test: (start < end) ==> (end <= arrLen)
// GT_LE loop test: (start <= end) ==> (end < arrLen)
//
// GT_NE loop test (stride = +/-1; see IsIncreasing/DecreasingLoop):
// For increasing: visited indices are [init..end-1] => guard end <= arrLen (same as LT).
// `ident` is the loop's end value, so the per-access condition is (end <= arrLen).
// For decreasing: visited indices are [end+1..init] => guard init < arrLen (same as GT).
// `ident` is the loop's init value (set in the init-conditions section above and not
// overwritten by the GT_NE limit branch), so the per-access condition is
// (init < arrLen). This is why the switch below maps decreasing GT_NE to GT_LT.
//
// Decreasing loops
// Always check if iter var is less than array length.
genTreeOps opLimitCondition;
switch (iterInfo->TestOper())
{
case GT_LT:

opLimitCondition = GT_LE;
break;
case GT_LE:
case GT_GE:
case GT_GT:
opLimitCondition = GT_LT;
break;
case GT_NE:
opLimitCondition = isIncreasingLoop ? GT_LE : GT_LT;
break;
default:
unreached();
}
Expand DownExpand Up@@ -1496,6 +1515,54 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
}

// For GT_NE loops (with stride exactly +/-1; see IsIncreasing/DecreasingLoop),
// the cloned fast path preserves the "i != limit" exit test. If the IV starts
// past the limit, the loop would wrap around the type and access arbitrary
// memory because the fast path has its bounds checks removed. Guard the fast
// path with an ordered "init RELOP limit" condition (init <= limit for
// increasing, init >= limit for decreasing) so that a misordered init falls
// back to the slow path with bounds checks.
if (iterInfo->TestOper() == GT_NE)
{
LC_Ident neInitIdent;
if (iterInfo->HasConstInit)
{
assert(iterInfo->ConstInitValue >= 0);
neInitIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
const unsigned initLcl = iterInfo->IterVar;
assert(genActualTypeIsInt(lvaGetDesc(initLcl)));
neInitIdent = LC_Ident::CreateVar(initLcl, iterInfo->Iterator()->TypeGet());
}

LC_Ident neLimitIdent;
if (iterInfo->HasConstLimit)
{
const int limit = iterInfo->ConstLimit();
assert(limit >= 0);
neLimitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
assert(genActualTypeIsInt(lvaGetDesc(limitLcl)));
neLimitIdent = LC_Ident::CreateVar(limitLcl, iterInfo->Limit()->TypeGet());
}
else
{
assert(iterInfo->HasArrayLengthLimit);
assert(limitArrIndex != nullptr);
neLimitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}

const genTreeOps cmpOp = isIncreasingLoop ? GT_LE : GT_GE;
LC_Condition initLimitCond(cmpOp, LC_Expr(neInitIdent), LC_Expr(neLimitIdent));
context->EnsureConditions(loop->GetIndex())->Push(initLimitCond);
JITDUMP("Added NE init-vs-limit cloning condition\n");
}

JITDUMP("Conditions: ");
DBEXEC(verbose, context->PrintConditions(loop->GetIndex()));
JITDUMP("\n");
Expand Down
42 changes: 42 additions & 0 deletions src/coreclr/jit/rangecheck.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1337,6 +1337,48 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp
}
}
}
// Current assertion is of the form "X != (BoundVN + 0)" or "X == (BoundVN + 0)"
// where BoundVN is a length-like checked bound (e.g. an array length). These are
// generated from loop exit tests written as "i != arr.Length"; we can tighten the
// induction variable's range when its current upper/lower limit equals the bound.
else if (canUseCheckedBounds && curAssertion.KindIs(Compiler::OAK_EQUAL, Compiler::OAK_NOT_EQUAL) &&
(curAssertion.GetOp1().GetVN() == normalLclVN) &&
curAssertion.GetOp2().KindIs(Compiler::O2K_VN_ADD_CNS) && (curAssertion.GetOp2().GetCns() == 0) &&
comp->vnStore->IsVNCheckedBound(curAssertion.GetOp2().GetVN()))
{
const ValueNum boundVN = curAssertion.GetOp2().GetVN();

if (curAssertion.KindIs(Compiler::OAK_EQUAL))
{
// X == bound: range tightens to exactly [bound, bound].
limit = Limit(Limit::keBinOpArray, boundVN, 0);
cmpOper = GT_EQ;
}
else
{
// X != bound: only useful when pRange's upper or lower limit already equals
// the bound. Tighten:
// [lo, bound] -> [lo, bound - 1]
// [bound, hi] -> [bound + 1, hi]
if (pRange->UpperLimit().IsBinOpArray() && (pRange->UpperLimit().vn == boundVN) &&
(pRange->UpperLimit().GetConstant() == 0))

@EgorBoEgorBoJun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

presumably we don't have to check pRange->UpperLimit().GetConstant() == 0 ?
e.g. [lo, bound - 1] -> [lo, bound - 2]
ah, actually nvm, it's not correct

{
limit = Limit(Limit::keBinOpArray, boundVN, -1);
cmpOper = GT_LE;
}
else if (pRange->LowerLimit().IsBinOpArray() && (pRange->LowerLimit().vn == boundVN) &&
(pRange->LowerLimit().GetConstant() == 0))
{
limit = Limit(Limit::keBinOpArray, boundVN, 1);
cmpOper = GT_GE;
}
else
{
// Nothing to deduce from this assertion at this site.
continue;
}
}
}
// Current assertion asserts a bounds check does not throw
else if (curAssertion.IsBoundsCheckNoThrow())
{
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions src/coreclr/jit/assertionprop.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1525,7 +1525,10 @@ void Compiler::optDebugCheckAssertion(const AssertionDsc& assertion) const
case O2K_VN_ADD_CNS:
assert(!optLocalAssertionProp);
assert(assertion.GetOp1().KindIs(O1K_VN));
assert(assertion.IsRelop());
// Most O2K_VN_ADD_CNS assertions are ordered relops ("i <relop> bnd + cns"), but
// we also create equality assertions against a checked bound (e.g. "i != arr.Length")
// for use by RangeCheck.
assert(assertion.IsRelop() || assertion.CanPropEqualOrNotEqual());
break;

case O2K_ZEROOBJ:
Expand DownExpand Up@@ -1678,6 +1681,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
}

bool isUnsignedRelop;
bool isEqualityRelop = false;
if (relopFuncApp.FuncIs(VNF_LE, VNF_LT, VNF_GE, VNF_GT))
{
isUnsignedRelop = false;
Expand All@@ -1686,10 +1690,20 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
{
isUnsignedRelop = true;
}
else if (relopFuncApp.FuncIs(VNF_EQ, VNF_NE))
{
// Equality relops against a checked bound (e.g. "i != arr.Length") are
// useful for RangeCheck to tighten ranges on loop back-edges. They flow
// through the CheckedBound paths below; other equality assertions
// (against constants, locals, type handles, etc.) are handled in
// optAssertionGenJtrue.
isUnsignedRelop = false;
isEqualityRelop = true;
}
else
{
// Not a relop we're interested in.
// Assertions for NE/EQ are handled elsewhere.
// Assertions for EQ/NE not against a checked bound are handled elsewhere.
return NO_ASSERTION_INDEX;
}

Expand All@@ -1706,21 +1720,42 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
// "CheckedBnd <relop> X"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op1VN))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// For equality relops where the non-bound side is a constant (e.g. "len != 0"), the
// LCLVAR-based equality assertion created below by optAssertionGenJtrue is strictly more
// useful than a CompareCheckedBound form -- downstream consumers (folding bounds checks,
// proving "len > 0" after a "len != 0" test) only recognize the LCLVAR form. Skip the new
// assertion in that case and let the LCLVAR path produce it.
if (!(isEqualityRelop && vnStore->IsVNConstant(op2VN)))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// "X <relop> CheckedBnd"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op2VN))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// Symmetric guard: leave constant-vs-CheckedBound equality assertions to the LCLVAR path.
if (!(isEqualityRelop && vnStore->IsVNConstant(op1VN)))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// The remaining "(CheckedBnd + CNS) <relop> X" cases are only useful when the
// comparison is ordered (LT/LE/GT/GE). For equality relops we don't produce
// CheckedBoundAddConst-shaped assertions; the consumers (RangeCheck) only
// tighten ranges from equality assertions whose RHS is the bound itself.
if (isEqualityRelop)
{
return NO_ASSERTION_INDEX;
}

// "(CheckedBnd + CNS) <relop> X"
Expand Down
32 changes: 26 additions & 6 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -6848,9 +6848,20 @@ genTreeOps NaturalLoopIterInfo::TestOper()
bool NaturalLoopIterInfo::IsIncreasingLoop()
{
// Increasing loop is the one that has "+=" increment operation and "< or <=" limit check.
bool isLessThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_LT, GT_LE);
return (isLessThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0))));
// We also recognize "!=" against a limit when the IV step is exactly +1 (or -1 with GT_SUB):
// such loops visit indices [init, limit) provided that "init <= limit" holds on entry.
// Stride must be ±1 to avoid parity/overflow issues where the IV could skip past the limit
// (e.g. "for (i = 0; i != 5; i += 2)" never terminates and wraps around).
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_LT, GT_LE))
{
return (((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == 1)) || ((IterOper() == GT_SUB) && (IterConst() == -1));
}
return false;
}

//------------------------------------------------------------------------
Expand All@@ -6864,9 +6875,18 @@ bool NaturalLoopIterInfo::IsDecreasingLoop()
{
// Decreasing loop is the one that has "-=" decrement operation and "> or >=" limit check. If the operation is
// "+=", make sure the constant is negative to give an effect of decrementing the iterator.
bool isGreaterThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_GT, GT_GE);
return (isGreaterThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0))));
// As with IsIncreasingLoop, we also recognize "!=" against a limit when the IV step is exactly -1
// (or +1 with GT_SUB); the stride must be exactly +/-1 to avoid parity/overflow issues.
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_GT, GT_GE))
{
return (((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == -1)) || ((IterOper() == GT_SUB) && (IterConst() == 1));
}
return false;
}

//------------------------------------------------------------------------
Expand Down
81 changes: 74 additions & 7 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1189,10 +1189,11 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

NaturalLoopIterInfo* iterInfo = context->GetLoopIterInfo(loop->GetIndex());
// Note we see cases where the test oper is NE (array.Len) which we could handle
// with some extra care.
// Loop tests we can reason about for cloning: ordered relops (LT/LE/GT/GE) and
// NE limits (treated as LT/GT-equivalent when stride is exactly +/-1; see
// NaturalLoopIterInfo::IsIncreasingLoop/IsDecreasingLoop).
//
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE))
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE, GT_NE))
{
// We can't reason about how this loop iterates
return false;
Expand DownExpand Up@@ -1312,9 +1313,17 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
// adjusted for IsReversed and ExitedOnTrue. For GT_NE we substitute an
// ordered relop (LT for increasing, GT for decreasing) so that the
// runtime guard strictly orders init and limit. Using the raw "init !=
// limit" form would let a misordered init pass while the fast clone
// (with bounds checks removed) wraps the IV through the type.
genTreeOps zeroTripOp = iterInfo->TestOper();
if (zeroTripOp == GT_NE)
{
zeroTripOp = iterInfo->IsIncreasingLoop() ? GT_LT : GT_GT;
}
LC_Condition zeroTrip(zeroTripOp, LC_Expr(initIdent), LC_Expr(limitIdent), iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}
Expand DownExpand Up@@ -1427,20 +1436,30 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
// GT_LT loop test: (start < end) ==> (end <= arrLen)
// GT_LE loop test: (start <= end) ==> (end < arrLen)
//
// GT_NE loop test (stride = +/-1; see IsIncreasing/DecreasingLoop):
// For increasing: visited indices are [init..end-1] => guard end <= arrLen (same as LT).
// `ident` is the loop's end value, so the per-access condition is (end <= arrLen).
// For decreasing: visited indices are [end+1..init] => guard init < arrLen (same as GT).
// `ident` is the loop's init value (set in the init-conditions section above and not
// overwritten by the GT_NE limit branch), so the per-access condition is
// (init < arrLen). This is why the switch below maps decreasing GT_NE to GT_LT.
//
// Decreasing loops
// Always check if iter var is less than array length.
genTreeOps opLimitCondition;
switch (iterInfo->TestOper())
{
case GT_LT:

opLimitCondition = GT_LE;
break;
case GT_LE:
case GT_GE:
case GT_GT:
opLimitCondition = GT_LT;
break;
case GT_NE:
opLimitCondition = isIncreasingLoop ? GT_LE : GT_LT;
break;
default:
unreached();
}
Expand DownExpand Up@@ -1496,6 +1515,54 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
}

// For GT_NE loops (with stride exactly +/-1; see IsIncreasing/DecreasingLoop),
// the cloned fast path preserves the "i != limit" exit test. If the IV starts
// past the limit, the loop would wrap around the type and access arbitrary
// memory because the fast path has its bounds checks removed. Guard the fast
// path with an ordered "init RELOP limit" condition (init <= limit for
// increasing, init >= limit for decreasing) so that a misordered init falls
// back to the slow path with bounds checks.
if (iterInfo->TestOper() == GT_NE)
{
LC_Ident neInitIdent;
if (iterInfo->HasConstInit)
{
assert(iterInfo->ConstInitValue >= 0);
neInitIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
const unsigned initLcl = iterInfo->IterVar;
assert(genActualTypeIsInt(lvaGetDesc(initLcl)));
neInitIdent = LC_Ident::CreateVar(initLcl, iterInfo->Iterator()->TypeGet());
}

LC_Ident neLimitIdent;
if (iterInfo->HasConstLimit)
{
const int limit = iterInfo->ConstLimit();
assert(limit >= 0);
neLimitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
assert(genActualTypeIsInt(lvaGetDesc(limitLcl)));
neLimitIdent = LC_Ident::CreateVar(limitLcl, iterInfo->Limit()->TypeGet());
}
else
{
assert(iterInfo->HasArrayLengthLimit);
assert(limitArrIndex != nullptr);
neLimitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}

const genTreeOps cmpOp = isIncreasingLoop ? GT_LE : GT_GE;
LC_Condition initLimitCond(cmpOp, LC_Expr(neInitIdent), LC_Expr(neLimitIdent));
context->EnsureConditions(loop->GetIndex())->Push(initLimitCond);
JITDUMP("Added NE init-vs-limit cloning condition\n");
}

JITDUMP("Conditions: ");
DBEXEC(verbose, context->PrintConditions(loop->GetIndex()));
JITDUMP("\n");
Expand Down
42 changes: 42 additions & 0 deletions src/coreclr/jit/rangecheck.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1337,6 +1337,48 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp
}
}
}
// Current assertion is of the form "X != (BoundVN + 0)" or "X == (BoundVN + 0)"
// where BoundVN is a length-like checked bound (e.g. an array length). These are
// generated from loop exit tests written as "i != arr.Length"; we can tighten the
// induction variable's range when its current upper/lower limit equals the bound.
else if (canUseCheckedBounds && curAssertion.KindIs(Compiler::OAK_EQUAL, Compiler::OAK_NOT_EQUAL) &&
(curAssertion.GetOp1().GetVN() == normalLclVN) &&
curAssertion.GetOp2().KindIs(Compiler::O2K_VN_ADD_CNS) && (curAssertion.GetOp2().GetCns() == 0) &&
comp->vnStore->IsVNCheckedBound(curAssertion.GetOp2().GetVN()))
{
const ValueNum boundVN = curAssertion.GetOp2().GetVN();

if (curAssertion.KindIs(Compiler::OAK_EQUAL))
{
// X == bound: range tightens to exactly [bound, bound].
limit = Limit(Limit::keBinOpArray, boundVN, 0);
cmpOper = GT_EQ;
}
else
{
// X != bound: only useful when pRange's upper or lower limit already equals
// the bound. Tighten:
// [lo, bound] -> [lo, bound - 1]
// [bound, hi] -> [bound + 1, hi]
if (pRange->UpperLimit().IsBinOpArray() && (pRange->UpperLimit().vn == boundVN) &&
(pRange->UpperLimit().GetConstant() == 0))

@EgorBoEgorBoJun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

presumably we don't have to check pRange->UpperLimit().GetConstant() == 0 ?
e.g. [lo, bound - 1] -> [lo, bound - 2]
ah, actually nvm, it's not correct

{
limit = Limit(Limit::keBinOpArray, boundVN, -1);
cmpOper = GT_LE;
}
else if (pRange->LowerLimit().IsBinOpArray() && (pRange->LowerLimit().vn == boundVN) &&
(pRange->LowerLimit().GetConstant() == 0))
{
limit = Limit(Limit::keBinOpArray, boundVN, 1);
cmpOper = GT_GE;
}
else
{
// Nothing to deduce from this assertion at this site.
continue;
}
}
}
// Current assertion asserts a bounds check does not throw
else if (curAssertion.IsBoundsCheckNoThrow())
{
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions src/coreclr/jit/assertionprop.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1525,7 +1525,10 @@ void Compiler::optDebugCheckAssertion(const AssertionDsc& assertion) const
case O2K_VN_ADD_CNS:
assert(!optLocalAssertionProp);
assert(assertion.GetOp1().KindIs(O1K_VN));
assert(assertion.IsRelop());
// Most O2K_VN_ADD_CNS assertions are ordered relops ("i <relop> bnd + cns"), but
// we also create equality assertions against a checked bound (e.g. "i != arr.Length")
// for use by RangeCheck.
assert(assertion.IsRelop() || assertion.CanPropEqualOrNotEqual());
break;

case O2K_ZEROOBJ:
Expand DownExpand Up@@ -1678,6 +1681,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
}

bool isUnsignedRelop;
bool isEqualityRelop = false;
if (relopFuncApp.FuncIs(VNF_LE, VNF_LT, VNF_GE, VNF_GT))
{
isUnsignedRelop = false;
Expand All@@ -1686,10 +1690,20 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
{
isUnsignedRelop = true;
}
else if (relopFuncApp.FuncIs(VNF_EQ, VNF_NE))
{
// Equality relops against a checked bound (e.g. "i != arr.Length") are
// useful for RangeCheck to tighten ranges on loop back-edges. They flow
// through the CheckedBound paths below; other equality assertions
// (against constants, locals, type handles, etc.) are handled in
// optAssertionGenJtrue.
isUnsignedRelop = false;
isEqualityRelop = true;
}
else
{
// Not a relop we're interested in.
// Assertions for NE/EQ are handled elsewhere.
// Assertions for EQ/NE not against a checked bound are handled elsewhere.
return NO_ASSERTION_INDEX;
}

Expand All@@ -1706,21 +1720,42 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
// "CheckedBnd <relop> X"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op1VN))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// For equality relops where the non-bound side is a constant (e.g. "len != 0"), the
// LCLVAR-based equality assertion created below by optAssertionGenJtrue is strictly more
// useful than a CompareCheckedBound form -- downstream consumers (folding bounds checks,
// proving "len > 0" after a "len != 0" test) only recognize the LCLVAR form. Skip the new
// assertion in that case and let the LCLVAR path produce it.
if (!(isEqualityRelop && vnStore->IsVNConstant(op2VN)))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// "X <relop> CheckedBnd"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op2VN))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// Symmetric guard: leave constant-vs-CheckedBound equality assertions to the LCLVAR path.
if (!(isEqualityRelop && vnStore->IsVNConstant(op1VN)))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// The remaining "(CheckedBnd + CNS) <relop> X" cases are only useful when the
// comparison is ordered (LT/LE/GT/GE). For equality relops we don't produce
// CheckedBoundAddConst-shaped assertions; the consumers (RangeCheck) only
// tighten ranges from equality assertions whose RHS is the bound itself.
if (isEqualityRelop)
{
return NO_ASSERTION_INDEX;
}

// "(CheckedBnd + CNS) <relop> X"
Expand Down
32 changes: 26 additions & 6 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -6848,9 +6848,20 @@ genTreeOps NaturalLoopIterInfo::TestOper()
bool NaturalLoopIterInfo::IsIncreasingLoop()
{
// Increasing loop is the one that has "+=" increment operation and "< or <=" limit check.
bool isLessThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_LT, GT_LE);
return (isLessThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0))));
// We also recognize "!=" against a limit when the IV step is exactly +1 (or -1 with GT_SUB):
// such loops visit indices [init, limit) provided that "init <= limit" holds on entry.
// Stride must be ±1 to avoid parity/overflow issues where the IV could skip past the limit
// (e.g. "for (i = 0; i != 5; i += 2)" never terminates and wraps around).
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_LT, GT_LE))
{
return (((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == 1)) || ((IterOper() == GT_SUB) && (IterConst() == -1));
}
return false;
}

//------------------------------------------------------------------------
Expand All@@ -6864,9 +6875,18 @@ bool NaturalLoopIterInfo::IsDecreasingLoop()
{
// Decreasing loop is the one that has "-=" decrement operation and "> or >=" limit check. If the operation is
// "+=", make sure the constant is negative to give an effect of decrementing the iterator.
bool isGreaterThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_GT, GT_GE);
return (isGreaterThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0))));
// As with IsIncreasingLoop, we also recognize "!=" against a limit when the IV step is exactly -1
// (or +1 with GT_SUB); the stride must be exactly +/-1 to avoid parity/overflow issues.
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_GT, GT_GE))
{
return (((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == -1)) || ((IterOper() == GT_SUB) && (IterConst() == 1));
}
return false;
}

//------------------------------------------------------------------------
Expand Down
81 changes: 74 additions & 7 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1189,10 +1189,11 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

NaturalLoopIterInfo* iterInfo = context->GetLoopIterInfo(loop->GetIndex());
// Note we see cases where the test oper is NE (array.Len) which we could handle
// with some extra care.
// Loop tests we can reason about for cloning: ordered relops (LT/LE/GT/GE) and
// NE limits (treated as LT/GT-equivalent when stride is exactly +/-1; see
// NaturalLoopIterInfo::IsIncreasingLoop/IsDecreasingLoop).
//
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE))
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE, GT_NE))
{
// We can't reason about how this loop iterates
return false;
Expand DownExpand Up@@ -1312,9 +1313,17 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
// adjusted for IsReversed and ExitedOnTrue. For GT_NE we substitute an
// ordered relop (LT for increasing, GT for decreasing) so that the
// runtime guard strictly orders init and limit. Using the raw "init !=
// limit" form would let a misordered init pass while the fast clone
// (with bounds checks removed) wraps the IV through the type.
genTreeOps zeroTripOp = iterInfo->TestOper();
if (zeroTripOp == GT_NE)
{
zeroTripOp = iterInfo->IsIncreasingLoop() ? GT_LT : GT_GT;
}
LC_Condition zeroTrip(zeroTripOp, LC_Expr(initIdent), LC_Expr(limitIdent), iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}
Expand DownExpand Up@@ -1427,20 +1436,30 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
// GT_LT loop test: (start < end) ==> (end <= arrLen)
// GT_LE loop test: (start <= end) ==> (end < arrLen)
//
// GT_NE loop test (stride = +/-1; see IsIncreasing/DecreasingLoop):
// For increasing: visited indices are [init..end-1] => guard end <= arrLen (same as LT).
// `ident` is the loop's end value, so the per-access condition is (end <= arrLen).
// For decreasing: visited indices are [end+1..init] => guard init < arrLen (same as GT).
// `ident` is the loop's init value (set in the init-conditions section above and not
// overwritten by the GT_NE limit branch), so the per-access condition is
// (init < arrLen). This is why the switch below maps decreasing GT_NE to GT_LT.
//
// Decreasing loops
// Always check if iter var is less than array length.
genTreeOps opLimitCondition;
switch (iterInfo->TestOper())
{
case GT_LT:

opLimitCondition = GT_LE;
break;
case GT_LE:
case GT_GE:
case GT_GT:
opLimitCondition = GT_LT;
break;
case GT_NE:
opLimitCondition = isIncreasingLoop ? GT_LE : GT_LT;
break;
default:
unreached();
}
Expand DownExpand Up@@ -1496,6 +1515,54 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
}

// For GT_NE loops (with stride exactly +/-1; see IsIncreasing/DecreasingLoop),
// the cloned fast path preserves the "i != limit" exit test. If the IV starts
// past the limit, the loop would wrap around the type and access arbitrary
// memory because the fast path has its bounds checks removed. Guard the fast
// path with an ordered "init RELOP limit" condition (init <= limit for
// increasing, init >= limit for decreasing) so that a misordered init falls
// back to the slow path with bounds checks.
if (iterInfo->TestOper() == GT_NE)
{
LC_Ident neInitIdent;
if (iterInfo->HasConstInit)
{
assert(iterInfo->ConstInitValue >= 0);
neInitIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
const unsigned initLcl = iterInfo->IterVar;
assert(genActualTypeIsInt(lvaGetDesc(initLcl)));
neInitIdent = LC_Ident::CreateVar(initLcl, iterInfo->Iterator()->TypeGet());
}

LC_Ident neLimitIdent;
if (iterInfo->HasConstLimit)
{
const int limit = iterInfo->ConstLimit();
assert(limit >= 0);
neLimitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
assert(genActualTypeIsInt(lvaGetDesc(limitLcl)));
neLimitIdent = LC_Ident::CreateVar(limitLcl, iterInfo->Limit()->TypeGet());
}
else
{
assert(iterInfo->HasArrayLengthLimit);
assert(limitArrIndex != nullptr);
neLimitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}

const genTreeOps cmpOp = isIncreasingLoop ? GT_LE : GT_GE;
LC_Condition initLimitCond(cmpOp, LC_Expr(neInitIdent), LC_Expr(neLimitIdent));
context->EnsureConditions(loop->GetIndex())->Push(initLimitCond);
JITDUMP("Added NE init-vs-limit cloning condition\n");
}

JITDUMP("Conditions: ");
DBEXEC(verbose, context->PrintConditions(loop->GetIndex()));
JITDUMP("\n");
Expand Down
42 changes: 42 additions & 0 deletions src/coreclr/jit/rangecheck.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1337,6 +1337,48 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp
}
}
}
// Current assertion is of the form "X != (BoundVN + 0)" or "X == (BoundVN + 0)"
// where BoundVN is a length-like checked bound (e.g. an array length). These are
// generated from loop exit tests written as "i != arr.Length"; we can tighten the
// induction variable's range when its current upper/lower limit equals the bound.
else if (canUseCheckedBounds && curAssertion.KindIs(Compiler::OAK_EQUAL, Compiler::OAK_NOT_EQUAL) &&
(curAssertion.GetOp1().GetVN() == normalLclVN) &&
curAssertion.GetOp2().KindIs(Compiler::O2K_VN_ADD_CNS) && (curAssertion.GetOp2().GetCns() == 0) &&
comp->vnStore->IsVNCheckedBound(curAssertion.GetOp2().GetVN()))
{
const ValueNum boundVN = curAssertion.GetOp2().GetVN();

if (curAssertion.KindIs(Compiler::OAK_EQUAL))
{
// X == bound: range tightens to exactly [bound, bound].
limit = Limit(Limit::keBinOpArray, boundVN, 0);
cmpOper = GT_EQ;
}
else
{
// X != bound: only useful when pRange's upper or lower limit already equals
// the bound. Tighten:
// [lo, bound] -> [lo, bound - 1]
// [bound, hi] -> [bound + 1, hi]
if (pRange->UpperLimit().IsBinOpArray() && (pRange->UpperLimit().vn == boundVN) &&
(pRange->UpperLimit().GetConstant() == 0))

@EgorBoEgorBoJun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

presumably we don't have to check pRange->UpperLimit().GetConstant() == 0 ?
e.g. [lo, bound - 1] -> [lo, bound - 2]
ah, actually nvm, it's not correct

{
limit = Limit(Limit::keBinOpArray, boundVN, -1);
cmpOper = GT_LE;
}
else if (pRange->LowerLimit().IsBinOpArray() && (pRange->LowerLimit().vn == boundVN) &&
(pRange->LowerLimit().GetConstant() == 0))
{
limit = Limit(Limit::keBinOpArray, boundVN, 1);
cmpOper = GT_GE;
}
else
{
// Nothing to deduce from this assertion at this site.
continue;
}
}
}
// Current assertion asserts a bounds check does not throw
else if (curAssertion.IsBoundsCheckNoThrow())
{
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions src/coreclr/jit/assertionprop.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1525,7 +1525,10 @@ void Compiler::optDebugCheckAssertion(const AssertionDsc& assertion) const
case O2K_VN_ADD_CNS:
assert(!optLocalAssertionProp);
assert(assertion.GetOp1().KindIs(O1K_VN));
assert(assertion.IsRelop());
// Most O2K_VN_ADD_CNS assertions are ordered relops ("i <relop> bnd + cns"), but
// we also create equality assertions against a checked bound (e.g. "i != arr.Length")
// for use by RangeCheck.
assert(assertion.IsRelop() || assertion.CanPropEqualOrNotEqual());
break;

case O2K_ZEROOBJ:
Expand DownExpand Up@@ -1678,6 +1681,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
}

bool isUnsignedRelop;
bool isEqualityRelop = false;
if (relopFuncApp.FuncIs(VNF_LE, VNF_LT, VNF_GE, VNF_GT))
{
isUnsignedRelop = false;
Expand All@@ -1686,10 +1690,20 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
{
isUnsignedRelop = true;
}
else if (relopFuncApp.FuncIs(VNF_EQ, VNF_NE))
{
// Equality relops against a checked bound (e.g. "i != arr.Length") are
// useful for RangeCheck to tighten ranges on loop back-edges. They flow
// through the CheckedBound paths below; other equality assertions
// (against constants, locals, type handles, etc.) are handled in
// optAssertionGenJtrue.
isUnsignedRelop = false;
isEqualityRelop = true;
}
else
{
// Not a relop we're interested in.
// Assertions for NE/EQ are handled elsewhere.
// Assertions for EQ/NE not against a checked bound are handled elsewhere.
return NO_ASSERTION_INDEX;
}

Expand All@@ -1706,21 +1720,42 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
// "CheckedBnd <relop> X"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op1VN))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// For equality relops where the non-bound side is a constant (e.g. "len != 0"), the
// LCLVAR-based equality assertion created below by optAssertionGenJtrue is strictly more
// useful than a CompareCheckedBound form -- downstream consumers (folding bounds checks,
// proving "len > 0" after a "len != 0" test) only recognize the LCLVAR form. Skip the new
// assertion in that case and let the LCLVAR path produce it.
if (!(isEqualityRelop && vnStore->IsVNConstant(op2VN)))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// "X <relop> CheckedBnd"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op2VN))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// Symmetric guard: leave constant-vs-CheckedBound equality assertions to the LCLVAR path.
if (!(isEqualityRelop && vnStore->IsVNConstant(op1VN)))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// The remaining "(CheckedBnd + CNS) <relop> X" cases are only useful when the
// comparison is ordered (LT/LE/GT/GE). For equality relops we don't produce
// CheckedBoundAddConst-shaped assertions; the consumers (RangeCheck) only
// tighten ranges from equality assertions whose RHS is the bound itself.
if (isEqualityRelop)
{
return NO_ASSERTION_INDEX;
}

// "(CheckedBnd + CNS) <relop> X"
Expand Down
32 changes: 26 additions & 6 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -6848,9 +6848,20 @@ genTreeOps NaturalLoopIterInfo::TestOper()
bool NaturalLoopIterInfo::IsIncreasingLoop()
{
// Increasing loop is the one that has "+=" increment operation and "< or <=" limit check.
bool isLessThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_LT, GT_LE);
return (isLessThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0))));
// We also recognize "!=" against a limit when the IV step is exactly +1 (or -1 with GT_SUB):
// such loops visit indices [init, limit) provided that "init <= limit" holds on entry.
// Stride must be ±1 to avoid parity/overflow issues where the IV could skip past the limit
// (e.g. "for (i = 0; i != 5; i += 2)" never terminates and wraps around).
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_LT, GT_LE))
{
return (((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == 1)) || ((IterOper() == GT_SUB) && (IterConst() == -1));
}
return false;
}

//------------------------------------------------------------------------
Expand All@@ -6864,9 +6875,18 @@ bool NaturalLoopIterInfo::IsDecreasingLoop()
{
// Decreasing loop is the one that has "-=" decrement operation and "> or >=" limit check. If the operation is
// "+=", make sure the constant is negative to give an effect of decrementing the iterator.
bool isGreaterThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_GT, GT_GE);
return (isGreaterThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0))));
// As with IsIncreasingLoop, we also recognize "!=" against a limit when the IV step is exactly -1
// (or +1 with GT_SUB); the stride must be exactly +/-1 to avoid parity/overflow issues.
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_GT, GT_GE))
{
return (((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == -1)) || ((IterOper() == GT_SUB) && (IterConst() == 1));
}
return false;
}

//------------------------------------------------------------------------
Expand Down
81 changes: 74 additions & 7 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1189,10 +1189,11 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

NaturalLoopIterInfo* iterInfo = context->GetLoopIterInfo(loop->GetIndex());
// Note we see cases where the test oper is NE (array.Len) which we could handle
// with some extra care.
// Loop tests we can reason about for cloning: ordered relops (LT/LE/GT/GE) and
// NE limits (treated as LT/GT-equivalent when stride is exactly +/-1; see
// NaturalLoopIterInfo::IsIncreasingLoop/IsDecreasingLoop).
//
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE))
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE, GT_NE))
{
// We can't reason about how this loop iterates
return false;
Expand DownExpand Up@@ -1312,9 +1313,17 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
// adjusted for IsReversed and ExitedOnTrue. For GT_NE we substitute an
// ordered relop (LT for increasing, GT for decreasing) so that the
// runtime guard strictly orders init and limit. Using the raw "init !=
// limit" form would let a misordered init pass while the fast clone
// (with bounds checks removed) wraps the IV through the type.
genTreeOps zeroTripOp = iterInfo->TestOper();
if (zeroTripOp == GT_NE)
{
zeroTripOp = iterInfo->IsIncreasingLoop() ? GT_LT : GT_GT;
}
LC_Condition zeroTrip(zeroTripOp, LC_Expr(initIdent), LC_Expr(limitIdent), iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}
Expand DownExpand Up@@ -1427,20 +1436,30 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
// GT_LT loop test: (start < end) ==> (end <= arrLen)
// GT_LE loop test: (start <= end) ==> (end < arrLen)
//
// GT_NE loop test (stride = +/-1; see IsIncreasing/DecreasingLoop):
// For increasing: visited indices are [init..end-1] => guard end <= arrLen (same as LT).
// `ident` is the loop's end value, so the per-access condition is (end <= arrLen).
// For decreasing: visited indices are [end+1..init] => guard init < arrLen (same as GT).
// `ident` is the loop's init value (set in the init-conditions section above and not
// overwritten by the GT_NE limit branch), so the per-access condition is
// (init < arrLen). This is why the switch below maps decreasing GT_NE to GT_LT.
//
// Decreasing loops
// Always check if iter var is less than array length.
genTreeOps opLimitCondition;
switch (iterInfo->TestOper())
{
case GT_LT:

opLimitCondition = GT_LE;
break;
case GT_LE:
case GT_GE:
case GT_GT:
opLimitCondition = GT_LT;
break;
case GT_NE:
opLimitCondition = isIncreasingLoop ? GT_LE : GT_LT;
break;
default:
unreached();
}
Expand DownExpand Up@@ -1496,6 +1515,54 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
}

// For GT_NE loops (with stride exactly +/-1; see IsIncreasing/DecreasingLoop),
// the cloned fast path preserves the "i != limit" exit test. If the IV starts
// past the limit, the loop would wrap around the type and access arbitrary
// memory because the fast path has its bounds checks removed. Guard the fast
// path with an ordered "init RELOP limit" condition (init <= limit for
// increasing, init >= limit for decreasing) so that a misordered init falls
// back to the slow path with bounds checks.
if (iterInfo->TestOper() == GT_NE)
{
LC_Ident neInitIdent;
if (iterInfo->HasConstInit)
{
assert(iterInfo->ConstInitValue >= 0);
neInitIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
const unsigned initLcl = iterInfo->IterVar;
assert(genActualTypeIsInt(lvaGetDesc(initLcl)));
neInitIdent = LC_Ident::CreateVar(initLcl, iterInfo->Iterator()->TypeGet());
}

LC_Ident neLimitIdent;
if (iterInfo->HasConstLimit)
{
const int limit = iterInfo->ConstLimit();
assert(limit >= 0);
neLimitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
assert(genActualTypeIsInt(lvaGetDesc(limitLcl)));
neLimitIdent = LC_Ident::CreateVar(limitLcl, iterInfo->Limit()->TypeGet());
}
else
{
assert(iterInfo->HasArrayLengthLimit);
assert(limitArrIndex != nullptr);
neLimitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}

const genTreeOps cmpOp = isIncreasingLoop ? GT_LE : GT_GE;
LC_Condition initLimitCond(cmpOp, LC_Expr(neInitIdent), LC_Expr(neLimitIdent));
context->EnsureConditions(loop->GetIndex())->Push(initLimitCond);
JITDUMP("Added NE init-vs-limit cloning condition\n");
}

JITDUMP("Conditions: ");
DBEXEC(verbose, context->PrintConditions(loop->GetIndex()));
JITDUMP("\n");
Expand Down
42 changes: 42 additions & 0 deletions src/coreclr/jit/rangecheck.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1337,6 +1337,48 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp
}
}
}
// Current assertion is of the form "X != (BoundVN + 0)" or "X == (BoundVN + 0)"
// where BoundVN is a length-like checked bound (e.g. an array length). These are
// generated from loop exit tests written as "i != arr.Length"; we can tighten the
// induction variable's range when its current upper/lower limit equals the bound.
else if (canUseCheckedBounds && curAssertion.KindIs(Compiler::OAK_EQUAL, Compiler::OAK_NOT_EQUAL) &&
(curAssertion.GetOp1().GetVN() == normalLclVN) &&
curAssertion.GetOp2().KindIs(Compiler::O2K_VN_ADD_CNS) && (curAssertion.GetOp2().GetCns() == 0) &&
comp->vnStore->IsVNCheckedBound(curAssertion.GetOp2().GetVN()))
{
const ValueNum boundVN = curAssertion.GetOp2().GetVN();

if (curAssertion.KindIs(Compiler::OAK_EQUAL))
{
// X == bound: range tightens to exactly [bound, bound].
limit = Limit(Limit::keBinOpArray, boundVN, 0);
cmpOper = GT_EQ;
}
else
{
// X != bound: only useful when pRange's upper or lower limit already equals
// the bound. Tighten:
// [lo, bound] -> [lo, bound - 1]
// [bound, hi] -> [bound + 1, hi]
if (pRange->UpperLimit().IsBinOpArray() && (pRange->UpperLimit().vn == boundVN) &&
(pRange->UpperLimit().GetConstant() == 0))

@EgorBoEgorBoJun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

presumably we don't have to check pRange->UpperLimit().GetConstant() == 0 ?
e.g. [lo, bound - 1] -> [lo, bound - 2]
ah, actually nvm, it's not correct

{
limit = Limit(Limit::keBinOpArray, boundVN, -1);
cmpOper = GT_LE;
}
else if (pRange->LowerLimit().IsBinOpArray() && (pRange->LowerLimit().vn == boundVN) &&
(pRange->LowerLimit().GetConstant() == 0))
{
limit = Limit(Limit::keBinOpArray, boundVN, 1);
cmpOper = GT_GE;
}
else
{
// Nothing to deduce from this assertion at this site.
continue;
}
}
}
// Current assertion asserts a bounds check does not throw
else if (curAssertion.IsBoundsCheckNoThrow())
{
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions src/coreclr/jit/assertionprop.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1525,7 +1525,10 @@ void Compiler::optDebugCheckAssertion(const AssertionDsc& assertion) const
case O2K_VN_ADD_CNS:
assert(!optLocalAssertionProp);
assert(assertion.GetOp1().KindIs(O1K_VN));
assert(assertion.IsRelop());
// Most O2K_VN_ADD_CNS assertions are ordered relops ("i <relop> bnd + cns"), but
// we also create equality assertions against a checked bound (e.g. "i != arr.Length")
// for use by RangeCheck.
assert(assertion.IsRelop() || assertion.CanPropEqualOrNotEqual());
break;

case O2K_ZEROOBJ:
Expand DownExpand Up@@ -1678,6 +1681,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
}

bool isUnsignedRelop;
bool isEqualityRelop = false;
if (relopFuncApp.FuncIs(VNF_LE, VNF_LT, VNF_GE, VNF_GT))
{
isUnsignedRelop = false;
Expand All@@ -1686,10 +1690,20 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
{
isUnsignedRelop = true;
}
else if (relopFuncApp.FuncIs(VNF_EQ, VNF_NE))
{
// Equality relops against a checked bound (e.g. "i != arr.Length") are
// useful for RangeCheck to tighten ranges on loop back-edges. They flow
// through the CheckedBound paths below; other equality assertions
// (against constants, locals, type handles, etc.) are handled in
// optAssertionGenJtrue.
isUnsignedRelop = false;
isEqualityRelop = true;
}
else
{
// Not a relop we're interested in.
// Assertions for NE/EQ are handled elsewhere.
// Assertions for EQ/NE not against a checked bound are handled elsewhere.
return NO_ASSERTION_INDEX;
}

Expand All@@ -1706,21 +1720,42 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree)
// "CheckedBnd <relop> X"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op1VN))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// For equality relops where the non-bound side is a constant (e.g. "len != 0"), the
// LCLVAR-based equality assertion created below by optAssertionGenJtrue is strictly more
// useful than a CompareCheckedBound form -- downstream consumers (folding bounds checks,
// proving "len > 0" after a "len != 0" test) only recognize the LCLVAR form. Skip the new
// assertion in that case and let the LCLVAR path produce it.
if (!(isEqualityRelop && vnStore->IsVNConstant(op2VN)))
{
// Move the checked bound to the right side for simplicity
relopFunc = ValueNumStore::SwapRelop(relopFunc);
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// "X <relop> CheckedBnd"
if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op2VN))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
// Symmetric guard: leave constant-vs-CheckedBound equality assertions to the LCLVAR path.
if (!(isEqualityRelop && vnStore->IsVNConstant(op1VN)))
{
AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0);
AssertionIndex idx = optAddAssertion(dsc);
optCreateComplementaryAssertion(idx);
return idx;
}
}

// The remaining "(CheckedBnd + CNS) <relop> X" cases are only useful when the
// comparison is ordered (LT/LE/GT/GE). For equality relops we don't produce
// CheckedBoundAddConst-shaped assertions; the consumers (RangeCheck) only
// tighten ranges from equality assertions whose RHS is the bound itself.
if (isEqualityRelop)
{
return NO_ASSERTION_INDEX;
}

// "(CheckedBnd + CNS) <relop> X"
Expand Down
32 changes: 26 additions & 6 deletions src/coreclr/jit/flowgraph.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -6848,9 +6848,20 @@ genTreeOps NaturalLoopIterInfo::TestOper()
bool NaturalLoopIterInfo::IsIncreasingLoop()
{
// Increasing loop is the one that has "+=" increment operation and "< or <=" limit check.
bool isLessThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_LT, GT_LE);
return (isLessThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0))));
// We also recognize "!=" against a limit when the IV step is exactly +1 (or -1 with GT_SUB):
// such loops visit indices [init, limit) provided that "init <= limit" holds on entry.
// Stride must be ±1 to avoid parity/overflow issues where the IV could skip past the limit
// (e.g. "for (i = 0; i != 5; i += 2)" never terminates and wraps around).
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_LT, GT_LE))
{
return (((IterOper() == GT_ADD) && (IterConst() > 0)) || ((IterOper() == GT_SUB) && (IterConst() < 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == 1)) || ((IterOper() == GT_SUB) && (IterConst() == -1));
}
return false;
}

//------------------------------------------------------------------------
Expand All@@ -6864,9 +6875,18 @@ bool NaturalLoopIterInfo::IsDecreasingLoop()
{
// Decreasing loop is the one that has "-=" decrement operation and "> or >=" limit check. If the operation is
// "+=", make sure the constant is negative to give an effect of decrementing the iterator.
bool isGreaterThanLimitCheck = GenTree::StaticOperIs(TestOper(), GT_GT, GT_GE);
return (isGreaterThanLimitCheck &&
(((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0))));
// As with IsIncreasingLoop, we also recognize "!=" against a limit when the IV step is exactly -1
// (or +1 with GT_SUB); the stride must be exactly +/-1 to avoid parity/overflow issues.
const genTreeOps testOp = TestOper();
if (GenTree::StaticOperIs(testOp, GT_GT, GT_GE))
{
return (((IterOper() == GT_ADD) && (IterConst() < 0)) || ((IterOper() == GT_SUB) && (IterConst() > 0)));
}
if (testOp == GT_NE)
{
return ((IterOper() == GT_ADD) && (IterConst() == -1)) || ((IterOper() == GT_SUB) && (IterConst() == 1));
}
return false;
}

//------------------------------------------------------------------------
Expand Down
81 changes: 74 additions & 7 deletions src/coreclr/jit/loopcloning.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1189,10 +1189,11 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

NaturalLoopIterInfo* iterInfo = context->GetLoopIterInfo(loop->GetIndex());
// Note we see cases where the test oper is NE (array.Len) which we could handle
// with some extra care.
// Loop tests we can reason about for cloning: ordered relops (LT/LE/GT/GE) and
// NE limits (treated as LT/GT-equivalent when stride is exactly +/-1; see
// NaturalLoopIterInfo::IsIncreasingLoop/IsDecreasingLoop).
//
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE))
if (!GenTree::StaticOperIs(iterInfo->TestOper(), GT_LT, GT_LE, GT_GT, GT_GE, GT_NE))
{
// We can't reason about how this loop iterates
return false;
Expand DownExpand Up@@ -1312,9 +1313,17 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}

// TestOper() returns the stays-in-loop relop in IV-on-lhs form, already
// adjusted for IsReversed and ExitedOnTrue.
LC_Condition zeroTrip(iterInfo->TestOper(), LC_Expr(initIdent), LC_Expr(limitIdent),
iterInfo->TestTree->IsUnsigned());
// adjusted for IsReversed and ExitedOnTrue. For GT_NE we substitute an
// ordered relop (LT for increasing, GT for decreasing) so that the
// runtime guard strictly orders init and limit. Using the raw "init !=
// limit" form would let a misordered init pass while the fast clone
// (with bounds checks removed) wraps the IV through the type.
genTreeOps zeroTripOp = iterInfo->TestOper();
if (zeroTripOp == GT_NE)
{
zeroTripOp = iterInfo->IsIncreasingLoop() ? GT_LT : GT_GT;
}
LC_Condition zeroTrip(zeroTripOp, LC_Expr(initIdent), LC_Expr(limitIdent), iterInfo->TestTree->IsUnsigned());
context->EnsureConditions(loop->GetIndex())->Push(zeroTrip);
JITDUMP("Added zero-trip guard cloning condition\n");
}
Expand DownExpand Up@@ -1427,20 +1436,30 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
// GT_LT loop test: (start < end) ==> (end <= arrLen)
// GT_LE loop test: (start <= end) ==> (end < arrLen)
//
// GT_NE loop test (stride = +/-1; see IsIncreasing/DecreasingLoop):
// For increasing: visited indices are [init..end-1] => guard end <= arrLen (same as LT).
// `ident` is the loop's end value, so the per-access condition is (end <= arrLen).
// For decreasing: visited indices are [end+1..init] => guard init < arrLen (same as GT).
// `ident` is the loop's init value (set in the init-conditions section above and not
// overwritten by the GT_NE limit branch), so the per-access condition is
// (init < arrLen). This is why the switch below maps decreasing GT_NE to GT_LT.
//
// Decreasing loops
// Always check if iter var is less than array length.
genTreeOps opLimitCondition;
switch (iterInfo->TestOper())
{
case GT_LT:

opLimitCondition = GT_LE;
break;
case GT_LE:
case GT_GE:
case GT_GT:
opLimitCondition = GT_LT;
break;
case GT_NE:
opLimitCondition = isIncreasingLoop ? GT_LE : GT_LT;
break;
default:
unreached();
}
Expand DownExpand Up@@ -1496,6 +1515,54 @@ bool Compiler::optDeriveLoopCloningConditions(FlowGraphNaturalLoop* loop, LoopCl
}
}

// For GT_NE loops (with stride exactly +/-1; see IsIncreasing/DecreasingLoop),
// the cloned fast path preserves the "i != limit" exit test. If the IV starts
// past the limit, the loop would wrap around the type and access arbitrary
// memory because the fast path has its bounds checks removed. Guard the fast
// path with an ordered "init RELOP limit" condition (init <= limit for
// increasing, init >= limit for decreasing) so that a misordered init falls
// back to the slow path with bounds checks.
if (iterInfo->TestOper() == GT_NE)
{
LC_Ident neInitIdent;
if (iterInfo->HasConstInit)
{
assert(iterInfo->ConstInitValue >= 0);
neInitIdent = LC_Ident::CreateConst(static_cast<unsigned>(iterInfo->ConstInitValue));
}
else
{
const unsigned initLcl = iterInfo->IterVar;
assert(genActualTypeIsInt(lvaGetDesc(initLcl)));
neInitIdent = LC_Ident::CreateVar(initLcl, iterInfo->Iterator()->TypeGet());
}

LC_Ident neLimitIdent;
if (iterInfo->HasConstLimit)
{
const int limit = iterInfo->ConstLimit();
assert(limit >= 0);
neLimitIdent = LC_Ident::CreateConst(static_cast<unsigned>(limit));
}
else if (iterInfo->HasInvariantLocalLimit)
{
const unsigned limitLcl = iterInfo->VarLimit();
assert(genActualTypeIsInt(lvaGetDesc(limitLcl)));
neLimitIdent = LC_Ident::CreateVar(limitLcl, iterInfo->Limit()->TypeGet());
}
else
{
assert(iterInfo->HasArrayLengthLimit);
assert(limitArrIndex != nullptr);
neLimitIdent = LC_Ident::CreateArrAccess(LC_Array(LC_Array::Jagged, limitArrIndex, LC_Array::ArrLen));
}

const genTreeOps cmpOp = isIncreasingLoop ? GT_LE : GT_GE;
LC_Condition initLimitCond(cmpOp, LC_Expr(neInitIdent), LC_Expr(neLimitIdent));
context->EnsureConditions(loop->GetIndex())->Push(initLimitCond);
JITDUMP("Added NE init-vs-limit cloning condition\n");
}

JITDUMP("Conditions: ");
DBEXEC(verbose, context->PrintConditions(loop->GetIndex()));
JITDUMP("\n");
Expand Down
42 changes: 42 additions & 0 deletions src/coreclr/jit/rangecheck.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -1337,6 +1337,48 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp
}
}
}
// Current assertion is of the form "X != (BoundVN + 0)" or "X == (BoundVN + 0)"
// where BoundVN is a length-like checked bound (e.g. an array length). These are
// generated from loop exit tests written as "i != arr.Length"; we can tighten the
// induction variable's range when its current upper/lower limit equals the bound.
else if (canUseCheckedBounds && curAssertion.KindIs(Compiler::OAK_EQUAL, Compiler::OAK_NOT_EQUAL) &&
(curAssertion.GetOp1().GetVN() == normalLclVN) &&
curAssertion.GetOp2().KindIs(Compiler::O2K_VN_ADD_CNS) && (curAssertion.GetOp2().GetCns() == 0) &&
comp->vnStore->IsVNCheckedBound(curAssertion.GetOp2().GetVN()))
{
const ValueNum boundVN = curAssertion.GetOp2().GetVN();

if (curAssertion.KindIs(Compiler::OAK_EQUAL))
{
// X == bound: range tightens to exactly [bound, bound].
limit = Limit(Limit::keBinOpArray, boundVN, 0);
cmpOper = GT_EQ;
}
else
{
// X != bound: only useful when pRange's upper or lower limit already equals
// the bound. Tighten:
// [lo, bound] -> [lo, bound - 1]
// [bound, hi] -> [bound + 1, hi]
if (pRange->UpperLimit().IsBinOpArray() && (pRange->UpperLimit().vn == boundVN) &&
(pRange->UpperLimit().GetConstant() == 0))

@EgorBoEgorBoJun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

presumably we don't have to check pRange->UpperLimit().GetConstant() == 0 ?
e.g. [lo, bound - 1] -> [lo, bound - 2]
ah, actually nvm, it's not correct

{
limit = Limit(Limit::keBinOpArray, boundVN, -1);
cmpOper = GT_LE;
}
else if (pRange->LowerLimit().IsBinOpArray() && (pRange->LowerLimit().vn == boundVN) &&
(pRange->LowerLimit().GetConstant() == 0))
{
limit = Limit(Limit::keBinOpArray, boundVN, 1);
cmpOper = GT_GE;
}
else
{
// Nothing to deduce from this assertion at this site.
continue;
}
}
}
// Current assertion asserts a bounds check does not throw
else if (curAssertion.IsBoundsCheckNoThrow())
{
Expand Down
Loading
Loading