[
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)