Paul King created GROOVY-12460:
----------------------------------

             Summary: 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
             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)

Reply via email to