Repository navigation
Description
BitOperations.TrailingZeroCount with a compile-time 64-bit constant builds a TYP_LONG importer node where the intrinsic returns int. On 32-bit targets this throws InvalidProgramException on released runtimes, and asserts in the importer on a checked JIT.
Repro
using System.Numerics;
using System.Runtime.CompilerServices;
[MethodImpl(MethodImplOptions.NoInlining)]
static int Tzc() => BitOperations.TrailingZeroCount(0x100000000UL);
Console.WriteLine(Tzc()); // expected: 32Run on x86 with DOTNET_TieredCompilation=0.
Expected: 32
Actual: System.InvalidProgramException
Affected versions
The same net6.0 binary, executed against stock runtimes:
| runtime | result |
|---|---|
| x86 6.0.36, 7.0.20 | 32 — correct |
| x86 8.0.30, 9.0.19, 10.0.11 | InvalidProgramException |
| x64 (all of the above, same binary) | 32 — correct |
TrailingZeroCount(v) with a variable operand, and LeadingZeroCount / PopCount / Log2 with the same constant, are correct on every configuration — so this is specific to TrailingZeroCount with a constant on a 32-bit target.
On a checked JIT (crossgen2 --targetarch:x86):
Assertion failed '(genActualType(op2->TypeGet()) == genActualType(info.compRetType)) || ...
: Possibly bad IL with CEE_ret at offset 0007h (op1=NULL op2=long stkDepth=0)'
The IL is valid: one byte-identical assembly compiles cleanly for x64 and arm64 and asserts only for x86 and arm.
Root cause
src/coreclr/jit/importercalls.cpp, NI_PRIMITIVE_TrailingZeroCount (case at line 7159 on main):
if (varTypeIsLong(baseType))
{
uint64_t cns = static_cast<uint64_t>(op1->AsIntConCommon()->LngValue());
result = gtNewLconNode(BitOperations::TrailingZeroCount(cns)); // TYP_LONG
}
...
baseType = retType; // line 7182 -- defeats the fixup below
break;TrailingZeroCount returns int, so the TYP_LONG node needs narrowing. The function already does this at line 7275:
if ((result != nullptr) && (retType != baseType))
{
// We're either LONG->INT or INT->LONG
result = gtFoldExpr(gtNewCastNode(retType, result, /* unsigned */ true, retType));
}Assigning baseType = retType makes that condition false, so the LONG->INT cast never happens and a TYP_LONG value reaches a context expecting int.
The sibling intrinsics suggest this is unintended rather than deliberate:
NI_PRIMITIVE_LeadingZeroCount(case at line 6819) builds the identicalgtNewLconNode, but its constant path goes straight tobreak;without assigningbaseType— so the line-7275 cast fires, and LZC is correct on x86.NI_PRIMITIVE_Log2assignsbaseType = retTypeafter inserting a cast, not instead of one.
On 64-bit targets the malformed node is harmless, which is why only 32-bit targets are affected.
Suggested fix
Remove the unconditional baseType = retType; and let the existing reconciliation run, matching LeadingZeroCount.
A/B on a checked x86 altjit over a 1,200-method corpus: unfixed dies after 347 methods; fixed compiles 1200/1200, both in base mode and under JitStress=2. The folded constants were disassembled to confirm they are correct and not merely assert-free: TZC(0)→64, TZC(1)→0, TZC(2)→1, TZC(1<<32)→32, TZC(1<<63)→63.
Regression
Introduced by #80789 (Jan 2023) — baseType = retType; is present in that commit, which matches the .NET 7-correct / .NET 8-broken boundary measured above.
Note for a regression test
The malformed node is produced for every constant form, but only constants that Roslyn must encode as ldc.i8 escalate on a release JIT. Constants encoded as ldc.i4* + conv.i8 (for example 0UL, or 0xFFFF_FFFF_FFFF_FFFF via ldc.i4.m1) return correct values on a release runtime while still asserting on a checked JIT.
- A release-execution test must use a constant requiring
ldc.i8(e.g.0x1_0000_0000UL), otherwise it passes on the unfixed compiler. - A checked-JIT test may use any constant form.
Configuration
Windows x64 host, x86 runtimes 6.0.36 / 7.0.20 / 8.0.25 / 8.0.30 / 9.0.14 / 9.0.19 / 10.0.5 / 10.0.11, DOTNET_TieredCompilation=0. Found by a differential fuzzer compiling generated methods with crossgen2 across four target architectures.
Activity
dotnet-policy-service commentedon Sep 2, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
AndyAyersMS commentedon Sep 2, 2026
@EgorBo I have a PR up already...
JIT: fix TrailingZeroCount constant folding on 32-bit targets (#133093)