Skip to content

JIT: Assertion failed 'm_compGenTreeID == m_compiler->compGenTreeID' during 'Optimize Induction Variables' #129527

@jakobbotsch

Description

@jakobbotsch

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

Metadata

Metadata

Labels

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

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions