Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions build-logic/src/main/groovy/org.apache.groovy-tested.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ tasks.withType(Test).configureEach {
if (lambdaHoist != null) {
systemProperty 'groovy.target.lambda.hoist', lambdaHoist
}
// Forward the automatic @CompileStatic closure-packing flag (GEP-27) to the test JVM. Set only when present.
def closurePack = findProperty('groovy.target.closure.pack') ?: System.getProperty('groovy.target.closure.pack')
if (closurePack != null) {
systemProperty 'groovy.target.closure.pack', closurePack
}
def testdb = System.properties['groovy.testdb.props']
if (testdb) {
systemProperty 'groovy.testdb.props', testdb
Expand Down Expand Up @@ -251,6 +256,13 @@ tasks.withType(GroovyCompile).configureEach {
}
}
}
// Forward the opt-in closure-packing flag (GEP-27) to the forked Groovy compiler: packing happens
// at compile time, so test sources are only packed when the compiler JVM sees the property (the
// Test-task systemProperty above covers runtime assertScript compilation). Set only when present.
def closurePack = findProperty('groovy.target.closure.pack') ?: System.getProperty('groovy.target.closure.pack')
if (closurePack != null) {
groovyOptions.forkOptions.jvmArgs += ["-Dgroovy.target.closure.pack=${closurePack}" as String]
}
}

class TestCommandLineArgumentProvider implements CommandLineArgumentProvider {
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/groovy/lang/Closure.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import org.codehaus.groovy.runtime.memoize.ConcurrentSoftCache;
import org.codehaus.groovy.runtime.memoize.LRUCache;
import org.codehaus.groovy.runtime.memoize.Memoize;
import org.codehaus.groovy.runtime.metaclass.ClosureMetaClass;
import org.codehaus.groovy.runtime.metaclass.PackedClosureMetaClass;

import java.io.IOException;
import java.io.InvalidObjectException;
Expand Down Expand Up @@ -604,7 +606,8 @@ public V call(final Object... arguments) {
private boolean nonStockMetaClass() {
MetaClass mc = super.getMetaClass();
Class<?> mcClass = (mc == null) ? null : mc.getClass();
return mcClass != org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.class && mcClass != MetaClassImpl.class;
return mcClass != ClosureMetaClass.class && mcClass != MetaClassImpl.class
&& mcClass != PackedClosureMetaClass.class;
}

@Override
Expand Down
11 changes: 10 additions & 1 deletion src/main/java/groovy/lang/MetaClassRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
package groovy.lang;

import org.codehaus.groovy.runtime.GeneratedClosure;
import org.codehaus.groovy.runtime.PackedClosure;
import org.codehaus.groovy.runtime.metaclass.ClosureMetaClass;
import org.codehaus.groovy.runtime.metaclass.PackedClosureMetaClass;

import java.lang.reflect.Constructor;
import java.util.Iterator;
Expand Down Expand Up @@ -167,7 +169,14 @@ private MetaClass createWithCustomLookup(Class theClass, MetaClassRegistry regis
* @return the default meta class implementation for the type
*/
protected MetaClass createNormalMetaClass(Class theClass,MetaClassRegistry registry) {
if (GeneratedClosure.class.isAssignableFrom(theClass)) {
if (PackedClosure.class.isAssignableFrom(theClass)) {
// the packed adapter family shares its dispatch machinery, so it gets its own
// metaclass with reflection-free call/doCall dispatch and instance-faithful
// introspection. Checked before GeneratedClosure: the fixed-arity members carry
// that marker (for class-level arity introspection) but must not fall into
// ClosureMetaClass, whose reflective dispatch assumes per-literal classes.
return new PackedClosureMetaClass(registry, theClass);
} else if (GeneratedClosure.class.isAssignableFrom(theClass)) {
return new ClosureMetaClass(registry,theClass);
} else {
return new MetaClassImpl(registry, theClass);
Expand Down
126 changes: 126 additions & 0 deletions src/main/java/groovy/transform/PackedClosures.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package groovy.transform;

import org.apache.groovy.lang.annotation.Incubating;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Opt-in for compact closure compilation: within the annotated class or method, an eligible closure
* literal's body is hoisted into a synthetic method on the enclosing class and the literal is replaced
* by a shared {@link org.codehaus.groovy.runtime.PackedClosure} adapter, instead of generating one
* inner class per closure. The value stays a real {@code groovy.lang.Closure}, so {@code curry},
* {@code memoize}, {@code trampoline} and iteration keep working; captured values are threaded by
* value (read-only) or via a shared {@code groovy.lang.Reference} (when written), so a packed closure
* behaves identically to the class-based form.
* <p>
* Packing is best-effort: a closure that cannot be packed is compiled exactly as today (a generated
* closure class), so the annotation is always safe to add. A closure is kept as a class when it:
* <ul>
* <li>references {@code owner}, {@code delegate}, {@code thisObject}, {@code resolveStrategy},
* {@code directive}, {@code metaClass} or {@code super} — it needs a real {@code Closure};</li>
* <li>has default parameter values, or contains an anonymous inner class;</li>
* <li>is nested inside another closure, or visibly escapes its method (returned, stored to a
* field/property/index, initialising a field, appended, or placed in a collection literal);</li>
* <li>is visibly serialization-bound (cast or coerced to a {@code Serializable} type, passed
* directly to a {@code writeObject} call, or held in a local that is) — it keeps its class
* so serialization works;</li>
* <li>is written where the adapter cannot stand in for a generated class: as an argument to a
* {@code this(...)}/{@code super(...)} constructor call, inside a trait, or cast to an
* intersection type;</li>
* <li>under dynamic compilation, resolves any free name — an implicit-this call, a bare
* field/property-bound or dynamic variable, or an explicit {@code this}-property — that the
* delegate chain or the MOP could intercept;</li>
* <li>under {@code @CompileStatic}, resolves a name against a delegate (e.g. via {@code @DelegatesTo}
* or {@code with}), is left to runtime resolution by a type-checking extension (mixed-mode
* dynamic islands), or touches {@code this}-properties of a {@code Map}-implementing owner —
* packing is otherwise proven sound from the type checker's resolution.</li>
* </ul>
* Under dynamic compilation the annotation is a trust assertion the compiler cannot verify; the
* {@code PackedClosure} adapter then fails fast if a delegate, or a delegate-consulting
* {@code resolveStrategy}, is later set on a packed closure.
* Automatic packing of provably-safe {@code @CompileStatic} closures — without this annotation — is
* available behind the {@code groovy.target.closure.pack} flag. Use {@link #mode()} to have declines
* reported (or to opt a scope out); on the un-annotated flag path,
* {@code groovy.target.closure.pack.report=true} reports every decline with its reason as a
* compiler warning — the operational way to see where the packability boundary falls in a codebase.
* <p>
* Because every packed closure is an instance of a small fixed-arity adapter family
* ({@code PackedClosure$Fixed0..4}, {@code $FixedIt}, {@code $FixedN}) rather than its own class,
* three differences from the class-based form remain that cannot, in general, be detected at
* compile time (the visibly serialization-bound cases above are the detectable exception):
* <ul>
* <li><em>Serialization:</em> a packed closure is not serializable — attempting it fails fast
* at runtime with a message naming the closure and this opt-out (note {@code dehydrate()}
* cannot make it serializable — the dispatch state remains — although a dehydrated packed
* closure stays callable), so scopes that serialize their closures should not be packed;</li>
* <li><em>Class identity:</em> {@code closure.getClass()} distinguishes arity (enough for
* class-level introspection such as SAM-overload selection) but not individual literals, so
* code keyed on per-closure generated class names or types will not find them;</li>
* <li><em>Class-level metaclass changes:</em> modifying a family member's metaclass affects
* every packed closure of that arity, where a per-closure-class change was scoped to one
* literal. Per-instance {@code setMetaClass} is fully honoured — a packed closure whose
* metaclass has been replaced or wrapped routes all dispatch through it, exactly as a
* generated closure class does.</li>
* </ul>
*
* @see PackMode
*/
@Incubating
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface PackedClosures {
/**
* The packing behaviour of this scope: {@link PackMode#LENIENT} (pack, silent on declines, the
* default), {@link PackMode#WARN} (pack, a compiler warning per decline with the reason),
* {@link PackMode#STRICT} (pack, a compiler error on any decline), or {@link PackMode#DISABLED}
* (do not pack). The most-specific declaration wins, so a {@code DISABLED} method opts out of a
* packed class (and {@code DISABLED} also overrides the automatic {@code groovy.target.closure.pack}
* flag); the WARN/STRICT diagnostics apply only to the annotated scope, never to the flag.
*/
PackMode mode() default PackMode.LENIENT;

/**
* The packing behaviour of a {@link PackedClosures} scope. {@link #LENIENT}, {@link #WARN} and
* {@link #STRICT} all pack, differing only in how they report a closure that could <em>not</em> be
* packed (declines are otherwise silent). {@link #DISABLED} is the opposite: it opts the scope out
* of packing entirely, so the most-specific declaration wins — a {@code DISABLED} method inside a
* packed class, or vice versa, and it overrides the automatic {@code groovy.target.closure.pack}
* flag as well.
*/
@Incubating
enum PackMode {

/** Do not pack any closure in this scope; overrides an enclosing opt-in and the automatic flag. */
DISABLED,

/** Silently fall back to a generated closure class for any closure that cannot be packed (default). */
LENIENT,

/** Emit a compiler warning, naming the closure and why it declined, for each that cannot be packed. */
WARN,

/** Emit a compiler error for any closure in the scope that cannot be packed. */
STRICT
}
}
30 changes: 16 additions & 14 deletions src/main/java/org/codehaus/groovy/ast/ClassNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -1820,23 +1820,25 @@ public void visitContents(GroovyClassVisitor visitor) {
}

private void visitMethods(GroovyClassVisitor visitor) {
// create snapshot of the method list to avoid ConcurrentModificationException
List<MethodNode> methodList = new ArrayList<>(getMethods());
for (MethodNode mn : methodList) {
visitor.visitMethod(mn);
}

// visit the method nodes added while iterating,
// e.g. synthetic method for constructor reference
final List<MethodNode> newMethodList = getMethods();
if (newMethodList.size() > methodList.size()) { // if the newly added method nodes found, visit them
List<MethodNode> changedMethodList = new ArrayList<>(newMethodList);
boolean changed = changedMethodList.removeAll(methodList);
if (changed) {
for (MethodNode mn : changedMethodList) {
// Visit each method exactly once, including methods added WHILE visiting -- a synthetic method
// for a constructor reference, a lambda deserialization hook, or a hoisted closure body that in
// turn hoists a nested one. Track already-visited methods by identity (MethodNode uses the
// default equals) in a hash set for O(1) membership, and re-scan getMethods() after each round
// for the newly added ones, until none remain.
Set<MethodNode> visited = Collections.newSetFromMap(new java.util.IdentityHashMap<>());
List<MethodNode> pending = new ArrayList<>(getMethods()); // snapshot avoids ConcurrentModificationException
while (!pending.isEmpty()) {
for (MethodNode mn : pending) {
if (visited.add(mn)) {
visitor.visitMethod(mn);
}
}
pending = new ArrayList<>();
for (MethodNode mn : getMethods()) {
if (!visited.contains(mn)) {
pending.add(mn);
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,9 @@ public void visitClass(final ClassNode classNode) {
MopWriter mopWriter = mopWriterFactory.create(controller);
mopWriter.createMopMethods();
controller.getCallSiteWriter().generateCallSiteArray();
// GROOVY-12151: after every method (including hoisted bodies added during the
// visit) has claimed its id, render the class's packed-closure dispatch table
controller.getClosureWriter().writePackedDispatcher();
createSyntheticStaticFields();
}
}
Expand Down
Loading
Loading