Assertion failed 'm_compGenTreeID == m_compiler->compGenTreeID' during 'Optimize Induction Variables'
Fuzzlyn found the following example that asserts on arm64 (osx-arm64, FullOpts, Runtime Async):
Assertion failed 'm_compGenTreeID == m_compiler->compGenTreeID' in 'Program:M0()' during 'Optimize Induction Variables' (IL size 160; hash 0xaf50ff37; FullOpts)
File: /Users/runner/work/1/s/src/coreclr/jit/phase.cpp Line: 47
// Generated by Fuzzlyn v3.3 on 2026-06-14 15:55:11
// Run on Arm64 MacOS
// Seed: 9828420025282385592-async,runtimeasync,vectort,vector64,vector128,armadvsimd,armadvsimdarm64,armaes,armarmbase,armarmbasearm64,armcrc32,armcrc32arm64,armdp,armrdm,armrdmarm64,armsha1,armsha256
// Reduced from 104.7 KiB to 1.3 KiB in 00:02:09
// Hits JIT assert for Release with Runtime Async:
// Assertion failed 'm_compGenTreeID == m_compiler->compGenTreeID' in 'Program:M0()' during 'Optimize Induction Variables' (IL size 160; hash 0xaf50ff37; FullOpts)
//
// File: /Users/runner/work/1/s/src/coreclr/jit/phase.cpp Line: 47
//
using System.Threading.Tasks;
using System.Runtime.Intrinsics.Arm;
public struct S1
{
public long F3;
}
public class Program
{
public static IRuntime s_rt;
public static bool[] s_8;
public static long s_13;
public static void Main()
{
M0();
}
public static void M0()
{
uint var9 = default(uint);
S1[] var24 = default(S1[]);
ulong var34 = default(ulong);
try
{
var9 = 1;
s_8[0] |= false;
}
catch (System.Exception)when (true)
{
}
for (int lvar31 = -2147483646; lvar31 > -2147483648; lvar31--)
{
var vr0 = (ulong)(lvar31 - var9++);
short var33 = (short)Crc32.Arm64.ComputeCrc32C(0, vr0);
s_13 = var24[0].F3;
M6().GetAwaiter().GetResult();
s_rt.WriteLine("c_122", var34);
s_rt.WriteLine("c_126", var33);
}
}
public static async Task<S1> M6()
{
return new S1();
}
}
public interface IRuntime
{
void WriteLine<T>(string site, T value);
}
public class Runtime : IRuntime
{
public void WriteLine<T>(string site, T value) => System.Console.WriteLine(value);
}
Analysis of jitdump (AI generated)
Analysis (AI generated)
Assertion: m_compGenTreeID == m_compiler->compGenTreeID
src/coreclr/jit/phase.cpp:47, in Program:M0() during Optimize Induction Variables (arm64, FullOpts).
What the assert means
Phase::Observations::Check (phase.cpp:38-52) runs after every phase. When a
phase returns PhaseStatus::MODIFIED_NOTHING, it asserts that several compiler
counters are unchanged, including compGenTreeID (the running allocation counter
for GenTree nodes). The assert fires because the Optimize Induction Variables
phase returned MODIFIED_NOTHING even though it allocated new GenTree nodes
(compGenTreeID increased).
The jitdump trace
For loop L00 the phase attempts strength reduction of the derived IV:
All uses of primary IV V01 are used to compute a 2-derived IV
<L00, ((zext<64>(V01.6) * -1) + sext<64>(<L00, -2147483646, -1>)), -1>
Found a legal insertion point after a last use of the IV in BB10 after STMT00024
Skipping: init value could not be materialized
...
*************** Finishing PHASE Optimize Induction Variables [no changes]
So strength reduction got as far as trying to materialize the init value of
the new IV and then bailed out ("init value could not be materialized"), reporting
no changes. But it had already created IR by that point.
Root cause
StrengthReductionContext::TryReplaceUsesWithNewPrimaryIV
(inductionvariableopts.cpp:2425) materializes the IV start value:
GenTree* initValue = m_scevContext.Materialize(iv->Start); // line 2445
if (initValue == nullptr)
{
JITDUMP(" Skipping: init value could not be materialized\n");
return false; // changed stays false
}
The start SCEV here is an Add:
(zext<64>(V01.6) * -1) + sext<64>(<L00, -2147483646, -1>).
ScalarEvolutionContext::Materialize(scev, createIR=true, ...) (scev.cpp:1286)
recurses operand-by-operand and allocates GenTree nodes as it goes:
case ScevOper::Add: case ScevOper::Mul: case ScevOper::Lsh:
{
...
if (!Materialize(binop->Op1, createIR, &op1, &op1VN) ||
!Materialize(binop->Op2, createIR, &op2, &op2VN))
{
return false; // <-- Op1's nodes were already created and are now orphaned
}
...
}
For this SCEV:
Op1 = zext<64>(V01.6) * -1 materializes successfully, creating a CAST, a
LCL_VAR and a NEG node (incrementing compGenTreeID).
Op2 = sext<64>(<L00, -2147483646, -1>) contains an AddRec, and
case ScevOper::AddRec: return false; (scev.cpp:1427-1428). So Op2
materialization fails.
Because Op2 fails, the whole Materialize returns false/nullptr, but the
nodes already allocated for Op1 are leaked (never inserted into any block).
TryReplaceUsesWithNewPrimaryIV then returns false, changed is never set, and
optInductionVariables returns PhaseStatus::MODIFIED_NOTHING
(inductionvariableopts.cpp:2910). The post-phase check sees the bumped
compGenTreeID and asserts.
In short: Materialize is not transactional. It can allocate GenTree nodes
for early operands and then fail on a later operand (un-materializable AddRec,
non-zero TYP_REF/TYP_BYREF constant, or a 32-bit long multiply), returning
nullptr after having already advanced compGenTreeID. Callers that treat
nullptr as "no work done" therefore violate the MODIFIED_NOTHING invariant.
Trigger conditions
A loop where strength reduction finds a derived-IV cursor and reaches the init-value
materialization step, but the start SCEV is a binary expression whose first operand
is materializable while a later operand is not (here a nested AddRec under a
SignExtend). This is a DEBUG-only invariant assert; in release the orphaned nodes
are simply unused allocations, but the partial-allocation-then-bail behavior is the
underlying defect.
Attached details.zip file that includes the repro.mc and jitdump.txt files for this example.
cc @dotnet/jit-contrib
details.zip
Assertion failed 'm_compGenTreeID == m_compiler->compGenTreeID' during 'Optimize Induction Variables'
Fuzzlyn found the following example that asserts on arm64 (osx-arm64, FullOpts, Runtime Async):
Analysis of jitdump (AI generated)
Analysis (AI generated)
Assertion:
m_compGenTreeID == m_compiler->compGenTreeIDsrc/coreclr/jit/phase.cpp:47, inProgram:M0()during Optimize Induction Variables (arm64, FullOpts).What the assert means
Phase::Observations::Check(phase.cpp:38-52) runs after every phase. When aphase returns
PhaseStatus::MODIFIED_NOTHING, it asserts that several compilercounters are unchanged, including
compGenTreeID(the running allocation counterfor
GenTreenodes). The assert fires because the Optimize Induction Variablesphase returned
MODIFIED_NOTHINGeven though it allocated newGenTreenodes(
compGenTreeIDincreased).The jitdump trace
For loop
L00the phase attempts strength reduction of the derived IV:So strength reduction got as far as trying to materialize the init value of
the new IV and then bailed out ("init value could not be materialized"), reporting
no changes. But it had already created IR by that point.
Root cause
StrengthReductionContext::TryReplaceUsesWithNewPrimaryIV(inductionvariableopts.cpp:2425) materializes the IV start value:
The start SCEV here is an
Add:(zext<64>(V01.6) * -1) + sext<64>(<L00, -2147483646, -1>).ScalarEvolutionContext::Materialize(scev, createIR=true, ...)(scev.cpp:1286)recurses operand-by-operand and allocates
GenTreenodes as it goes:For this SCEV:
Op1 = zext<64>(V01.6) * -1materializes successfully, creating aCAST, aLCL_VARand aNEGnode (incrementingcompGenTreeID).Op2 = sext<64>(<L00, -2147483646, -1>)contains anAddRec, andcase ScevOper::AddRec: return false;(scev.cpp:1427-1428). SoOp2materialization fails.
Because
Op2fails, the wholeMaterializereturnsfalse/nullptr, but thenodes already allocated for
Op1are leaked (never inserted into any block).TryReplaceUsesWithNewPrimaryIVthen returnsfalse,changedis never set, andoptInductionVariablesreturnsPhaseStatus::MODIFIED_NOTHING(inductionvariableopts.cpp:2910). The post-phase check sees the bumped
compGenTreeIDand asserts.In short:
Materializeis not transactional. It can allocateGenTreenodesfor early operands and then fail on a later operand (un-materializable
AddRec,non-zero
TYP_REF/TYP_BYREFconstant, or a 32-bitlongmultiply), returningnullptrafter having already advancedcompGenTreeID. Callers that treatnullptras "no work done" therefore violate theMODIFIED_NOTHINGinvariant.Trigger conditions
A loop where strength reduction finds a derived-IV cursor and reaches the init-value
materialization step, but the start SCEV is a binary expression whose first operand
is materializable while a later operand is not (here a nested
AddRecunder aSignExtend). This is a DEBUG-only invariant assert; in release the orphaned nodesare simply unused allocations, but the partial-allocation-then-bail behavior is the
underlying defect.
Attached details.zip file that includes the repro.mc and jitdump.txt files for this example.
cc @dotnet/jit-contrib
details.zip