Uh oh!
There was an error while loading. Please reload this page.
JIT: fix TrailingZeroCount constant folding on 32-bit targets - #133093
JIT: fix TrailingZeroCount constant folding on 32-bit targets#133093AndyAyersMS wants to merge 4 commits into
Conversation
The importer's constant-folding path for `BitOperations.TrailingZeroCount`
builds a `TYP_LONG` node for a 64-bit operand:
result = gtNewLconNode(BitOperations::TrailingZeroCount(cns));
but `TrailingZeroCount` returns `int`. `impPrimitiveNamedIntrinsic` already
reconciles that at the end of the function:
if ((result != nullptr) && (retType != baseType))
{
// We're either LONG->INT or INT->LONG
result = gtFoldExpr(gtNewCastNode(retType, result, true, retType));
}
The `baseType = retType;` in the constant path made that condition false, so
the LONG->INT cast never ran and a `TYP_LONG` value reached a context expecting
`int`. On 32-bit targets that surfaced as `InvalidProgramException` at runtime
and as an importer assert in a checked JIT; on 64-bit targets the malformed
node happened to be harmless.
`NI_PRIMITIVE_LeadingZeroCount` has the identical constant path but no
`baseType` assignment, so its cast fires and it is correct on x86 --
which is the shape this restores. `NI_PRIMITIVE_Log2` assigns `baseType`
*after* inserting a cast, not instead of one.
Fixesdotnet#133092|
Azure Pipelines: Successfully started running 5 pipeline(s). 11 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
🟡 Changes recommended
The new regression test directory is missing a matching Runtime_133092.csproj, so the test likely won’t be built/executed in CI (and may miss required env settings like DOTNET_TieredCompilation=0).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes a CoreCLR JIT importer type-mismatch in the BitOperations.TrailingZeroCount(ulong) constant-folding path so 32-bit targets don’t end up with a TYP_LONG node where an int is required, and adds a regression test intended to cover the failing constant forms.
Changes:
- Remove the
baseType = retType;assignment in theNI_PRIMITIVE_TrailingZeroCountconstant path so the existing LONG↔INT reconciliation logic runs. - Add a new JitBlue regression test that exercises
TrailingZeroCountconstant folding (includingldc.i8-encoded constants) plus a few control cases.
File summaries
| File | Description |
|---|---|
| src/coreclr/jit/importercalls.cpp | Ensures TrailingZeroCount constant folding preserves the operand type so the end-of-function cast reconciliation can narrow long → int when needed. |
| src/tests/JIT/Regression/JitBlue/Runtime_133092/Runtime_133092.cs | Adds a regression test for constant-folded TrailingZeroCount(ulong) cases that previously failed on 32-bit targets. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
Uh oh!
There was an error while loading. Please reload this page.
AndyAyersMS
commented
Sep 2, 2026
Local A/B on this exact commit, using a checked x86 altjit and crossgen2 built from this branch (so the JIT-EE version matches and there is no cross-tree skew). With the fix: Negative control — same tree, same crossgen2, only Only the Worth noting for anyone reading the issue's trigger discussion: Build check: |
Uh oh!
There was an error while loading. Please reload this page.
tannergooding
left a comment
There was a problem hiding this comment.
LGTM. Had to think through the handling because its a bit nuanced.
Might still be a valuenum issue, but I've not fully thought through whether or not that is an issue or not.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1c0b5362-6ffc-4f6d-8bdf-7adf4c55ca61
There was a problem hiding this comment.
🟢 Approval recommended
The change is minimal, aligns with existing importer reconciliation behavior, and is covered by an appropriate regression test added to the JIT test suite.
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
AndyAyersMS
commented
Sep 2, 2026
@tannergooding can you re-approve? Had to fix test integration. |
AndyAyersMS
commented
Sep 3, 2026
@copilot resolve the merge conflicts in this pull request |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b83a30c2-8c19-451e-aca1-c471bdd3cb2f
tannergooding
commented
Sep 4, 2026
Looks like there's a merge conflict again.... I wonder if we can reorganize the CC. @jkoritzinsky as I believe you were looking at some of the merged test runner stuff recently |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b83a30c2-8c19-451e-aca1-c471bdd3cb2f
There was a problem hiding this comment.
🟢 Approval recommended
The change is narrowly scoped, aligns the TZC constant-folding path with existing sibling intrinsic patterns, and includes a targeted regression test to prevent recurrence.
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
Fixes#133092.
BitOperations.TrailingZeroCountwith a compile-time 64-bit constant produced aTYP_LONGimporter node where the intrinsic returnsint. On 32-bit targets that surfaced asInvalidProgramExceptionat runtime, and as an importer assert in a checked JIT. It has been broken since #80789 (Jan 2023), so .NET 8, 9 and 10 are affected; .NET 6 and 7 are correct.The fix
impPrimitiveNamedIntrinsicalready reconciles aLONG/INTmismatch at the end of the function:The
baseType = retType;in theTrailingZeroCountconstant path made that condition false, so theLONG->INTcast never ran. Removing it lets the existing reconciliation do its job.This restores the shape the sibling intrinsics already have:
NI_PRIMITIVE_LeadingZeroCountbuilds the identicalgtNewLconNodebut has nobaseTypeassignment, so its cast fires — and LZC is correct on x86 today.NI_PRIMITIVE_Log2assignsbaseTypeafter inserting a cast, not instead of one.On 64-bit targets the cast is now inserted where previously it was skipped; since
resultis a constant,gtFoldExprfolds it, so the emitted value is unchanged.Verification
Measured on stock x86 runtimes before the fix — same
net6.0binary,DOTNET_TieredCompilation=0:TZC(0x100000000UL)32— correctInvalidProgramException32— correctA/B with a checked x86 altjit over a 1,200-method generated corpus:
0xC0000003JitStress=20xC0000003JitStress=2The folded constants were disassembled to confirm they are correct rather than merely assert-free:
TZC(0)→mov eax, 64,TZC(1)→xor eax, eax,TZC(2)→1,TZC(1<<32)→32,TZC(1<<63)→63.Note on the regression test
The malformed node was produced for every constant form, but only constants that Roslyn must encode as
ldc.i8escalate to a failure on a release JIT. Constants encoded asldc.i4* + conv.i8— such as0UL, or0xFFFFFFFFFFFFFFFFvialdc.i4.m1— returned correct values even on the unfixed compiler, while still tripping the assert on a checked JIT.The test therefore leads with
1<<32,1<<63and0x100000001, which are the cases that actually fail without the fix. A test written only with0ULwould pass against the unfixed compiler in a release run. It also includesLeadingZeroCount,PopCountand a non-constantTrailingZeroCountas controls.How this was found
A differential fuzzer that compiles generated methods with crossgen2 across four target architectures. The bug was reachable for ~3 years but invisible to the existing campaign because every machine in it is x64 or arm64; compiling for 32-bit targets does not require 32-bit hardware.