From 2c5c8a258fdbe9548779d42a0e97f0c342155815 Mon Sep 17 00:00:00 2001 From: egorbot Date: Sat, 21 Feb 2026 20:41:52 +0100 Subject: [PATCH 1/4] add assertions for allocators --- src/coreclr/jit/assertionprop.cpp | 16 +++++++++ src/coreclr/jit/compiler.h | 2 +- src/coreclr/jit/earlyprop.cpp | 2 +- src/coreclr/jit/gentree.cpp | 7 ++-- src/coreclr/jit/valuenum.cpp | 57 ++++++++++++++++++------------- src/coreclr/jit/valuenum.h | 3 ++ 6 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 27df00a92e667d..ac1351eaeff56f 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -2086,6 +2086,22 @@ void Compiler::optAssertionGen(GenTree* tree) assert(thisArg != nullptr); assertionInfo = optCreateAssertion(thisArg, nullptr, /*equals*/ false); } + else if (!optLocalAssertionProp) + { + // Array allocation creates an assertion that the length argument is non-negative. + GenTree* lenArg = getArrayLengthFromAllocation(call); + if (lenArg != nullptr) + { + ValueNum lenVN = vnStore->VNIgnoreIntToLongCast(optConservativeNormalVN(lenArg)); + if ((lenVN != ValueNumStore::NoVN) && !vnStore->IsVNConstant(lenVN) && + vnStore->TypeOfVN(lenVN) == TYP_INT) + { + ValueNum zeroVN = vnStore->VNZeroForType(TYP_INT); + assertionInfo = optAddAssertion(AssertionDsc::CreateConstantBound(this, VNF_GE, lenVN, zeroVN)); + break; + } + } + } } break; diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 6d63894fa8f8ea..f2d55a5cb078f7 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -7595,7 +7595,7 @@ class Compiler typedef JitHashTable, GenTree*> LocalNumberToNullCheckTreeMap; - GenTree* getArrayLengthFromAllocation(GenTree* tree DEBUGARG(BasicBlock* block)); + GenTree* getArrayLengthFromAllocation(GenTree* tree); GenTree* optPropGetValueRec(unsigned lclNum, unsigned ssaNum, optPropKind valueKind, int walkDepth); GenTree* optPropGetValue(unsigned lclNum, unsigned ssaNum, optPropKind valueKind); GenTree* optEarlyPropRewriteTree(GenTree* tree, LocalNumberToNullCheckTreeMap* nullCheckMap); diff --git a/src/coreclr/jit/earlyprop.cpp b/src/coreclr/jit/earlyprop.cpp index b606e66399d894..0c6efd0985ee70 100644 --- a/src/coreclr/jit/earlyprop.cpp +++ b/src/coreclr/jit/earlyprop.cpp @@ -281,7 +281,7 @@ GenTree* Compiler::optPropGetValueRec(unsigned lclNum, unsigned ssaNum, optPropK { if (valueKind == optPropKind::OPK_ARRAYLEN) { - value = getArrayLengthFromAllocation(defValue DEBUGARG(ssaVarDsc->GetBlock())); + value = getArrayLengthFromAllocation(defValue); if (value != nullptr) { if (!value->IsCnsIntOrI()) diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index 036014e4baaaa0..e8bc0806675b01 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -2050,13 +2050,12 @@ bool GenTreeCall::IsPure(Compiler* compiler) const // helper call. // // Arguments: -// tree - The array allocation helper call. -// block - tree's basic block. +// tree - The array allocation helper call. // // Return Value: // Return the array length node. -GenTree* Compiler::getArrayLengthFromAllocation(GenTree* tree DEBUGARG(BasicBlock* block)) +GenTree* Compiler::getArrayLengthFromAllocation(GenTree* tree) { assert(tree != nullptr); @@ -2252,7 +2251,7 @@ bool GenTreeCall::HasSideEffects(Compiler* compiler, bool ignoreExceptions, bool // Consider array allocators side-effect free for constant length (if it's not negative and fits into i32) if (helperProperties.IsAllocator(helper)) { - GenTree* arrLen = compiler->getArrayLengthFromAllocation((GenTree*)this DEBUGARG(nullptr)); + GenTree* arrLen = compiler->getArrayLengthFromAllocation((GenTree*)this); // if arrLen is nullptr it means it wasn't an array allocator if ((arrLen != nullptr) && arrLen->IsIntCnsFitsInI32()) { diff --git a/src/coreclr/jit/valuenum.cpp b/src/coreclr/jit/valuenum.cpp index 2be00e7e1057c3..abe13af85c1189 100644 --- a/src/coreclr/jit/valuenum.cpp +++ b/src/coreclr/jit/valuenum.cpp @@ -2045,6 +2045,37 @@ ValueNum ValueNumStore::VNForCastOper(var_types castToType, bool srcIsUnsigned) return result; } +//------------------------------------------------------------------------ +// VNIgnoreIntToLongCast: Looks through a sign-extending int-to-long cast. +// +// Arguments: +// vn - The value number to inspect. +// +// Return Value: +// The value number of the original TYP_INT operand if 'vn' is a VNF_Cast +// that sign-extends a TYP_INT to TYP_LONG; otherwise returns 'vn' unchanged. +// +// Notes: +// This is useful when comparing array lengths or indices whose value numbers +// may differ only by an int-to-long widening cast introduced during IR +// transformations. +// +ValueNum ValueNumStore::VNIgnoreIntToLongCast(ValueNum vn) +{ + VNFuncApp funcApp; + if (GetVNFunc(vn, &funcApp) && funcApp.FuncIs(VNF_Cast)) + { + var_types castToType; + bool srcIsUnsigned; + GetCastOperFromVN(funcApp.m_args[1], &castToType, &srcIsUnsigned); + if (castToType == TYP_LONG && !srcIsUnsigned && TypeOfVN(funcApp.m_args[0]) == TYP_INT) + { + return funcApp.m_args[0]; + } + } + return vn; +} + void ValueNumStore::GetCastOperFromVN(ValueNum vn, var_types* pCastToType, bool* pSrcIsUnsigned) { assert(pCastToType != nullptr); @@ -2625,30 +2656,10 @@ ValueNum ValueNumStore::VNForFunc(var_types typ, VNFunc func, ValueNum arg0VN) VNFuncApp newArrFuncApp; if (GetVNFunc(arg0VN, &newArrFuncApp) && (newArrFuncApp.m_func == VNF_JitNewArr)) { - ValueNum actualSizeVN = newArrFuncApp.m_args[1]; - var_types actualSizeVNType = TypeOfVN(actualSizeVN); - - // JitNewArr's size argument (args[1]) is typically upcasted to TYP_LONG via VNF_Cast. - if (actualSizeVNType == TYP_LONG) - { - VNFuncApp castFuncApp; - if (GetVNFunc(actualSizeVN, &castFuncApp) && (castFuncApp.m_func == VNF_Cast)) - { - var_types castToType; - bool srcIsUnsigned; - GetCastOperFromVN(castFuncApp.m_args[1], &castToType, &srcIsUnsigned); - - // Make sure we have exactly (TYP_LONG)myInt32 cast: - if (!srcIsUnsigned && (castToType == TYP_LONG) && TypeOfVN(castFuncApp.m_args[0]) == TYP_INT) - { - // If that is the case, return the original size argument - *resultVN = castFuncApp.m_args[0]; - } - } - } - else if (actualSizeVNType == TYP_INT) + ValueNum actualSizeVN = VNIgnoreIntToLongCast(newArrFuncApp.m_args[1]); + if (TypeOfVN(actualSizeVN) == TYP_INT) { - *resultVN = actualSizeVN; + return actualSizeVN; } } } diff --git a/src/coreclr/jit/valuenum.h b/src/coreclr/jit/valuenum.h index 016eabdc2f84d5..49b4c63b4898a6 100644 --- a/src/coreclr/jit/valuenum.h +++ b/src/coreclr/jit/valuenum.h @@ -508,6 +508,9 @@ class ValueNumStore // Unpacks the information stored by VNForCastOper in the constant represented by the value number. void GetCastOperFromVN(ValueNum vn, var_types* pCastToType, bool* pSrcIsUnsigned); + // Returns the underlying value number if 'vn' represents a sign-extending int-to-long cast; otherwise returns 'vn'. + ValueNum VNIgnoreIntToLongCast(ValueNum vn); + // We keep handle values in a separate pool, so we don't confuse a handle with an int constant // that happens to be the same... ValueNum VNForHandle(ssize_t cnsVal, GenTreeFlags iconFlags); From 5d3d1cc3d84eaedf2d889975ff9ffea423afd8a0 Mon Sep 17 00:00:00 2001 From: egorbot Date: Sat, 21 Feb 2026 21:57:24 +0100 Subject: [PATCH 2/4] more assertions --- src/coreclr/jit/assertionprop.cpp | 52 +++++++++++++++++++++++++++++++ src/coreclr/jit/valuenum.cpp | 10 ++++++ 2 files changed, 62 insertions(+) diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index ac1351eaeff56f..57a886ace395c4 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -2089,6 +2089,9 @@ void Compiler::optAssertionGen(GenTree* tree) else if (!optLocalAssertionProp) { // Array allocation creates an assertion that the length argument is non-negative. + // + // var arr = new int[n]; - creates n >= 0 assertion + // GenTree* lenArg = getArrayLengthFromAllocation(call); if (lenArg != nullptr) { @@ -2101,10 +2104,59 @@ void Compiler::optAssertionGen(GenTree* tree) break; } } + + // CORINFO_HELP_ARRADDR_ST(arrRef, idx, value) creates an assertion that "idx" is within the bounds of + // "arrRef" array + // + // arr[idx] = value; - creates idx is within bounds of arr assertion + // + if (call->IsHelperCall(this, CORINFO_HELP_ARRADDR_ST)) + { + GenTree* arrRef = call->gtArgs.GetUserArgByIndex(0)->GetNode(); + GenTree* idx = call->gtArgs.GetUserArgByIndex(1)->GetNode(); + + ValueNum idxVN = vnStore->VNIgnoreIntToLongCast(optConservativeNormalVN(idx)); + if ((idxVN != ValueNumStore::NoVN) && (vnStore->TypeOfVN(idxVN) == TYP_INT)) + { + ValueNum arrRefVN = optConservativeNormalVN(arrRef); + if (arrRefVN != ValueNumStore::NoVN) + { + // Compose a VN_ARR_LENGTH VN for the array reference and use it + // to create a bounds check assertion on the index. + ValueNum lenVN = vnStore->VNForFunc(TYP_INT, VNF_ARR_LENGTH, arrRefVN); + assertionInfo = optAddAssertion(AssertionDsc::CreateNoThrowArrBnd(this, idxVN, lenVN)); + break; + } + } + } } } break; + case GT_DIV: + case GT_UDIV: + case GT_MOD: + case GT_UMOD: + if (!optLocalAssertionProp) + { + // For division/modulo, we can create an assertion that the divisor is not zero + // + // c = a / b; - creates b != 0 assertion + // + ValueNum divisorVN = optConservativeNormalVN(tree->AsOp()->gtGetOp2()); + if ((divisorVN != ValueNumStore::NoVN) && !vnStore->IsVNConstant(divisorVN)) + { + var_types divisorType = vnStore->TypeOfVN(divisorVN); + if (varTypeIsIntegral(divisorType)) + { + ValueNum zeroVN = vnStore->VNZeroForType(divisorType); + assertionInfo = + optAddAssertion(AssertionDsc::CreateConstantBound(this, VNF_NE, divisorVN, zeroVN)); + } + } + } + break; + case GT_JTRUE: assertionInfo = optAssertionGenJtrue(tree); break; diff --git a/src/coreclr/jit/valuenum.cpp b/src/coreclr/jit/valuenum.cpp index abe13af85c1189..f91d299a89dc79 100644 --- a/src/coreclr/jit/valuenum.cpp +++ b/src/coreclr/jit/valuenum.cpp @@ -2062,6 +2062,11 @@ ValueNum ValueNumStore::VNForCastOper(var_types castToType, bool srcIsUnsigned) // ValueNum ValueNumStore::VNIgnoreIntToLongCast(ValueNum vn) { + if (TypeOfVN(vn) != TYP_LONG) + { + return vn; + } + VNFuncApp funcApp; if (GetVNFunc(vn, &funcApp) && funcApp.FuncIs(VNF_Cast)) { @@ -2073,6 +2078,11 @@ ValueNum ValueNumStore::VNIgnoreIntToLongCast(ValueNum vn) return funcApp.m_args[0]; } } + int intCns; + if (IsVNIntegralConstant(vn, &intCns)) + { + return VNForIntCon(intCns); + } return vn; } From d6c953e22588efe1eabff299150fe3784eef86c1 Mon Sep 17 00:00:00 2001 From: egorbot Date: Sat, 21 Feb 2026 22:29:33 +0100 Subject: [PATCH 3/4] cleanup --- src/coreclr/jit/assertionprop.cpp | 1 + src/coreclr/jit/valuenum.cpp | 45 +++++++++++++++---------------- src/coreclr/jit/valuenum.h | 1 - 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 57a886ace395c4..4f48caa06326c2 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -2112,6 +2112,7 @@ void Compiler::optAssertionGen(GenTree* tree) // if (call->IsHelperCall(this, CORINFO_HELP_ARRADDR_ST)) { + assert(call->gtArgs.CountUserArgs() == 3); GenTree* arrRef = call->gtArgs.GetUserArgByIndex(0)->GetNode(); GenTree* idx = call->gtArgs.GetUserArgByIndex(1)->GetNode(); diff --git a/src/coreclr/jit/valuenum.cpp b/src/coreclr/jit/valuenum.cpp index f91d299a89dc79..071263cb6fc88e 100644 --- a/src/coreclr/jit/valuenum.cpp +++ b/src/coreclr/jit/valuenum.cpp @@ -2046,43 +2046,40 @@ ValueNum ValueNumStore::VNForCastOper(var_types castToType, bool srcIsUnsigned) } //------------------------------------------------------------------------ -// VNIgnoreIntToLongCast: Looks through a sign-extending int-to-long cast. +// VNIgnoreIntToLongCast: Looks through a sign-extending int-to-long cast +// or convert long-typed integral constants to int. // // Arguments: // vn - The value number to inspect. // // Return Value: // The value number of the original TYP_INT operand if 'vn' is a VNF_Cast -// that sign-extends a TYP_INT to TYP_LONG; otherwise returns 'vn' unchanged. -// -// Notes: -// This is useful when comparing array lengths or indices whose value numbers -// may differ only by an int-to-long widening cast introduced during IR -// transformations. +// that sign-extends a TYP_INT to TYP_LONG; or the value number of a TYP_INT +// constant if 'vn' is a TYP_LONG constant that fits in an int; otherwise, 'vn' itself. // ValueNum ValueNumStore::VNIgnoreIntToLongCast(ValueNum vn) { - if (TypeOfVN(vn) != TYP_LONG) + if (TypeOfVN(vn) == TYP_LONG) { - return vn; - } + VNFuncApp funcApp; + if (GetVNFunc(vn, &funcApp) && funcApp.FuncIs(VNF_Cast)) + { + var_types castToType; + bool srcIsUnsigned; + GetCastOperFromVN(funcApp.m_args[1], &castToType, &srcIsUnsigned); + if ((castToType == TYP_LONG) && !srcIsUnsigned && TypeOfVN(funcApp.m_args[0]) == TYP_INT) + { + return funcApp.m_args[0]; + } + } - VNFuncApp funcApp; - if (GetVNFunc(vn, &funcApp) && funcApp.FuncIs(VNF_Cast)) - { - var_types castToType; - bool srcIsUnsigned; - GetCastOperFromVN(funcApp.m_args[1], &castToType, &srcIsUnsigned); - if (castToType == TYP_LONG && !srcIsUnsigned && TypeOfVN(funcApp.m_args[0]) == TYP_INT) + // Also look through any long-typed integral constant that fits in an int. + int intCns; + if (IsVNIntegralConstant(vn, &intCns)) { - return funcApp.m_args[0]; + return VNForIntCon(intCns); } } - int intCns; - if (IsVNIntegralConstant(vn, &intCns)) - { - return VNForIntCon(intCns); - } return vn; } @@ -2669,7 +2666,7 @@ ValueNum ValueNumStore::VNForFunc(var_types typ, VNFunc func, ValueNum arg0VN) ValueNum actualSizeVN = VNIgnoreIntToLongCast(newArrFuncApp.m_args[1]); if (TypeOfVN(actualSizeVN) == TYP_INT) { - return actualSizeVN; + *resultVN = actualSizeVN; } } } diff --git a/src/coreclr/jit/valuenum.h b/src/coreclr/jit/valuenum.h index 49b4c63b4898a6..02050154df46cf 100644 --- a/src/coreclr/jit/valuenum.h +++ b/src/coreclr/jit/valuenum.h @@ -508,7 +508,6 @@ class ValueNumStore // Unpacks the information stored by VNForCastOper in the constant represented by the value number. void GetCastOperFromVN(ValueNum vn, var_types* pCastToType, bool* pSrcIsUnsigned); - // Returns the underlying value number if 'vn' represents a sign-extending int-to-long cast; otherwise returns 'vn'. ValueNum VNIgnoreIntToLongCast(ValueNum vn); // We keep handle values in a separate pool, so we don't confuse a handle with an int constant From d82c83cc8ce4090e4bf585e049228f61f546c38a Mon Sep 17 00:00:00 2001 From: egorbot Date: Sat, 21 Feb 2026 22:52:09 +0100 Subject: [PATCH 4/4] add CORINFO_HELP_LDELEMA_REF as well --- src/coreclr/jit/assertionprop.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 4f48caa06326c2..60389763455d30 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -2097,7 +2097,7 @@ void Compiler::optAssertionGen(GenTree* tree) { ValueNum lenVN = vnStore->VNIgnoreIntToLongCast(optConservativeNormalVN(lenArg)); if ((lenVN != ValueNumStore::NoVN) && !vnStore->IsVNConstant(lenVN) && - vnStore->TypeOfVN(lenVN) == TYP_INT) + (vnStore->TypeOfVN(lenVN) == TYP_INT)) { ValueNum zeroVN = vnStore->VNZeroForType(TYP_INT); assertionInfo = optAddAssertion(AssertionDsc::CreateConstantBound(this, VNF_GE, lenVN, zeroVN)); @@ -2110,7 +2110,8 @@ void Compiler::optAssertionGen(GenTree* tree) // // arr[idx] = value; - creates idx is within bounds of arr assertion // - if (call->IsHelperCall(this, CORINFO_HELP_ARRADDR_ST)) + CorInfoHelpFunc helperId = eeGetHelperNum(call->gtCallMethHnd); + if ((helperId == CORINFO_HELP_ARRADDR_ST) || (helperId == CORINFO_HELP_LDELEMA_REF)) { assert(call->gtArgs.CountUserArgs() == 3); GenTree* arrRef = call->gtArgs.GetUserArgByIndex(0)->GetNode();