[
https://issues.apache.org/jira/browse/GROOVY-12234?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-12234:
-------------------------------
Description:
Dynamic Groovy 5 cannot run in a GraalVM native image. Since indy became the
sole compilation target (Groovy 3+ default, Groovy 5+ only option without the
opt-in {{groovy-callsite}} module), every dynamic method call, property access,
and operator links through {{IndyInterface.bootstrap}} — and that linkage dies
inside an image. The current workaround is to compile the application *and
every Groovy dependency* with {{-Dgroovy.target.indy=false}} plus
{{groovy-callsite}}, which is impossible for jars you do not build — including
the Groovy distribution's own Groovy-compiled modules (e.g.
{{groovy-http-builder}}), so even fully {{@CompileStatic}} applications crash
the moment a dependency crosses one dynamic site:
{noformat}
java.lang.BootstrapMethodError: ...
com.oracle.svm.core.jdk.UnsupportedFeatureError: Unsupported method
java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(CallSite,
MethodHandle)
{noformat}
A capability probe (GraalVM CE 25.2.4, runtime-created handles) shows the wall
is narrow. Everything Groovy's indy runtime composes works natively —
{{Lookup.findStatic}}, {{invokeExact}}, {{asType}}/{{asSpreader}},
{{insertArguments}}, {{guardWithTest}}, {{catchException}}, SwitchPoint
creation and guarding, {{MutableCallSite}} creation and dispatch,
{{ConstantCallSite}}. Exactly two primitives fail, and they are the same
operation: *retargeting an existing call site* ({{MutableCallSite.setTarget}}
and {{SwitchPoint.invalidateAll}}). Worse, after the error the site keeps
dispatching through the stale target, so catch-and-continue is semantically
wrong — mutation cannot be papered over; it has to be designed out.
h2. Key insight
In Groovy's indy design those two primitives only ever *install* or *invalidate
caches*. The dispatch semantics live entirely in method selection
({{selectMethod}} / the PIC in {{CacheableCallSite}}); a site that never
retargets is slower, never wrong. The runtime already carries most of the
data-side machinery an AOT mode needs: the per-site LRU PIC, {{defaultTarget}}
(the cache-consulting path), and the reflective cold tier (GROOVY-12137).
h2. The fix: AOT link mode
Under AOT —
{{"runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))}},
or {{-Dgroovy.indy.aot.link=true}} as a diagnostic knob so the whole mode is
testable on a plain JVM — the bootstrap links each site *once, permanently*:
* The site becomes a {{ConstantCallSite}} whose target is one bound handle into
{{aotDispatch}}: PIC lookup (a new allocation-free
{{CacheableCallSite.getIfPresent}}), freshness check, and invocation, all in
ordinary compiled Java. The {{CacheableCallSite}} is never installed as the
call site — it is the state carrier (PIC, fallback target) — and its
{{setTarget}} fails fast in AOT mode so a missed gate surfaces on the JVM under
the knob.
* Cache freshness moves from SwitchPoint guards (which can never fire natively)
to a global invalidation stamp ({{AotDispatch}}): every invalidation path — all
three {{SwitchPoint.invalidateAll}} call sites now funnel through
{{AotDispatch.invalidateAll}} — advances the stamp; each cached
{{MethodHandleWrapper}} captures it at selection; a mismatch on a PIC hit is a
miss and re-selects. Coarser than GROOVY-12191's scoped invalidation, which is
safe: over-invalidation re-selects, staleness is impossible.
* The reflective cold tier (GROOVY-12137) is the AOT steady state — promotion
to full method-handle chains is gated off, since chains run in the native MH
interpreter while reflective dispatch uses AOT-compiled invocation stubs.
The JVM path is unchanged: the mode is decided per link and never cached in
statics (native image may initialize classes at build time, where the imagecode
property reports {{buildtime}} — caching would bake the wrong answer into the
image heap); hot paths read a site-local flag captured at link time; the stamp
is written but never read outside AOT mode.
*GraalVM bug found en route:* the native-image runtime invokedynamic path
invokes a bootstrap method *without running its declaring class's <clinit>* —
{{IndyInterface.bootstrap}} executed against null static finals. An ordinary
cross-class {{getstatic}} carries the missing initialization barrier (and
{{Class.forName(initialize=true)}} does not recover), so every BSM entry calls
{{ensureInitialized()}}, which triggers initialization through a helper class's
field read. Minimal repro available; to be filed upstream alongside the narrow
retargeting ask (runtime-created sites are never constant-folded into AOT code,
so {{setTarget}} on them is morally a volatile store).
h2. What this enables
*Every already-published indy-compiled jar becomes native-capable without
recompilation* — the BSM reference in existing class files is
{{IndyInterface.bootstrap}}, so Groovy controls linking entirely from the
runtime side. That includes the distribution's own modules: the
{{groovy-http-builder}} {{BootstrapMethodError}} above simply disappears.
h2. Verification
||check||result||
|20-scenario dynamic gauntlet: property read/write, closures/GDK, polymorphic
dispatch through one site, ExpandoMetaClass change observed by an already-hot
site, per-instance metaclass, category enter/leave with both transitions,
operators, BigDecimal|passes on JVM normal mode, JVM AOT mode (knob), and *in a
native image built from stock-indy class files with only agent-recorded
metadata* — no extra flags|
|category / EMC / metaclass-registry / indy JVM suites|1,529 tests, 0 failures|
|zero retargets in AOT mode|enforced by the fail-fast {{setTarget}}, exercised
across the gauntlet under the knob|
|real-world: the fully dynamic
[GroovyPolicyMCP|https://github.com/paulk-asert/GroovyPolicyMCP] MCP server,
unmodified, stock indy compilation|builds and serves all four tools natively,
including a live HTTPS fetch through dynamically-compiled
{{groovy-http-builder}}|
Measured against the same server hand-converted to {{@CompileStatic}} +
indy-off + {{groovy-callsite}} (warm runs, GraalVM CE 25.2.4):
||metric||JVM dynamic||native static conversion||native dynamic (this mode)||
|ready after spawn|434 ms|23 ms|24–27 ms|
|get_policy|115 ms|6 ms|8–14 ms|
|search_policies|79 ms|8 ms|242 ms|
|refresh_cache (HTTPS)|342 ms|*BootstrapMethodError*|82–87 ms|
|binary size| |66.0 MB|55.8 MB|
The dynamic image is the first of the two with all four tools working: the
static conversion's network path still crossed indy sites in the distribution
jars.
h2. Performance model and known limitation
Layer-by-layer native bisection: AOT-compiled reflection stubs run at ~10 ns,
but *invoking any runtime-created MethodHandle costs ~4.5 us* — a per-entry
interpreter cost, independent of chain depth, adapter count, or {{invokeExact}}
vs {{invoke}}. Each dynamic call site's invokedynamic hop into its
runtime-linked target pays it once per call, so a workload crossing N dynamic
sites costs ~N x 4 us natively. Rule of thumb: ~1,000 dynamic ops per action ≈
4 ms (invisible for CLI/script/MCP workloads); hot loops crossing tens of
thousands of sites show it plainly (search_policies above: ~60k ops ≈ 242 ms,
matching the model).
This floor is structural (the indy-to-runtime-target boundary) and not fixable
from Groovy's side; the escapes are {{@CompileStatic}} for hot code (statically
compiled code emits no per-operation indy sites and is unaffected), a
build-time rewrite of indy sites to {{invokestatic aotDispatch}} (removes the
runtime-MH boundary entirely; the shallow dispatcher is deliberately shaped as
its target), and upstream GraalVM work on compiling runtime-linked code
(Ristretto/Project Crema).
h2. Scope and follow-ups (not this ticket)
* Experimental, spike-quality; proposed behind its current automatic detection
with the diagnostic knob for CI.
* Runtime *compilation* ({{GroovyShell}}/{{Eval}}) remains out of reach until
GraalVM's dynamic class loading matures; runtime proxy generation for abstract
classes likewise.
* Follow-ups: per-class stamps hung off {{ClassInfo}} (whose version counter
already exists) to reduce over-invalidation; a {{-Dgroovy.indy.aot.stats}}
site-crossing counter so users can measure which zone they are in; the de-indy
build-step rewrite tool; the two upstream GraalVM filings; shipping Groovy's
own reachability metadata so the agent step shrinks.
Prototype branch with three commits available; happy to split delivery into the
invalidation funnel, the link mode, and the dispatcher if that eases review.
was:
Dynamic Groovy cannot run in a GraalVM native image. Since indy became the sole
compilation target (Groovy 3+ default, Groovy 5+ only option without the opt-in
{{groovy-callsite}} module), every dynamic method call, property access, and
operator links through {{IndyInterface.bootstrap}} — and that linkage dies
inside an image. The current workaround is to compile the application *and
every Groovy dependency* with {{-Dgroovy.target.indy=false}} plus
{{groovy-callsite}}, which is impossible for jars you do not build — including
the Groovy distribution's own Groovy-compiled modules (e.g.
{{groovy-http-builder}}), so even fully {{@CompileStatic}} applications crash
the moment a dependency crosses one dynamic site:
{noformat}
java.lang.BootstrapMethodError: ...
com.oracle.svm.core.jdk.UnsupportedFeatureError: Unsupported method
java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(CallSite,
MethodHandle)
{noformat}
A capability probe (GraalVM CE 25.2.4, runtime-created handles) shows the wall
is narrow. Everything Groovy's indy runtime composes works natively —
{{Lookup.findStatic}}, {{invokeExact}}, {{asType}}/{{asSpreader}},
{{insertArguments}}, {{guardWithTest}}, {{catchException}}, SwitchPoint
creation and guarding, {{MutableCallSite}} creation and dispatch,
{{ConstantCallSite}}. Exactly two primitives fail, and they are the same
operation: *retargeting an existing call site* ({{MutableCallSite.setTarget}}
and {{SwitchPoint.invalidateAll}}). Worse, after the error the site keeps
dispatching through the stale target, so catch-and-continue is semantically
wrong — mutation cannot be papered over; it has to be designed out.
h2. Key insight
In Groovy's indy design those two primitives only ever *install* or *invalidate
caches*. The dispatch semantics live entirely in method selection
({{selectMethod}} / the PIC in {{CacheableCallSite}}); a site that never
retargets is slower, never wrong. The runtime already carries most of the
data-side machinery an AOT mode needs: the per-site LRU PIC, {{defaultTarget}}
(the cache-consulting path), and the reflective cold tier (GROOVY-12137).
h2. The fix: AOT link mode
Under AOT —
{{"runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))}},
or {{-Dgroovy.indy.aot.link=true}} as a diagnostic knob so the whole mode is
testable on a plain JVM — the bootstrap links each site *once, permanently*:
* The site becomes a {{ConstantCallSite}} whose target is one bound handle into
{{aotDispatch}}: PIC lookup (a new allocation-free
{{CacheableCallSite.getIfPresent}}), freshness check, and invocation, all in
ordinary compiled Java. The {{CacheableCallSite}} is never installed as the
call site — it is the state carrier (PIC, fallback target) — and its
{{setTarget}} fails fast in AOT mode so a missed gate surfaces on the JVM under
the knob.
* Cache freshness moves from SwitchPoint guards (which can never fire natively)
to a global invalidation stamp ({{AotDispatch}}): every invalidation path — all
three {{SwitchPoint.invalidateAll}} call sites now funnel through
{{AotDispatch.invalidateAll}} — advances the stamp; each cached
{{MethodHandleWrapper}} captures it at selection; a mismatch on a PIC hit is a
miss and re-selects. Coarser than GROOVY-12191's scoped invalidation, which is
safe: over-invalidation re-selects, staleness is impossible.
* The reflective cold tier (GROOVY-12137) is the AOT steady state — promotion
to full method-handle chains is gated off, since chains run in the native MH
interpreter while reflective dispatch uses AOT-compiled invocation stubs.
The JVM path is unchanged: the mode is decided per link and never cached in
statics (native image may initialize classes at build time, where the imagecode
property reports {{buildtime}} — caching would bake the wrong answer into the
image heap); hot paths read a site-local flag captured at link time; the stamp
is written but never read outside AOT mode.
*GraalVM bug found en route:* the native-image runtime invokedynamic path
invokes a bootstrap method *without running its declaring class's <clinit>* —
{{IndyInterface.bootstrap}} executed against null static finals. An ordinary
cross-class {{getstatic}} carries the missing initialization barrier (and
{{Class.forName(initialize=true)}} does not recover), so every BSM entry calls
{{ensureInitialized()}}, which triggers initialization through a helper class's
field read. Minimal repro available; to be filed upstream alongside the narrow
retargeting ask (runtime-created sites are never constant-folded into AOT code,
so {{setTarget}} on them is morally a volatile store).
h2. What this enables
*Every already-published indy-compiled jar becomes native-capable without
recompilation* — the BSM reference in existing class files is
{{IndyInterface.bootstrap}}, so Groovy controls linking entirely from the
runtime side. That includes the distribution's own modules: the
{{groovy-http-builder}} {{BootstrapMethodError}} above simply disappears.
h2. Verification
||check||result||
|20-scenario dynamic gauntlet: property read/write, closures/GDK, polymorphic
dispatch through one site, ExpandoMetaClass change observed by an already-hot
site, per-instance metaclass, category enter/leave with both transitions,
operators, BigDecimal|passes on JVM normal mode, JVM AOT mode (knob), and *in a
native image built from stock-indy class files with only agent-recorded
metadata* — no extra flags|
|category / EMC / metaclass-registry / indy JVM suites|1,529 tests, 0 failures|
|zero retargets in AOT mode|enforced by the fail-fast {{setTarget}}, exercised
across the gauntlet under the knob|
|real-world: the fully dynamic
[GroovyPolicyMCP|https://github.com/paulk-asert/GroovyPolicyMCP] MCP server,
unmodified, stock indy compilation|builds and serves all four tools natively,
including a live HTTPS fetch through dynamically-compiled
{{groovy-http-builder}}|
Measured against the same server hand-converted to {{@CompileStatic}} +
indy-off + {{groovy-callsite}} (warm runs, GraalVM CE 25.2.4):
||metric||JVM dynamic||native static conversion||native dynamic (this mode)||
|ready after spawn|434 ms|23 ms|24–27 ms|
|get_policy|115 ms|6 ms|8–14 ms|
|search_policies|79 ms|8 ms|242 ms|
|refresh_cache (HTTPS)|342 ms|*BootstrapMethodError*|82–87 ms|
|binary size| |66.0 MB|55.8 MB|
The dynamic image is the first of the two with all four tools working: the
static conversion's network path still crossed indy sites in the distribution
jars.
h2. Performance model and known limitation
Layer-by-layer native bisection: AOT-compiled reflection stubs run at ~10 ns,
but *invoking any runtime-created MethodHandle costs ~4.5 us* — a per-entry
interpreter cost, independent of chain depth, adapter count, or {{invokeExact}}
vs {{invoke}}. Each dynamic call site's invokedynamic hop into its
runtime-linked target pays it once per call, so a workload crossing N dynamic
sites costs ~N x 4 us natively. Rule of thumb: ~1,000 dynamic ops per action ≈
4 ms (invisible for CLI/script/MCP workloads); hot loops crossing tens of
thousands of sites show it plainly (search_policies above: ~60k ops ≈ 242 ms,
matching the model).
This floor is structural (the indy-to-runtime-target boundary) and not fixable
from Groovy's side; the escapes are {{@CompileStatic}} for hot code (statically
compiled code emits no per-operation indy sites and is unaffected), a
build-time rewrite of indy sites to {{invokestatic aotDispatch}} (removes the
runtime-MH boundary entirely; the shallow dispatcher is deliberately shaped as
its target), and upstream GraalVM work on compiling runtime-linked code
(Ristretto/Project Crema).
h2. Scope and follow-ups (not this ticket)
* Experimental, spike-quality; proposed behind its current automatic detection
with the diagnostic knob for CI.
* Runtime *compilation* ({{GroovyShell}}/{{Eval}}) remains out of reach until
GraalVM's dynamic class loading matures; runtime proxy generation for abstract
classes likewise.
* Follow-ups: per-class stamps hung off {{ClassInfo}} (whose version counter
already exists) to reduce over-invalidation; a {{-Dgroovy.indy.aot.stats}}
site-crossing counter so users can measure which zone they are in; the de-indy
build-step rewrite tool; the two upstream GraalVM filings; shipping Groovy's
own reachability metadata so the agent step shrinks.
Prototype branch with three commits available; happy to split delivery into the
invalidation funnel, the link mode, and the dispatcher if that eases review.
> Indy: AOT link mode so dynamic Groovy dispatch works in GraalVM native images
> -----------------------------------------------------------------------------
>
> Key: GROOVY-12234
> URL: https://issues.apache.org/jira/browse/GROOVY-12234
> Project: Groovy
> Issue Type: Improvement
> Reporter: Paul King
> Assignee: Paul King
> Priority: Major
>
> Dynamic Groovy 5 cannot run in a GraalVM native image. Since indy became the
> sole compilation target (Groovy 3+ default, Groovy 5+ only option without the
> opt-in {{groovy-callsite}} module), every dynamic method call, property
> access, and operator links through {{IndyInterface.bootstrap}} — and that
> linkage dies inside an image. The current workaround is to compile the
> application *and every Groovy dependency* with {{-Dgroovy.target.indy=false}}
> plus {{groovy-callsite}}, which is impossible for jars you do not build —
> including the Groovy distribution's own Groovy-compiled modules (e.g.
> {{groovy-http-builder}}), so even fully {{@CompileStatic}} applications crash
> the moment a dependency crosses one dynamic site:
> {noformat}
> java.lang.BootstrapMethodError: ...
> com.oracle.svm.core.jdk.UnsupportedFeatureError: Unsupported method
> java.lang.invoke.MethodHandleNatives.setCallSiteTargetNormal(CallSite,
> MethodHandle)
> {noformat}
> A capability probe (GraalVM CE 25.2.4, runtime-created handles) shows the
> wall is narrow. Everything Groovy's indy runtime composes works natively —
> {{Lookup.findStatic}}, {{invokeExact}}, {{asType}}/{{asSpreader}},
> {{insertArguments}}, {{guardWithTest}}, {{catchException}}, SwitchPoint
> creation and guarding, {{MutableCallSite}} creation and dispatch,
> {{ConstantCallSite}}. Exactly two primitives fail, and they are the same
> operation: *retargeting an existing call site* ({{MutableCallSite.setTarget}}
> and {{SwitchPoint.invalidateAll}}). Worse, after the error the site keeps
> dispatching through the stale target, so catch-and-continue is semantically
> wrong — mutation cannot be papered over; it has to be designed out.
> h2. Key insight
> In Groovy's indy design those two primitives only ever *install* or
> *invalidate caches*. The dispatch semantics live entirely in method selection
> ({{selectMethod}} / the PIC in {{CacheableCallSite}}); a site that never
> retargets is slower, never wrong. The runtime already carries most of the
> data-side machinery an AOT mode needs: the per-site LRU PIC,
> {{defaultTarget}} (the cache-consulting path), and the reflective cold tier
> (GROOVY-12137).
> h2. The fix: AOT link mode
> Under AOT —
> {{"runtime".equals(System.getProperty("org.graalvm.nativeimage.imagecode"))}},
> or {{-Dgroovy.indy.aot.link=true}} as a diagnostic knob so the whole mode is
> testable on a plain JVM — the bootstrap links each site *once, permanently*:
> * The site becomes a {{ConstantCallSite}} whose target is one bound handle
> into {{aotDispatch}}: PIC lookup (a new allocation-free
> {{CacheableCallSite.getIfPresent}}), freshness check, and invocation, all in
> ordinary compiled Java. The {{CacheableCallSite}} is never installed as the
> call site — it is the state carrier (PIC, fallback target) — and its
> {{setTarget}} fails fast in AOT mode so a missed gate surfaces on the JVM
> under the knob.
> * Cache freshness moves from SwitchPoint guards (which can never fire
> natively) to a global invalidation stamp ({{AotDispatch}}): every
> invalidation path — all three {{SwitchPoint.invalidateAll}} call sites now
> funnel through {{AotDispatch.invalidateAll}} — advances the stamp; each
> cached {{MethodHandleWrapper}} captures it at selection; a mismatch on a PIC
> hit is a miss and re-selects. Coarser than GROOVY-12191's scoped
> invalidation, which is safe: over-invalidation re-selects, staleness is
> impossible.
> * The reflective cold tier (GROOVY-12137) is the AOT steady state — promotion
> to full method-handle chains is gated off, since chains run in the native MH
> interpreter while reflective dispatch uses AOT-compiled invocation stubs.
> The JVM path is unchanged: the mode is decided per link and never cached in
> statics (native image may initialize classes at build time, where the
> imagecode property reports {{buildtime}} — caching would bake the wrong
> answer into the image heap); hot paths read a site-local flag captured at
> link time; the stamp is written but never read outside AOT mode.
> *GraalVM bug found en route:* the native-image runtime invokedynamic path
> invokes a bootstrap method *without running its declaring class's <clinit>* —
> {{IndyInterface.bootstrap}} executed against null static finals. An ordinary
> cross-class {{getstatic}} carries the missing initialization barrier (and
> {{Class.forName(initialize=true)}} does not recover), so every BSM entry
> calls {{ensureInitialized()}}, which triggers initialization through a helper
> class's field read. Minimal repro available; to be filed upstream alongside
> the narrow retargeting ask (runtime-created sites are never constant-folded
> into AOT code, so {{setTarget}} on them is morally a volatile store).
> h2. What this enables
> *Every already-published indy-compiled jar becomes native-capable without
> recompilation* — the BSM reference in existing class files is
> {{IndyInterface.bootstrap}}, so Groovy controls linking entirely from the
> runtime side. That includes the distribution's own modules: the
> {{groovy-http-builder}} {{BootstrapMethodError}} above simply disappears.
> h2. Verification
> ||check||result||
> |20-scenario dynamic gauntlet: property read/write, closures/GDK, polymorphic
> dispatch through one site, ExpandoMetaClass change observed by an already-hot
> site, per-instance metaclass, category enter/leave with both transitions,
> operators, BigDecimal|passes on JVM normal mode, JVM AOT mode (knob), and *in
> a native image built from stock-indy class files with only agent-recorded
> metadata* — no extra flags|
> |category / EMC / metaclass-registry / indy JVM suites|1,529 tests, 0
> failures|
> |zero retargets in AOT mode|enforced by the fail-fast {{setTarget}},
> exercised across the gauntlet under the knob|
> |real-world: the fully dynamic
> [GroovyPolicyMCP|https://github.com/paulk-asert/GroovyPolicyMCP] MCP server,
> unmodified, stock indy compilation|builds and serves all four tools natively,
> including a live HTTPS fetch through dynamically-compiled
> {{groovy-http-builder}}|
> Measured against the same server hand-converted to {{@CompileStatic}} +
> indy-off + {{groovy-callsite}} (warm runs, GraalVM CE 25.2.4):
> ||metric||JVM dynamic||native static conversion||native dynamic (this mode)||
> |ready after spawn|434 ms|23 ms|24–27 ms|
> |get_policy|115 ms|6 ms|8–14 ms|
> |search_policies|79 ms|8 ms|242 ms|
> |refresh_cache (HTTPS)|342 ms|*BootstrapMethodError*|82–87 ms|
> |binary size| |66.0 MB|55.8 MB|
> The dynamic image is the first of the two with all four tools working: the
> static conversion's network path still crossed indy sites in the distribution
> jars.
> h2. Performance model and known limitation
> Layer-by-layer native bisection: AOT-compiled reflection stubs run at ~10 ns,
> but *invoking any runtime-created MethodHandle costs ~4.5 us* — a per-entry
> interpreter cost, independent of chain depth, adapter count, or
> {{invokeExact}} vs {{invoke}}. Each dynamic call site's invokedynamic hop
> into its runtime-linked target pays it once per call, so a workload crossing
> N dynamic sites costs ~N x 4 us natively. Rule of thumb: ~1,000 dynamic ops
> per action ≈ 4 ms (invisible for CLI/script/MCP workloads); hot loops
> crossing tens of thousands of sites show it plainly (search_policies above:
> ~60k ops ≈ 242 ms, matching the model).
> This floor is structural (the indy-to-runtime-target boundary) and not
> fixable from Groovy's side; the escapes are {{@CompileStatic}} for hot code
> (statically compiled code emits no per-operation indy sites and is
> unaffected), a build-time rewrite of indy sites to {{invokestatic
> aotDispatch}} (removes the runtime-MH boundary entirely; the shallow
> dispatcher is deliberately shaped as its target), and upstream GraalVM work
> on compiling runtime-linked code (Ristretto/Project Crema).
> h2. Scope and follow-ups (not this ticket)
> * Experimental, spike-quality; proposed behind its current automatic
> detection with the diagnostic knob for CI.
> * Runtime *compilation* ({{GroovyShell}}/{{Eval}}) remains out of reach until
> GraalVM's dynamic class loading matures; runtime proxy generation for
> abstract classes likewise.
> * Follow-ups: per-class stamps hung off {{ClassInfo}} (whose version counter
> already exists) to reduce over-invalidation; a {{-Dgroovy.indy.aot.stats}}
> site-crossing counter so users can measure which zone they are in; the
> de-indy build-step rewrite tool; the two upstream GraalVM filings; shipping
> Groovy's own reachability metadata so the agent step shrinks.
> Prototype branch with three commits available; happy to split delivery into
> the invalidation funnel, the link mode, and the dispatcher if that eases
> review.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)