Skip to content

JIT: fix FitsIn<int32_t> assert in BitOperations.Rotate{Left,Right} const-fold#129136

Open
AndyAyersMS wants to merge 2 commits into
dotnet:mainfrom
AndyAyersMS:fix-129099-rotate-fold
Open

JIT: fix FitsIn<int32_t> assert in BitOperations.Rotate{Left,Right} const-fold#129136
AndyAyersMS wants to merge 2 commits into
dotnet:mainfrom
AndyAyersMS:fix-129099-rotate-fold

Conversation

@AndyAyersMS

Copy link
Copy Markdown
Member

Note

PR description is AI-generated (GitHub Copilot CLI). The investigation, fix, and regression test are checked by me.

Fixes #129099.

Root cause

NI_PRIMITIVE_RotateLeft / NI_PRIMITIVE_RotateRight's constant-folding path (importercalls.cpp) passes the unsigned fold result directly to gtNewIconNode(ssize_t, TYP_INT):

uint32_t cns1 = static_cast<uint32_t>(op1->AsIntConCommon()->IconValue());
result        = gtNewIconNode(BitOperations::RotateLeft(cns1, cns2), baseType);

For TYP_INT/TYP_UINT operands with the high bit set — e.g. BitOperations.RotateRight(0xFFFFFFFFu, k) which folds to 0xFFFFFFFF — the implicit uint32_tssize_t conversion zero-extends to a positive value (4294967295) that does not fit in int32_t. GenTreeIntCon::SetIconValue then trips its FitsIn<int32_t>(value) assert during Morph - Global.

The ulong overload is unaffected because it uses gtNewLconNode (64-bit).

Fix

Cast through int32_t first so the sign bit is preserved when widened to ssize_t:

result = gtNewIconNode(
    static_cast<int32_t>(BitOperations::RotateLeft(cns1, cns2)), baseType);

RotateLeft(0xFFFFFFFFu, 1) is still 0xFFFFFFFFu; reinterpreting that as int32_t gives -1; widening -1 to ssize_t gives -1; FitsIn<int32_t>(-1) is true. Downstream consumers reading the IconValue as a uint32_t (via static_cast<uint32_t>(IconValue)) recover 0xFFFFFFFF correctly.

Regression test

src/tests/JIT/Regression/JitBlue/Runtime_129099/Runtime_129099.cs exercises both RotateLeft and RotateRight on 0xFFFFFFFFu, 0x80000000u (high-bit-only), and int.RotateRight(-1, _) (signed-int overload, sanity). Each call stores into a volatile static so the fold result must be materialized — without this the JIT can dead-code-eliminate the call before the assert fires.

Wired into src/tests/JIT/Regression/Regression_ro_2.csproj.

Verified locally on osx-arm64.Checked:

  • Before fix: Assert failure ... 'FitsIn<int32_t>(value)' ... 'Runtime_129099:FoldRotateRightUInt():uint' during 'Morph - Global'
  • After fix: all four cases pass.

A ~3,000-trial ReifyCs sweep that previously found 7 instances of this assert now finds 0.

…onst-fold

NI_PRIMITIVE_RotateLeft/Right's constant-folding path passes the unsigned
fold result directly to gtNewIconNode(ssize_t, TYP_INT). For TYP_INT/TYP_UINT
operands with the high bit set (e.g. RotateRight(0xFFFFFFFFu, k) which folds
to 0xFFFFFFFF), the implicit uint32_t-to-ssize_t conversion zero-extends to
a positive value (4294967295) that does not fit in int32_t, tripping
GenTreeIntCon::SetIconValue's FitsIn<int32_t>(value) assert during
'Morph - Global'.

Cast through int32_t first so the sign bit is preserved when widened to
ssize_t. Fixes dotnet#129099.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 8, 2026 20:34
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jun 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a JIT constant-folding bug for NI_PRIMITIVE_RotateLeft / NI_PRIMITIVE_RotateRight where 32-bit rotate results with the high bit set could be represented as a zero-extended ssize_t, ultimately tripping int32-range assertions during later IR mutation.

Changes:

  • Adjust rotate constant-folding to cast the folded 32-bit result through int32_t before creating the GT_CNS_INT node.
  • Add a new JitBlue regression test covering RotateLeft/RotateRight folding for high-bit-set inputs and wire it into the regression project.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/coreclr/jit/importercalls.cpp Casts 32-bit rotate fold results through int32_t to preserve sign when widened and avoid later int32-range asserts.
src/tests/JIT/Regression/JitBlue/Runtime_129099/Runtime_129099.cs Adds a regression test that forces materialization of folded rotate results (via volatile sinks) and validates expected outputs.
src/tests/JIT/Regression/Regression_ro_2.csproj Includes the new Runtime_129099 regression test in the JIT regression build.

Comment thread src/coreclr/jit/importercalls.cpp Outdated
Comment on lines +6411 to +6415
// Sign-extend the unsigned fold result to int32_t so gtNewIconNode's
// FitsIn<int32_t>(value) check for TYP_INT/TYP_UINT folds doesn't trip
// on the high-bit-set case (e.g. RotateLeft(0xFFFFFFFFu, k)).
result = gtNewIconNode(
static_cast<int32_t>(BitOperations::RotateLeft(cns1, cns2)), baseType);
Comment thread src/coreclr/jit/importercalls.cpp Outdated
Comment on lines +6464 to +6468
// Sign-extend the unsigned fold result to int32_t so gtNewIconNode's
// FitsIn<int32_t>(value) check for TYP_INT/TYP_UINT folds doesn't trip
// on the high-bit-set case (e.g. RotateRight(0xFFFFFFFFu, k)).
result = gtNewIconNode(
static_cast<int32_t>(BitOperations::RotateRight(cns1, cns2)), baseType);
Comment on lines +4 to +6
// NI_PRIMITIVE_RotateLeft/Right's const-fold path stored the unsigned
// fold result into gtNewIconNode(ssize_t, TYP_INT). For uint operands
// with the high bit set (e.g. RotateRight(0xFFFFFFFFu, k)) the result
Comment on lines +6414 to +6415
result = gtNewIconNode(
static_cast<int32_t>(BitOperations::RotateLeft(cns1, cns2)), baseType);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the best/correct way to do it?

That is, this seems like a general issue with gtNewIconNode(ssize_t) since we default to TYP_INT and so that scenario should really have an assert(FitsIn<int32_t>(ssize_t)) or insert the static_cast<int32_t>(value) itself, since anything else is just "incorrect IR"

Anything that doesn't fit rather should be TYP_LONG and should've gone through gtNewLconNode instead (or possibly TYP_BYREF on 64-bit for the few cases that have it).

I wonder if even the general signature of gtNewIconNode is "incorrect" and if it rather should be int32_t instead, to help enforce correctness here; particularly since any larger value may need to be LconNode to work on 32-bit.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Reply is AI-generated (GitHub Copilot CLI).

Agreed — the call-site cast is a workaround for a missing API contract. I've pushed an update that also adds the invariant at gtNewIconNode itself:

GenTreeIntCon* Compiler::gtNewIconNode(ssize_t value, var_types type)
{
    assert(genActualType(type) == type);
    assert(genTypeSize(type) > genTypeSize(TYP_INT) || FitsIn<int32_t>(value));
    return new (this, GT_CNS_INT) GenTreeIntCon(type, value);
}

This catches the bug class at construction time rather than per-call-site. Smoke-verified locally:

  • All built Regression_ro_* JIT regression tests still pass
  • A 120-trial ReifyCs sweep produces no firings of the new assert
  • The fix-129099 regression test continues to fail without the call-site fix (the assert at gtNewIconNode would also fire) and pass with it

On the deeper signature question (int32_t vs ssize_t): a much larger refactor that touches 455 call sites — many of which legitimately want ssize_t for TYP_I_IMPL/TYP_BYREF/TYP_LONG use. The assert above gives us the safety net without the API churn. I'd suggest filing a separate issue for the signature change if you want to pursue it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I don't know if int32_t is the right direction but will dig into it some.

There's something unnecessarily clunky about the icon nodes in general. Not sure I want to revisit that right now though.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... AI is getting ahead of itself here and just replying on its own. Let me reign it in a bit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, a bit unsure as to the direction myself, but I think assert is a good starting point and will help catch any other issues longer term.

- Comments now correctly attribute the FitsIn<int32_t> assert to the
  downstream SetIconValue/BashToConst call (the construction itself is
  unchecked). Updated both the call-site comments and the regression
  test's header.
- Add the same FitsIn<int32_t> assert to gtNewIconNode itself (per
  @tannergooding) so this invariant is enforced at the IR-construction
  boundary, not just at the rotate call site. Anything larger than int32
  should go through gtNewLconNode (TYP_LONG) or use TYP_I_IMPL on 64-bit.
  Verified: all built Regression_ro_*.dll tests still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JIT: assertion 'FitsIn<int32_t>(value)' on constant-folded BitOperations.RotateRight(0xFFFFFFFFu, k) with multiple uses

3 participants