From 29b3909a7e025bee7580d8c3f8c7f6ea9468d06b Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 26 Aug 2025 19:57:54 +0200 Subject: [PATCH 1/5] Allow single-line lambdas after `:` Previously, we need to indent after the error, e.g. ```scala xs.map: x => x + 1 ``` We now also allow to write the lambda on a single line: ```scala xs.map: x => x + 1 ``` The lambda extends to the end of the line. --- .../dotty/tools/dotc/parsing/Parsers.scala | 66 +++++++++++-------- .../dotty/tools/dotc/parsing/Scanners.scala | 8 ++- .../src/dotty/tools/dotc/parsing/Tokens.scala | 6 +- tests/neg/closure-args.check | 55 ++++++++++++++++ tests/neg/closure-args.scala | 19 ++---- tests/neg/i22193.check | 34 ++++++++++ tests/neg/i22193.scala | 4 +- tests/neg/i22906.check | 15 +++-- tests/neg/i22906.scala | 2 +- tests/pos/change-lambda.scala | 7 ++ tests/pos/closure-args.scala | 43 ++++-------- 11 files changed, 178 insertions(+), 81 deletions(-) create mode 100644 tests/neg/closure-args.check create mode 100644 tests/neg/i22193.check create mode 100644 tests/pos/change-lambda.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 16afbe9ad433..3a13499f1eee 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1089,15 +1089,22 @@ object Parsers { * something enclosed in (...) or [...], and this is followed by a `=>` or `?=>` * and an INDENT. */ - def followingIsLambdaAfterColon(): Boolean = + def followingIsLambdaAfterColon(): Option[() => Tree] = val lookahead = in.LookaheadScanner(allowIndent = true) .tap(_.currentRegion.knownWidth = in.currentRegion.indentWidth) - def isArrowIndent() = - lookahead.isArrow - && { + def isArrowIndent(): Option[() => Tree] = + if lookahead.isArrow then lookahead.observeArrowIndented() - lookahead.token == INDENT || lookahead.token == EOF - } + if lookahead.token == INDENT || lookahead.token == EOF then + Some(() => expr(Location.InColonArg)) + else if !in.currentRegion.isInstanceOf[InParens] then + Some: () => + val t = inSepRegion(SingleLineLambda(_)): + expr(Location.InColonArg) + accept(ENDLAMBDA) + t + else None + else None lookahead.nextToken() if lookahead.isIdent || lookahead.token == USCORE then lookahead.nextToken() @@ -1105,7 +1112,8 @@ object Parsers { else if lookahead.token == LPAREN || lookahead.token == LBRACKET then lookahead.skipParens() isArrowIndent() - else false + else + None /** Can the next lookahead token start an operand as defined by * leadingOperandTokens, or is postfix ops enabled? @@ -1174,8 +1182,10 @@ object Parsers { * : (params) => * body */ - def isColonLambda = - sourceVersion.enablesFewerBraces && in.token == COLONfollow && followingIsLambdaAfterColon() + def isColonLambda: Option[() => Tree] = + if sourceVersion.enablesFewerBraces && in.token == COLONfollow + then followingIsLambdaAfterColon() + else None /** operand { infixop operand | MatchClause } [postfixop], * @@ -1199,17 +1209,19 @@ object Parsers { opStack = OpInfo(top1, op, in.offset) :: opStack colonAtEOLOpt() newLineOptWhenFollowing(canStartOperand) - if isColonLambda then - in.nextToken() - recur(expr(Location.InColonArg)) - else if maybePostfix && !canStartOperand(in.token) then - val topInfo = opStack.head - opStack = opStack.tail - val od = reduceStack(base, topInfo.operand, 0, true, in.name, isType) - atSpan(startOffset(od), topInfo.offset) { - PostfixOp(od, topInfo.operator) - } - else recur(operand(location)) + isColonLambda match + case Some(parseExpr) => + in.nextToken() + recur(parseExpr()) + case _ => + if maybePostfix && !canStartOperand(in.token) then + val topInfo = opStack.head + opStack = opStack.tail + val od = reduceStack(base, topInfo.operand, 0, true, in.name, isType) + atSpan(startOffset(od), topInfo.offset) { + PostfixOp(od, topInfo.operator) + } + else recur(operand(location)) else val t = reduceStack(base, top, minPrec, leftAssoc = true, in.name, isType) if !isType && in.token == MATCH then recurAtMinPrec(matchClause(t)) @@ -2848,12 +2860,14 @@ object Parsers { makeParameter(name.asTermName, typedOpt(), Modifiers(), isBackquoted = isBackquoted(id)) } case _ => t - else if isColonLambda then - val app = atSpan(startOffset(t), in.skipToken()) { - Apply(t, expr(Location.InColonArg) :: Nil) - } - simpleExprRest(app, location, canApply = true) - else t + else isColonLambda match + case Some(parseExpr) => + val app = + atSpan(startOffset(t), in.skipToken()): + Apply(t, parseExpr() :: Nil) + simpleExprRest(app, location, canApply = true) + case None => + t end simpleExprRest /** SimpleExpr ::= ‘new’ ConstrApp {`with` ConstrApp} [TemplateBody] diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 52e03de60dea..9d897eda124f 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -617,7 +617,9 @@ object Scanners { && !statCtdTokens.contains(lastToken) && !isTrailingBlankLine - if newlineIsSeparating + if currentRegion.closedBy == ENDLAMBDA then + insert(ENDLAMBDA, lineOffset) + else if newlineIsSeparating && canEndStatTokens.contains(lastToken) && canStartStatTokens.contains(token) && !isLeadingInfixOperator(nextWidth) @@ -1599,6 +1601,8 @@ object Scanners { * InParens a pair of parentheses (...) or brackets [...] * InBraces a pair of braces { ... } * Indented a pair of ... tokens + * InCase a case of a match + * SingleLineLambda the rest of a line following a `:` */ abstract class Region(val closedBy: Token): @@ -1667,6 +1671,7 @@ object Scanners { case _: InBraces => "}" case _: InCase => "=>" case _: Indented => "UNDENT" + case _: SingleLineLambda => "end of single-line lambda" /** Show open regions as list of lines with decreasing indentations */ def visualize: String = @@ -1680,6 +1685,7 @@ object Scanners { case class InParens(prefix: Token, outer: Region) extends Region(prefix + 1) case class InBraces(outer: Region) extends Region(RBRACE) case class InCase(outer: Region) extends Region(OUTDENT) + case class SingleLineLambda(outer: Region) extends Region(ENDLAMBDA) /** A class describing an indentation region. * @param width The principal indentation width diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index d47e6dab005f..1ed65bf98144 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -203,8 +203,10 @@ object Tokens extends TokensCommon { // A `:` recognized as starting an indentation block inline val SELFARROW = 90; enter(SELFARROW, "=>") // reclassified ARROW following self-type + inline val ENDLAMBDA = 99; enter(ENDLAMBDA, "end of single-line lambda") + /** XML mode */ - inline val XMLSTART = 99; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate + inline val XMLSTART = 100; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate final val alphaKeywords: TokenSet = tokenRange(IF, END) final val symbolicKeywords: TokenSet = tokenRange(USCORE, CTXARROW) @@ -267,7 +269,7 @@ object Tokens extends TokensCommon { final val canStartStatTokens3: TokenSet = canStartExprTokens3 | mustStartStatTokens | BitSet( AT, CASE, END) - final val canEndStatTokens: TokenSet = atomicExprTokens | BitSet(TYPE, GIVEN, RPAREN, RBRACE, RBRACKET, OUTDENT) + final val canEndStatTokens: TokenSet = atomicExprTokens | BitSet(TYPE, GIVEN, RPAREN, RBRACE, RBRACKET, OUTDENT, ENDLAMBDA) /** Tokens that stop a lookahead scan search for a `<-`, `then`, or `do`. * Used for disambiguating between old and new syntax. diff --git a/tests/neg/closure-args.check b/tests/neg/closure-args.check new file mode 100644 index 000000000000..1000862696f1 --- /dev/null +++ b/tests/neg/closure-args.check @@ -0,0 +1,55 @@ +-- [E040] Syntax Error: tests/neg/closure-args.scala:2:25 -------------------------------------------------------------- +2 |val x = List(1).map: (x: => Int) => // error + | ^^ + | an identifier expected, but '=>' found + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg/closure-args.scala:14:0 ---------------------------------------------------------------------------- +14 | y => y > 0 // error // error + |^ + |indented definitions expected, end of single-line lambda found +-- [E103] Syntax Error: tests/neg/closure-args.scala:14:4 -------------------------------------------------------------- +14 | y => y > 0 // error // error + | ^ + | Illegal start of toplevel definition + | + | longer explanation available when compiling with `-explain` +-- [E018] Syntax Error: tests/neg/closure-args.scala:18:46 ------------------------------------------------------------- +18 |val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error + | ^^^^ + | expression expected but case found + | + | longer explanation available when compiling with `-explain` +-- [E008] Not Found Error: tests/neg/closure-args.scala:10:4 ----------------------------------------------------------- + 8 |val b: Int = xs + 9 | .map: x => x +10 | * x // error + | ^ + | value * is not a member of List[Int]. + | Note that `*` is treated as an infix operator in Scala 3. + | If you do not want that, insert a `;` or empty line in front + | or drop any spaces behind the operator. +-- [E006] Not Found Error: tests/neg/closure-args.scala:16:21 ---------------------------------------------------------- +16 |val c = List(xs.map: y => y + y) // error // error // error // error + | ^ + | Not found: type y + | + | longer explanation available when compiling with `-explain` +-- [E006] Not Found Error: tests/neg/closure-args.scala:16:28 ---------------------------------------------------------- +16 |val c = List(xs.map: y => y + y) // error // error // error // error + | ^ + | Not found: type + + | + | longer explanation available when compiling with `-explain` +-- [E006] Not Found Error: tests/neg/closure-args.scala:16:26 ---------------------------------------------------------- +16 |val c = List(xs.map: y => y + y) // error // error // error // error + | ^ + | Not found: type y + | + | longer explanation available when compiling with `-explain` +-- [E006] Not Found Error: tests/neg/closure-args.scala:16:30 ---------------------------------------------------------- +16 |val c = List(xs.map: y => y + y) // error // error // error // error + | ^ + | Not found: type y + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/closure-args.scala b/tests/neg/closure-args.scala index 76e590ad28b9..4986a6f6e62c 100644 --- a/tests/neg/closure-args.scala +++ b/tests/neg/closure-args.scala @@ -1,4 +1,3 @@ -import language.`3.3` val x = List(1).map: (x: => Int) => // error ??? @@ -6,18 +5,14 @@ val z = List(1).map: + => // ok ??? val xs = List(1) -val b: Int = xs // error - .map: x => x * x // error - .filter: y => y > 0 // error - (0) -val d = xs // error +val b: Int = xs + .map: x => x + * x // error + +val d = xs .map: x => x.toString + xs.dropWhile: - y => y > 0 + y => y > 0 // error // error val c = List(xs.map: y => y + y) // error // error // error // error -val d2: String = xs // error - .map: x => x.toString + xs.dropWhile: y => y > 0 // error // error - .filter: z => !z.isEmpty // error - (0) -val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error // error +val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error diff --git a/tests/neg/i22193.check b/tests/neg/i22193.check new file mode 100644 index 000000000000..5a51a272c217 --- /dev/null +++ b/tests/neg/i22193.check @@ -0,0 +1,34 @@ +-- [E018] Syntax Error: tests/neg/i22193.scala:15:68 ------------------------------------------------------------------- +15 | arg2 = "the quick brown fox jumped over the lazy dog"): env => // error + | ^ + | expression expected but end of single-line lambda found + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg/i22193.scala:22:2 ---------------------------------------------------------------------------------- +22 | env => // error indented definitions expected, identifier env found + | ^^^ + | indented definitions expected, identifier env found +-- Error: tests/neg/i22193.scala:31:2 ---------------------------------------------------------------------------------- +31 | val x = "Hello" // error + | ^^^ + | indented definitions expected, val found +-- [E006] Not Found Error: tests/neg/i22193.scala:16:10 ---------------------------------------------------------------- +16 | val x = env // error + | ^^^ + | Not found: env + | + | longer explanation available when compiling with `-explain` +-- [E178] Type Error: tests/neg/i22193.scala:28:2 ---------------------------------------------------------------------- +28 | fn3( // error missing argument list for value of type (=> Unit) => Unit + | ^ + | missing argument list for value of type (=> Unit) => Unit +29 | arg = "blue sleeps faster than tuesday", +30 | arg2 = "the quick brown fox jumped over the lazy dog"): + | + | longer explanation available when compiling with `-explain` +-- [E006] Not Found Error: tests/neg/i22193.scala:32:10 ---------------------------------------------------------------- +32 | println(x) // error + | ^ + | Not found: x + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i22193.scala b/tests/neg/i22193.scala index f7ee5b1cf5e1..d3628b3e6b98 100644 --- a/tests/neg/i22193.scala +++ b/tests/neg/i22193.scala @@ -10,9 +10,9 @@ def test1() = val x = env println(x) - fn2( // error not a legal formal parameter for a function literal + fn2( arg = "blue sleeps faster than tuesday", - arg2 = "the quick brown fox jumped over the lazy dog"): env => + arg2 = "the quick brown fox jumped over the lazy dog"): env => // error val x = env // error println(x) diff --git a/tests/neg/i22906.check b/tests/neg/i22906.check index 118f9f4fa069..c2ff40929a39 100644 --- a/tests/neg/i22906.check +++ b/tests/neg/i22906.check @@ -1,6 +1,11 @@ Flag -indent set repeatedly --- Error: tests/neg/i22906.scala:6:15 ---------------------------------------------------------------------------------- -6 | {`1`: Int => 5} // error - | ^ - | parentheses are required around the parameter of a lambda - | This construct can be rewritten automatically under -rewrite -source 3.0-migration. +-- [E040] Syntax Error: tests/neg/i22906.scala:6:20 -------------------------------------------------------------------- +6 | {`1`: Int => 5} // error // error + | ^ + | end of single-line lambda expected, but '}' found +-- [E006] Not Found Error: tests/neg/i22906.scala:6:5 ------------------------------------------------------------------ +6 | {`1`: Int => 5} // error // error + | ^^^ + | Not found: 1 + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i22906.scala b/tests/neg/i22906.scala index ca464e99bd48..343b13ce495e 100644 --- a/tests/neg/i22906.scala +++ b/tests/neg/i22906.scala @@ -3,4 +3,4 @@ // does not reproduce under "vulpix" test rig, which enforces certain flag sets? def program: Int => Int = - {`1`: Int => 5} // error + {`1`: Int => 5} // error // error diff --git a/tests/pos/change-lambda.scala b/tests/pos/change-lambda.scala new file mode 100644 index 000000000000..85814ff52eeb --- /dev/null +++ b/tests/pos/change-lambda.scala @@ -0,0 +1,7 @@ +def foo(x: Any) = ??? + +def test(xs: List[Int]) = + xs.map: x => x + foo: + xs.map: x => x + 1 + diff --git a/tests/pos/closure-args.scala b/tests/pos/closure-args.scala index 9d7778e2e5e0..b3d321a5df73 100644 --- a/tests/pos/closure-args.scala +++ b/tests/pos/closure-args.scala @@ -1,36 +1,15 @@ -import language.`3.3` -object Test1: - val xs = List(1, 2, 3) - val ys = xs.map: x => - x + 1 - val ys1 = List(1) map: x => - x + 1 - val x = ys.foldLeft(0): (x, y) => - x + y - val y = ys.foldLeft(0): (x: Int, y: Int) => - val z = x + y - z * z - val a: Int = xs - .map: x => - x * x - .filter: (y: Int) => - y > 0 - (0) - val e = xs.map: - case 1 => 2 - case 2 => 3 - case x => x - .filter: - x => x > 0 +val z = List(1).map: + => // ok + ??? - extension (xs: List[Int]) def foo(f: [X] => X => X) = () +val xs = List(1) +val b: Int = xs + .map: x => x * x + .filter: y => y > 0 + (0) - val p = xs.foo: - [X] => (x: X) => x - - val q = (x: String => String) => x - - val r = x < 0 && locally: - y > 0 +val d2: String = xs + .map: x => x.toString + xs.dropWhile: y => y > 0 + .filter: z => !z.isEmpty + (0) From 97208f838087140a92e722761cba88b46223f23b Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Aug 2025 10:25:42 +0200 Subject: [PATCH 2/5] Small tweaks --- .../dotty/tools/dotc/parsing/Parsers.scala | 30 ++++++++++++++----- .../dotty/tools/dotc/parsing/Scanners.scala | 6 ++-- .../src/dotty/tools/dotc/parsing/Tokens.scala | 4 +-- tests/neg/closure-args.check | 12 +++++--- tests/neg/closure-args.scala | 3 ++ 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 3a13499f1eee..339b8142d392 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1085,9 +1085,19 @@ object Parsers { } /** Is the token sequence following the current `:` token classified as a lambda? - * This is the case if the input starts with an identifier, a wildcard, or - * something enclosed in (...) or [...], and this is followed by a `=>` or `?=>` - * and an INDENT. + * If yes return a defined parsing function to parse the lambda body, if not + * return None. The case is triggered in two :if the input starts with an identifier, + * a wildcard, or something enclosed in (...) or [...], this is followed by a + * `=>` or `?=>`, one one of the following two cases applies: + * 1. The next token is an indent. In this case the return parsing function parses + * an Expr in location Location.InColonArg. + * 2. The next token is on the same line and the enclosing region is not `(...)`. + * In this case the parsing function parses an Expr in location Location.InColonArg + * enclosed in a SingleLineLambda region, and then eats the ENDlambda token + * generated by the Scanner at the end of that region. + * The reason for excluding (2) in regions enclosed in parentheses is to avoid + * an ambiguity with type ascription `(x: A => B)`, where function types are only + * allowed inside parentheses. */ def followingIsLambdaAfterColon(): Option[() => Tree] = val lookahead = in.LookaheadScanner(allowIndent = true) @@ -1101,7 +1111,7 @@ object Parsers { Some: () => val t = inSepRegion(SingleLineLambda(_)): expr(Location.InColonArg) - accept(ENDLAMBDA) + accept(ENDlambda) t else None else None @@ -1178,11 +1188,14 @@ object Parsers { case _ => infixOp } - /** True if we are seeing a lambda argument after a colon of the form: + /** Optionally, if we are seeing a lambda argument after a colon of the form * : (params) => * body + * or a single-line lambda + * : (params) => body + * then return the function used to parse `body`. */ - def isColonLambda: Option[() => Tree] = + def detectColonLambda: Option[() => Tree] = if sourceVersion.enablesFewerBraces && in.token == COLONfollow then followingIsLambdaAfterColon() else None @@ -1209,7 +1222,7 @@ object Parsers { opStack = OpInfo(top1, op, in.offset) :: opStack colonAtEOLOpt() newLineOptWhenFollowing(canStartOperand) - isColonLambda match + detectColonLambda match case Some(parseExpr) => in.nextToken() recur(parseExpr()) @@ -2778,6 +2791,7 @@ object Parsers { * | SimpleExpr1 ColonArgument * ColonArgument ::= colon [LambdaStart] * indent (CaseClauses | Block) outdent + * | colon LambdaStart expr ENDlambda -- under experimental.relaxedLambdaSyntax * LambdaStart ::= FunParams (‘=>’ | ‘?=>’) * | TypTypeParamClause ‘=>’ * ColonArgBody ::= indent (CaseClauses | Block) outdent @@ -2860,7 +2874,7 @@ object Parsers { makeParameter(name.asTermName, typedOpt(), Modifiers(), isBackquoted = isBackquoted(id)) } case _ => t - else isColonLambda match + else detectColonLambda match case Some(parseExpr) => val app = atSpan(startOffset(t), in.skipToken()): diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 9d897eda124f..ec246f7a3742 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -617,8 +617,8 @@ object Scanners { && !statCtdTokens.contains(lastToken) && !isTrailingBlankLine - if currentRegion.closedBy == ENDLAMBDA then - insert(ENDLAMBDA, lineOffset) + if currentRegion.closedBy == ENDlambda then + insert(ENDlambda, lineOffset) else if newlineIsSeparating && canEndStatTokens.contains(lastToken) && canStartStatTokens.contains(token) @@ -1685,7 +1685,7 @@ object Scanners { case class InParens(prefix: Token, outer: Region) extends Region(prefix + 1) case class InBraces(outer: Region) extends Region(RBRACE) case class InCase(outer: Region) extends Region(OUTDENT) - case class SingleLineLambda(outer: Region) extends Region(ENDLAMBDA) + case class SingleLineLambda(outer: Region) extends Region(ENDlambda) /** A class describing an indentation region. * @param width The principal indentation width diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 1ed65bf98144..b3728ac0f547 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -203,7 +203,7 @@ object Tokens extends TokensCommon { // A `:` recognized as starting an indentation block inline val SELFARROW = 90; enter(SELFARROW, "=>") // reclassified ARROW following self-type - inline val ENDLAMBDA = 99; enter(ENDLAMBDA, "end of single-line lambda") + inline val ENDlambda = 99; enter(ENDlambda, "end of single-line lambda") /** XML mode */ inline val XMLSTART = 100; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate @@ -269,7 +269,7 @@ object Tokens extends TokensCommon { final val canStartStatTokens3: TokenSet = canStartExprTokens3 | mustStartStatTokens | BitSet( AT, CASE, END) - final val canEndStatTokens: TokenSet = atomicExprTokens | BitSet(TYPE, GIVEN, RPAREN, RBRACE, RBRACKET, OUTDENT, ENDLAMBDA) + final val canEndStatTokens: TokenSet = atomicExprTokens | BitSet(TYPE, GIVEN, RPAREN, RBRACE, RBRACKET, OUTDENT, ENDlambda) /** Tokens that stop a lookahead scan search for a `<-`, `then`, or `do`. * Used for disambiguating between old and new syntax. diff --git a/tests/neg/closure-args.check b/tests/neg/closure-args.check index 1000862696f1..e4590e9147c1 100644 --- a/tests/neg/closure-args.check +++ b/tests/neg/closure-args.check @@ -14,12 +14,16 @@ | Illegal start of toplevel definition | | longer explanation available when compiling with `-explain` --- [E018] Syntax Error: tests/neg/closure-args.scala:18:46 ------------------------------------------------------------- -18 |val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error - | ^^^^ - | expression expected but case found +-- [E018] Syntax Error: tests/neg/closure-args.scala:18:20 ------------------------------------------------------------- +18 |val e = xs.map: y => // error + | ^ + | expression expected but end of single-line lambda found | | longer explanation available when compiling with `-explain` +-- [E040] Syntax Error: tests/neg/closure-args.scala:21:64 ------------------------------------------------------------- +21 |val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error + | ^^^^ + | end of single-line lambda expected, but 'case' found -- [E008] Not Found Error: tests/neg/closure-args.scala:10:4 ----------------------------------------------------------- 8 |val b: Int = xs 9 | .map: x => x diff --git a/tests/neg/closure-args.scala b/tests/neg/closure-args.scala index 4986a6f6e62c..5e4cc4a02f03 100644 --- a/tests/neg/closure-args.scala +++ b/tests/neg/closure-args.scala @@ -15,4 +15,7 @@ val d = xs val c = List(xs.map: y => y + y) // error // error // error // error +val e = xs.map: y => // error +y + 1 + val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error From d94c344f986de8a8049b423f5725c3134a5e8267 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Aug 2025 10:27:29 +0200 Subject: [PATCH 3/5] Allow single case clauses as expressions --- .../dotty/tools/dotc/parsing/Parsers.scala | 15 ++++++++----- docs/_docs/internals/syntax.md | 3 +++ tests/run/single-case-expr.scala | 21 +++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 tests/run/single-case-expr.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 339b8142d392..6f81e71844a2 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2378,6 +2378,7 @@ object Parsers { /** Expr ::= [`implicit'] FunParams (‘=>’ | ‘?=>’) Expr * | TypTypeParamClause ‘=>’ Expr + * | ExprCaseClause * | Expr1 * FunParams ::= Bindings * | id @@ -2429,6 +2430,8 @@ object Parsers { val arrowOffset = accept(ARROW) val body = expr(location) makePolyFunction(tparams, body, "literal", errorTermTree(arrowOffset), start, arrowOffset) + case CASE => + singleCaseMatch() case _ => val saved = placeholderParams placeholderParams = Nil @@ -2492,9 +2495,8 @@ object Parsers { if in.token == CATCH then val span = in.offset in.nextToken() - (if in.token == CASE then Match(EmptyTree, caseClause(exprOnly = true) :: Nil) - else subExpr(), - span) + (if in.token == CASE then singleCaseMatch() else subExpr(), + span) else (EmptyTree, -1) handler match { @@ -3188,9 +3190,9 @@ object Parsers { 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. + warning(em"""Misleading indentation: this expression forms part of the preceding 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 + |Otherwise, if the handler is intended to be empty, use a multi-line match or catch with |an indented case.""") expr() else block() @@ -3206,6 +3208,9 @@ object Parsers { CaseDef(pat, grd1, body) } + def singleCaseMatch() = + Match(EmptyTree, caseClause(exprOnly = true) :: Nil) + /** TypeCaseClause ::= ‘case’ (InfixType | ‘_’) ‘=>’ Type [semi] */ def typeCaseClause(): CaseDef = atSpan(in.offset) { diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index 7fd7ec1be2e1..c304d8059743 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -243,6 +243,7 @@ CapFilter ::= ‘.’ ‘as’ ‘[’ QualId ’]’ ```ebnf Expr ::= FunParams (‘=>’ | ‘?=>’) Expr Function(args, expr), Function(ValDef([implicit], id, TypeTree(), EmptyTree), expr) | TypTypeParamClause ‘=>’ Expr PolyFunction(ts, expr) + | ExprCaseClause | Expr1 BlockResult ::= FunParams (‘=>’ | ‘?=>’) Block | TypTypeParamClause ‘=>’ Block @@ -295,6 +296,8 @@ SimpleExpr ::= SimpleRef | XmlExpr -- to be dropped ColonArgument ::= colon [LambdaStart] indent (CaseClauses | Block) outdent + | colon LambdaStart expr ENDlambda -- ENDlambda is inserted for each production at next EOL + -- does not apply if enclosed in parens LambdaStart ::= FunParams (‘=>’ | ‘?=>’) | TypTypeParamClause ‘=>’ Quoted ::= ‘'’ ‘{’ Block ‘}’ diff --git a/tests/run/single-case-expr.scala b/tests/run/single-case-expr.scala new file mode 100644 index 000000000000..afda049c601f --- /dev/null +++ b/tests/run/single-case-expr.scala @@ -0,0 +1,21 @@ +case class Foo(x: Int, y: Int) +@main def Test = + val f: List[Int] => Int = case y :: ys => y + val xs = List(1, 2, 3) + assert(f(xs) == 1) + + val g: Foo => Int = identity(case Foo(a, b) => a) + val foo = Foo(1, 2) + assert(g(foo) == 1) + + val a1 = Seq((1, 2), (3, 4)).collect(case (a, b) if b > 2 => a) + assert(a1 == Seq(3)) + + var a2 = Seq((1, 2), (3, 4)).collect( + case (a, b) => + println(b) + a + ) + assert(a2 == Seq(1, 3)) + + val partial: PartialFunction[(Int, Int), Int] = case (a, b) if b > 2 => a From 8665a3c1f63ac5bc4dcd2b0c427a5e4ffb0485fe Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Aug 2025 11:06:17 +0200 Subject: [PATCH 4/5] Allow single-case lambda after colon --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 5 ++++- docs/_docs/internals/syntax.md | 1 + tests/pos/closure-args.scala | 5 +++++ tests/run/single-case-expr.scala | 9 +++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 6f81e71844a2..3e735e11f125 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1122,6 +1122,8 @@ object Parsers { else if lookahead.token == LPAREN || lookahead.token == LBRACKET then lookahead.skipParens() isArrowIndent() + else if lookahead.token == CASE then + Some(() => singleCaseMatch()) else None @@ -2378,7 +2380,7 @@ object Parsers { /** Expr ::= [`implicit'] FunParams (‘=>’ | ‘?=>’) Expr * | TypTypeParamClause ‘=>’ Expr - * | ExprCaseClause + * | ExprCaseClause -- under experimental.relaxedLambdaSyntax * | Expr1 * FunParams ::= Bindings * | id @@ -2794,6 +2796,7 @@ object Parsers { * ColonArgument ::= colon [LambdaStart] * indent (CaseClauses | Block) outdent * | colon LambdaStart expr ENDlambda -- under experimental.relaxedLambdaSyntax + * | colon ExprCaseClause -- under experimental.relaxedLambdaSyntax * LambdaStart ::= FunParams (‘=>’ | ‘?=>’) * | TypTypeParamClause ‘=>’ * ColonArgBody ::= indent (CaseClauses | Block) outdent diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index c304d8059743..89b180274e55 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -298,6 +298,7 @@ ColonArgument ::= colon [LambdaStart] indent (CaseClauses | Block) outdent | colon LambdaStart expr ENDlambda -- ENDlambda is inserted for each production at next EOL -- does not apply if enclosed in parens + | colon ExprCaseClause LambdaStart ::= FunParams (‘=>’ | ‘?=>’) | TypTypeParamClause ‘=>’ Quoted ::= ‘'’ ‘{’ Block ‘}’ diff --git a/tests/pos/closure-args.scala b/tests/pos/closure-args.scala index b3d321a5df73..66256364875d 100644 --- a/tests/pos/closure-args.scala +++ b/tests/pos/closure-args.scala @@ -13,3 +13,8 @@ val d2: String = xs .filter: z => !z.isEmpty (0) +val d3: String = xs + .map: x => x.toString + xs.collect: case y if y > 0 => y + .filter: z => !z.isEmpty + (0) + diff --git a/tests/run/single-case-expr.scala b/tests/run/single-case-expr.scala index afda049c601f..3c8c6180df7b 100644 --- a/tests/run/single-case-expr.scala +++ b/tests/run/single-case-expr.scala @@ -8,6 +8,10 @@ case class Foo(x: Int, y: Int) val foo = Foo(1, 2) assert(g(foo) == 1) + val h1: Foo => Int = identity: case Foo(a, b) => a + val h2: Foo => Int = identity: case Foo(a, b) => + a + val a1 = Seq((1, 2), (3, 4)).collect(case (a, b) if b > 2 => a) assert(a1 == Seq(3)) @@ -18,4 +22,9 @@ case class Foo(x: Int, y: Int) ) assert(a2 == Seq(1, 3)) + val a3 = Seq((1, 2), (3, 4)).collect: case (a, b) if b > 2 => a + assert(a3 == Seq(3)) + val partial: PartialFunction[(Int, Int), Int] = case (a, b) if b > 2 => a + + From 07611bf9a503d163210452ad831adfd5bdbbbb67 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Aug 2025 15:17:57 +0200 Subject: [PATCH 5/5] Put extensions under language.experimental.relaxedLambdaSyntax --- .../src/dotty/tools/dotc/config/Feature.scala | 1 + .../dotty/tools/dotc/parsing/Parsers.scala | 40 +++++++++++-------- library/src/scala/language.scala | 4 ++ .../runtime/stdLibPatches/language.scala | 5 +++ tests/neg/closure-args.scala | 2 +- tests/neg/i22193.scala | 2 +- tests/neg/i22906.scala | 2 +- tests/pos/change-lambda.scala | 2 + tests/pos/closure-args.scala | 1 + tests/run/single-case-expr.scala | 2 + 10 files changed, 41 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 70a77c9560b2..6bad5d6b7c77 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -38,6 +38,7 @@ object Feature: val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions") val packageObjectValues = experimental("packageObjectValues") val subCases = experimental("subCases") + val relaxedLambdaSyntax = experimental("relaxedLambdaSyntax") def experimentalAutoEnableFeatures(using Context): List[TermName] = defn.languageExperimentalFeatures diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 3e735e11f125..004aeb12b0a2 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1086,18 +1086,20 @@ object Parsers { /** Is the token sequence following the current `:` token classified as a lambda? * If yes return a defined parsing function to parse the lambda body, if not - * return None. The case is triggered in two :if the input starts with an identifier, - * a wildcard, or something enclosed in (...) or [...], this is followed by a - * `=>` or `?=>`, one one of the following two cases applies: - * 1. The next token is an indent. In this case the return parsing function parses - * an Expr in location Location.InColonArg. - * 2. The next token is on the same line and the enclosing region is not `(...)`. - * In this case the parsing function parses an Expr in location Location.InColonArg - * enclosed in a SingleLineLambda region, and then eats the ENDlambda token - * generated by the Scanner at the end of that region. - * The reason for excluding (2) in regions enclosed in parentheses is to avoid - * an ambiguity with type ascription `(x: A => B)`, where function types are only - * allowed inside parentheses. + * return None. The case is triggered in two situations: + * 1. If the input starts with an identifier, a wildcard, or something + * enclosed in (...) or [...], this is followed by a `=>` or `?=>`, + * and one of the following two subcases applies: + * 1a. The next token is an indent. In this case the return parsing function parses + * an Expr in location Location.InColonArg. + * 1b. Under relaxedLambdaSyntax: the next token is on the same line and the enclosing region is not `(...)`. + * In this case the parsing function parses an Expr in location Location.InColonArg + * enclosed in a SingleLineLambda region, and then eats the ENDlambda token + * generated by the Scanner at the end of that region. + * The reason for excluding (1b) in regions enclosed in parentheses is to avoid + * an ambiguity with type ascription `(x: A => B)`, where function types are only + * allowed inside parentheses. + * 2. Under relaxedLambdaSyntax: the input starts with a `case`. */ def followingIsLambdaAfterColon(): Option[() => Tree] = val lookahead = in.LookaheadScanner(allowIndent = true) @@ -1107,7 +1109,9 @@ object Parsers { lookahead.observeArrowIndented() if lookahead.token == INDENT || lookahead.token == EOF then Some(() => expr(Location.InColonArg)) - else if !in.currentRegion.isInstanceOf[InParens] then + else if in.featureEnabled(Feature.relaxedLambdaSyntax) + && !in.currentRegion.isInstanceOf[InParens] + then Some: () => val t = inSepRegion(SingleLineLambda(_)): expr(Location.InColonArg) @@ -1122,7 +1126,7 @@ object Parsers { else if lookahead.token == LPAREN || lookahead.token == LBRACKET then lookahead.skipParens() isArrowIndent() - else if lookahead.token == CASE then + else if lookahead.token == CASE && in.featureEnabled(Feature.relaxedLambdaSyntax) then Some(() => singleCaseMatch()) else None @@ -1193,9 +1197,11 @@ object Parsers { /** Optionally, if we are seeing a lambda argument after a colon of the form * : (params) => * body - * or a single-line lambda + * or a single-line lambda (under relaxedLambdaSyntax) * : (params) => body - * then return the function used to parse `body`. + * or a case clause (under relaxedLambdaSyntax) + * : case pat guard => rhs + * then return the function used to parse `body` or the case clause. */ def detectColonLambda: Option[() => Tree] = if sourceVersion.enablesFewerBraces && in.token == COLONfollow @@ -2432,7 +2438,7 @@ object Parsers { val arrowOffset = accept(ARROW) val body = expr(location) makePolyFunction(tparams, body, "literal", errorTermTree(arrowOffset), start, arrowOffset) - case CASE => + case CASE if in.featureEnabled(Feature.relaxedLambdaSyntax) => singleCaseMatch() case _ => val saved = placeholderParams diff --git a/library/src/scala/language.scala b/library/src/scala/language.scala index bacbb09ad615..685d5aed8d67 100644 --- a/library/src/scala/language.scala +++ b/library/src/scala/language.scala @@ -355,6 +355,10 @@ 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 `:` + */ + @compileTimeOnly("`relaxedLambdaSyntax` can only be used at compile time in import statements") + object relaxedLambdaSyntax } /** The deprecated object contains features that are no longer officially suypported in Scala. diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 9d38ea4371ff..87677cb72c03 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -161,6 +161,11 @@ 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 `:` + */ + @compileTimeOnly("`relaxedLambdaSyntax` can only be used at compile time in import statements") + object relaxedLambdaSyntax end experimental /** The deprecated object contains features that are no longer officially suypported in Scala. diff --git a/tests/neg/closure-args.scala b/tests/neg/closure-args.scala index 5e4cc4a02f03..2544b1be3a66 100644 --- a/tests/neg/closure-args.scala +++ b/tests/neg/closure-args.scala @@ -1,4 +1,4 @@ - +import language.experimental.relaxedLambdaSyntax val x = List(1).map: (x: => Int) => // error ??? val z = List(1).map: + => // ok diff --git a/tests/neg/i22193.scala b/tests/neg/i22193.scala index d3628b3e6b98..638e8e7e4b03 100644 --- a/tests/neg/i22193.scala +++ b/tests/neg/i22193.scala @@ -1,4 +1,4 @@ - +import language.experimental.relaxedLambdaSyntax def fn2(arg: String, arg2: String)(f: String => Unit): Unit = f(arg) def fn3(arg: String, arg2: String)(f: => Unit): Unit = f diff --git a/tests/neg/i22906.scala b/tests/neg/i22906.scala index 343b13ce495e..065da159e2a8 100644 --- a/tests/neg/i22906.scala +++ b/tests/neg/i22906.scala @@ -1,6 +1,6 @@ //> using options -rewrite -indent //> nominally using scala 3.7.0-RC1 // does not reproduce under "vulpix" test rig, which enforces certain flag sets? - +import language.experimental.relaxedLambdaSyntax def program: Int => Int = {`1`: Int => 5} // error // error diff --git a/tests/pos/change-lambda.scala b/tests/pos/change-lambda.scala index 85814ff52eeb..b5abf24dfd4c 100644 --- a/tests/pos/change-lambda.scala +++ b/tests/pos/change-lambda.scala @@ -1,3 +1,5 @@ +import language.experimental.relaxedLambdaSyntax + def foo(x: Any) = ??? def test(xs: List[Int]) = diff --git a/tests/pos/closure-args.scala b/tests/pos/closure-args.scala index 66256364875d..6ff1d6c40619 100644 --- a/tests/pos/closure-args.scala +++ b/tests/pos/closure-args.scala @@ -1,3 +1,4 @@ +import language.experimental.relaxedLambdaSyntax val z = List(1).map: + => // ok ??? diff --git a/tests/run/single-case-expr.scala b/tests/run/single-case-expr.scala index 3c8c6180df7b..efc963b68e01 100644 --- a/tests/run/single-case-expr.scala +++ b/tests/run/single-case-expr.scala @@ -1,3 +1,5 @@ +import language.experimental.relaxedLambdaSyntax + case class Foo(x: Int, y: Int) @main def Test = val f: List[Int] => Int = case y :: ys => y