Skip to content

Commit 767eb9e

Browse files
committed
Better error recovery in comma-separated lists
Check for trailing commas in parser instead of scanner Instead of passing down a flag, add a bit to regions to say if we are in a comma-separated region. delimited instead of expectedToken
1 parent 3ca087c commit 767eb9e

File tree

9 files changed

+269
-59
lines changed

9 files changed

+269
-59
lines changed

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

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,13 @@ object Parsers {
249249

250250
/** Skip on error to next safe point.
251251
*/
252-
protected def skip(stopAtComma: Boolean): Unit =
252+
protected def skip(): Unit =
253253
val lastRegion = in.currentRegion
254+
val stopAtComma = lastRegion match {
255+
case InParens(_, _, commaSeparated) => commaSeparated
256+
case InBraces(_, commaSeparated) => commaSeparated
257+
case _ => true
258+
}
254259
def atStop =
255260
in.token == EOF
256261
|| ((stopAtComma && in.token == COMMA) || skipStopTokens.contains(in.token)) && (in.currentRegion eq lastRegion)
@@ -278,13 +283,13 @@ object Parsers {
278283
if (in.token == EOF) incompleteInputError(msg)
279284
else
280285
syntaxError(msg, offset)
281-
skip(stopAtComma = true)
286+
skip()
282287

283288
def syntaxErrorOrIncomplete(msg: Message, span: Span): Unit =
284289
if (in.token == EOF) incompleteInputError(msg)
285290
else
286291
syntaxError(msg, span)
287-
skip(stopAtComma = true)
292+
skip()
288293

289294
/** Consume one token of the specified type, or
290295
* signal an error if it is not there.
@@ -352,7 +357,7 @@ object Parsers {
352357
false // it's a statement that might be legal in an outer context
353358
else
354359
in.nextToken() // needed to ensure progress; otherwise we might cycle forever
355-
skip(stopAtComma=false)
360+
skip()
356361
true
357362

358363
in.observeOutdented()
@@ -559,19 +564,55 @@ object Parsers {
559564
def inDefScopeBraces[T](body: => T, rewriteWithColon: Boolean = false): T =
560565
inBracesOrIndented(body, rewriteWithColon)
561566

562-
/** part { `separator` part }
563-
*/
564-
def tokenSeparated[T](separator: Int, part: () => T): List[T] = {
565-
val ts = new ListBuffer[T] += part()
566-
while (in.token == separator) {
567+
/** part { `,` part }
568+
* @param delimited If true, this comma-separated list must surrounded by brackets, parens, or braces.
569+
*/
570+
def commaSeparated[T](part: () => T, delimited: Boolean = true, readFirst: Boolean = true): List[T] = {
571+
val expectedEnd = if (delimited) {
572+
in.currentRegion match {
573+
case InParens(t, outer, _) =>
574+
in.currentRegion = InParens(t, outer, commaSeparated = true)
575+
t match {
576+
case LPAREN => RPAREN
577+
case LBRACKET => RBRACKET
578+
case _ => EMPTY
579+
}
580+
case InBraces(outer, _) =>
581+
in.currentRegion = InBraces(outer, commaSeparated = true)
582+
RBRACE
583+
case _ => EMPTY
584+
}
585+
} else EMPTY
586+
val ts = new ListBuffer[T]
587+
if (readFirst) ts += part()
588+
var done = false
589+
while (in.token == COMMA && !done) {
567590
in.nextToken()
568-
ts += part()
591+
if (in.isAfterLineEnd && (in.token == OUTDENT || (expectedEnd != EMPTY && in.token == expectedEnd))) {
592+
// skip the trailing comma
593+
done = true
594+
} else {
595+
ts += part()
596+
}
597+
}
598+
if (expectedEnd != EMPTY && in.token != expectedEnd) {
599+
// As a side effect, will skip to the nearest safe point, which might be a comma
600+
syntaxErrorOrIncomplete(ExpectedTokenButFound(expectedEnd, in.token))
601+
if (in.token == COMMA) {
602+
ts ++= commaSeparated(part, delimited)
603+
}
604+
}
605+
if (delimited) {
606+
in.currentRegion match {
607+
case InParens(t, outer, true) =>
608+
in.currentRegion = InParens(t, outer, commaSeparated = false)
609+
case InBraces(outer, true) => in.currentRegion = InBraces(outer, commaSeparated = false)
610+
case _ =>
611+
}
569612
}
570613
ts.toList
571614
}
572615

573-
def commaSeparated[T](part: () => T): List[T] = tokenSeparated(COMMA, part)
574-
575616
def inSepRegion[T](f: Region => Region)(op: => T): T =
576617
val cur = in.currentRegion
577618
in.currentRegion = f(cur)
@@ -1387,14 +1428,7 @@ object Parsers {
13871428
else
13881429
Function(params, t)
13891430
}
1390-
def funTypeArgsRest(first: Tree, following: () => Tree) = {
1391-
val buf = new ListBuffer[Tree] += first
1392-
while (in.token == COMMA) {
1393-
in.nextToken()
1394-
buf += following()
1395-
}
1396-
buf.toList
1397-
}
1431+
13981432
var isValParamList = false
13991433

14001434
val t =
@@ -1410,11 +1444,10 @@ object Parsers {
14101444
val ts = funArgType() match {
14111445
case Ident(name) if name != tpnme.WILDCARD && in.isColon() =>
14121446
isValParamList = true
1413-
funTypeArgsRest(
1414-
typedFunParam(paramStart, name.toTermName, imods),
1415-
() => typedFunParam(in.offset, ident(), imods))
1447+
typedFunParam(paramStart, name.toTermName, imods) :: commaSeparated(
1448+
() => typedFunParam(in.offset, ident(), imods), readFirst = false)
14161449
case t =>
1417-
funTypeArgsRest(t, funArgType)
1450+
t :: commaSeparated(funArgType,readFirst = false)
14181451
}
14191452
accept(RPAREN)
14201453
if isValParamList || in.isArrow then
@@ -2539,7 +2572,7 @@ object Parsers {
25392572
if (leading == LBRACE || in.token == CASE)
25402573
enumerators()
25412574
else {
2542-
val pats = patternsOpt()
2575+
val pats = patternsOpt(delimited=false)
25432576
val pat =
25442577
if (in.token == RPAREN || pats.length > 1) {
25452578
wrappedEnums = false
@@ -2731,7 +2764,7 @@ object Parsers {
27312764
case USCORE =>
27322765
wildcardIdent()
27332766
case LPAREN =>
2734-
atSpan(in.offset) { makeTupleOrParens(inParens(patternsOpt())) }
2767+
atSpan(in.offset) { makeTupleOrParens(inParens(patternsOpt(delimited=true))) }
27352768
case QUOTE =>
27362769
simpleExpr(Location.InPattern)
27372770
case XMLSTART =>
@@ -2767,11 +2800,11 @@ object Parsers {
27672800

27682801
/** Patterns ::= Pattern [`,' Pattern]
27692802
*/
2770-
def patterns(location: Location = Location.InPattern): List[Tree] =
2771-
commaSeparated(() => pattern(location))
2803+
def patterns(delimited: Boolean=false, location: Location = Location.InPattern): List[Tree] =
2804+
commaSeparated(() => pattern(location), delimited)
27722805

2773-
def patternsOpt(location: Location = Location.InPattern): List[Tree] =
2774-
if (in.token == RPAREN) Nil else patterns(location)
2806+
def patternsOpt(delimited: Boolean, location: Location = Location.InPattern): List[Tree] =
2807+
if (in.token == RPAREN) Nil else patterns(delimited, location)
27752808

27762809
/** ArgumentPatterns ::= ‘(’ [Patterns] ‘)’
27772810
* | ‘(’ [Patterns ‘,’] PatVar ‘*’ ‘)’
@@ -3120,7 +3153,7 @@ object Parsers {
31203153
*/
31213154
def importClause(leading: Token, mkTree: ImportConstr): List[Tree] = {
31223155
val offset = accept(leading)
3123-
commaSeparated(importExpr(mkTree)) match {
3156+
commaSeparated(importExpr(mkTree), delimited=false) match {
31243157
case t :: rest =>
31253158
// The first import should start at the start offset of the keyword.
31263159
val firstPos =
@@ -3197,9 +3230,9 @@ object Parsers {
31973230
}
31983231
else ImportSelector(from)
31993232

3200-
def importSelectors(idOK: Boolean): List[ImportSelector] =
3233+
def importSelector(idOK: Boolean)(): ImportSelector =
32013234
val isWildcard = in.token == USCORE || in.token == GIVEN || isIdent(nme.raw.STAR)
3202-
val selector = atSpan(in.offset) {
3235+
atSpan(in.offset) {
32033236
in.token match
32043237
case USCORE => wildcardSelector()
32053238
case GIVEN => givenSelector()
@@ -3209,13 +3242,6 @@ object Parsers {
32093242
if !idOK then syntaxError(i"named imports cannot follow wildcard imports")
32103243
namedSelector(termIdent())
32113244
}
3212-
val rest =
3213-
if in.token == COMMA then
3214-
in.nextToken()
3215-
importSelectors(idOK = idOK && !isWildcard)
3216-
else
3217-
Nil
3218-
selector :: rest
32193245

32203246
def importSelection(qual: Tree): Tree =
32213247
if in.isIdent(nme.as) && qual.isInstanceOf[RefTree] then
@@ -3233,7 +3259,7 @@ object Parsers {
32333259
case GIVEN =>
32343260
mkTree(qual, givenSelector() :: Nil)
32353261
case LBRACE =>
3236-
mkTree(qual, inBraces(importSelectors(idOK = true)))
3262+
mkTree(qual, inBraces(commaSeparated(importSelector(idOK = true))))
32373263
case _ =>
32383264
if isIdent(nme.raw.STAR) then
32393265
mkTree(qual, wildcardSelector() :: Nil)
@@ -3290,7 +3316,7 @@ object Parsers {
32903316
var lhs = first match {
32913317
case id: Ident if in.token == COMMA =>
32923318
in.nextToken()
3293-
id :: commaSeparated(() => termIdent())
3319+
id :: commaSeparated(() => termIdent(), delimited=false)
32943320
case _ =>
32953321
first :: Nil
32963322
}
@@ -3561,7 +3587,7 @@ object Parsers {
35613587
val id = termIdent()
35623588
if (in.token == COMMA) {
35633589
in.nextToken()
3564-
val ids = commaSeparated(() => termIdent())
3590+
val ids = commaSeparated(() => termIdent(), delimited=false)
35653591
PatDef(mods1, id :: ids, TypeTree(), EmptyTree)
35663592
}
35673593
else {
@@ -3765,7 +3791,7 @@ object Parsers {
37653791
val derived =
37663792
if (isIdent(nme.derives)) {
37673793
in.nextToken()
3768-
tokenSeparated(COMMA, () => convertToTypeId(qualId()))
3794+
commaSeparated(() => convertToTypeId(qualId()), delimited=false)
37693795
}
37703796
else Nil
37713797
possibleTemplateStart()

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

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ object Scanners {
281281
/** Are we in a `${ }` block? such that RBRACE exits back into multiline string. */
282282
private def inMultiLineInterpolatedExpression =
283283
currentRegion match {
284-
case InBraces(InString(true, _)) => true
284+
case InBraces(InString(true, _), _) => true
285285
case _ => false
286286
}
287287

@@ -315,7 +315,7 @@ object Scanners {
315315
dropBraces()
316316
case RPAREN | RBRACKET =>
317317
currentRegion match {
318-
case InParens(prefix, outer) if prefix + 1 == lastToken => currentRegion = outer
318+
case InParens(prefix, outer, _) if prefix + 1 == lastToken => currentRegion = outer
319319
case _ =>
320320
}
321321
case STRINGLIT =>
@@ -655,13 +655,6 @@ object Scanners {
655655
insert(OUTDENT, offset)
656656
currentRegion = r.outer
657657
case _ =>
658-
lookAhead()
659-
if isAfterLineEnd
660-
&& (token == RPAREN || token == RBRACKET || token == RBRACE || token == OUTDENT)
661-
then
662-
() /* skip the trailing comma */
663-
else
664-
reset()
665658
case END =>
666659
if !isEndMarker then token = IDENTIFIER
667660
case COLON =>
@@ -1451,7 +1444,7 @@ object Scanners {
14511444
def proposeKnownWidth(width: IndentWidth, lastToken: Token) =
14521445
if knownWidth == null then
14531446
this match
1454-
case InParens(_, _) if lastToken != LPAREN =>
1447+
case InParens(_, _, _) if lastToken != LPAREN =>
14551448
useOuterWidth()
14561449
case _ =>
14571450
knownWidth = width
@@ -1462,8 +1455,8 @@ object Scanners {
14621455

14631456
private def delimiter = this match
14641457
case _: InString => "}(in string)"
1465-
case InParens(LPAREN, _) => ")"
1466-
case InParens(LBRACKET, _) => "]"
1458+
case InParens(LPAREN, _, _) => ")"
1459+
case InParens(LBRACKET, _, _) => "]"
14671460
case _: InBraces => "}"
14681461
case _: InCase => "=>"
14691462
case _: Indented => "UNDENT"
@@ -1478,8 +1471,8 @@ object Scanners {
14781471
end Region
14791472

14801473
case class InString(multiLine: Boolean, outer: Region) extends Region
1481-
case class InParens(prefix: Token, outer: Region) extends Region
1482-
case class InBraces(outer: Region) extends Region
1474+
case class InParens(prefix: Token, outer: Region, commaSeparated: Boolean = false) extends Region
1475+
case class InBraces(outer: Region, commaSeparated: Boolean = false) extends Region
14831476
case class InCase(outer: Region) extends Region
14841477

14851478
/** A class describing an indentation region.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:3:21 ----------------------------------------------------
2+
3 | def foo(x: Int = 5 6, y Int = 7, z: Int 5, x = 5): Unit = () // error // error // error // error
3+
| ^
4+
| ')' expected, but integer literal found
5+
-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:3:26 ----------------------------------------------------
6+
3 | def foo(x: Int = 5 6, y Int = 7, z: Int 5, x = 5): Unit = () // error // error // error // error
7+
| ^^^
8+
| ':' expected, but identifier found
9+
-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:3:42 ----------------------------------------------------
10+
3 | def foo(x: Int = 5 6, y Int = 7, z: Int 5, x = 5): Unit = () // error // error // error // error
11+
| ^
12+
| ')' expected, but integer literal found
13+
-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:3:47 ----------------------------------------------------
14+
3 | def foo(x: Int = 5 6, y Int = 7, z: Int 5, x = 5): Unit = () // error // error // error // error
15+
| ^
16+
| ':' expected, but '=' found
17+
-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:11:16 ---------------------------------------------------
18+
11 | case Plus(4 1) => // error
19+
| ^
20+
| ')' expected, but integer literal found
21+
-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:12:16 ---------------------------------------------------
22+
12 | case Plus(4 5 6 7, 1, 2 3) => // error // error
23+
| ^
24+
| ')' expected, but integer literal found
25+
-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:12:28 ---------------------------------------------------
26+
12 | case Plus(4 5 6 7, 1, 2 3) => // error // error
27+
| ^
28+
| ')' expected, but integer literal found
29+
-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:14:12 ---------------------------------------------------
30+
14 | val x: A[T=Int, T=Int] = ??? // error // error
31+
| ^
32+
| ']' expected, but '=' found
33+
-- [E040] Syntax Error: tests/neg/comma-separated-errors.scala:14:19 ---------------------------------------------------
34+
14 | val x: A[T=Int, T=Int] = ??? // error // error
35+
| ^
36+
| ']' expected, but '=' found
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class A[T]
2+
object o {
3+
def foo(x: Int = 5 6, y Int = 7, z: Int 5, x = 5): Unit = () // error // error // error // error
4+
5+
case class Plus(a: Int, b: Int)
6+
7+
object Plus {
8+
def unapply(r: Int): Plus = Plus(r - 1, 1)
9+
}
10+
5 match {
11+
case Plus(4 1) => // error
12+
case Plus(4 5 6 7, 1, 2 3) => // error // error
13+
}
14+
val x: A[T=Int, T=Int] = ??? // error // error
15+
}

tests/neg/i1679.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
class A[T]
22
object o {
33
// Testing compiler crash, this test should be modified when named type argument are completely implemented
4-
val x: A[T=Int, T=Int] = ??? // error: ']' expected, but '=' found // error
4+
val x: A[T=Int, T=Int] = ??? // error: ']' expected, but '=' found // error: ']' expected, but '=' found
55
}

tests/neg/t11900.check

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- Error: tests/neg/t11900.scala:44:16 ---------------------------------------------------------------------------------
2+
44 | a => a + 1, // error: weird comma
3+
| ^
4+
| end of statement expected but ',' found
5+
-- Error: tests/neg/t11900.scala:48:16 ---------------------------------------------------------------------------------
6+
48 | println("a"), // error: weird comma
7+
| ^
8+
| end of statement expected but ',' found
9+
-- Error: tests/neg/t11900.scala:52:16 ---------------------------------------------------------------------------------
10+
52 | println("b"), // error: weird comma
11+
| ^
12+
| end of statement expected but ',' found
13+
-- [E032] Syntax Error: tests/neg/t11900.scala:64:8 --------------------------------------------------------------------
14+
64 | _*, // error
15+
| ^
16+
| pattern expected
17+
|
18+
| longer explanation available when compiling with `-explain`

0 commit comments

Comments
 (0)