paulk-asert commented on PR #15557:
URL: https://github.com/apache/grails-core/pull/15557#issuecomment-4326453660
I tried to get Claude to build a standalone reproducer for this row:
@Delegate field on a trait silently returns null because Groovy 5's
DelegateASTTransformation emits direct field-access instead of
trait-helper-method access
Any help getting a reproducer would be greatly appreciated.
--------
## Findings
**TL;DR — I cannot reproduce the regression as described, and the bytecode
evidence contradicts the commit message's diagnosis.** Before filing a Groovy
issue, I'd want to verify the failure actually reproduces against a clean
commit.
### Relevant commits in PR #15557 (3 of the 81)
| SHA | Date | Author | What
|
| ------------ | ----- | ---------------------------------- |
-------------------------------------------------------------------------------------------------------------
|
| `3608a76a63` | Apr 6 | James Fredley (assisted by Claude) | First attempt:
add explicit `@CompileDynamic render(Map)` to the trait
|
| `43f98a1564` | Apr 6 | James Fredley (assisted by Claude) | Second
attempt: bypass named-arg `render(Map)` by calling
`templateRenderer.render(...)` with positional args |
| `a0ee062e89` | Apr 7 | James Fredley (assisted by Claude) | **Final "fix":
convert `GrailsApplicationCommand` trait → abstract class** (and revert the
workarounds) |
All three are the AI-assisted `Assisted-by: Claude Code` commits. The third
one is the diagnostic source for "Groovy 5's `DelegateASTTransformation` emits
direct field-access instead of trait-helper-method access".
### Reproducer
`/tmp/delegate-trait-repro/` — a faithful, standalone reproducer mirroring
`GrailsApplicationCommand`:
- `src/Service.groovy` — `Named`/`Described` interfaces, `BaseCommand`
(`@CompileStatic` trait extending them), `ModelBuilder` trait, multi-overload
`TemplateRenderer` interface + impl, `FileSystemInteraction` interface + impl
- `src/MyCommand.groovy` — `@CompileStatic trait MyCommand implements
BaseCommand, ModelBuilder` with **two** `@Delegate` fields and an `init()` that
assigns them from inside the trait body (mirrors `handle(ExecutionContext)`)
- `src/GenerateCommand.groovy` — `@CompileStatic class GenerateCommand
implements MyCommand` with a `run()` method that issues the named-arg call
`render(template: …, destination: file('…'), overwrite: true)` exactly like
`GenerateControllerCommand`
- `Main.groovy` — driver that calls `init()` then `run()`
### Result
| Groovy | Behaviour
|
| ------ |
---------------------------------------------------------------------------- |
| 4.0.27 | ✅ Field set, `render(Map)` correctly dispatched, output produced
|
| 5.0.5 | ✅ **Same** — field set, `render(Map)` correctly dispatched,
output produced |
I tried every variation I could think of: `@CompileStatic` only on the impl
class; `@CompileStatic` on both trait and impl; multi-trait composition;
setting fields from inside the trait body; named-arg vs Map-literal call sites;
multi-file vs single-script compilation. **All combinations work correctly
under both Groovy versions.**
### Bytecode evidence (the reason I'm skeptical)
The `@Delegate`-generated `render(Map)` on the implementing class compiles
to essentially identical bytecode under both:
```
0: aload_0
1: getfield MyCommand__templateRenderer:LTemplateRenderer; // direct
field access — both versions
4: aload_1
5: invokeinterface TemplateRenderer.render:(Ljava/util/Map;)V
```
The field name `MyCommand__templateRenderer` is the trait field correctly
copied onto the impl class, and `getfield` against it works fine on Groovy 5 —
the field is set by the trait setter in the normal way.
Source-side, `DelegateASTTransformation.java` between `GROOVY_4_0_27` and
`GROOVY_5_0_5` uses the *same* `delegate.getOp = varX(fieldNode)` line. There's
no shift from "helper-method access" to "direct field access" in the @Delegate
transform itself — both versions use `varX`.
### What I think actually happened
The commit-message rationale was AI-authored ("Assisted-by: Claude Code").
The trait→class conversion clearly *did* fix some test failure, but the
bytecode and source evidence suggests the diagnostic explanation is post-hoc
speculation, not the real root cause. Plausible alternatives:
- The failing test (`ScaffoldingSpec`) is an integration test that forks a
Gradle JVM and runs `generate-controller` end-to-end — there are many moving
parts (Spring Boot 4, classpath, Spock parallel forks, static state in the
trait helper) that could fail for reasons unrelated to `@Delegate`
- The real fix may have been the *removal* of the `@CompileDynamic
render(Map)` workaround that was added in `3608a76a63` and was masking
something else
- A genuine Groovy-5 regression may exist somewhere in this neighborhood,
but it isn't what the commit message describes
### Recommendation before raising a Groovy issue
1. Revert just `a0ee062e89` on top of HEAD and confirm the test actually
fails again (verifies the fix's necessity)
2. If it does fail, drop in `printlns`/breakpoints in the trait helper and
`TemplateRendererImpl.render(Map)` to determine whether `templateRenderer` is
actually null, or whether some other dispatch is going wrong
3. Only then is it worth taking a reproducer to the Groovy team — and the
reproducer should be the one that actually fails, not the one inferred from the
commit message
Sources:
- [Apache Groovy GitHub](https://github.com/apache/groovy)
- [Delegate (Groovy
5.0.5)](https://docs.groovy-lang.org/latest/html/api/groovy/lang/Delegate.html)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]