oracle/graal · error · PermanentBailoutException
Graal implementation restriction: Method with %s loop explos
Error message
Graal implementation restriction: Method with %s loop explosion must not have more than one top-level loop
What it means
GraphDecoder handles merge-explode loop explosion by converting loop begins into plain merges and reconstructing loops later; it supports at most one top-level loop per method (tracked via loopExplosionHead). When a second top-level loop is encountered during MERGE_EXPLODE decoding, it throws a documented Graal implementation restriction as a PermanentBailoutException.
Source
Thrown at compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/GraphDecoder.java:1743
for (EndNode predecessor : predecessors) {
existingState.merge.addForwardEnd(predecessor);
}
return;
}
}
/*
* Merge explosion: When doing merge-explode PE loops are detected after partial evaluation
* in a dedicated steps. Therefore, we create merge nodes instead of loop begins and loop
* exits and later replace them with the detected loop begin and loop exit nodes.
*/
MergeNode merge = graph.add(new MergeNode());
methodScope.loopExplosionMerges.add(merge);
if (methodScope.loopExplosion.mergeLoops()) {
if (loopScope.iterationStates.size() == 0 && loopScope.loopDepth == 1) {
if (methodScope.loopExplosionHead != null) {
throw new PermanentBailoutException("Graal implementation restriction: Method with %s loop explosion must not have more than one top-level loop",
LoopExplosionPlugin.LoopExplosionKind.MERGE_EXPLODE);
}
methodScope.loopExplosionHead = merge;
}
}
loopBegin.replaceAtUsagesAndDelete(merge);
merge.setStateAfter(frameState);
merge.setNext(successor);
for (EndNode predecessor : predecessors) {
merge.addForwardEnd(predecessor);
}
if (methodScope.loopExplosion.mergeLoops()) {
LoopExplosionState explosionState = new LoopExplosionState(frameState, merge);
loopScope.iterationStates.put(explosionState, explosionState);
}
}View on GitHub (pinned to a66e9ccd1d)
Solutions
- Move each top-level loop into its own method so no MERGE_EXPLODE method has more than one top-level loop
- Switch that method to a different LoopExplosionKind (FULL_UNROLL, DUP_WITHOUT_EXIT, ...) that does not carry the single-loop restriction
- Drop the explode annotation from the offending method if the explosion is not essential
Example fix
// before
@LoopExplosionKind(MERGE_EXPLODE)
static int run(VMState s) {
while (s.step1()) { }
while (s.step2()) { } // second top-level loop -> bailout
}
// after
@LoopExplosionKind(MERGE_EXPLODE) static int run1(VMState s) { while (s.step1()) { } return 0; }
@LoopExplosionKind(MERGE_EXPLODE) static int run2(VMState s) { while (s.step2()) { } return 0; }
static int run(VMState s) { run1(s); return run2(s); } Defensive patterns
Strategy: validation
Validate before calling
// Static check before annotating/running PE: count top-level loops in the method
long topLevelLoops = countTopLevelLoops(methodBytecode);
if (kind == MERGE_EXPLODE && topLevelLoops > 1) {
throw new IllegalArgumentException("MERGE_EXPLODE supports only one top-level loop: " + method);
} Try / catch
try { pe(method); } catch (PermanentBailoutException e) { /* restructure method or change explosion kind */ throw e; } Prevention
- Keep exactly one top-level loop per MERGE_EXPLODE method; extract siblings into helpers
- Prefer FULL_UNROLL or DUP_WITHOUT_EXIT when a method must contain several loops
- Add an architectural test asserting exploded interpreter methods stay single-loop
When it happens
Trigger: Partial evaluation of a method annotated (or registered via LoopExplosionPlugin) with LoopExplosionKind.MERGE_EXPLODE whose encoded graph contains more than one top-level loop (loopScope.loopDepth == 1 with an already-set loopExplosionHead).
Common situations: Truffle interpreters or substitution graphs where a helper method got MERGE_EXPLODE semantics (annotation, plugin, or @LoopExplosionKind inheritance) but the method body contains two sibling loops; refactoring exploded methods by adding a second loop.
Related errors
- Graal implementation restriction: Method with %s loop explos
- too many iterations in computeGlobalLiveSets
- Frame size (%d) exceeded maximum allowed frame size (%d).
- NormalizeCompareNode connected to %s (%s %s %s)
- LogFile substitution %s cannot be combined with any other ch
AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14).
Data as JSON: /api/errors/6932db344a6b0a2b.
Report an issue: GitHub.