Skip to content

Commit 0a7f843

Browse files
authored
Match if sub cases (#23786)
Based on #23736
2 parents 17ba653 + c432c0f commit 0a7f843

24 files changed

+432
-32
lines changed

compiler/src/dotty/tools/dotc/ast/Trees.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,17 +608,25 @@ object Trees {
608608
extends TermTree[T] {
609609
type ThisTree[+T <: Untyped] = Match[T]
610610
def isInline = false
611+
def isSubMatch = false
611612
}
612613
class InlineMatch[+T <: Untyped] private[ast] (selector: Tree[T], cases: List[CaseDef[T]])(implicit @constructorOnly src: SourceFile)
613614
extends Match(selector, cases) {
614615
override def isInline = true
615616
override def toString = s"InlineMatch($selector, $cases)"
616617
}
618+
/** with selector match { cases } */
619+
final class SubMatch[+T <: Untyped] private[ast] (selector: Tree[T], cases: List[CaseDef[T]])(implicit @constructorOnly src: SourceFile)
620+
extends Match(selector, cases) {
621+
override def isSubMatch = true
622+
}
617623

618624
/** case pat if guard => body */
619625
case class CaseDef[+T <: Untyped] private[ast] (pat: Tree[T], guard: Tree[T], body: Tree[T])(implicit @constructorOnly src: SourceFile)
620626
extends Tree[T] {
621627
type ThisTree[+T <: Untyped] = CaseDef[T]
628+
/** Should this case be considered partial for exhaustivity and unreachability checking */
629+
def maybePartial(using Context): Boolean = !guard.isEmpty || body.isInstanceOf[SubMatch[T]]
622630
}
623631

624632
/** label[tpt]: { expr } */
@@ -1180,6 +1188,7 @@ object Trees {
11801188
type Closure = Trees.Closure[T]
11811189
type Match = Trees.Match[T]
11821190
type InlineMatch = Trees.InlineMatch[T]
1191+
type SubMatch = Trees.SubMatch[T]
11831192
type CaseDef = Trees.CaseDef[T]
11841193
type Labeled = Trees.Labeled[T]
11851194
type Return = Trees.Return[T]
@@ -1329,6 +1338,7 @@ object Trees {
13291338
def Match(tree: Tree)(selector: Tree, cases: List[CaseDef])(using Context): Match = tree match {
13301339
case tree: Match if (selector eq tree.selector) && (cases eq tree.cases) => tree
13311340
case tree: InlineMatch => finalize(tree, untpd.InlineMatch(selector, cases)(sourceFile(tree)))
1341+
case tree: SubMatch => finalize(tree, untpd.SubMatch(selector, cases)(sourceFile(tree)))
13321342
case _ => finalize(tree, untpd.Match(selector, cases)(sourceFile(tree)))
13331343
}
13341344
def CaseDef(tree: Tree)(pat: Tree, guard: Tree, body: Tree)(using Context): CaseDef = tree match {

compiler/src/dotty/tools/dotc/ast/tpd.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
143143
def InlineMatch(selector: Tree, cases: List[CaseDef])(using Context): Match =
144144
ta.assignType(untpd.InlineMatch(selector, cases), selector, cases)
145145

146+
def SubMatch(selector: Tree, cases: List[CaseDef])(using Context): Match =
147+
ta.assignType(untpd.SubMatch(selector, cases), selector, cases)
148+
146149
def Labeled(bind: Bind, expr: Tree)(using Context): Labeled =
147150
ta.assignType(untpd.Labeled(bind, expr))
148151

compiler/src/dotty/tools/dotc/ast/untpd.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
411411
def Closure(env: List[Tree], meth: Tree, tpt: Tree)(implicit src: SourceFile): Closure = new Closure(env, meth, tpt)
412412
def Match(selector: Tree, cases: List[CaseDef])(implicit src: SourceFile): Match = new Match(selector, cases)
413413
def InlineMatch(selector: Tree, cases: List[CaseDef])(implicit src: SourceFile): Match = new InlineMatch(selector, cases)
414+
def SubMatch(selector: Tree, cases: List[CaseDef])(implicit src: SourceFile): SubMatch = new SubMatch(selector, cases)
414415
def CaseDef(pat: Tree, guard: Tree, body: Tree)(implicit src: SourceFile): CaseDef = new CaseDef(pat, guard, body)
415416
def Labeled(bind: Bind, expr: Tree)(implicit src: SourceFile): Labeled = new Labeled(bind, expr)
416417
def Return(expr: Tree, from: Tree)(implicit src: SourceFile): Return = new Return(expr, from)

compiler/src/dotty/tools/dotc/config/Feature.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ object Feature:
3737
val modularity = experimental("modularity")
3838
val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions")
3939
val packageObjectValues = experimental("packageObjectValues")
40+
val subCases = experimental("subCases")
4041

4142
def experimentalAutoEnableFeatures(using Context): List[TermName] =
4243
defn.languageExperimentalFeatures

compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
590590
if (tree.isInline)
591591
if (selector.isEmpty) writeByte(IMPLICIT)
592592
else { writeByte(INLINE); pickleTree(selector) }
593+
else if tree.isSubMatch then { writeByte(LAZY); pickleTree(selector) }
593594
else pickleTree(selector)
594595
tree.cases.foreach(pickleTree)
595596
}

compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,6 +1539,9 @@ class TreeUnpickler(reader: TastyReader,
15391539
readByte()
15401540
InlineMatch(readTree(), readCases(end))
15411541
}
1542+
else if nextByte == LAZY then // similarly to InlineMatch we use an arbitrary Cat.1 tag
1543+
readByte()
1544+
SubMatch(readTree(), readCases(end))
15421545
else Match(readTree(), readCases(end)))
15431546
case RETURN =>
15441547
val from = readSymRef()

compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -394,9 +394,13 @@ class InlineReducer(inliner: Inliner)(using Context):
394394
case ConstantValue(v: Boolean) => (v, true)
395395
case _ => (false, false)
396396
}
397-
if guardOK then Some((caseBindings.map(_.subst(from, to)), cdef.body.subst(from, to), canReduceGuard))
398-
else if canReduceGuard then None
399-
else Some((caseBindings.map(_.subst(from, to)), cdef.body.subst(from, to), canReduceGuard))
397+
if !canReduceGuard then Some((List.empty, EmptyTree, false))
398+
else if !guardOK then None
399+
else cdef.body.subst(from, to) match
400+
case t: SubMatch => // a sub match of an inline match is also inlined
401+
reduceInlineMatch(t.selector, t.selector.tpe, t.cases, typer).map:
402+
(subCaseBindings, rhs) => (caseBindings.map(_.subst(from, to)) ++ subCaseBindings, rhs, true)
403+
case b => Some((caseBindings.map(_.subst(from, to)), b, true))
400404
}
401405
else None
402406
}

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2589,11 +2589,52 @@ object Parsers {
25892589
mkIf(cond, thenp, elsep)
25902590
}
25912591

2592+
/* When parsing (what will become) a sub sub match, that is,
2593+
* when in a guard of case of a match, in a guard of case of a match;
2594+
* we will eventually reach Scanners.handleNewLine at the end of the sub sub match
2595+
* with an in.currretRegion of the shape `InCase +: Indented :+ InCase :+ Indented :+ ...`
2596+
* if we did not do dropInnerCaseRegion.
2597+
* In effect, a single outdent would be inserted by handleNewLine after the sub sub match.
2598+
* This causes the remaining cases of the outer match to be included in the intermediate sub match.
2599+
* For example:
2600+
* match
2601+
* case x1 if x1 match
2602+
* case y if y match
2603+
* case z => "a"
2604+
* case x2 => "b"
2605+
* would become
2606+
* match
2607+
* case x1 if x1 match {
2608+
* case y if y match {
2609+
* case z => "a"
2610+
* }
2611+
* case x2 => "b"
2612+
* }
2613+
* This issue is avoided by dropping the `InCase` region when parsing match clause,
2614+
* since `Indetented :+ Indented :+ ...` now allows handleNewLine to insert two outdents.
2615+
* Note that this _could_ break previous code which relied on matches within guards
2616+
* being considered as a separate region without explicit indentation.
2617+
*/
2618+
private def dropInnerCaseRegion(): Unit =
2619+
in.currentRegion match
2620+
case Indented(width, prefix, Scanners.InCase(r)) => in.currentRegion = Indented(width, prefix, r)
2621+
case Scanners.InCase(r) => in.currentRegion = r
2622+
case _ =>
2623+
25922624
/** MatchClause ::= `match' `{' CaseClauses `}'
2625+
* | `match' ExprCaseClause
25932626
*/
25942627
def matchClause(t: Tree): Match =
25952628
atSpan(startOffset(t), in.skipToken()) {
2596-
Match(t, inBracesOrIndented(caseClauses(() => caseClause())))
2629+
val cases =
2630+
if in.featureEnabled(Feature.subCases) then
2631+
dropInnerCaseRegion()
2632+
if in.token == CASE
2633+
then caseClause(exprOnly = true) :: Nil // single case without new line
2634+
else inBracesOrIndented(caseClauses(() => caseClause()))
2635+
else
2636+
inBracesOrIndented(caseClauses(() => caseClause()))
2637+
Match(t, cases)
25972638
}
25982639

25992640
/** `match' <<< TypeCaseClauses >>>
@@ -3096,24 +3137,45 @@ object Parsers {
30963137
buf.toList
30973138
}
30983139

3099-
/** CaseClause ::= ‘case’ Pattern [Guard] `=>' Block
3100-
* ExprCaseClause ::= ‘case’ Pattern [Guard] ‘=>’ Expr
3140+
/** CaseClause ::= ‘case’ Pattern [Guard] (‘if’ InfixExpr MatchClause | `=>' Block)
3141+
* ExprCaseClause ::= ‘case’ Pattern [Guard] (‘if’ InfixExpr MatchClause | `=>' Expr)
31013142
*/
31023143
def caseClause(exprOnly: Boolean = false): CaseDef = atSpan(in.offset) {
31033144
val (pat, grd) = inSepRegion(InCase) {
31043145
accept(CASE)
31053146
(withinMatchPattern(pattern()), guard())
31063147
}
3107-
CaseDef(pat, grd, atSpan(accept(ARROW)) {
3108-
if exprOnly then
3109-
if in.indentSyntax && in.isAfterLineEnd && in.token != INDENT then
3110-
warning(em"""Misleading indentation: this expression forms part of the preceding catch case.
3111-
|If this is intended, it should be indented for clarity.
3112-
|Otherwise, if the handler is intended to be empty, use a multi-line catch with
3113-
|an indented case.""")
3114-
expr()
3115-
else block()
3116-
})
3148+
var grd1 = grd // may be reset to EmptyTree (and used as sub match body instead) if there is no leading ARROW
3149+
val tok = in.token
3150+
3151+
extension (self: Tree) def asSubMatch: Tree = self match
3152+
case Match(sel, cases) if in.featureEnabled(Feature.subCases) =>
3153+
if in.isStatSep then in.nextToken() // else may have been consumed by sub sub match
3154+
SubMatch(sel, cases)
3155+
case _ =>
3156+
syntaxErrorOrIncomplete(ExpectedTokenButFound(ARROW, tok))
3157+
atSpan(self.span)(Block(Nil, EmptyTree))
3158+
3159+
val body = tok match
3160+
case ARROW => atSpan(in.skipToken()):
3161+
if exprOnly then
3162+
if in.indentSyntax && in.isAfterLineEnd && in.token != INDENT then
3163+
warning(em"""Misleading indentation: this expression forms part of the preceding catch case.
3164+
|If this is intended, it should be indented for clarity.
3165+
|Otherwise, if the handler is intended to be empty, use a multi-line catch with
3166+
|an indented case.""")
3167+
expr()
3168+
else block()
3169+
case IF => atSpan(in.skipToken()):
3170+
// a sub match after a guard is parsed the same as one without
3171+
val t = inSepRegion(InCase)(postfixExpr(Location.InGuard))
3172+
t.asSubMatch
3173+
case other =>
3174+
val t = grd1.asSubMatch
3175+
grd1 = EmptyTree
3176+
t
3177+
3178+
CaseDef(pat, grd1, body)
31173179
}
31183180

31193181
/** TypeCaseClause ::= ‘case’ (InfixType | ‘_’) ‘=>’ Type [semi]

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,10 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
565565
selTxt ~ keywordStr(" match ") ~ blockText(cases)
566566
}
567567
case CaseDef(pat, guard, body) =>
568-
keywordStr("case ") ~ inPattern(toText(pat)) ~ optText(guard)(keywordStr(" if ") ~ _) ~ " => " ~ caseBlockText(body)
568+
val bodyText = body match
569+
case t: SubMatch => keywordStr(" if ") ~ toText(t)
570+
case t => " => " ~ caseBlockText(t)
571+
keywordStr("case ") ~ inPattern(toText(pat)) ~ optText(guard)(keywordStr(" if ") ~ _) ~ bodyText
569572
case Labeled(bind, expr) =>
570573
changePrec(GlobalPrec) { toText(bind.name) ~ keywordStr("[") ~ toText(bind.symbol.info) ~ keywordStr("]: ") ~ toText(expr) }
571574
case Return(expr, from) =>

compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,11 @@ class ExpandSAMs extends MiniPhase:
180180

181181
def isDefinedAtRhs(paramRefss: List[List[Tree]])(using Context) =
182182
val tru = Literal(Constant(true))
183-
def translateCase(cdef: CaseDef) = cpy.CaseDef(cdef)(body = tru)
183+
def translateCase(cdef: CaseDef): CaseDef =
184+
val body1 = cdef.body match
185+
case b: SubMatch => cpy.Match(b)(b.selector, b.cases.map(translateCase))
186+
case _ => tru
187+
cpy.CaseDef(cdef)(body = body1)
184188
val paramRef = paramRefss.head.head
185189
val defaultValue = Literal(Constant(false))
186190
translateMatch(isDefinedAtFn)(paramRef.symbol, pfRHS.cases.map(translateCase), defaultValue)

0 commit comments

Comments
 (0)