From 1e32411479740f84ac132f76e573d0ccfabfcaad Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sat, 27 Dec 2025 07:57:38 -0800 Subject: [PATCH 1/2] Relaxed colon case syntax --- .../src/dotty/tools/dotc/config/Feature.scala | 2 ++ .../dotty/tools/dotc/parsing/Scanners.scala | 18 ++++++++++---- library/src/scala/language.scala | 11 +++++---- project/MiMaFilters.scala | 3 +++ tests/pos/case-indent.scala | 24 +++++++++++++++++++ 5 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 tests/pos/case-indent.scala diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 09410c78eba4..3ff88e290fbe 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -41,6 +41,7 @@ object Feature: val multiSpreads = experimental("multiSpreads") val subCases = experimental("subCases") val relaxedLambdaSyntax = experimental("relaxedLambdaSyntax") + val relaxedColonSyntax = experimental("relaxedColonSyntax") def experimentalAutoEnableFeatures(using Context): List[TermName] = defn.languageExperimentalFeatures @@ -73,6 +74,7 @@ object Feature: (multiSpreads, "Enable experimental varargs with multi-spreads"), (subCases, "Enable experimental match expressions with sub-cases"), (relaxedLambdaSyntax, "Enable experimental relaxed lambda syntax"), + (relaxedColonSyntax, "Enable experimental relaxed colon syntax"), ) // legacy language features from Scala 2 that are no longer supported. diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 796704759722..e69a88a27700 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -628,7 +628,13 @@ object Scanners { insert(if (pastBlankLine) NEWLINES else NEWLINE, lineOffset) else if indentIsSignificant then if nextWidth < lastWidth - || nextWidth == lastWidth && (indentPrefix == MATCH || indentPrefix == CATCH) && token != CASE then + || nextWidth == lastWidth + && token != CASE + && indentPrefix.match + case MATCH | CATCH => true + case CASE => featureEnabled(Feature.relaxedColonSyntax) + case _ => false + then if currentRegion.isOutermost then if nextWidth < lastWidth then currentRegion = topLevelRegion(nextWidth) else if canDedent then @@ -654,9 +660,13 @@ object Scanners { else if r.isInstanceOf[InBraces] && !closingRegionTokens.contains(token) then report.warning("Line is indented too far to the left, or a `}` is missing", sourcePos()) else if lastWidth < nextWidth - || lastWidth == nextWidth && (lastToken == MATCH || lastToken == CATCH) && token == CASE then + || lastWidth == nextWidth + && token == CASE + && (lastToken == MATCH || lastToken == CATCH || featureEnabled(Feature.relaxedColonSyntax)) + then if canStartIndentTokens.contains(lastToken) then - currentRegion = Indented(nextWidth, lastToken, currentRegion) + val prefix = if token == CASE && featureEnabled(Feature.relaxedColonSyntax) then CASE else lastToken + currentRegion = Indented(nextWidth, prefix, currentRegion) insert(INDENT, offset) else if lastToken == SELFARROW then currentRegion.knownWidth = nextWidth @@ -1694,7 +1704,7 @@ object Scanners { case class Indented(width: IndentWidth, prefix: Token, outer: Region | Null) extends Region(OUTDENT): knownWidth = width - /** Other indendation widths > width of lines in the same region */ + /** Other indentation widths > width of lines in the same region */ var otherIndentWidths: Set[IndentWidth] = Set() override def coversIndent(w: IndentWidth) = width == w || otherIndentWidths.contains(w) diff --git a/library/src/scala/language.scala b/library/src/scala/language.scala index ee3efb4fa66b..90a12c44ac67 100644 --- a/library/src/scala/language.scala +++ b/library/src/scala/language.scala @@ -368,13 +368,16 @@ object language { @compileTimeOnly("`subCases` can only be used at compile time in import statements") object subCases - /** Experimental support for single-line lambdas and case clause expressions after `:` - */ + /** Experimental support for single-line lambdas and case clause expressions after `:`. */ @compileTimeOnly("`relaxedLambdaSyntax` can only be used at compile time in import statements") object relaxedLambdaSyntax + + /** Experimental support for unindented case block after `:`. */ + @compileTimeOnly("`relaxedColonSyntax` can only be used at compile time in import statements") + object relaxedColonSyntax } - /** The deprecated object contains features that are no longer officially suypported in Scala. + /** The deprecated object contains features that are no longer officially suypported in Scala. * Features in this object are slated for removal. New code should not use them and * old code should migrate away from them. */ @@ -448,7 +451,7 @@ object language { object `future-migration` /** Sets source version to 2.13. Effectively, this doesn't change the source language, - * but rather adapts the generated code as if it was compiled with Scala 2.13 + * but rather adapts the generated code as if it were compiled with Scala 2.13. */ @compileTimeOnly("`2.13` can only be used at compile time in import statements") private[scala] object `2.13` diff --git a/project/MiMaFilters.scala b/project/MiMaFilters.scala index 75f4c9e86465..1a8bfe55955d 100644 --- a/project/MiMaFilters.scala +++ b/project/MiMaFilters.scala @@ -11,6 +11,9 @@ object MiMaFilters { ProblemFilters.exclude[DirectMissingMethodProblem]("scala.caps.package#package.freeze"), // scala/scala3#24545 / scala/scala3#24788 ProblemFilters.exclude[MissingClassProblem]("scala.annotation.unchecked.uncheckedOverride"), + ProblemFilters.exclude[MissingFieldProblem]("scala.language#experimental.relaxedColonSyntax"), + ProblemFilters.exclude[MissingClassProblem]("scala.language$experimental$relaxedColonSyntax$"), + // scala/scala3#24841 ), ) diff --git a/tests/pos/case-indent.scala b/tests/pos/case-indent.scala new file mode 100644 index 000000000000..a61bfb095bd9 --- /dev/null +++ b/tests/pos/case-indent.scala @@ -0,0 +1,24 @@ +import language.experimental.relaxedColonSyntax + +def f[A](xs: List[A]): List[String] = + xs.map: + case s: String => s + case x => x.toString + +class Extra: + val pf: PartialFunction[String, Int] = + case "foo" => 1 + case "bar" => 2 + + def tryit(xs: List[String]) = xs.collect(pf) + +class Functional: + val f: Int => PartialFunction[String, Int] = + i => + case _ => i + +@main def main = + println: + f(List(42)) + println: + Extra().tryit("baz" :: "bar" :: "foo" :: Nil) From 76c571d6dd303cc3a69aaff21b76ee467efd47c4 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sun, 4 Jan 2026 15:21:24 -0800 Subject: [PATCH 2/2] More examples of relaxed colon case --- tests/pos/case-indent.scala | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/pos/case-indent.scala b/tests/pos/case-indent.scala index a61bfb095bd9..f52c3c454b4e 100644 --- a/tests/pos/case-indent.scala +++ b/tests/pos/case-indent.scala @@ -1,9 +1,19 @@ import language.experimental.relaxedColonSyntax +import language.experimental.relaxedLambdaSyntax def f[A](xs: List[A]): List[String] = xs.map: case s: String => s case x => x.toString + .map: + case s: String if s.length > 4 => s + case s => f"$s%.04s" + +def g(xs: List[Int]): List[String] = + xs.map: case i: Int => i.toString // "relaxed lambda" + .map: + case s: String if s.length > 4 => s + case s => f"$s%.04s" class Extra: val pf: PartialFunction[String, Int] = @@ -12,6 +22,17 @@ class Extra: def tryit(xs: List[String]) = xs.collect(pf) +class Possibly(b: Boolean): + val pf: PartialFunction[String, Int] = + if b then + case "foo" => 1 + case "bar" => 2 + else + case "foo" => 42 + case "bar" => 27 + + def tryit(xs: List[String]) = xs.collect(pf) + class Functional: val f: Int => PartialFunction[String, Int] = i => @@ -20,5 +41,9 @@ class Functional: @main def main = println: f(List(42)) + println: + g(List(42)) println: Extra().tryit("baz" :: "bar" :: "foo" :: Nil) + println: + Possibly(false).tryit("baz" :: "bar" :: "foo" :: Nil)