Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -5366,6 +5366,7 @@ class Compiler
CORINFO_SIG_INFO* sig
R2RARG(CORINFO_CONST_LOOKUP* entryPoint),
bool mustExpand);
GenTree* impRotateHelper(var_types baseType, genTreeOps rotateOper);

#ifdef FEATURE_HW_INTRINSICS
bool IsValidForShuffle(GenTree* indices,
Expand Down
163 changes: 81 additions & 82 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6048,6 +6048,85 @@ GenTree* Compiler::impSRCSUnsafeIntrinsic(NamedIntrinsic intrinsic,
}
}

//------------------------------------------------------------------------
// impRotateHelper: import a NI_PRIMITIVE_RotateLeft or
// NI_PRIMITIVE_RotateRight intrinsic.
//
// Arguments:
// baseType - the type being rotated (TYP_INT or TYP_LONG)
// rotateOper - GT_ROL for RotateLeft, GT_ROR for RotateRight
//
// Returns:
// IR tree to use in place of the call, or nullptr if the jit should treat
// the intrinsic call like a normal call.
//
GenTree* Compiler::impRotateHelper(var_types baseType, genTreeOps rotateOper)
{
assert((rotateOper == GT_ROL) || (rotateOper == GT_ROR));

GenTree* op2 = impStackTop().val;

unsigned rotateMask = varTypeIsLong(baseType) ? 0x3F : 0x1F;

if (!op2->IsIntegralConst())
{
#ifndef TARGET_64BIT
Comment thread
dhartglassMSFT marked this conversation as resolved.
Outdated
if (varTypeIsLong(baseType))
{
// TODO-X86-CQ: variable-sized long rotates need special handling on 32-bit.
return nullptr;
}
#endif // !TARGET_64BIT

// The rotate amount is not a constant. Import ROL/ROR(op1, AND(op2, mask)) directly
// instead of bailing to the managed fallback. The native rotate instructions mask
// the amount implicitly on the platforms we care about; the explicit AND keeps the
// IR in the masked form the rest of the JIT expects and is stripped again in
// lowering where possible.
impPopStack();
GenTree* rotateValue = impPopStack().val;
GenTree* rotateAmount = gtNewOperNode(GT_AND, genActualType(op2), op2,
gtNewIconNode(rotateMask, genActualType(op2)));
return gtNewOperNode(rotateOper, baseType, rotateValue, rotateAmount);
}

// Pop the value from the stack
impPopStack();

GenTree* op1 = impPopStack().val;
uint32_t cns2 = static_cast<uint32_t>(op2->AsIntConCommon()->IconValue());

// Mask the offset to ensure deterministic xplat behavior for overshifting
cns2 &= rotateMask;

if (cns2 == 0)
{
// No rotation is a nop
return op1;
}

if (op1->IsIntegralConst())
{
if (varTypeIsLong(baseType))
{
uint64_t cns1 = static_cast<uint64_t>(op1->AsIntConCommon()->LngValue());
uint64_t res =
(rotateOper == GT_ROL) ? BitOperations::RotateLeft(cns1, cns2) : BitOperations::RotateRight(cns1, cns2);
return gtNewLconNode(res);
}
else
{
uint32_t cns1 = static_cast<uint32_t>(op1->AsIntConCommon()->IconValue());
uint32_t res =
(rotateOper == GT_ROL) ? BitOperations::RotateLeft(cns1, cns2) : BitOperations::RotateRight(cns1, cns2);
return gtNewIconNode(res, baseType);
}
}

op2->AsIntConCommon()->SetIconValue(cns2);
return gtFoldExpr(gtNewOperNode(rotateOper, baseType, op1, op2));
}

//------------------------------------------------------------------------
// impPrimitiveNamedIntrinsic: import a NamedIntrinsic representing a primitive operation
//
Expand Down Expand Up @@ -6605,47 +6684,7 @@ GenTree* Compiler::impPrimitiveNamedIntrinsic(NamedIntrinsic intrinsic,
assert(sig->numArgs == 2);
assert(!varTypeIsSmall(retType) && !varTypeIsSmall(baseType));

GenTree* op2 = impStackTop().val;

if (!op2->IsIntegralConst())
{
// TODO-CQ: ROL currently expects op2 to be a constant
break;
}

// Pop the value from the stack
impPopStack();

GenTree* op1 = impPopStack().val;
uint32_t cns2 = static_cast<uint32_t>(op2->AsIntConCommon()->IconValue());

// Mask the offset to ensure deterministic xplat behavior for overshifting
cns2 &= varTypeIsLong(baseType) ? 0x3F : 0x1F;

if (cns2 == 0)
{
// No rotation is a nop
return op1;
}

if (op1->IsIntegralConst())
{
if (varTypeIsLong(baseType))
{
uint64_t cns1 = static_cast<uint64_t>(op1->AsIntConCommon()->LngValue());
result = gtNewLconNode(BitOperations::RotateLeft(cns1, cns2));
}
else
{
uint32_t cns1 = static_cast<uint32_t>(op1->AsIntConCommon()->IconValue());
result = gtNewIconNode(BitOperations::RotateLeft(cns1, cns2), baseType);
}
break;
}

op2->AsIntConCommon()->SetIconValue(cns2);
result = gtFoldExpr(gtNewOperNode(GT_ROL, baseType, op1, op2));

result = impRotateHelper(baseType, GT_ROL);
break;
}

Expand All @@ -6654,47 +6693,7 @@ GenTree* Compiler::impPrimitiveNamedIntrinsic(NamedIntrinsic intrinsic,
assert(sig->numArgs == 2);
assert(!varTypeIsSmall(retType) && !varTypeIsSmall(baseType));

GenTree* op2 = impStackTop().val;

if (!op2->IsIntegralConst())
{
// TODO-CQ: ROR currently expects op2 to be a constant
break;
}

// Pop the value from the stack
impPopStack();

GenTree* op1 = impPopStack().val;
uint32_t cns2 = static_cast<uint32_t>(op2->AsIntConCommon()->IconValue());

// Mask the offset to ensure deterministic xplat behavior for overshifting
cns2 &= varTypeIsLong(baseType) ? 0x3F : 0x1F;

if (cns2 == 0)
{
// No rotation is a nop
return op1;
}

if (op1->IsIntegralConst())
{
if (varTypeIsLong(baseType))
{
uint64_t cns1 = static_cast<uint64_t>(op1->AsIntConCommon()->LngValue());
result = gtNewLconNode(BitOperations::RotateRight(cns1, cns2));
}
else
{
uint32_t cns1 = static_cast<uint32_t>(op1->AsIntConCommon()->IconValue());
result = gtNewIconNode(BitOperations::RotateRight(cns1, cns2), baseType);
}
break;
}

op2->AsIntConCommon()->SetIconValue(cns2);
result = gtFoldExpr(gtNewOperNode(GT_ROR, baseType, op1, op2));

result = impRotateHelper(baseType, GT_ROR);
break;
}

Expand Down
45 changes: 34 additions & 11 deletions src/coreclr/jit/lower.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,12 @@ GenTree* Lowering::LowerNode(GenTree* node)
return next;
}

#if defined(TARGET_XARCH) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64)

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.

Also TARGET_WASM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

sure, fyi this same ifdef appears something like 5 times in this file in (unchanged) places, I assume at some point we'd want to allow WASM through this path at least?

#if defined(TARGET_XARCH) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64)
            LowerShift(node->AsOp());
#else
            ContainCheckShiftRotate(node->AsOp());
#endif

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.

If you'd rather defer fixing the Wasm aspects and open a follow-up issue for Wasm that's fine by me.

@dhartglassMSFT dhartglassMSFT Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll push the WASM changes, if stuff fails though I'll probably back it out and instead do it as a follow up

Would existing test coverage in PR hit that shift path, hypothetically?

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.

Possibly, but there isn't much that gets tested yet.

// These targets mask the rotate amount implicitly, so strip a redundant
// AND(amount, mask) before lowering the rotate.
TryRemoveShiftRotateMask(node->AsOp());
#endif

LowerRotate(node);
break;
}
Expand Down Expand Up @@ -8812,30 +8818,30 @@ bool Lowering::TryFoldBinop(GenTreeOp* node)
}

//------------------------------------------------------------------------
// LowerShift: Lower shift nodes
// TryRemoveShiftRotateMask: Remove a redundant mask on a shift or rotate count.
//
// Arguments:
// shift - the shift node (GT_LSH, GT_RSH or GT_RSZ)
// op - the shift or rotate node (GT_LSH, GT_RSH, GT_RSZ, GT_ROL or GT_ROR)
//
// Notes:
// Remove unnecessary shift count masking, xarch shift instructions
// mask the shift count to 5 bits (or 6 bits for 64 bit operations).
// Some targets' shift/rotate instructions mask their count to bitsize.
// Remove the explicit AND(count, mask) that keeps at least those low bits.
//
void Lowering::LowerShift(GenTreeOp* shift)
void Lowering::TryRemoveShiftRotateMask(GenTreeOp* op)
{
assert(shift->OperIs(GT_LSH, GT_RSH, GT_RSZ));
assert(op->OperIs(GT_LSH, GT_RSH, GT_RSZ, GT_ROL, GT_ROR));

size_t mask = 0x1f;
#ifdef TARGET_64BIT
if (varTypeIsLong(shift->TypeGet()))
if (varTypeIsLong(op->TypeGet()))
Comment thread
dhartglassMSFT marked this conversation as resolved.
{
mask = 0x3f;
}
#else
assert(!varTypeIsLong(shift->TypeGet()));
assert(!varTypeIsLong(op->TypeGet()));
#endif

for (GenTree* andOp = shift->gtGetOp2(); andOp->OperIs(GT_AND); andOp = andOp->gtGetOp1())
for (GenTree* andOp = op->gtGetOp2(); andOp->OperIs(GT_AND); andOp = andOp->gtGetOp1())
{
GenTree* maskOp = andOp->gtGetOp2();

Expand All @@ -8849,12 +8855,29 @@ void Lowering::LowerShift(GenTreeOp* shift)
break;
}

shift->gtOp2 = andOp->gtGetOp1();
op->gtOp2 = andOp->gtGetOp1();
BlockRange().Remove(andOp);
BlockRange().Remove(maskOp);
// The parent was replaced, clear contain and regOpt flag.
shift->gtOp2->ClearContained();
op->gtOp2->ClearContained();
}
}

//------------------------------------------------------------------------
// LowerShift: Lower shift nodes
//
// Arguments:
// shift - the shift node (GT_LSH, GT_RSH or GT_RSZ)
//
// Notes:
// Remove unnecessary shift count masking, xarch shift instructions
// mask the shift count to 5 bits (or 6 bits for 64 bit operations).
//
void Lowering::LowerShift(GenTreeOp* shift)
{
assert(shift->OperIs(GT_LSH, GT_RSH, GT_RSZ));

TryRemoveShiftRotateMask(shift);

ContainCheckShiftRotate(shift);

Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/lower.h
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ class Lowering final : public Phase
GenTree* LowerStoreLoc(GenTreeLclVarCommon* tree);
void LowerRotate(GenTree* tree);
void LowerShift(GenTreeOp* shift);
void TryRemoveShiftRotateMask(GenTreeOp* op);
bool TryFoldBinop(GenTreeOp* node);
#ifdef FEATURE_HW_INTRINSICS
GenTree* LowerHWIntrinsic(GenTreeHWIntrinsic* node);
Expand Down
20 changes: 20 additions & 0 deletions src/coreclr/jit/morph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12534,6 +12534,26 @@ GenTree* Compiler::fgRecognizeAndMorphBitwiseRotation(GenTree* tree)
{
noway_assert(GenTree::OperIsRotate(rotateOp));

// Explicitly mask the rotate amount to the range [0, bitsize-1]. Otherwise, a later
// transform can stick an out of range constant here and trip up lowering. If the
// target's rotate or shift instructions mask their operand implicitly, those targets
Comment thread
dhartglassMSFT marked this conversation as resolved.
// remove this mask again during lowering.
if (rotateIndex->IsCnsIntOrI())
{
ssize_t rotateAmount = rotateIndex->AsIntCon()->IconValue();

if ((rotateAmount < 0) || (rotateAmount > minimalMask))
{
rotateIndex->AsIntCon()->SetIconValue(rotateAmount & minimalMask);
}
}
else
{
rotateIndex =
gtNewOperNode(GT_AND, genActualType(rotateIndex), rotateIndex, gtNewIconNode(minimalMask));
rotateIndex->SetMorphed(this, /* doChildren */ true);
}

GenTreeFlags inputTreeEffects = tree->gtFlags & GTF_ALL_EFFECT;

// We can use the same tree only during global morph; reusing the tree in a later morph
Expand Down
62 changes: 62 additions & 0 deletions src/tests/JIT/Regression/JitBlue/Runtime_129298/Runtime_129298.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// Testcase exposed an ROR node with an out of range operand on arm64,
// asserted in lowering

using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using Xunit;

public class Runtime_129298
{
private static volatile uint Input_p0 = 1;
private static volatile uint Input_p1 = 1;

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
public static uint Fn(uint p0, uint p1)
{
unchecked
{
uint v1, v3, v5, v6, v8, v11, v15, v16, v22, v26, v28;
uint v33 = 0, v40, v44;
uint v21 = 0;
int v2, v4, v7, v9, v10, v29;
v1 = p0 % p1;
v2 = BitOperations.LeadingZeroCount(v1); v3 = (uint)v2;
v4 = BitOperations.LeadingZeroCount(v1); v5 = (uint)v4;
v6 = BitOperations.RotateLeft(p0, 0);
v7 = BitOperations.IsPow2(0x80000000u) ? 1 : 0; v8 = (uint)v7;
v9 = (p1 < p0) ? 1 : 0;
if (v9 == 0)
{
v22 = Math.Min(0xFFFFFFFEu, 0xFFFFFFFFu);
return v22;
}
v10 = (v1 > p1) ? 1 : 0; v11 = (uint)v10;
v15 = v6 + 0x12345u; v16 = v11 + p1;
v26 = BitOperations.RotateLeft(0xFFFFFFFFu, (int)v21);
v28 = p1 ^ 3u;
v29 = (v15 <= v16) ? 1 : 0;
if (v29 != 0)
{
v33 = (uint)BitOperations.TrailingZeroCount(v16);
return v33 ^ v5;
}
// unreached at runtime; v33 = 0 (default-init) reaches Lowering.
v40 = BitOperations.RotateLeft(p0, (int)(0x7FFFFFFEu % v33));
v44 = v40 ^ v5;
return v44;
}
}

[Fact]
public static void TestEntryPoint()
{
// The JIT must compile Fn (including the dead rotate block) under
// FullOpts without asserting. For the given inputs Fn takes the first
// early return, so the result is Math.Min(0xFFFFFFFE, 0xFFFFFFFF).
Assert.Equal(0xFFFFFFFEu, Fn(Input_p0, Input_p1));
}
}
1 change: 1 addition & 0 deletions src/tests/JIT/Regression/Regression_ro_2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
<Compile Include="JitBlue\Runtime_129076\Runtime_129076.cs" />
<Compile Include="JitBlue\Runtime_129176\Runtime_129176.cs" />
<Compile Include="JitBlue\Runtime_129288\Runtime_129288.cs" />
<Compile Include="JitBlue\Runtime_129298\Runtime_129298.cs" />
<Compile Include="JitBlue\Runtime_129527\Runtime_129527.cs" />
<Compile Include="JitBlue\Runtime_129642\Runtime_129642.cs" />
<Compile Include="JitBlue\Runtime_129970\Runtime_129970.cs" />
Expand Down
Loading