From 59f5a24e147a97c8f76264517d2ad80c6b04cda4 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Fri, 8 May 2026 13:53:59 +0200 Subject: [PATCH 1/6] introduce O2K_VN for arbitrary O1K_VN O2K_VN comparisons --- src/coreclr/jit/assertionprop.cpp | 35 +++++++++++ src/coreclr/jit/compiler.h | 26 +++++++- src/coreclr/jit/rangecheck.cpp | 99 ++++++++++++++++++++++++++++++- src/coreclr/jit/rangecheck.h | 12 ++++ 4 files changed, 169 insertions(+), 3 deletions(-) diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 2d4a84a3e41da4..f2989ae7b45a72 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -904,6 +904,10 @@ void Compiler::optPrintAssertion(const AssertionDsc& curAssertion, AssertionInde curAssertion.GetOp2().GetCheckedBoundConstant()); break; + case O2K_VN: + printf("VN " FMT_VN "", curAssertion.GetOp2().GetVN()); + break; + default: unreached(); break; @@ -1421,6 +1425,13 @@ AssertionIndex Compiler::optAddAssertion(const AssertionDsc& newAssertion) mayHaveDuplicates |= optAssertionHasAssertionsForVN(addOpVN, /* addIfNotFound */ canAddNewAssertions); } } + else if (newAssertion.GetOp2().KindIs(O2K_VN)) + { + // For VN VN assertions, register op2's VN too so consumers can find + // the assertion when iterating from the op2 side. + mayHaveDuplicates |= optAssertionHasAssertionsForVN(newAssertion.GetOp2().GetVN(), + /* addIfNotFound */ canAddNewAssertions); + } if (mayHaveDuplicates) { @@ -1550,6 +1561,12 @@ void Compiler::optDebugCheckAssertion(const AssertionDsc& assertion) const assert(optLocalAssertionProp); break; + case O2K_VN: + assert(!optLocalAssertionProp); + assert(assertion.GetOp1().KindIs(O1K_VN)); + assert(assertion.IsRelop()); + break; + case O2K_ZEROOBJ: // We only make these assertion for stores (not control flow). assert(assertion.KindIs(OAK_EQUAL)); @@ -1807,6 +1824,24 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree) return idx; } + // "X relop Y" where neither side is a constant nor a checked bound. + // For now, we only create such assertions for signed comparisons of int32 (and smaller, after promotion). + // This widens what global assertion prop can reason about: e.g. "b > a" combined with "a > 10" + // can be used to deduce "b > 10". + // + // To keep table pressure under control, we only create the assertion if at least one of the + // operands already has assertions registered. Otherwise the new assertion has no other facts + // it can chain with and is unlikely to enable any deduction, while still consuming a slot + // (and potentially crowding out useful ones). + if (!isUnsignedRelop && (op1VN != op2VN) && !vnStore->IsVNConstant(op1VN) && !vnStore->IsVNConstant(op2VN) && + (optAssertionHasAssertionsForVN(op1VN) || optAssertionHasAssertionsForVN(op2VN))) + { + AssertionDsc dsc = AssertionDsc::CreateRelopVN(this, relopFunc, op1VN, op2VN); + AssertionIndex idx = optAddAssertion(dsc); + optCreateComplementaryAssertion(idx); + return idx; + } + return NO_ASSERTION_INDEX; } diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index f964828161fac9..81af9929c5a008 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -7984,7 +7984,8 @@ class Compiler // nor it implies that it's never negative. O2K_ZEROOBJ, O2K_SUBRANGE, - O2K_CONST_VEC + O2K_CONST_VEC, + O2K_VN, // op2 is an arbitrary value number (used for VN VN assertions in global prop). }; struct AssertionDsc @@ -8130,7 +8131,7 @@ class Compiler ValueNum GetVN() const { assert(!m_compiler->optLocalAssertionProp); - assert(KindIs(O2K_CONST_INT, O2K_CONST_DOUBLE, O2K_ZEROOBJ, O2K_CONST_VEC)); + assert(KindIs(O2K_CONST_INT, O2K_CONST_DOUBLE, O2K_ZEROOBJ, O2K_CONST_VEC, O2K_VN)); assert(m_vn != ValueNumStore::NoVN); return m_vn; } @@ -8481,6 +8482,9 @@ class Compiler case O2K_SUBRANGE: return GetOp2().GetIntegralRange().Equals(that.GetOp2().GetIntegralRange()); + case O2K_VN: + return GetOp2().GetVN() == that.GetOp2().GetVN(); + default: assert(!"Unexpected value for GetOp2().m_kind in AssertionDsc."); break; @@ -8740,6 +8744,24 @@ class Compiler dsc.m_op2.m_icon.m_iconVal = cns; return dsc; } + + // Create "op1VN op2VN" assertion where both operands are arbitrary value numbers + // (used by global assertion prop for VN-to-VN signed comparisons of int32 and smaller). + static AssertionDsc CreateRelopVN(const Compiler* comp, VNFunc relop, ValueNum op1VN, ValueNum op2VN) + { + assert(!comp->optLocalAssertionProp); + assert(op1VN != ValueNumStore::NoVN); + assert(op2VN != ValueNumStore::NoVN); + assert(op1VN != op2VN); + + AssertionDsc dsc = CreateEmptyAssertion(comp); + dsc.m_assertionKind = FromVNFunc(relop); + dsc.m_op1.m_kind = O1K_VN; + dsc.m_op1.m_vn = op1VN; + dsc.m_op2.m_kind = O2K_VN; + dsc.m_op2.m_vn = op2VN; + return dsc; + } }; protected: diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 55a2c5ab7e3545..ee75d8ced5d351 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -938,7 +938,12 @@ Range RangeCheck::GetRangeFromAssertionsWorker( result = phiRange; } - MergeEdgeAssertions(comp, num, ValueNumStore::NoVN, assertions, &result, false); + // MergeEdgeAssertionsWorker may recursively call back to GetRangeFromAssertionsWorker for other VN-to-VN assertion + // lookups. + int edgeAssertionsBudget = min(2, budget); + MergeEdgeAssertionsWorker(comp, num, ValueNumStore::NoVN, assertions, &result, /* canUseCheckedBounds */ false, + edgeAssertionsBudget, visited); + assert(result.IsConstantRange()); return result; } @@ -1044,6 +1049,37 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, ASSERT_VALARG_TP assertions, Range* pRange, bool canUseCheckedBounds) +{ + // Public entry point: no shared visited set; create a fresh one and use a small recursion + // budget for VN-to-VN assertion lookups. + ValueNumStore::SmallValueNumSet visited; + MergeEdgeAssertionsWorker(comp, normalLclVN, preferredBoundVN, assertions, pRange, canUseCheckedBounds, + /* budget */ 2, &visited); +} + +//------------------------------------------------------------------------ +// MergeEdgeAssertionsWorker: Worker for MergeEdgeAssertions that takes a visited set and recursion +// budget for VN-to-VN assertion lookups. +// +// Arguments: +// comp - the compiler instance +// normalLclVN - the value number to look for assertions for +// preferredBoundVN - when this VN is set, it will be given preference over constant limits +// assertions - the assertions to use +// pRange - the range to tighten with assertions +// canUseCheckedBounds - true if we can use checked bounds assertions (cache) +// budget - the remaining budget for recursive VN-to-VN assertion lookups +// visited - the set of value numbers already visited in the current search path to prevent infinite +// recursion +// +void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp, + ValueNum normalLclVN, + ValueNum preferredBoundVN, + ASSERT_VALARG_TP assertions, + Range* pRange, + bool canUseCheckedBounds, + int budget, + ValueNumStore::SmallValueNumSet* visited) { Range assertedRange = Range(Limit(Limit::keUnknown)); if (BitVecOps::IsEmpty(comp->apTraits, assertions)) @@ -1343,6 +1379,67 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, continue; } } + // Current assertion is of the form "X Y" where both X and Y are arbitrary VNs + // We try to derive a bound on normalLclVN by recursively computing the range of the other operand. + // + // Example: + // + // if (a > 10 && a < 100) + // { + // if (normalLclVN > a) // a is an unknown VN with [11..99] range derived from assertions. + // { + // + else if (curAssertion.IsRelop() && curAssertion.GetOp2().KindIs(Compiler::O2K_VN) && + (curAssertion.GetOp1().GetVN() == normalLclVN || curAssertion.GetOp2().GetVN() == normalLclVN)) + { + ValueNum op1VN = curAssertion.GetOp1().GetVN(); + ValueNum op2VN = curAssertion.GetOp2().GetVN(); + + cmpOper = Compiler::AssertionDsc::ToCompareOper(curAssertion.GetKind(), &isUnsigned); + if (isUnsigned) + { + continue; + } + + ValueNum otherVN; + if (op1VN == normalLclVN) + { + otherVN = op2VN; + cmpOper = GenTree::SwapRelop(cmpOper); + } + else + { + assert(op2VN == normalLclVN); + otherVN = op1VN; + } + + Range otherRange = GetRangeFromAssertionsWorker(comp, otherVN, assertions, budget - 1, visited); + if (!otherRange.IsConstantRange()) + { + continue; + } + + // Derive a constant limit for normalLclVN from the constant range of otherVN. + // We use the most useful bound for each direction of the comparison. + int derivedLimit; + switch (cmpOper) + { + case GT_LT: + case GT_LE: + // normalLclVN < otherVN (or <=) => normalLclVN <(=) otherVN.UpperLimit + derivedLimit = otherRange.UpperLimit().GetConstant(); + break; + case GT_GT: + case GT_GE: + // normalLclVN > otherVN (or >=) => normalLclVN >(=) otherVN.LowerLimit + derivedLimit = otherRange.LowerLimit().GetConstant(); + break; + + default: + continue; + } + limit = Limit(Limit::keConstant, derivedLimit); + } // Current assertion is not supported, ignore it else { diff --git a/src/coreclr/jit/rangecheck.h b/src/coreclr/jit/rangecheck.h index cab98b29c85b60..ea399f334edbeb 100644 --- a/src/coreclr/jit/rangecheck.h +++ b/src/coreclr/jit/rangecheck.h @@ -827,6 +827,18 @@ class RangeCheck Range* pRange, bool canUseCheckedBounds = true); + // Internal worker used by GetRangeFromAssertionsWorker: same as the public overload + // but threads a recursion budget and visited set so that VN-to-VN assertions can be + // resolved by recursing into GetRangeFromAssertionsWorker without unbounded work. + static void MergeEdgeAssertionsWorker(Compiler* comp, + ValueNum num, + ValueNum preferredBoundVN, + ASSERT_VALARG_TP assertions, + Range* pRange, + bool canUseCheckedBounds, + int budget, + ValueNumStore::SmallValueNumSet* visited); + // The maximum possible value of the given "limit". If such a value could not be determined // return "false". For example: CORINFO_Array_MaxLength for array length. bool GetLimitMax(Limit& limit, int* pMax); From 55624390a57b25e0f186eef6d864465ed733ac76 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Fri, 8 May 2026 14:24:17 +0200 Subject: [PATCH 2/6] fix bug --- src/coreclr/jit/rangecheck.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index ee75d8ced5d351..add31b1385d4f0 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -1404,13 +1404,16 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp ValueNum otherVN; if (op1VN == normalLclVN) { + // Assertion is "normalLclVN otherVN" - keep cmpOper as-is. otherVN = op2VN; - cmpOper = GenTree::SwapRelop(cmpOper); } else { + // Assertion is "otherVN normalLclVN" - swap to get + // "normalLclVN otherVN". assert(op2VN == normalLclVN); otherVN = op1VN; + cmpOper = GenTree::SwapRelop(cmpOper); } Range otherRange = GetRangeFromAssertionsWorker(comp, otherVN, assertions, budget - 1, visited); From 4276b7340ef0a760ccfd10d34e713def0595ade5 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Fri, 8 May 2026 14:46:16 +0200 Subject: [PATCH 3/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/coreclr/jit/rangecheck.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index add31b1385d4f0..1949b0d4134546 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -1416,7 +1416,13 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp cmpOper = GenTree::SwapRelop(cmpOper); } - Range otherRange = GetRangeFromAssertionsWorker(comp, otherVN, assertions, budget - 1, visited); + if (budget <= 0) + { + continue; + } + + budget--; + Range otherRange = GetRangeFromAssertionsWorker(comp, otherVN, assertions, budget, visited); if (!otherRange.IsConstantRange()) { continue; From 72ec895d46a31a00365a14a25ca9cb765ed7ceb1 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Fri, 8 May 2026 16:32:57 +0200 Subject: [PATCH 4/6] bump budget --- src/coreclr/jit/rangecheck.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 1949b0d4134546..ae8e0dd1a86598 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -940,7 +940,7 @@ Range RangeCheck::GetRangeFromAssertionsWorker( // MergeEdgeAssertionsWorker may recursively call back to GetRangeFromAssertionsWorker for other VN-to-VN assertion // lookups. - int edgeAssertionsBudget = min(2, budget); + int edgeAssertionsBudget = min(4, budget); MergeEdgeAssertionsWorker(comp, num, ValueNumStore::NoVN, assertions, &result, /* canUseCheckedBounds */ false, edgeAssertionsBudget, visited); @@ -1054,7 +1054,7 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, // budget for VN-to-VN assertion lookups. ValueNumStore::SmallValueNumSet visited; MergeEdgeAssertionsWorker(comp, normalLclVN, preferredBoundVN, assertions, pRange, canUseCheckedBounds, - /* budget */ 2, &visited); + /* budget */ 4, &visited); } //------------------------------------------------------------------------ From 09b6d8b0eda8524b21cda23716e5554df75261a5 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Sun, 10 May 2026 13:55:21 +0200 Subject: [PATCH 5/6] Update rangecheck.cpp --- src/coreclr/jit/rangecheck.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index ae8e0dd1a86598..68d6e5fd14334a 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -940,7 +940,7 @@ Range RangeCheck::GetRangeFromAssertionsWorker( // MergeEdgeAssertionsWorker may recursively call back to GetRangeFromAssertionsWorker for other VN-to-VN assertion // lookups. - int edgeAssertionsBudget = min(4, budget); + int edgeAssertionsBudget = min(3, budget); MergeEdgeAssertionsWorker(comp, num, ValueNumStore::NoVN, assertions, &result, /* canUseCheckedBounds */ false, edgeAssertionsBudget, visited); From d319a1351eab1a2a58641723144775790c900035 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Sun, 10 May 2026 13:55:46 +0200 Subject: [PATCH 6/6] Update rangecheck.cpp --- src/coreclr/jit/rangecheck.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 68d6e5fd14334a..2d92e6cc0a8ab9 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -1054,7 +1054,7 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, // budget for VN-to-VN assertion lookups. ValueNumStore::SmallValueNumSet visited; MergeEdgeAssertionsWorker(comp, normalLclVN, preferredBoundVN, assertions, pRange, canUseCheckedBounds, - /* budget */ 4, &visited); + /* budget */ 3, &visited); } //------------------------------------------------------------------------