From d68dbafd77d604a95a2d810795d3af5afe9d7b2e Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 25 Aug 2025 14:23:12 +0200 Subject: [PATCH 1/2] Add reference doc details --- .../dotty/tools/dotc/parsing/Parsers.scala | 1 + .../_docs/reference/experimental/sub-cases.md | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index c4a77f17060c..b56fba87bf70 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3171,6 +3171,7 @@ object Parsers { val t = inSepRegion(InCase)(postfixExpr(Location.InGuard)) t.asSubMatch case other => + // the guard is reinterpreted as a sub-match when there is no leading IF or ARROW token val t = grd1.asSubMatch grd1 = EmptyTree t diff --git a/docs/_docs/reference/experimental/sub-cases.md b/docs/_docs/reference/experimental/sub-cases.md index de80a8cfb752..227fc6f36709 100644 --- a/docs/_docs/reference/experimental/sub-cases.md +++ b/docs/_docs/reference/experimental/sub-cases.md @@ -36,11 +36,6 @@ 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 @@ -62,11 +57,21 @@ def version(d: Option[Document]) = d match case _ => "unsupported" ``` +## Details +Sub-cases allow: +- Arbitrary nesting, e.g. sub-sub-matches are supported. +- Interleaving 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. +Sub-cases are supported for: +- match clauses +- catch clauses +- partial functions +Similarly to catch clauses, match expressions with a single case can now be written on single line (without braces), +e.g., `Some(1) match case Some(x) => x`. +Exhaustivity and reachability checking conservatively assume the sub-cases to be partial, similarly boolean guards. - - - +A sub-match is inlined iff the outer match is inlined, with the same semantics as the usual match expressions. From 3032f0824537b065e4618e9017c86b7e5b8a66b0 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Mon, 25 Aug 2025 19:07:29 +0200 Subject: [PATCH 2/2] Replace pickling of sub matches to use an annotation s.t. the TastyFormat is unchanged Co-authored-by: Hamza Remmal --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 1 + .../src/dotty/tools/dotc/core/tasty/TreePickler.scala | 5 ++++- .../src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala | 9 ++++----- library/src/scala/annotation/internal/$subMatch.scala | 7 +++++++ project/Build.scala | 2 ++ 5 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 library/src/scala/annotation/internal/$subMatch.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 02c6d1d66023..71752a6ce1a8 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1061,6 +1061,7 @@ class Definitions { @tu lazy val ScalaStaticAnnot: ClassSymbol = requiredClass("scala.annotation.static") @tu lazy val SerialVersionUIDAnnot: ClassSymbol = requiredClass("scala.SerialVersionUID") @tu lazy val SilentIntoAnnot: ClassSymbol = requiredClass("scala.annotation.internal.$into") + @tu lazy val SilentSubMatchAnnot: ClassSymbol = requiredClass("scala.annotation.internal.$subMatch") @tu lazy val TailrecAnnot: ClassSymbol = requiredClass("scala.annotation.tailrec") @tu lazy val ThreadUnsafeAnnot: ClassSymbol = requiredClass("scala.annotation.threadUnsafe") @tu lazy val ConstructorOnlyAnnot: ClassSymbol = requiredClass("scala.annotation.constructorOnly") diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 20a405271078..311ad8026302 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -590,7 +590,10 @@ 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 if tree.isSubMatch then + // Temporary measure until we can change TastyFormat + val annot = New(defn.SilentSubMatchAnnot.typeRef, Nil) + pickleTree(selector.annotated(annot)) else pickleTree(selector) tree.cases.foreach(pickleTree) } diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 7ed71dde2455..c1daf3cd2844 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -1542,7 +1542,7 @@ class TreeUnpickler(reader: TastyReader, val tpt = ifBefore(end)(readTpt(), EmptyTree) Closure(Nil, meth, tpt) case MATCH => - simplifyLub( + simplifyLub: if (nextByte == IMPLICIT) { readByte() InlineMatch(EmptyTree, readCases(end)) @@ -1551,10 +1551,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))) + else readTree() match + case Typed(sel, tpt) if tpt.tpe.hasAnnotation(defn.SilentSubMatchAnnot) => SubMatch(sel, readCases(end)) + case sel => Match(sel, readCases(end)) case RETURN => val from = readSymRef() val expr = ifBefore(end)(readTree(), EmptyTree) diff --git a/library/src/scala/annotation/internal/$subMatch.scala b/library/src/scala/annotation/internal/$subMatch.scala new file mode 100644 index 000000000000..df46713d3cc2 --- /dev/null +++ b/library/src/scala/annotation/internal/$subMatch.scala @@ -0,0 +1,7 @@ +package scala.annotation.internal + +import scala.annotation.experimental + +/** An internal annotation on (part of) that serves as a marker for sub matches. */ +@experimental +class $subMatch extends annotation.StaticAnnotation \ No newline at end of file diff --git a/project/Build.scala b/project/Build.scala index f7ab11833574..26b227e30bf9 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1201,6 +1201,7 @@ object Build { file(s"${baseDirectory.value}/src/scala/annotation/internal/SourceFile.scala"), file(s"${baseDirectory.value}/src/scala/annotation/internal/reachCapability.scala"), file(s"${baseDirectory.value}/src/scala/annotation/internal/$$into.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/internal/$$subMatch.scala"), file(s"${baseDirectory.value}/src/scala/annotation/internal/TASTYLongSignature.java"), file(s"${baseDirectory.value}/src/scala/annotation/internal/readOnlyCapability.scala"), file(s"${baseDirectory.value}/src/scala/annotation/internal/unshared.scala"), @@ -1339,6 +1340,7 @@ object Build { file(s"${baseDirectory.value}/src/scala/annotation/internal/SourceFile.scala"), file(s"${baseDirectory.value}/src/scala/annotation/internal/reachCapability.scala"), file(s"${baseDirectory.value}/src/scala/annotation/internal/$$into.scala"), + file(s"${baseDirectory.value}/src/scala/annotation/internal/$$subMatch.scala"), file(s"${baseDirectory.value}/src/scala/annotation/internal/TASTYLongSignature.java"), file(s"${baseDirectory.value}/src/scala/annotation/internal/readOnlyCapability.scala"), file(s"${baseDirectory.value}/src/scala/annotation/internal/unshared.scala"),