From b8bd366e718ec2ffd22f119e233d09d165d5bd19 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Fri, 13 Feb 2026 16:19:11 +0100 Subject: [PATCH 1/4] Track span.Length's negativity + remove unsound assumption from MergeEdgeAssertions + support VNF_Not in GetRangeFromAssertions --- src/coreclr/jit/assertionprop.cpp | 32 ++++++++++++++++++------ src/coreclr/jit/compiler.h | 41 ++++++++++++++++++++++--------- src/coreclr/jit/rangecheck.cpp | 20 +++------------ src/coreclr/jit/rangecheck.h | 18 ++++++++++++++ 4 files changed, 74 insertions(+), 37 deletions(-) diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 6290f6be85fee6..27df00a92e667d 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -1703,7 +1703,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree) { // Move the checked bound to the right side for simplicity relopFunc = ValueNumStore::SwapRelop(relopFunc); - AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(relopFunc, op2VN, op1VN, 0); + AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, op1VN, 0); AssertionIndex idx = optAddAssertion(dsc); optCreateComplementaryAssertion(idx); return idx; @@ -1712,7 +1712,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree) // "X CheckedBnd" if (!isUnsignedRelop && vnStore->IsVNCheckedBound(op2VN)) { - AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(relopFunc, op1VN, op2VN, 0); + AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, op2VN, 0); AssertionIndex idx = optAddAssertion(dsc); optCreateComplementaryAssertion(idx); return idx; @@ -1725,7 +1725,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree) { // Move the (CheckedBnd + CNS) part to the right side for simplicity relopFunc = ValueNumStore::SwapRelop(relopFunc); - AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(relopFunc, op2VN, checkedBnd, checkedBndCns); + AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op2VN, checkedBnd, checkedBndCns); AssertionIndex idx = optAddAssertion(dsc); optCreateComplementaryAssertion(idx); return idx; @@ -1734,7 +1734,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree) // "X (CheckedBnd + CNS)" if (!isUnsignedRelop && vnStore->IsVNCheckedBoundAddConst(op2VN, &checkedBnd, &checkedBndCns)) { - AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(relopFunc, op1VN, checkedBnd, checkedBndCns); + AssertionDsc dsc = AssertionDsc::CreateCompareCheckedBound(this, relopFunc, op1VN, checkedBnd, checkedBndCns); AssertionIndex idx = optAddAssertion(dsc); optCreateComplementaryAssertion(idx); return idx; @@ -1748,7 +1748,7 @@ AssertionInfo Compiler::optCreateJTrueBoundsAssertion(GenTree* tree) ValueNum idxVN = vnStore->VNNormalValue(unsignedCompareBnd.vnIdx); ValueNum lenVN = vnStore->VNNormalValue(unsignedCompareBnd.vnBound); - AssertionDsc dsc = AssertionDsc::CreateNoThrowArrBnd(idxVN, lenVN); + AssertionDsc dsc = AssertionDsc::CreateNoThrowArrBnd(this, idxVN, lenVN); AssertionIndex index = optAddAssertion(dsc); if (unsignedCompareBnd.cmpOper == VNF_GE_UN) { @@ -2006,6 +2006,21 @@ void Compiler::optAssertionGen(GenTree* tree) } break; + case GT_LCL_VAR: + if (!optLocalAssertionProp && tree->TypeIs(TYP_INT) && + lvaGetDesc(tree->AsLclVarCommon())->IsNeverNegative()) + { + // Create "LCL_VAR >= 0" assertion for int local variables that are never negative. + // Typically, it's Span.Length. + ValueNum opVN = optConservativeNormalVN(tree); + if (opVN != ValueNumStore::NoVN) + { + assertionInfo = optAddAssertion( + AssertionDsc::CreateConstantBound(this, VNF_GE, opVN, vnStore->VNZeroForType(TYP_INT))); + } + } + break; + case GT_IND: case GT_XAND: case GT_XORR: @@ -2036,8 +2051,9 @@ void Compiler::optAssertionGen(GenTree* tree) case GT_BOUNDS_CHECK: if (!optLocalAssertionProp) { - ValueNum idxVN = optConservativeNormalVN(tree->AsBoundsChk()->GetIndex()); - ValueNum lenVN = optConservativeNormalVN(tree->AsBoundsChk()->GetArrayLength()); + GenTree* arrLenNode = tree->AsBoundsChk()->GetArrayLength(); + ValueNum idxVN = optConservativeNormalVN(tree->AsBoundsChk()->GetIndex()); + ValueNum lenVN = optConservativeNormalVN(arrLenNode); if ((idxVN == ValueNumStore::NoVN) || (lenVN == ValueNumStore::NoVN)) { assertionInfo = NO_ASSERTION_INDEX; @@ -2047,7 +2063,7 @@ void Compiler::optAssertionGen(GenTree* tree) // GT_BOUNDS_CHECK node provides the following contract: // * idxVN < lenVN // * lenVN is non-negative - assertionInfo = optAddAssertion(AssertionDsc::CreateNoThrowArrBnd(idxVN, lenVN)); + assertionInfo = optAddAssertion(AssertionDsc::CreateNoThrowArrBnd(this, idxVN, lenVN)); } } break; diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index ec02cb7caedf12..e8052fa1722ce5 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -7712,7 +7712,6 @@ class Compiler enum optOp2Kind : uint8_t { - O2K_INVALID, O2K_LCLVAR_COPY, O2K_CONST_INT, O2K_CONST_DOUBLE, @@ -7732,6 +7731,7 @@ class Compiler friend struct AssertionDsc; // For AssertionDsc::Create* factory methods private: + INDEBUG(const Compiler* m_compiler); optOp1Kind m_kind; union { @@ -7753,6 +7753,7 @@ class Compiler ValueNum GetVN() const { + assert(!m_compiler->optLocalAssertionProp); // TODO-Cleanup: O1K_LCLVAR should be Local-AP only. assert(m_vn != ValueNumStore::NoVN); return m_vn; @@ -7760,6 +7761,7 @@ class Compiler unsigned GetLclNum() const { + assert(m_compiler->optLocalAssertionProp); assert(m_lclNum != BAD_VAR_NUM); assert(KindIs(O1K_LCLVAR)); return m_lclNum; @@ -7776,6 +7778,7 @@ class Compiler friend struct AssertionDsc; // For AssertionDsc::Create* factory methods private: + INDEBUG(const Compiler* m_compiler); optOp2Kind m_kind; bool m_checkedBoundIsNeverNegative; // only meaningful for O2K_CHECKED_BOUND_ADD_CNS kind uint16_t m_encodedIconFlags; // encoded icon gtFlags @@ -7806,6 +7809,7 @@ class Compiler unsigned GetLclNum() const { + assert(m_compiler->optLocalAssertionProp); assert(KindIs(O2K_LCLVAR_COPY)); return m_lclNum; } @@ -7824,6 +7828,7 @@ class Compiler IntegralRange GetIntegralRange() const { + assert(m_compiler->optLocalAssertionProp); assert(KindIs(O2K_SUBRANGE)); return m_range; } @@ -7837,6 +7842,7 @@ class Compiler ValueNum GetVN() const { + assert(!m_compiler->optLocalAssertionProp); assert(KindIs(O2K_CONST_INT, O2K_CONST_DOUBLE, O2K_ZEROOBJ)); assert(m_vn != ValueNumStore::NoVN); return m_vn; @@ -7845,6 +7851,7 @@ class Compiler // For "checkedBndVN + cns" form, return the "cns" part. int GetCheckedBoundConstant() const { + assert(!m_compiler->optLocalAssertionProp); assert(KindIs(O2K_CHECKED_BOUND_ADD_CNS)); assert(FitsIn(m_icon.m_iconVal)); return (int)m_icon.m_iconVal; @@ -7854,6 +7861,7 @@ class Compiler // We intentionally don't allow to use it via GetVN() to avoid confusion. ValueNum GetCheckedBound() const { + assert(!m_compiler->optLocalAssertionProp); assert(KindIs(O2K_CHECKED_BOUND_ADD_CNS)); assert(m_vn != ValueNumStore::NoVN); return m_vn; @@ -7861,6 +7869,7 @@ class Compiler bool IsCheckedBoundNeverNegative() const { + assert(!m_compiler->optLocalAssertionProp); assert(KindIs(O2K_CHECKED_BOUND_ADD_CNS)); return m_checkedBoundIsNeverNegative; } @@ -7911,6 +7920,14 @@ class Compiler optAssertionKind m_assertionKind; AssertionDscOp1 m_op1; AssertionDscOp2 m_op2; + + static AssertionDsc CreateEmptyAssertion(const Compiler* comp) + { + AssertionDsc dsc = {}; + INDEBUG(dsc.m_op1.m_compiler = comp); + INDEBUG(dsc.m_op2.m_compiler = comp); + return dsc; + } public: optAssertionKind GetKind() const @@ -8168,7 +8185,6 @@ class Compiler case O2K_SUBRANGE: return GetOp2().GetIntegralRange().Equals(that.GetOp2().GetIntegralRange()); - case O2K_INVALID: default: assert(!"Unexpected value for GetOp2().m_kind in AssertionDsc."); break; @@ -8203,7 +8219,7 @@ class Compiler GenTreeFlags iconFlags = GTF_EMPTY, FieldSeq* fldSeq = nullptr) { - AssertionDsc dsc = {}; + AssertionDsc dsc = CreateEmptyAssertion(comp); dsc.m_assertionKind = equals ? OAK_EQUAL : OAK_NOT_EQUAL; dsc.m_op1.m_kind = O1K_LCLVAR; @@ -8284,7 +8300,7 @@ class Compiler assert(lclNum1 != BAD_VAR_NUM); assert(lclNum2 != BAD_VAR_NUM); - AssertionDsc dsc = {}; + AssertionDsc dsc = CreateEmptyAssertion(comp); dsc.m_assertionKind = equals ? OAK_EQUAL : OAK_NOT_EQUAL; dsc.m_op1.m_kind = O1K_LCLVAR; dsc.m_op1.m_lclNum = lclNum1; @@ -8300,7 +8316,7 @@ class Compiler assert(comp->optLocalAssertionProp); assert(lclNum != BAD_VAR_NUM); - AssertionDsc dsc = {}; + AssertionDsc dsc = CreateEmptyAssertion(comp); dsc.m_assertionKind = OAK_SUBRANGE; dsc.m_op1.m_kind = O1K_LCLVAR; dsc.m_op1.m_lclNum = lclNum; @@ -8321,7 +8337,7 @@ class Compiler assert(!comp->vnStore->IsVNHandle(op2VN)); assert(!comp->optLocalAssertionProp); - AssertionDsc dsc = {}; + AssertionDsc dsc = CreateEmptyAssertion(comp); dsc.m_assertionKind = equals ? OAK_EQUAL : OAK_NOT_EQUAL; dsc.m_op1.m_vn = op1VN; dsc.m_op2.m_vn = op2VN; @@ -8336,7 +8352,7 @@ class Compiler { assert((objVN != ValueNumStore::NoVN) && comp->vnStore->IsVNTypeHandle(typeHndVN)); - AssertionDsc dsc = {}; + AssertionDsc dsc = CreateEmptyAssertion(comp); dsc.m_op1.m_kind = exact ? O1K_EXACT_TYPE : O1K_SUBTYPE; dsc.m_op1.m_vn = objVN; dsc.m_op2.m_kind = O2K_CONST_INT; @@ -8348,12 +8364,12 @@ class Compiler // Create a no-throw bounds check assertion: idxVN u< lenVN where lenVN is never negative // Effectively, this means "idxVN is in range [0, lenVN)". - static AssertionDsc CreateNoThrowArrBnd(ValueNum idxVN, ValueNum lenVN) + static AssertionDsc CreateNoThrowArrBnd(const Compiler* comp, ValueNum idxVN, ValueNum lenVN) { assert(idxVN != ValueNumStore::NoVN); assert(lenVN != ValueNumStore::NoVN); - AssertionDsc dsc = {}; + AssertionDsc dsc = CreateEmptyAssertion(comp); dsc.m_assertionKind = OAK_LT_UN; dsc.m_op1.m_kind = O1K_VN; dsc.m_op1.m_vn = idxVN; @@ -8367,12 +8383,13 @@ class Compiler } // Create "i (bnd + cns)" assertion - static AssertionDsc CreateCompareCheckedBound(VNFunc relop, ValueNum op1VN, ValueNum checkedBndVN, int cns) + static AssertionDsc CreateCompareCheckedBound( + const Compiler* comp, VNFunc relop, ValueNum op1VN, ValueNum checkedBndVN, int cns) { assert(op1VN != ValueNumStore::NoVN); assert(checkedBndVN != ValueNumStore::NoVN); - AssertionDsc dsc = {}; + AssertionDsc dsc = CreateEmptyAssertion(comp); dsc.m_assertionKind = FromVNFunc(relop); dsc.m_op1.m_kind = O1K_VN; dsc.m_op1.m_vn = op1VN; @@ -8391,7 +8408,7 @@ class Compiler bool op2IsCns = comp->vnStore->IsVNIntegralConstant(cnsVN, &cns); assert(op2IsCns); - AssertionDsc dsc = {}; + AssertionDsc dsc = CreateEmptyAssertion(comp); dsc.m_assertionKind = FromVNFunc(relop); dsc.m_op1.m_kind = O1K_VN; dsc.m_op1.m_vn = op1VN; diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 798afae835dbf9..b6c726a1e3ab8e 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -705,10 +705,11 @@ Range RangeCheck::GetRangeFromAssertions(Compiler* comp, ValueNum num, ASSERT_VA } break; + case VNF_NOT: case VNF_NEG: { Range r1 = GetRangeFromAssertions(comp, funcApp.m_args[0], assertions, --budget); - Range unaryOpResult = RangeOps::Negate(r1); + Range unaryOpResult = funcApp.FuncIs(VNF_NEG) ? RangeOps::Negate(r1) : RangeOps::Not(r1); // We can use the result only if it never overflows. result = unaryOpResult.IsConstantRange() ? unaryOpResult : result; @@ -1051,14 +1052,6 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, GenTree::SwapRelop(Compiler::AssertionDsc::ToCompareOper(curAssertion.GetKind(), &isUnsigned)); limit = Limit(Limit::keConstant, comp->vnStore->ConstantValue(curAssertion.GetOp1().GetVN())); } - // Otherwise, report it as "normalLclVN some-other-checked-bound (op1.vn)". - else if (canUseCheckedBounds && comp->vnStore->IsVNCheckedBound(curAssertion.GetOp1().GetVN())) - { - // Since we are swapping the operands, we also need to swap the comparison operator - cmpOper = - GenTree::SwapRelop(Compiler::AssertionDsc::ToCompareOper(curAssertion.GetKind(), &isUnsigned)); - limit = Limit(Limit::keBinOpArray, curAssertion.GetOp1().GetVN(), 0); - } else { continue; @@ -1092,14 +1085,7 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, int cnstLimit = (int)curAssertion.GetOp2().GetIntConstant(); assert(cnstLimit == comp->vnStore->CoercedConstantValue(curAssertion.GetOp2().GetVN())); - if ((cnstLimit == 0) && curAssertion.KindIs(Compiler::OAK_NOT_EQUAL) && canUseCheckedBounds && - comp->vnStore->IsVNCheckedBound(curAssertion.GetOp1().GetVN())) - { - // we have arr.Len != 0, so the length must be atleast one - limit = Limit(Limit::keConstant, 1); - cmpOper = GT_GE; - } - else if (curAssertion.KindIs(Compiler::OAK_EQUAL)) + if (curAssertion.KindIs(Compiler::OAK_EQUAL)) { limit = Limit(Limit::keConstant, cnstLimit); cmpOper = GT_EQ; diff --git a/src/coreclr/jit/rangecheck.h b/src/coreclr/jit/rangecheck.h index 5d0e1946d45046..b9cfbe3a0626bc 100644 --- a/src/coreclr/jit/rangecheck.h +++ b/src/coreclr/jit/rangecheck.h @@ -655,6 +655,24 @@ struct RangeOps return result; } + static Range Not(const Range& range) + { + int constVal; + if (range.IsSingleValueConstant(&constVal) && ((constVal == 0) || (constVal == 1))) + { + // ![0..0] = [1..1] + // ![1..1] = [0..0] + return Range(Limit(Limit::keConstant, constVal == 0 ? 1 : 0)); + } + if (range.LowerLimit().IsConstant() && (range.LowerLimit().GetConstant() == 0) && + range.UpperLimit().IsConstant() && (range.UpperLimit().GetConstant() == 1)) + { + // ![0..1] = [0..1] + return range; + } + return Limit(Limit::keUnknown); + } + //------------------------------------------------------------------------ // EvalRelop: Evaluate the relation between two ranges for the given relop // Example: "x >= y" is AlwaysTrue when "x.LowerLimit() >= y.UpperLimit()" From 22458d29cf10796c911848307f0167fa571f7ccd Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Fri, 13 Feb 2026 17:48:07 +0100 Subject: [PATCH 2/4] Enhance range check handling for checked bounds --- src/coreclr/jit/rangecheck.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index b6c726a1e3ab8e..ec31f048402189 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -1052,6 +1052,14 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, GenTree::SwapRelop(Compiler::AssertionDsc::ToCompareOper(curAssertion.GetKind(), &isUnsigned)); limit = Limit(Limit::keConstant, comp->vnStore->ConstantValue(curAssertion.GetOp1().GetVN())); } + // Otherwise, report it as "normalLclVN some-other-checked-bound (op1.vn)". + else if (canUseCheckedBounds && comp->vnStore->IsVNCheckedBound(curAssertion.GetOp1().GetVN())) + { + // Since we are swapping the operands, we also need to swap the comparison operator + cmpOper = + GenTree::SwapRelop(Compiler::AssertionDsc::ToCompareOper(curAssertion.GetKind(), &isUnsigned)); + limit = Limit(Limit::keBinOpArray, curAssertion.GetOp1().GetVN(), 0); + } else { continue; From 186cdc7e18c9620a94847e809757a707332373f3 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Mon, 16 Feb 2026 15:56:06 +0100 Subject: [PATCH 3/4] Update rangecheck.h --- src/coreclr/jit/rangecheck.h | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/coreclr/jit/rangecheck.h b/src/coreclr/jit/rangecheck.h index b9cfbe3a0626bc..5d0e1946d45046 100644 --- a/src/coreclr/jit/rangecheck.h +++ b/src/coreclr/jit/rangecheck.h @@ -655,24 +655,6 @@ struct RangeOps return result; } - static Range Not(const Range& range) - { - int constVal; - if (range.IsSingleValueConstant(&constVal) && ((constVal == 0) || (constVal == 1))) - { - // ![0..0] = [1..1] - // ![1..1] = [0..0] - return Range(Limit(Limit::keConstant, constVal == 0 ? 1 : 0)); - } - if (range.LowerLimit().IsConstant() && (range.LowerLimit().GetConstant() == 0) && - range.UpperLimit().IsConstant() && (range.UpperLimit().GetConstant() == 1)) - { - // ![0..1] = [0..1] - return range; - } - return Limit(Limit::keUnknown); - } - //------------------------------------------------------------------------ // EvalRelop: Evaluate the relation between two ranges for the given relop // Example: "x >= y" is AlwaysTrue when "x.LowerLimit() >= y.UpperLimit()" From 9daf71f3c226c95d4d7bb56946ecdc485a3e6e50 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Mon, 16 Feb 2026 15:56:56 +0100 Subject: [PATCH 4/4] Update rangecheck.cpp --- src/coreclr/jit/rangecheck.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index ec31f048402189..475f2cb91605d3 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -705,11 +705,10 @@ Range RangeCheck::GetRangeFromAssertions(Compiler* comp, ValueNum num, ASSERT_VA } break; - case VNF_NOT: case VNF_NEG: { Range r1 = GetRangeFromAssertions(comp, funcApp.m_args[0], assertions, --budget); - Range unaryOpResult = funcApp.FuncIs(VNF_NEG) ? RangeOps::Negate(r1) : RangeOps::Not(r1); + Range unaryOpResult = RangeOps::Negate(r1); // We can use the result only if it never overflows. result = unaryOpResult.IsConstantRange() ? unaryOpResult : result;