Update GetRangeFromAssertions to handle some basic TYP_LONG scenarios where it FitsIn<int32_t>#128906
Update GetRangeFromAssertions to handle some basic TYP_LONG scenarios where it FitsIn<int32_t>#128906tannergooding wants to merge 3 commits into
Conversation
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
This PR broadens assertion-based range derivation in the JIT so it can reason about some TYP_LONG value numbers when the resulting values are known to fit in int32, and wires the updated API through rangecheck and assertion propagation to enable additional folding / bounds-check reasoning.
Changes:
- Update
ValueNumStore::IsVNIntegralConstantto coerce constants asint64_t, allowingTYP_LONGconstants that fit to be recognized asint32constants. - Extend
RangeCheck::GetRangeFromAssertions/worker to accept an explicitvar_typesand add limited handling forTYP_LONGscenarios (notablyRSZ/RSHshift cases and other VN ops). - Update assertion propagation and range analysis callsites to pass the expression type and tolerate unknown ranges where
TYP_LONGcan’t be represented as anint32-basedRange.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/coreclr/jit/valuenum.h | Enables integral-constant extraction from TYP_LONG VNs via int64_t coercion. |
| src/coreclr/jit/rangecheck.h | Updates GetRangeFromAssertions signature and adds Range::IsUnknown() helper. |
| src/coreclr/jit/rangecheck.cpp | Implements the new typed assertion-range logic and extends range computation to consult it in more cases. |
| src/coreclr/jit/assertionprop.cpp | Adapts assertion-prop folding to the new API and to possibly-unknown ranges for wider types. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/coreclr/jit/rangecheck.cpp:796
- In VNF_Cast handling, when
resultis non-constant (e.g., casting to/from types that Range can’t represent) the code unconditionally propagatescastOpRangeif it’s a constant range. This is unsound for sign-changing casts (e.g.,int -> uint,uint -> long) when the operand range can include negatives: the cast changes negative values to large positives, butcastOpRangewould still contain negatives and exclude the large values.
This can cause incorrect tightening and downstream folding/removal based on a range that doesn’t describe the cast result.
// 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 (varTypeIsIntegral(arg0Typ))
{
Range castOpRange =
GetRangeFromAssertionsWorker(comp, arg0Typ, arg0VN, assertions, --budget, visited);
if (castOpRange.IsConstantRange())
{
if (!result.IsConstantRange())
{
result = castOpRange;
}
else if ((castOpRange.LowerLimit().GetConstant() >= result.LowerLimit().GetConstant()) &&
(castOpRange.UpperLimit().GetConstant() <= result.UpperLimit().GetConstant()))
{
result = castOpRange;
}
}
64aae0c to
2e4ba16
Compare
… where it FitsIn<int32_t>
2e4ba16 to
f72c56d
Compare
…ll GetRangeFromAssertions
|
/azp run fuzzlyn, runtime-coreclr jitstress, runtime-coreclr jitstressregs |
|
Azure Pipelines successfully started running 3 pipeline(s). |
|
Diffs are here. Linux Arm64Linux x64Windows Arm64Windows x64Linux armWindows x86Linux x64Windows arm64Windows x64 |
|
Overall the diffs are teh standard set you'd expect. We have places that change from This lights up for places where we're explicitly using |
| } | ||
| else if ((elementCount == 32) && varTypeIsLong(rangeType)) | ||
| { | ||
| return {SymbolicIntegerValue::Zero, UpperBoundForType(TYP_UINT)}; |
There was a problem hiding this comment.
why not just rangeType = TYP_UINT like above?
There was a problem hiding this comment.
Because LowerBoundForType doesn't handle TYP_UINT, only UpperBoundForType does, and so ForType would hit an unreached.
I believe this is intentional and to avoid bugs since we shouldn't normally encounter TYP_UINT for anything except rare special scenarios like this
| *isKnownNonNegative = true; | ||
| } | ||
| if ((rng.LowerLimit().GetConstant() > 0) || (rng.UpperLimit().GetConstant() < 0)) | ||
| Range rng = RangeCheck::GetRangeFromAssertions(this, tree->TypeGet(), treeVN, assertions); |
There was a problem hiding this comment.
I really don't like the fact we need to pass type. It should be evaluated from VN, shouldn't it?
There was a problem hiding this comment.
The issue is the initial if ((num == ValueNumStore::NoVN) || (budget <= 0)) check in GetRangeFromAssertions.
i.e, handling producing a range if no VN exists, which previously relied on the fact we would only ever have TYP_INT, but now we can have TYP_LONG as well.
We'd have to have no VN produce keUnknown and for the callers to assume a constant range based on the type in that case instead, which is additional churn either way. I don't have a strong preference here on either approach, and went with passing the type through to avoid regressing the status quo.
There was a problem hiding this comment.
so can we return just keUnknown if no VN? since this function already may return keUnknown
There was a problem hiding this comment.
We could, but then that will regress TYP_INT scenarios with no VN where we previously would've gotten an appropriate [INT32_MIN, INT32_MAX] constant range.
That might be fine, since most things should have VNs, but it might also not be since we lose VN info in various places or may not have it for new nodes.
I could restrict it to just GetRangeFromAssertions, have it take the tree, extract the VN, and then call GetRangeFromAssertionsWorker which doesn't further propagate the type since it must be from a VN at that point, but I think that's the "best" we can do since we need a range for two possible types now.
| if ((rng.LowerLimit().GetConstant() > 0) || (rng.UpperLimit().GetConstant() < 0)) | ||
| Range rng = RangeCheck::GetRangeFromAssertions(this, tree->TypeGet(), treeVN, assertions); | ||
|
|
||
| if (rng.IsConstantRange()) |
There was a problem hiding this comment.
it's unfortunate we broke the contract for GetRangeFromAssertions to always return a constant range. My opinion we shouldn't do it and instead either upgrade Range to TYP_LONG or introduce a new GetRangeFromAssertions for 64-bit ranges
There was a problem hiding this comment.
or introduce a new GetRangeFromAssertions for 64-bit ranges
This is a lot of unnecessary code duplication and keeping the same overall handling regardless. That is, it doesn't change what assertionprop has to handle here, rather it just forces it to two paths one calling GetRangeFromAssertions32 and one calling GetRangeFromAssertions64 and still handling the fact that one path may not produce a constant range. It saves nothing and just makes things more complex.
With this approach, we have a single method handling both and the caller just has to handle the fact it might not be constant, rather than having to own dispatch to the right method and still handle that nuance anyways.
instead either upgrade Range to TYP_LONG
This is the eventual goal, but I'm trying to get this done incrementally and in a way that is easier to test, review, and handle.
Fully handling TYP_LONG is quite a bit more complex than only extending it to handle places where FitsIn<int32_t> remains "trivially true".
Namely, it involves extending Range/Limit to track int64_t cns, to then have all RangeOperations support int64_t, to have additional range ops that handle the 32 vs 64-bit limits, to ensure we understand ADD(int, x, y) and ADD(long, x, y) have different overflow limits to check and track for example, and to have the various handlers consider those nuances as well.
| } | ||
| else | ||
| { | ||
| // TODO: We could return `0, keUnknown` for `elementCount == 32` if the result is TYP_LONG |
There was a problem hiding this comment.
I still don't understand what 0, keUnknown is supposed to mean, keUnknown implies it can be something that overflows making the range invalid
There was a problem hiding this comment.
The point here is more conceptual and using it as a sentinel because we don't just use it for overflow, but just generally as a "limit cannot be determined/represented". It could well just be something like say 0, keMaxValue instead.
The general point, however, is that while Range is limited to int32_t, it may still be beneficial to propagate up that we know the lower bound and simply cannot represent the upper bound, thus a given TYP_LONG is known to be "never negative" and all the usual "known unsigned value" optimizations can kick in.
There was a problem hiding this comment.
Well, this problem won't exist if we we make Range 64bit (or Range)
There was a problem hiding this comment.
Right, which is an eventual goal. I'm going to try to do it after this PR lands even, its just a much more involved change and has potentially reaching arms into many other places in the JIT where we're limiting checks to just genActualType() == TYP_INT
| if (varTypeIsGC(vnType)) | ||
| { | ||
| #if TARGET_64BIT | ||
| return Limit(Limit::keUnknown); |
There was a problem hiding this comment.
I suspect it's fine to always give up on gc types unconditionally
There was a problem hiding this comment.
We are always giving up unconditionally? The difference is just preserving the status quo where it returned a constant range for GC types on 32-bit.
There was a problem hiding this comment.
I don't think it's useful, just some weird scenario where a TYP_INT PHI had a byref PHI_ARG on 32bit, I doubt we can ever deduce anything from that anyway, and it's 32bit
There was a problem hiding this comment.
Right, but the same consideration will exist when we eventually extend this to full 64-bit. So we basically want to make a decision of "return the full range in that scenario, if we can" or "always return keUnknown". I deferred to maintaing the status quo since that's the less risky change.
…t is a constant range before use
| // We're going from a small type to a large type | ||
| // and so regardless of whether we zero or sign-extend | ||
| // the value is preserved within the confines of its | ||
| // original input for the destination, i.e. it always | ||
| // passes the FitsIn<fromType> check. | ||
|
|
| 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); |
All of the APIs that can overflow ( But then for
|

This is an alternative to #128676. It needs confirmation that the diffs/TP is acceptable and may require a few iterations or pulling back prior to it being ready for review.