G
YouTube TikTok Games
Skip to content

GROOVY-12602: SC: pass $self as the receiver of a lambda in a trait method - #3129

Open
codeconsole wants to merge 3 commits into
apache:masterfrom
codeconsole:sc-trait-lambda-implicit-this
Open

codeconsole wants to merge 3 commits into
apache:masterfrom
codeconsole:sc-trait-lambda-implicit-this

Conversation

@codeconsole

@codeconsole codeconsole commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Under @CompileStatic, a lambda in a trait method that calls an instance method of the trait by implicit this fails at run time:

import java.util.function.Supplier
@groovy.transform.CompileStatic
trait U {
    String getUname() { 'u' }
    String a() { Supplier<String> s = () -> getUname(); s.get() }
}
class C implements U {}
assert new C().a() == 'u' // ClassCastException: class java.lang.Class cannot be cast to class U

The same happens for a super trait's accessor (uname or getUname()), a lambda in a lambda, a closure in a lambda and a lambda in a trait field initializer, and in 4.0.33, 5.0.8 and 6.0.0. Dynamic Groovy, @TypeChecked and closures work.

A trait method body becomes a static method of T$Trait$Helper that takes the instance as $self. A closure there is rewritten to closure.rehydrate($self,$self,$self). Since GROOVY-11265 static compilation drops that rewrite for a native lambda, and StaticTypesLambdaWriter then passes the helper class as the lambda's owner and this object, because the enclosing method is static. The implicit-this call reads getThisObject() and casts it to the trait.

StaticTypesLambdaWriter.loadLambdaReceiver now passes the enclosing helper method's $self as the owner and this object of a lambda that accesses instance members, as rehydrate does for a closure. Closures nested in such a lambda take its this object, so they work too. Static trait methods are unchanged. Doing this in TraitReceiverTransformer instead would break a lambda coerced to Closure that resolves names through a delegate (for example obj.with((b) -> prop)): that transform runs before type checking and cannot tell the two kinds of lambda apart.

A lambda in a static trait method gets the same treatment. Its helper method takes the implementing class as $static$self, and the lambda now receives that, so an implicit-this call of a Class method there, such as getSimpleName(), sees the implementing class (C, not T$Trait$Helper), as in a closure. TraitWithClosureOrLambda.testInvokeClassMethodFromTraitLambdaInStaticMethod covers this. #3092 (GROOVY-12563) makes the same change to StaticTypesLambdaWriter; whichever lands second keeps one copy.

Tests

TraitWithClosureOrLambda gets five tests. testInvokeTraitMethodFromTraitLambda, testInvokeSuperTraitMethodFromTraitLambda, testInvokeTraitMethodFromTraitLambdaInFieldInitializer and testInvokeTraitMethodFromTraitLambdaOrClosure fail without the change. testInvokeTraitStaticOrPrivateMethodFromTraitLambda covers neighbouring shapes. groovy.transform.stc.*, org.codehaus.groovy.classgen.asm.sc.*, org.codehaus.groovy.transform.traitx.* and groovy.ThisAndSuperTest pass. The Checkstyle and CodeNarc gates pass.

@codecov-commenter

codecov-commenter commented Oct 31, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.4168%. Comparing base (5222105) to head (411f2e5).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
...roovy/classgen/asm/sc/StaticTypesLambdaWriter.java 80.0000% 0 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #3129        +/-   ##
==================================================
+ Coverage     74.4085%   74.4168%   +0.0084%     
- Complexity      44367      44389        +22     
==================================================
  Files            1699       1699                
  Lines          151050     151076        +26     
  Branches        28783      28794        +11     
==================================================
+ Hits           112394     112426        +32     
- Misses          28376      28381         +5     
+ Partials        10280      10269        -11     
Files with missing lines Coverage Δ
...roovy/classgen/asm/sc/StaticTypesLambdaWriter.java 90.0356% <80.0000%> (-0.0379%) ⬇️

... and 18 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codeconsole

codeconsole commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Note: #3129 (GROOVY-12602) and #3139 (GROOVY-12612) fix different shapes of a call from a @CompileStatic lambda or closure in a trait instance method to a method of the trait. Neither depends on the other, and they can merge in either order.

With both applied, every shape I probed passes under dynamic, @TypeChecked and @CompileStatic. The shapes were a lambda with arguments, an overridden trait method, a nested lambda, a closure inside a lambda, this::foo, properties, private fields, static and private methods. 4.0.33, 5.0.8 and 6.0.0 fail the same way, and both JIRA issues now list them. The two branches merge cleanly with each other and with current master.

@daniellansun daniellansun left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks correct for GROOVY-12602.

After GROOVY-11265 dropped rehydrate($self,$self,$self) for native lambdas under SC, StaticTypesLambdaWriter treated the enclosing static helper as owner/this and implicit-this calls cast getThisObject() to the trait — hence ClassCastException: Class cannot be cast to U. Loading $self when the enclosing method is a trait helper and the lambda accesses instance members restores the closure-equivalent receiver.

Tests cover getters/properties, nested lambda/closure mixes, method refs, field-initializer lambdas, private/static helpers, and three compile modes. Solid.

CI: lts (17, windows-latest, 1) is red; other LTS cells green. Happy to APPROVE once that cell is re-run green.

@codeconsole

Copy link
Copy Markdown
Contributor Author

@daniellansun I can't re-run jobs on apache/groovy, so I merged current master (492ae79) to get a fresh run, which also brings in the Windows Gradle cache fix (#3110). The failed cell was the lts (17, windows-latest, 1) timeout. No code changes; the core test suite (19,691 tests) passes locally on the merge.

…static trait method

A lambda in a static trait method still received the trait helper class
as its receiver, so an implicit-this call such as getSimpleName() named
the helper, while a closure in the same method names the implementing
class. The lambda now receives $static$self, as it receives $self in an
instance method.
@testlens-app

testlens-app Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 411f2e5
▶️ Tests: 23417 executed
⚪️ Checks: 26/26 completed


Learn more about TestLens at testlens.app/docs.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants