Hello, code that worked on groovy 5.0.4 is now thr...
# groovy
j
Hello, code that worked on groovy 5.0.4 is now throwing MethodTooLargeException from MethodWriter.java:2088, but AFAIS the method is not too large
p
If you can create a Jira ticket with a reproducer we can try to help you work it out.
j
Its a spock spec with 595 lines in total, 38k chars, 348 asserts. I cant create a reproducer.If i comment/rm any three asserts the code compiles and runs
Happens inside spock expect, verifyAll
I can work around it by splitting the spock Specification.verifyAll-call
I will try to create a reproducer
I have reproducer. With 5.0.4 we can have 601 “complex” asserts inside verifyAll, with 5.0.5 590. Note that my actual production code has less than 350. With trivial 1==1 asserts i could not reproduce the diff
Is this kind of observable change in code compilation considered a bug? The limits are quite arbitrary
p
I can reproduce and FYI, here is Claude's analysis: This is the JVM's hard 64 KB per-method bytecode limit (
code_length
in the classfile spec is a
u4
but practically capped at 65535). It's not really a Groovy bug — ASM is faithfully reporting that the generated method can't fit. Why this reproducer blows up so quickly (at ~589 assertions): 1. Everything inside
verifyAll { … }
becomes the body of a single closure's
doCall
method. 2. Spock's
@Specification
AST transform rewrites every
assert
inside that closure into a value-recording form (each subexpression is captured into a
ValueRecorder
so Spock can render the pretty "Condition not satisfied" table on failure). That rewrite typically produces ~80–150 bytes of bytecode per assert, far more than a plain
assert
. 3. Groovy itself also adds line-number, callsite, and power-assert scaffolding per statement. So even though the source looks tiny, each
assert obj.fieldN == 'valueN'
expands to a substantial chunk, and 589 of them push
doCall
past 65535 bytes. Fixes / workarounds (in order of preference): - Split the
verifyAll
block
—
verifyAll
is allowed multiple times in one
expect:
/
then:
block, and each one becomes its own closure/method:
Copy code
groovy
  expect:
      verifyAll { /* first ~300 asserts */ }
      verifyAll { /* next  ~300 asserts */ }
- Use a data-driven feature — put the field/value pairs in a
where:
table and assert once; the loop replaces the unrolled asserts entirely. - Split into multiple feature methods — each
def "…"()
is its own method, so the 64 KB budget resets. - Drop into a plain `for`/`each` loop inside
verifyAll
— one
assert
in a loop instead of N textual asserts. Can Groovy do anything about it? Not really, without changing semantics: - Automatic method-splitting for closures is very risky because closures capture locals,
this
, delegate, owner, and return/continue semantics — splitting would need to preserve all of that. The compiler already splits some large `<clinit>`/`<init>` patterns, but closure bodies are harder. - A nicer error message is fair game, though — catching
MethodTooLargeException
during class writing and re-reporting it with the offending source method/closure name and a hint ("consider splitting the block") would be a legitimate small UX improvement in `CompilationUnit`'s phase-7 writer. If you want, I can look at wiring that in.
j
Yeah, but why does the behaviour change in 5.0.5 vs 5.0.4? Groovyc doing something differently?
I can find i acceptable if u dont consider this a bug but clearly something changed in the compilation so that we hit this limit now earlier
p
I would certainly be interested in understanding why and I am not saying it isn't a bug but just setting expectations that it might not be something that can be (easily) fixed. From the release notes: https://groovy-lang.org/changelogs/changelog-5.0.5.html Nothing stands out to me as something that would have changed things much.
j
Ok anyways we do have a workaround and spock specs with 350 asserts are def not the norm. Maybe u could have a test case that tracks the compiled method size or so, to catch worse regressions
p
PRs welcome! 😜
😁 1
Claude still pondering ... Confirmed — threshold dropped from 596 → 586 asserts between 5.0.4 and 5.0.5. Let me get the bytecode diff.
Closure class is ~1114 bytes larger in 5.0.5 (at 500 asserts) I see a constant-pool difference. 5.0.4 has a direct Methodref ValueRecorder.record; 5.0.5 has an InvokeDynamic invoke:(ValueRecorder,I,Object)Object taking its place.
Found it. 5.0.4 emits invokevirtual ValueRecorder.record (3 bytes); 5.0.5 emits invokedynamic invoke:(ValueRecorder,I,Object)Object (5 bytes) — +2 bytes per record call.
👍 1
j
Nice 🤓 from my pov that does not need to be “fixed”.
p
There are a bunch of performance improvements we have been working on for Groovy 6, e.g.: https://github.com/apache/groovy/pull/2438 as just one. The number of asserts might increase then. Maybe.
Empirical confirmation: patching just InvokeDynamicWriter.class in the 5.0.5 jar back to the pre-GROOVY-7785 version produced closure bytecode byte-identical to 5.0.4 (84106 bytes exactly). So the regression is isolated to that one commit. Summary Cause: f1d6e5f025 GROOVY-7785 "StackoverflowException when using too many chained method calls" (Daniel Sun, Feb 8). The fix added an iterative receiver-chain walker to InvokeDynamicWriter.prepareIndyCall: // old: receiver.visit(controller.getAcg()); // new: visitReceiverOfMethodCall(receiver); // flattens chain, emits each intermediate call via finishIndyCallForChain → invokedynamic Why Spock's verifyAll is affected. Each assert obj.fieldN == 'valueN' rewrites into roughly: verifyCondition(ec, vr, "…", line, col, null, vr.record(vr.startRecordingValue(3), compareEqual( vr.record(vr.startRecordingValue(1), vr.record(vr.startRecordingValue(0), obj.get()) // ← this record() .getProperty("fieldN")), // ← is the receiver of getProperty vr.record(vr.startRecordingValue(2), "valueN")))) The inner record(...) call has another method call — getProperty(...) — as its parent, i.e. it is a receiver that is itself a MethodCallExpression. In 5.0.4 visiting that receiver recursed through AsmClassGenerator.visitMethodCallExpression, where the resolved method target let it emit invokevirtual ValueRecorder.record (3 bytes). In 5.0.5 the new visitReceiverOfMethodCall pushes it onto a chain and emits it via finishIndyCallForChain as invokedynamic invoke:(ValueRecorder,I,Object)Object (5 bytes). Arithmetic: +2 bytes per assert × 500 asserts = +1000 bytes, matching the observed 1114-byte growth. That shrinks the 64 KB headroom by ~10 asserts, matching the 596 → 586 threshold drop. Suggested fix direction: the intent of GROOVY-7785 is only to prevent real recursion blowups for long a.b().c().d()… chains. A minimal tweak in visitReceiverOfMethodCall would preserve the original non-regressive behavior for short chains, e.g. keep receiver.visit(acg) when chain.size() <= N (pick N around ~32–64, well under the JVM recursion limit), and only switch to the iterative/indy path when the chain is genuinely deep. That restores direct invokevirtual for common cases while still fixing the deep-chain StackOverflow. Alternatively, finishIndyCallForChain could honor a resolved MethodNode target (set by AST transforms such as Spock's) and emit a direct invokevirtual for those.
So, it was a fix we applied to make performance better for a different situation that is causing what you see.
👍 1
That does look like something we can action.
But, you are right that we should have a test that would put bounds on a potential fix.
👍 1
I think I will place all that info in a jira including your test if that is okay, or happy if you want to create it.
j
Please create it, im not familiar with your jira. You can copy paste or link my code. Thanks for figuring this out, it was very interesting. Have a good day!
p
Jira is having issues at the moment but I'll create an issue some time soon.
There is a fix in place for 5.0.6 maybe a week or two at most away that fixes your problem. Note that you are approaching the JVM limit for Spock annotations even after the fix - as your were on 5.0.4.
👍 1
j
Thanks alot! 🍻