Skip to content

Commit 2f97633

Browse files
authored
Allow single-line lambdas after : (#23821)
Previously, we need to 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 to the end of the line. This is a trial balloon to see whether anything breaks. If we want to pursue this it needs a SIP.
2 parents ad0b86e + 10c7d2b commit 2f97633

File tree

20 files changed

+450
-100
lines changed

20 files changed

+450
-100
lines changed

compiler/src/dotty/tools/dotc/config/Feature.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ object Feature:
4040
val packageObjectValues = experimental("packageObjectValues")
4141
val multiSpreads = experimental("multiSpreads")
4242
val subCases = experimental("subCases")
43+
val relaxedLambdaSyntax = experimental("relaxedLambdaSyntax")
4344

4445
def experimentalAutoEnableFeatures(using Context): List[TermName] =
4546
defn.languageExperimentalFeatures

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,27 +1090,56 @@ object Parsers {
10901090
}
10911091

10921092
/** Is the token sequence following the current `:` token classified as a lambda?
1093-
* This is the case if the input starts with an identifier, a wildcard, or
1094-
* something enclosed in (...) or [...], and this is followed by a `=>` or `?=>`
1095-
* and an INDENT.
1096-
*/
1097-
def followingIsLambdaAfterColon(): Boolean =
1093+
* If yes return a defined parsing function to parse the lambda body, if not
1094+
* return None. The case is triggered in two situations:
1095+
* 1. If the input starts with an identifier, a wildcard, or something
1096+
* enclosed in (...) or [...], this is followed by a `=>` or `?=>`,
1097+
* and one of the following two subcases applies:
1098+
* 1a. The next token is an indent. In this case the return parsing function parses
1099+
* an Expr in location Location.InColonArg.
1100+
* 1b. Under relaxedLambdaSyntax: the next token is on the same line and the enclosing region is not `(...)`.
1101+
* In this case the parsing function parses an Expr in location Location.InColonArg
1102+
* enclosed in a SingleLineLambda region, and then eats the ENDlambda token
1103+
* generated by the Scanner at the end of that region.
1104+
* The reason for excluding (1b) in regions enclosed in parentheses is to avoid
1105+
* an ambiguity with type ascription `(x: A => B)`, where function types are only
1106+
* allowed inside parentheses.
1107+
* 2. Under relaxedLambdaSyntax: the input starts with a `case`.
1108+
*/
1109+
def followingIsLambdaAfterColon(): Option[() => Tree] =
10981110
val lookahead = in.LookaheadScanner(allowIndent = true)
10991111
.tap(_.currentRegion.knownWidth = in.currentRegion.indentWidth)
1100-
def isArrowIndent() =
1101-
lookahead.isArrow
1102-
&& {
1112+
def isArrowIndent(): Option[() => Tree] =
1113+
if lookahead.isArrow then
11031114
lookahead.observeArrowIndented()
1104-
lookahead.token == INDENT || lookahead.token == EOF
1105-
}
1106-
lookahead.nextToken()
1107-
if lookahead.isIdent || lookahead.token == USCORE then
1115+
if lookahead.token == INDENT || lookahead.token == EOF then
1116+
Some(() => expr(Location.InColonArg))
1117+
else if in.featureEnabled(Feature.relaxedLambdaSyntax) then
1118+
isParamsAndArrow() match
1119+
case success @ Some(_) => success
1120+
case _ if !in.currentRegion.isInstanceOf[InParens] =>
1121+
Some: () =>
1122+
val t = inSepRegion(SingleLineLambda(_)):
1123+
expr(Location.InColonArg)
1124+
accept(ENDlambda)
1125+
t
1126+
case _ => None
1127+
else None
1128+
else None
1129+
def isParamsAndArrow(): Option[() => Tree] =
11081130
lookahead.nextToken()
1109-
isArrowIndent()
1110-
else if lookahead.token == LPAREN || lookahead.token == LBRACKET then
1111-
lookahead.skipParens()
1112-
isArrowIndent()
1113-
else false
1131+
if lookahead.isIdent || lookahead.token == USCORE then
1132+
lookahead.nextToken()
1133+
isArrowIndent()
1134+
else if lookahead.token == LPAREN || lookahead.token == LBRACKET then
1135+
lookahead.skipParens()
1136+
isArrowIndent()
1137+
else if lookahead.token == CASE && in.featureEnabled(Feature.relaxedLambdaSyntax) then
1138+
Some(() => singleCaseMatch())
1139+
else
1140+
None
1141+
isParamsAndArrow()
1142+
end followingIsLambdaAfterColon
11141143

11151144
/** Can the next lookahead token start an operand as defined by
11161145
* leadingOperandTokens, or is postfix ops enabled?
@@ -1175,12 +1204,19 @@ object Parsers {
11751204
case _ => infixOp
11761205
}
11771206

1178-
/** True if we are seeing a lambda argument after a colon of the form:
1207+
/** Optionally, if we are seeing a lambda argument after a colon of the form
11791208
* : (params) =>
11801209
* body
1210+
* or a single-line lambda (under relaxedLambdaSyntax)
1211+
* : (params) => body
1212+
* or a case clause (under relaxedLambdaSyntax)
1213+
* : case pat guard => rhs
1214+
* then return the function used to parse `body` or the case clause.
11811215
*/
1182-
def isColonLambda =
1183-
sourceVersion.enablesFewerBraces && in.token == COLONfollow && followingIsLambdaAfterColon()
1216+
def detectColonLambda: Option[() => Tree] =
1217+
if sourceVersion.enablesFewerBraces && in.token == COLONfollow
1218+
then followingIsLambdaAfterColon()
1219+
else None
11841220

11851221
/** operand { infixop operand | MatchClause } [postfixop],
11861222
*
@@ -1204,17 +1240,19 @@ object Parsers {
12041240
opStack = OpInfo(top1, op, in.offset) :: opStack
12051241
colonAtEOLOpt()
12061242
newLineOptWhenFollowing(canStartOperand)
1207-
if isColonLambda then
1208-
in.nextToken()
1209-
recur(expr(Location.InColonArg))
1210-
else if maybePostfix && !canStartOperand(in.token) then
1211-
val topInfo = opStack.head
1212-
opStack = opStack.tail
1213-
val od = reduceStack(base, topInfo.operand, 0, true, in.name, isType)
1214-
atSpan(startOffset(od), topInfo.offset) {
1215-
PostfixOp(od, topInfo.operator)
1216-
}
1217-
else recur(operand(location))
1243+
detectColonLambda match
1244+
case Some(parseExpr) =>
1245+
in.nextToken()
1246+
recur(parseExpr())
1247+
case _ =>
1248+
if maybePostfix && !canStartOperand(in.token) then
1249+
val topInfo = opStack.head
1250+
opStack = opStack.tail
1251+
val od = reduceStack(base, topInfo.operand, 0, true, in.name, isType)
1252+
atSpan(startOffset(od), topInfo.offset) {
1253+
PostfixOp(od, topInfo.operator)
1254+
}
1255+
else recur(operand(location))
12181256
else
12191257
val t = reduceStack(base, top, minPrec, leftAssoc = true, in.name, isType)
12201258
if !isType && in.token == MATCH then recurAtMinPrec(matchClause(t))
@@ -2358,6 +2396,7 @@ object Parsers {
23582396

23592397
/** Expr ::= [`implicit'] FunParams (‘=>’ | ‘?=>’) Expr
23602398
* | TypTypeParamClause ‘=>’ Expr
2399+
* | ExprCaseClause -- under experimental.relaxedLambdaSyntax
23612400
* | Expr1
23622401
* FunParams ::= Bindings
23632402
* | id
@@ -2409,6 +2448,8 @@ object Parsers {
24092448
val arrowOffset = accept(ARROW)
24102449
val body = expr(location)
24112450
makePolyFunction(tparams, body, "literal", errorTermTree(arrowOffset), start, arrowOffset)
2451+
case CASE if in.featureEnabled(Feature.relaxedLambdaSyntax) =>
2452+
singleCaseMatch()
24122453
case _ =>
24132454
val saved = placeholderParams
24142455
placeholderParams = Nil
@@ -2472,9 +2513,8 @@ object Parsers {
24722513
if in.token == CATCH then
24732514
val span = in.offset
24742515
in.nextToken()
2475-
(if in.token == CASE then Match(EmptyTree, caseClause(exprOnly = true) :: Nil)
2476-
else subExpr(),
2477-
span)
2516+
(if in.token == CASE then singleCaseMatch() else subExpr(),
2517+
span)
24782518
else (EmptyTree, -1)
24792519

24802520
handler match {
@@ -2769,8 +2809,10 @@ object Parsers {
27692809
* | SimpleExpr (TypeArgs | NamedTypeArgs)
27702810
* | SimpleExpr1 ArgumentExprs
27712811
* | SimpleExpr1 ColonArgument
2772-
* ColonArgument ::= colon [LambdaStart]
2812+
* ColonArgument ::= colon {LambdaStart}
27732813
* indent (CaseClauses | Block) outdent
2814+
* | colon LambdaStart {LambdaStart} expr ENDlambda -- under experimental.relaxedLambdaSyntax
2815+
* | colon ExprCaseClause -- under experimental.relaxedLambdaSyntax
27742816
* LambdaStart ::= FunParams (‘=>’ | ‘?=>’)
27752817
* | TypTypeParamClause ‘=>’
27762818
* ColonArgBody ::= indent (CaseClauses | Block) outdent
@@ -2853,12 +2895,14 @@ object Parsers {
28532895
makeParameter(name.asTermName, typedOpt(), Modifiers(), isBackquoted = isBackquoted(id))
28542896
}
28552897
case _ => t
2856-
else if isColonLambda then
2857-
val app = atSpan(startOffset(t), in.skipToken()) {
2858-
Apply(t, expr(Location.InColonArg) :: Nil)
2859-
}
2860-
simpleExprRest(app, location, canApply = true)
2861-
else t
2898+
else detectColonLambda match
2899+
case Some(parseExpr) =>
2900+
val app =
2901+
atSpan(startOffset(t), in.skipToken()):
2902+
Apply(t, parseExpr() :: Nil)
2903+
simpleExprRest(app, location, canApply = true)
2904+
case None =>
2905+
t
28622906
end simpleExprRest
28632907

28642908
/** SimpleExpr ::= ‘new’ ConstrApp {`with` ConstrApp} [TemplateBody]
@@ -3165,9 +3209,9 @@ object Parsers {
31653209
case ARROW => atSpan(in.skipToken()):
31663210
if exprOnly then
31673211
if in.indentSyntax && in.isAfterLineEnd && in.token != INDENT then
3168-
warning(em"""Misleading indentation: this expression forms part of the preceding catch case.
3212+
warning(em"""Misleading indentation: this expression forms part of the preceding case.
31693213
|If this is intended, it should be indented for clarity.
3170-
|Otherwise, if the handler is intended to be empty, use a multi-line catch with
3214+
|Otherwise, if the handler is intended to be empty, use a multi-line match or catch with
31713215
|an indented case.""")
31723216
expr()
31733217
else block()
@@ -3184,6 +3228,9 @@ object Parsers {
31843228
CaseDef(pat, grd1, body)
31853229
}
31863230

3231+
def singleCaseMatch() =
3232+
Match(EmptyTree, caseClause(exprOnly = true) :: Nil)
3233+
31873234
/** TypeCaseClause ::= ‘case’ (InfixType | ‘_’) ‘=>’ Type [semi]
31883235
*/
31893236
def typeCaseClause(): CaseDef = atSpan(in.offset) {

compiler/src/dotty/tools/dotc/parsing/Scanners.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,9 @@ object Scanners {
617617
&& !statCtdTokens.contains(lastToken)
618618
&& !isTrailingBlankLine
619619

620-
if newlineIsSeparating
620+
if currentRegion.closedBy == ENDlambda then
621+
insert(ENDlambda, lineOffset)
622+
else if newlineIsSeparating
621623
&& canEndStatTokens.contains(lastToken)
622624
&& canStartStatTokens.contains(token)
623625
&& !isLeadingInfixOperator(nextWidth)
@@ -1599,6 +1601,8 @@ object Scanners {
15991601
* InParens a pair of parentheses (...) or brackets [...]
16001602
* InBraces a pair of braces { ... }
16011603
* Indented a pair of <indent> ... <outdent> tokens
1604+
* InCase a case of a match
1605+
* SingleLineLambda the rest of a line following a `:`
16021606
*/
16031607
abstract class Region(val closedBy: Token):
16041608

@@ -1667,6 +1671,7 @@ object Scanners {
16671671
case _: InBraces => "}"
16681672
case _: InCase => "=>"
16691673
case _: Indented => "UNDENT"
1674+
case _: SingleLineLambda => "end of single-line lambda"
16701675

16711676
/** Show open regions as list of lines with decreasing indentations */
16721677
def visualize: String =
@@ -1680,6 +1685,7 @@ object Scanners {
16801685
case class InParens(prefix: Token, outer: Region) extends Region(prefix + 1)
16811686
case class InBraces(outer: Region) extends Region(RBRACE)
16821687
case class InCase(outer: Region) extends Region(OUTDENT)
1688+
case class SingleLineLambda(outer: Region) extends Region(ENDlambda)
16831689

16841690
/** A class describing an indentation region.
16851691
* @param width The principal indentation width

compiler/src/dotty/tools/dotc/parsing/Tokens.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,10 @@ object Tokens extends TokensCommon {
203203
// A `:` recognized as starting an indentation block
204204
inline val SELFARROW = 90; enter(SELFARROW, "=>") // reclassified ARROW following self-type
205205

206+
inline val ENDlambda = 99; enter(ENDlambda, "end of single-line lambda")
207+
206208
/** XML mode */
207-
inline val XMLSTART = 99; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate
209+
inline val XMLSTART = 100; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate
208210

209211
final val alphaKeywords: TokenSet = tokenRange(IF, END)
210212
final val symbolicKeywords: TokenSet = tokenRange(USCORE, CTXARROW)
@@ -267,7 +269,7 @@ object Tokens extends TokensCommon {
267269
final val canStartStatTokens3: TokenSet = canStartExprTokens3 | mustStartStatTokens | BitSet(
268270
AT, CASE, END)
269271

270-
final val canEndStatTokens: TokenSet = atomicExprTokens | BitSet(TYPE, GIVEN, RPAREN, RBRACE, RBRACKET, OUTDENT)
272+
final val canEndStatTokens: TokenSet = atomicExprTokens | BitSet(TYPE, GIVEN, RPAREN, RBRACE, RBRACKET, OUTDENT, ENDlambda)
271273

272274
/** Tokens that stop a lookahead scan search for a `<-`, `then`, or `do`.
273275
* Used for disambiguating between old and new syntax.

docs/_docs/internals/syntax.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ CapFilter ::= ‘.’ ‘as’ ‘[’ QualId ’]’
243243
```ebnf
244244
Expr ::= FunParams (‘=>’ | ‘?=>’) Expr Function(args, expr), Function(ValDef([implicit], id, TypeTree(), EmptyTree), expr)
245245
| TypTypeParamClause ‘=>’ Expr PolyFunction(ts, expr)
246+
| ExprCaseClause
246247
| Expr1
247248
BlockResult ::= FunParams (‘=>’ | ‘?=>’) Block
248249
| TypTypeParamClause ‘=>’ Block
@@ -293,8 +294,11 @@ SimpleExpr ::= SimpleRef
293294
| SimpleExpr ColonArgument -- under language.experimental.fewerBraces
294295
| SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped)
295296
| XmlExpr -- to be dropped
296-
ColonArgument ::= colon [LambdaStart]
297+
ColonArgument ::= colon {LambdaStart}
297298
indent (CaseClauses | Block) outdent
299+
| colon LambdaStart {LambdaStart} expr ENDlambda -- ENDlambda is inserted for each production at next EOL
300+
-- does not apply if enclosed in parens
301+
| colon ExprCaseClause
298302
LambdaStart ::= FunParams (‘=>’ | ‘?=>’)
299303
| TypTypeParamClause ‘=>’
300304
Quoted ::= ‘'’ ‘{’ Block ‘}’

0 commit comments

Comments
 (0)