James Daugherty created GROOVY-12731:
----------------------------------------
Summary: Indy: compound assignment keeps invoking the operator
method of another instance's metaclass
Key: GROOVY-12731
URL: https://issues.apache.org/jira/browse/GROOVY-12731
Project: Groovy
Issue Type: Bug
Affects Versions: 6.0.0
Reporter: James Daugherty
On Groovy 6 with invokedynamic, a compound assignment ({{+=}}, {{-=}}, ...)
call site that first runs for a Groovy object with its own metaclass keeps
invoking that metaclass's operator method for every later instance of the
class. A method call site re-links for the plain instance. Groovy 5, and Groovy
6 with classic call sites, invoke the right method.
Per-instance metaclasses are common in tests: assigning
{{instance.metaClass.plus}} gives that one Groovy object its own
{{ExpandoMetaClass}}, and a per-instance {{DelegatingMetaClass}} behaves the
same way. Code that runs {{total += item}} for a customized instance and then
for ordinary instances gets the customized closure for all of them, including
in a later test that runs in the same JVM.
*Reproducer:* the script below has no dependencies. {{customized()}} returns a
{{Counter}} whose own metaclass replaces {{plus}} and {{next}}. One call site
invokes {{next()}} and one {{+=}} site invokes {{plus()}}; each runs first for
a customized instance and then for a plain one. Output on Groovy 5.1.3 and
6.0.0, with {{-Dgroovy.target.indy=false}} selecting classic call sites:
{code:groovy}
// Two instances of one Groovy class, the first given its own metaclass, as
tests often do with
// instance.metaClass.method = { ... }. The same call site then runs for a
plain instance.
class Counter {
def plus(int n) { 'real' }
def next() { 'real' }
}
class Caller {
def next(Counter counter) { counter.next() }
def addOne(Object value) { def result = value; result += 1; result }
}
Counter customized() {
Counter counter = new Counter()
counter.metaClass.plus = { int n -> 'customized' }
counter.metaClass.next = { -> 'customized' }
counter
}
def caller = new Caller()
String mode = "indy=${System.getProperty('groovy.target.indy', 'true
(default)')}"
caller.next(customized())
def a = caller.next(new Counter())
println "${mode}: method call on a plain instance after a customized one:
${a} ${a == 'real' ? '(OK)' : '(WRONG: the call site invokes the other
instance\'s method)'}"
caller.addOne(customized())
def b = caller.addOne(new Counter())
println "${mode}: compound assignment on a plain instance after a customized
one: ${b} ${b == 'real' ? '(OK)' : '(WRONG: the call site invokes the other
instance\'s plus)'}"
{code}
{noformat}
== Groovy 5.1.3
indy=true: method call on a plain instance after a customized one:
real (OK)
indy=true: compound assignment on a plain instance after a customized one:
real (OK)
== Groovy 6.0.0
indy=true: method call on a plain instance after a customized one:
real (OK)
indy=true: compound assignment on a plain instance after a customized one:
customized (WRONG: the call site invokes the other instance's plus)
indy=false: method call on a plain instance after a customized one:
real (OK)
indy=false: compound assignment on a plain instance after a customized one:
real (OK)
{noformat}
The same happens on master (7.0.0-SNAPSHOT at 492ae79db4), and for {{-=}}. The
reverse order, a customized instance after a plain one, invokes the customized
method.
*Cause:* GEP-15 (6.0.0-alpha-2) links a compound assignment through
{{IndyCompoundAssign.resolve}}, which guards the linked target on the
receiver's class and the argument's class ({{IndyCompoundAssign.guard}}) and on
the class-domain SwitchPoints. It does not check the receiver's metaclass. A
method call site also guards a {{GroovyObject}} receiver with {{SAME_MC}}, the
metaclass instance it was linked with ({{Selector.MethodSelector.setGuards}}),
so it re-links when a plain instance follows a customized one. A Groovy object
keeps its own metaclass in its {{metaClass}} field, which
{{ClassInfo.hasPerInstanceMetaClasses()}} does not track, so the target is also
cached in the call site's inline cache under the receiver's class.
*Proposed fix:* guard a {{GroovyObject}} receiver on its metaclass instance in
{{IndyCompoundAssign.resolve}}, as {{Selector}} does for a method call.
Found while adding a {{DelegatingMetaClass}} control to the GROOVY-12558 tests
(https://github.com/apache/groovy/pull/3086); it is independent of that change.
Environment: JDK 21.0.12, macOS.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)