[ 
https://issues.apache.org/jira/browse/GROOVY-12731?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18140479#comment-18140479
 ] 

James Daugherty commented on GROOVY-12731:
------------------------------------------

Reproducer repository: 
https://github.com/jdaugherty/groovy-indy-compound-assign-metaclass-reproducer 
({{./run.sh 5.1.3 6.0.0}}). {{run.sh}} also takes the path of a Groovy checkout 
built with {{./gradlew :repackageJar :groovy-callsite:repackageJar}}.

> 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
>            Priority: Major
>
> 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