Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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(WITH); 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 == WITH then
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
38 changes: 26 additions & 12 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2592,6 +2592,15 @@ object Parsers {
Match(t, inBracesOrIndented(caseClauses(() => caseClause())))
}

/** SubMatchClause ::= `match' `{' CaseClauses `}'
*/
def subMatchClause(t: Tree): SubMatch = atSpan(startOffset(t), accept(MATCH)):
val cases =
if in.token == CASE
then caseClause(exprOnly = true) :: Nil // single sub case without new line
else inBracesOrIndented(caseClauses(() => caseClause()))
SubMatch(t, cases)

/** `match' <<< TypeCaseClauses >>>
*/
def matchType(t: Tree): MatchTypeTree =
Expand Down Expand Up @@ -3092,24 +3101,29 @@ 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()
})
val body =
if in.token == WITH && in.featureEnabled(Feature.subCases) then atSpan(in.skipToken()):
val t = subMatchClause(simpleExpr(Location.ElseWhere))
if in.isStatSep then in.nextToken() // else may have been consumed by sub sub match
t
else 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()
CaseDef(pat, grd, 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
7 changes: 6 additions & 1 deletion library/src/scala/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -342,14 +342,19 @@ object language {
* @see [[https://github.com/scala/improvement-proposals/pull/79]]
*/
@compileTimeOnly("`betterFors` can only be used at compile time in import statements")
@deprecated("The `experimental.betterFors` language import no longer has any effect, the feature is being stablised and can be enabled using `-preview` flag", since = "3.7")
@deprecated("The `experimental.betterFors` language import no longer has any effect, the feature is being stabilised and can be enabled using `-preview` flag", since = "3.7")
object betterFors

/** Experimental support for package object values
*/
@compileTimeOnly("`packageObjectValues` can only be used at compile time in import statements")
object packageObjectValues

/** Experimental support for match expressions with sub cases.
*/
@compileTimeOnly("`subCases` can only be used at compile time in import statements")
object subCases

}

/** The deprecated object contains features that are no longer officially suypported in Scala.
Expand Down
7 changes: 6 additions & 1 deletion library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,18 @@ object language:
* @see [[https://github.com/scala/improvement-proposals/pull/79]]
*/
@compileTimeOnly("`betterFors` can only be used at compile time in import statements")
@deprecated("The `experimental.betterFors` language import no longer has any effect, the feature is being stablised and can be enabled using `-preview` flag", since = "3.7")
@deprecated("The `experimental.betterFors` language import no longer has any effect, the feature is being stabilised and can be enabled using `-preview` flag", since = "3.7")
object betterFors

/** Experimental support for package object values
*/
@compileTimeOnly("`packageObjectValues` can only be used at compile time in import statements")
object packageObjectValues

/** Experimental support for match expressions with sub cases.
*/
@compileTimeOnly("`subCases` can only be used at compile time in import statements")
object subCases
end experimental

/** The deprecated object contains features that are no longer officially suypported in Scala.
Expand Down
1 change: 1 addition & 0 deletions tasty/src/dotty/tools/tasty/TastyFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ object TastyFormat {
final val EMPTYCLAUSE = 45
final val SPLITCLAUSE = 46
final val TRACKED = 47
final val WITH = 48

// Tree Cat. 2: tag Nat
final val firstNatTreeTag = SHAREDterm
Expand Down
30 changes: 30 additions & 0 deletions tests/pos/inline-match-sub-cases.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import scala.language.experimental.subCases

object Test:

// using transparent to test whether test whether reduced as expected
transparent inline def foo(i: Int, j: Int): String =
inline i match
case 0 with j match
case 1 => "01"
case 2 => "02"
case 1 with j match
case 1 => "11"
case 2 => "12"
case _ => "3"

val r01: "01" = foo(0, 1)
val r02: "02" = foo(0, 2)
val r11: "11" = foo(1, 1)
val r31: "3" = foo(3, 1)

transparent inline def bar(x: Option[Any]): String =
inline x match
case Some(y: Int) with y match
case 1 => "a"
case 2 => "b"
case Some(z: String) => "c"
case _ => "d"

val x = bar(Some(2)) // FIX this reduces to "c", but this appears to be a more general issue than sub-matches
val y = bar(Some("hello"))
17 changes: 17 additions & 0 deletions tests/pos/match-single-sub-case.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import scala.language.experimental.subCases

// single sub case can be one the same line as outer case
object Test:
val x: Option[Option[Int]] = ???
x match
case Some(x2) with x2 match case Some(x3) => "aa"
case Some(x2) if false with x2 match case Some(x3) if true => "aa"
case Some(x2) with x2 match case Some(x3) with x2 match case Some(x3) => "bb"
case Some(y2) with y2 match
case Some(y3) with y3 match
case 1 => "a"
case 2 => "b"
case Some(x2) with x2 match case Some(x3) with x3 match
case 1 => "a"
case 2 => "b"
case None => "d"
20 changes: 20 additions & 0 deletions tests/pos/match-sub-sub-cases.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import scala.language.experimental.subCases

object Test:
val x: Option[Option[Int]] = ???
x match
case Some(x2) with x2 match
case Some(x3) with x3 match
case 1 => "a"
case 2 => "b"
case None => "d"

x match {
case Some(x2) with x2 match {
case Some (x3) with x3 match {
case 1 => "a"
case 2 => "b"
}
}
case None => "d"
}
Loading
Loading