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)

Reply via email to