From 3c6eae298e5c99c0458182e7be45e245e3fed993 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Tue, 2 Jun 2026 21:48:49 -0700 Subject: [PATCH 1/3] Update GetRangeFromAssertions to handle constants that FitsIn --- src/coreclr/jit/assertionprop.cpp | 94 +++++++++++++++------------ src/coreclr/jit/rangecheck.cpp | 104 ++++++++++++++++++++++-------- src/coreclr/jit/rangecheck.h | 4 +- 3 files changed, 133 insertions(+), 69 deletions(-) diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 7255607103f7af..3bfbc8b14b6b0e 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -300,6 +300,10 @@ bool IntegralRange::Contains(int64_t value) const { rangeType = TYP_USHORT; } + else if ((elementCount == 32) && varTypeIsLong(rangeType)) + { + return {SymbolicIntegerValue::Zero, UpperBoundForType(TYP_UINT)}; + } break; } @@ -4082,17 +4086,20 @@ void Compiler::optAssertionProp_RangeProperties(ASSERT_VALARG_TP assertions, } // Let's see if MergeEdgeAssertions can help us: - if (genActualType(tree) == TYP_INT) + if (varTypeIsIntegral(tree)) { - Range rng = RangeCheck::GetRangeFromAssertions(this, treeVN, assertions); - assert(rng.IsConstantRange()); - if (rng.LowerLimit().GetConstant() >= 0) - { - *isKnownNonNegative = true; - } - if ((rng.LowerLimit().GetConstant() > 0) || (rng.UpperLimit().GetConstant() < 0)) + Range rng = RangeCheck::GetRangeFromAssertions(this, tree->TypeGet(), treeVN, assertions); + + if (rng.IsConstantRange()) { - *isKnownNonZero = true; + if (rng.LowerLimit().GetConstant() >= 0) + { + *isKnownNonNegative = true; + } + if ((rng.LowerLimit().GetConstant() > 0) || (rng.UpperLimit().GetConstant() < 0)) + { + *isKnownNonZero = true; + } } } } @@ -4118,8 +4125,10 @@ GenTree* Compiler::optAssertionProp_AddMulSub(ASSERT_VALARG_TP assertions, GenTr GenTree* op1 = tree->gtGetOp1(); GenTree* op2 = tree->gtGetOp2(); - Range op1Rng = RangeCheck::GetRangeFromAssertions(this, optConservativeNormalVN(op1), assertions); - Range op2Rng = RangeCheck::GetRangeFromAssertions(this, optConservativeNormalVN(op2), assertions); + Range op1Rng = + RangeCheck::GetRangeFromAssertions(this, op1->TypeGet(), optConservativeNormalVN(op1), assertions); + Range op2Rng = + RangeCheck::GetRangeFromAssertions(this, op2->TypeGet(), optConservativeNormalVN(op2), assertions); Range result = Limit(Limit::keUnknown); if (tree->OperIs(GT_MUL)) { @@ -4496,7 +4505,7 @@ GenTree* Compiler::optAssertionPropGlobal_RelOp(ASSERT_VALARG_TP assertions, // We don't need the op1->TypeIs(TYP_INT) check, but it seems to improve the TP quite a bit. if (op1->TypeIs(TYP_INT)) { - Range relopRange = RangeCheck::GetRangeFromAssertions(this, relopVN, assertions); + Range relopRange = RangeCheck::GetRangeFromAssertions(this, tree->TypeGet(), relopVN, assertions); int relopResult; if (!relopRange.IsSingleValueConstant(&relopResult)) @@ -4512,8 +4521,8 @@ GenTree* Compiler::optAssertionPropGlobal_RelOp(ASSERT_VALARG_TP assertions, } else { - Range op1Range = RangeCheck::GetRangeFromAssertions(this, op1VN, assertions); - Range op2Range = RangeCheck::GetRangeFromAssertions(this, op2VN, assertions); + Range op1Range = RangeCheck::GetRangeFromAssertions(this, op1->TypeGet(), op1VN, assertions); + Range op2Range = RangeCheck::GetRangeFromAssertions(this, op2->TypeGet(), op2VN, assertions); relopRange = RangeOps::EvalRelop(tree->OperGet(), tree->IsUnsigned(), op1Range, op2Range); } } @@ -4940,29 +4949,32 @@ GenTree* Compiler::optAssertionProp_Cast(ASSERT_VALARG_TP assertions, if (FitsIn(castLo) && FitsIn(castHi)) { Range castToTypeRange = Range(Limit(Limit::keConstant, (int)castLo), Limit(Limit::keConstant, (int)castHi)); - if (castToTypeRange.IsConstantRange() && (genActualType(cast->CastOp()) == TYP_INT)) + GenTree* castOp = cast->CastOp(); + if (castToTypeRange.IsConstantRange() && (genActualType(castOp) == TYP_INT)) { - ValueNum castOpVN = optConservativeNormalVN(cast->CastOp()); - Range castOpRng = RangeCheck::GetRangeFromAssertions(this, castOpVN, assertions); - assert(castOpRng.IsConstantRange()); + ValueNum castOpVN = optConservativeNormalVN(castOp); + Range castOpRng = RangeCheck::GetRangeFromAssertions(this, castOp->TypeGet(), castOpVN, assertions); - int castFromLo = castOpRng.LowerLimit().GetConstant(); - int castFromHi = castOpRng.UpperLimit().GetConstant(); - int castToLo = castToTypeRange.LowerLimit().GetConstant(); - int castToHi = castToTypeRange.UpperLimit().GetConstant(); - - if ((castFromLo >= castToLo) && (castFromHi <= castToHi)) + if (castOpRng.IsConstantRange()) { - if (canDropCast) + int castFromLo = castOpRng.LowerLimit().GetConstant(); + int castFromHi = castOpRng.UpperLimit().GetConstant(); + int castToLo = castToTypeRange.LowerLimit().GetConstant(); + int castToHi = castToTypeRange.UpperLimit().GetConstant(); + + if ((castFromLo >= castToLo) && (castFromHi <= castToHi)) { - JITDUMP("Removing cast %06u as redundant based on VN assertions.\n", dspTreeID(cast)); - return optAssertionProp_Update(cast->CastOp(), cast, stmt); - } + if (canDropCast) + { + JITDUMP("Removing cast %06u as redundant based on VN assertions.\n", dspTreeID(cast)); + return optAssertionProp_Update(cast->CastOp(), cast, stmt); + } - assert(cast->gtOverflow()); - JITDUMP("Clearing overflow flag for cast %06u based on VN assertions.\n", dspTreeID(cast)); - cast->ClearOverflow(); - return optAssertionProp_Update(cast, cast, stmt); + assert(cast->gtOverflow()); + JITDUMP("Clearing overflow flag for cast %06u based on VN assertions.\n", dspTreeID(cast)); + cast->ClearOverflow(); + return optAssertionProp_Update(cast, cast, stmt); + } } } } @@ -5525,9 +5537,11 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree } #endif // FEATURE_ENABLE_NO_RANGE_CHECKS - GenTreeBoundsChk* arrBndsChk = tree->AsBoundsChk(); - ValueNum vnCurIdx = vnStore->VNConservativeNormalValue(arrBndsChk->GetIndex()->gtVNPair); - ValueNum vnCurLen = vnStore->VNConservativeNormalValue(arrBndsChk->GetArrayLength()->gtVNPair); + GenTreeBoundsChk* arrBndsChk = tree->AsBoundsChk(); + GenTree* arrBndsChkIdx = arrBndsChk->GetIndex(); + GenTree* arrBndsChkLen = arrBndsChk->GetArrayLength(); + ValueNum vnCurIdx = vnStore->VNConservativeNormalValue(arrBndsChk->GetIndex()->gtVNPair); + ValueNum vnCurLen = vnStore->VNConservativeNormalValue(arrBndsChk->GetArrayLength()->gtVNPair); auto dropBoundsCheck = [&](INDEBUG(const char* reason)) -> GenTree* { JITDUMP("\nVN based redundant (%s) bounds check assertion prop in " FMT_BB ":\n", reason, compCurBB->bbNum); @@ -5555,7 +5569,7 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree if ((add0 == vnCurLen) && vnStore->IsVNInt32Constant(add1)) { - Range rng = RangeCheck::GetRangeFromAssertions(this, vnCurLen, assertions); + Range rng = RangeCheck::GetRangeFromAssertions(this, arrBndsChkLen->TypeGet(), vnCurLen, assertions); // Lower known limit of ArrLen: const int lenLowerLimit = rng.LowerLimit().GetConstant(); @@ -5588,7 +5602,7 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree assert(curAssertion.GetOp2().IsVNNeverNegative()); // Do we have a previous range check involving the same 'vnLen' upper bound? - if (curAssertion.GetOp2().GetVN() == optConservativeNormalVN(arrBndsChk->GetArrayLength())) + if (curAssertion.GetOp2().GetVN() == optConservativeNormalVN(arrBndsChkLen)) { // Do we have the exact same lower bound 'vnIdx'? // a[i] followed by a[i] @@ -5599,7 +5613,7 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree // Are we using zero as the index? // It can always be considered as redundant with any previous value // a[*] followed by a[0] - else if (vnCurIdx == vnStore->VNZeroForType(arrBndsChk->GetIndex()->TypeGet())) + else if (vnCurIdx == vnStore->VNZeroForType(arrBndsChkIdx->TypeGet())) { return dropBoundsCheck(INDEBUG("a[*] followed by a[0]")); } @@ -5654,8 +5668,8 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree if ((genActualType(vnStore->TypeOfVN(vnCurIdx)) == TYP_INT) && (genActualType(vnStore->TypeOfVN(vnCurLen)) == TYP_INT)) { - Range idxRng = RangeCheck::GetRangeFromAssertions(this, vnCurIdx, assertions); - Range lenRng = RangeCheck::GetRangeFromAssertions(this, vnCurLen, assertions); + Range idxRng = RangeCheck::GetRangeFromAssertions(this, arrBndsChkIdx->TypeGet(), vnCurIdx, assertions); + Range lenRng = RangeCheck::GetRangeFromAssertions(this, arrBndsChkLen->TypeGet(), vnCurLen, assertions); if (idxRng.IsConstantRange() && lenRng.IsConstantRange()) { int idxLo = idxRng.LowerLimit().GetConstant(); diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index c8ae428e1a649a..b23405e4721026 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -649,6 +649,7 @@ void RangeCheck::MergeEdgeAssertions(GenTreeLclVarCommon* lcl, ASSERT_VALARG_TP // // Arguments: // comp - the compiler instance +// type - the type for which we are getting the range // num - the value number to analyze range for // assertions - the assertions to use // budget - the remaining budget for recursive analysis @@ -656,10 +657,11 @@ void RangeCheck::MergeEdgeAssertions(GenTreeLclVarCommon* lcl, ASSERT_VALARG_TP // Return Value: // The computed range // -Range RangeCheck::GetRangeFromAssertions(Compiler* comp, ValueNum num, ASSERT_VALARG_TP assertions, int budget) +Range RangeCheck::GetRangeFromAssertions( + Compiler* comp, var_types type, ValueNum num, ASSERT_VALARG_TP assertions, int budget) { ValueNumStore::SmallValueNumSet set; - return GetRangeFromAssertionsWorker(comp, num, assertions, budget, &set); + return GetRangeFromAssertionsWorker(comp, type, num, assertions, budget, &set); } //------------------------------------------------------------------------ @@ -668,6 +670,7 @@ Range RangeCheck::GetRangeFromAssertions(Compiler* comp, ValueNum num, ASSERT_VA // // Arguments: // comp - the compiler instance +// type - the type for which we are getting the range // num - the value number to analyze range for // assertions - the assertions to use // budget - the remaining budget for recursive analysis @@ -677,18 +680,21 @@ Range RangeCheck::GetRangeFromAssertions(Compiler* comp, ValueNum num, ASSERT_VA // Return Value: // The computed range // -Range RangeCheck::GetRangeFromAssertionsWorker( - Compiler* comp, ValueNum num, ASSERT_VALARG_TP assertions, int budget, ValueNumStore::SmallValueNumSet* visited) +Range RangeCheck::GetRangeFromAssertionsWorker(Compiler* comp, + var_types type, + ValueNum num, + ASSERT_VALARG_TP assertions, + int budget, + ValueNumStore::SmallValueNumSet* visited) { - // Start with the widest possible constant range. - Range result = Range(Limit(Limit::keConstant, INT32_MIN), Limit(Limit::keConstant, INT32_MAX)); - if ((num == ValueNumStore::NoVN) || (budget <= 0)) { - return result; + // Start with the widest possible constant range. + return GetRangeFromType(type); } - // Currently, we only handle int32 and smaller integer types. + // Currently, we only handle int32 or smaller integer types + // and a small subset of int64 values that are known to fit in int32 var_types vnType = comp->vnStore->TypeOfVN(num); if (varTypeIsGC(vnType)) @@ -699,22 +705,40 @@ Range RangeCheck::GetRangeFromAssertionsWorker( // The PhiDef visitor below recurses into reaching VNs, so a BYREF VN can show up // here even though our public callers only pass us int-typed trees. // We have no useful integer range to derive from a pointer, so just give up. - assert(TARGET_POINTER_SIZE == 4); - return result; + +#if TARGET_64BIT + return Limit(Limit::keUnknown); +#else + return GetRangeFromType(TYP_INT); +#endif } - assert(genActualType(vnType) == TYP_INT); - result = GetRangeFromType(vnType); + Range result = GetRangeFromType(vnType); // // First, let's see if we can tighten the range based on VN information. // // If it's a constant, it's already as tight as it can get. - int cns; - if (comp->vnStore->IsVNIntegralConstant(num, &cns)) + if (comp->vnStore->IsVNConstant(num)) { - return Range(Limit(Limit::keConstant, cns)); + int cns; + if (comp->vnStore->IsVNIntegralConstant(num, &cns)) + { + return Range(Limit(Limit::keConstant, cns)); + } + else + { + // TODO: We could return `0, keUnknown` if the constant is known positive + // but this would require more handling in other places to take advantage of. + return Limit(Limit::keUnknown); + } + } + + if (genActualType(vnType) != TYP_INT) + { + // TODO: We should handle TYP_LONG for anything that FitsIn + return Limit(Limit::keUnknown); } VNFuncApp funcApp; @@ -728,6 +752,9 @@ Range RangeCheck::GetRangeFromAssertionsWorker( bool srcIsUnsigned; comp->vnStore->GetCastOperFromVN(funcApp.GetArg(1), &castToType, &srcIsUnsigned); + ValueNum arg0VN = funcApp.GetArg(0); + var_types arg0Typ = comp->vnStore->TypeOfVN(arg0VN); + // GetRangeFromType returns a non-constant range if it can't be represented with Range Range castToTypeRange = GetRangeFromType(castToType); if (castToTypeRange.IsConstantRange()) @@ -736,10 +763,10 @@ Range RangeCheck::GetRangeFromAssertionsWorker( // Now see if we can do better by looking at the cast source. // if its range is within the castTo range, we can use that (and the cast is basically a no-op). - if (genActualType(comp->vnStore->TypeOfVN(funcApp.GetArg(0))) == TYP_INT) + if (genActualType(arg0Typ) == TYP_INT) { Range castOpRange = - GetRangeFromAssertionsWorker(comp, funcApp.GetArg(0), assertions, --budget, visited); + GetRangeFromAssertionsWorker(comp, arg0Typ, arg0VN, assertions, --budget, visited); if (castOpRange.IsConstantRange() && (castOpRange.LowerLimit().GetConstant() >= castToTypeRange.LowerLimit().GetConstant()) && (castOpRange.UpperLimit().GetConstant() <= castToTypeRange.UpperLimit().GetConstant())) @@ -753,7 +780,10 @@ Range RangeCheck::GetRangeFromAssertionsWorker( case VNF_NEG: { - Range r1 = GetRangeFromAssertionsWorker(comp, funcApp.GetArg(0), assertions, --budget, visited); + ValueNum arg0VN = funcApp.GetArg(0); + var_types arg0Typ = comp->vnStore->TypeOfVN(arg0VN); + + Range r1 = GetRangeFromAssertionsWorker(comp, arg0Typ, arg0VN, assertions, --budget, visited); Range unaryOpResult = RangeOps::Negate(r1); // We can use the result only if it never overflows. @@ -772,8 +802,14 @@ Range RangeCheck::GetRangeFromAssertionsWorker( case VNF_UMOD: { // Get ranges of both operands and perform the same operation on the ranges. - Range r1 = GetRangeFromAssertionsWorker(comp, funcApp.GetArg(0), assertions, --budget, visited); - Range r2 = GetRangeFromAssertionsWorker(comp, funcApp.GetArg(1), assertions, --budget, visited); + ValueNum arg0VN = funcApp.GetArg(0); + var_types arg0Typ = comp->vnStore->TypeOfVN(arg0VN); + + ValueNum arg1VN = funcApp.GetArg(1); + var_types arg1Typ = comp->vnStore->TypeOfVN(arg1VN); + + Range r1 = GetRangeFromAssertionsWorker(comp, arg0Typ, arg0VN, assertions, --budget, visited); + Range r2 = GetRangeFromAssertionsWorker(comp, arg1Typ, arg1VN, assertions, --budget, visited); Range binOpResult = Range(Limit(Limit::keUnknown)); switch (funcApp.GetFunc()) { @@ -834,13 +870,18 @@ Range RangeCheck::GetRangeFromAssertionsWorker( result.lLimit = Limit(Limit::keConstant, 0); result.uLimit = Limit(Limit::keConstant, 1); + ValueNum arg0VN = funcApp.GetArg(0); + var_types arg0Typ = comp->vnStore->TypeOfVN(arg0VN); + + ValueNum arg1VN = funcApp.GetArg(1); + var_types arg1Typ = comp->vnStore->TypeOfVN(arg1VN); + // But maybe we can do better and determine if they are always true or always false, // hence, return [1..1] or [0..0] - if ((genActualType(comp->vnStore->TypeOfVN(funcApp.GetArg(0))) == TYP_INT) && - (genActualType(comp->vnStore->TypeOfVN(funcApp.GetArg(1))) == TYP_INT)) + if ((genActualType(arg0Typ) == TYP_INT) && (genActualType(arg1Typ) == TYP_INT)) { - Range r1 = GetRangeFromAssertionsWorker(comp, funcApp.GetArg(0), assertions, --budget, visited); - Range r2 = GetRangeFromAssertionsWorker(comp, funcApp.GetArg(1), assertions, --budget, visited); + Range r1 = GetRangeFromAssertionsWorker(comp, arg0Typ, arg0VN, assertions, --budget, visited); + Range r2 = GetRangeFromAssertionsWorker(comp, arg1Typ, arg1VN, assertions, --budget, visited); bool isUnsigned = true; genTreeOps cmpOper; @@ -1033,7 +1074,9 @@ Range RangeCheck::GetRangeFromAssertionsWorker( Range phiRange = Range(Limit(Limit::keUndef)); auto visitor = [comp, &phiRange, &budget, visited](ValueNum reachingVN, ASSERT_TP reachingAssertions) { // call GetRangeFromAssertionsWorker for each reaching VN using reachingAssertions - Range edgeRange = GetRangeFromAssertionsWorker(comp, reachingVN, reachingAssertions, --budget, visited); + var_types reachingTyp = comp->vnStore->TypeOfVN(reachingVN); + Range edgeRange = + GetRangeFromAssertionsWorker(comp, reachingTyp, reachingVN, reachingAssertions, --budget, visited); // If phiRange is not yet set, set it to the first edgeRange // else merge it with the new edgeRange. Example: [10..100] U [50..150] = [10..150] @@ -1063,7 +1106,11 @@ Range RangeCheck::GetRangeFromAssertionsWorker( MergeEdgeAssertionsWorker(comp, num, ValueNumStore::NoVN, assertions, &result, /* canUseCheckedBounds */ false, edgeAssertionsBudget, visited); - assert(result.IsConstantRange()); + if (!result.IsConstantRange()) + { + assert(varTypeIsLong(vnType)); + return Limit(Limit::keUnknown); + } return result; } @@ -1546,7 +1593,8 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp } budget--; - Range otherRange = GetRangeFromAssertionsWorker(comp, otherVN, assertions, budget, visited); + var_types otherTyp = comp->vnStore->TypeOfVN(otherVN); + Range otherRange = GetRangeFromAssertionsWorker(comp, otherTyp, otherVN, assertions, budget, visited); if (!otherRange.IsConstantRange()) { continue; diff --git a/src/coreclr/jit/rangecheck.h b/src/coreclr/jit/rangecheck.h index c9f189deeb73de..2203dbae585a24 100644 --- a/src/coreclr/jit/rangecheck.h +++ b/src/coreclr/jit/rangecheck.h @@ -776,7 +776,8 @@ class RangeCheck bool TryGetRange(BasicBlock* block, GenTree* expr, Range* pRange); // Cheaper version of TryGetRange that is based only on incoming assertions. - static Range GetRangeFromAssertions(Compiler* comp, ValueNum num, ASSERT_VALARG_TP assertions, int budget = 10); + static Range GetRangeFromAssertions( + Compiler* comp, var_types type, ValueNum num, ASSERT_VALARG_TP assertions, int budget = 10); // Compute the range from the given type static Range GetRangeFromType(var_types type); @@ -788,6 +789,7 @@ class RangeCheck // Cheaper version of TryGetRange that is based only on incoming assertions. static Range GetRangeFromAssertionsWorker(Compiler* comp, + var_types type, ValueNum num, ASSERT_VALARG_TP assertions, int budget, From 1a2b5b16b26e77d42ace8195a5bacdeb85d468bc Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Wed, 3 Jun 2026 08:17:21 -0700 Subject: [PATCH 2/3] Fix merge conflict --- 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 7f33b0dcfe37c4..78a9d4ee16f4a5 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -2411,7 +2411,7 @@ Range RangeCheck::ComputeRange(BasicBlock* block, GenTree* expr, bool monIncreas else if (genActualType(expr) == TYP_INT) { // Use GetRangeFromAssertions for everything else since they won't produce dependent or symbolic ranges - range = GetRangeFromAssertions(m_compiler, vn, block->bbAssertionIn); + range = GetRangeFromAssertions(m_compiler, expr->TypeGet(), vn, block->bbAssertionIn); } else { From 5de3aa43d95d139bb5158eb4c253a816e3158082 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Wed, 3 Jun 2026 11:54:18 -0700 Subject: [PATCH 3/3] Allow GetRangeFromAssertions to process TYP_LONG casts --- src/coreclr/jit/rangecheck.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 78a9d4ee16f4a5..7807f2f85d439d 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -735,12 +735,6 @@ Range RangeCheck::GetRangeFromAssertionsWorker(Compiler* } } - if (genActualType(vnType) != TYP_INT) - { - // TODO: We should handle TYP_LONG for anything that FitsIn - return Limit(Limit::keUnknown); - } - VNFuncApp funcApp; if (comp->vnStore->GetVNFunc(num, &funcApp)) {