Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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)

AltStyle によって変換されたページ (->オリジナル) /