Hi all,
A follow-up to my previous mail, since I have been thinking about this
some more and also found some relevant older discussions.
I think the `asType` example is actually exposing a more general problem
than just `asType`.
My current understanding is that if we say that an extension method is
conceptually similar to an instance method, then ideally it should
participate in dispatch in roughly the same way.
For example, suppose:
```
class A {
}
class B extends A {
def foo() {
"instance"
}
}
```
and there is an extension method:
```
foo(A)
```
Then for:
```
A x = new B()
x.foo()
```
I would expect the instance method on B to win, even though the static
type of x is A.
Likewise, if:
```
class C extends B {
}
```
and there is an extension method:
```
foo(C)
```
then for a runtime C I would expect the more specific C extension to win
over the A extension.
This is not usually a practical problem because most extension methods
are on interfaces or base classes which are not subsequently extended
with an instance method of the same signature. But it is still the
semantics I would expect if extension methods are supposed to behave
like methods added to the receiver.
There is an important qualification here, though: I don't think the
answer can simply be "use normal Groovy dynamic dispatch".
For `@CompileStatic`, I think it is generally desirable that arbitrary
MetaClass changes do not influence the compiler's decisions. That is one
of the reasons static compilation exists in the first place. Categories
and other runtime metaprogramming mechanisms raise similar questions.
So I am not suggesting that we should turn every extension-method call
under static compilation into a normal Groovy dynamic call site.
The problem is that, once we exclude the full dynamic MOP semantics, I
am not sure what the correct replacement semantics are.
For ordinary instance methods, the JVM already gives us the required
mechanism:
```
invokevirtual
```
The compiler can statically select A.foo as the symbolic target, while
the runtime receiver still determines which override is executed.
For an extension method, however, we have an `invokestatic`. Once we
have selected:
```
DGM.foo(A, x)
```
we have effectively already decided that this is the extension method to
call.
We can make extension-method selection smarter and, for example,
generate a dispatch table based on the runtime type of x. But that still
only dispatches between extension methods.
It cannot recover the possibility that x has an instance method which
should have taken precedence.
And if extension methods can come from multiple extension modules, there
is another problem: what is the complete set of extension methods which
participates in this dispatch in the first place?
This made me look back at some of the earlier discussions.
**GROOVY-8788** was already about making extension-method selection
under `@CompileStatic` agree with the dynamic runtime. Eric's change
made the static compiler prefer the same extension method as the dynamic
runtime, and the change was noted as potentially breaking:
https://issues.apache.org/jira/browse/GROOVY-8788
The discussion also makes the more general principle explicit: the
static compiler should behave consistently with dynamic Groovy in terms
of what extension methods mean.
But I think GROOVY-8788 only solves the extension-vs-extension part of
the problem. It does not solve instance-method-vs-extension-method
dispatch where the runtime receiver is a subtype of the static type.
I also realized that I had already started down a related path in
January in the `asBoolean` discussion:
https://www.mail-archive.com/dev%40groovy.apache.org/msg07596.html
There the question was whether Groovy Truth should effectively always
mean `foo.asBoolean()`, rather than having the compiler replace that
operation with various static/primitive/null fast paths.
That discussion also brought up using `invokedynamic` for the cases
where runtime dispatch is actually needed.
Looking at `asType` now, I think the issue is broader: even without
MetaClass changes, choosing the extension method statically can already
lose information which is relevant to the eventual dispatch.
There is another interesting comparison with the recent `*Assign` work /
GEP-15:
https://www.mail-archive.com/commits%40groovy.apache.org/msg27325.html
There the language-level operation is explicitly allowed to resolve to
an instance method, extension method or category method. Under static
compilation the proposal uses `findMethod()` and emits a direct call,
whereas dynamic Groovy uses the MOP.
That seems reasonable if static compilation is allowed to make the
operation static. But it also illustrates the question I am now running
into:
**when is `findMethod()` actually sufficient to represent the Groovy
semantics, and when does the operation inherently require some form of
runtime dispatch?**
For me this leaves roughly these possibilities:
1. We deliberately define extension methods under static compilation as
statically selected methods. This is simple and fast, but then extension
methods are not really virtual-like in the way I would intuitively expect.
2. We define a restricted virtual-like dispatch model for extension
methods. The runtime receiver can then influence the result, but only
within a precisely defined set of candidates. This would avoid full MOP
semantics, but we would need to define the candidate set and precedence
very carefully.
3. We emit a specialized dynamic call site for cases where the runtime
type can affect the result. This would still be dynamic semantically,
but could potentially be optimized/cached using `invokedynamic`. The
question would then be exactly which parts of Groovy's dynamic semantics
such a call site is supposed to implement.
4. We accept that `@CompileStatic` has a different semantic contract
here and document the differences.
The problem I see with (2) is that extension modules make this an
open-world problem.
If module M1 provides:
```
foo(A)
```
and module M2 provides:
```
foo(B)
```
then a compiler which only knows about M1 cannot necessarily construct
the complete dispatch hierarchy.
If we also allow an instance method on B to participate, we effectively
have to combine:
```
runtime class hierarchy
+ instance methods
+ extension methods
+ extension modules
```
into one dispatch decision, while deliberately *not* necessarily
including the full MetaClass/category machinery.
At that point I don't see how an ordinary `invokestatic` can preserve
the semantics.
On the other hand, I also don't think "just make it dynamic" is a
satisfactory answer for `@CompileStatic`, because arbitrary runtime
MetaClass changes should not normally be able to invalidate the static
compilation model.
So perhaps the interesting question is whether there is a well-defined
middle ground: a **static-compilation-specific dispatch model for
extension methods**, which has enough runtime information to preserve
virtual-like extension semantics but is still closed and deterministic
with respect to the parts of Groovy metaprogramming that
`@CompileStatic` is supposed to ignore.
I am particularly interested in what people think the intended contract
actually is here.
In particular:
* Is the intended model for extension methods under `@CompileStatic`
fundamentally "static overload resolution", or should they be considered
virtual-like methods for purposes of dispatch?
* If they are virtual-like, what should the precedence be between an
instance method and an extension method when the instance method only
exists on a runtime subtype?
* What should happen when the more specific extension method comes from
a different extension module?
* Is there already an established design decision around this which I am
missing?
* And, specifically for `asType` / `asBoolean`, should we regard these
as special language operations which are allowed to use a restricted
form of dynamic dispatch under static compilation?
There may well be historical context that is no longer obvious from the
current code or Jira issues.
My current feeling is that `asType` is only the symptom. The underlying
question is what semantics we actually want extension methods to have
under static compilation, and whether those semantics can be represented
by static method selection at all.
bye
Jochen