[
https://issues.apache.org/jira/browse/GROOVY-12293?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18107616#comment-18107616
]
Paul King commented on GROOVY-12293:
------------------------------------
AI thoughts:
{quote}Thanks for raising this. The error-consistency gaps on the Groovy side
are being handled in the linked GROOVY-12294; some thoughts on the remaining
parts below.
h5. The Spock {{@Use}} case
That one can't be made consistent from the Groovy side, I'm afraid. Spock's
{{@Use}} is a runtime extension ({{{}UseExtension{}}} wraps the feature
execution in {{GroovyCategorySupport.use}} via an interceptor), so the compiler
never sees a lexical {{use()}} call — there is simply no marker STC could key
off, which is why you get the plain "cannot find matching method" error. If
your STC transform pre-expanded {{@Use}} annotations into lexical \{{use(...) {
}}} blocks before the type checker runs, you'd at least get the category error
consistently — and it would automatically pick up any future STC support for
categories.
h5. Why lifting the limitation isn't just method resolution
The category methods could indeed be resolved statically — {{@Category}} output
is exactly extension-method shape ({{{}static m(TargetType $this, ...){}}}),
which the static compiler already knows how to call directly. The hard part is
that it would be a semantic change, not just a resolution change:
* {{use()}} is _dynamically_ (thread) scoped, not lexically scoped: while the
block is open, any dynamic dispatch on that thread — including inside methods
called from the block, and in third-party code — sees the category methods. A
compile-time version can only ever cover the lexically visible calls.
* Dynamic {{use()}} is late-bound: method existence and overload selection are
decided per call against whatever version of the category class is on the
_runtime_ classpath. Freezing at compile time pins selection (Java-linkage-like
semantics; version skew surfaces as {{NoSuchMethodError}} instead of silently
different dispatch). That's the same trade extension modules already made, so
arguably acceptable — but it is a behavioral difference from the dynamic
construct being mimicked.
* Precedence inverts: at runtime, category methods _shadow_ existing instance
methods; under STC, extension methods _lose_ to declared instance methods (and
the runtime tie rules are already murky, cf. GROOVY-8121). Faithful freezing
needs a category-shadows-instance rule the type checker doesn't currently have.
* It could only work for statically recognizable forms (class literals +
closure literal) — \{{use(someVar) { }}} would remain an error.
h5. What works today
A {{@Category}} class is already a valid extension class — the same class can
be registered as an extension module with a one-line
{{META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule}} descriptor, and
then works under {{{}@TypeChecked{}}}/{{{}@CompileStatic{}}} (globally rather
than scoped, resolved to direct static calls). For mixed codebases there's also
the type-checking-extension route ({{{}makeDynamic{}}}) with the {{use()}} call
kept in dynamic code.
h5. Longer term
There's real, long-standing demand for scoped, static-compilation-friendly
extension methods — which is essentially what a statically-resolved {{use()}}
would be:
* GROOVY-2521 ({{{}use{}}} with import-like locality, 2008)
* GROOVY-11143 (extension methods with static-import locality, 2023)
* the {{@Use}} annotation asks (GROOVY-3387, GROOVY-8417)
* JRuby-like parity: Ruby's refinements do exactly this (lexically scoped
augmentation), though dynamic-only — Groovy could do it
static-compilation-friendly
* this issue
With Groovy 6 close to feature freeze, my current thinking is that this belongs
in a proper design discussion for Groovy 7 rather than special-casing {{use()}}
blocks under STC now. A declarative mechanism would likely also serve the Spock
case better than block recognition, since {{@Use}} could map onto it directly.
To make that concrete, a potential shape to support something like what your
are after is an annotation on a class or script, naming one or more extension
classes — anything in category/extension-module shape, i.e. public static
methods whose first parameter is the receiver type, which is exactly what
{{@Category}} already generates:
{code:groovy}
@UseExtensions(StringExtras)
class MyService {
def greet(String name) { name.titleCase() } // resolves to
StringExtras.titleCase(String)
}
{code}
Within the annotated scope (and only there), the named classes' methods would
participate in method resolution exactly as extension-module methods do today —
the type checker sees them, and {{@CompileStatic}} compiles them to direct
static calls, so there's no runtime registration, no metaclass mutation, and no
thread-scoped {{use()}} machinery. Effectively it's the existing
extension-module mechanism with the global registration replaced by a lexical
opt-in: same method shape, same resolution rules, same codegen — just scoped to
the annotated unit instead of every compilation unit sharing the classpath.
Because resolution can work from the {{{}ClassNode{}}}, it would also work for
extension classes compiled in the same compilation unit, which the
descriptor-based mechanism can't do.
The open design questions are mostly about dynamic code rather than static:
what a _dynamic_ method call inside the annotated scope should see (nothing
extra? a per-call fallback through the category machinery? — the former is
simplest and still covers the static-compilation use case that motivates the
feature), and precedence when an extension method collides with a real instance
method (runtime categories shadow instance methods; extension methods today
defer to them — the deferring rule is probably the right one for a compile-time
feature). One cautionary tale worth keeping in mind for the dynamic answer:
whatever we pick shouldn't reintroduce what -{{@Mixin}}- gave us — that
transform just injected a {{metaClass.mixin(...)}} call, i.e. unscoped runtime
metaclass mutation leaking well beyond the annotated code, which is a big part
of why it was deprecated in favour of traits. The scoping must stay lexical
even on the dynamic path, or be absent there. Ruby's refinements answer the
scoping question with lexical-only (not inherited, not transitive), which is
likely the right default here too. None of this needs new syntax — it's
resolver and type-checker work only — which is what makes an incubating
annotation plausible as a first step, with first-class {{{}import
extension{}}}-style syntax as a possible later refinement if the semantics
prove out.
{quote}
> STC with category classes or at least consistent error
> ------------------------------------------------------
>
> Key: GROOVY-12293
> URL: https://issues.apache.org/jira/browse/GROOVY-12293
> Project: Groovy
> Issue Type: Improvement
> Reporter: Björn Kautler
> Priority: Major
>
> If you use a category like `use(...) { ... }` the STC throws a compile error,
> that usage of categories is not possible with STC due to their dynamic nature.
> It would be nice if this limitation could be lifted. Why can't the category
> method not be resolved like other methods and under static compilation used
> directly?
> I probably miss something why this is not possible, but maybe then at least a
> consistent error could somehow be issued. If I have a Spock specification
> under STC (there is a PR to make this possible and I ported it to an AST
> transform that also makes it possible) and there have a `@Use(...)`
> annotation, then the compile error just says that the method is not found
> while expectation was that it was resolved from the category.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)