[
https://issues.apache.org/jira/browse/GROOVY-12235?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-12235:
-------------------------------
Description:
An interface {{default}} method whose body contains *no* dynamic code (e.g.
{{return null}}) is broken under {{indy=false}}. GROOVY-11982 routes the
call-site array prologue to the synthetic helper class ({{MyInterface$1}}), but
the helper is only materialised when at least one named call site is
registered. The prologue ({{INVOKESTATIC $getCallSiteArray()}}) is emitted
unconditionally for every method body, so a default method with zero dynamic
calls ends up with a dangling {{Methodref}} to a class that is never generated,
and the first invocation throws {{NoClassDefFoundError: MyInterface$1}}.
Reproducer:
{code:groovy}
import org.codehaus.groovy.control.CompilerConfiguration
def config = new CompilerConfiguration()
config.optimizationOptions.put('indy', false)
new GroovyShell(config).evaluate '''
interface MyInterface {
default Object defaultValue() {
return null
}
}
class MyImpl implements MyInterface {
}
assert new MyImpl().defaultValue() == null
'''
{code}
Expected: assertion passes.
Actual (master / 5.0.x / 5.1.x with the GROOVY-11982 fix):
{noformat}
java.lang.NoClassDefFoundError: MyInterface$1
at MyInterface.defaultValue(Script1.groovy)
{noformat}
On releases predating the GROOVY-11982 fix (e.g. 5.0.4) the same shape fails
with {{IncompatibleClassChangeError}} instead, because the prologue's owner is
the interface itself — so this shape has never worked under {{indy=false}}; the
fix only changed the failure mode.
Root cause: {{AsmClassGenerator.visitStdMethod}} calls
{{CallSiteWriter.makeSiteEntry()}} before the method body is visited, so the
prologue cannot know the body will register no call sites. The guard in
{{AsmClassGenerator.createInterfaceSyntheticStaticFields}} checks
{{getCallSites().isEmpty()}}, which only reflects *named* call sites, and skips
emitting the helper.
Suggested fix: materialise the helper whenever a prologue was actually emitted
for the interface (e.g. track a flag in {{CallSiteWriter.makeSiteEntry()}}),
not only when named call sites were registered. A helper with an empty
{{CallSite[]}} array is valid. This also covers static interface methods with
no dynamic code.
The existing {{Groovy11982.groovy}} tests all use default methods *with*
dynamic bodies (GStrings, dynamic calls), which is why this gap went unnoticed.
Real-world impact: hit by the Grails 9 / Groovy 6 canary (apache/grails-core PR
15558) — {{grails.core.GrailsApplicationLifeCycle#beanRegistrar()}} ({{default
... return null}}) broke app boot under {{-PgrailsIndy=false}}; Grails worked
around it by converting the interface to Java (commit 20e8ec71d7e4). The
workaround row can be removed once this is fixed.
Should be backported to 5.1.x and 5.0.x, which carry the same residual gap via
the GROOVY-11982 backport.
> Interface default method without dynamic calls references unemitted call-site
> helper under indy=false
> -----------------------------------------------------------------------------------------------------
>
> Key: GROOVY-12235
> URL: https://issues.apache.org/jira/browse/GROOVY-12235
> Project: Groovy
> Issue Type: Bug
> Reporter: Paul King
> Assignee: Paul King
> Priority: Major
>
> An interface {{default}} method whose body contains *no* dynamic code (e.g.
> {{return null}}) is broken under {{indy=false}}. GROOVY-11982 routes the
> call-site array prologue to the synthetic helper class ({{MyInterface$1}}),
> but the helper is only materialised when at least one named call site is
> registered. The prologue ({{INVOKESTATIC $getCallSiteArray()}}) is emitted
> unconditionally for every method body, so a default method with zero dynamic
> calls ends up with a dangling {{Methodref}} to a class that is never
> generated, and the first invocation throws {{NoClassDefFoundError:
> MyInterface$1}}.
> Reproducer:
> {code:groovy}
> import org.codehaus.groovy.control.CompilerConfiguration
> def config = new CompilerConfiguration()
> config.optimizationOptions.put('indy', false)
> new GroovyShell(config).evaluate '''
> interface MyInterface {
> default Object defaultValue() {
> return null
> }
> }
> class MyImpl implements MyInterface {
> }
> assert new MyImpl().defaultValue() == null
> '''
> {code}
> Expected: assertion passes.
> Actual (master / 5.0.x / 5.1.x with the GROOVY-11982 fix):
> {noformat}
> java.lang.NoClassDefFoundError: MyInterface$1
> at MyInterface.defaultValue(Script1.groovy)
> {noformat}
> On releases predating the GROOVY-11982 fix (e.g. 5.0.4) the same shape fails
> with {{IncompatibleClassChangeError}} instead, because the prologue's owner
> is the interface itself — so this shape has never worked under
> {{indy=false}}; the fix only changed the failure mode.
> Root cause: {{AsmClassGenerator.visitStdMethod}} calls
> {{CallSiteWriter.makeSiteEntry()}} before the method body is visited, so the
> prologue cannot know the body will register no call sites. The guard in
> {{AsmClassGenerator.createInterfaceSyntheticStaticFields}} checks
> {{getCallSites().isEmpty()}}, which only reflects *named* call sites, and
> skips emitting the helper.
> Suggested fix: materialise the helper whenever a prologue was actually
> emitted for the interface (e.g. track a flag in
> {{CallSiteWriter.makeSiteEntry()}}), not only when named call sites were
> registered. A helper with an empty {{CallSite[]}} array is valid. This also
> covers static interface methods with no dynamic code.
> The existing {{Groovy11982.groovy}} tests all use default methods *with*
> dynamic bodies (GStrings, dynamic calls), which is why this gap went
> unnoticed.
> Real-world impact: hit by the Grails 9 / Groovy 6 canary (apache/grails-core
> PR 15558) — {{grails.core.GrailsApplicationLifeCycle#beanRegistrar()}}
> ({{default ... return null}}) broke app boot under {{-PgrailsIndy=false}};
> Grails worked around it by converting the interface to Java (commit
> 20e8ec71d7e4). The workaround row can be removed once this is fixed.
> Should be backported to 5.1.x and 5.0.x, which carry the same residual gap
> via the GROOVY-11982 backport.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)