[
https://issues.apache.org/jira/browse/GROOVY-12460?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18128840#comment-18128840
]
ASF GitHub Bot commented on GROOVY-12460:
-----------------------------------------
testlens-app[bot] commented on PR #2991:
URL: https://github.com/apache/groovy/pull/2991#issuecomment-6272494505
## ✅ All tests passed ✅
🏷️ Commit: 2f53efab48a663c84c0a00f694e356fef37da918
▶️ Tests: 122256 executed
⚪️ Checks: 32/32 completed
---
_Learn more about TestLens at
[testlens.app/docs](https://testlens.app/docs/features/pr-comment/)._
> Allow @Pure on parameters and local variables
> ---------------------------------------------
>
> Key: GROOVY-12460
> URL: https://issues.apache.org/jira/browse/GROOVY-12460
> Project: Groovy
> Issue Type: Improvement
> Components: groovy-typecheckers
> Reporter: Paul King
> Assignee: Paul King
> Priority: Major
> Fix For: 7.x
>
>
> h2. Summary
> Allow {{@Pure}} on parameters and local variables so {{PurityChecker}} can
> verify closures passed as arguments, and treat closure-taking GDK methods as
> pure when their closure is pure.
> h2. Background
> {{PurityChecker}} (incubating, 6.0) checks that a {{@Pure}} method has no
> side effects. It already descends into closure literals inside the method
> body, so an effect inside xs.each \{ println it } is reported. However,
> {{@Pure}} targets methods only, so there is no way to state that a parameter
> must receive a pure closure, and the checker has no notion of a method being
> pure *because* its closure argument is pure. Compiling the following under
> {{@TypeChecked(extensions = 'groovy.typecheckers.PurityChecker')}} on
> 6.0.0-beta-2 gives:
> {code:groovy}
> @Pure
> static int applyTwice(Closure<Integer> f, int x) { f(f(x)) }
> // @Pure warning: call to 'f.call()' — purity cannot be verified (consider
> adding @Pure)
> @Pure
> static List<Integer> doubled(List<Integer> xs) { xs.collect { it * 2 } }
> // @Pure warning: call to 'xs.collect()' — purity cannot be verified
> (consider adding @Pure)
> @Pure
> static int total(List<Integer> xs) { xs.inject(0) { a, b -> a + b } }
> // @Pure warning: call to 'xs.inject()' — purity cannot be verified (consider
> adding @Pure)
> {code}
> The consequences:
> * A {{@Pure}} method cannot take a callback at all.
> * A {{@Pure}} method cannot use {{collect}}, {{inject}}, {{findAll}}, stream
> pipelines or any other closure-taking idiom, which is how most Groovy code is
> written. The known-pure method list excludes closure-taking methods
> deliberately.
> * Library APIs that need a pure callback for correctness (a parallel
> reduction combiner, a memoized function, a transformation over a sensitive
> value) cannot say so.
> Two incidental observations from the same probe:
> * {{sink << v}} on a parameter passes silently because the left-shift
> operator is not a {{MethodCallExpression}}, while {{sink.add(v)}} is
> reported. Worth a separate ticket.
> * The "purity cannot be verified" messages are worded as warnings but raised
> as errors.
> h2. Proposal
> h3. 1. {{@Pure}} on parameters and locals
> Add {{ElementType.PARAMETER}} and {{ElementType.LOCAL_VARIABLE}} to the
> targets of {{groovy.transform.Pure}}. On a parameter it means "the argument
> passed here must be pure". On a local it means "the closure assigned here is
> checked once, and the variable may then be passed where a pure closure is
> required". The existing {{allows}} grades apply unchanged.
> h3. 2. Call-site checking
> When the checker sees a call whose resolved target has a {{@Pure}} parameter,
> the corresponding argument is accepted if it is:
> * a closure literal or lambda whose body passes the existing purity visitor,
> plus a captured-write rule (below);
> * a method pointer or method reference to a {{@Pure}} method;
> * a variable, parameter or field itself declared {{@Pure}}.
> Otherwise a static type error is reported.
> Inside a {{@Pure}} method body, a call through a parameter declared {{@Pure}}
> ({{f\(x)}}, {{f.call\(x)}}, {{f.apply\(x)}}) is accepted.
> h3. 3. Captured-write rule for closure literals
> Groovy closures share captured locals by reference, so an assignment, prefix
> or postfix operation whose target is a variable declared outside the closure
> is reported as a violation, in the same way a field write is today. Reads of
> captured variables remain fine.
> h3. 4. Closure-polymorphic GDK methods
> Add a list to the checker of GDK and {{java.util.stream}} methods that are
> pure whenever their closure argument is pure: {{collect}}, {{collectMany}},
> {{collectEntries}}, {{inject}}, {{findAll}}, {{find}}, {{any}}, {{every}},
> {{count}}, {{sum}}, {{max}}, {{min}}, {{sort}} (non-mutating form),
> {{groupBy}}, {{each}}, {{eachWithIndex}}, {{withIndex}}, {{takeWhile}},
> {{dropWhile}}, {{map}}, {{filter}}, {{flatMap}}, {{reduce}}, {{mapToInt}} and
> similar. When a call to one of these has a closure argument that passes the
> body check, the call is accepted.
> This list is one more of the lists {{PurityChecker}} already keeps
> (known-pure methods, non-deterministic methods, logging, metrics and I/O
> prefixes). It is not part of the GEP-25 model library and has no dependency
> on GEP-25. The long-term direction is to annotate the GDK methods themselves,
> for example {{@Pure collect(Iterable self, @Pure Closure transform)}}, at
> which point the checker's existing callee-annotation lookup makes the list
> unnecessary.
> h2. Examples after the change
> {code:groovy}
> @TypeChecked(extensions = 'groovy.typecheckers.PurityChecker')
> class Pipeline {
> @Pure
> static int applyTwice(@Pure Closure<Integer> f, int x) { f(f(x)) } //
> verifies
> @Pure
> static List<Integer> doubled(List<Integer> xs) { xs.collect { it * 2 } }
> // verifies
> @Pure
> static void shout(List<Integer> xs) { xs.each { println it } } // one
> error, for println only
> @Pure
> static int leak(List<Integer> xs, List<Integer> sink) {
> xs.each { sink.add(it) } //
> error: mutates parameter 'sink'
> xs.size()
> }
> }
> int counter = 0
> Pipeline.applyTwice({ it + 1 }, 3) // ok
> Pipeline.applyTwice(Maths.&square, 3) // ok if Maths.square is @Pure
> Pipeline.applyTwice({ counter++; it }, 3) // error: closure passed to
> @Pure parameter 'f' writes to captured variable 'counter'
> Pipeline.applyTwice({ println it; it }, 3) // error: 'println()' is a
> logging/output call
> @Pure Closure<Integer> inc = { it + 1 } // checked once here
> Closure<Integer> g = { it + 1 }
> Pipeline.applyTwice(inc, 3) // ok
> Pipeline.applyTwice(g, 3) // error: 'g' is not declared
> @Pure
> static void audit(@Pure(allows = 'LOGGING') Closure<Void> report) { ... }
> audit { log.info "checked $it" } // ok
> audit { new File('x').text = it } // error: I/O is not allowed
> {code}
> h2. Why it matters on its own
> * A {{@Pure}} method that takes a callback can be verified instead of refused.
> * {{@Pure}} methods can use ordinary collection idioms.
> * {{injectParallel}} and similar can require a combiner that is pure as well
> as associative, so a non-deterministic parallel fold is caught by two
> independent checkers.
> * Memoization and {{groovy.concurrent}} agent updates get a checked purity
> premise.
> * Any library wanting a side-effect-free transformation (for example a
> wrapper for sensitive values whose {{map}} must not leak) can state it and
> have it enforced.
> h2. Scope and limits
> * Static compilation only ({{@TypeChecked}} or {{@CompileStatic}}). Dynamic
> dispatch cannot be checked.
> * Closure bodies are walked with the existing visitor; no new effect
> categories.
> * Method references to Java methods without annotations remain unverifiable,
> as today.
> h2. Suggested tests
> * Callback parameter: pure literal accepted; captured write, logging, I/O and
> non-deterministic call each rejected with a message naming the parameter.
> * Each closure-polymorphic GDK method with a pure literal accepted, with an
> impure literal rejected for the body's reason only (no second "cannot be
> verified" error for the GDK call).
> * {{@Pure}} local assigned a literal, passed on; unannotated local rejected.
> * Method pointer to a {{@Pure}} method accepted; to an unannotated method
> rejected.
> * {{allows}} grade on a parameter honoured.
> * Lambda syntax behaves the same as closure syntax.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)