Re: Script caching

Per Nyfelt <[email protected]> Wed, 3 Jun 2026 00:11:55 +0200
Newsgroups gmane.comp.lang.groovy.devel
Organization Alipsa HB
Message-ID <[email protected]>
Thanks for the feedback Paul!

Below is an updated proposal (also attached as an md). Next, I am going 
to create a wrapper script to validate as many assumption as i can. A 
benchmark should hopefully confirm that cache-hit time is meaningfully 
lower than fresh groovy invocation time, and that fingerprinting + 
class-loading overhead does not negate the saving.


  GEP-TBD: Persistent Script Compilation Cache for the Groovy
  Command-Line Launcher


    Metadata

Field 	Value
Number 	GEP-TBD
Title 	Persistent Script Compilation Cache for the Groovy Command-Line 
Launcher
Version 	0.2
Type 	Feature
Status 	Draft
Leader 	Per Nyfelt
Created 	2026-06-01
Last modification 	2026-06-02
Target Groovy version 	TBD
Discussion 	TBD
Reference implementation 	TBD


    Abstract

This proposal introduces an optional persistent compilation cache for 
Groovyscripts executed through the|groovy|command-line launcher.

When an eligible script is executed,the launcher may store the generated 
classfiles in a local cache.On later executions,if the script source and 
allrelevant compilation inputs are unchanged,the launcher may load the 
cachedclass files instead of parsing,transforming,and compiling the 
script again.

The goal is to improve startup time for short-lived Groovy scripts 
andcommand-line tools while preserving current semantics.The cache 
complementsJVM startup improvements such as CDS and Project Leyden;it is 
not intended toreplace them.


    Rationale

Groovy is frequently used for scripting,automation,build 
tooling,dataprocessing,and command-line utilities.In these use 
cases,process lifetime isoften short,and startup overhead matters.

For simple scripts,actual execution time may be negligible compared to:

  * JVM startup
  * loading the Groovy runtime
  * bootstrapping the metaclass registry
  * JIT warmup
  * parsing the script
  * applying AST transformations
  * bytecode generation
  * class loading

Recent and ongoing JVM work,such as CDS and Project Leyden,can reduce 
JVMstartup and class-loading overhead.A persistent script compilation 
cache doesnot remove JVM startup,Groovy runtime initialization,or JIT 
costs.Its directbenefit is narrower:it can avoid parsing,AST 
transformations,and bytecodegeneration for an unchanged script.A cache 
hit may also avoid loading parts ofthe compiler frontend.

The realistic performance ceiling is therefore the portion of wall-clock 
timespent compiling the script.That portion is likely to be smallest for 
trivialscripts and larger for scripts with substantial source or AST 
transformationwork.The actual benefit must be measured across 
representative scripts.

Groovy already caches compiled script classes during the lifetime of 
a|GroovyClassLoader|,but this cache is lost when the process 
exits.Apersistent launcher-level cache would allow repeated invocations 
of unchangedscripts to skip most compilation work.

This would make Groovy more attractive for command-line scripting and 
developertooling,especially when used alongside JVM startup improvements.


    Goals

The goals of this proposal are:

 1. Reduce repeated startup overhead for unchanged Groovy scripts.
 2. Avoid changing Groovy language semantics.
 3. Make the feature safe and conservative by default.
 4. Provide explicit ways to disable, clear, and inspect the cache.
 5. Ensure cache invalidation accounts for relevant compilation inputs.
 6. Allow future integration with JVM startup technologies such as CDS
    or Leyden-style caches.
 7. Preserve a fresh JVM for each invocation, without sharing mutable
    runtime state between script executions.


    Non-Goals

This proposal does not aim to:

 1. Replace|groovyc|.
 2. Change Groovy compilation semantics.
 3. Cache arbitrary runtime results.
 4. Guarantee improved performance for all scripts.
 5. Provide a distributed or shared build cache.
 6. Cache scripts run inside long-lived embedded Groovy runtimes.
 7. Solve dependency resolution caching for|@Grab|, although it must
    interact safely with it.
 8. Remove JVM startup, Groovy runtime initialization, or JIT warmup costs.
 9. Provide a resident JVM or daemon process.


    Proposed Behavior

When running:

|<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#>groovy 
myscript.groovy |

when the cache is enabled,the launcher may:

 1. Determine whether the script is eligible for caching.
 2. If eligible, compute a cache key from the script and compilation
    environment.
 3. Look for previously generated class files matching that key.
 4. If found and valid, load the cached classes.
 5. If the script is ineligible or no valid cache entry was found,
    compile the script normally and, if eligible, store the generated
    classes in the cache.

The user-visible behavior of the script must remain the same as if the 
scripthad been compiled from source during that invocation.


      Correctness-First Principle

The cache is an optimization.Reusing stale or incompatible bytecode is 
arelease-blocking defect.Whenever the launcher cannot establish that 
everyrelevant compilation input is unchanged,it must compile the script 
normallyinstead of reusing a cache entry.


      Initial Scope

The first implementation should be deliberately narrow.It should cache 
onlyfile-backed scripts executed through the|groovy|command-line 
launcher whenthe compilation environment can be fingerprinted reliably.

The initial implementation should treat the following as ineligible:

  * scripts using|@Grab|
  * scripts using externally supplied or dynamically mutated compiler
    configuration, including configuration scripts, that cannot be
    fingerprinted reliably
  * scripts affected by launcher configuration, such as|GROOVY_CONF|,
    that cannot be fingerprinted reliably
  * scripts affected by launcher startup hooks or JVM options when those
    hooks or options cannot be fingerprinted reliably
  * scripts affected by AST transformations that cannot be fingerprinted
    reliably
  * scripts with compilation inputs or dependencies that cannot be
    identified reliably

An ineligible script must be compiled normally.Ineligibility is a cache 
miss,not an error.


      Cache Location

The default cache location should follow platform conventions.

Suggested defaults:

Platform 	Cache location
Linux/Unix 
|$XDG_CACHE_HOME/groovy/script-cache|or|~/.cache/groovy/script-cache|
macOS 	|~/Library/Caches/Groovy/script-cache|
Windows 	|%LOCALAPPDATA%\Groovy\script-cache|

A system property or environment variable should allow overriding 
thelocation:

|<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#>groovy 
-Dgroovy.script.cache.dir=/path/to/cache myscript.groovy 
GROOVY_SCRIPT_CACHE_DIR=/path/to/cache groovy myscript.groovy |


      Enabling and Disabling

The cache should initially be opt-in unless the Groovy project decides 
theinvalidation model is sufficiently conservative for default use.

The proposed command-line options are:

|<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#>groovy 
--script-cache myscript.groovy groovy --no-script-cache myscript.groovy 
groovy --clear-script-cache |

|--clear-script-cache|clears all entries in the selected cache directory 
andexits.It does not require a script argument.

The proposed system properties are:

|<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#>-Dgroovy.script.cache=true 
-Dgroovy.script.cache=false -Dgroovy.script.cache.dir=/path/to/cache 
-Dgroovy.script.cache.maxSizeBytes=<bytes>|

When multiple mechanisms configure the same setting,the precedence 
should be:

 1. command-line option
 2. system property
 3. environment variable, such as|GROOVY_SCRIPT_CACHE_DIR|for the cache
    location
 4. platform default

Cache enablement remains opt-in 
through|--script-cache|or|-Dgroovy.script.cache=true|.

If the feature later proves safe and reliable,it could become enabled 
bydefault for normal file-based scripts.


      Cache Management

The cache must not grow without bound.The implementation should enforce 
aconfigurable size limit,such 
as|-Dgroovy.script.cache.maxSizeBytes=<bytes>|,and evict entries 
automatically when that limit is exceeded.The exact defaultlimit and 
eviction policy should be determined by the reference implementation.

Entries from older Groovy versions or cache format versions must be 
eligiblefor cleanup.Automatic cleanup should be best-effort and must not 
preventnormal script execution.Users can remove all entries in the 
selected cachedirectory explicitly with|--clear-script-cache|.

An optional cache-size summary or listing command is discussed underOpen 
Questions 
<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#open-questions>.


      Source Identity

Before computing a cache key for a file-backed script,the launcher 
shouldresolve the script to its canonical path.Relative paths should 
therefore beresolved against the current working directory,and symlink 
aliases shouldidentify the same source file.

This matches the existing file-backed|GroovyCodeSource|behavior.


      Package Declarations

File-backed scripts with package declarations should remain eligible 
forcaching.The source hash already accounts for the package 
declaration.Thecache entry must record the binary names of all generated 
classes so they canbe defined with the same names and packages on a 
cache hit.


      Cache Key

The cache key and validation metadata must include enough information to 
avoidreusing stale or incompatible bytecode.The fingerprint should 
beover-inclusive:uncertainty must cause a cache miss rather than reuse 
of anentry that might be stale.

At minimum,the key should include:

  * canonical script path
  * script source hash
  * cache format version
  * Groovy version and a fingerprint of the effective Groovy runtime
    distribution
  * Java version or class file target version
  * effective classpath
  * compiler configuration
  * invokedynamic setting
  * |--enable-preview|, the|groovy.preview.features|system property set
    by the current Groovy launchers when preview mode is enabled, and
    any related compiler or JVM options that affect generated bytecode
  * script base class
  * active AST transformations
  * classpath-discovered extension modules
  * effective launcher configuration,
    including|GROOVY_HOME|,|groovy.home|,|GROOVY_CONF|, and
    configuration scripts where relevant
  * relevant system properties that affect compilation

The effective classpath fingerprint must detect changes to 
extension-moduledescriptors and their implementation classes.Groovy 
discovers extensionmodules from both:

  * |META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule|
  * |META-INF/services/org.codehaus.groovy.runtime.ExtensionModule|

Launcher startup hooks and JVM options,such as 
relevant|JAVA_OPTS|,maychange compilation behavior indirectly.The 
fingerprint should representeffective compilation-affecting inputs after 
launcher processing rather thanblindly hashing every launcher 
environment variable or JVM option.Unrelatedruntime options should not 
create unnecessary cache misses.

The Groovy version alone is not sufficient to distinguish custom 
orvendor-modified distributions.The distribution fingerprint must 
detectchanges to Groovy runtime artifacts that can affect 
compilation,even when thereported Groovy version is 
unchanged.|GROOVY_HOME|and|groovy.home|should berepresented through 
their effective compilation inputs rather than treated aspath strings 
when the resulting distribution and configuration are equivalent.

The implementation must also account for dependent Groovy sources 
discoveredduring compilation.A changed sibling script must invalidate 
any cached scriptthat depends on it.|GroovyScriptEngine|already tracks 
source dependenciesduring compilation and should inform this design.

The first implementation should not cache scripts using|@Grab|.A 
laterimplementation may reconsider this if resolved 
dependencies,compile-timeclasspath mutation,and discovered 
transformations can be represented safely.

Fingerprinting must also be efficient enough to preserve the expected 
benefit.For example,a full content hash of every classpath entry may be 
safe but tooexpensive.The implementation should benchmark competing 
strategies and prefera cache miss when a cheap,reliable fingerprint is 
unavailable.


      Cache Contents

The cache should store:

  * generated|.class|files
  * binary names of all generated classes
  * metadata describing the compilation environment
  * cache format version
  * source hash
  * Groovy version
  * Groovy runtime distribution fingerprint
  * Java or class file target
  * classpath fingerprint
  * dependency metadata for discovered Groovy sources
  * timestamp of creation
  * last-access metadata and entry size where required by the eviction
    policy
  * optional diagnostic information

The cache format should be treated as internal and may change between 
Groovyversions.

Cached classes must retain equivalent|CodeSource|and 
protection-domainbehavior when they are loaded in a new process.


      Cache Entry Layout

Each cache entry should be a self-contained unit identified by a digest 
of itscache key.It should contain the metadata and every generated class 
file neededfor that script.Writers should create temporary entries on 
the same filesystemas the cache and publish complete entries atomically.

The implementation may shard entries by key prefix to avoid excessively 
largedirectories.The exact directory names,metadata encoding,and 
sharding schemeare internal cache-format details to be determined by the 
referenceimplementation.


      Invalidation

A cached script must be invalidated when any relevant compilation 
inputchanges.

Examples:

  * script source changed
  * Groovy version changed
  * Groovy runtime distribution changed without a version change
  * Java target changed
  * classpath changed
  * AST transform implementation changed
  * compiler configuration changed
  * |@Grab|dependencies changed, for a future implementation that
    enables|@Grab|caching
  * cache format changed
  * dependent Groovy source changed
  * extension-module descriptor or implementation changed
  * compilation-affecting launcher configuration or JVM option changed

If validation fails or metadata is unreadable,the launcher should 
silentlyfall back to normal compilation unless diagnostics are enabled.

As required by theCorrectness-First Principle 
<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#correctness-first-principle>,the 
implementation must default to a cache miss whenever it cannot 
establishthat all relevant compilation inputs are unchanged.


      Diagnostics

The launcher should provide optional diagnostics.

The proposed diagnostic options are:

|<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#>groovy 
--script-cache-info myscript.groovy groovy --script-cache-verbose 
myscript.groovy |

|--script-cache-info|should report whether the invocation was a 
hit,miss,orineligible for caching,together with a concise 
reason.|--script-cache-verbose|should include the same result plus 
detailed validation diagnostics,such asthe cache entry location and the 
compilation inputs that caused a miss or madethe script ineligible.

Example output:

|Groovy script cache: miss Reason: source hash changed |

or:

|Groovy script cache: hit Cache entry: ~/.cache/groovy/script-cache/... |

Diagnostics should be disabled by default to preserve normal script output.


    Security Considerations

The cache stores executable bytecode.Therefore:

 1. Cache entries should be private to the current user by default.
 2. The cache directory should not be world-writable.
 3. The launcher should avoid loading cache entries with unsafe permissions.
 4. Cache entries should be isolated by operating-system user. If a
    configured cache directory may be shared by multiple users, the
    launcher must use separate user-specific namespaces or disable caching.
 5. The cache should not weaken existing script security assumptions.
 6. Cache writes and reads should defend against symlink-based
    replacement and similar filesystem races where supported by the
    platform.
 7. Cached classes should retain equivalent|CodeSource|and
    protection-domain behavior when they are loaded in a new process.

The canonical script path and source hash distinguish scripts from 
differentprojects.User isolation should come from cache-directory 
scoping andpermissions rather than a user-controlled system property.

On systems where permissions cannot be verified reliably,the launcher 
maydisable caching or use a more conservative mode.


    Concurrency

Multiple processes may execute the same script concurrently.

The implementation should use atomic writes,temporary files,and safe 
renamesto avoid corrupted cache entries.

If another process is replacing a cache entry,the launcher may either 
waitbriefly,use the previous complete entry if it remains valid,or 
compilenormally.Readers should not observe partially written entries.


    Failure Handling

The cache is an optimization.Cache failures must not prevent an 
otherwisevalid script from running.

If a cache read fails because an entry is 
missing,unreadable,invalid,orcorrupt,the launcher should compile the 
script normally.If a cache writefails because the disk is 
full,permissions are insufficient,or another I/Oerror occurs,the 
launcher should run the normally compiled script withoutpersisting the 
entry.

Temporary files should be removed on a best-effort basis.Failures should 
bereported only when cache diagnostics are enabled unless they prevent 
normalscript execution for an unrelated reason.


    Interaction with Existing Groovy Facilities


      |GroovyShell|

The|groovy|command-line launcher executes scripts 
through|GroovyShell|.|GroovyShell|delegates compilation 
to|GroovyClassLoader|,so it is part of thenatural execution path for the 
cache.The final ownership boundary shouldfollow the reference 
implementation rather than be fixed prematurely.


      |GroovyClassLoader|

|GroovyClassLoader|already maintains in-memory caches for loaded classes 
andcompiled sources.Its source cache uses a key derived from script text 
and codesource,allowing repeated compilation requests within one process 
to reuse anexisting|Class|instance.

|GroovyClassLoader|can also add synthetic timestamp fields to 
generatedclasses when source recompilation is enabled.Those fields 
support sourcestaleness checks for classes loaded within an existing 
runtime.

The proposed persistent cache complements this machinery by surviving 
acrossprocess invocations.It cannot simply serialize the existing source 
cache,because that cache stores loaded|Class|instances rather than 
portablebytecode artifacts.A persistent cache must capture all generated 
class files,store validation metadata,and define the classes safely in a 
new process.

Existing hashing,class collection,and timestamp behavior should be 
reused orfactored where practical.The implementation should avoid 
introducing aparallel staleness model that can disagree with existing 
recompilationbehavior.


      |GroovyScriptEngine|

|GroovyScriptEngine|is the most similar existing facility.It caches 
scriptclasses in memory for long-lived hosts and tracks dependencies 
discoveredduring compilation.It walks those dependencies when deciding 
whether a scriptmust be recompiled.

The persistent launcher cache should study and reuse or factor 
thisdependency-tracking behavior where practical,especially for scripts 
thatdepend on sibling Groovy sources.|GroovyScriptEngine|remains an 
in-memory,timestamp-based facility and does not itself solve persistence 
acrossshort-lived command-line invocations.


      |groovyc|

This proposal does not replace|groovyc|.Users who want explicit 
ahead-of-timecompilation can continue using|groovyc|.

The script cache is intended for the common case where users execute 
sourcescripts directly with the|groovy|command.


      |@Grab|

Scripts using|@Grab|may be cached only if the resolved dependency set 
can beincluded in the cache key.

The first implementation should disable persistent script caching for 
scriptsusing|@Grab|.


      AST Transformations

AST transformations affect generated bytecode and must be part of 
thecompilation fingerprint.This includes classpath-discovered global 
transformsand source-selected local transforms.If this cannot be done 
reliably,cachingshould be disabled for affected scripts.


    Possible Implementation Approach

One possible implementation is:

 1. Add a cache-aware execution path for eligible file-backed scripts
    launched by the|groovy|command.
 2. Before compilation, determine whether the script is eligible for
    caching.
 3. If the script is ineligible, compile and run it normally without
    caching.
 4. For eligible scripts, compute a|ScriptCacheKey|and look for a
    matching cache entry.
 5. If present, validate the metadata and load every generated class
    using an appropriate class loader while preserving
    equivalent|CodeSource|and protection-domain behavior.
 6. If the cache entry is absent or invalid, compile it as today.
 7. For eligible cache misses, reuse or factor existing class collection
    and dependency-tracking machinery where practical.
 8. Capture all generated bytecode and discovered dependency metadata.
 9. Persist generated bytecode and metadata atomically.

Stored metadata is validated before loading cached classes on 
futureinvocations.

The implementation should be internal and not expose cache internals as 
stablepublic API in the first version.The reference implementation 
should determinewhether the cache belongs in the 
launcher,|GroovyShell|,|GroovyClassLoader|,or a focused internal 
component used by them.


    Reference Implementation

A reference implementation has not yet been provided.


    Testing

Tests should cover:

  * cache miss on first execution
  * cache hit on second execution
  * invalidation when script source changes
  * invalidation when a dependent sibling Groovy source changes
  * invalidation when classpath changes
  * invalidation when an extension-module descriptor or implementation
    changes
  * invalidation when Groovy version, runtime distribution, or cache
    format changes
  * invalidation when a compilation-affecting launcher or JVM option changes
  * disabling the cache
  * clearing the entire selected cache directory
  * configuration precedence
  * bounded growth and eviction
  * cleanup of entries from older Groovy or cache format versions
  * atomic publication of complete cache entries
  * concurrent execution
  * scripts with imports
  * scripts with local classes
  * scripts with closures
  * scripts using AST transformations
  * scripts using different compiler configurations
  * scripts using|--enable-preview|
  * scripts with package declarations
  * equivalent behavior for relative, absolute, and symlinked script paths
  * equivalent fingerprinting for|GROOVY_HOME|locations with equivalent
    compilation inputs
  * ineligible scripts using|@Grab|
  * ineligible scripts with compilation inputs that cannot be fingerprinted
  * failure fallback to normal compilation
  * fallback when cache writes fail because of disk-full, permission, or
    I/O errors
  * cache directory permission checks where supported
  * user isolation when a configured cache location may be shared
  * filesystem race and symlink checks where supported
  * equivalent|CodeSource|and protection-domain behavior on cache hits

For a representative corpus of eligible scripts,the test suite should 
comparea cache hit with a fresh compilation.It should compare every 
generated class,not only the main script class.The comparison should 
include:

  * differential behavior tests
  * normalized bytecode comparison that excludes known volatile metadata
  * invalidation tests for each supported fingerprint input

Raw byte-for-byte equality is not always an appropriate oracle because 
Groovymay embed recompilation timestamps in generated classes.Known 
volatilemetadata includes 
synthetic|__timeStamp|and|__timeStamp__...|fields addedfor 
source-recompilation checks.Any unexplained semantic or 
normalized-bytecodedifference must be investigated as a potential 
violation of theCorrectness-First Principle 
<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#correctness-first-principle>.

Performance tests should measure:

  * trivial script
  * script with imports
  * script with AST transforms
  * script with a larger source file
  * script with a dependency-heavy classpath
  * script using|@CompileStatic|
  * script using dynamic Groovy features

Benchmarks should report cold execution,uncached execution,and 
cache-hitexecution separately.They should also measure fingerprinting 
overhead,including the cost of classpath validation,so the cache is not 
enabled forcases where validation consumes the expected savings.


    Impact

This proposal is intended to be backward compatible.

If disabled,behavior is unchanged.

If enabled,the observable behavior of a script should be equivalent to 
normalsource compilation.If the cache cannot guarantee this,it should 
not be used.

The initial implementation would add complexity to the command-line 
launcherbut should not expose cache internals as stable public API.

Unlike a resident JVM service,the cache preserves a fresh JVM for 
everyinvocation.It does persist executable artifacts on disk,so it is 
notstateless in the filesystem sense.


    Risks

The main risks are:

 1. Incorrect cache invalidation.
 2. Security issues from loading cached bytecode.
 3. Increased launcher complexity.
 4. Hard-to-debug behavior if cached bytecode differs from source
    compilation.
 5. Limited benefit for scripts where runtime dominates startup.
 6. Fingerprinting overhead that consumes the expected compilation savings.

These risks can be mitigated by making the feature initially 
opt-in,usingtheCorrectness-First Principle 
<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#correctness-first-principle>,providingdiagnostics,and 
falling back to normal compilation whenever uncertaintyexists.


    Alternatives Considered


      Use|groovyc|

Users can already precompile scripts with|groovyc|.However,this changes 
theworkflow and removes the convenience of directly 
running|.groovy|sourcefiles.


      Rely Only on JVM Startup Improvements

JVM-level startup improvements help Groovy,but they do not 
removeGroovy-specific parsing,AST transformation,and bytecode generation 
costs.


      Keep Only In-Memory Caching

Groovy already benefits from in-memory class caching in long-lived 
processes.This does not help repeated short-lived invocations of 
the|groovy|command.


      External Wrapper Tool

An external script runner could implement persistent 
caching,butlauncher-level support would be more 
discoverable,portable,and consistent.


      GroovyServ

GroovyServ uses a resident warmed JVM plus a thin client.It can avoid 
JVMstartup,runtime initialization,and repeated compilation costs,so 
itsperformance ceiling is substantially higher than that of a persistent 
bytecodecache.

The tradeoff is a stateful daemon process:mutable runtime state may be 
sharedacross executions,environment and standard streams must be 
propagated,andlifecycle and listener security must be managed.A 
persistent launcher cacheserves a different use case.Each invocation 
runs in a fresh JVM while stillavoiding compilation work when a disk 
entry can be validated safely.The twoapproaches are complementary.


    Open Questions

The following decisions should be resolved before the proposal advances 
from|Draft|to|Accepted|:

  * Should the proposed command-line options, diagnostic options, and
    system-property names be adopted as written?
  * What default cache-size limit and eviction policy should be used?
  * Should the first version provide a cache-size summary command, a
    cache-entry listing command, or both? A listing command may expose
    local script paths and should account for that privacy concern.
  * What is the most efficient classpath fingerprinting strategy that
    remains conservative enough for cache reuse?
  * What is the most efficient Groovy runtime distribution fingerprint
    that detects compilation-affecting changes without unnecessary cache
    misses?
  * Should the cache-aware component live in the
    launcher,|GroovyShell|,|GroovyClassLoader|, or a focused internal
    component used by them?


    Future Work

Future extensions could include:

  * enabling the cache by default
  * cache statistics
  * integration with CDS or Leyden-style JVM caches
  * shared cache support for trusted environments
  * tooling integrations for workflows that invoke file-backed scripts
  * reusable public APIs for embedders
  * support for caching generated stubs where applicable
  * smarter dependency fingerprinting for|@Grab|


    Conclusion

A persistent script compilation cache would address a long-standing pain 
pointfor Groovy as a scripting language:repeated startup cost for 
short-livedscripts.

By caching generated class files across 
invocations,the|groovy|launchercould avoid unnecessary repeated 
parsing,AST transformation,and bytecodegeneration when eligible scripts 
and their compilation inputs are unchanged.

Implemented conservatively,this feature would preserve Groovy semantics 
whilereducing compilation overhead for repeated command-line script 
execution.Itsbenefit should be evaluated alongside JVM startup 
improvements such as CDS andProject Leyden.


    Update History

Version 	Date 	Description
0.2 	2026-06-02 	Refined conservative v1 scope, cache lifecycle, 
fingerprinting, security, diagnostics, implementation flow, and testing 
requirements.
0.1 	2026-06-01 	Initial draft formatted as a Groovy Enhancement Proposal.


Best regards,

Per


On 6/2/26 13:04, Paul King wrote:
> Thanks Per for a well thought out proposal. I generally like the idea.
> Below are some Claude thoughts we'd need to factor in when taking this
> idea further. I wouldn't necessarily give the same priority to things
> it mentions (like @Grab) but they are things we need to think through
> carefully none-the-less.
>
> Cheers, Paul.
>
> -----------
>
> Subject: Re: GEP — Persistent Script Compilation Cache for the Groovy launcher
>
> It's a thorough draft and it targets a real pain point. Here's how I'd
> situate it against the facilities we already have (GroovyShell,
> GroovyClassLoader, GroovyScriptEngine) and against GroovyServ, with a
> few things worth addressing before we go further.
>
> **Where the time actually goes**
>
> A `groovy myscript.groovy` invocation pays for: (1) JVM startup, (2)
> loading the Groovy runtime and bootstrapping the MetaClass registry,
> (3) JIT warmup, (4) parsing to AST, (5) AST transforms, (6) bytecode
> generation, (7) loading the script class, and (8) execution.
>
> The proposed cache only ever saves (4)–(6). It doesn't touch (1)–(3),
> which usually dominate for the small, short-lived scripts the
> Motivation leads with. So the realistic ceiling on the benefit is the
> share of wall-clock spent in parse/transform/codegen — smallest for
> trivial scripts, largest for big or `@CompileStatic`/heavy-transform
> scripts (which are also the hardest to invalidate correctly). The
> proposal already defers (1)–(3) to CDS/Leyden, which is the right
> call: this feature is complementary to CDS, not a standalone startup
> story, and I'd pitch it that way.
>
> **Relationship to what we already have**
>
> - *GroovyClassLoader* already has a content-addressed compilation
> cache — it's just in-memory and process-scoped. `genSourceCacheKey`
> hashes the script text, `parseClass(..., shouldCacheSource)` serves
> the cached class, and generated classes carry a synthetic
> `__timeStamp` field that `isSourceNewer` uses for staleness. The
> proposal is essentially "lift that in-memory source cache onto disk."
> The catch: the in-memory key is *only* the source text, because within
> one process the Groovy version, classpath, compiler config, indy flag
> and active transforms are all constant. Persisting across processes
> makes none of those constant — so the entire "Cache Key" section is
> the genuinely new and hard work, and it's exactly the part the current
> cache never had to solve.
>
> - *GroovyShell* is a thin wrapper over GroovyClassLoader (and is what
> the launcher uses), so it adds nothing persistent but is the natural
> insertion point.
>
> - *GroovyScriptEngine* is the closest relative and worth studying: it
> already does dependency-aware invalidation that this GEP
> under-specifies. Each cache entry tracks a dependency set built during
> compilation, and it walks that graph to decide recompilation. That's a
> better model than a single monolithic classpath hash for catching a
> changed sibling script. But GSE is still in-memory and
> timestamp-based, built for long-lived hosts — it does nothing for the
> short-lived CLI case.
>
> So most of the building blocks (content hashing, the timestamp stamp,
> dependency tracking) already exist. I'd strongly prefer extending them
> over growing a parallel mechanism — two timestamp models that can
> disagree is a maintenance trap.
>
> **Versus GroovyServ**
>
> GroovyServ solves a bigger problem a heavier way: a resident warmed
> JVM plus a thin client, which removes (1)–(3) entirely and gets
> (4)–(6) for free from the existing in-memory cache. That's a far
> larger win than a bytecode cache can deliver — by construction this
> cache cannot touch (1)–(3). The trade is everything that comes with a
> stateful daemon: shared mutable state across runs (system properties,
> static initialisers, thread/classloader leaks), environment
> propagation (cwd, env, stdio, exit codes), lifecycle management, and
> the security of a long-lived listener (GroovyServ uses an auth-cookie
> file — a useful precedent for our security section).
>
> The real selling point of this proposal *over* GroovyServ is that it's
> stateless and side-effect-free per run: every invocation is a clean
> process, so it inherits none of the daemon's state-bleed or admin
> overhead — at the price of a much smaller performance ceiling. The two
> aren't mutually exclusive.
>
> **The main risk: key completeness**
>
> This is the make-or-break. Python's `.pyc` and Ruby's bootsnap can
> ship persistent bytecode caches on-by-default because their
> compilation inputs are small and enumerable. Groovy's are open-ended:
> global AST transforms discovered from the classpath (which `@Grab` can
> mutate at compile time), local transforms discovered only during
> compilation, externally supplied
> `CompilerConfiguration`/`GROOVY_CONF`, and a long, non-exhaustive list
> of compilation-affecting system properties. Two consequences:
>
> 1. You often can't compute a correct key without doing the expensive
> work (resolving `@Grab`, running early phases). Conservatively
> disabling the cache for `@Grab` in v1 is the right move.
> 2. The failure mode is silent wrong behaviour — a missed input gives
> stale bytecode that runs differently from a fresh compile, with no
> error. That's strictly worse than a cache miss and the one thing that
> would damage trust in the launcher. The fingerprint must be
> over-inclusive (miss when in doubt), though note a full classpath
> content hash can itself eat the savings, so there's a real balance to
> measure.
>
> A test the GEP doesn't yet state but should, in my view: *for a corpus
> of scripts, a cache hit must produce byte-identical class files to a
> fresh compile.* If that ever fails, a fingerprint input is missing.
>
> **Bottom line**
>
> It fills a real gap none of
> GroovyShell/GroovyClassLoader/GroovyScriptEngine fill — persistence
> across processes — and it's the safe, stateless counterpart to
> GroovyServ's fast, stateful daemon. I'd support pursuing it provided
> we: build on the existing hashing/timestamp/dependency machinery
> rather than a parallel cache; keep it opt-in and
> default-to-miss-on-uncertainty; disable it for `@Grab` initially;
> pitch it alongside CDS rather than as a standalone speed win; and
> treat silent stale bytecode as a release blocker.
>
> On Tue, Jun 2, 2026 at 3:15 AM Per Nyfelt<[email protected]> wrote:
>> Hi,
>>
>> I think it would be nice if we supported caching for Groovy scripts to speed up execution time for subsequent runs. Below is a GEP style proposal for that. What do you think?
>>
>> GEP: Persistent Script Compilation Cache for the Groovy Command-Line Launcher
>>
>> Metadata
>>
>> Type: Feature
>> Status: Draft
>> Target Groovy Version: TBD
>> Author: Per Nyfelt
>> Discussion: TBD
>> Created: 2026-06-01
>>
>> Abstract
>>
>> This proposal introduces an optional persistent compilation cache for Groovy scripts executed through the groovy command-line launcher.
>>
>> When a script is executed, the launcher may store the generated class files in a local cache. On later executions, if the script source and relevant compilation inputs are unchanged, the launcher may load the cached class files instead of parsing, transforming, and compiling the script again.
>>
>> The goal is to improve startup time for short-lived Groovy scripts and command-line tools while preserving current semantics by default.
>>
>> Motivation
>>
>> Groovy is frequently used for scripting, automation, build tooling, data processing, and command-line utilities. In these use cases, process lifetime is often short, and startup overhead matters.
>>
>> For simple scripts, actual execution time may be negligible compared to:
>>
>> JVM startup
>>
>> loading the Groovy runtime
>>
>> parsing the script
>>
>> applying AST transformations
>>
>> bytecode generation
>>
>> class loading
>>
>> Recent and ongoing JVM work, such as CDS and Project Leyden, can reduce JVM startup and class-loading overhead. However, raw Groovy script execution still pays the cost of compiling the script source on every invocation.
>>
>> Groovy already caches compiled script classes during the lifetime of a GroovyClassLoader, but this cache is lost when the process exits. A persistent launcher-level cache would allow repeated invocations of unchanged scripts to skip most compilation work.
>>
>> This would make Groovy more attractive for command-line scripting and developer tooling, especially compared with languages and runtimes that already persist bytecode or compilation artifacts between runs.
>>
>> Goals
>>
>> The goals of this proposal are:
>>
>> Reduce repeated startup overhead for unchanged Groovy scripts.
>>
>> Avoid changing Groovy language semantics.
>>
>> Make the feature safe and conservative by default.
>>
>> Provide explicit ways to disable, clear, and inspect the cache.
>>
>> Ensure cache invalidation accounts for relevant compilation inputs.
>>
>> Allow future integration with JVM startup technologies such as CDS or Leyden-style caches.
>>
>> Non-Goals
>>
>> This proposal does not aim to:
>>
>> Replace groovyc.
>>
>> Change Groovy compilation semantics.
>>
>> Cache arbitrary runtime results.
>>
>> Guarantee improved performance for all scripts.
>>
>> Provide a distributed or shared build cache.
>>
>> Cache scripts run inside long-lived embedded Groovy runtimes unless those runtimes explicitly opt in.
>>
>> Solve dependency resolution caching for @Grab, although it must interact safely with it.
>>
>> Proposed Behavior
>>
>> When running:
>>
>> groovy myscript.groovy
>>
>> the launcher may:
>>
>> Compute a cache key from the script and compilation environment.
>>
>> Look for previously generated class files matching that key.
>>
>> If found and valid, load the cached classes.
>>
>> Otherwise compile the script normally and store the generated classes in the cache.
>>
>> The user-visible behavior of the script must remain the same as if the script had been compiled from source during that invocation.
>>
>> Cache Location
>>
>> The default cache location should follow platform conventions.
>>
>> Suggested defaults:
>>
>> Linux/Unix:
>>    $XDG_CACHE_HOME/groovy/script-cache
>>    or ~/.cache/groovy/script-cache
>>
>> macOS:
>>    ~/Library/Caches/Groovy/script-cache
>>
>> Windows:
>>    %LOCALAPPDATA%\Groovy\script-cache
>>
>> A system property or environment variable should allow overriding the location:
>>
>> groovy -Dgroovy.script.cache.dir=/path/to/cache myscript.groovy
>>
>> Possible environment variable:
>>
>> GROOVY_SCRIPT_CACHE_DIR=/path/to/cache
>>
>> Enabling and Disabling
>>
>> The cache should initially be opt-in unless the Groovy project decides the invalidation model is sufficiently conservative for default use.
>>
>> Possible command-line options:
>>
>> groovy --script-cache myscript.groovy
>> groovy --no-script-cache myscript.groovy
>> groovy --clear-script-cache
>>
>> Possible system properties:
>>
>> -Dgroovy.script.cache=true
>> -Dgroovy.script.cache=false
>> -Dgroovy.script.cache.dir=/path/to/cache
>>
>> If the feature later proves safe and reliable, it could become enabled by default for normal file-based scripts.
>>
>> Cache Key
>>
>> The cache key must include enough information to avoid reusing stale or incompatible bytecode.
>>
>> At minimum, the key should include:
>>
>> absolute or canonical script path
>>
>> script source hash
>>
>> Groovy version
>>
>> Java version or class file target version
>>
>> effective classpath
>>
>> compiler configuration
>>
>> invokedynamic setting
>>
>> preview/incubating compiler flags where relevant
>>
>> script base class
>>
>> active AST transformations
>>
>> relevant system properties that affect compilation
>>
>> For scripts using @Grab, the resolved dependency coordinates and artifact versions should be included after dependency resolution.
>>
>> A conservative implementation may choose to skip caching when the compilation environment cannot be reliably fingerprinted.
>>
>> Cache Contents
>>
>> The cache should store:
>>
>> generated .class files
>>
>> metadata describing the compilation environment
>>
>> cache format version
>>
>> source hash
>>
>> Groovy version
>>
>> Java/classfile target
>>
>> classpath fingerprint
>>
>> timestamp of creation
>>
>> optional diagnostic information
>>
>> The cache format should be treated as internal and may change between Groovy versions.
>>
>> Invalidation
>>
>> A cached script must be invalidated when any relevant compilation input changes.
>>
>> Examples:
>>
>> script source changed
>>
>> Groovy version changed
>>
>> Java target changed
>>
>> classpath changed
>>
>> AST transform implementation changed
>>
>> compiler configuration changed
>>
>> @Grab dependencies changed
>>
>> cache format changed
>>
>> If validation fails or metadata is unreadable, the launcher should silently fall back to normal compilation unless diagnostics are enabled.
>>
>> Diagnostics
>>
>> The launcher should provide optional diagnostics.
>>
>> Examples:
>>
>> groovy --script-cache-info myscript.groovy
>> groovy --script-cache-verbose myscript.groovy
>>
>> Possible output:
>>
>> Groovy script cache: miss
>> Reason: source hash changed
>>
>> or:
>>
>> Groovy script cache: hit
>> Cache entry: ~/.cache/groovy/script-cache/...
>>
>> Diagnostics should be disabled by default to preserve normal script output.
>>
>> Security Considerations
>>
>> The cache stores executable bytecode. Therefore:
>>
>> Cache entries should be private to the current user by default.
>>
>> The cache directory should not be world-writable.
>>
>> The launcher should avoid loading cache entries with unsafe permissions.
>>
>> Cache keys should prevent cross-user or cross-project collisions.
>>
>> The cache should not weaken existing script security assumptions.
>>
>> On systems where permissions cannot be verified reliably, the launcher may disable caching or use a more conservative mode.
>>
>> Concurrency
>>
>> Multiple processes may execute the same script concurrently.
>>
>> The implementation should use atomic writes, temporary files, and safe renames to avoid corrupted cache entries.
>>
>> If a cache entry is being written by another process, the launcher may either wait briefly, ignore the incomplete entry, or compile normally.
>>
>> Interaction with Existing Groovy Facilities
>>
>> GroovyClassLoader
>>
>> The existing in-memory class cache remains useful within a single JVM process. The proposed persistent cache complements it by surviving across process invocations.
>>
>> groovyc
>>
>> This proposal does not replace groovyc. Users who want explicit ahead-of-time compilation can continue using groovyc.
>>
>> The script cache is intended for the common case where users execute source scripts directly with the groovy command.
>>
>> @Grab
>>
>> Scripts using @Grab may be cached only if the resolved dependency set can be included in the cache key.
>>
>> A first implementation may conservatively disable persistent script caching for scripts using @Grab.
>>
>> AST Transformations
>>
>> AST transformations affect generated bytecode and must be part of the compilation fingerprint. If this cannot be done reliably, caching should be disabled for affected scripts.
>>
>> Possible Implementation Approach
>>
>> One possible implementation is:
>>
>> Extend the groovy launcher with a cache-aware script runner.
>>
>> Before compilation, compute a ScriptCacheKey.
>>
>> Look for a matching cache entry.
>>
>> If present, load generated classes using an appropriate class loader.
>>
>> If absent, compile the script as today.
>>
>> Capture generated bytecode.
>>
>> Persist generated bytecode and metadata atomically.
>>
>> On future runs, validate metadata before loading.
>>
>> The implementation should be internal and not expose cache internals as stable public API in the first version.
>>
>> Testing
>>
>> Tests should cover:
>>
>> cache miss on first execution
>>
>> cache hit on second execution
>>
>> invalidation when script source changes
>>
>> invalidation when classpath changes
>>
>> invalidation when Groovy version or cache format changes
>>
>> disabling the cache
>>
>> clearing the cache
>>
>> concurrent execution
>>
>> scripts with imports
>>
>> scripts with local classes
>>
>> scripts using AST transformations
>>
>> scripts using different compiler configurations
>>
>> failure fallback to normal compilation
>>
>> cache directory permission checks where supported
>>
>> Performance tests should measure:
>>
>> trivial script
>>
>> script with imports
>>
>> script with AST transforms
>>
>> script with larger source file
>>
>> script with dependency-heavy classpath
>>
>> script using @CompileStatic
>>
>> script using dynamic Groovy features
>>
>> Backward Compatibility
>>
>> This proposal should be backward compatible.
>>
>> If disabled, behavior is unchanged.
>>
>> If enabled, the observable behavior of a script should be equivalent to normal source compilation. If the cache cannot guarantee this, it should not be used.
>>
>> Risks
>>
>> The main risks are:
>>
>> Incorrect cache invalidation.
>>
>> Security issues from loading cached bytecode.
>>
>> Increased launcher complexity.
>>
>> Hard-to-debug behavior if cached bytecode differs from source compilation.
>>
>> Limited benefit for scripts where runtime dominates startup.
>>
>> These risks can be mitigated by making the feature initially opt-in, using conservative invalidation, providing diagnostics, and falling back to normal compilation whenever uncertainty exists.
>>
>> Alternatives Considered
>>
>> Use groovyc
>>
>> Users can already precompile scripts with groovyc. However, this changes the workflow and removes the convenience of directly running .groovy source files.
>>
>> Rely Only on JVM Startup Improvements
>>
>> JVM-level startup improvements help Groovy, but they do not remove Groovy-specific parsing, AST transformation, and bytecode generation costs.
>>
>> Keep Only In-Memory Caching
>>
>> Groovy already benefits from in-memory class caching in long-lived processes. This does not help repeated short-lived invocations of the groovy command.
>>
>> External Wrapper Tool
>>
>> An external script runner could implement persistent caching, but launcher-level support would be more discoverable, portable, and consistent.
>>
>> Future Work
>>
>> Future extensions could include:
>>
>> enabling the cache by default
>>
>> cache statistics
>>
>> integration with CDS or Leyden-style JVM caches
>>
>> shared cache support for trusted environments
>>
>> Gradle/Maven integration
>>
>> reusable public APIs for embedders
>>
>> support for caching generated stubs where applicable
>>
>> smarter dependency fingerprinting for @Grab
>>
>> Conclusion
>>
>> A persistent script compilation cache would address a long-standing pain point for Groovy as a scripting language: repeated startup cost for short-lived scripts.
>>
>> By caching generated class files across invocations, the groovy launcher could avoid unnecessary repeated parsing, AST transformation, and bytecode generation when scripts are unchanged.
>>
>> Implemented conservatively, this feature would preserve Groovy semantics while making Groovy scripts feel significantly faster in day-to-day command-line use.
>>
>>
scripting-cache-gep.md (text/markdown, 29.7 KB)
# GEP-TBD: Persistent Script Compilation Cache for the Groovy Command-Line Launcher

## Metadata

| Field                    | Value                                                                    |
|--------------------------|--------------------------------------------------------------------------|
| Number                   | GEP-TBD                                                                  |
| Title                    | Persistent Script Compilation Cache for the Groovy Command-Line Launcher |
| Version                  | 0.2                                                                      |
| Type                     | Feature                                                                  |
| Status                   | Draft                                                                    |
| Leader                   | Per Nyfelt                                                               |
| Created                  | 2026-06-01                                                               |
| Last modification        | 2026-06-02                                                               |
| Target Groovy version    | TBD                                                                      |
| Discussion               | TBD                                                                      |
| Reference implementation | TBD                                                                      |

## Abstract

This proposal introduces an optional persistent compilation cache for Groovy
scripts executed through the `groovy` command-line launcher.

When an eligible script is executed, the launcher may store the generated class
files in a local cache. On later executions, if the script source and all
relevant compilation inputs are unchanged, the launcher may load the cached
class files instead of parsing, transforming, and compiling the script again.

The goal is to improve startup time for short-lived Groovy scripts and
command-line tools while preserving current semantics. The cache complements
JVM startup improvements such as CDS and Project Leyden; it is not intended to
replace them.

## Rationale

Groovy is frequently used for scripting, automation, build tooling, data
processing, and command-line utilities. In these use cases, process lifetime is
often short, and startup overhead matters.

For simple scripts, actual execution time may be negligible compared to:

* JVM startup
* loading the Groovy runtime
* bootstrapping the metaclass registry
* JIT warmup
* parsing the script
* applying AST transformations
* bytecode generation
* class loading

Recent and ongoing JVM work, such as CDS and Project Leyden, can reduce JVM
startup and class-loading overhead. A persistent script compilation cache does
not remove JVM startup, Groovy runtime initialization, or JIT costs. Its direct
benefit is narrower: it can avoid parsing, AST transformations, and bytecode
generation for an unchanged script. A cache hit may also avoid loading parts of
the compiler frontend.

The realistic performance ceiling is therefore the portion of wall-clock time
spent compiling the script. That portion is likely to be smallest for trivial
scripts and larger for scripts with substantial source or AST transformation
work. The actual benefit must be measured across representative scripts.

Groovy already caches compiled script classes during the lifetime of a
`GroovyClassLoader`, but this cache is lost when the process exits. A
persistent launcher-level cache would allow repeated invocations of unchanged
scripts to skip most compilation work.

This would make Groovy more attractive for command-line scripting and developer
tooling, especially when used alongside JVM startup improvements.

## Goals

The goals of this proposal are:

1. Reduce repeated startup overhead for unchanged Groovy scripts.
2. Avoid changing Groovy language semantics.
3. Make the feature safe and conservative by default.
4. Provide explicit ways to disable, clear, and inspect the cache.
5. Ensure cache invalidation accounts for relevant compilation inputs.
6. Allow future integration with JVM startup technologies such as CDS or
   Leyden-style caches.
7. Preserve a fresh JVM for each invocation, without sharing mutable runtime
   state between script executions.

## Non-Goals

This proposal does not aim to:

1. Replace `groovyc`.
2. Change Groovy compilation semantics.
3. Cache arbitrary runtime results.
4. Guarantee improved performance for all scripts.
5. Provide a distributed or shared build cache.
6. Cache scripts run inside long-lived embedded Groovy runtimes.
7. Solve dependency resolution caching for `@Grab`, although it must interact
   safely with it.
8. Remove JVM startup, Groovy runtime initialization, or JIT warmup costs.
9. Provide a resident JVM or daemon process.

## Proposed Behavior

When running:

```shell
groovy myscript.groovy
```

when the cache is enabled, the launcher may:

1. Determine whether the script is eligible for caching.
2. If eligible, compute a cache key from the script and compilation environment.
3. Look for previously generated class files matching that key.
4. If found and valid, load the cached classes.
5. If the script is ineligible or no valid cache entry was found, compile the
   script normally and, if eligible, store the generated classes in the cache.

The user-visible behavior of the script must remain the same as if the script
had been compiled from source during that invocation.

### Correctness-First Principle

The cache is an optimization. Reusing stale or incompatible bytecode is a
release-blocking defect. Whenever the launcher cannot establish that every
relevant compilation input is unchanged, it must compile the script normally
instead of reusing a cache entry.

### Initial Scope

The first implementation should be deliberately narrow. It should cache only
file-backed scripts executed through the `groovy` command-line launcher when
the compilation environment can be fingerprinted reliably.

The initial implementation should treat the following as ineligible:

* scripts using `@Grab`
* scripts using externally supplied or dynamically mutated compiler
  configuration, including configuration scripts, that cannot be fingerprinted
  reliably
* scripts affected by launcher configuration, such as `GROOVY_CONF`, that
  cannot be fingerprinted reliably
* scripts affected by launcher startup hooks or JVM options when those hooks or
  options cannot be fingerprinted reliably
* scripts affected by AST transformations that cannot be fingerprinted
  reliably
* scripts with compilation inputs or dependencies that cannot be identified
  reliably

An ineligible script must be compiled normally. Ineligibility is a cache miss,
not an error.

### Cache Location

The default cache location should follow platform conventions.

Suggested defaults:

| Platform | Cache location |
| --- | --- |
| Linux/Unix | `$XDG_CACHE_HOME/groovy/script-cache` or `~/.cache/groovy/script-cache` |
| macOS | `~/Library/Caches/Groovy/script-cache` |
| Windows | `%LOCALAPPDATA%\Groovy\script-cache` |

A system property or environment variable should allow overriding the
location:

```shell
groovy -Dgroovy.script.cache.dir=/path/to/cache myscript.groovy
GROOVY_SCRIPT_CACHE_DIR=/path/to/cache groovy myscript.groovy
```

### Enabling and Disabling

The cache should initially be opt-in unless the Groovy project decides the
invalidation model is sufficiently conservative for default use.

The proposed command-line options are:

```shell
groovy --script-cache myscript.groovy
groovy --no-script-cache myscript.groovy
groovy --clear-script-cache
```

`--clear-script-cache` clears all entries in the selected cache directory and
exits. It does not require a script argument.

The proposed system properties are:

```shell
-Dgroovy.script.cache=true
-Dgroovy.script.cache=false
-Dgroovy.script.cache.dir=/path/to/cache
-Dgroovy.script.cache.maxSizeBytes=<bytes>
```

When multiple mechanisms configure the same setting, the precedence should be:

1. command-line option
2. system property
3. environment variable, such as `GROOVY_SCRIPT_CACHE_DIR` for the cache
   location
4. platform default

Cache enablement remains opt-in through `--script-cache` or
`-Dgroovy.script.cache=true`.

If the feature later proves safe and reliable, it could become enabled by
default for normal file-based scripts.

### Cache Management

The cache must not grow without bound. The implementation should enforce a
configurable size limit, such as `-Dgroovy.script.cache.maxSizeBytes=<bytes>`,
and evict entries automatically when that limit is exceeded. The exact default
limit and eviction policy should be determined by the reference implementation.

Entries from older Groovy versions or cache format versions must be eligible
for cleanup. Automatic cleanup should be best-effort and must not prevent
normal script execution. Users can remove all entries in the selected cache
directory explicitly with `--clear-script-cache`.

An optional cache-size summary or listing command is discussed under
[Open Questions](#open-questions).

### Source Identity

Before computing a cache key for a file-backed script, the launcher should
resolve the script to its canonical path. Relative paths should therefore be
resolved against the current working directory, and symlink aliases should
identify the same source file.

This matches the existing file-backed `GroovyCodeSource` behavior.

### Package Declarations

File-backed scripts with package declarations should remain eligible for
caching. The source hash already accounts for the package declaration. The
cache entry must record the binary names of all generated classes so they can
be defined with the same names and packages on a cache hit.

### Cache Key

The cache key and validation metadata must include enough information to avoid
reusing stale or incompatible bytecode. The fingerprint should be
over-inclusive: uncertainty must cause a cache miss rather than reuse of an
entry that might be stale.

At minimum, the key should include:

* canonical script path
* script source hash
* cache format version
* Groovy version and a fingerprint of the effective Groovy runtime distribution
* Java version or class file target version
* effective classpath
* compiler configuration
* invokedynamic setting
* `--enable-preview`, the `groovy.preview.features` system property set by the
  current Groovy launchers when preview mode is enabled, and any related
  compiler or JVM options that affect generated bytecode
* script base class
* active AST transformations
* classpath-discovered extension modules
* effective launcher configuration, including `GROOVY_HOME`, `groovy.home`,
  `GROOVY_CONF`, and configuration scripts where relevant
* relevant system properties that affect compilation

The effective classpath fingerprint must detect changes to extension-module
descriptors and their implementation classes. Groovy discovers extension
modules from both:

* `META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule`
* `META-INF/services/org.codehaus.groovy.runtime.ExtensionModule`

Launcher startup hooks and JVM options, such as relevant `JAVA_OPTS`, may
change compilation behavior indirectly. The fingerprint should represent
effective compilation-affecting inputs after launcher processing rather than
blindly hashing every launcher environment variable or JVM option. Unrelated
runtime options should not create unnecessary cache misses.

The Groovy version alone is not sufficient to distinguish custom or
vendor-modified distributions. The distribution fingerprint must detect
changes to Groovy runtime artifacts that can affect compilation, even when the
reported Groovy version is unchanged. `GROOVY_HOME` and `groovy.home` should be
represented through their effective compilation inputs rather than treated as
path strings when the resulting distribution and configuration are equivalent.

The implementation must also account for dependent Groovy sources discovered
during compilation. A changed sibling script must invalidate any cached script
that depends on it. `GroovyScriptEngine` already tracks source dependencies
during compilation and should inform this design.

The first implementation should not cache scripts using `@Grab`. A later
implementation may reconsider this if resolved dependencies, compile-time
classpath mutation, and discovered transformations can be represented safely.

Fingerprinting must also be efficient enough to preserve the expected benefit.
For example, a full content hash of every classpath entry may be safe but too
expensive. The implementation should benchmark competing strategies and prefer
a cache miss when a cheap, reliable fingerprint is unavailable.

### Cache Contents

The cache should store:

* generated `.class` files
* binary names of all generated classes
* metadata describing the compilation environment
* cache format version
* source hash
* Groovy version
* Groovy runtime distribution fingerprint
* Java or class file target
* classpath fingerprint
* dependency metadata for discovered Groovy sources
* timestamp of creation
* last-access metadata and entry size where required by the eviction policy
* optional diagnostic information

The cache format should be treated as internal and may change between Groovy
versions.

Cached classes must retain equivalent `CodeSource` and protection-domain
behavior when they are loaded in a new process.

### Cache Entry Layout

Each cache entry should be a self-contained unit identified by a digest of its
cache key. It should contain the metadata and every generated class file needed
for that script. Writers should create temporary entries on the same filesystem
as the cache and publish complete entries atomically.

The implementation may shard entries by key prefix to avoid excessively large
directories. The exact directory names, metadata encoding, and sharding scheme
are internal cache-format details to be determined by the reference
implementation.

### Invalidation

A cached script must be invalidated when any relevant compilation input
changes.

Examples:

* script source changed
* Groovy version changed
* Groovy runtime distribution changed without a version change
* Java target changed
* classpath changed
* AST transform implementation changed
* compiler configuration changed
* `@Grab` dependencies changed, for a future implementation that enables
  `@Grab` caching
* cache format changed
* dependent Groovy source changed
* extension-module descriptor or implementation changed
* compilation-affecting launcher configuration or JVM option changed

If validation fails or metadata is unreadable, the launcher should silently
fall back to normal compilation unless diagnostics are enabled.

As required by the [Correctness-First Principle](#correctness-first-principle),
the implementation must default to a cache miss whenever it cannot establish
that all relevant compilation inputs are unchanged.

### Diagnostics

The launcher should provide optional diagnostics.

The proposed diagnostic options are:

```shell
groovy --script-cache-info myscript.groovy
groovy --script-cache-verbose myscript.groovy
```

`--script-cache-info` should report whether the invocation was a hit, miss, or
ineligible for caching, together with a concise reason. `--script-cache-verbose`
should include the same result plus detailed validation diagnostics, such as
the cache entry location and the compilation inputs that caused a miss or made
the script ineligible.

Example output:

```text
Groovy script cache: miss
Reason: source hash changed
```

or:

```text
Groovy script cache: hit
Cache entry: ~/.cache/groovy/script-cache/...
```

Diagnostics should be disabled by default to preserve normal script output.

## Security Considerations

The cache stores executable bytecode. Therefore:

1. Cache entries should be private to the current user by default.
2. The cache directory should not be world-writable.
3. The launcher should avoid loading cache entries with unsafe permissions.
4. Cache entries should be isolated by operating-system user. If a configured
   cache directory may be shared by multiple users, the launcher must use
   separate user-specific namespaces or disable caching.
5. The cache should not weaken existing script security assumptions.
6. Cache writes and reads should defend against symlink-based replacement and
   similar filesystem races where supported by the platform.
7. Cached classes should retain equivalent `CodeSource` and protection-domain
   behavior when they are loaded in a new process.

The canonical script path and source hash distinguish scripts from different
projects. User isolation should come from cache-directory scoping and
permissions rather than a user-controlled system property.

On systems where permissions cannot be verified reliably, the launcher may
disable caching or use a more conservative mode.

## Concurrency

Multiple processes may execute the same script concurrently.

The implementation should use atomic writes, temporary files, and safe renames
to avoid corrupted cache entries.

If another process is replacing a cache entry, the launcher may either wait
briefly, use the previous complete entry if it remains valid, or compile
normally. Readers should not observe partially written entries.

## Failure Handling

The cache is an optimization. Cache failures must not prevent an otherwise
valid script from running.

If a cache read fails because an entry is missing, unreadable, invalid, or
corrupt, the launcher should compile the script normally. If a cache write
fails because the disk is full, permissions are insufficient, or another I/O
error occurs, the launcher should run the normally compiled script without
persisting the entry.

Temporary files should be removed on a best-effort basis. Failures should be
reported only when cache diagnostics are enabled unless they prevent normal
script execution for an unrelated reason.

## Interaction with Existing Groovy Facilities

### `GroovyShell`

The `groovy` command-line launcher executes scripts through `GroovyShell`.
`GroovyShell` delegates compilation to `GroovyClassLoader`, so it is part of the
natural execution path for the cache. The final ownership boundary should
follow the reference implementation rather than be fixed prematurely.

### `GroovyClassLoader`

`GroovyClassLoader` already maintains in-memory caches for loaded classes and
compiled sources. Its source cache uses a key derived from script text and code
source, allowing repeated compilation requests within one process to reuse an
existing `Class` instance.

`GroovyClassLoader` can also add synthetic timestamp fields to generated
classes when source recompilation is enabled. Those fields support source
staleness checks for classes loaded within an existing runtime.

The proposed persistent cache complements this machinery by surviving across
process invocations. It cannot simply serialize the existing source cache,
because that cache stores loaded `Class` instances rather than portable
bytecode artifacts. A persistent cache must capture all generated class files,
store validation metadata, and define the classes safely in a new process.

Existing hashing, class collection, and timestamp behavior should be reused or
factored where practical. The implementation should avoid introducing a
parallel staleness model that can disagree with existing recompilation
behavior.

### `GroovyScriptEngine`

`GroovyScriptEngine` is the most similar existing facility. It caches script
classes in memory for long-lived hosts and tracks dependencies discovered
during compilation. It walks those dependencies when deciding whether a script
must be recompiled.

The persistent launcher cache should study and reuse or factor this
dependency-tracking behavior where practical, especially for scripts that
depend on sibling Groovy sources. `GroovyScriptEngine` remains an in-memory,
timestamp-based facility and does not itself solve persistence across
short-lived command-line invocations.

### `groovyc`

This proposal does not replace `groovyc`. Users who want explicit ahead-of-time
compilation can continue using `groovyc`.

The script cache is intended for the common case where users execute source
scripts directly with the `groovy` command.

### `@Grab`

Scripts using `@Grab` may be cached only if the resolved dependency set can be
included in the cache key.

The first implementation should disable persistent script caching for scripts
using `@Grab`.

### AST Transformations

AST transformations affect generated bytecode and must be part of the
compilation fingerprint. This includes classpath-discovered global transforms
and source-selected local transforms. If this cannot be done reliably, caching
should be disabled for affected scripts.

## Possible Implementation Approach

One possible implementation is:

1. Add a cache-aware execution path for eligible file-backed scripts launched
   by the `groovy` command.
2. Before compilation, determine whether the script is eligible for caching.
3. If the script is ineligible, compile and run it normally without caching.
4. For eligible scripts, compute a `ScriptCacheKey` and look for a matching
   cache entry.
5. If present, validate the metadata and load every generated class using an
   appropriate class loader while preserving equivalent `CodeSource` and
   protection-domain behavior.
6. If the cache entry is absent or invalid, compile it as today.
7. For eligible cache misses, reuse or factor existing class collection and
   dependency-tracking machinery where practical.
8. Capture all generated bytecode and discovered dependency metadata.
9. Persist generated bytecode and metadata atomically.

Stored metadata is validated before loading cached classes on future
invocations.

The implementation should be internal and not expose cache internals as stable
public API in the first version. The reference implementation should determine
whether the cache belongs in the launcher, `GroovyShell`, `GroovyClassLoader`,
or a focused internal component used by them.

## Reference Implementation

A reference implementation has not yet been provided.

## Testing

Tests should cover:

* cache miss on first execution
* cache hit on second execution
* invalidation when script source changes
* invalidation when a dependent sibling Groovy source changes
* invalidation when classpath changes
* invalidation when an extension-module descriptor or implementation changes
* invalidation when Groovy version, runtime distribution, or cache format
  changes
* invalidation when a compilation-affecting launcher or JVM option changes
* disabling the cache
* clearing the entire selected cache directory
* configuration precedence
* bounded growth and eviction
* cleanup of entries from older Groovy or cache format versions
* atomic publication of complete cache entries
* concurrent execution
* scripts with imports
* scripts with local classes
* scripts with closures
* scripts using AST transformations
* scripts using different compiler configurations
* scripts using `--enable-preview`
* scripts with package declarations
* equivalent behavior for relative, absolute, and symlinked script paths
* equivalent fingerprinting for `GROOVY_HOME` locations with equivalent
  compilation inputs
* ineligible scripts using `@Grab`
* ineligible scripts with compilation inputs that cannot be fingerprinted
* failure fallback to normal compilation
* fallback when cache writes fail because of disk-full, permission, or I/O
  errors
* cache directory permission checks where supported
* user isolation when a configured cache location may be shared
* filesystem race and symlink checks where supported
* equivalent `CodeSource` and protection-domain behavior on cache hits

For a representative corpus of eligible scripts, the test suite should compare
a cache hit with a fresh compilation. It should compare every generated class,
not only the main script class. The comparison should include:

* differential behavior tests
* normalized bytecode comparison that excludes known volatile metadata
* invalidation tests for each supported fingerprint input

Raw byte-for-byte equality is not always an appropriate oracle because Groovy
may embed recompilation timestamps in generated classes. Known volatile
metadata includes synthetic `__timeStamp` and `__timeStamp__...` fields added
for source-recompilation checks. Any unexplained semantic or normalized-bytecode
difference must be investigated as a potential violation of the
[Correctness-First Principle](#correctness-first-principle).

Performance tests should measure:

* trivial script
* script with imports
* script with AST transforms
* script with a larger source file
* script with a dependency-heavy classpath
* script using `@CompileStatic`
* script using dynamic Groovy features

Benchmarks should report cold execution, uncached execution, and cache-hit
execution separately. They should also measure fingerprinting overhead,
including the cost of classpath validation, so the cache is not enabled for
cases where validation consumes the expected savings.

## Impact

This proposal is intended to be backward compatible.

If disabled, behavior is unchanged.

If enabled, the observable behavior of a script should be equivalent to normal
source compilation. If the cache cannot guarantee this, it should not be used.

The initial implementation would add complexity to the command-line launcher
but should not expose cache internals as stable public API.

Unlike a resident JVM service, the cache preserves a fresh JVM for every
invocation. It does persist executable artifacts on disk, so it is not
stateless in the filesystem sense.

## Risks

The main risks are:

1. Incorrect cache invalidation.
2. Security issues from loading cached bytecode.
3. Increased launcher complexity.
4. Hard-to-debug behavior if cached bytecode differs from source compilation.
5. Limited benefit for scripts where runtime dominates startup.
6. Fingerprinting overhead that consumes the expected compilation savings.

These risks can be mitigated by making the feature initially opt-in, using
the [Correctness-First Principle](#correctness-first-principle), providing
diagnostics, and falling back to normal compilation whenever uncertainty
exists.

## Alternatives Considered

### Use `groovyc`

Users can already precompile scripts with `groovyc`. However, this changes the
workflow and removes the convenience of directly running `.groovy` source
files.

### Rely Only on JVM Startup Improvements

JVM-level startup improvements help Groovy, but they do not remove
Groovy-specific parsing, AST transformation, and bytecode generation costs.

### Keep Only In-Memory Caching

Groovy already benefits from in-memory class caching in long-lived processes.
This does not help repeated short-lived invocations of the `groovy` command.

### External Wrapper Tool

An external script runner could implement persistent caching, but
launcher-level support would be more discoverable, portable, and consistent.

### GroovyServ

GroovyServ uses a resident warmed JVM plus a thin client. It can avoid JVM
startup, runtime initialization, and repeated compilation costs, so its
performance ceiling is substantially higher than that of a persistent bytecode
cache.

The tradeoff is a stateful daemon process: mutable runtime state may be shared
across executions, environment and standard streams must be propagated, and
lifecycle and listener security must be managed. A persistent launcher cache
serves a different use case. Each invocation runs in a fresh JVM while still
avoiding compilation work when a disk entry can be validated safely. The two
approaches are complementary.

## Open Questions

The following decisions should be resolved before the proposal advances from
`Draft` to `Accepted`:

* Should the proposed command-line options, diagnostic options, and
  system-property names be adopted as written?
* What default cache-size limit and eviction policy should be used?
* Should the first version provide a cache-size summary command, a cache-entry
  listing command, or both? A listing command may expose local script paths and
  should account for that privacy concern.
* What is the most efficient classpath fingerprinting strategy that remains
  conservative enough for cache reuse?
* What is the most efficient Groovy runtime distribution fingerprint that
  detects compilation-affecting changes without unnecessary cache misses?
* Should the cache-aware component live in the launcher, `GroovyShell`,
  `GroovyClassLoader`, or a focused internal component used by them?

## Future Work

Future extensions could include:

* enabling the cache by default
* cache statistics
* integration with CDS or Leyden-style JVM caches
* shared cache support for trusted environments
* tooling integrations for workflows that invoke file-backed scripts
* reusable public APIs for embedders
* support for caching generated stubs where applicable
* smarter dependency fingerprinting for `@Grab`

## Conclusion

A persistent script compilation cache would address a long-standing pain point
for Groovy as a scripting language: repeated startup cost for short-lived
scripts.

By caching generated class files across invocations, the `groovy` launcher
could avoid unnecessary repeated parsing, AST transformation, and bytecode
generation when eligible scripts and their compilation inputs are unchanged.

Implemented conservatively, this feature would preserve Groovy semantics while
reducing compilation overhead for repeated command-line script execution. Its
benefit should be evaluated alongside JVM startup improvements such as CDS and
Project Leyden.

## Update History

| Version | Date       | Description                                                                                                                           |
|---------|------------|---------------------------------------------------------------------------------------------------------------------------------------|
| 0.2     | 2026-06-02 | Refined conservative v1 scope, cache lifecycle, fingerprinting, security, diagnostics, implementation flow, and testing requirements. |
| 0.1     | 2026-06-01 | Initial draft formatted as a Groovy Enhancement Proposal.                                                                             |