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.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]