[
https://issues.apache.org/jira/browse/GROOVY-12399?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18114277#comment-18114277
]
ASF GitHub Bot commented on GROOVY-12399:
-----------------------------------------
daniellansun commented on PR #2922:
URL: https://github.com/apache/groovy/pull/2922#issuecomment-5636301822
I’d hesitate to land this as-is. The unmatched throw is fixed and the tests
for that path are good. The encoding feels heavier than the language rule
needs, and statement-position `return` / `break` / `continue` still fail.
## The rule looks right
After GROOVY-12255, an arrow `switch` became a `SwitchExpression` whenever
the arms looked like expressions:
```groovy
def f(int i) {
def r = 'none'
switch (i) { case 1 -> r = 'one' }
r
}
f(42) // IllegalStateException; type-checker error under @TypeChecked
```
Java (JEP 361) decides statement vs expression by **position**, not by arrow
vs colon. The spec in this commit matches that. The implementation is the
part I’d like to revisit.
## Three modes, cloned into the parser
| Position | AST | Unmatched |
|---|---|---|
| Value unused | `SwitchStatement` | no-op |
| Implicit return | `SwitchExpression` + `_SWITCH_UNMATCHED_YIELDS_NULL` |
`null` |
| Real expression (`return switch`, assignment, argument, arrow-arm value) |
`SwitchExpression` | throw / exhaustiveness |
The third mode is driven by `AstBuilder.isImplicitReturnPosition`, an ANTLR
parent walk over labelled statements, `if`/`else`, `try`/`catch`, method /
closure / lambda / script bodies, colon-switch groups, trailing `break`, and
constructor vs method.
That is `ReturnAdder` on a different tree. `ReturnAdder` already knows those
sites; STC already uses it; classgen already emits `return null` when a body
`maybeFallsThrough`. None of that is reused, and the walks already disagree:
for a non-last colon case without `break`, the parse-tree walk still treats
the arm as an implicit return; `ReturnAdder` does not.
It also adds ~90 lines to `AstBuilder`, which is already very large.
## Simpler encodings
Two options; both look simpler than what we have.
**Prefer: make statement-position arrow `switch` a `SwitchStatement`**,
including implicit return. GROOVY-12255 already does this for incomplete
block arms.
- Matched: `ReturnAdder` already turns `case 1 -> 'one'` into `return 'one'`.
- Unmatched: the switch falls through; `AsmClassGenerator` already does
`ACONST_NULL; ARETURN` for a reference return.
- Exhaustiveness, ISE, and ICCE never apply.
- The flag, `writeUnmatchedSelector`, the STC early-return, and the
`complete && !unmatchedYieldsNull(...)` guard can go.
`visitCommandExprAlt` stays close to GROOVY-12255: rewrite unless the
statement is an arrow arm of an enclosing switch expression.
`isSwitchValueUsed`’s arrow-vs-colon check still belongs there; implicit
return does not.
Caveat: for a primitive return type, fall-through yields `0` / `false`, not
unbox-of-null. 4.x returned `null` and NPEd on unbox. If that matters,
document it — or keep a `SwitchExpression` only for primitives. I wouldn’t
keep the third mode for `def` / `Object` / `String` just to preserve AST
shape. `voidMethodAndConstructorEndWithAStatement` asserting
`SwitchExpression` tests the encoding, not the language.
**If the third mode stays, stamp it with `ReturnAdder`.** Parse time keeps
only the incomplete-arm rewrite. After the AST exists, run `ReturnAdder`
(`doAdd = false`) and stamp the flag when the implicit-return expression is
a `SwitchExpression`; rewrite leftover
`ExpressionStatement(SwitchExpression)`
nodes to `SwitchStatement`. Arrow-arm values are already wrapped in `yield`
by then, so they stay strict. Closures need a small walk (or a helper pulled
out of `ReturnAdder`). Same concept, right layer, no second ReturnAdder.
## `return` / `break` / `continue` still see an expression
`visitCommandExprAlt` rewrites only after the arms have been visited.
`visitReturnStmtAlt` still rejects `return` whenever a
`SwitchExpressionContext` is on the stack, so this Java-legal statement
does not compile:
```groovy
def f(int i) {
switch (i) {
case 1 -> { return 'one' }
}
'none'
}
```
Same for unlabeled `break` / `continue`. JEP 361 allows those in statements.
The GROOVY-12255 tests only cover `def r = switch ... { return }`. This was
raised on the PR and is not addressed here.
Decide statement vs expression **before** visiting arms
(`isSwitchUsedAsStatement` / `isArrowGroup` already exist for yield rules)
and consult that in `visitReturnStmtAlt` / `visitBreakStatement` /
`visitContinueStatement`. `isEnclosingSwitchUsedAsStatement()` is close;
`visitReturnStmtAlt` does not use it.
## The flag is on the wrong type
`UNMATCHED_YIELDS_NULL` means “this `SwitchExpression` is lenient.” It lives
on `SwitchExpressionUtils`, which classifies labels for tableswitch /
lookupswitch / enum-name dispatch. If the flag stays, it belongs on
`SwitchExpression`.
`writeDefaultOrThrow` still prefers `completeEnum` over the flag. Callers
were updated (`complete = false` when the flag is set), so behaviour looks
fine today; the method itself still has an implicit contract. Safer if the
flag wins inside `writeDefaultOrThrow`.
Dropping `OperandStack.doGroovyCast` in `writeUnmatchedSelector` is right:
STC wraps result types, dynamic compilation sees `Object`, `null` needs no
cast.
## Tests
`Groovy12399` is in the right shape and covers a lot: unused statement,
class labels, loops, `if` not in return position, colon-arm nesting,
implicit return through `if` / `try` / colon switch, closures, scripts,
complete-enum `null` and ICCE-vs-null, void/ctor vs value method.
Gaps for the new rules:
- `return` / `break` / `continue` in a statement-position arrow block.
- Nested `case 1 -> switch (y) { case 2 -> 'two' }` as implicit return of
the outer. The inner stays strict (unmatched throws). May be intended;
4.x returned `null`.
- Primitive implicit return: `int f(int i) { switch (i) { case 1 -> 1 } }` —
NPE vs `0` is the `SwitchExpression` vs `SwitchStatement` fork.
- `@CompileStatic` on the exhaustiveness-in-expression-position cases
(only `@TypeChecked` today).
The `return switch` edits in `Groovy12255` follow from lenient implicit
return; a one-line comment on the enum test (as on the exhaustiveness test)
would make that obvious.
`COMPATIBILITY.md` now describes the implicit-return exception, then **Who
is affected** still says every dynamic switch expression throws on unmatched.
## What looks good
- `isArrowGroup`: a `switch` in a colon arm of a switch expression is a
statement; the value comes from `yield`. The nested-colon test matches.
- Complete-enum ICCE and null-selector ISE consult the flag on the optimized
path; the two complete-enum tests lock that in.
- `core-semantics.adoc` matches the JIRA: `return switch (...)` opts into
the strict rules.
## Suggestion
Keep the language rule; consider dropping this encoding.
Either rewrite statement-position arrow switches (including implicit return)
to `SwitchStatement` and let `ReturnAdder` plus fall-through do the work, or
stamp lenient expressions with `ReturnAdder` after the AST exists. Either
way we can delete `isImplicitReturnPosition` / `returnsValue` /
`isLastStatement` from `AstBuilder`. Decide statement vs expression before
visiting arms so `return` / `break` / `continue` follow JEP 361.
As it stands this is a working patch that puts a compiler concept in the
parser, and it doesn’t quite finish the Java alignment the issue argues for.
Happy to be talked out of that.
> Arrow switch statement without default throws IllegalStateException when no
> arm matches
> ---------------------------------------------------------------------------------------
>
> Key: GROOVY-12399
> URL: https://issues.apache.org/jira/browse/GROOVY-12399
> Project: Groovy
> Issue Type: Bug
> Affects Versions: 6.0.0-RC-2
> Reporter: Paul King
> Priority: Major
>
> Since GROOVY-12255 (first-class switch expressions), a {{switch}} used as a
> *statement* is only rewritten to a {{SwitchStatement}} when an arrow arm is a
> block that does not {{yield}}. A single expression arm, or a block containing
> exactly one expression statement, becomes an implicit {{yield}} and the
> switch stays a {{SwitchExpression}} even though its value is discarded. Such
> a switch now inherits the expression rules: an unmatched selector throws
> {{IllegalStateException}} in dynamic code and a missing {{default}} is a
> compile error under {{@TypeChecked}} / {{@CompileStatic}}.
> {code:groovy}
> def f(int i) {
> def r = 'none'
> switch (i) {
> case 1 -> r = 'one'
> }
> r
> }
> f(42)
> {code}
> Groovy 5.1.2: returns {{'none'}}. Groovy 6.0.0-RC-1:
> {noformat}
> java.lang.IllegalStateException: the switch expression does not cover the
> value 42
> {noformat}
> With {{@TypeChecked}} on {{f}}:
> {noformat}
> the switch expression does not cover all possible input values
> {noformat}
> The equivalent Java compiles and runs, returning {{"none"}}: an arrow-form
> switch *statement* needs no {{default}} and does nothing when unmatched.
> {code:java}
> static String f(int i) {
> String r = "none";
> switch (i) {
> case 1 -> r = "one";
> }
> return r;
> }
> f(42); // "none"
> {code}
> The boundary in Groovy depends on arm shape rather than position, which shows
> it is not intended:
> {code:groovy}
> switch (i) { case 1 -> r = 'one' } // throws when unmatched
> switch (i) { case 1 -> { r = 'one' } } // throws when unmatched
> switch (i) { case 1 -> { r = 'one'; r += '!' } } // fine
> switch (i) { case 1 -> { if (i) r = 'one' } } // fine
> switch (i) { case 1: r = 'one' } // fine
> {code}
> h3. Background
> In Java (JEP 361) the arrow is not what makes a switch an expression: both
> switch statements and switch expressions may use either the arrow or the
> colon form. The arrow controls fall-through; the position controls whether a
> value is produced. A Java switch statement with arrow arms needs no
> {{default}} and does nothing when unmatched (exhaustiveness is only required
> for statements that use patterns or {{case null}}, JEP 441).
> Groovy's implementation grew up with the other reading. GROOVY-9272 (4.0)
> added the arrow form _as_ the switch expression: the grammar rule is
> {{switchExpression}}, it accepts only arrows, and a switch in statement
> position was simply a switch expression whose value nobody read. That was
> harmless while the lowering was a closure call that evaluated to {{null}}
> when nothing matched. GROOVY-12255 kept "arrow means expression", gave
> expressions real semantics (exhaustiveness, the unmatched throw) and added a
> statement rewrite only for the one shape that visibly broke, blocks without
> {{yield}}. The GROOVY-12255 compatibility notes describe the new behaviour
> for switch _expressions_ only and state that a switch used as a statement is
> a {{SwitchStatement}}; the statement-position tests in {{Groovy12255.groovy}}
> all carry a {{default}} or always match, so the unmatched case was never
> exercised.
> h3. Fix
> Classify by position rather than syntax, as Java does: a switch whose value
> is not used always builds a {{SwitchStatement}}, whatever the arm shape. This
> applies to dynamic and statically compiled code alike since the rewrite
> happens in the parser. The exhaustiveness error and the unmatched-selector
> throw stay reserved for switches whose value is used ({{def x = switch ...}},
> {{return switch ...}}, an argument, an arm of an enclosing switch expression).
> Implicit-return position, which Java has no equivalent for, is the one
> Groovy-specific case: the last statement of a method, closure or script body
> (followed through nested blocks, if/else branches, try/catch blocks and
> colon-switch arms, as the compiler's {{ReturnAdder}} does). Both idioms below
> were valid in 4.x/5.x and pull in opposite directions, so neither "statement"
> nor "strict expression" fits:
> {code:groovy}
> list.collect { switch (it) { case 1 -> 'a'; default -> 'b' } } // the
> closure's value is the switch
> switch (args[0]) { case 'run' -> run() } // last
> statement of a script; no match must not throw
> {code}
> There the switch keeps its value, but an unmatched selector yields {{null}}
> rather than throwing and the type checker does not require exhaustiveness,
> exactly as 4.x/5.x behaved. Code that wants the strict expression rules in
> that position writes "return switch (...) \{...}".
--
This message was sent by Atlassian Jira
(v8.20.10#820010)