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

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

paulk-asert opened a new pull request, #2701:
URL: https://github.com/apache/groovy/pull/2701

   …der, making static-compilation output path-dependent




> chooseBestMethod returns candidates in identity-hash order, making 
> static-compilation output path-dependent
> -----------------------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12156
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12156
>             Project: Groovy
>          Issue Type: Improvement
>          Components: Static compilation
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> h2. Description
> {{StaticTypeCheckingSupport.chooseBestMethod}} accumulates its result in a 
> {{HashSet}} and returns it as a list:
> {code:java}
> // StaticTypeCheckingSupport.java:1066
> Set<MethodNode> bestMethods = new HashSet<>(); // choose best method(s) for 
> each possible receiver
> for (ClassNode rcvr : duckType ? ((UnionTypeClassNode) 
> receiver).getDelegates() : new ClassNode[]{receiver}) {
>     ...
>     bestMethods.addAll(view);
> }
> return new LinkedList<>(bestMethods); // assumes caller wants remove to be 
> inexpensive
> {code}
> {{MethodNode}} overrides neither {{equals()}} nor {{hashCode()}} (unlike 
> {{ClassNode}}, whose {{hashCode()}} is text-based and stable). The 
> {{HashSet}} therefore buckets by *identity hash code*, and the returned 
> candidate order is whatever identity hashing produces.
> h3. Why this is not benign
> HotSpot's default identity hash ({{-XX:hashCode=5}}) is a *thread-local 
> xorshift PRNG*. The value an object receives depends on how many identity 
> hashes that thread has already handed out, and each thread has its own state. 
> So the order is not random per run — it is _path-dependent_:
> {noformat}
> main thread, 0 prior hashes : [m5, m3, m1, m2, m4]
> main thread, 3 prior hashes : [m5, m2, m4, m3, m1]
> main thread, 7 prior hashes : [m2, m5, m4, m1, m3]
> worker thread               : [m1, m2, m3, m5, m4]
> worker thread               : [m5, m3, m1, m4, m2]
> {noformat}
> (5 objects with no {{hashCode()}} override, added to a {{HashSet}}, 
> iterated.) A fresh single-threaded JVM repeating identical work reproduces 
> the same order — but the candidate order changes when the compile runs on a 
> different thread or after a different amount of prior work. In practice that 
> means a *long-lived, reused Gradle daemon*, *parallel compile workers*, or 
> simply *a different set of classes compiled earlier in the same JVM* can each 
> shift the order. Reproducible builds require identical output from identical 
> sources regardless of that state. The nondeterministic reflection order in 
> GROOVY-12149 also perturbs the sequence of {{hashCode()}} calls, so the two 
> defects amplify each other.
> h3. Where the order reaches generated code
> Most callers reduce to a single candidate and raise an ambiguity error 
> otherwise ({{StaticTypeCheckingVisitor:4397}}, {{:3581}}, 
> {{findMethodOrFail:5694}}) — those are safe, since an ambiguity error emits 
> no bytecode. But three callers accept a multi-candidate list *without error*:
> # *Union-type receiver (GROOVY-8965), {{StaticTypeCheckingVisitor:4385}}* — 
> the case that _deliberately_ fills the set with one best method per union 
> delegate, and so is a non-error path by construction:
> {code:java}
> if (mn.size() > 1 && obj instanceof UnionTypeClassNode) {
>     ClassNode returnType = mn.stream().map(MethodNode::getReturnType)
>                              
> .reduce(WideningCategories::lowestUpperBound).get();
>     call.putNodeMetaData(DYNAMIC_RESOLUTION, returnType);
> {code}
> {{Stream.reduce}} folds in list order, and {{lowestUpperBound}} is not 
> reliably order-insensitive (folding interface types can build differing 
> synthetic LUB nodes). The resulting inferred type drives casts and return 
> types in emitted bytecode.
> # *Method pointers / method references ({{::}}), 
> {{StaticTypeCheckingVisitor:3236-3268}}* — on more than one candidate it 
> calls {{extension.handleAmbiguousMethods}}, whose default implementation 
> ({{TypeCheckingExtension:190}}) returns the list unchanged, then applies the 
> same order-sensitive {{lowestUpperBound}} fold to type the closure.
> # *Macro dispatch, {{MacroCallTransformingVisitor:106-119}}* — 
> {{findMacroMethods}} returns {{chooseBestMethod(...)}} directly and the 
> caller loops over the candidates until one succeeds, so *the first candidate 
> wins*. When two macro methods both apply, identity-hash order decides which 
> one expands the call — a _different generated AST_, not merely a different 
> byte layout.
> Ambiguity *error messages* also list candidates in this order, so diagnostics 
> are unstable too.
> h3. Proposed fix
> Use an insertion-ordered set:
> {code:java}
> Set<MethodNode> bestMethods = new LinkedHashSet<>();
> {code}
> {{MethodNode}} has identity {{equals}}/{{hashCode}}, so this is a 
> semantics-preserving drop-in — deduplication behaviour is unchanged. It 
> yields a _meaningful_ order rather than an arbitrary one: candidates follow 
> the {{duckType}} loop, i.e. the union's declared delegate order. 
> {{StaticTypeCheckingVisitor:671}} already uses {{LinkedHashSet<MethodNode>}} 
> for {{PV_METHODS_ACCESS}}, so this matches existing practice.
> h3. Suggested test
> A {{@CompileStatic}} union-type receiver (per GROOVY-8965) whose delegates 
> declare the same method with different return types, asserting the inferred 
> {{DYNAMIC_RESOLUTION}} type; and a macro case with two applicable macro 
> methods, asserting which expansion wins. To exercise the path-dependence, run 
> the compile on a worker thread and/or after a varying number of identity-hash 
> allocations — with the {{HashSet}} the selection shifts, with 
> {{LinkedHashSet}} it does not.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to