[ 
https://issues.apache.org/jira/browse/GROOVY-12460?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-12460:
-------------------------------
    Description: 
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.


  was:
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.



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

Reply via email to