Repository navigation
GROOVY-12488: SC: call the constructor of a constructor reference directly - #3022
codeconsole wants to merge 15 commits into
Conversation
2a2825e to
d5395be
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3022 +/- ##
==================================================
+ Coverage 73.6711% 73.6818% +0.0107%
- Complexity 42584 42609 +25
==================================================
Files 1678 1678
Lines 146907 146944 +37
Branches 27790 27800 +10
==================================================
+ Hits 108228 108271 +43
+ Misses 28528 28520 -8
- Partials 10151 10153 +2
🚀 New features to boost your workflow:
|
daniellansun
left a comment
There was a problem hiding this comment.
Thanks for closing the long-standing TODO in StaticTypesMethodReferenceExpressionWriter.addSyntheticMethodForConstructorReference. Setting StaticTypesMarker.DIRECT_METHOD_CALL_TARGET on the synthetic ctorX and flipping STATIC_COMPILE_NODE to true when a single visible, directly callable constructor matches the SAM parameter types is the natural SC counterpart to an ordinary statically compiled new. Visibility is filtered with filterMethodsByVisibility(…, controller.getThisType()) (enclosing class, including from closures/lambdas), while bytecode accessibility uses controller.getClassNode() via isDirectlyAccessible — the right split. The new MethodReferenceTest cases (overloads, varargs, generics, nestmates, package/protected across compilation units, EMC constructor intercept, interface static/default methods) plus bytecode sequences for INVOKESPECIAL vs INVOKEDYNAMIC init are thorough.
I am leaving a COMMENT (not an approval): the approach is sound and well tested, but a few SC/accessibility edges and one incomplete path deserve attention before this lands.
Observations / concerns
-
isDirectlyAccessiblefor protected/package-private requiresdeclaringClass.isPrimaryClassNode()— correct, but easy to misread.
Declining directINVOKESPECIALwhen the constructor’s class comes from another compilation unit / class loader (same package name, different runtime package) matches the JVM and is locked bytestProtectedAndPackagePrivateConstructorsOfClassCompiledBeforeAreSelectedAtRuntime/testFunctionCN23. Please add a one-sentence comment thatisPrimaryClassNode()here is a same-compilation-unit proxy for “same class loader”, not a general “source vs binary” style check — future readers may otherwise “fix” it to!isResolved()or similar and reintroduceIllegalAccessError. -
Protected constructors of a superclass in another package correctly stay dynamic — confirm no SC false sense of security.
testFunctionCN17/testProtectedConstructorOfAnotherPackageIsSelectedAtRuntimeshow runtime selection forp.A::newfrom a subclass and forObjectInputStream::new. That is right: a static factory emittingNEW+INVOKESPECIALis not a legal use of a protected ctor the waysuper(…)is. Worth a javadoc line onisDirectlyAccessiblestating explicitly that subclass privilege does not grant direct constructor-reference emission. Without that, someone may addaccessingClass.isDerivedFrom(declaringClass)by analogy withAsmClassGenerator.isMemberDirectlyAccessibleand break verification / access control. -
Array constructor references remain
STATIC_COMPILE_NODE = false.
ThereturnType.isArray()branch still builds anArrayExpressionand never sets a direct target, so array::newstays on the old non-SC synthetic path. Pre-existing limitation, but this PR’s TODO removal makes the asymmetry louder. Either:- briefly note in a comment why arrays are out of scope for GROOVY-12488, or
- file a follow-up to mark the array factory synthetic method
STATIC_COMPILE_NODE = truewhen the component type / dims are known (the bytecode tests already distinguish array factories).
Not a blocker for this PR’s class::new goal.
-
Ambiguity / conversion edges fall back to runtime — good — but default-parameter bridges need a careful look.
chooseBestMethod+ctors.size() == 1means overloads that STC would reject as ambiguous, or SAM types that need MetaClass conversion (Function<Object, Foo>with onlyFoo(String)— GROOVY-10971 /testFunctionCN16), correctly keepSTATIC_COMPILE_NODE = false. Default-parameter constructors (testFunctionCN15) rely on Groovy’s generated overloads matching SAM arity. Please add a case where the only declared ctor has defaults and the SAM arity matches a generated bridge that is synthetic — confirmisDirectlyAccessible/ emission still works (synthetic constructors are usually callable; ifchooseBestMethodreturns the bridge, SC must be allowed to invoke it). If synthetic bridges are ever filtered elsewhere, this path could silently degrade to runtime. -
Interface host: dropping
ACC_FINALon the synthetic helper is necessary and slightly orthogonal.
addSyntheticMethodnow usesACC_PRIVATE | ACC_STATIC | (interface ? 0 : ACC_FINAL)because the JVM rejectsprivate+finalinterface methods.testMethodReferencesInStaticAndDefaultMethodsOfInterfacecovers ctor and method refs. Good catch. Please ensure method-reference helpers for non-constructor refs on interfaces get the same flag treatment if they shareaddSyntheticMethod(they do — so this fixes a latent VerifyError for any interface-hosted method reference helper, not only::new). A one-line comment pointing at the JVMS rule would help. -
EMC constructor intercept is bypassed — intentional SC semantics, should be stated on the writer.
testFunctionCN24showsFoo::newunder SC ignores an EMC constructor whilenew Foo(s)from dynamic code does not. That matches other SC direct calls, but constructor references are a popular dynamic-looking API. A short javadoc onfindConstructorReferenceTarget(“when a direct target is chosen, MetaClass constructor intercepts are not honoured”) would set expectations next to the code, not only in the test. -
filterMethodsByVisibility+ private nestmate from closure.
Visibility usesgetThisType()(outer class); accessibility usesgetClassNode()(closure/lambda class) with nestmate checks.testPrivateConstructorOfNestedClassIsCalledDirectlyFromClosureOrLambdalocks this. One softer case: a constructor reference returned from a closure and invoked later still embeds the helper in the closure class — already covered. No change requested; calling out so reviewers see the split is deliberate.
Test gaps I would still like to see
- Comment or follow-up for array
::newstatic compile (item 3). - Default-parameter / synthetic bridge ctor chosen by arity (item 4).
- Optional:
enum E { ; E(String s){} }/E::newasFunction<String,E>—findConstructorReferenceTargetreturns null for enums; a bytecode assert ofINVOKEDYNAMIC initwould document that enums never take the direct path (even when a ctor looks writable). - Optional: abstract class with protected ctor + anonymous SC subclass exposing
::new— expect runtime path.
Non-blocking nits
- Import of
chooseBestMethod/samePackageNamekeeps the writer aligned with STC rather than reimplementing overload selection — appreciated. compileStaticMethodBytecodeAgainstLibraryis a useful helper; consider whether it belongs onAbstractBytecodeTestCasefor reuse by other SC tests.- Updating the older nestmate bytecode test from indy-init to
NEW/INVOKESPECIALis the right golden-bytecode adjustment.
Stance
Directionally I am in favour: this is the SC constructor-reference story we have wanted since the TODO was written, with careful accessibility rules and strong tests. The main asks before approval are documenting why protected subclass privilege must not be added to isDirectlyAccessible, clarifying the primary-class-node / class-loader proxy, and acknowledging the array-::new leftover. I am not approving yet; happy to re-review once those are addressed.
|
Pushed 35346fd; its last two commits fix CI failures (a new test that counted JaCoCo's
Test gaps:
Nits:
16 of the 35 new |
|
Thanks Scott, this is a thorough piece of work and the core change is right: under static compilation, X::new now selects its constructor the way 1. Documentation. The behaviour changes in the description (overload chosen by the SAM's static types, Object[] to varargs as one element, NPE for null to a primitive, visible-constructor selection, the EMC constructor override bypass) are method-resolution changes, which COMPATIBILITY.md classes as breaking. They are the right semantics for @CompileStatic, but they need a "Groovy 7" entry there, plus a sentence in the constructor-reference section of core-operators.adoc. Same for the new compile error on 2. Scope. The constructor-reference fix needs the run-time-package guard for constructors, since without it a direct call would regress a GroovyShell script referencing a package-private constructor from an earlier parse. Everything beyond that is fixing pre-existing bugs, some unrelated to constructor references. I'd like those under their own JIRAs even if the commits stay here, and the BroadcastChannel, test de-flaking and LogTest changes moved to a separate PR. Suggested grouping: A. Constructor references, Groovy 7 and the 6.0.1 backport Grails wants: B. The same run-time-package rule applied to C. Extras, each its own JIRA and PR: 3. Backport. Group A is contained and well tested; I'd take it to 6.0.1 after a heads-up on the dev list, since it changes overload selection in a point release. Groups B and C should soak on master first. Two smaller notes: the protected-constructor error is raised from StaticInvocationWriter during class generation rather than by the type checker, so @TypeChecked code is silent and works dynamically; tolerable, but worth a comment. And GroovyClassLoader marks every parseClass compilation as defined in memory regardless of the collector, so a subclass that defines classes into the loader itself will take the meta-class path where direct access would work; not a failure, but a sentence in the javadoc would help the next reader. |
daniellansun
left a comment
There was a problem hiding this comment.
Thanks for the substantial follow-up on tip 35346fd, and for the detailed reply to my earlier COMMENT on f35e725. I also read Paul’s notes on documentation and scope.
Prior technical asks — largely closed
-
Same-runtime-package proxy.
AsmClassGenerator.isInSameRuntimePackagewith the clarified “defined by the class loader of the accessing class” Javadoc is much clearer than readingisPrimaryClassNode()alone. Restricting the meta-class fallback to in-memoryGroovyClassLoadercompilations (class files / incremental / joint keep direct access) is the right boundary. -
No subclass privilege for protected ctors. Documented on
isDirectlyAccessible;newof a protected ctor from another package is now a compile error under SC;::newkeeps runtime selection; anonymous-subclass cases added. Please do not later addisDerivedFromby analogy with member access. -
Array
::new. Marking the synthetic methodSTATIC_COMPILE_NODE = true(bytecode already NEWARRAY / ANEWARRAY / MULTIANEWARRAY) removes the asymmetry. Good. -
Default-argument bridges. Confirmed non-synthetic (
Verifier.addConstructorcopies modifiers) and pinned for late-declared / precompiled / package-private shapes. -
Interface helpers / ACC_FINAL. Shared
addSyntheticMethod+ JVMS 4.6 comment, with ClassFormatError regression coverage for array, extension, Class method, and other-loader helpers, closes the latent VerifyError for every interface-hosted helper — not only::new. -
EMC bypass. Javadoc on
findConstructorReferenceTarget+testFunctionCN24set expectations. -
Enum / abstract / anonymous protected cases filled as discussed.
Blocking / process before I would approve
-
Merge conflicts. Tip is
mergeable_state: dirtyagainst current master. Please rebase / resolve before any approval can mean anything operationally. -
Scope carve-up (agreeing with Paul). The core
X::newstatic compile is coherent and well tested. The PR also carries runtime-package generalisation for methods/fields/package-private classes, BroadcastChannel publisher-after-close, sleep→await de-flaking, and LogTest GC races. Those are valuable, but they are separate risk and backport stories. Paul’s A / B / C split (ctor-ref + necessary ctor runtime-package guard for 6.0.1; broader runtime-package + diagnostics for master soak; BroadcastChannel / LogTest / de-flakes as their own JIRAs) is the right way to land this without blocking Grails on unrelated surface area. -
Documentation. Overload selection by SAM static types, Object[]→varargs-as-one-element, NPE for null→primitive, visible-ctor selection, and EMC constructor bypass are COMPATIBILITY.md-class changes under SC. A Groovy 7 entry there, a sentence in core-operators for constructor references, and a native-image note that fallback cases still need reflection metadata should land with group A.
Smaller residual
- Protected-ctor error raised from
StaticInvocationWriter(classgen) means@TypeCheckedstays silent and works dynamically — tolerable; a code comment stating that is enough for now. - Paul’s note that marking every
parseClasscompilation as in-memory can push subclass loaders onto the meta-class path even when direct access would work — not a failure, but a Javadoc sentence would help the next reader.
Stance
I am leaving a COMMENT, not an approval: tip conflicts, and the PR still mixes a backport-worthy ctor-ref fix with broader runtime-package and unrelated cleanups. Once conflicts are cleared and group A is separable (with COMPATIBILITY / docs), I would be glad to re-review that slice toward approval. Thank you for the depth of the work — the technical answers to the earlier review are solid.
…ectly Under static compilation, a constructor reference such as Made::new is implemented by a synthetic method of the enclosing class, which the LambdaMetafactory call site targets. That method was generated with STATIC_COMPILE_NODE set to false, because its constructor call had no target constructor. So it created the object through the meta-class, an "init" invokedynamic call site followed by a "cast" call site, and the constructor was selected at run time on every call. In a GraalVM native image, the constructor also had to be registered for reflection although the code was statically compiled. StaticTypesMethodReferenceExpressionWriter now selects the constructor when it builds the synthetic method: chooseBestMethod matches the declared constructors of the type against the parameter types of the functional interface, as the arguments of a constructor call are matched. When a single constructor matches and the current class can call it, it becomes the DIRECT_METHOD_CALL_TARGET of the constructor call and the synthetic method is statically compiled, so StaticInvocationWriter emits NEW and INVOKESPECIAL, with any boxing, unboxing, widening or variadic array the arguments need. As for other method references, the constructor is then chosen by the parameter types of the functional interface, not by the run-time types of the arguments. The constructor is still selected at run time when no single constructor accepts the parameter types, for example Foo::new as a Function<Object,Foo> when Foo only declares Foo(String), and when the constructor is protected and declared in another package, which only a super(...) call can use. Array constructor references are unchanged: they already create the array directly.
… the visible ones The constructor of a statically compiled constructor reference was chosen from all the constructors the type declares. When the best match was a constructor the enclosing class cannot use, such as private Foo(String) next to a public Foo(Object), the reference fell back to run-time selection and the meta-class called the inaccessible constructor, while new Foo(s) in the same class calls Foo(Object). The constructors are now filtered by visibility before chooseBestMethod, as the type checker does for a constructor call, so the reference calls Foo(Object) directly, as new Foo(s) and Java do. Visibility is checked for the enclosing class of a closure or lambda, not for the generated class, so a private constructor of a nested class stays visible from a closure or lambda of the outer class, as it is for the type checker.
…y only within the compilation unit A constructor reference called a protected or package-private constructor of the same package directly. The JVM checks the run-time package, which is the package name and the class loader, so the call failed with IllegalAccessError when the class came from another class loader with the same package name: a class of an earlier GroovyShell parse, which GroovyClassLoader defines with another InnerLoader, or a precompiled class used from a script. Such a constructor is now called directly only when its class is compiled in the same compilation unit, which defines all its classes with one class loader. Otherwise the constructor is selected at run time, as before the constructor reference was compiled statically.
…final in an interface The synthetic method that implements a constructor reference, or adapts a variadic or extension method reference, is private, static and final. The JVM rejects a final private method in an interface, so a method reference that needs one, such as Foo::new or Arrays::asList as a Supplier<List>, in a static or default method of a statically compiled interface failed with ClassFormatError when the interface was loaded. The method is no longer final in an interface; it is private, so it cannot be overridden anyway.
…he meta-class A statically compiled constructor reference now calls the constructor directly, as new Foo(s) does under static compilation, so a constructor added to the meta-class, for example with ExpandoMetaClass, is no longer used by it. Pin this with a test, and mark the tests added for GROOVY-12488 with the issue key.
…cked code does new A(map), where map is a Map variable rather than a map literal and A declares only a no-argument constructor, is a map-style construction: Groovy calls the no-argument constructor and sets a property for each entry of the map. The type checker accepts it: as the property names are only known at run time, it does not select a constructor, and type-checked code constructs the object through the meta-class. Under static compilation nothing compiled such a call, and compilation stopped with the internal error "Target constructor for constructor call expression hasn't been set". A constructor reference such as Function<Map, A> f = A::new stopped with the same error, because the type checker checks it as new A(map). The type checker now marks such a call for dynamic resolution, and the static compiler creates the object through the meta-class, as type-checked and dynamic code do. A map literal is still compiled to the no-argument constructor and a setter call for each entry, and a constructor reference to such a class still selects the constructor at run time. (Test only: GROOVY-12454 on master already makes static compilation construct such an object through the meta-class, so only the test of the constructor reference, testFunctionCN25, is kept.)
…ugh the meta-class only for classes defined in memory The previous commits accessed a protected or package-private member of a class with the same package name through the meta-class whenever that class was compiled by another compilation unit. That also hit class files: a Java class of a joint compilation, a main class used by a test, a class left over from an incremental compilation. Such classes are loaded by one class loader at run time, as javac assumes, so the direct access worked; the meta-class path was slower, followed meta-class semantics, needed reflection metadata in a native image, and made an incremental build give other bytecode than a full one. The run-time package of such a class only differs when the classes are defined in memory by a GroovyClassLoader, which defines each compilation (a GroovyShell script, parseClass, JSR-223) with an InnerLoader of its own. GroovyClassLoader now records that on the compilation unit, and AsmClassGenerator.isInSameRuntimePackage treats a class of another compilation unit as another run-time package only then. Constructors, map-style construction, methods, getters and setters, method references, fields, dynamic super and this.@ access and super$ methods all use this check. Compiling to class files keeps direct access, as before these changes and as javac does. (Constructor part: the GroovyClassLoader marker, AsmClassGenerator.isInSameRuntimePackage and StaticInvocationWriter.isDirectlyAccessible, which constructor references use. The same rule for new, methods, fields, dynamic super, this.@ and super$ methods follows separately.)
…structor reference The synthetic method of an array constructor reference such as String[]::new was the one constructor-reference method still marked as not statically compiled. It is now compiled by the static writers, like the method of a class constructor reference that has a target constructor. Its bytecode does not change: the array was already created with NEWARRAY, ANEWARRAY or MULTIANEWARRAY, and the size unboxed the way a statically compiled new T[n] does (Integer.intValue, or DefaultTypeTransformation.intUnbox for a Short or Byte). The new tests pin that bytecode, compared with new String[s], and the behavior for each size type the type checker accepts (int, short, byte and their wrappers), multi-dimensional and primitive arrays, closures and lambdas, a negative size and null.
…rules of constructor references - isInSameRuntimePackage: isPrimaryClassNode() stands for "defined by the class loader of the accessing class", not for "compiled from source". - StaticInvocationWriter.isDirectlyAccessible: unlike isMemberDirectlyAccessible it has no subclass rule, since the verifier rejects NEW and INVOKESPECIAL of a protected constructor of another package in a subclass too. - findConstructorReferenceTarget: a selected constructor is called directly, so a meta-class constructor override is not used, as for a statically compiled new. The guard comment now says why enums are left to the meta-class: the type checker accepts an enum constructor reference whose arguments match the generated constructor, but Java code cannot instantiate an enum. - addSyntheticMethod: the ACC_FINAL rule of JVMS 4.6 applies to every helper method of a method reference in an interface. (Constructor part: the notes on statically compiled new follow separately.)
…nd abstract-class constructors Default arguments: the constructors that Verifier generates for default arguments keep the modifiers of the declared constructor and are not synthetic, and every class of a compilation is verified before any class is generated, so a constructor reference whose functional interface matches a generated constructor calls it directly, also when the class is declared after the reference, when the constructor is package-private, when every parameter has a default, and for a public constructor of a class compiled by an earlier parse. A package-private one of such a class is still selected at run time. Enums: the type checker accepts E::new inside the enum when the functional interface matches the constructor Groovy generates (name, ordinal, ...); the reference stays on the run-time path, while javac rejects it outright. Abstract classes: A::new is a type checking error also inside an anonymous subclass. A protected constructor of another package referenced from an anonymous subclass is selected at run time and works.
… interface All helper methods that a method reference adds to the current class are created by addSyntheticMethod, which leaves out ACC_FINAL in an interface. The interface tests covered the constructor-reference and variadic helpers; they now also cover the array constructor reference, the static extension method helper (Thread::startDaemon), the Class method helper (String::getName) and the helper that calls a package-private method of a class of another class loader (M::spm, M::pm), in static and default methods, and check that every helper is private and not final. With ACC_FINAL put back, both tests fail with ClassFormatError (illegal modifiers 0x101A). (Constructor-reference part: the case of a method of another run-time package follows separately.)
…AbstractBytecodeTestCase MethodReferenceTest and StaticCompileRuntimePackageTest each had a copy of the helper that compiles a script against the classes of an earlier parse of the same GroovyClassLoader, and of the code that keeps only the instructions of one method. AbstractBytecodeTestCase now provides them as compileAgainstLibrary, extractMethodSequence and selectMethod, and both tests use them. Test code only; the tests are unchanged. (Here MethodReferenceTest only; StaticCompileRuntimePackageTest, which comes separately, uses the same helpers.)
…ce helpers testMethodReferencesInStaticAndDefaultMethodsOfInterface listed every synthetic method of the interface. With coverage on, JaCoCo adds its own synthetic $jacocoInit, so the CI coverage job failed. The test now skips names starting with '$'.
…nstructor references COMPATIBILITY.md lists the changes for Groovy 7: the overload is chosen by the static parameter types of the functional interface, an Object[] is passed to a variadic constructor as one element, null for a primitive parameter throws NullPointerException, a constructor the enclosing class cannot call no longer wins over one it can, and a meta-class constructor is not used. The method reference section of the operators guide says how the constructor of a constructor reference is selected, when it is still selected at run time, and that those cases need reflection metadata in a native image.
…er subclass GroovyClassLoader marks every parseClass compilation as defined in memory, whatever its class collector does. A subclass whose collector defines the classes with the GroovyClassLoader itself therefore takes the meta-class path for non-public members of classes of its other compilations, where a direct access would also work. The javadoc of the mark now says so.
35346fd to
73b3cc8
Compare
|
@paulk-asert @daniellansun I've split this as Paul suggested. This PR is now group A, rebuilt on current master, so the conflicts are gone:
This branch passes the |
✅ All tests passed ✅🏷️ Commit: 73b3cc8 Learn more about TestLens at testlens.app/docs. |
Under
@CompileStatic, a constructor reference such asMade::newis implemented by a synthetic methodctorRef$<method>$<n>on the enclosing class, which the LambdaMetafactory call site targets. That method was not statically compiled:So every call selected the constructor through the meta-class at run time. In a GraalVM native image the constructor also needed reflection metadata, although the code is statically compiled; Grails replaced
X::newwith() -> new X()for this reason. Other method references (String::length,Math::abs) already link their target directly.The cause is in
StaticTypesMethodReferenceExpressionWriter.addSyntheticMethodForConstructorReference. It builtctorX(type, args)without a target constructor and setSTATIC_COMPILE_NODEto false, as its TODO described.StaticInvocationWriter.writeInvokeConstructoremits a direct call only when the constructor call carriesDIRECT_METHOD_CALL_TARGET.Constructor selection
The writer now selects the constructor among the constructors visible from the enclosing class, matched against the functional interface's parameter types with
StaticTypeCheckingSupport.chooseBestMethod, as the type checker selects the constructor ofnew X(args). Inside a closure or lambda, visibility is checked for the enclosing class, not the generated class, as the type checker does, so a private constructor of a nested class stays visible from a closure in the outer class.When exactly one constructor matches and the current class can call it, the writer sets it as
DIRECT_METHOD_CALL_TARGETand compiles the synthetic method statically. The result isNEW/INVOKESPECIAL, with the existing writer handling boxing, unboxing, widening and variadic arrays. Private constructors of nestmates are called directly, and so are protected and package-private constructors of a class of the same package, unless that class comes from another class loader (see below).Run-time selection is kept in these cases:
Function<Object,Foo>whereFooonly hasFoo(String)(GROOVY-10971);super(...)can call;GroovyClassLoaderdefines the compilation in memory: a class of an earlierGroovyShellparse, or a precompiled class used from a script. It is in another run-time package even with the same package name, so a direct call would fail withIllegalAccessError.GroovyClassLoadermarks such compilations, andAsmClassGenerator.isInSameRuntimePackageuses the mark. Code compiled to class files (groovyc, Gradle, Maven, joint compilation) calls such a constructor directly, as javac does.In a native image those cases still need reflection metadata for the constructors. A constructor that Groovy generates for default arguments is selected like a declared one: with
D(String a, String b = 'b'),Function<String, D> f = D::newcallsD(String)directly.Function<Map, A> f = A::new, whereAhas only a no-argument constructor, does a map-style construction likenew A(map)with aMapvariable (GROOVY-12454).The synthetic method of an array constructor reference such as
String[]::newis compiled as statically compiled code too. Its bytecode does not change: it already created the array withNEWARRAY,ANEWARRAYorMULTIANEWARRAY, and unboxed the size as a statically compilednew String[n]does.Behavior differences
The constructor is now chosen at compile time by the static compiler's rules, as for
new X(args)in the same class and as in Java, rather than from the run-time argument types:Function<CharSequence,X> f = X::newcallsX(CharSequence)even when it is given aString;IL(Object)andIL(long),Function<Integer,IL>callsIL(Object)(before:IL(long));IL2(int)andIL2(Integer),IntFunction<IL2>callsIL2(int)(before:IL2(Integer));VO(Object...),Function<Object,VO>given anObject[3]passes it as one element (before: three);PI(int),Function<Integer,PI>givennullthrowsNullPointerException(before:GroovyRuntimeException).A constructor the enclosing class cannot call no longer wins over one it can: with
private Foo(String)andpublic Foo(Object),Function<String,Foo>from another class callsFoo(Object), asnew Foo(s)does. Before, the meta-class called the private constructor.A constructor added to the meta-class, for example with an
ExpandoMetaClassconstructoroverride, is no longer used by a statically compiled constructor reference, just as it is not used by a statically compilednew.These are listed in a Groovy 7 entry of COMPATIBILITY.md, and the method reference section of the operators guide says how a constructor reference selects its constructor and which cases still need reflection metadata in a native image.
Interfaces
The synthetic method was private, static and final. The JVM rejects ACC_FINAL on any method of an interface (JVMS 4.6), so a method reference that needs a synthetic method in a static or default method of a
@CompileStaticinterface failed withClassFormatError(illegal modifiers 0x101A) when the interface was loaded. That covered every helper method a method reference adds:Foo::newandString[]::new, variadic and static extension method references such asArrays::asListas aSupplier<List>orThread::startDaemon, and a method ofClasssuch asString::getName. The method is no longer final in an interface; it is private, so it cannot be overridden anyway. This failure also happens on master.Tests
The tests are in
MethodReferenceTest, marked// GROOVY-12488:init/castsequence is updated;Mapparameter, constructor and method references in static and default methods of an interface (each kind of helper method, checked private and not final), array constructor references for each size type the type checker accepts, and default-argument constructors of a class declared later or compiled before.The
groovy,gls,bugs,org.codehaus.groovy.classgenandorg.codehaus.groovy.transformpackages pass (13,420 tests), apart frombugs.Groovy12363, which also fails on master when it runs afterbugs.Groovy12142in the same JVM (GROOVY-12580, #3096, fixes it). The Checkstyle and CodeNarc gates pass.Backport
The javap above is from Groovy 6.0.0, so 6.0.x has the same behavior. A backport to
GROOVY_6_0_Xwould let Grails 9, which builds with Groovy 6, use constructor references in native images. As it changes which constructor some statically compiled constructor references call, it would go to 6.0.1 after a heads-up on the dev list.JIRA: GROOVY-12488