Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -608,17 +608,25 @@ object Trees {
extends TermTree[T] {
type ThisTree[+T <: Untyped] = Match[T]
def isInline = false
def isSubMatch = false
}
class InlineMatch[+T <: Untyped] private[ast] (selector: Tree[T], cases: List[CaseDef[T]])(implicit @constructorOnly src: SourceFile)
extends Match(selector, cases) {
override def isInline = true
override def toString = s"InlineMatch($selector, $cases)"
}
/** with selector match { cases } */
final class SubMatch[+T <: Untyped] private[ast] (selector: Tree[T], cases: List[CaseDef[T]])(implicit @constructorOnly src: SourceFile)
extends Match(selector, cases) {
override def isSubMatch = true
}

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

/** label[tpt]: { expr } */
Expand Down Expand Up @@ -1180,6 +1188,7 @@ object Trees {
type Closure = Trees.Closure[T]
type Match = Trees.Match[T]
type InlineMatch = Trees.InlineMatch[T]
type SubMatch = Trees.SubMatch[T]
type CaseDef = Trees.CaseDef[T]
type Labeled = Trees.Labeled[T]
type Return = Trees.Return[T]
Expand Down Expand Up @@ -1329,6 +1338,7 @@ object Trees {
def Match(tree: Tree)(selector: Tree, cases: List[CaseDef])(using Context): Match = tree match {
case tree: Match if (selector eq tree.selector) && (cases eq tree.cases) => tree
case tree: InlineMatch => finalize(tree, untpd.InlineMatch(selector, cases)(sourceFile(tree)))
case tree: SubMatch => finalize(tree, untpd.SubMatch(selector, cases)(sourceFile(tree)))
case _ => finalize(tree, untpd.Match(selector, cases)(sourceFile(tree)))
}
def CaseDef(tree: Tree)(pat: Tree, guard: Tree, body: Tree)(using Context): CaseDef = tree match {
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/tpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
def InlineMatch(selector: Tree, cases: List[CaseDef])(using Context): Match =
ta.assignType(untpd.InlineMatch(selector, cases), selector, cases)

def SubMatch(selector: Tree, cases: List[CaseDef])(using Context): Match =
ta.assignType(untpd.SubMatch(selector, cases), selector, cases)

def Labeled(bind: Bind, expr: Tree)(using Context): Labeled =
ta.assignType(untpd.Labeled(bind, expr))

Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
def Closure(env: List[Tree], meth: Tree, tpt: Tree)(implicit src: SourceFile): Closure = new Closure(env, meth, tpt)
def Match(selector: Tree, cases: List[CaseDef])(implicit src: SourceFile): Match = new Match(selector, cases)
def InlineMatch(selector: Tree, cases: List[CaseDef])(implicit src: SourceFile): Match = new InlineMatch(selector, cases)
def SubMatch(selector: Tree, cases: List[CaseDef])(implicit src: SourceFile): SubMatch = new SubMatch(selector, cases)
def CaseDef(pat: Tree, guard: Tree, body: Tree)(implicit src: SourceFile): CaseDef = new CaseDef(pat, guard, body)
def Labeled(bind: Bind, expr: Tree)(implicit src: SourceFile): Labeled = new Labeled(bind, expr)
def Return(expr: Tree, from: Tree)(implicit src: SourceFile): Return = new Return(expr, from)
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ object Feature:
val modularity = experimental("modularity")
val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions")
val packageObjectValues = experimental("packageObjectValues")
val subCases = experimental("subCases")

def experimentalAutoEnableFeatures(using Context): List[TermName] =
defn.languageExperimentalFeatures
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ class TreePickler(pickler: TastyPickler, attributes: Attributes) {
if (tree.isInline)
if (selector.isEmpty) writeByte(IMPLICIT)
else { writeByte(INLINE); pickleTree(selector) }
else if tree.isSubMatch then { writeByte(LAZY); pickleTree(selector) }
else pickleTree(selector)
tree.cases.foreach(pickleTree)
}
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,9 @@ class TreeUnpickler(reader: TastyReader,
readByte()
InlineMatch(readTree(), readCases(end))
}
else if nextByte == LAZY then // similarly to InlineMatch we use an arbitrary Cat.1 tag
readByte()
SubMatch(readTree(), readCases(end))
else Match(readTree(), readCases(end)))
case RETURN =>
val from = readSymRef()
Expand Down
10 changes: 7 additions & 3 deletions compiler/src/dotty/tools/dotc/inlines/InlineReducer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,13 @@ class InlineReducer(inliner: Inliner)(using Context):
case ConstantValue(v: Boolean) => (v, true)
case _ => (false, false)
}
if guardOK then Some((caseBindings.map(_.subst(from, to)), cdef.body.subst(from, to), canReduceGuard))
else if canReduceGuard then None
else Some((caseBindings.map(_.subst(from, to)), cdef.body.subst(from, to), canReduceGuard))
if !canReduceGuard then Some((List.empty, EmptyTree, false))
else if !guardOK then None
else cdef.body.subst(from, to) match
case t: SubMatch => // a sub match of an inline match is also inlined
reduceInlineMatch(t.selector, t.selector.tpe, t.cases, typer).map:
(subCaseBindings, rhs) => (caseBindings.map(_.subst(from, to)) ++ subCaseBindings, rhs, true)
case b => Some((caseBindings.map(_.subst(from, to)), b, true))
}
else None
}
Expand Down
88 changes: 75 additions & 13 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2585,11 +2585,52 @@ object Parsers {
mkIf(cond, thenp, elsep)
}

/* When parsing (what will become) a sub sub match, that is,
* when in a guard of case of a match, in a guard of case of a match;
* we will eventually reach Scanners.handleNewLine at the end of the sub sub match
* with an in.currretRegion of the shape `InCase +: Indented :+ InCase :+ Indented :+ ...`
* if we did not do dropInnerCaseRegion.
* In effect, a single outdent would be inserted by handleNewLine after the sub sub match.
* This causes the remaining cases of the outer match to be included in the intermediate sub match.
* For example:
* match
* case x1 if x1 match
* case y if y match
* case z => "a"
* case x2 => "b"
* would become
* match
* case x1 if x1 match {
* case y if y match {
* case z => "a"
* }
* case x2 => "b"
* }
* This issue is avoided by dropping the `InCase` region when parsing match clause,
* since `Indetented :+ Indented :+ ...` now allows handleNewLine to insert two outdents.
* Note that this _could_ break previous code which relied on matches within guards
* being considered as a separate region without explicit indentation.
*/
private def dropInnerCaseRegion(): Unit =
in.currentRegion match
case Indented(width, prefix, Scanners.InCase(r)) => in.currentRegion = Indented(width, prefix, r)
case Scanners.InCase(r) => in.currentRegion = r
case _ =>

/** MatchClause ::= `match' `{' CaseClauses `}'
* | `match' ExprCaseClause
*/
def matchClause(t: Tree): Match =
atSpan(startOffset(t), in.skipToken()) {
Match(t, inBracesOrIndented(caseClauses(() => caseClause())))
val cases =
if in.featureEnabled(Feature.subCases) then
dropInnerCaseRegion()
if in.token == CASE
then caseClause(exprOnly = true) :: Nil // single case without new line
else inBracesOrIndented(caseClauses(() => caseClause()))
else
inBracesOrIndented(caseClauses(() => caseClause()))
Match(t, cases)
}

/** `match' <<< TypeCaseClauses >>>
Expand Down Expand Up @@ -3092,24 +3133,45 @@ object Parsers {
buf.toList
}

/** CaseClause ::= ‘case’ Pattern [Guard] `=>' Block
* ExprCaseClause ::= ‘case’ Pattern [Guard] ‘=>’ Expr
/** CaseClause ::= ‘case’ Pattern [Guard] (‘with’ SimpleExpr SubMatchClause | `=>' Block)
* ExprCaseClause ::= ‘case’ Pattern [Guard] (‘with’ SimpleExpr SubMatchClause | `=>' Expr)
*/
def caseClause(exprOnly: Boolean = false): CaseDef = atSpan(in.offset) {
val (pat, grd) = inSepRegion(InCase) {
accept(CASE)
(withinMatchPattern(pattern()), guard())
}
CaseDef(pat, grd, atSpan(accept(ARROW)) {
if exprOnly then
if in.indentSyntax && in.isAfterLineEnd && in.token != INDENT then
warning(em"""Misleading indentation: this expression forms part of the preceding catch case.
|If this is intended, it should be indented for clarity.
|Otherwise, if the handler is intended to be empty, use a multi-line catch with
|an indented case.""")
expr()
else block()
})
var grd1 = grd // may be reset to EmptyTree (and used as sub match body instead) if there is no leading ARROW
val tok = in.token

extension (self: Tree) def asSubMatch: Tree = self match
case Match(sel, cases) if in.featureEnabled(Feature.subCases) =>
if in.isStatSep then in.nextToken() // else may have been consumed by sub sub match
SubMatch(sel, cases)
case _ =>
syntaxErrorOrIncomplete(ExpectedTokenButFound(ARROW, tok))
atSpan(self.span)(Block(Nil, EmptyTree))

val body = tok match
case ARROW => atSpan(in.skipToken()):
if exprOnly then
if in.indentSyntax && in.isAfterLineEnd && in.token != INDENT then
warning(em"""Misleading indentation: this expression forms part of the preceding catch case.
|If this is intended, it should be indented for clarity.
|Otherwise, if the handler is intended to be empty, use a multi-line catch with
|an indented case.""")
expr()
else block()
case IF => atSpan(in.skipToken()):
// a sub match after a guard is parsed the same as one without
val t = inSepRegion(InCase)(postfixExpr(Location.InGuard))
t.asSubMatch
case other =>
val t = grd1.asSubMatch
grd1 = EmptyTree
t

CaseDef(pat, grd1, body)
}

/** TypeCaseClause ::= ‘case’ (InfixType | ‘_’) ‘=>’ Type [semi]
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,10 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
selTxt ~ keywordStr(" match ") ~ blockText(cases)
}
case CaseDef(pat, guard, body) =>
keywordStr("case ") ~ inPattern(toText(pat)) ~ optText(guard)(keywordStr(" if ") ~ _) ~ " => " ~ caseBlockText(body)
val bodyText = body match
case t: SubMatch => keywordStr(" with ") ~ toText(t)
case t => " => " ~ caseBlockText(t)
keywordStr("case ") ~ inPattern(toText(pat)) ~ optText(guard)(keywordStr(" if ") ~ _) ~ bodyText
case Labeled(bind, expr) =>
changePrec(GlobalPrec) { toText(bind.name) ~ keywordStr("[") ~ toText(bind.symbol.info) ~ keywordStr("]: ") ~ toText(expr) }
case Return(expr, from) =>
Expand Down
6 changes: 5 additions & 1 deletion compiler/src/dotty/tools/dotc/transform/ExpandSAMs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,11 @@ class ExpandSAMs extends MiniPhase:

def isDefinedAtRhs(paramRefss: List[List[Tree]])(using Context) =
val tru = Literal(Constant(true))
def translateCase(cdef: CaseDef) = cpy.CaseDef(cdef)(body = tru)
def translateCase(cdef: CaseDef): CaseDef =
val body1 = cdef.body match
case b: SubMatch => cpy.Match(b)(b.selector, b.cases.map(translateCase))
case _ => tru
cpy.CaseDef(cdef)(body = body1)
val paramRef = paramRefss.head.head
val defaultValue = Literal(Constant(false))
translateMatch(isDefinedAtFn)(paramRef.symbol, pfRHS.cases.map(translateCase), defaultValue)
Expand Down
27 changes: 21 additions & 6 deletions compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class PatternMatcher extends MiniPhase {

override def transformMatch(tree: Match)(using Context): Tree =
if (tree.isInstanceOf[InlineMatch]) tree
else if tree.isSubMatch then
// A sub match in a case def body will be transformed when the outer match is processed.
// This assummes that no earlier miniphase needs sub matches to have been transformed before the outer match.
tree
else {
// Widen termrefs with underlying `=> T` types. Otherwise ElimByName will produce
// inconsistent types. See i7743.scala.
Expand Down Expand Up @@ -482,12 +486,23 @@ object PatternMatcher {
}
}

private def caseDefPlan(scrutinee: Symbol, cdef: CaseDef): Plan = {
var onSuccess: Plan = ResultPlan(cdef.body)
if (!cdef.guard.isEmpty)
onSuccess = TestPlan(GuardTest, cdef.guard, cdef.guard.span, onSuccess)
patternPlan(scrutinee, cdef.pat, onSuccess)
}
private def caseDefPlan(scrutinee: Symbol, cdef: CaseDef): Plan =
val CaseDef(pat, guard, body) = cdef
val caseDefBodyPlan: Plan = body match
case t: SubMatch => subMatchPlan(t)
case _ => ResultPlan(body)
val onSuccess: Plan =
if guard.isEmpty then caseDefBodyPlan
else TestPlan(GuardTest, guard, guard.span, caseDefBodyPlan)
patternPlan(scrutinee, pat, onSuccess)
end caseDefPlan

// like matchPlan but without a final matchError ResultPlan at the end of SeqPlans
// s.t. we fall back to the outer SeqPlan
private def subMatchPlan(tree: SubMatch): Plan =
letAbstract(tree.selector) { scrutinee =>
tree.cases.map(caseDefPlan(scrutinee, _)).reduceRight(SeqPlan(_, _))
}

private def matchPlan(tree: Match): Plan =
letAbstract(tree.selector) { scrutinee =>
Expand Down
8 changes: 4 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,7 @@ object SpaceEngine {
val targetSpace = trace(i"targetSpace($selTyp)")(project(selTyp))

val patternSpace = Or(m.cases.foldLeft(List.empty[Space]) { (acc, x) =>
val space = if x.guard.isEmpty then trace(i"project(${x.pat})")(project(x.pat)) else Empty
val space = if x.maybePartial then Empty else trace(i"project(${x.pat})")(project(x.pat))
space :: acc
})

Expand Down Expand Up @@ -925,7 +925,7 @@ object SpaceEngine {
@tailrec def recur(cases: List[CaseDef], prevs: List[Space], deferred: List[Tree]): Unit =
cases match
case Nil =>
case CaseDef(pat, guard, _) :: rest =>
case (c @ CaseDef(pat, _, _)) :: rest =>
val curr = trace(i"project($pat)")(projectPat(pat))
val covered = trace("covered")(simplify(intersect(curr, targetSpace)))
val prev = trace("prev")(simplify(Or(prevs)))
Expand All @@ -951,8 +951,8 @@ object SpaceEngine {
hadNullOnly = true
report.warning(MatchCaseOnlyNullWarning(), pat.srcPos)

// in redundancy check, take guard as false in order to soundly approximate
val newPrev = if guard.isEmpty then covered :: prevs else prevs
// in redundancy check, take guard as false (or potential sub cases as partial) for a sound approximation
val newPrev = if c.maybePartial then prevs else covered :: prevs
recur(rest, newPrev, Nil)

recur(m.cases, Nil, Nil)
Expand Down
72 changes: 72 additions & 0 deletions docs/_docs/reference/experimental/sub-cases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
layout: doc-page
title: "Match Expressions with Sub Cases"
---

A case in a match expression can be followed by another sub-match expression, introduced by the `if` keyword.
For example:

```scala
enum Version:
case Legacy
case Stable(major: Int, minor: Int)

case class Document(title: String, version: Version)

def version(d: Option[Document]) = d match
case Some(x) if x.version match
case Version.Stable(m, n) if m > 2 => s"$m.$n"
case Version.Legacy => "legacy"
case _ => "unsupported"

assert(version(Some(Document("...", Version.Stable(3, 1)))) = "3.1")
assert(version(Some(Document("...", Version.Stable(2, 1)))) = "unsupported")
assert(version(Some(Document("...", Version.Legacy))) = "legacy")
assert(version(Some(Document("...", None))) = "unsupported")
```

The cases of a sub-match expression are tested iff the outer case matches.
The sub match scrutinee can refer to variables bound from the outer pattern.
Evaluation of sub-matches then proceeds as usual.
For example, if `version` is applied on `Some(Document("...", Version.Stable(3, 1)))`, the first outer pattern matches (i.e., `Some(x)`), causing the sub match expression to be evaluated with scrutinee `Version.Stable(3, 1)`, yielding `"3.1"`.

The cases of a sub-match expression need not be exhaustive.
If they were, we would not need sub-match at all: a usual match in the body of the first case would suffice,
e.g., `case Some(x) => x.version match ...`.
If none of the sub-cases succeed, then control flow returns to the outer match expression and proceeds as though the current case had not matched.
For example, `Some(Document("...", Version.Stable(2, 1)))` matches the first pattern, but none of its sub-cases, and we therefore obtain the result `"unsupported"`.

More generally, sub-matches also allow:
- Arbitrary nesting, e.g. sub-sub-matches are supported.
- Interleaved boolean guards, e.g. `case Some(x: Int) if x != 0 if x match ...`.
- Interleaving pattern extractors and computations for the scrutinees of sub-matches.


## Motivation

Without sub matches, one would typically duplicate either the default case or the outer pattern.
That is, use:
```scala
def version(d: Option[Document]) = d match
case Some(x) => x.version match
case Version.Stable(m, n) if m > 2 => s"$m.$n"
case Version.Legacy => "legacy"
case _ => "unsupported"
case _ => "unsupported"
```
or
```scala
def version(d: Option[Document]) = d match
case Some(Document(_, Version.Stable(m, n))) if m > 2 => s"$m.$n"
case Some(Document(_, Version.Legacy)) => "legacy"
case _ => "unsupported"
```









Loading
Loading