From e317d52d168bc3488d80e0d3b062f435e4491600 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:09:29 +0000 Subject: [PATCH 1/7] Initial plan From eeec066ced8a8ea71954b7d2f721f51bbeef86a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:45:17 +0000 Subject: [PATCH 2/7] Eliminate redundant bounds checks for arr[^N-1] after arr[^N] Add optimization in optAssertionProp_BndsChk: when a IsBoundsCheckNoThrow assertion exists for VNF_ADD(vnLen, -K1) and the current index is VNF_ADD(vnLen, -K2) with 0 < K2 <= K1, the current bounds check is redundant because the earlier check proved vnLen >= K1 >= K2. This eliminates 3 out of 4 bounds checks in patterns like: source[^4] | source[^3] | source[^2] | source[^1] Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com> --- src/coreclr/jit/assertionprop.cpp | 43 +++++++++++++++++++ .../JIT/opt/RangeChecks/ElidedBoundsChecks.cs | 10 +++++ 2 files changed, 53 insertions(+) diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 27df00a92e667d..5432af49f64e6a 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -5167,6 +5167,49 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree } } } + // Check if both the asserted index and the current index are of the form + // VNF_ADD(vnLen, -cns), where vnLen is the checked bound. + // If the asserted constant is more negative (larger absolute value), + // the current bounds check is redundant. + // a[a.Len-K1] followed by a[a.Len-K2] where 0 < K2 <= K1 + { + VNFuncApp assertionIdxFunc; + VNFuncApp curIdxFunc; + if (vnStore->GetVNFunc(curAssertion.GetOp1().GetVN(), &assertionIdxFunc) && + assertionIdxFunc.m_func == VNF_ADD && + vnStore->GetVNFunc(vnCurIdx, &curIdxFunc) && + curIdxFunc.m_func == VNF_ADD) + { + // Normalize constants to the right side. + if (!vnStore->IsVNInt32Constant(assertionIdxFunc.m_args[1])) + { + std::swap(assertionIdxFunc.m_args[0], assertionIdxFunc.m_args[1]); + } + if (!vnStore->IsVNInt32Constant(curIdxFunc.m_args[1])) + { + std::swap(curIdxFunc.m_args[0], curIdxFunc.m_args[1]); + } + + // Both must have the checked bound (array length) as the non-constant operand. + if (assertionIdxFunc.m_args[0] == vnCurLen && + curIdxFunc.m_args[0] == vnCurLen && + vnStore->IsVNInt32Constant(assertionIdxFunc.m_args[1]) && + vnStore->IsVNInt32Constant(curIdxFunc.m_args[1])) + { + int assertionDelta = vnStore->GetConstantInt32(assertionIdxFunc.m_args[1]); + int curDelta = vnStore->GetConstantInt32(curIdxFunc.m_args[1]); + + // Both deltas must be negative (indexing from end of array). + // assertionDelta <= curDelta means the asserted index was further from the end. + if (assertionDelta < 0 && curDelta < 0 && assertionDelta <= curDelta) + { + return dropBoundsCheck( + INDEBUG("a[a.Len-K1] followed by a[a.Len-K2], with 0 < K2 <= K1")); + } + } + } + } + // Extend this to remove additional redundant bounds checks: // i.e. a[i+1] followed by a[i] by using the VN(i+1) >= VN(i) // a[i] followed by a[j] when j is known to be >= i diff --git a/src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs b/src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs index e9438a1ba78934..e9eb1fbbc4b271 100644 --- a/src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs +++ b/src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs @@ -83,6 +83,13 @@ static bool IndexPlusConstLessThanLen(ReadOnlySpan span) return false; } + // After bounds-checking arr[^4], the checks for arr[^3], arr[^2], arr[^1] are redundant. + [MethodImpl(MethodImplOptions.NoInlining)] + static int IndexFromEndRedundant(ReadOnlySpan source) + { + return source[^4] | source[^3] | source[^2] | source[^1]; + } + [Fact] public static int TestEntryPoint() { @@ -119,6 +126,9 @@ public static int TestEntryPoint() if (IndexPlusConstLessThanLen("hello".AsSpan()) != false) return 0; + if (IndexFromEndRedundant(new int[] { 1, 2, 3, 4 }) != (1 | 2 | 3 | 4)) + return 0; + return 100; } } From 991c696cc8f7ab576de0a93b2feb774d9887db43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:23:17 +0000 Subject: [PATCH 3/7] Move bounds check optimization to MergeEdgeAssertions Revert the previous approach in optAssertionProp_BndsChk and instead fix range extraction in MergeEdgeAssertions. When a IsBoundsCheckNoThrow assertion has indexVN = VNF_ADD(lenVN, -CNS), decompose it to deduce that lenVN >= CNS. This allows the existing range-based bounds check elimination in optAssertionProp_BndsChk to work correctly. Also remove the test as requested - the existing infrastructure validates correctness. Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com> --- src/coreclr/jit/assertionprop.cpp | 43 ------------------- src/coreclr/jit/rangecheck.cpp | 21 +++++++-- .../JIT/opt/RangeChecks/ElidedBoundsChecks.cs | 10 ----- 3 files changed, 17 insertions(+), 57 deletions(-) diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 5432af49f64e6a..27df00a92e667d 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -5167,49 +5167,6 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree } } } - // Check if both the asserted index and the current index are of the form - // VNF_ADD(vnLen, -cns), where vnLen is the checked bound. - // If the asserted constant is more negative (larger absolute value), - // the current bounds check is redundant. - // a[a.Len-K1] followed by a[a.Len-K2] where 0 < K2 <= K1 - { - VNFuncApp assertionIdxFunc; - VNFuncApp curIdxFunc; - if (vnStore->GetVNFunc(curAssertion.GetOp1().GetVN(), &assertionIdxFunc) && - assertionIdxFunc.m_func == VNF_ADD && - vnStore->GetVNFunc(vnCurIdx, &curIdxFunc) && - curIdxFunc.m_func == VNF_ADD) - { - // Normalize constants to the right side. - if (!vnStore->IsVNInt32Constant(assertionIdxFunc.m_args[1])) - { - std::swap(assertionIdxFunc.m_args[0], assertionIdxFunc.m_args[1]); - } - if (!vnStore->IsVNInt32Constant(curIdxFunc.m_args[1])) - { - std::swap(curIdxFunc.m_args[0], curIdxFunc.m_args[1]); - } - - // Both must have the checked bound (array length) as the non-constant operand. - if (assertionIdxFunc.m_args[0] == vnCurLen && - curIdxFunc.m_args[0] == vnCurLen && - vnStore->IsVNInt32Constant(assertionIdxFunc.m_args[1]) && - vnStore->IsVNInt32Constant(curIdxFunc.m_args[1])) - { - int assertionDelta = vnStore->GetConstantInt32(assertionIdxFunc.m_args[1]); - int curDelta = vnStore->GetConstantInt32(curIdxFunc.m_args[1]); - - // Both deltas must be negative (indexing from end of array). - // assertionDelta <= curDelta means the asserted index was further from the end. - if (assertionDelta < 0 && curDelta < 0 && assertionDelta <= curDelta) - { - return dropBoundsCheck( - INDEBUG("a[a.Len-K1] followed by a[a.Len-K2], with 0 < K2 <= K1")); - } - } - } - } - // Extend this to remove additional redundant bounds checks: // i.e. a[i+1] followed by a[i] by using the VN(i+1) >= VN(i) // a[i] followed by a[j] when j is known to be >= i diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 475f2cb91605d3..a4080991adab11 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -1179,10 +1179,23 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, } else { - // We've seen arr[unknown_index] assertion while normalLclVN == arr.Length. - // This means the array has at least one element, so we can deduce "normalLclVN > 0". - cmpOper = GT_GT; - limit = Limit(Limit::keConstant, 0); + // Check if indexVN is VNF_ADD(lenVN, -CNS) which means lenVN >= CNS. + // Example: arr[arr.Length - 4] is in bounds implies arr.Length >= 4. + ValueNum addOpVN; + int addOpCns; + if (comp->vnStore->IsVNBinFuncWithConst(indexVN, VNF_ADD, &addOpVN, &addOpCns) && + (addOpVN == lenVN) && (addOpCns < 0) && (addOpCns > INT_MIN)) + { + cmpOper = GT_GT; + limit = Limit(Limit::keConstant, -addOpCns - 1); + } + else + { + // We've seen arr[unknown_index] assertion while normalLclVN == arr.Length. + // This means the array has at least one element, so we can deduce "normalLclVN > 0". + cmpOper = GT_GT; + limit = Limit(Limit::keConstant, 0); + } } } else diff --git a/src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs b/src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs index e9eb1fbbc4b271..e9438a1ba78934 100644 --- a/src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs +++ b/src/tests/JIT/opt/RangeChecks/ElidedBoundsChecks.cs @@ -83,13 +83,6 @@ static bool IndexPlusConstLessThanLen(ReadOnlySpan span) return false; } - // After bounds-checking arr[^4], the checks for arr[^3], arr[^2], arr[^1] are redundant. - [MethodImpl(MethodImplOptions.NoInlining)] - static int IndexFromEndRedundant(ReadOnlySpan source) - { - return source[^4] | source[^3] | source[^2] | source[^1]; - } - [Fact] public static int TestEntryPoint() { @@ -126,9 +119,6 @@ public static int TestEntryPoint() if (IndexPlusConstLessThanLen("hello".AsSpan()) != false) return 0; - if (IndexFromEndRedundant(new int[] { 1, 2, 3, 4 }) != (1 | 2 | 3 | 4)) - return 0; - return 100; } } From de7e7e6ef9c5c15b35746d1f22b4d6c3975267c2 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Wed, 18 Feb 2026 23:26:50 +0100 Subject: [PATCH 4/7] Refactor bounds check logic in rangecheck.cpp --- src/coreclr/jit/rangecheck.cpp | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index a4080991adab11..6ae9ac06621f6b 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -1129,6 +1129,8 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, // IsBoundsCheckNoThrow is "op1VN (Idx) LT_UN op2VN (Len)" ValueNum indexVN = curAssertion.GetOp1().GetVN(); ValueNum lenVN = curAssertion.GetOp2().GetCheckedBound(); + ValueNum indexOp1VN; + int indexOp2Cns; assert(curAssertion.GetOp2().GetCheckedBoundConstant() == 0); assert(curAssertion.GetOp2().IsCheckedBoundNeverNegative()); @@ -1161,6 +1163,14 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, } } } + // Example: we have IsBoundsCheckNoThrow assertion for arr[i - 2] with i == normalLclVN + // This means i >= 2. NOTE: "i - 2" is VNF_ADD(i, -2). + else if (comp->vnStore->IsVNBinFuncWithConst(indexVN, VNF_ADD, &indexOp1VN, &indexOp2Cns) && + (indexOp1VN == normalLclVN) && (indexOp2Cns < 0) && (indexOp2Cns > INT32_MIN)) + { + cmpOper = GT_GE; + limit = Limit(Limit::keConstant, -indexOp2Cns); + } else if (normalLclVN == lenVN) { if (comp->vnStore->IsVNInt32Constant(indexVN)) @@ -1179,23 +1189,10 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, } else { - // Check if indexVN is VNF_ADD(lenVN, -CNS) which means lenVN >= CNS. - // Example: arr[arr.Length - 4] is in bounds implies arr.Length >= 4. - ValueNum addOpVN; - int addOpCns; - if (comp->vnStore->IsVNBinFuncWithConst(indexVN, VNF_ADD, &addOpVN, &addOpCns) && - (addOpVN == lenVN) && (addOpCns < 0) && (addOpCns > INT_MIN)) - { - cmpOper = GT_GT; - limit = Limit(Limit::keConstant, -addOpCns - 1); - } - else - { - // We've seen arr[unknown_index] assertion while normalLclVN == arr.Length. - // This means the array has at least one element, so we can deduce "normalLclVN > 0". - cmpOper = GT_GT; - limit = Limit(Limit::keConstant, 0); - } + // We've seen arr[unknown_index] assertion while normalLclVN == arr.Length. + // This means the array has at least one element, so we can deduce "normalLclVN > 0". + cmpOper = GT_GT; + limit = Limit(Limit::keConstant, 0); } } else From 820f5d776c389a1fcb4e92ebb0f274d5678d0b38 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Wed, 18 Feb 2026 23:41:29 +0100 Subject: [PATCH 5/7] Update rangecheck.cpp --- src/coreclr/jit/rangecheck.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 6ae9ac06621f6b..253d27f113c6ff 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -1129,8 +1129,6 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, // IsBoundsCheckNoThrow is "op1VN (Idx) LT_UN op2VN (Len)" ValueNum indexVN = curAssertion.GetOp1().GetVN(); ValueNum lenVN = curAssertion.GetOp2().GetCheckedBound(); - ValueNum indexOp1VN; - int indexOp2Cns; assert(curAssertion.GetOp2().GetCheckedBoundConstant() == 0); assert(curAssertion.GetOp2().IsCheckedBoundNeverNegative()); @@ -1163,16 +1161,11 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, } } } - // Example: we have IsBoundsCheckNoThrow assertion for arr[i - 2] with i == normalLclVN - // This means i >= 2. NOTE: "i - 2" is VNF_ADD(i, -2). - else if (comp->vnStore->IsVNBinFuncWithConst(indexVN, VNF_ADD, &indexOp1VN, &indexOp2Cns) && - (indexOp1VN == normalLclVN) && (indexOp2Cns < 0) && (indexOp2Cns > INT32_MIN)) - { - cmpOper = GT_GE; - limit = Limit(Limit::keConstant, -indexOp2Cns); - } else if (normalLclVN == lenVN) { + ValueNum indexOp1VN; + int indexOp2Cns; + if (comp->vnStore->IsVNInt32Constant(indexVN)) { // We have "Const < arr.Length" assertion, it means that "arr.Length > Const" @@ -1187,6 +1180,13 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, continue; } } + // arr[arr.Length - 2] means arr.Length > 2, so we can deduce "normalLclVN > CNS" + else if (comp->vnStore->IsVNBinFuncWithConst(indexVN, VNF_ADD, &indexOp1VN, &indexOp2Cns) && + (indexOp1VN == normalLclVN) && (indexOp2Cns < 0) && (indexOp2Cns > INT32_MIN)) + { + cmpOper = GT_GT; + limit = Limit(Limit::keConstant, -indexOp2Cns); + } else { // We've seen arr[unknown_index] assertion while normalLclVN == arr.Length. From 4743b7dbb634ae1cf15530bb91094c31bc4f1fca Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Wed, 18 Feb 2026 23:44:51 +0100 Subject: [PATCH 6/7] Update comment for clarity on VN level deduction --- src/coreclr/jit/rangecheck.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 253d27f113c6ff..8046355cffa0e8 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -1180,7 +1180,8 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, continue; } } - // arr[arr.Length - 2] means arr.Length > 2, so we can deduce "normalLclVN > CNS" + // arr[arr.Length - CNS] means arr.Length > CNS, so we can deduce "normalLclVN > CNS" + // On the VN level it's VNF_ADD(normalLclVN, -CNS). else if (comp->vnStore->IsVNBinFuncWithConst(indexVN, VNF_ADD, &indexOp1VN, &indexOp2Cns) && (indexOp1VN == normalLclVN) && (indexOp2Cns < 0) && (indexOp2Cns > INT32_MIN)) { From b780ab673ae5d897af7553f419b6c40b9a7da4c9 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Wed, 18 Feb 2026 23:47:31 +0100 Subject: [PATCH 7/7] Update src/coreclr/jit/rangecheck.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- 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 8046355cffa0e8..e362d73bac21d1 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -1180,12 +1180,12 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, continue; } } - // arr[arr.Length - CNS] means arr.Length > CNS, so we can deduce "normalLclVN > CNS" + // arr[arr.Length - CNS] means arr.Length >= CNS, so we can deduce "normalLclVN >= CNS" // On the VN level it's VNF_ADD(normalLclVN, -CNS). else if (comp->vnStore->IsVNBinFuncWithConst(indexVN, VNF_ADD, &indexOp1VN, &indexOp2Cns) && (indexOp1VN == normalLclVN) && (indexOp2Cns < 0) && (indexOp2Cns > INT32_MIN)) { - cmpOper = GT_GT; + cmpOper = GT_GE; limit = Limit(Limit::keConstant, -indexOp2Cns); } else