diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 979b0e295e0081..8a3c9857529b0f 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -3572,30 +3572,6 @@ void Compiler::optAssertionProp_RangeProperties(ASSERT_VALARG_TP assertions, { const AssertionDsc& curAssertion = optGetAssertion(GetAssertionIndex(index)); - // if treeVN has a bound-check assertion where it's an index, then - // it means it's not negative, example: - // - // array[idx] = 42; // creates 'BoundsCheckNoThrow' assertion - // return idx % 8; // idx is known to be never negative here, hence, MOD->UMOD - // - if (curAssertion.IsBoundsCheckNoThrow() && (curAssertion.GetOp1().GetVN() == treeVN)) - { - *isKnownNonNegative = true; - continue; - } - - // Same for Length, example: - // - // array[idx] = 42; - // array.Length is known to be non-negative and non-zero here - // - if (curAssertion.IsBoundsCheckNoThrow() && (curAssertion.GetOp2().GetCheckedBound() == treeVN)) - { - *isKnownNonNegative = true; - *isKnownNonZero = true; - return; // both properties are known, no need to check other assertions - } - // First, analyze possible X ==/!= CNS assertions. if (curAssertion.IsConstantInt32Assertion() && (curAssertion.GetOp1().GetVN() == treeVN)) { @@ -3637,14 +3613,15 @@ void Compiler::optAssertionProp_RangeProperties(ASSERT_VALARG_TP assertions, } // Let's see if MergeEdgeAssertions can help us: - if (tree->TypeIs(TYP_INT)) + if (genActualType(tree) == TYP_INT) { Range rng = RangeCheck::GetRangeFromAssertions(this, treeVN, assertions); + assert(rng.IsConstantRange()); if (rng.LowerLimit().GetConstant() >= 0) { *isKnownNonNegative = true; } - if (rng.LowerLimit().GetConstant() > 0) + if ((rng.LowerLimit().GetConstant() > 0) || (rng.UpperLimit().GetConstant() < 0)) { *isKnownNonZero = true; } @@ -4430,21 +4407,55 @@ GenTree* Compiler::optAssertionProp_Cast(ASSERT_VALARG_TP assertions, } } - // If we don't have a cast of a LCL_VAR then bail. - if (!lcl->OperIs(GT_LCL_VAR)) + bool removeCast = false; + if (!optLocalAssertionProp) { - return nullptr; - } + // Get the non-overflowing input range for a cast. ForCastInput takes care of special cases like + // small types and IsUnsigned flag for checked casts. + IntegralRange castRng = IntegralRange::ForCastInput(cast); + int64_t castLo = IntegralRange::SymbolicToRealValue(castRng.GetLowerBound()); + int64_t castHi = IntegralRange::SymbolicToRealValue(castRng.GetUpperBound()); + 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)) + { + ValueNum castOpVN = optConservativeNormalVN(cast->CastOp()); + Range castOpRng = RangeCheck::GetRangeFromAssertions(this, castOpVN, assertions); + assert(castOpRng.IsConstantRange()); - if (!optLocalAssertionProp) + int castFromLo = castOpRng.LowerLimit().GetConstant(); + int castFromHi = castOpRng.UpperLimit().GetConstant(); + int castToLo = castToTypeRange.LowerLimit().GetConstant(); + int castToHi = castToTypeRange.UpperLimit().GetConstant(); + + if (castOpRng.IsConstantRange() && (castFromLo >= castToLo) && (castFromHi <= castToHi)) + { + removeCast = true; + if (!lcl->OperIs(GT_LCL_VAR)) + { + // We cannot remove the cast, but can we just remove the GTF_OVERFLOW flag? + if (!cast->gtOverflow()) + { + return nullptr; + } + + // Just clear the overflow flag then. + JITDUMP("Clearing overflow flag for cast %06u based on assertions.\n", dspTreeID(cast)); + cast->ClearOverflow(); + return optAssertionProp_Update(cast, cast, stmt); + } + } + } + } + } + else { - // optAssertionIsSubrange is only for local assertion prop. - return nullptr; + removeCast = lcl->OperIs(GT_LCL_VAR) && + optAssertionIsSubrange(lcl, IntegralRange::ForCastInput(cast), assertions) != NO_ASSERTION_INDEX; } - IntegralRange range = IntegralRange::ForCastInput(cast); - AssertionIndex index = optAssertionIsSubrange(lcl, range, assertions); - if (index != NO_ASSERTION_INDEX) + if (removeCast) { LclVarDsc* varDsc = lvaGetDesc(lcl->AsLclVarCommon()); @@ -4456,13 +4467,8 @@ GenTree* Compiler::optAssertionProp_Cast(ASSERT_VALARG_TP assertions, { return nullptr; } -#ifdef DEBUG - if (verbose) - { - printf("\nSubrange prop for index #%02u in " FMT_BB ":\n", index, compCurBB->bbNum); - DISPNODE(cast); - } -#endif + + JITDUMP("Clearing overflow flag for cast %06u based on assertions.\n", dspTreeID(cast)); cast->ClearOverflow(); return optAssertionProp_Update(cast, cast, stmt); } @@ -4481,13 +4487,7 @@ GenTree* Compiler::optAssertionProp_Cast(ASSERT_VALARG_TP assertions, op1->ChangeType(varDsc->TypeGet()); } -#ifdef DEBUG - if (verbose) - { - printf("\nSubrange prop for index #%02u in " FMT_BB ":\n", index, compCurBB->bbNum); - DISPNODE(cast); - } -#endif + JITDUMP("Removing cast %06u as redundant based on assertions.\n", dspTreeID(cast)); return optAssertionProp_Update(op1, cast, stmt); } @@ -5090,6 +5090,23 @@ GenTree* Compiler::optAssertionProp_BndsChk(ASSERT_VALARG_TP assertions, GenTree } } + // Let's see if we can remove the bounds check based on the ranges. + 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); + if (idxRng.IsConstantRange() && lenRng.IsConstantRange()) + { + // idx.lo >= 0 && idx.hi < len.lo --> drop bounds check + if (idxRng.LowerLimit().GetConstant() >= 0 && + idxRng.UpperLimit().GetConstant() < lenRng.LowerLimit().GetConstant()) + { + return dropBoundsCheck(INDEBUG("upper bound of index is less than lower bound of length")); + } + } + } + return nullptr; } diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 459a2f80709534..994378cb151af5 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -1056,18 +1056,28 @@ void RangeCheck::MergeEdgeAssertions(Compiler* comp, } } } - else if ((normalLclVN == lenVN) && comp->vnStore->IsVNInt32Constant(indexVN)) + else if (normalLclVN == lenVN) { - // We have "Const < arr.Length" assertion, it means that "arr.Length > Const" - int indexCns = comp->vnStore->GetConstantInt32(indexVN); - if (indexCns >= 0) + if (comp->vnStore->IsVNInt32Constant(indexVN)) { - cmpOper = GT_GT; - limit = Limit(Limit::keConstant, indexCns); + // We have "Const < arr.Length" assertion, it means that "arr.Length > Const" + int indexCns = comp->vnStore->GetConstantInt32(indexVN); + if (indexCns >= 0) + { + cmpOper = GT_GT; + limit = Limit(Limit::keConstant, indexCns); + } + else + { + continue; + } } else { - continue; + // 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/coreclr/jit/rangecheck.h b/src/coreclr/jit/rangecheck.h index 835ec0f4eb3a66..d121f7379300a0 100644 --- a/src/coreclr/jit/rangecheck.h +++ b/src/coreclr/jit/rangecheck.h @@ -761,6 +761,9 @@ class RangeCheck // 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); + // Compute the range from the given type + static Range GetRangeFromType(var_types type); + private: typedef JitHashTable, bool> OverflowMap; typedef JitHashTable, Range*> RangeMap; @@ -782,9 +785,6 @@ class RangeCheck // Internal worker for GetRange. Range GetRangeWorker(BasicBlock* block, GenTree* expr, bool monIncreasing DEBUGARG(int indent)); - // Compute the range from the given type - static Range GetRangeFromType(var_types type); - // Given the local variable, first find the definition of the local and find the range of the rhs. // Helper for GetRangeWorker. Range ComputeRangeForLocalDef(BasicBlock* block, GenTreeLclVarCommon* lcl, bool monIncreasing DEBUGARG(int indent)); diff --git a/src/tests/JIT/opt/CheckedContext/CheckedContext.cs b/src/tests/JIT/opt/CheckedContext/CheckedContext.cs index 943d1948351b83..576f56b346517c 100644 --- a/src/tests/JIT/opt/CheckedContext/CheckedContext.cs +++ b/src/tests/JIT/opt/CheckedContext/CheckedContext.cs @@ -561,6 +561,283 @@ static int SubNarrowedMinusConst(int a) return checked(a - 2_000_000_000); } + // ===================================================================== + // CAST — safe (checked context should be removed) + // ===================================================================== + + // --- Signed int → smaller signed types --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastIntToByte_Guarded(int a) + { + if (a < 0 || a > 255) + return 0; + return checked((byte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static sbyte CastIntToSByte_Guarded(int a) + { + if (a < -128 || a > 127) + return 0; + return checked((sbyte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static short CastIntToShort_Guarded(int a) + { + if (a < -32768 || a > 32767) + return 0; + return checked((short)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static ushort CastIntToUShort_Guarded(int a) + { + if (a < 0 || a > 65535) + return 0; + return checked((ushort)a); + } + + // --- Unsigned int → smaller types --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastUIntToByte_Guarded(uint a) + { + if (a > 255) + return 0; + return checked((byte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static ushort CastUIntToUShort_Guarded(uint a) + { + if (a > 65535) + return 0; + return checked((ushort)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static short CastUIntToShort_Guarded(uint a) + { + if (a > 32767) + return 0; + return checked((short)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static sbyte CastUIntToSByte_Guarded(uint a) + { + if (a > 127) + return 0; + return checked((sbyte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static int CastUIntToInt_Guarded(uint a) + { + if (a > (uint)int.MaxValue) + return 0; + return checked((int)a); + } + + // --- Tighter range guards (range well inside target type) --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastIntToByte_TightRange(int a) + { + if (a < 10 || a > 100) + return 0; + return checked((byte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static sbyte CastIntToSByte_PositiveOnly(int a) + { + if (a < 0 || a > 50) + return 0; + return checked((sbyte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static sbyte CastIntToSByte_NegativeRange(int a) + { + if (a < -100 || a > -1) + return 0; + return checked((sbyte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static short CastIntToShort_NarrowRange(int a) + { + if (a < -1000 || a > 1000) + return 0; + return checked((short)a); + } + + // --- Cast from non-LCL_VAR (field load) — overflow flag cleared, cast kept --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastFieldToByte() + { + int v = s_intField; + if (v < 0 || v > 200) + return 0; + return checked((byte)v); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static short CastFieldToShort() + { + int v = s_intField; + if (v < -1000 || v > 1000) + return 0; + return checked((short)v); + } + + // --- Cast with guard using array length --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastArrayLengthToByte(int[] arr) + { + if (arr.Length > 200) + return 0; + return checked((byte)arr.Length); + } + + // --- Signed cast after arithmetic narrowing --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastAfterAdd_Safe(int a, int b) + { + if (a < 0 || a > 100) + return 0; + if (b < 0 || b > 100) + return 0; + int sum = a + b; // [0..200] + return checked((byte)sum); + } + + // --- LE/GE style guards --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastIntToByte_LEGuard(int a) + { + if (a >= 0 && a <= 255) + return checked((byte)a); + return 0; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static sbyte CastIntToSByte_LEGuard(int a) + { + if (a >= -128 && a <= 127) + return checked((sbyte)a); + return 0; + } + + // --- Boundary-exact: value exactly at target type boundary --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastIntToByte_ExactBoundary(int a) + { + if (a < 0 || a > 255) + return 0; + return checked((byte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static short CastIntToShort_ExactBoundary(int a) + { + if (a < short.MinValue || a > short.MaxValue) + return 0; + return checked((short)a); + } + + // ===================================================================== + // CAST — must overflow (checked must NOT be removed) + // ===================================================================== + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastIntToByte_NoGuard(int a) + { + return checked((byte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static sbyte CastIntToSByte_NoGuard(int a) + { + return checked((sbyte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static short CastIntToShort_NoGuard(int a) + { + return checked((short)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static ushort CastIntToUShort_NoGuard(int a) + { + return checked((ushort)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastUIntToByte_NoGuard(uint a) + { + return checked((byte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static int CastUIntToInt_NoGuard(uint a) + { + return checked((int)a); + } + + // --- Guard too loose — range exceeds target --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastIntToByte_GuardTooLoose(int a) + { + if (a < 0 || a > 300) + return 0; + return checked((byte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static sbyte CastIntToSByte_GuardTooLoose(int a) + { + if (a < -200 || a > 200) + return 0; + return checked((sbyte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static short CastIntToShort_GuardTooLoose(int a) + { + if (a < -50000 || a > 50000) + return 0; + return checked((short)a); + } + + // --- Negative value cast to unsigned --- + + [MethodImpl(MethodImplOptions.NoInlining)] + static byte CastNegativeIntToByte(int a) + { + if (a < -10 || a > 10) + return 0; + return checked((byte)a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static ushort CastNegativeIntToUShort(int a) + { + if (a < -100 || a > 100) + return 0; + return checked((ushort)a); + } + // ===================================================================== // XUnit entry point // ===================================================================== @@ -814,6 +1091,144 @@ public static int TestEntryPoint() if (SubArrayLengths(arr200, arr10) != 190) return 0; + // ----- CAST safe cases ----- + + if (CastIntToByte_Guarded(OpaqueVal(200)) != 200) + return 0; + + if (CastIntToSByte_Guarded(OpaqueVal(-50)) != -50) + return 0; + + if (CastIntToShort_Guarded(OpaqueVal(-10000)) != -10000) + return 0; + + if (CastIntToUShort_Guarded(OpaqueVal(50000)) != 50000) + return 0; + + if (CastUIntToByte_Guarded(OpaqueVal((uint)100)) != 100) + return 0; + + if (CastUIntToUShort_Guarded(OpaqueVal((uint)40000)) != 40000) + return 0; + + if (CastUIntToShort_Guarded(OpaqueVal((uint)30000)) != 30000) + return 0; + + if (CastUIntToSByte_Guarded(OpaqueVal((uint)100)) != 100) + return 0; + + if (CastUIntToInt_Guarded(OpaqueVal((uint)1_000_000)) != 1_000_000) + return 0; + + if (CastIntToByte_TightRange(OpaqueVal(50)) != 50) + return 0; + + if (CastIntToSByte_PositiveOnly(OpaqueVal(25)) != 25) + return 0; + + if (CastIntToSByte_NegativeRange(OpaqueVal(-50)) != -50) + return 0; + + if (CastIntToShort_NarrowRange(OpaqueVal(-500)) != -500) + return 0; + + s_intField = OpaqueVal(150); + if (CastFieldToByte() != 150) + return 0; + + s_intField = OpaqueVal(-500); + if (CastFieldToShort() != -500) + return 0; + + int[] arr150 = new int[OpaqueVal(150)]; + if (CastArrayLengthToByte(arr150) != 150) + return 0; + + if (CastAfterAdd_Safe(OpaqueVal(80), OpaqueVal(80)) != 160) + return 0; + + if (CastIntToByte_LEGuard(OpaqueVal(128)) != 128) + return 0; + + if (CastIntToSByte_LEGuard(OpaqueVal(-100)) != -100) + return 0; + + if (CastIntToByte_ExactBoundary(OpaqueVal(255)) != 255) + return 0; + + if (CastIntToShort_ExactBoundary(OpaqueVal(short.MaxValue)) != short.MaxValue) + return 0; + + // ----- CAST overflow cases ----- + + threw = false; + try { CastIntToByte_NoGuard(OpaqueVal(256)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + + threw = false; + try { CastIntToSByte_NoGuard(OpaqueVal(128)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + + threw = false; + try { CastIntToShort_NoGuard(OpaqueVal(40000)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + + threw = false; + try { CastIntToUShort_NoGuard(OpaqueVal(-1)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + + threw = false; + try { CastUIntToByte_NoGuard(OpaqueVal((uint)300)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + + threw = false; + try { CastUIntToInt_NoGuard(OpaqueVal(uint.MaxValue)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + + // guard too loose — value in guard range but overflows cast + threw = false; + try { CastIntToByte_GuardTooLoose(OpaqueVal(260)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + + threw = false; + try { CastIntToSByte_GuardTooLoose(OpaqueVal(130)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + + threw = false; + try { CastIntToShort_GuardTooLoose(OpaqueVal(40000)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + + // negative value to unsigned — must throw + threw = false; + try { CastNegativeIntToByte(OpaqueVal(-5)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + + threw = false; + try { CastNegativeIntToUShort(OpaqueVal(-50)); } + catch (OverflowException) { threw = true; } + if (!threw) + return 0; + return 100; } }