From 057d1f0abc065ce9daa6032ac05f4ea81e6e80c2 Mon Sep 17 00:00:00 2001 From: Wojciech Mazur Date: Wed, 12 Nov 2025 14:34:37 +0100 Subject: [PATCH 1/7] 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. [Cherry-picked 271f18f991495693507d9cdf7616bc1545d5f094][modified] --- .../dotty/tools/dotc/parsing/Parsers.scala | 90 ++++++++++++------- .../dotty/tools/dotc/parsing/Scanners.scala | 8 +- .../src/dotty/tools/dotc/parsing/Tokens.scala | 6 +- tests/neg/closure-args.check | 59 ++++++++++++ tests/neg/closure-args.scala | 22 +++-- tests/neg/i22193.check | 34 +++++++ tests/neg/i22193.scala | 4 +- tests/neg/i22906.scala | 2 +- tests/pos/change-lambda.scala | 7 ++ tests/pos/closure-args.scala | 43 +++------ 10 files changed, 194 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 64eb442c239a..ff48c1825dce 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1090,19 +1090,36 @@ 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. - */ - def followingIsLambdaAfterColon(): Boolean = + * 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) .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() @@ -1110,7 +1127,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? @@ -1175,12 +1193,17 @@ 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 = - sourceVersion.enablesFewerBraces && in.token == COLONfollow && followingIsLambdaAfterColon() + def detectColonLambda: Option[() => Tree] = + if sourceVersion.enablesFewerBraces && in.token == COLONfollow + then followingIsLambdaAfterColon() + else None /** operand { infixop operand | MatchClause } [postfixop], * @@ -1204,17 +1227,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)) + detectColonLambda 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)) @@ -2771,6 +2796,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 @@ -2853,12 +2879,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 detectColonLambda 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..ec246f7a3742 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..b3728ac0f547 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..e4590e9147c1 --- /dev/null +++ b/tests/neg/closure-args.check @@ -0,0 +1,59 @@ +-- [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: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 +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..5e4cc4a02f03 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,17 @@ 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 e = xs.map: y => // error +y + 1 + +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.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 67270fd38be08eca706fee0836f143480f41f081 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Aug 2025 10:27:29 +0200 Subject: [PATCH 2/7] Allow single case clauses as expressions [Cherry-picked 59bddd4bc5b892503aa0229cea064b0cd610f353] --- .../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 ff48c1825dce..92523a64f8e5 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2383,6 +2383,7 @@ object Parsers { /** Expr ::= [`implicit'] FunParams (‘=>’ | ‘?=>’) Expr * | TypTypeParamClause ‘=>’ Expr + * | ExprCaseClause * | Expr1 * FunParams ::= Bindings * | id @@ -2434,6 +2435,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 @@ -2497,9 +2500,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 { @@ -3193,9 +3195,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() @@ -3212,6 +3214,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 686d0551e0f6..a1826146c4a9 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 f61d750232f8691a483045b6b89b17763c89fcdb Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Aug 2025 11:06:17 +0200 Subject: [PATCH 3/7] Allow single-case lambda after colon [Cherry-picked ba0ec2ae2b2c6c2200c1978af02c0408e0f8b5de] --- 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 92523a64f8e5..2ce409020a6a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1127,6 +1127,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 @@ -2383,7 +2385,7 @@ object Parsers { /** Expr ::= [`implicit'] FunParams (‘=>’ | ‘?=>’) Expr * | TypTypeParamClause ‘=>’ Expr - * | ExprCaseClause + * | ExprCaseClause -- under experimental.relaxedLambdaSyntax * | Expr1 * FunParams ::= Bindings * | id @@ -2799,6 +2801,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 a1826146c4a9..f218c2ac3277 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 2f13937639ab21d6df46699aef070525faaddc30 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 22 Sep 2025 10:00:32 +0200 Subject: [PATCH 4/7] Put extensions under language.experimental.relaxedLambdaSyntax [Cherry-picked d70de9a8c2e3637aa1f649c8d82a528b4903330e] --- .../src/dotty/tools/dotc/config/Feature.scala | 1 + .../dotty/tools/dotc/parsing/Parsers.scala | 40 +++++++++++-------- library/src/scala/language.scala | 5 +++ .../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 | 9 +++++ 10 files changed, 49 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 6eaa4d5c98a3..5aa7ed84f72d 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -40,6 +40,7 @@ object Feature: val packageObjectValues = experimental("packageObjectValues") val multiSpreads = experimental("multiSpreads") 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 2ce409020a6a..8605a62fa181 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1091,18 +1091,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) @@ -1112,7 +1114,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) @@ -1127,7 +1131,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 @@ -1198,9 +1202,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 @@ -2437,7 +2443,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 ffc9ad2fdca1..5f39ce2017e4 100644 --- a/library/src/scala/language.scala +++ b/library/src/scala/language.scala @@ -367,6 +367,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 } /** 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 abd2a34efa77..34227259de0d 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -174,6 +174,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..098bd519c636 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 @@ -25,6 +27,13 @@ case class Foo(x: Int, y: Int) val a3 = Seq((1, 2), (3, 4)).collect: case (a, b) if b > 2 => a assert(a3 == Seq(3)) + val a4 = Seq((1, 2), (3, 4)).collect: case (a, b) if b > 2 => + a + val partial: PartialFunction[(Int, Int), Int] = case (a, b) if b > 2 => a + val mtup = (1, true).map: [T] => (x: T) => List(x) + val _: (List[Int], List[Boolean]) = mtup + + From c3c34252cf76e9014a7d6ddb69473ab7884a8351 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 25 Sep 2025 21:59:01 +0200 Subject: [PATCH 5/7] Allow curried lambdas with indented blocks [Cherry-picked eaaeb7c07b87cea077703f59a2eb3cab3f29761b] --- .../dotty/tools/dotc/parsing/Parsers.scala | 43 +++++++++++-------- docs/_docs/internals/syntax.md | 4 +- tests/neg/closure-args.check | 15 +++++-- tests/neg/closure-args.scala | 2 +- tests/pos/curried-colon-lambda.scala | 12 ++++++ 5 files changed, 50 insertions(+), 26 deletions(-) create mode 100644 tests/pos/curried-colon-lambda.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 8605a62fa181..a48103b04548 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1114,27 +1114,32 @@ object Parsers { lookahead.observeArrowIndented() if lookahead.token == INDENT || lookahead.token == EOF then Some(() => expr(Location.InColonArg)) - else if in.featureEnabled(Feature.relaxedLambdaSyntax) - && !in.currentRegion.isInstanceOf[InParens] - then - Some: () => - val t = inSepRegion(SingleLineLambda(_)): - expr(Location.InColonArg) - accept(ENDlambda) - t + else if in.featureEnabled(Feature.relaxedLambdaSyntax) then + isParamsAndArrow() match + case success @ Some(_) => success + case _ if !in.currentRegion.isInstanceOf[InParens] => + Some: () => + val t = inSepRegion(SingleLineLambda(_)): + expr(Location.InColonArg) + accept(ENDlambda) + t + case _ => None else None else None - lookahead.nextToken() - if lookahead.isIdent || lookahead.token == USCORE then + def isParamsAndArrow(): Option[() => Tree] = lookahead.nextToken() - isArrowIndent() - else if lookahead.token == LPAREN || lookahead.token == LBRACKET then - lookahead.skipParens() - isArrowIndent() - else if lookahead.token == CASE && in.featureEnabled(Feature.relaxedLambdaSyntax) then - Some(() => singleCaseMatch()) - else - None + if lookahead.isIdent || lookahead.token == USCORE then + lookahead.nextToken() + isArrowIndent() + else if lookahead.token == LPAREN || lookahead.token == LBRACKET then + lookahead.skipParens() + isArrowIndent() + else if lookahead.token == CASE && in.featureEnabled(Feature.relaxedLambdaSyntax) then + Some(() => singleCaseMatch()) + else + None + isParamsAndArrow() + end followingIsLambdaAfterColon /** Can the next lookahead token start an operand as defined by * leadingOperandTokens, or is postfix ops enabled? @@ -2804,7 +2809,7 @@ object Parsers { * | SimpleExpr (TypeArgs | NamedTypeArgs) * | SimpleExpr1 ArgumentExprs * | SimpleExpr1 ColonArgument - * ColonArgument ::= colon [LambdaStart] + * ColonArgument ::= colon {LambdaStart} * indent (CaseClauses | Block) outdent * | colon LambdaStart expr ENDlambda -- under experimental.relaxedLambdaSyntax * | colon ExprCaseClause -- under experimental.relaxedLambdaSyntax diff --git a/docs/_docs/internals/syntax.md b/docs/_docs/internals/syntax.md index f218c2ac3277..d1a5be895022 100644 --- a/docs/_docs/internals/syntax.md +++ b/docs/_docs/internals/syntax.md @@ -294,9 +294,9 @@ SimpleExpr ::= SimpleRef | SimpleExpr ColonArgument -- under language.experimental.fewerBraces | SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped) | XmlExpr -- to be dropped -ColonArgument ::= colon [LambdaStart] +ColonArgument ::= colon {LambdaStart} indent (CaseClauses | Block) outdent - | colon LambdaStart expr ENDlambda -- ENDlambda is inserted for each production at next EOL + | colon LambdaStart {LambdaStart} expr ENDlambda -- ENDlambda is inserted for each production at next EOL -- does not apply if enclosed in parens | colon ExprCaseClause LambdaStart ::= FunParams (‘=>’ | ‘?=>’) diff --git a/tests/neg/closure-args.check b/tests/neg/closure-args.check index e4590e9147c1..0ce4a2e60eb6 100644 --- a/tests/neg/closure-args.check +++ b/tests/neg/closure-args.check @@ -20,10 +20,10 @@ | 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 +-- [E040] Syntax Error: tests/neg/closure-args.scala:21:41 ------------------------------------------------------------- +21 |val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error // error + | ^ + | 'case' expected, but identifier found -- [E008] Not Found Error: tests/neg/closure-args.scala:10:4 ----------------------------------------------------------- 8 |val b: Int = xs 9 | .map: x => x @@ -57,3 +57,10 @@ | Not found: type y | | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/closure-args.scala:21:97 ------------------------------------------------------ +21 |val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error // error + | ^ + | Found: Unit + | Required: List[Int] => Int + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/closure-args.scala b/tests/neg/closure-args.scala index 2544b1be3a66..28c472644cf1 100644 --- a/tests/neg/closure-args.scala +++ b/tests/neg/closure-args.scala @@ -18,4 +18,4 @@ 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 +val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error // error diff --git a/tests/pos/curried-colon-lambda.scala b/tests/pos/curried-colon-lambda.scala new file mode 100644 index 000000000000..1be884ce3c1b --- /dev/null +++ b/tests/pos/curried-colon-lambda.scala @@ -0,0 +1,12 @@ +import language.experimental.relaxedLambdaSyntax + +def fun(f: Int => Int => Int): Int = f(1)(2) + +val a = fun: (x: Int) => + (y: Int) => x + y + +val b = fun: (x: Int) => (y: Int) => x + y + +val c = fun: (x: Int) => (y: Int) => + x + y + From b3c2aecfda56b81eb1d08f6de6d4ddb1e6564ee7 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 28 Oct 2025 13:43:41 +0100 Subject: [PATCH 6/7] Add doc page [Cherry-picked 007c4dee7e451c2f5ecfde71e788a360e0549a76] --- .../dotty/tools/dotc/parsing/Parsers.scala | 2 +- .../reference/experimental/relaxed-lambdas.md | 130 ++++++++++++++++++ docs/sidebar.yml | 1 + .../referenceReplacements/sidebar.yml | 1 + 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 docs/_docs/reference/experimental/relaxed-lambdas.md diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index a48103b04548..147a650370ac 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2811,7 +2811,7 @@ object Parsers { * | SimpleExpr1 ColonArgument * ColonArgument ::= colon {LambdaStart} * indent (CaseClauses | Block) outdent - * | colon LambdaStart expr ENDlambda -- under experimental.relaxedLambdaSyntax + * | colon LambdaStart {LambdaStart} expr ENDlambda -- under experimental.relaxedLambdaSyntax * | colon ExprCaseClause -- under experimental.relaxedLambdaSyntax * LambdaStart ::= FunParams (‘=>’ | ‘?=>’) * | TypTypeParamClause ‘=>’ diff --git a/docs/_docs/reference/experimental/relaxed-lambdas.md b/docs/_docs/reference/experimental/relaxed-lambdas.md new file mode 100644 index 000000000000..e5c9d628ec22 --- /dev/null +++ b/docs/_docs/reference/experimental/relaxed-lambdas.md @@ -0,0 +1,130 @@ +--- +layout: doc-page +title: "Relaxed Lambda Syntax" +nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/relaxed-lambdas.html +--- + +# Relaxed Lambda Syntax + +This experimental addition combines several small improvements to write function literals in more flexible ways. These improvements are specified in +[SIP 74](https://github.com/scala/improvement-proposals/pull/113) and +[SIP 75](https://github.com/scala/improvement-proposals/pull/118). +They are enabled by the experimental language import +```scala +import language.experimental.relaxedLambdas +``` + +## Single Line Lambdas + +Lambda expression following a `:` on the same line are now supported. Previously, +we needed a newline and indent after the arrow, 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 in this case to the end of the line. + +The syntax works for all kinds of function literals. They can start with one or more parameters, or with type parameters, or they can be partial functions starting +with `case`. + +```scala +Seq((1, 2), (3, 4)).map: (a, b) => a + b + +Seq((1, 2), (3, 4)).map: (a: Int, b: Int) => a + b + +Seq((1, 2), (3, 4)).collect: case (a, b) if b > 2 = a + +(1, true).map: [T] => (x: T) => List(x) +``` + +The syntax does not work for function values that do not contain a `=>` or `?=>`. For instance the following are illegal. + +```scala +Seq((1, 2), (3, 4)).map: _ + _ // error + +Seq(1, 2, 3).map: plus1 // error +``` + +Single-line lambdas can be nested, as in: +```scala + xs.map: x => x.toString + xs.dropWhile: y => y > 0 +``` + +### Detailed Spec + +A `:` means application if its is followed by one of the following: + + 1. a line end and an indented block, + 2. a parameter section, followed by `=>` or `?=>`, a line end and an indented block, + 3. a parameter section, followed by `=>` or `?=>` and an expression on a single line, + 4. a case clause, representing a single-case partial function. + +(1) and (2) is the status quo, (3) and (4) are new. + +**Restriction:** (3) and (4) do not apply in code that is immediately enclosed in parentheses (without being more closely enclosed in braces or indentation). This is to avoid an ambiguity with type ascription. For instance, +```scala +( + x: Int => Int +) +``` +still means type ascription, no interpretation as function application is attempted. + +## Curried Multi-Line Lambdas + +Previously, we admitted only a single parameter section and an arrow before +an indented block. We now also admit multiple such sections. So the following +is now legal: + +```scala +def fun(f: Int => Int => Int): Int = f(1)(2) + +fun: (x: Int) => y => + x + y +``` + +In the detailed spec above, point (2) is modified as follows: + +2. _one or more_ parameter sections, _each_ followed by `=>` or `?=>`, and finally a line end and an indented block. + +## Case Expressions + +Previously, case clauses making up a partial function had to be written in +braces or an indented block. If there is only a single case clause, we now allow it to be written also inside parentheses or as a top-level expression. + +Examples: + +```scala +case class Pair(x: Int, y: Int) + +Seq(Pair(1, 2), Pair(3, 4)).collect(case Pair(a, b) if b > 2 => a) + +Seq(Pair(1, 2), Pair(3, 4)).collect( + case (a, b) => + println(b) + a +) + +val partial: PartialFunction[(Int, Int), Int] = case (a, b) if b > 2 => a +``` + +## Syntax Changes + +``` +Expr ::= ... + | ExprCaseClause + +ColonArgument ::= colon {LambdaStart} indent (CaseClauses | Block) outdent + | colon LambdaStart {LambdaStart} expr ENDlambda + | colon ExprCaseClause +``` +Here, ENDlambda is a token synthesized at the next end of line following the +token that starts the production. ` + +`ExprCaseClause` already exists in the grammar. It is defined as follows: +``` +ExprCaseClause ::= ‘case’ Pattern [Guard] ‘=>’ Expr +``` diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 276491cfd0a8..b55a8087b0b3 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -184,6 +184,7 @@ subsection: - page: reference/experimental/unrolled-defs.md - page: reference/experimental/package-object-values.md - page: reference/experimental/quoted-patterns-with-polymorphic-functions.md + - page: reference/experimental/relaxed-lambdas.md - page: reference/syntax.md - title: Language Versions index: reference/language-versions/language-versions.md diff --git a/project/resources/referenceReplacements/sidebar.yml b/project/resources/referenceReplacements/sidebar.yml index b343f2644a9b..d3ca00297c2f 100644 --- a/project/resources/referenceReplacements/sidebar.yml +++ b/project/resources/referenceReplacements/sidebar.yml @@ -165,6 +165,7 @@ subsection: - page: reference/experimental/capture-checking/how-to-use.md - page: reference/experimental/capture-checking/internals.md - page: reference/experimental/tupled-function.md + - page: reference/experimental/relaxed-lambdas.md - page: reference/syntax.md - title: Language Versions index: reference/language-versions/language-versions.md From 5dd1f706db929c9a60fcf2257b3a7a32358f68bb Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 30 Oct 2025 18:25:51 +0100 Subject: [PATCH 7/7] Update docs/_docs/reference/experimental/relaxed-lambdas.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Oliver Bračevac [Cherry-picked 10c7d2bd664d67233cb519597284672d614e463f] --- docs/_docs/reference/experimental/relaxed-lambdas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/reference/experimental/relaxed-lambdas.md b/docs/_docs/reference/experimental/relaxed-lambdas.md index e5c9d628ec22..ecedda82d89a 100644 --- a/docs/_docs/reference/experimental/relaxed-lambdas.md +++ b/docs/_docs/reference/experimental/relaxed-lambdas.md @@ -122,7 +122,7 @@ ColonArgument ::= colon {LambdaStart} indent (CaseClauses | Block) outdent | colon ExprCaseClause ``` Here, ENDlambda is a token synthesized at the next end of line following the -token that starts the production. ` +token that starts the production. `ExprCaseClause` already exists in the grammar. It is defined as follows: ```