Skip to content

Commit ef0e7e0

Browse files
committed
Allow indentation inside (...)
Change the rules so that indentation is recognized everywhere, and not just at the toplevel and insider parentheses.
1 parent 9352ede commit ef0e7e0

File tree

7 files changed

+122
-81
lines changed

7 files changed

+122
-81
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ object Parsers {
651651
/** Check that this is not the start of a statement that's indented relative to the current region.
652652
*/
653653
def checkNextNotIndented(): Unit = in.currentRegion match
654-
case r: IndentSignificantRegion if in.isNewLine =>
654+
case r: NewLineSignificantRegion if in.isNewLine =>
655655
val nextIndentWidth = in.indentWidth(in.next.offset)
656656
if r.indentWidth < nextIndentWidth then
657657
warning(i"Line is indented too far to the right, or a `{` or `:` is missing", in.next.offset)

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

Lines changed: 43 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ object Scanners {
289289
/** Are we in a `${ }` block? such that RBRACE exits back into multiline string. */
290290
private def inMultiLineInterpolatedExpression =
291291
currentRegion match {
292-
case InBraces(_, InString(true, _)) => true
292+
case InBraces(InString(true, _)) => true
293293
case _ => false
294294
}
295295

@@ -305,7 +305,7 @@ object Scanners {
305305
case LPAREN | LBRACKET =>
306306
currentRegion = InParens(lastToken, currentRegion)
307307
case LBRACE =>
308-
currentRegion = InBraces(null, currentRegion)
308+
currentRegion = InBraces(currentRegion)
309309
case RBRACE =>
310310
def dropBraces(): Unit = currentRegion match {
311311
case r: InBraces =>
@@ -360,6 +360,7 @@ object Scanners {
360360

361361
/** Insert `token` at assumed `offset` in front of current one. */
362362
def insert(token: Token, offset: Int) = {
363+
assert(next.token == EMPTY, next)
363364
next.copyFrom(this)
364365
this.offset = offset
365366
this.token = token
@@ -518,26 +519,24 @@ object Scanners {
518519
* I.e. `a <= b` iff `b.startsWith(a)`. If indentation is significant it is considered an error
519520
* if the current indentation width and the indentation of the current token are incomparable.
520521
*/
521-
def handleNewLine(lastToken: Token) = {
522+
def handleNewLine(lastToken: Token) =
522523
var indentIsSignificant = false
523524
var newlineIsSeparating = false
524525
var lastWidth = IndentWidth.Zero
525526
var indentPrefix = EMPTY
526527
val nextWidth = indentWidth(offset)
527-
currentRegion match {
528+
currentRegion match
528529
case r: Indented =>
529530
indentIsSignificant = indentSyntax
530531
lastWidth = r.width
531532
newlineIsSeparating = lastWidth <= nextWidth || r.isOutermost
532533
indentPrefix = r.prefix
533-
case r: InBraces =>
534+
case r =>
534535
indentIsSignificant = indentSyntax
535-
if (r.width == null) r.width = nextWidth
536-
lastWidth = r.width
537-
newlineIsSeparating = true
538-
indentPrefix = LBRACE
539-
case _ =>
540-
}
536+
if (r.knownWidth == null) r.knownWidth = nextWidth
537+
lastWidth = r.knownWidth
538+
newlineIsSeparating = r.isInstanceOf[InBraces]
539+
541540
if newlineIsSeparating
542541
&& canEndStatTokens.contains(lastToken)
543542
&& canStartStatTokens.contains(token)
@@ -580,7 +579,7 @@ object Scanners {
580579
currentRegion = Indented(curWidth, others + nextWidth, prefix, outer)
581580
case _ =>
582581
}
583-
}
582+
end handleNewLine
584583

585584
def spaceTabMismatchMsg(lastWidth: IndentWidth, nextWidth: IndentWidth) =
586585
i"""Incompatible combinations of tabs and spaces in indentation prefixes.
@@ -597,10 +596,7 @@ object Scanners {
597596
def observeIndented(): Unit =
598597
if indentSyntax && isNewLine then
599598
val nextWidth = indentWidth(next.offset)
600-
val lastWidth = currentRegion match
601-
case r: IndentSignificantRegion => r.indentWidth
602-
case _ => nextWidth
603-
599+
val lastWidth = currentRegion.indentWidth
604600
if lastWidth < nextWidth then
605601
currentRegion = Indented(nextWidth, Set(), COLONEOL, currentRegion)
606602
offset = next.offset
@@ -650,15 +646,27 @@ object Scanners {
650646
lookahead()
651647
if (token != ELSE) reset()
652648
case COMMA =>
653-
lookahead()
654-
if (isAfterLineEnd && (token == RPAREN || token == RBRACKET || token == RBRACE || token == OUTDENT)) {
655-
/* skip the trailing comma */
656-
} else if (token == EOF) { // e.g. when the REPL is parsing "val List(x, y, _*,"
657-
/* skip the trailing comma */
658-
} else reset()
649+
def isEnclosedInParens(r: Region): Boolean = r match
650+
case r: Indented => isEnclosedInParens(r.outer)
651+
case _: InParens => true
652+
case _ => false
653+
currentRegion match
654+
case r: Indented if isEnclosedInParens(r.outer) =>
655+
insert(OUTDENT, offset)
656+
currentRegion = r.outer
657+
case _ =>
658+
lookahead()
659+
if isAfterLineEnd
660+
&& (token == RPAREN || token == RBRACKET || token == RBRACE || token == OUTDENT)
661+
then
662+
() /* skip the trailing comma */
663+
else if token == EOF then // e.g. when the REPL is parsing "val List(x, y, _*,"
664+
() /* skip the trailing comma */
665+
else
666+
reset()
659667
case COLON =>
660668
if colonSyntax then observeColonEOL()
661-
case EOF | RBRACE =>
669+
case EOF | RBRACE | RPAREN | RBRACKET =>
662670
currentRegion match {
663671
case r: Indented if !r.isOutermost =>
664672
insert(OUTDENT, offset)
@@ -1374,7 +1382,7 @@ object Scanners {
13741382
* InBraces a pair of braces { ... }
13751383
* Indented a pair of <indent> ... <outdent> tokens
13761384
*/
1377-
abstract class Region {
1385+
abstract class Region:
13781386
/** The region enclosing this one, or `null` for the outermost region */
13791387
def outer: Region | Null
13801388

@@ -1384,32 +1392,27 @@ object Scanners {
13841392
/** The enclosing region, which is required to exist */
13851393
def enclosing: Region = outer.asInstanceOf[Region]
13861394

1387-
/** If this is an InBraces or Indented region, its indentation width, or Zero otherwise */
1388-
def indentWidth: IndentWidth = IndentWidth.Zero
1389-
}
1395+
var knownWidth: IndentWidth | Null = null
13901396

1391-
case class InString(multiLine: Boolean, outer: Region) extends Region
1392-
case class InParens(prefix: Token, outer: Region) extends Region
1397+
/** The indentation width, Zero if not known */
1398+
final def indentWidth: IndentWidth =
1399+
if knownWidth == null then IndentWidth.Zero else knownWidth
1400+
end Region
13931401

1394-
abstract class IndentSignificantRegion extends Region
1402+
trait NewLineSignificantRegion extends Region
13951403

1396-
case class InBraces(var width: IndentWidth | Null, outer: Region)
1397-
extends IndentSignificantRegion {
1398-
// The indent width starts out as `null` when the opening brace is encountered
1399-
// It is then adjusted when the next token on a new line is encountered.
1400-
override def indentWidth: IndentWidth =
1401-
if width == null then IndentWidth.Zero else width
1402-
}
1404+
case class InString(multiLine: Boolean, outer: Region) extends Region
1405+
case class InParens(prefix: Token, outer: Region) extends Region
1406+
case class InBraces(outer: Region) extends NewLineSignificantRegion
14031407

14041408
/** A class describing an indentation region.
14051409
* @param width The principal indendation width
14061410
* @param others Other indendation widths > width of lines in the same region
14071411
* @param prefix The token before the initial <indent> of the region
14081412
*/
14091413
case class Indented(width: IndentWidth, others: Set[IndentWidth], prefix: Token, outer: Region | Null)
1410-
extends IndentSignificantRegion {
1411-
override def indentWidth = width
1412-
}
1414+
extends NewLineSignificantRegion:
1415+
knownWidth = width
14131416

14141417
enum IndentWidth {
14151418
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
@@ -269,7 +269,7 @@ object Tokens extends TokensCommon {
269269

270270
final val statCtdTokens: BitSet = BitSet(THEN, ELSE, DO, CATCH, FINALLY, YIELD, MATCH)
271271

272-
final val closingRegionTokens = BitSet(RBRACE, CASE) | statCtdTokens
272+
final val closingRegionTokens = BitSet(RBRACE, RPAREN, RBRACKET, CASE) | statCtdTokens
273273

274274
final val canStartIndentTokens: BitSet =
275275
statCtdTokens | BitSet(COLONEOL, EQUALS, ARROW, LARROW, WHILE, TRY, FOR, IF)

docs/docs/reference/other-new-features/indentation.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ title: Optional Braces
44
---
55

66
As an experimental feature, Scala 3 enforces some rules on indentation and allows
7-
some occurrences of braces `{...}` to be optional.
7+
some occurrences of braces `{...}` to be optional.
88
It can be turned off with the compiler flag `-noindent`.
99

1010
- First, some badly indented programs are flagged with warnings.
@@ -78,10 +78,11 @@ There are two rules:
7878
If the indentation width of the token on the next line is still less than the new current indentation width, step (2) repeats. Therefore, several `<outdent>` tokens
7979
may be inserted in a row.
8080

81-
An `<outdent>` is also inserted if the next statement following a statement sequence starting with an `<indent>` closes an indentation region, i.e. is one of `then`, `else`, `do`, `catch`, `finally`, `yield`, `}` or `case`.
81+
An `<outdent>` is also inserted if the next token following a statement sequence starting with an `<indent>` closes an indentation region, i.e. is one of `then`, `else`, `do`, `catch`, `finally`, `yield`, `}`, `)`, `]` or `case`.
8282

83-
It is an error if the indentation width of the token following an `<outdent>` does not
84-
match the indentation of some previous line in the enclosing indentation region. For instance, the following would be rejected.
83+
An `<outdent>` is finally inserted in front of a comma that follows a statement sequence starting with an `<indent>` if the indented region is itself enclosed in parentheses
84+
85+
It is an error if the indentation width of the token following an `<outdent>` does not match the indentation of some previous line in the enclosing indentation region. For instance, the following would be rejected.
8586
```scala
8687
if x < 0
8788
-x

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 // error // error // error
55
// error

tests/pos/indent-in-parens.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
def f(x: Int) =
2+
assert(
3+
if x > 0 then
4+
true
5+
else
6+
false
7+
)
8+
assert(
9+
if x > 0 then
10+
true
11+
else
12+
false)
13+
assert(
14+
if x > 0 then
15+
true
16+
else
17+
false, "fail")
18+
assert(
19+
if x > 0 then
20+
true
21+
else
22+
if x < 0 then
23+
true
24+
else
25+
false, "fail")
26+
(
27+
if x > 0 then
28+
println(x)
29+
x
30+
else
31+
s"""foo${
32+
if x > 0 then
33+
println(x)
34+
x
35+
else
36+
-x
37+
}"""
38+
)

tests/run/LazyValsLongs.scala

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,41 +23,40 @@ object Test {
2323
def main(args: Array[String]): Unit = {
2424
val c = new I
2525
import c._
26-
val l1 = List(A1,
27-
A2,
28-
A3,
29-
A4,
30-
A5,
31-
A6,
32-
A7,
33-
A8,
34-
A9,
35-
A10,
36-
A11,
37-
A12,
38-
A13,
39-
A14,
40-
A15,
41-
A16,
42-
A17,
26+
val l1 = List(A1,
27+
A2,
28+
A3,
29+
A4,
30+
A5, A6,
31+
A7,
32+
A8,
33+
A9,
34+
A10,
35+
A11,
36+
A12,
37+
A13,
38+
A14,
39+
A15,
40+
A16,
41+
A17,
4342
A18)
44-
val l2 = List(A1,
45-
A2,
46-
A3,
47-
A4,
48-
A5,
49-
A6,
50-
A7,
51-
A8,
52-
A9,
53-
A10,
54-
A11,
55-
A12,
56-
A13,
57-
A14,
58-
A15,
59-
A16,
60-
A17,
43+
val l2 = List(A1,
44+
A2,
45+
A3,
46+
A4,
47+
A5,
48+
A6,
49+
A7,
50+
A8,
51+
A9,
52+
A10,
53+
A11,
54+
A12,
55+
A13,
56+
A14,
57+
A15,
58+
A16,
59+
A17,
6160
A18)
6261
assert(l1.mkString == l2.mkString)
6362
assert(!l1.contains(null)) // @odersky - 2.12 encoding seems wonky here as well

0 commit comments

Comments
 (0)