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

Paul King updated GROOVY-12324:
-------------------------------
    Description: 
h2. Summary

{{ChannelSelect}} has no way to disable one branch of a select. Every offer 
passed to
{{ChannelSelect.from(...)}} / {{ChannelSelect.offers(...)}} is always live, so 
the classic guarded ALT --
"take a PUT only while the buffer has room, take a GET only while it has 
content" -- cannot be written.

In CSP terms {{ChannelSelect}} today has only the right-hand half of an ALT 
guard. occam's grammar is
{{guard ::= input | boolean & input | boolean & SKIP}}; GROOVY-12323 added the 
send offers (output guards);
the {{boolean &}} part is still missing.

h2. Why the obvious workaround does not work

The only way to express a guard today is to vary the argument list:

{code:groovy}
ChannelSelect alt = counter == 0        ? ChannelSelect.from(get)
                  : counter == elements ? ChannelSelect.from(put)
                                        : ChannelSelect.from(put, get)
ChannelSelect.Result r = await alt.select()
if (r.index == 0) { /* which channel is this? */ }
{code}

{{Result.getIndex()}} is *positional*, so masking off anything other than the 
last branch renumbers the
rest: {{get}} is branch 1 in one arm and branch 0 in another, and no 
branch-wise code or specification
means anything. The workaround silently changes what {{index}} denotes rather 
than failing.

This is not hypothetical -- it is the canonical teaching example. Jon 
Kerridge's _Using Concurrency and
Parallelism Effectively_ c05 {{Queue.groovy}} is a bounded circular buffer that 
masks *both* guards, and
JCSP's own {{Alternative}} javadoc uses the same shape (the "canteen"). Neither 
can be ported.

h2. Prior art

The strongest precedent is Groovy's own previous concurrency library. *GPars 
shipped exactly this, called
it "guards", and used the identical {index, value} result shape:*

{code:java}
// groovyx.gpars.dataflow.Select
SelectResult<T> select(List<Boolean> mask)
SelectResult<T> prioritySelect(List<Boolean> mask)
Promise<SelectResult<T>> selectToPromise(List<Boolean> mask)
// "Only the channels marked with 'true' in the supplied mask will be 
considered."
{code}

{{SelectResult}} is {{getIndex()}} + {{getValue()}} -- the same contract as 
{{ChannelSelect.Result}} -- and
index stability is explicit in its implementation 
({{GuardedSelectRequest.matchesMask(index)}};
{{SelectBase}} reports the original position). So this reads as a regression 
against GPars, not a new idea.

|| System || Disable a branch? || How || Winner identified by || Index stable? 
||
| JCSP | yes | {{select/priSelect/fairSelect(boolean[] preCondition)}} | index 
| *yes* |
| GPars | yes | {{select(List<Boolean> mask)}}, {{setGuard(i, b)}} | index | 
*yes* |
| occam / occam-pi | yes | grammar: {{bool & c ? x}} | the alternative | no 
index |
| Ada | yes | {{when Cond =>}}; entry barriers | the accept | no index |
| Erlang / Elixir | yes | {{Pattern when Guard ->}} | the clause | no index |
| Go | indirectly | {{nil}} channel never proceeds | the case body | no index |
| Kotlin coroutines | yes | conditional clause registration in {{select \{ \}}} 
| the lambda | no index |
| Clojure core.async | yes | build the {{ports}} vector per call | *the port 
object* | not observable |
| Concurrent ML / PyCSP | yes | omit, or {{never}} | value / channel end | no 
index |
| Rust crossbeam | yes | {{Select::remove\(i)}}, ids never reused | index | 
*yes* |
| C++CSP2 | *no mask* | only {{replaceGuard(i, g)}} | index | fixed slots |
| Limbo (Inferno) | *no* | -- | {{(index, value)}} tuple | -- |
| Groovy 6 today | *no* | -- | {{Result.getIndex()}} | breaks on rebuild |

Limbo is the cautionary one: its array {{alt}} returns "a tuple containing the 
index of the channel over
which a communication was received and the value received" -- Groovy 6's exact 
shape, with Groovy 6's exact
gap. Go dropped the index and the problem disappeared. Groovy cannot drop it 
without breaking API, which is
why the mask (which preserves a positional result) is the fitting approach here.

h2. Proposal

Add a per-call precondition mask:

{code:java}
public Awaitable<Result> select(boolean... enabled)
{code}

* {{enabled.length}} must equal {{offers.size()}}; otherwise 
{{IllegalArgumentException}} (JCSP does the
  same, with a message naming both lengths).
* A branch whose flag is {{false}} is not registered for this selection. 
Indices are unchanged: the
  {{Result.index}} of an enabled branch is its position in the original offer 
list, exactly as today.
* The mask is an argument to {{select()}}, *not* a builder step. This is 
deliberate: a {{disable\(i)}}
  returning a new instance would either shift indices (the bug being fixed) or 
discard the {{lastWinner}}
  rotation state that a held {{fair()}} instance carries -- and GROOVY-12320 
exists precisely so that
  rotation state is meaningful. JCSP and GPars both put the mask on the 
selection call for the same reason.
* Applies to {{offers(...)}} as well as {{from(...)}}, so send offers can be 
guarded too. Keeping one
  offer-list + one mask (as JCSP keeps one {{Guard\[\]}} + one {{boolean\[\]}}) 
means a later timer, skip or
  barrier guard needs no second mechanism.

h3. All branches disabled

Raise, do not hang. Ada RM 9.7.1 specifies {{Program_Error}} "if all 
alternatives are closed and there is
no else part". JCSP, read at source, instead leaves {{selected = 
NONE_SELECTED}} and waits on its monitor
forever; that behaviour appears to be undocumented and is the one part of the 
JCSP design not worth
copying. Suggested: complete the returned {{Awaitable}} exceptionally with 
{{IllegalStateException("no
offer is enabled")}}.

h2. Implementation sketch

The class is already shaped for this; the change is contained to {{select()}} 
and {{registrationOrder()}}.

* In the registration loop, skip {{k}} where {{!enabled\[order\[k\]\]}}. 
{{branches\[index\]}} simply stays
  {{null}} -- {{withdraw()}} is *already* null-tolerant ({{if (branch != null) 
branch.cancel()}}), so
  nothing downstream changes.
* {{closedCount.incrementAndGet() == count}} becomes {{== enabledCount}}, or an 
all-closed select over a
  partially enabled set never reports {{ChannelClosedException("all channels 
are closed")}}.
* {{registrationOrder}} rotates over *enabled* indices only. {{lastWinner}} 
keeps storing the absolute
  index, so the {{FAIR}} start becomes "the next enabled index after 
{{lastWinner}}", and the
  {{PRIORITY}} and {{RANDOM}} orders are the same permutation restricted to the 
enabled set.
* No change to {{Offer}}, {{Result}}, the claim protocol, or {{resend}}.

The existing no-arg {{select()}} stays as {{select()}} = all enabled, so 
nothing existing changes.

h3. Optional complement

Additively, {{Result.getChannel()}} -- returning the {{AsyncChannel}} that 
committed. {{Offer}} already
holds its channel and the winning offer is in scope at the {{new Result(...)}} 
call, so it is one field.
That lets branch-wise code key on identity rather than position (the approach 
core.async, PyCSP and CML
take), which makes a dynamically rebuilt {{from(...)}} safe even without a 
mask. Independent of the mask,
and useful on its own.

h2. Reproduction

The gap was found by a static verifier for {{groovy.concurrent}} (the same tool 
that produced
GROOVY-12320 and GROOVY-12323). It now refuses the c05 bounded buffer with the 
reason:

{noformat}
Skipped channel verification for queue (channel 'put' is a branch of a guarded 
ALT whose branch POSITIONS
differ between the arms of its condition, so r.index would not name the same 
channel in every arm. Offer
the branches in the same positions (a guarded ALT may drop a branch only from 
the END) --
ChannelSelect.from(...) is positional, where JCSP's select(preCon) masks a 
guard while keeping its index;
this API has no equivalent)
{noformat}

A guarded ALT that drops only its *last* branch verifies today, which is the 
whole of what can be
expressed without this change.

h2. Notes for reviewers

* {{fair()}}'s javadoc currently guarantees that "every offer that is ready is 
taken within {{n}} calls,
  where {{n}} is the number of offers". Under a mask that wants one qualifier: 
{{n}} becomes the number of
  *enabled* offers, and an offer whose precondition is never true is never 
taken at all. This is inherent
  to guarded choice rather than a defect -- occam and Ada behave the same way 
-- but the sentence should be
  restated so the bound is not read as unconditional.
* JCSP's {{select(boolean\[\])}} is literally {{fairSelect(preCondition)}}; 
{{priSelect(boolean\[\])}}
  resets its favourite to 0. Groovy's policies are already explicit ({{fair()}} 
/ {{random()}} / default
  priority), so the mask needs no policy-specific overloads.


  was:
h2. Summary

{{ChannelSelect}} has no way to disable one branch of a select. Every offer 
passed to
{{ChannelSelect.from(...)}} / {{ChannelSelect.offers(...)}} is always live, so 
the classic guarded ALT --
"take a PUT only while the buffer has room, take a GET only while it has 
content" -- cannot be written.

In CSP terms {{ChannelSelect}} today has only the right-hand half of an ALT 
guard. occam's grammar is
{{guard ::= input | boolean & input | boolean & SKIP}}; GROOVY-12323 added the 
send offers (output guards);
the {{boolean &}} part is still missing.

h2. Why the obvious workaround does not work

The only way to express a guard today is to vary the argument list:

{code:groovy}
ChannelSelect alt = counter == 0        ? ChannelSelect.from(get)
                  : counter == elements ? ChannelSelect.from(put)
                                        : ChannelSelect.from(put, get)
ChannelSelect.Result r = await alt.select()
if (r.index == 0) { /* which channel is this? */ }
{code}

{{Result.getIndex()}} is *positional*, so masking off anything other than the 
last branch renumbers the
rest: {{get}} is branch 1 in one arm and branch 0 in another, and no 
branch-wise code or specification
means anything. The workaround silently changes what {{index}} denotes rather 
than failing.

This is not hypothetical -- it is the canonical teaching example. Jon 
Kerridge's _Using Concurrency and
Parallelism Effectively_ c05 {{Queue.groovy}} is a bounded circular buffer that 
masks *both* guards, and
JCSP's own {{Alternative}} javadoc uses the same shape (the "canteen"). Neither 
can be ported.

h2. Prior art

The strongest precedent is Groovy's own previous concurrency library. *GPars 
shipped exactly this, called
it "guards", and used the identical {index, value} result shape:*

{code:java}
// groovyx.gpars.dataflow.Select
SelectResult<T> select(List<Boolean> mask)
SelectResult<T> prioritySelect(List<Boolean> mask)
Promise<SelectResult<T>> selectToPromise(List<Boolean> mask)
// "Only the channels marked with 'true' in the supplied mask will be 
considered."
{code}

{{SelectResult}} is {{getIndex()}} + {{getValue()}} -- the same contract as 
{{ChannelSelect.Result}} -- and
index stability is explicit in its implementation 
({{GuardedSelectRequest.matchesMask(index)}};
{{SelectBase}} reports the original position). So this reads as a regression 
against GPars, not a new idea.

|| System || Disable a branch? || How || Winner identified by || Index stable? 
||
| JCSP | yes | {{select/priSelect/fairSelect(boolean[] preCondition)}} | index 
| *yes* |
| GPars | yes | {{select(List<Boolean> mask)}}, {{setGuard(i, b)}} | index | 
*yes* |
| occam / occam-pi | yes | grammar: {{bool & c ? x}} | the alternative | no 
index |
| Ada | yes | {{when Cond =>}}; entry barriers | the accept | no index |
| Erlang / Elixir | yes | {{Pattern when Guard ->}} | the clause | no index |
| Go | indirectly | {{nil}} channel never proceeds | the case body | no index |
| Kotlin coroutines | yes | conditional clause registration in {{select \{ \}}} 
| the lambda | no index |
| Clojure core.async | yes | build the {{ports}} vector per call | *the port 
object* | not observable |
| Concurrent ML / PyCSP | yes | omit, or {{never}} | value / channel end | no 
index |
| Rust crossbeam | yes | {{Select::remove(i)}}, ids never reused | index | 
*yes* |
| C++CSP2 | *no mask* | only {{replaceGuard(i, g)}} | index | fixed slots |
| Limbo (Inferno) | *no* | -- | {{(index, value)}} tuple | -- |
| Groovy 6 today | *no* | -- | {{Result.getIndex()}} | breaks on rebuild |

Limbo is the cautionary one: its array {{alt}} returns "a tuple containing the 
index of the channel over
which a communication was received and the value received" -- Groovy 6's exact 
shape, with Groovy 6's exact
gap. Go dropped the index and the problem disappeared. Groovy cannot drop it 
without breaking API, which is
why the mask (which preserves a positional result) is the fitting approach here.

h2. Proposal

Add a per-call precondition mask:

{code:java}
public Awaitable<Result> select(boolean... enabled)
{code}

* {{enabled.length}} must equal {{offers.size()}}; otherwise 
{{IllegalArgumentException}} (JCSP does the
  same, with a message naming both lengths).
* A branch whose flag is {{false}} is not registered for this selection. 
Indices are unchanged: the
  {{Result.index}} of an enabled branch is its position in the original offer 
list, exactly as today.
* The mask is an argument to {{select()}}, *not* a builder step. This is 
deliberate: a {{disable(i)}}
  returning a new instance would either shift indices (the bug being fixed) or 
discard the {{lastWinner}}
  rotation state that a held {{fair()}} instance carries -- and GROOVY-12320 
exists precisely so that
  rotation state is meaningful. JCSP and GPars both put the mask on the 
selection call for the same reason.
* Applies to {{offers(...)}} as well as {{from(...)}}, so send offers can be 
guarded too. Keeping one
  offer-list + one mask (as JCSP keeps one {{Guard\[\]}} + one {{boolean\[\]}}) 
means a later timer, skip or
  barrier guard needs no second mechanism.

h3. All branches disabled

Raise, do not hang. Ada RM 9.7.1 specifies {{Program_Error}} "if all 
alternatives are closed and there is
no else part". JCSP, read at source, instead leaves {{selected = 
NONE_SELECTED}} and waits on its monitor
forever; that behaviour appears to be undocumented and is the one part of the 
JCSP design not worth
copying. Suggested: complete the returned {{Awaitable}} exceptionally with 
{{IllegalStateException("no
offer is enabled")}}.

h2. Implementation sketch

The class is already shaped for this; the change is contained to {{select()}} 
and {{registrationOrder()}}.

* In the registration loop, skip {{k}} where {{!enabled\[order\[k\]\]}}. 
{{branches\[index\]}} simply stays
  {{null}} -- {{withdraw()}} is *already* null-tolerant ({{if (branch != null) 
branch.cancel()}}), so
  nothing downstream changes.
* {{closedCount.incrementAndGet() == count}} becomes {{== enabledCount}}, or an 
all-closed select over a
  partially enabled set never reports {{ChannelClosedException("all channels 
are closed")}}.
* {{registrationOrder}} rotates over *enabled* indices only. {{lastWinner}} 
keeps storing the absolute
  index, so the {{FAIR}} start becomes "the next enabled index after 
{{lastWinner}}", and the
  {{PRIORITY}} and {{RANDOM}} orders are the same permutation restricted to the 
enabled set.
* No change to {{Offer}}, {{Result}}, the claim protocol, or {{resend}}.

The existing no-arg {{select()}} stays as {{select()}} = all enabled, so 
nothing existing changes.

h3. Optional complement

Additively, {{Result.getChannel()}} -- returning the {{AsyncChannel}} that 
committed. {{Offer}} already
holds its channel and the winning offer is in scope at the {{new Result(...)}} 
call, so it is one field.
That lets branch-wise code key on identity rather than position (the approach 
core.async, PyCSP and CML
take), which makes a dynamically rebuilt {{from(...)}} safe even without a 
mask. Independent of the mask,
and useful on its own.

h2. Reproduction

The gap was found by a static verifier for {{groovy.concurrent}} (the same tool 
that produced
GROOVY-12320 and GROOVY-12323). It now refuses the c05 bounded buffer with the 
reason:

{noformat}
Skipped channel verification for queue (channel 'put' is a branch of a guarded 
ALT whose branch POSITIONS
differ between the arms of its condition, so r.index would not name the same 
channel in every arm. Offer
the branches in the same positions (a guarded ALT may drop a branch only from 
the END) --
ChannelSelect.from(...) is positional, where JCSP's select(preCon) masks a 
guard while keeping its index;
this API has no equivalent)
{noformat}

A guarded ALT that drops only its *last* branch verifies today, which is the 
whole of what can be
expressed without this change.

h2. Notes for reviewers

* {{fair()}}'s javadoc currently guarantees that "every offer that is ready is 
taken within {{n}} calls,
  where {{n}} is the number of offers". Under a mask that wants one qualifier: 
{{n}} becomes the number of
  *enabled* offers, and an offer whose precondition is never true is never 
taken at all. This is inherent
  to guarded choice rather than a defect -- occam and Ada behave the same way 
-- but the sentence should be
  restated so the bound is not read as unconditional.
* JCSP's {{select(boolean\[\])}} is literally {{fairSelect(preCondition)}}; 
{{priSelect(boolean\[\])}}
  resets its favourite to 0. Groovy's policies are already explicit ({{fair()}} 
/ {{random()}} / default
  priority), so the mask needs no policy-specific overloads.



> ChannelSelect: add per-select preconditions so a guard can be masked off 
> without renumbering branches
> -----------------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12324
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12324
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> h2. Summary
> {{ChannelSelect}} has no way to disable one branch of a select. Every offer 
> passed to
> {{ChannelSelect.from(...)}} / {{ChannelSelect.offers(...)}} is always live, 
> so the classic guarded ALT --
> "take a PUT only while the buffer has room, take a GET only while it has 
> content" -- cannot be written.
> In CSP terms {{ChannelSelect}} today has only the right-hand half of an ALT 
> guard. occam's grammar is
> {{guard ::= input | boolean & input | boolean & SKIP}}; GROOVY-12323 added 
> the send offers (output guards);
> the {{boolean &}} part is still missing.
> h2. Why the obvious workaround does not work
> The only way to express a guard today is to vary the argument list:
> {code:groovy}
> ChannelSelect alt = counter == 0        ? ChannelSelect.from(get)
>                   : counter == elements ? ChannelSelect.from(put)
>                                         : ChannelSelect.from(put, get)
> ChannelSelect.Result r = await alt.select()
> if (r.index == 0) { /* which channel is this? */ }
> {code}
> {{Result.getIndex()}} is *positional*, so masking off anything other than the 
> last branch renumbers the
> rest: {{get}} is branch 1 in one arm and branch 0 in another, and no 
> branch-wise code or specification
> means anything. The workaround silently changes what {{index}} denotes rather 
> than failing.
> This is not hypothetical -- it is the canonical teaching example. Jon 
> Kerridge's _Using Concurrency and
> Parallelism Effectively_ c05 {{Queue.groovy}} is a bounded circular buffer 
> that masks *both* guards, and
> JCSP's own {{Alternative}} javadoc uses the same shape (the "canteen"). 
> Neither can be ported.
> h2. Prior art
> The strongest precedent is Groovy's own previous concurrency library. *GPars 
> shipped exactly this, called
> it "guards", and used the identical {index, value} result shape:*
> {code:java}
> // groovyx.gpars.dataflow.Select
> SelectResult<T> select(List<Boolean> mask)
> SelectResult<T> prioritySelect(List<Boolean> mask)
> Promise<SelectResult<T>> selectToPromise(List<Boolean> mask)
> // "Only the channels marked with 'true' in the supplied mask will be 
> considered."
> {code}
> {{SelectResult}} is {{getIndex()}} + {{getValue()}} -- the same contract as 
> {{ChannelSelect.Result}} -- and
> index stability is explicit in its implementation 
> ({{GuardedSelectRequest.matchesMask(index)}};
> {{SelectBase}} reports the original position). So this reads as a regression 
> against GPars, not a new idea.
> || System || Disable a branch? || How || Winner identified by || Index 
> stable? ||
> | JCSP | yes | {{select/priSelect/fairSelect(boolean[] preCondition)}} | 
> index | *yes* |
> | GPars | yes | {{select(List<Boolean> mask)}}, {{setGuard(i, b)}} | index | 
> *yes* |
> | occam / occam-pi | yes | grammar: {{bool & c ? x}} | the alternative | no 
> index |
> | Ada | yes | {{when Cond =>}}; entry barriers | the accept | no index |
> | Erlang / Elixir | yes | {{Pattern when Guard ->}} | the clause | no index |
> | Go | indirectly | {{nil}} channel never proceeds | the case body | no index 
> |
> | Kotlin coroutines | yes | conditional clause registration in {{select \{ 
> \}}} | the lambda | no index |
> | Clojure core.async | yes | build the {{ports}} vector per call | *the port 
> object* | not observable |
> | Concurrent ML / PyCSP | yes | omit, or {{never}} | value / channel end | no 
> index |
> | Rust crossbeam | yes | {{Select::remove\(i)}}, ids never reused | index | 
> *yes* |
> | C++CSP2 | *no mask* | only {{replaceGuard(i, g)}} | index | fixed slots |
> | Limbo (Inferno) | *no* | -- | {{(index, value)}} tuple | -- |
> | Groovy 6 today | *no* | -- | {{Result.getIndex()}} | breaks on rebuild |
> Limbo is the cautionary one: its array {{alt}} returns "a tuple containing 
> the index of the channel over
> which a communication was received and the value received" -- Groovy 6's 
> exact shape, with Groovy 6's exact
> gap. Go dropped the index and the problem disappeared. Groovy cannot drop it 
> without breaking API, which is
> why the mask (which preserves a positional result) is the fitting approach 
> here.
> h2. Proposal
> Add a per-call precondition mask:
> {code:java}
> public Awaitable<Result> select(boolean... enabled)
> {code}
> * {{enabled.length}} must equal {{offers.size()}}; otherwise 
> {{IllegalArgumentException}} (JCSP does the
>   same, with a message naming both lengths).
> * A branch whose flag is {{false}} is not registered for this selection. 
> Indices are unchanged: the
>   {{Result.index}} of an enabled branch is its position in the original offer 
> list, exactly as today.
> * The mask is an argument to {{select()}}, *not* a builder step. This is 
> deliberate: a {{disable\(i)}}
>   returning a new instance would either shift indices (the bug being fixed) 
> or discard the {{lastWinner}}
>   rotation state that a held {{fair()}} instance carries -- and GROOVY-12320 
> exists precisely so that
>   rotation state is meaningful. JCSP and GPars both put the mask on the 
> selection call for the same reason.
> * Applies to {{offers(...)}} as well as {{from(...)}}, so send offers can be 
> guarded too. Keeping one
>   offer-list + one mask (as JCSP keeps one {{Guard\[\]}} + one 
> {{boolean\[\]}}) means a later timer, skip or
>   barrier guard needs no second mechanism.
> h3. All branches disabled
> Raise, do not hang. Ada RM 9.7.1 specifies {{Program_Error}} "if all 
> alternatives are closed and there is
> no else part". JCSP, read at source, instead leaves {{selected = 
> NONE_SELECTED}} and waits on its monitor
> forever; that behaviour appears to be undocumented and is the one part of the 
> JCSP design not worth
> copying. Suggested: complete the returned {{Awaitable}} exceptionally with 
> {{IllegalStateException("no
> offer is enabled")}}.
> h2. Implementation sketch
> The class is already shaped for this; the change is contained to {{select()}} 
> and {{registrationOrder()}}.
> * In the registration loop, skip {{k}} where {{!enabled\[order\[k\]\]}}. 
> {{branches\[index\]}} simply stays
>   {{null}} -- {{withdraw()}} is *already* null-tolerant ({{if (branch != 
> null) branch.cancel()}}), so
>   nothing downstream changes.
> * {{closedCount.incrementAndGet() == count}} becomes {{== enabledCount}}, or 
> an all-closed select over a
>   partially enabled set never reports {{ChannelClosedException("all channels 
> are closed")}}.
> * {{registrationOrder}} rotates over *enabled* indices only. {{lastWinner}} 
> keeps storing the absolute
>   index, so the {{FAIR}} start becomes "the next enabled index after 
> {{lastWinner}}", and the
>   {{PRIORITY}} and {{RANDOM}} orders are the same permutation restricted to 
> the enabled set.
> * No change to {{Offer}}, {{Result}}, the claim protocol, or {{resend}}.
> The existing no-arg {{select()}} stays as {{select()}} = all enabled, so 
> nothing existing changes.
> h3. Optional complement
> Additively, {{Result.getChannel()}} -- returning the {{AsyncChannel}} that 
> committed. {{Offer}} already
> holds its channel and the winning offer is in scope at the {{new 
> Result(...)}} call, so it is one field.
> That lets branch-wise code key on identity rather than position (the approach 
> core.async, PyCSP and CML
> take), which makes a dynamically rebuilt {{from(...)}} safe even without a 
> mask. Independent of the mask,
> and useful on its own.
> h2. Reproduction
> The gap was found by a static verifier for {{groovy.concurrent}} (the same 
> tool that produced
> GROOVY-12320 and GROOVY-12323). It now refuses the c05 bounded buffer with 
> the reason:
> {noformat}
> Skipped channel verification for queue (channel 'put' is a branch of a 
> guarded ALT whose branch POSITIONS
> differ between the arms of its condition, so r.index would not name the same 
> channel in every arm. Offer
> the branches in the same positions (a guarded ALT may drop a branch only from 
> the END) --
> ChannelSelect.from(...) is positional, where JCSP's select(preCon) masks a 
> guard while keeping its index;
> this API has no equivalent)
> {noformat}
> A guarded ALT that drops only its *last* branch verifies today, which is the 
> whole of what can be
> expressed without this change.
> h2. Notes for reviewers
> * {{fair()}}'s javadoc currently guarantees that "every offer that is ready 
> is taken within {{n}} calls,
>   where {{n}} is the number of offers". Under a mask that wants one 
> qualifier: {{n}} becomes the number of
>   *enabled* offers, and an offer whose precondition is never true is never 
> taken at all. This is inherent
>   to guarded choice rather than a defect -- occam and Ada behave the same way 
> -- but the sentence should be
>   restated so the bound is not read as unconditional.
> * JCSP's {{select(boolean\[\])}} is literally {{fairSelect(preCondition)}}; 
> {{priSelect(boolean\[\])}}
>   resets its favourite to 0. Groovy's policies are already explicit 
> ({{fair()}} / {{random()}} / default
>   priority), so the mask needs no policy-specific overloads.



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

Reply via email to