Skip to content

Commit 4180269

Browse files
committed
Improve syntax error recovery
- Drop Indented region only after seeing an OUTDENT node. This aligns region stack popping with the other closing tokens `]`, `)`, and `}` and thereby fixes a problem in `skip`. - When skipping, allow OUTDENT insertions even in n on-indent regions if the outdent matches a previous indent. This improves recovert when closing `]`, `)`, or `}` are missing. - When skipping an OUTDENT that matches a previous indent can discard nested regions. Fixes #14507
1 parent ab03bd0 commit 4180269

File tree

9 files changed

+137
-66
lines changed

9 files changed

+137
-66
lines changed

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -252,16 +252,9 @@ object Parsers {
252252

253253
/** Skip on error to next safe point.
254254
*/
255-
protected def skip(): Unit =
256-
val lastRegion = in.currentRegion
257-
in.skipping = true
258-
def atStop =
259-
in.token == EOF
260-
|| skipStopTokens.contains(in.token) && (in.currentRegion eq lastRegion)
261-
while !atStop do
262-
in.nextToken()
255+
def skip(): Unit =
256+
in.skip()
263257
lastErrorOffset = in.offset
264-
in.skipping = false
265258

266259
def warning(msg: Message, sourcePos: SourcePosition): Unit =
267260
report.warning(msg, sourcePos)

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

Lines changed: 104 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,6 @@ object Scanners {
170170
/** A switch whether operators at the start of lines can be infix operators */
171171
private[Scanners] var allowLeadingInfixOperators = true
172172

173-
var skipping = false
174-
175173
var debugTokenStream = false
176174
val showLookAheadOnDebug = false
177175

@@ -274,7 +272,35 @@ object Scanners {
274272
private val prev = newTokenData
275273

276274
/** The current region. This is initially an Indented region with zero indentation width. */
277-
var currentRegion: Region = Indented(IndentWidth.Zero, Set(), EMPTY, null)
275+
var currentRegion: Region = Indented(IndentWidth.Zero, EMPTY, null)
276+
277+
// Error recovery ------------------------------------------------------------
278+
279+
private def lastKnownIndentWidth: IndentWidth =
280+
def recur(r: Region): IndentWidth =
281+
if r.knownWidth == null then recur(r.enclosing) else r.knownWidth
282+
recur(currentRegion)
283+
284+
private var skipping = false
285+
286+
/** Skip on error to next safe point.
287+
*/
288+
def skip(): Unit =
289+
val lastRegion = currentRegion
290+
skipping = true
291+
def atStop =
292+
token == EOF
293+
|| (currentRegion eq lastRegion)
294+
&& (skipStopTokens.contains(token)
295+
|| token == OUTDENT && indentWidth(offset) < lastKnownIndentWidth)
296+
// stop at OUTDENT if the new indentwidth is smaller than the indent width of
297+
// currentRegion. This corrects for the problem that sometimes we don't see an INDENT
298+
// when skipping and therefore might erroneously end up syncing on a nested OUTDENT.
299+
// println(s"\nSTART SKIP AT ${sourcePos().line + 1}, $this in $currentRegion")
300+
while !atStop do
301+
nextToken()
302+
// println(s"\nSTOP SKIP AT ${sourcePos().line + 1}, $this in $currentRegion")
303+
skipping = false
278304

279305
// Get next token ------------------------------------------------------------
280306

@@ -305,27 +331,34 @@ object Scanners {
305331
nextToken()
306332
result
307333

334+
private inline def dropUntil(inline matches: Region => Boolean): Unit =
335+
while
336+
!currentRegion.isOutermost
337+
&& {
338+
val isLast = matches(currentRegion)
339+
currentRegion = currentRegion.enclosing
340+
!isLast
341+
}
342+
do ()
343+
308344
def adjustSepRegions(lastToken: Token): Unit = (lastToken: @switch) match {
309345
case LPAREN | LBRACKET =>
310346
currentRegion = InParens(lastToken, currentRegion)
311347
case LBRACE =>
312348
currentRegion = InBraces(currentRegion)
313349
case RBRACE =>
314-
def dropBraces(): Unit = currentRegion match {
315-
case r: InBraces =>
316-
currentRegion = r.enclosing
317-
case _ =>
318-
if (!currentRegion.isOutermost) {
319-
currentRegion = currentRegion.enclosing
320-
dropBraces()
321-
}
322-
}
323-
dropBraces()
350+
dropUntil(_.isInstanceOf[InBraces])
324351
case RPAREN | RBRACKET =>
325352
currentRegion match {
326353
case InParens(prefix, outer) if prefix + 1 == lastToken => currentRegion = outer
327354
case _ =>
328355
}
356+
case OUTDENT =>
357+
currentRegion match
358+
case r: Indented => currentRegion = r.enclosing
359+
case r =>
360+
if skipping && r.enclosing.isClosedByUndentAt(indentWidth(offset)) then
361+
dropUntil(_.isInstanceOf[Indented])
329362
case STRINGLIT =>
330363
currentRegion match {
331364
case InString(_, outer) => currentRegion = outer
@@ -413,8 +446,8 @@ object Scanners {
413446
|| {
414447
r.outer match
415448
case null => true
416-
case Indented(outerWidth, others, _, _) =>
417-
outerWidth < nextWidth && !others.contains(nextWidth)
449+
case ro @ Indented(outerWidth, _, _) =>
450+
outerWidth < nextWidth && !ro.otherIndentWidths.contains(nextWidth)
418451
case outer =>
419452
outer.indentWidth < nextWidth
420453
}
@@ -520,6 +553,15 @@ object Scanners {
520553
var lastWidth = IndentWidth.Zero
521554
var indentPrefix = EMPTY
522555
val nextWidth = indentWidth(offset)
556+
557+
// If nextWidth is an indentation level not yet seen by enclosing indentation
558+
// region, invoke `handler`.
559+
def handleNewIndentWidth(r: Region, handler: Indented => Unit): Unit = r match
560+
case r @ Indented(curWidth, prefix, outer)
561+
if curWidth < nextWidth && !r.otherIndentWidths.contains(nextWidth) && nextWidth != lastWidth =>
562+
handler(r)
563+
case _ =>
564+
523565
currentRegion match
524566
case r: Indented =>
525567
indentIsSignificant = indentSyntax
@@ -548,32 +590,30 @@ object Scanners {
548590
else if !isLeadingInfixOperator(nextWidth) && !statCtdTokens.contains(lastToken) && lastToken != INDENT then
549591
currentRegion match
550592
case r: Indented =>
551-
currentRegion = r.enclosing
552593
insert(OUTDENT, offset)
553-
case r: InBraces if !closingRegionTokens.contains(token) =>
594+
if next.token != COLON then
595+
handleNewIndentWidth(r.enclosing, ir =>
596+
errorButContinue(
597+
i"""The start of this line does not match any of the previous indentation widths.
598+
|Indentation width of current line : $nextWidth
599+
|This falls between previous widths: ${ir.width} and $lastWidth"""))
600+
case r: InBraces if !closingRegionTokens.contains(token) && !skipping =>
554601
report.warning("Line is indented too far to the left, or a `}` is missing", sourcePos())
555-
case _ =>
602+
case r =>
603+
if skipping && r.enclosing.isClosedByUndentAt(nextWidth) then
604+
insert(OUTDENT, offset)
556605

557606
else if lastWidth < nextWidth
558607
|| lastWidth == nextWidth && (lastToken == MATCH || lastToken == CATCH) && token == CASE then
559608
if canStartIndentTokens.contains(lastToken) then
560-
currentRegion = Indented(nextWidth, Set(), lastToken, currentRegion)
609+
currentRegion = Indented(nextWidth, lastToken, currentRegion)
561610
insert(INDENT, offset)
562611
else if lastToken == SELFARROW then
563612
currentRegion.knownWidth = nextWidth
564613
else if (lastWidth != nextWidth)
565614
errorButContinue(spaceTabMismatchMsg(lastWidth, nextWidth))
566-
currentRegion match
567-
case Indented(curWidth, others, prefix, outer)
568-
if curWidth < nextWidth && !others.contains(nextWidth) && nextWidth != lastWidth =>
569-
if token == OUTDENT && next.token != COLON then
570-
errorButContinue(
571-
i"""The start of this line does not match any of the previous indentation widths.
572-
|Indentation width of current line : $nextWidth
573-
|This falls between previous widths: $curWidth and $lastWidth""")
574-
else
575-
currentRegion = Indented(curWidth, others + nextWidth, prefix, outer)
576-
case _ =>
615+
if token != OUTDENT || next.token == COLON then
616+
handleNewIndentWidth(currentRegion, _.otherIndentWidths += nextWidth)
577617
end handleNewLine
578618

579619
def spaceTabMismatchMsg(lastWidth: IndentWidth, nextWidth: IndentWidth) =
@@ -593,7 +633,7 @@ object Scanners {
593633
val nextWidth = indentWidth(next.offset)
594634
val lastWidth = currentRegion.indentWidth
595635
if lastWidth < nextWidth then
596-
currentRegion = Indented(nextWidth, Set(), COLONEOL, currentRegion)
636+
currentRegion = Indented(nextWidth, COLONEOL, currentRegion)
597637
offset = next.offset
598638
token = INDENT
599639
end observeIndented
@@ -608,7 +648,6 @@ object Scanners {
608648
&& !(token == CASE && r.prefix == MATCH)
609649
&& next.token == EMPTY // can be violated for ill-formed programs, e.g. neg/i12605.sala
610650
=>
611-
currentRegion = r.enclosing
612651
insert(OUTDENT, offset)
613652
case _ =>
614653

@@ -623,9 +662,7 @@ object Scanners {
623662
}
624663

625664
def closeIndented() = currentRegion match
626-
case r: Indented if !r.isOutermost =>
627-
insert(OUTDENT, offset)
628-
currentRegion = r.outer
665+
case r: Indented if !r.isOutermost => insert(OUTDENT, offset)
629666
case _ =>
630667

631668
/** - Join CASE + CLASS => CASECLASS, CASE + OBJECT => CASEOBJECT
@@ -656,7 +693,6 @@ object Scanners {
656693
currentRegion match
657694
case r: Indented if isEnclosedInParens(r.outer) =>
658695
insert(OUTDENT, offset)
659-
currentRegion = r.outer
660696
case _ =>
661697
lookAhead()
662698
if isAfterLineEnd
@@ -1518,6 +1554,26 @@ object Scanners {
15181554
if enclosing.knownWidth == null then enclosing.useOuterWidth()
15191555
knownWidth = enclosing.knownWidth
15201556

1557+
/** Does `width` represent an undent of an enclosing indentation region?
1558+
* This is the case if there is an indentation region that goes deeper than `width`
1559+
* and that is enclosed in a region that contains `width` as an indentation width.
1560+
*/
1561+
def isClosedByUndentAt(width: IndentWidth): Boolean = this match
1562+
case _: Indented =>
1563+
!isOutermost && width <= indentWidth && enclosing.coversIndent(width)
1564+
case _ =>
1565+
enclosing.isClosedByUndentAt(width)
1566+
1567+
/** A region "covers" an indentation with `width` if it has `width` as known
1568+
* indentation width (either as primary, or in case of an Indent region as
1569+
* alternate width).
1570+
*/
1571+
protected def coversIndent(w: IndentWidth): Boolean =
1572+
knownWidth != null && w == indentWidth
1573+
1574+
def toList: List[Region] =
1575+
this :: (if outer == null then Nil else outer.toList)
1576+
15211577
private def delimiter = this match
15221578
case _: InString => "}(in string)"
15231579
case InParens(LPAREN, _) => ")"
@@ -1528,11 +1584,10 @@ object Scanners {
15281584

15291585
/** Show open regions as list of lines with decreasing indentations */
15301586
def visualize: String =
1531-
indentWidth.toPrefix
1532-
+ delimiter
1533-
+ outer.match
1534-
case null => ""
1535-
case next: Region => "\n" + next.visualize
1587+
toList.map(r => s"${r.indentWidth.toPrefix}${r.delimiter}").mkString("\n")
1588+
1589+
override def toString: String =
1590+
toList.map(r => s"(${r.indentWidth}, ${r.delimiter})").mkString(" in ")
15361591
end Region
15371592

15381593
case class InString(multiLine: Boolean, outer: Region) extends Region
@@ -1542,13 +1597,18 @@ object Scanners {
15421597

15431598
/** A class describing an indentation region.
15441599
* @param width The principal indendation width
1545-
* @param others Other indendation widths > width of lines in the same region
15461600
* @param prefix The token before the initial <indent> of the region
15471601
*/
1548-
case class Indented(width: IndentWidth, others: Set[IndentWidth], prefix: Token, outer: Region | Null) extends Region:
1602+
case class Indented(width: IndentWidth, prefix: Token, outer: Region | Null) extends Region:
15491603
knownWidth = width
15501604

1551-
def topLevelRegion(width: IndentWidth) = Indented(width, Set(), EMPTY, null)
1605+
/** Other indendation widths > width of lines in the same region */
1606+
var otherIndentWidths: Set[IndentWidth] = Set()
1607+
1608+
override def coversIndent(w: IndentWidth) = width == w || otherIndentWidths.contains(w)
1609+
end Indented
1610+
1611+
def topLevelRegion(width: IndentWidth) = Indented(width, EMPTY, null)
15521612

15531613
enum IndentWidth {
15541614
case Run(ch: Char, n: Int)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ object Tokens extends TokensCommon {
287287

288288
final val endMarkerTokens = identifierTokens | BitSet(IF, WHILE, FOR, MATCH, TRY, NEW, THROW, GIVEN, VAL, THIS)
289289

290-
final val skipStopTokens = BitSet(SEMI, COMMA, NEWLINE, NEWLINES, RBRACE, RPAREN, RBRACKET, OUTDENT)
290+
final val skipStopTokens = BitSet(SEMI, COMMA, NEWLINE, NEWLINES, RBRACE, RPAREN, RBRACKET)
291291

292292
final val softModifierNames = Set(nme.inline, nme.opaque, nme.open, nme.transparent, nme.infix)
293293
}

tests/neg/i12605.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
object Foo:
22
def joe(): List[(Int, Int)] =
33
List((2, 3), (3, 4)).filter case (a, b) => b > a // error // error
4-
// error

tests/neg/i14507.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class C:
2+
def test = g
3+
def f(b: Boolean): Int =
4+
if b then // error
5+
end if
6+
42
7+
def g = 27
8+
end C

tests/neg/indent.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ object Test {
44
val y3 =
55
if (1) max 10 gt 0 // error: end of statement expected but integer literal found // error // error
66
1
7-
else
7+
else // error
88
2
9-
} // error
9+
}

tests/neg/parser-stability-19.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
object x0 {
22
case class x0[](): // error
33
def x0( ) ] // error
4-
def x0 ( x0:x0 ):x0.type = x1 x0 // error // error
4+
def x0 ( x0:x0 ):x0.type = x1 x0
55
// error

tests/neg/syntax-error-recovery.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
object Test {
2+
def foo(x: Int) = {
3+
if (x < 0 then // error
4+
1
5+
else
6+
2
7+
}
8+
println(bar) // error
9+
10+
def foo2(x: Int) =
11+
if (x < 0 then // error
12+
1
13+
else
14+
2
15+
println(baz) // error
16+
17+
}
18+
19+

tests/neg/t6810.check

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,3 @@
2222
30 | val b = '
2323
| ^
2424
| unclosed character literal
25-
-- Warning: tests/neg/t6810.scala:6:0 ----------------------------------------------------------------------------------
26-
6 |' // but not embedded EOL sequences not represented as escapes
27-
|^
28-
|Line is indented too far to the left, or a `}` is missing
29-
-- Warning: tests/neg/t6810.scala:31:0 ---------------------------------------------------------------------------------
30-
31 |' // anypos-error CR seen as EOL by scanner; FSR, error only on open quote, unlike `y`
31-
|^
32-
|Line is indented too far to the left, or a `}` is missing

0 commit comments

Comments
 (0)