[ 
https://issues.apache.org/jira/browse/GROOVY-12751?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18141550#comment-18141550
 ] 

ASF GitHub Bot commented on GROOVY-12751:
-----------------------------------------

paulk-asert commented on PR #3284:
URL: https://github.com/apache/groovy/pull/3284#issuecomment-6752330880

   PR #3284 is solid overall, but one gap matters before merge: Java sources in 
a later compilation can't see classes an earlier compilation published.
   
   All 12 of the PR's `MemClassOutputPublicationTest` tests pass on the PR head 
(c61a950f18). I did not run the full suite.
   
   **1. Published classes are invisible to javac (confirmed, should fix or 
document before merge).** When javac builds its classpath 
(`JavacJavaCompiler.makeParameters`), it skips the in-memory URL. 
`InMemoryJavaFileManager` passes classpath lookups straight to the standard 
file manager, so nothing gives javac the published bytes. Groovy code in the 
later unit resolves them fine, through the class loader.
   
   I confirmed this with a scratch test:
   - Unit 1 compiles `p/Lib.java` and `p/G.groovy`, then gets published.
   - Unit 2 compiles `q/Use.java`, which calls `p.Lib.n() + p.G.g()`.
   - javac fails with `error: package p does not exist`.
   
   The PR's tests miss this because every second-unit Java source is either 
standalone or a replacement for an earlier class. Since the whole point of 
GROOVY-12751 is that a later compilation can use the earlier one's output, this 
should either work or be stated plainly as a limitation. Making it work means 
the file manager would list and serve published classes for `CLASS_PATH`.
   
   **2. `AsmDecompiler` now re-parses class files for many users who never 
enable the option.** `cacheableScheme` caches only `file`, `jar` and `jrt` 
URLs. Before this PR, URLs of every scheme were cached. So class files on OSGi 
(`bundleresource:`), JBoss (`vfs:`) and WebSphere (`wsjar:`) classpaths are now 
parsed again on every lookup. Only the in-memory protocol needs to skip the 
cache, so the check should exclude `x-groovy-mem` rather than list the three 
cacheable schemes.
   
   **3. The Console and groovysh changes are outside this PR's scope.** Neither 
tool ever turns on `memClassOutput`. The groovysh `PackageHelper` change is 
also a real behaviour change: it now silently skips any non-file, non-jar URL 
where it used to throw `ClassNotFoundException`. That's probably better, but 
it's unrelated and arguably its own commit. The `ModuleImportHelper` change is 
a genuine fix: `jar:` URLs used to escape as an uncaught 
`FileSystemNotFoundException`.
   
   **4. Minor points:**
   - **Exception cost on misses:** the in-memory URL is last in lookup order, 
and the JDK's handling for unknown protocols turns every miss there into a 
thrown `FileNotFoundException`. Groovy's class resolver does a lot of 
speculative lookups, so a stackless exception in `Connection.connect` would be 
cheaper.
   - **Shadowing warning is partial:** `warnIfShadowed` runs straight after 
javac, so it only checks javac's classes, not Groovy's own output.
   - **Option checked inconsistently:** `memStubEnabled` is read once in the 
constructor, but `isMemClassOutput()` is checked on every compile. The docs say 
you can turn the option off between compiles, but changing `memStub` that way 
has no effect.
   - **New public API:** `CompilationUnit.writeClassFile` is new protected API 
(`@since 7.0.0`). That's fine, but it's an extension point people will be able 
to override.
   
   Things I checked that hold up:
   - Saving the compiled classes only after the output phase completes, and 
discarding them on failure (`getPhase() > Phases.OUTPUT`).
   - Class files javac produces from Groovy stubs are kept out of the in-memory 
output.
   - `-proc:none` is added only in the new mode, so the default build is 
unchanged.
   - The in-memory URL never triggers DNS lookups in `equals`/`hashCode`.
   - The resource-path checks reject `..` and backslashes.
   




> In-memory javac class output across compilation units (follow-up to 
> GROOVY-12711)
> ---------------------------------------------------------------------------------
>
>                 Key: GROOVY-12751
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12751
>             Project: Groovy
>          Issue Type: Improvement
>          Components: Compiler
>            Reporter: Daniel Sun
>            Priority: Major
>
> h3. Background
> GROOVY-12711 ([PR #3242|https://github.com/apache/groovy/pull/3242]) adds the 
> joint-compilation option {{memClassOutput}} 
> ({{CompilerConfiguration.MEM_CLASS_OUTPUT}}). Together with {{memStub}}, it 
> keeps javac's {{CLASS_OUTPUT}} in memory, so a {{JavaAwareCompilationUnit}} 
> needs neither a stub directory nor a target directory on disk.
> In that change, the in-memory store is owned by a single compilation unit. An 
> earlier revision also kept a map from {{GroovyClassLoader}} to store, so that 
> a second compilation unit created on the same loader could see the classes 
> published by the first. That map was removed during review: the only scenario 
> it served was a test that deliberately built two units on one loader, and 
> none of the normal entry points ({{groovyc}}, the Ant task, 
> {{GroovyClassLoader#parseClass}}, groovy-lsp) actually reaches it.
> The underlying question is still a fair one, though, and deserves to be 
> looked at on its own merits: when a host keeps a long-lived class loader and 
> compiles more than once, how should in-memory class output from earlier 
> compilations be made visible to later ones?
> h3. Goal
> Define, and if justified implement, a publication rule for in-memory class 
> output that spans compilation units, driven by how real hosts use Groovy's 
> existing APIs rather than by a hand-built test scenario.
> Concretely, the outcome should answer:
> * Which loader is long-lived, and which loader defines the classes of each 
> compilation?
> * Where must the in-memory bytes live so that the loader actually searched by 
> {{getResource}} and {{loadClass}} can find them?
> * How do overlapping class names behave when a later compilation redefines a 
> class that an earlier one already produced?
> h3. Facts any design has to respect
> * {{URLClassLoader}} cannot remove a URL, and {{getResource}} returns the 
> first match in parent-first, insertion order. Appending one more in-memory 
> URL per compilation to a long-lived loader therefore lets the *oldest* bytes 
> win for overlapping names. That cannot serve as an update mechanism.
> * A class that a loader has already defined cannot be replaced. Recompiling a 
> loaded class always requires a new defining loader.
> * {{GroovyClassLoader#parseClass}} already separates the two roles: the outer 
> loader is long-lived, while each parse defines its classes on a fresh, 
> short-lived {{InnerLoader}} whose {{getResource}} and {{addURL}} delegate to 
> the outer loader. Joint compilation does not use this split today. A map 
> keyed on the compilation unit's loader does not follow it either.
> * groovy-lsp reuses one loader across compilation units while the compiler 
> settings are unchanged, and its recompiles usually overlap the previous ones. 
> It sets {{memStub}} but still writes javac output to a temporary directory. 
> If it ever adopts {{memClassOutput}}, overlapping names will be the common 
> case, not the exception.
> * The Ant task's incremental mode keeps earlier classes visible by putting 
> the destination directory on the classpath ({{includeDestClasses}}). An 
> in-memory equivalent has to state whether it means parent delegation to 
> earlier output, or a fresh compilation of the union of sources.
> h3. Proposed approach
> # Pick one or more concrete hosts (for example an embedder built on 
> {{parseClass}}, or groovy-lsp) and describe its loader lifetime through 
> existing, public API only.
> # Follow the {{parseClass}} shape: one long-lived parent that holds 
> previously published output only when the host asks for it, plus a new child 
> loader per compilation that defines that compilation's classes and is 
> searched first.
> # Put the in-memory bytes on the loader that the child actually searches, 
> rather than appending a sticky URL to a shared loader.
> # Specify what happens for overlapping names and for classes that are already 
> defined, and do not suggest that {{loadClass}} on a loader that already 
> defined a class will ever see new bytes.
> # Cover it with tests that go through the normal API 
> ({{CompilerConfiguration}}, {{JavaAwareCompilationUnit}}, 
> {{GroovyClassLoader}}), not with two units manually attached to one loader.
> h3. Out of scope
> * Enabling {{memClassOutput}} inside {{parseClass}} or groovy-lsp. Those are 
> potential consumers of whatever rule this issue settles, and can be changed 
> separately.
> * Any change to the single-unit behaviour delivered by GROOVY-12711.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to