Lower switches to if/conditional chains in a new SwitchDesugarer phase#417
Open
fabiomadge wants to merge 2 commits into
Open
Lower switches to if/conditional chains in a new SwitchDesugarer phase#417fabiomadge wants to merge 2 commits into
fabiomadge wants to merge 2 commits into
Conversation
8d1a691 to
12d4e27
Compare
4 tasks
711f08f to
a889b0b
Compare
a889b0b to
06b2046
Compare
fccec7f to
51290c8
Compare
51290c8 to
153b060
Compare
153b060 to
769a70a
Compare
Switch desugaring lives in its own TreeTranslator that runs as a
dedicated phase before suspend/lower/unsuspend in JavaLowerer.
Rationale: pre-LOWER tree mutations fall into two categories — defending
shapes LOWER would mutate (records, asserts; symmetric) and transforming
shapes into LOWER-resistant equivalents (switches; one-way). They share
a pipeline slot for non-incidental reasons (LOWER mangles switches into
bytecode-flavored fall-through that loses source positions and case
structure), but they're different concerns. Keeping them in one class
under the "Suspenders" name conflated reversible defense with one-way
lowering.
Suspenders shrinks back to its original symmetric concern: RECORD-flag
restoration around LOWER, and assert rewrite/restore around LOWER.
Switch handling — the if/conditional encoding from this commit's first
revision — moves verbatim to SwitchDesugarer with no semantic change.
Three latent bugs the round-trip masking used to cover are still fixed
in SwitchDesugarer:
- empty-cases base case in switchExpressionToConditional produced a
boolean falsepart that LOWER coerced via casts JavaToLaurelCompiler
can't accept; restructured to terminate recursion via the default
case explicitly (arrow form allows default in any position).
- switch statements with `default` not last lowered to
`else if (true) { ... }` making every later case unreachable;
partition before recursing.
- selector expressions were inlined into every generated equality
test, evaluating side-effecting selectors multiple times; hoist
into a fresh local ($switchSelectorN) and reference it from each
case. For switch expressions, the result is a synthesized
JCTree.LetExpr; JavaToLaurelCompiler#convertExpression gains a
case lowering it to a Laurel block.
The renames-aware LetExpr handling and SwitchDesugaring/SwitchInPostcondition
regression tests stay as in 2a's first revision.
Step 2a of PLAN.md §3. Step 2b will follow with diagnostic-quality
refusals for forms SwitchDesugarer can't encode (case-statement groups,
non-literal labels, pattern guards, case null).
Test plan:
- SwitchDesugaring regression test covers arrow-form expressions and
statements (with/without default, default-not-last, block-body,
non-exhaustive, char selector, non-trivial selector for hoist
coverage, Bad assertion case).
- SwitchInPostcondition is a regression test for the lambda-param
rename leak through the LetExpr's VarDef initializer.
- Switches.java stays @JVerifyTest(skip = "..."): the two case-null
methods need Step 9 before the full fixture flips.
- Full suite: 115 / 0 / 72 (was 113 / 0 / 72; +SwitchDesugaring,
+SwitchInPostcondition).
caseToExpression already refuses STATEMENT-form non-default cases. The partition loop at line 113-121, however, captures a STATEMENT-form default case unconditionally. switchToIf would then build a desugared shape: Block([selVarDecl, Block(defaultCase.stats)]) If the default body contains a 'break;' targeting the switch, the desugared form orphans that break — it now targets the enclosing loop (if any) instead of the (now-removed) switch. JavaToLaurelCompiler's JCBreak handling resolves through its own labelStack and would emit exit() to the wrong target. The bug is dormant on main (no JCBreak handling yet) but becomes a silent mistranslation once #418 (break/continue support) lands. Fix: refuse STATEMENT-form switches uniformly. Two-line guard at the top of switchToIf, symmetric with caseToExpression's per-case STATEMENT refusal. Refused trees fall through to LOWER, which mangles them into shapes JavaToLaurelCompiler doesn't accept; Step 1 then surfaces them as method-level skips. Step 2b (#419) replaces the silent skip with per-construct refusal diagnostics.
769a70a to
65f09cc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 2a of PLAN.md §3 — switch desugaring.
Resolves (partially)
Summary
Switch lowering moves into its own dedicated
SwitchDesugarerphase that runs beforesuspend(lower(unsuspend(_)))inJavaLowerer.JavaToLaurelCompilerconsumes the if/conditional shape directly, so the rewrite is one-way — no UNSUSPEND-style reversal.Pre-LOWER tree mutations fall into two categories:
Suspenders.SwitchDesugarer.They share a pipeline slot for non-incidental reasons (LOWER mangles switches into bytecode-flavored fall-through that loses source positions and case structure), but they're different concerns. Splitting them clarifies the contract:
Suspendersshrinks back to its narrow symmetric purpose;SwitchDesugarerowns the one-way encoding plus the latent-bug fixes the prior round-trip used to mask.What's in this PR
1. New
SwitchDesugarerclass withswitchToIf(statement form) andswitchExpressionToConditional(expression form). Suspenders no longer touches switches.2. Fix latent bug: switch-expression empty-cases base case. The old recursion produced a
falseliteral as falsepart, harmless when UNSUSPEND restored the switch but a real type mismatch when the if/conditional shape survives. LOWER inserts coercion castsJavaToLaurelCompilerdoesn't accept. Restructured to locate the default case explicitly (arrow form allows it anywhere in the list) and use its body as the innermost falsepart.3. Fix latent bug: switch-statement default-not-last.
switchToIfproduced an if-cascade in source order, sodefaultnot last lowered toelse if (true) { ... }making every later case unreachable — silent miscompile. Partition non-default cases from the default before building the cascade, with the default body in the trailingelse.4. Fix latent bug: selector evaluated multiple times. Selectors were inlined into every generated equality test.
switch (foo()) { case 0 -> ...; case 1 -> ...; }lowered toif (foo() == 0) ... else if (foo() == 1) ...— multiple evaluations offoo(), costing precision in verification for impure selectors. Hoist into a fresh local ($switchSelectorN) and reference it from each case. For switch expressions the result is a synthesizedJCTree.LetExpr;JavaToLaurelCompiler#convertExpressiongains a case lowering it to a Laurel block (aStmtExprusable in expression position).5. Make
convertStatementrenames-aware.convertExpressionalready took arenamesmap (used to rename postcondition-lambda parameters to "result");convertStatementdid not. The newLetExprarm reachesconvertStatementfrom inside a renamed scope, so the asymmetry now matters: a hoisted-selector'svar $sel = lambdaParamwould be lowered with the original lambda-param name and Strata would see an unbound identifier. ThreadingrenamesthroughconvertStatement,convertBlock, andextractLoopPartskeeps the LetExpr arm a one-liner and prevents the same bug class in future code paths that reachconvertStatementfrom a renamed scope.SwitchDesugaring.switchInPostconditionis the regression test.6. Refuse STATEMENT-form switches uniformly.
caseToExpressionalready refused STATEMENT-form non-default cases; the partition loop, however, captured a STATEMENT-form default case unconditionally, then wrapped its body in a plainJCBlock. If the body containsbreak;(targeting the original switch), the desugared form orphans that break — it now retargets the enclosing loop instead of the (now-removed) switch. Without this guard, the bug would have become a silent mistranslation when #418 (break/continue support) landed inmain. Two-line guard at the top ofswitchToIf, symmetric withcaseToExpression's per-case refusal.7. Translate compile-time constant references in expression position.
case CONST_INT(whereCONST_INTis astatic final int CONST_INT = 42),int x = SomeClass.MAX, etc. javac keeps these asJCFieldAccesswith the constant value attached on the expression's type.JavaToLaurelCompiler#convertExpressiongains aJCFieldAccessarm gated onfa.type.constValue() != nullthat mirrorsconvertLiteral's tag-based dispatch.SwitchDesugaring.switchExprCaseConstcovers same-class and external-class constants. Empirically verified that javac'sType.constValue()returnsIntegerforboolean/char(notBoolean/Character), so the dispatch is on the declared type tag, not the runtime value class.What flips, what doesn't
Switches.javastays@JVerifyTest(skip = "...")because two of its eleven methods usecase nullonint @Nullable [], which depends on Step 9.Forms
SwitchDesugarercan't encode (case-statement groups, pattern guards,case null) currently fall through to LOWER, which mutates them into shapes the translator doesn't accept; those surface as method-level skips via Step 1's per-method catch inJavaToLaurelCompiler. Step 2b (#419) replaces those skips with diagnostic-quality refusals at SwitchDesugarer time.On the LetExpr arm scope
LetExpris also emitted by javac's Lower for boxed compound-assign, boxed postops, switch-on-string, and pattern-matching switches. All of those require features JVerify doesn't support yet, blocking those paths before LetExpr would reach the new arm. Today, only SwitchDesugarer-synthesized LetExpr reaches it. If a future feature flip enables one of those upstream paths, the broad arm consumes those LetExprs too — semantically correct (a LetExpr is a block-with-final-expression) but a silent scope expansion worth being aware of.Test plan
./gradlew :verifier:testpasses.JavaViolationExceptionstack traces in test output.SwitchDesugaring.javacovers arrow-form switches end-to-end (statements, expressions, with default in any position, char selector, block body, non-exhaustive, non-trivial selector for hoist coverage, switch expression in postcondition lambda, compile-time constant case labels, Bad assertion).