Skip to content

Commit 271f18f

Browse files
committed
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.
1 parent 4689288 commit 271f18f

File tree

11 files changed

+204
-86
lines changed

11 files changed

+204
-86
lines changed

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

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,27 +1090,45 @@ 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 :if the input starts with an identifier,
1095+
* a wildcard, or something enclosed in (...) or [...], this is followed by a
1096+
* `=>` or `?=>`, one one of the following two cases applies:
1097+
* 1. The next token is an indent. In this case the return parsing function parses
1098+
* an Expr in location Location.InColonArg.
1099+
* 2. The next token is on the same line and the enclosing region is not `(...)`.
1100+
* In this case the parsing function parses an Expr in location Location.InColonArg
1101+
* enclosed in a SingleLineLambda region, and then eats the ENDlambda token
1102+
* generated by the Scanner at the end of that region.
1103+
* The reason for excluding (2) in regions enclosed in parentheses is to avoid
1104+
* an ambiguity with type ascription `(x: A => B)`, where function types are only
1105+
* allowed inside parentheses.
1106+
*/
1107+
def followingIsLambdaAfterColon(): Option[() => Tree] =
10981108
val lookahead = in.LookaheadScanner(allowIndent = true)
10991109
.tap(_.currentRegion.knownWidth = in.currentRegion.indentWidth)
1100-
def isArrowIndent() =
1101-
lookahead.isArrow
1102-
&& {
1110+
def isArrowIndent(): Option[() => Tree] =
1111+
if lookahead.isArrow then
11031112
lookahead.observeArrowIndented()
1104-
lookahead.token == INDENT || lookahead.token == EOF
1105-
}
1113+
if lookahead.token == INDENT || lookahead.token == EOF then
1114+
Some(() => expr(Location.InColonArg))
1115+
else if !in.currentRegion.isInstanceOf[InParens] then
1116+
Some: () =>
1117+
val t = inSepRegion(SingleLineLambda(_)):
1118+
expr(Location.InColonArg)
1119+
accept(ENDlambda)
1120+
t
1121+
else None
1122+
else None
11061123
lookahead.nextToken()
11071124
if lookahead.isIdent || lookahead.token == USCORE then
11081125
lookahead.nextToken()
11091126
isArrowIndent()
11101127
else if lookahead.token == LPAREN || lookahead.token == LBRACKET then
11111128
lookahead.skipParens()
11121129
isArrowIndent()
1113-
else false
1130+
else
1131+
None
11141132

11151133
/** Can the next lookahead token start an operand as defined by
11161134
* leadingOperandTokens, or is postfix ops enabled?
@@ -1175,12 +1193,17 @@ object Parsers {
11751193
case _ => infixOp
11761194
}
11771195

1178-
/** True if we are seeing a lambda argument after a colon of the form:
1196+
/** Optionally, if we are seeing a lambda argument after a colon of the form
11791197
* : (params) =>
11801198
* body
1199+
* or a single-line lambda
1200+
* : (params) => body
1201+
* then return the function used to parse `body`.
11811202
*/
1182-
def isColonLambda =
1183-
sourceVersion.enablesFewerBraces && in.token == COLONfollow && followingIsLambdaAfterColon()
1203+
def detectColonLambda: Option[() => Tree] =
1204+
if sourceVersion.enablesFewerBraces && in.token == COLONfollow
1205+
then followingIsLambdaAfterColon()
1206+
else None
11841207

11851208
/** operand { infixop operand | MatchClause } [postfixop],
11861209
*
@@ -1204,17 +1227,19 @@ object Parsers {
12041227
opStack = OpInfo(top1, op, in.offset) :: opStack
12051228
colonAtEOLOpt()
12061229
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))
1230+
detectColonLambda match
1231+
case Some(parseExpr) =>
1232+
in.nextToken()
1233+
recur(parseExpr())
1234+
case _ =>
1235+
if maybePostfix && !canStartOperand(in.token) then
1236+
val topInfo = opStack.head
1237+
opStack = opStack.tail
1238+
val od = reduceStack(base, topInfo.operand, 0, true, in.name, isType)
1239+
atSpan(startOffset(od), topInfo.offset) {
1240+
PostfixOp(od, topInfo.operator)
1241+
}
1242+
else recur(operand(location))
12181243
else
12191244
val t = reduceStack(base, top, minPrec, leftAssoc = true, in.name, isType)
12201245
if !isType && in.token == MATCH then recurAtMinPrec(matchClause(t))
@@ -2771,6 +2796,7 @@ object Parsers {
27712796
* | SimpleExpr1 ColonArgument
27722797
* ColonArgument ::= colon [LambdaStart]
27732798
* indent (CaseClauses | Block) outdent
2799+
* | colon LambdaStart expr ENDlambda -- under experimental.relaxedLambdaSyntax
27742800
* LambdaStart ::= FunParams (‘=>’ | ‘?=>’)
27752801
* | TypTypeParamClause ‘=>’
27762802
* ColonArgBody ::= indent (CaseClauses | Block) outdent
@@ -2853,12 +2879,14 @@ object Parsers {
28532879
makeParameter(name.asTermName, typedOpt(), Modifiers(), isBackquoted = isBackquoted(id))
28542880
}
28552881
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
2882+
else detectColonLambda match
2883+
case Some(parseExpr) =>
2884+
val app =
2885+
atSpan(startOffset(t), in.skipToken()):
2886+
Apply(t, parseExpr() :: Nil)
2887+
simpleExprRest(app, location, canApply = true)
2888+
case None =>
2889+
t
28622890
end simpleExprRest
28632891

28642892
/** SimpleExpr ::= ‘new’ ConstrApp {`with` ConstrApp} [TemplateBody]

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.

tests/neg/closure-args.check

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
-- [E040] Syntax Error: tests/neg/closure-args.scala:2:25 --------------------------------------------------------------
2+
2 |val x = List(1).map: (x: => Int) => // error
3+
| ^^
4+
| an identifier expected, but '=>' found
5+
|
6+
| longer explanation available when compiling with `-explain`
7+
-- Error: tests/neg/closure-args.scala:14:0 ----------------------------------------------------------------------------
8+
14 | y => y > 0 // error // error
9+
|^
10+
|indented definitions expected, end of single-line lambda found
11+
-- [E103] Syntax Error: tests/neg/closure-args.scala:14:4 --------------------------------------------------------------
12+
14 | y => y > 0 // error // error
13+
| ^
14+
| Illegal start of toplevel definition
15+
|
16+
| longer explanation available when compiling with `-explain`
17+
-- [E018] Syntax Error: tests/neg/closure-args.scala:18:20 -------------------------------------------------------------
18+
18 |val e = xs.map: y => // error
19+
| ^
20+
| expression expected but end of single-line lambda found
21+
|
22+
| longer explanation available when compiling with `-explain`
23+
-- [E040] Syntax Error: tests/neg/closure-args.scala:21:64 -------------------------------------------------------------
24+
21 |val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error
25+
| ^^^^
26+
| end of single-line lambda expected, but 'case' found
27+
-- [E008] Not Found Error: tests/neg/closure-args.scala:10:4 -----------------------------------------------------------
28+
8 |val b: Int = xs
29+
9 | .map: x => x
30+
10 | * x // error
31+
| ^
32+
| value * is not a member of List[Int].
33+
| Note that `*` is treated as an infix operator in Scala 3.
34+
| If you do not want that, insert a `;` or empty line in front
35+
| or drop any spaces behind the operator.
36+
-- [E006] Not Found Error: tests/neg/closure-args.scala:16:21 ----------------------------------------------------------
37+
16 |val c = List(xs.map: y => y + y) // error // error // error // error
38+
| ^
39+
| Not found: type y
40+
|
41+
| longer explanation available when compiling with `-explain`
42+
-- [E006] Not Found Error: tests/neg/closure-args.scala:16:28 ----------------------------------------------------------
43+
16 |val c = List(xs.map: y => y + y) // error // error // error // error
44+
| ^
45+
| Not found: type +
46+
|
47+
| longer explanation available when compiling with `-explain`
48+
-- [E006] Not Found Error: tests/neg/closure-args.scala:16:26 ----------------------------------------------------------
49+
16 |val c = List(xs.map: y => y + y) // error // error // error // error
50+
| ^
51+
| Not found: type y
52+
|
53+
| longer explanation available when compiling with `-explain`
54+
-- [E006] Not Found Error: tests/neg/closure-args.scala:16:30 ----------------------------------------------------------
55+
16 |val c = List(xs.map: y => y + y) // error // error // error // error
56+
| ^
57+
| Not found: type y
58+
|
59+
| longer explanation available when compiling with `-explain`

tests/neg/closure-args.scala

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
1-
import language.`3.3`
21

32
val x = List(1).map: (x: => Int) => // error
43
???
54
val z = List(1).map: + => // ok
65
???
76

87
val xs = List(1)
9-
val b: Int = xs // error
10-
.map: x => x * x // error
11-
.filter: y => y > 0 // error
12-
(0)
13-
val d = xs // error
8+
val b: Int = xs
9+
.map: x => x
10+
* x // error
11+
12+
val d = xs
1413
.map: x => x.toString + xs.dropWhile:
15-
y => y > 0
14+
y => y > 0 // error // error
1615

1716
val c = List(xs.map: y => y + y) // error // error // error // error
18-
val d2: String = xs // error
19-
.map: x => x.toString + xs.dropWhile: y => y > 0 // error // error
20-
.filter: z => !z.isEmpty // error
21-
(0)
2217

23-
val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error // error
18+
val e = xs.map: y => // error
19+
y + 1
20+
21+
val fs: List[List[Int] => Int] = xs.map: x => case y :: ys => y case Nil => -1 // error

tests/neg/i22193.check

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
-- [E018] Syntax Error: tests/neg/i22193.scala:15:68 -------------------------------------------------------------------
2+
15 | arg2 = "the quick brown fox jumped over the lazy dog"): env => // error
3+
| ^
4+
| expression expected but end of single-line lambda found
5+
|
6+
| longer explanation available when compiling with `-explain`
7+
-- Error: tests/neg/i22193.scala:22:2 ----------------------------------------------------------------------------------
8+
22 | env => // error indented definitions expected, identifier env found
9+
| ^^^
10+
| indented definitions expected, identifier env found
11+
-- Error: tests/neg/i22193.scala:31:2 ----------------------------------------------------------------------------------
12+
31 | val x = "Hello" // error
13+
| ^^^
14+
| indented definitions expected, val found
15+
-- [E006] Not Found Error: tests/neg/i22193.scala:16:10 ----------------------------------------------------------------
16+
16 | val x = env // error
17+
| ^^^
18+
| Not found: env
19+
|
20+
| longer explanation available when compiling with `-explain`
21+
-- [E178] Type Error: tests/neg/i22193.scala:28:2 ----------------------------------------------------------------------
22+
28 | fn3( // error missing argument list for value of type (=> Unit) => Unit
23+
| ^
24+
| missing argument list for value of type (=> Unit) => Unit
25+
29 | arg = "blue sleeps faster than tuesday",
26+
30 | arg2 = "the quick brown fox jumped over the lazy dog"):
27+
|
28+
| longer explanation available when compiling with `-explain`
29+
-- [E006] Not Found Error: tests/neg/i22193.scala:32:10 ----------------------------------------------------------------
30+
32 | println(x) // error
31+
| ^
32+
| Not found: x
33+
|
34+
| longer explanation available when compiling with `-explain`

tests/neg/i22193.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ def test1() =
1010
val x = env
1111
println(x)
1212

13-
fn2( // error not a legal formal parameter for a function literal
13+
fn2(
1414
arg = "blue sleeps faster than tuesday",
15-
arg2 = "the quick brown fox jumped over the lazy dog"): env =>
15+
arg2 = "the quick brown fox jumped over the lazy dog"): env => // error
1616
val x = env // error
1717
println(x)
1818

tests/neg/i22906.check

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Flag -indent set repeatedly
2-
-- Error: tests/neg/i22906.scala:6:15 ----------------------------------------------------------------------------------
3-
6 | {`1`: Int => 5} // error
4-
| ^
5-
| parentheses are required around the parameter of a lambda
6-
| This construct can be rewritten automatically under -rewrite -source 3.0-migration.
2+
-- [E040] Syntax Error: tests/neg/i22906.scala:6:20 --------------------------------------------------------------------
3+
6 | {`1`: Int => 5} // error // error
4+
| ^
5+
| end of single-line lambda expected, but '}' found
6+
-- [E006] Not Found Error: tests/neg/i22906.scala:6:5 ------------------------------------------------------------------
7+
6 | {`1`: Int => 5} // error // error
8+
| ^^^
9+
| Not found: 1
10+
|
11+
| longer explanation available when compiling with `-explain`

tests/neg/i22906.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
// does not reproduce under "vulpix" test rig, which enforces certain flag sets?
44

55
def program: Int => Int =
6-
{`1`: Int => 5} // error
6+
{`1`: Int => 5} // error // error

tests/pos/change-lambda.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def foo(x: Any) = ???
2+
3+
def test(xs: List[Int]) =
4+
xs.map: x => x
5+
foo:
6+
xs.map: x => x + 1
7+

0 commit comments

Comments
 (0)