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

ASF GitHub Bot commented on GROOVY-12557:
-----------------------------------------

paulk-asert commented on PR #3085:
URL: https://github.com/apache/groovy/pull/3085#issuecomment-6532734438

   Verdict: ready to merge. I found no correctness or metaprogramming blocker. 
James has already answered Daniel's asType question in the PR thread: a cast 
site never selects asType, while the as operator goes through an ordinary 
method call site whose metaclass guard the PR leaves alone. The PR has 
blackdrag's approval and green CI.
   
   To test it I built the PR head and its parent and ran probes against both. 
The jars and probe scripts are in 
/private/tmp/claude-501/-Users-paulk-Projects-groovy/deaa34fd-22fc-4390-b220-52371f39f6b5/scratchpad/p3085/;
 I've removed the scratch worktree.
   
   Other ways it could break, and what I found:
   
   1. Is anything else in the cast metaclass-dependent? No. Besides asBoolean, 
a cast links one of four handles:
      - identity;
      - the HashSet/ArrayList constructors;
      - closure-to-SAM conversion;
      - castToType.
   
      The first three depend only on the value's class. castToType runs fresh 
on every call, so any metaclass lookup inside it (such as a constructor through 
invokeConstructorOf) never gets baked into the cached handle. The Boolean and 
boolean targets are caught by handleBoolean first, which keeps the guard.
   2. The metaclass guard was also quietly a null guard. isSameMetaClass(mc, 
null) returns false, so a null value forced a relink. With the PR, a cast from 
a final static type gets only the switch-point guard; I confirmed in the indy 
log that nothing else is added. A null-unsafe handle cached for a non-null 
value could therefore be reused for null. In practice that can't happen:
      - Identity, SAM and castToType handle null fine.
      - The collection-constructor fast path is effectively unreachable, 
because isAbstractClassOf(HashSet, Set) tests the subtype relationship the 
wrong way round. Those casts fall through to castToType instead. That's an old 
oddity, not something this PR introduced.
   
      I warmed a final class Bag extends ArrayList → Set cast past the optimize 
threshold, then passed null. It returned null on both builds, and the final 
Number → int case still throws GroovyCastException on both.
   3. Per-instance asBoolean still relinks. The flag is only set when 
handleBoolean selects through the metaclass, and a new selector is created on 
every relink, so stale state can't carry over. The PR's alternating 
truthy/falsy test covers this.
   4. Proxy truthiness is still not fixed, but it matches classic call sites. I 
used a Groovy subclass whose getMetaClass() throws, standing in for a scoped 
proxy:
   
   | Shape                            | Before (indy) | PR (indy) | PR 
(classic) |
   
|----------------------------------|---------------|-----------|--------------|
   | Report r = p, (Report) p         | throws        | works     | works       
 |
   | boolean b = p, if (p), p ? :, !p | throws        | throws    | throws      
 |
   | p == p                           | works         | works     | works       
 |
      It's worth one sentence on the JIRA so Grails users don't expect if 
(scopedProxy) to work outside the scope.
   5. Small performance gain. Casting a GroovyObject no longer calls 
getMetaClass() on every invocation. Values with per-instance metaclasses no 
longer fail the guard and relink the cast site each time.
   
   Backports:
   - The PR links 6.0.x and 5.1.x commits, and Daniel agrees backporting is 
warranted. I didn't review James's hand-applied 5.1.x version; it's worth 
running the new test on that branch before you pick it.
   - The PR says the bug reproduces on 4.0.33, and GROOVY_4_0_X has the same 
unconditional receiver instanceof GroovyObject guard in setGuards. Grails 7 
runs on Groovy 4.0.x. If the scoped-proxy report comes from a Grails 7 app, 
4.0.x may be the backport that actually matters, so ask James whether he wants 
it there too.
   
   Nits (none blocking):
   - The test uses static counters, which is fine as long as tests run 
sequentially in one JVM.
   - No test uses a real proxy subclass, but Watched covers the same path.
   - The existing "call site stays uncached" log line in doCallSiteTargetSet is 
misleading: the target is in fact set later.




> Indy: casting a GroovyObject calls getMetaClass() on the value being cast
> -------------------------------------------------------------------------
>
>                 Key: GROOVY-12557
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12557
>             Project: Groovy
>          Issue Type: Bug
>    Affects Versions: 4.0.33, 5.1.3, 6.0.0
>            Reporter: James Daugherty
>            Priority: Major
>
> With invokedynamic, a cast of a {{GroovyObject}} that already has the target 
> type, such as {{Widget widget = value}}, calls {{value.getMetaClass()}}: once 
> to build the call site's guard and again on each call through it. With 
> classic call sites the same cast calls nothing on the value.
> That is observable whenever {{getMetaClass()}} is not a plain field read. A 
> Spring class-based (CGLIB) proxy of a Groovy class forwards every public 
> method, {{getMetaClass()}} included, to its target. For a step-, job-, 
> request- or session-scoped bean the target exists only while the scope is 
> active, so merely assigning the proxy to a typed variable outside the scope 
> fails:
> {noformat}
> org.springframework.beans.factory.support.ScopeNotActiveException: Error 
> creating bean with name 'scopedTarget.widget': Scope 'step' is not active for 
> the current thread
>   at 
> org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)
>   at 
> org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698)
>   at Widget$$SpringCGLIB$$0.getMetaClass
>   at org.codehaus.groovy.vmplugin.v8.Selector$MethodSelector.setGuards
>   at org.codehaus.groovy.vmplugin.v8.Selector$CastSelector.castAndSetGuards
>   at org.codehaus.groovy.vmplugin.v8.Selector$CastSelector.setCallSiteTarget
>   at org.codehaus.groovy.vmplugin.v8.IndyInterface.fallback
> {noformat}
> A Grails application moving from classic call sites to invokedynamic (the 
> Grails 9 default) meets this in code that handles scoped proxies dynamically.
> *Reproducer:* 
> https://github.com/jdaugherty/groovy-indy-cast-getmetaclass-reproducer 
> ({{./run.sh 4.0.33 5.1.3 6.0.0}}). With indy the cast {{Watched typed = 
> value}} makes 2 {{getMetaClass()}} calls on the value and fails when 
> {{getMetaClass()}} throws; with classic call sites it makes none. Reproduced 
> on 4.0.33, 5.1.3 and 6.0.0.
> *Cause:* {{Selector.CastSelector.castAndSetGuards()}} reuses 
> {{MethodSelector.setGuards(args[0])}}, which calls {{getMetaClass()}} on a 
> {{GroovyObject}} receiver to bind a {{SAME_MC}} guard. Only a cast to boolean 
> selects a method ({{asBoolean}}) through the metaclass; every other cast is 
> decided by the class of the value, which the argument guards and the class 
> SwitchPoint already cover.
> *Proposed fix:* keep the receiver metaclass guard for the {{asBoolean}} path 
> only, so a cast decided by the value's class never calls into the value, as 
> with classic call sites. A patch with tests follows as a pull request against 
> master, with backports for the 6.0.x and 5.1.x lines.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to