On 7/14/26 14:50, Paul King wrote:
Hi Jochen,
AI read below. It could be wrong. I am still pondering but won't get
time again until my tomorrow. So this is something to read in the
meantime.
take your time. I recently feel like I do reviews and comments on PRs in
100% of my spare time ;)
Some comments inline
[...]
1) The doCall-only contract has never held on the dynamic path.
I tested a Closure subclass declaring only `String call(String)` (no doCall)
across releases:
dynamic cl("foo") GDK collect(cl)
Groovy 3.0.21-25 works MissingMethodException
Groovy 4.0.32 works MissingMethodException
Groovy 5.0.6 works MissingMethodException
Groovy 6.0.0-alpha-1 works MissingMethodException
master (12164/65) works works
This is an obvious outcome. call(Object) delegates to call(Object...),
which looks for doCall. GDK does not see call(String), only
call(Object), thus the failure. Which is exactly why overloading call
methods in Closure was never really supported and only sometimes working
if you override the call method instead. But if you override
call(Object) and then do the call through call(Object...), it was still
failing, since the dispatch direction was designed the other way around.
The only choice left, if you want to avoid doCall, is then to override
call(Object...) instead. That aligns with the other mail I think. The
javadoc of Closure even mentions, that if you want to use the short form
like cl("foo") you have to provide a doCall method.
I will not deny that cl("foo") works, but I don`t think that was really
intended. More like accidentally worked and then nobody changed it.
[...] > That leaves a genuine decision rather than a bug report: either
custom call
overloads are accepted as the (now consistent) de-facto contract, or
doCall-only is enforced on BOTH paths — which is a behavioural break with
3.x-6.x dynamic dispatch.
Which is why I wanted to start a discussion in the first place.
[...]
2) On PackedClosure,
[...]
- one shared MetaClass for all packed closures: conceded, and it is the one
semantic difference that cannot be engineered away — per-literal metaclass
is definitionally what packing removes. Class-level metaClass changes on
PackedClosure would affect every packed closure. The delegate/
resolveStrategy axis is already fenced by the runtime guard; this axis
needs documenting as a caveat of the opt-in feature (and is one more reason
packing must stay off for code that plays MOP games).
the problem is that it will stay an opt-in feature, since it cannot
replace the regular construct for the same case.
- "the call path seems to be the only way to invoke the real method": true,
but I'd frame what that path now IS: a statically compiled, metaclass-free,
inlinable route (adapter -> per-arity dispatch table -> direct invocation of
the typed hoisted body). For statically compiled callers, invokevirtual
call() on a packed closure reaches a typed body with no MOP involvement at
all — which is the closest thing we currently have to your point (6),
since the typed doCall of a closure class is hidden behind Closure<T> for
static compilation, but a packed closure's entry is structurally fixed.
Recent measurements: packed closures now run 1.8-4.7x FASTER than generated
closure classes on capturing shapes with byte-identical behaviour, largely
because of this property.
With GeneratedClosure the class is part of the same package and module
of the class it was declared in. So if a class of module A gets an
instance of GeneratedClosure from module B, and I do a dynamic call from
A to B using that Closure, then the call will be a call from A via
doCall into B, requiring JPMS to allow it. Now in case of PackedClosure,
as long as we not bypass the call method and use the target directly, we
effectively make it a call from A into <Groovy Runtime> into B.
Bypassing any possible restriction between A and B. Of course I am only
talking about A and B as modules written in Groovy. And yes, this is
currently a theoretical case.
Now... the other part to think about is what happens if the MOP
involvement is intended? Like if EMC is used. Do we say it does not work
if PackedClosure is enabled? As I said, that probably disqualifies it as
an incubation feature that will become a standard without flag. If, on
the other hand, we move the call code into a special metaclass, we
should still achieve similar results in performance while we ensure a
potential MOP path stays more open. And we can enable potential direct
invocation paths with invokedynamic. Plus, most likely, optimizing that
path will also suggest similar optimizations for ClosureMetaClass. Then
the main difference between the two would be that PackedClosure saves on
class loading overhead.
- one point in the architecture's favour under your criterion (2): dynamic
call sites invoking closures are megamorphic across per-literal closure
classes, but monomorphic on the single PackedClosure class — the per-target
selection moves into a switch the JIT handles well. Indy caching gets
easier, not harder.
which is why I would like to have this as standard feature, not as opt-in
[...]
bye Jochen