Skip to content

Commit 9bc37f2

Browse files
committed
Significant indentation is turned on by default
Also: More systematic checking of conflicting rewrite options
1 parent 1398177 commit 9bc37f2

File tree

3 files changed

+119
-96
lines changed

3 files changed

+119
-96
lines changed

compiler/src/dotty/tools/dotc/config/Config.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ object Config {
157157
*/
158158
final val simplifyApplications = true
159159

160-
/** Always assume -indent */
161-
final val alwaysIndent = false
160+
/** Assume -indent by default */
161+
final val defaultIndent = true
162162

163163
/** If set, prints a trace of all symbol completions */
164164
final val showCompletions = false

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

+38-22
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import scala.internal.Chars._
1010
import util.NameTransformer.avoidIllegalChars
1111
import util.Spans.Span
1212
import config.Config
13+
import config.Printers.lexical
14+
import config.Settings.Setting
1315
import Tokens._
1416
import scala.annotation.{ switch, tailrec }
1517
import scala.collection.mutable
1618
import scala.collection.immutable.{SortedMap, BitSet}
1719
import rewrites.Rewrites.patch
18-
import config.Printers.lexical
1920

2021
object Scanners {
2122

@@ -229,17 +230,30 @@ object Scanners {
229230
/** A switch whether operators at the start of lines can be infix operators */
230231
private var allowLeadingInfixOperators = true
231232

233+
val isScala2Mode: Boolean = ctx.scala2Setting
234+
232235
val rewrite = ctx.settings.rewrite.value.isDefined
233236
val oldSyntax = ctx.settings.oldSyntax.value
234237
val newSyntax = ctx.settings.newSyntax.value
235238

236-
val noindentSyntax = ctx.settings.noindent.value
237-
val indentSyntax = Config.alwaysIndent || ctx.settings.indent.value || noindentSyntax && rewrite
238239
val rewriteToIndent = ctx.settings.indent.value && rewrite
239-
val rewriteNoIndent = noindentSyntax && rewrite
240+
val rewriteNoIndent = ctx.settings.noindent.value && rewrite
240241

241-
if (rewrite && oldSyntax & noindentSyntax)
242-
error("-rewrite cannot be used with both -old-syntax and -noindent; -noindent must come first")
242+
val noindentSyntax =
243+
ctx.settings.noindent.value ||
244+
ctx.settings.oldSyntax.value ||
245+
isScala2Mode
246+
val indentSyntax =
247+
(if (Config.defaultIndent) !noindentSyntax else ctx.settings.indent.value) ||
248+
rewriteNoIndent
249+
250+
if (rewrite) {
251+
val s = ctx.settings
252+
val rewriteTargets = List(s.newSyntax, s.oldSyntax, s.indent, s.noindent)
253+
val enabled = rewriteTargets.filter(_.value)
254+
if (enabled.length > 1)
255+
error(s"illegal combination of -rewrite targets: ${enabled(0).name} and ${enabled(1).name}")
256+
}
243257

244258
/** All doc comments kept by their end position in a `Map` */
245259
private[this] var docstringMap: SortedMap[Int, Comment] = SortedMap.empty
@@ -294,26 +308,14 @@ object Scanners {
294308
val next = newTokenData
295309
private val prev = newTokenData
296310

297-
/** a stack of tokens which indicates whether line-ends can be statement separators
298-
* also used for keeping track of nesting levels.
299-
* We keep track of the closing symbol of a region. This can be
300-
* RPAREN if region starts with '('
301-
* RBRACKET if region starts with '['
302-
* RBRACE if region starts with '{'
303-
* ARROW if region starts with `case'
304-
* STRINGLIT if region is a string interpolation expression starting with '${'
305-
* (the STRINGLIT appears twice in succession on the stack iff the
306-
* expression is a multiline string literal).
307-
*/
308-
var currentRegion: Region = Outermost
311+
/** The current region. This is initially an Indented region with indentation width. */
312+
var currentRegion: Region = Indented(IndentWidth.Zero, Set(), EMPTY, null)
309313

310314
/** The end marker that was skipped last */
311315
val endMarkers = new mutable.ListBuffer[EndMarker]
312316

313317
// Scala 2 compatibility
314318

315-
val isScala2Mode: Boolean = ctx.scala2Setting
316-
317319
/** Cannot use ctx.featureEnabled because accessing the context would force too much */
318320
def testScala2Mode(msg: String, span: Span = Span(offset)): Boolean = {
319321
if (isScala2Mode) ctx.migrationWarning(msg, source.atSpan(span))
@@ -1359,12 +1361,27 @@ object Scanners {
13591361
}
13601362
// end Scanner
13611363

1364+
/** A Region indicates what encloses the current token. It can be one of the following
1365+
*
1366+
* InString a string interpolation
1367+
* InParens a pair of parentheses (...) or brackets [...]
1368+
* InBraces a pair of braces { ... }
1369+
* Indented a pair of <indent> ... <outdent> tokens
1370+
*/
13621371
abstract class Region {
1372+
/** The region enclosing this one, or `null` for the outermost region */
13631373
def outer: Region | Null
1374+
1375+
/** Is this region the outermost region? */
13641376
def isOutermost = outer == null
1377+
1378+
/** The enclosing region, which is required to exist */
13651379
def enclosing: Region = outer.asInstanceOf[Region]
1380+
1381+
/** If this is an InBraces or Indented region, its indentation width, or Zero otherwise */
13661382
def indentWidth = IndentWidth.Zero
13671383
}
1384+
13681385
case class InString(multiLine: Boolean, outer: Region) extends Region
13691386
case class InParens(prefix: Token, outer: Region) extends Region
13701387
case class InBraces(var width: IndentWidth | Null, outer: Region) extends Region {
@@ -1374,13 +1391,12 @@ object Scanners {
13741391
/** A class describing an indentation region.
13751392
* @param width The principal indendation width
13761393
* @param others Other indendation widths > width of lines in the same region
1394+
* @param prefix The token before the initial <indent> of the region
13771395
*/
13781396
case class Indented(width: IndentWidth, others: Set[IndentWidth], prefix: Token, outer: Region | Null) extends Region {
13791397
override def indentWidth = width
13801398
}
13811399

1382-
val Outermost = Indented(IndentWidth.Zero, Set(), EMPTY, null)
1383-
13841400
enum IndentWidth {
13851401
case Run(ch: Char, n: Int)
13861402
case Conc(l: IndentWidth, r: Run)

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

+79-72
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ layout: doc-page
33
title: Significant Indentation
44
---
55

6-
As an experimental feature, Scala 3 treats indentation as significant.
7-
8-
Indentation is significant everywhere except inside regions delineated by braces `{...}`, brackets `[...]` or parentheses `(...)` or within string or character literals.
6+
As an experimental feature, Scala 3 treats indentation as significant. Indentation is significant everywhere except inside regions delineated by brackets `[...]` or parentheses `(...)`.
97

108
Where indentation is significant, the compiler will insert `<indent>` or `<outdent>`
119
tokens at certain line breaks. Grammatically, pairs of `<indent>` and `<outdent>` tokens have the same effect as pairs of braces `{` and `}`.
@@ -17,38 +15,50 @@ There are two rules:
1715
1. An `<indent>` is inserted at a line break, if
1816

1917
- the first token on the next line has an indentation width strictly greater
20-
than the current indentation width, and
18+
than the current indentation width, and
2119
- the last token on the previous line can start an indentation region.
2220

23-
The following tokens can start an indentation region:
21+
The following tokens can start an indentation region:
2422
```
25-
: = => <- if then else while do try catch finally for yield match
23+
: = => <- if then else while do try catch finally for yield match
2624
```
2725
28-
If an `<indent>` is inserted, the indentation width of the token on the next line
29-
is pushed onto `IW`, which makes it the new current indentation width.
26+
If an `<indent>` is inserted, the indentation width of the token on the next line
27+
is pushed onto `IW`, which makes it the new current indentation width.
3028
3129
2. An `<outdent>` is inserted at a line break, if
3230
3331
- the first token on the next line has an indentation width strictly less
34-
than the current indentation width, and
35-
- the first token on the next line is not a
36-
[leading infix operator](../changed-features/operators.html).
32+
than the current indentation width, and
33+
- the first token on the next line is not a
34+
[leading infix operator](../changed-features/operators.html).
3735
38-
If an `<outdent>` is inserted, the top element if popped from `IW`.
39-
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
40-
may be inserted in a row.
36+
If an `<outdent>` is inserted, the top element if popped from `IW`.
37+
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
38+
may be inserted in a row.
4139
4240
It is an error if the indentation width of the token following an `<outdent>` does not
4341
match the indentation of some previous line in the enclosing indentation region. For instance, the following would be rejected.
4442
```scala
4543
if x < 0 then
4644
-x
4745
else // error: `else` does not align correctly
48-
x
46+
x
4947
```
5048

51-
Indentation prefixes can consist of spaces and tabs. Indentation widths are the indentation prefixes themselves, ordered by the string prefix relation. So, so for instance "2 tabs, followed by 4 spaces" is strictly less than "2 tabs, followed by 5 spaces", but "2 tabs, followed by 4 spaces" is incomparable to "6 tabs" or to "4 spaces, followed by 2 tabs". It is an error if the indentation width of some line is incomparable with the indentation width of the region that's current at that point. To avoid such errors, it is a good idea not to mix spaces and tabs in the same source file.
49+
### Spaces vs Tabs
50+
51+
Indentation prefixes can consist of spaces and/or tabs. Indentation widths are the indentation prefixes themselves, ordered by the string prefix relation. So, so for instance "2 tabs, followed by 4 spaces" is strictly less than "2 tabs, followed by 5 spaces", but "2 tabs, followed by 4 spaces" is incomparable to "6 tabs" or to "4 spaces, followed by 2 tabs". It is an error if the indentation width of some line is incomparable with the indentation width of the region that's current at that point. To avoid such errors, it is a good idea not to mix spaces and tabs in the same source file.
52+
53+
### Indentation and Braces
54+
55+
Indentatation can be mixed freely with braces. For interpreting indentation inside braces, the following rules apply.
56+
57+
1. The assumed indentation width of a multiline region enclosed in braces is the
58+
indentation width of the first token that starts a new line after the opening brace.
59+
60+
2. On encountering a closing brace `}`, as many `<outdent>` tokens as necessary are
61+
inserted to close all open indentation regions inside the pair of braces.
5262

5363
### Indentation Marker `:`
5464

@@ -63,14 +73,13 @@ or
6373
```scala
6474
xs.map:
6575
x =>
66-
val y = x - 1
67-
y * y
76+
val y = x - 1
77+
y * y
6878
```
6979
Colons at the end of lines are their own token, distinct from normal `:`.
7080
The Scala grammar is changed so that colons at end of lines are accepted at all points
7181
where an opening brace is legal, except if the previous token can already start an
72-
indentation region. Special provisions are taken so that method result types can still use a colon on
73-
the end of a line, followed by the actual type on the next.
82+
indentation region. Special provisions are taken so that method result types can still use a colon on the end of a line, followed by the actual type on the next.
7483

7584
### Special Treatment of Case Clauses
7685

@@ -80,7 +89,7 @@ The indentation rules for `match` expressions and `catch` clauses are refined as
8089
appears at the indentation width that's current for the `match` itself.
8190
- In that case, the indentation region closes at the first token at that
8291
same indentation width that is not a `case`, or at any token with a smaller
83-
indentation width, whichever comes first.
92+
indentation width, whichever comes first.
8493

8594
The rules allow to write `match` expressions where cases are not indented themselves, as in the example below:
8695
```scala
@@ -113,8 +122,8 @@ An `end` marker consists of the identifier `end` which follows an `<outdent>` to
113122
```scala
114123
if while for match try new
115124
```
116-
If `end` is followed by a reserved word, the compiler checks that the marker closes an indentation region belonging to a construct that starts with the reserved word. If it is followed by an identifier _id_, the compiler checks that the marker closes an indentation region containing the right hand side of a `val`, `var`, or `def` or
117-
the body of a class, trait, object, enum, given instance, or package clause that defines _id_.
125+
If `end` is followed by a reserved word, the compiler checks that the marker closes an indentation region belonging to a construct that starts with the reserved word. If it is followed by an identifier _id_, the compiler checks that the marker closes a definition
126+
that defines _id_ or a package clause that refers to _id_.
118127

119128
`end` itself is a soft keyword. It is only treated as an `end` marker if it
120129
occurs at the start of a line and is followed by an identifier or one of the reserved words above.
@@ -127,59 +136,57 @@ Here is a (somewhat meta-circular) example of code using indentation. It provide
127136

128137
```scala
129138
enum IndentWidth:
130-
131-
/** A run of `n` characters `ch` */
132-
case Run(ch: Char, n: Int)
133-
134-
/** `l` followed by `r` */
135-
case Conc(l: IndentWidth, r: Run)
136-
137-
def <= (that: IndentWidth): Boolean =
138-
this match
139-
case Run(ch1, n1) =>
140-
that match
141-
case Run(ch2, n2) => n1 <= n2 && (ch1 == ch2 || n1 == 0)
142-
case Conc(l, r) => this <= l
143-
case Conc(l1, r1) =>
144-
that match
145-
case Conc(l2, r2) => l1 == l2 && r1 <= r2
146-
case _ => false
147-
148-
def < (that: IndentWidth): Boolean = this <= that && !(that <= this)
149-
150-
override def toString: String =
151-
this match
152-
case Run(ch, n) =>
153-
val kind = ch match
154-
case ' ' => "space"
155-
case '\t' => "tab"
156-
case _ => s"'$ch'-character"
157-
val suffix = if n == 1 then "" else "s"
158-
s"$n $kind$suffix"
159-
case Conc(l, r) =>
160-
s"$l, $r"
139+
case Run(ch: Char, n: Int)
140+
case Conc(l: IndentWidth, r: Run)
141+
142+
def <= (that: IndentWidth): Boolean =
143+
this match
144+
case Run(ch1, n1) =>
145+
that match
146+
case Run(ch2, n2) => n1 <= n2 && (ch1 == ch2 || n1 == 0)
147+
case Conc(l, r) => this <= l
148+
case Conc(l1, r1) =>
149+
that match
150+
case Conc(l2, r2) => l1 == l2 && r1 <= r2
151+
case _ => false
152+
153+
def < (that: IndentWidth): Boolean = this <= that && !(that <= this)
154+
155+
override def toString: String =
156+
this match
157+
case Run(ch, n) =>
158+
val kind = ch match
159+
case ' ' => "space"
160+
case '\t' => "tab"
161+
case _ => s"'$ch'-character"
162+
val suffix = if n == 1 then "" else "s"
163+
s"$n $kind$suffix"
164+
case Conc(l, r) =>
165+
s"$l, $r"
161166

162167
object IndentWidth:
163-
private inline val MaxCached = 40
164-
165-
private val spaces = IArray.tabulate(MaxCached + 1):
166-
new Run(' ', _)
167-
private val tabs = IArray.tabulate(MaxCached + 1):
168-
new Run('\t', _)
169-
170-
def Run(ch: Char, n: Int): Run =
171-
if n <= MaxCached && ch == ' ' then
172-
spaces(n)
173-
else if n <= MaxCached && ch == '\t' then
174-
tabs(n)
175-
else
176-
new Run(ch, n)
177-
178-
val Zero = Run(' ', 0)
179-
end IndentWidth
168+
private inline val MaxCached = 40
169+
170+
private val spaces = IArray.tabulate(MaxCached + 1):
171+
new Run(' ', _)
172+
private val tabs = IArray.tabulate(MaxCached + 1):
173+
new Run('\t', _)
174+
175+
def Run(ch: Char, n: Int): Run =
176+
if n <= MaxCached && ch == ' ' then
177+
spaces(n)
178+
else if n <= MaxCached && ch == '\t' then
179+
tabs(n)
180+
else
181+
new Run(ch, n)
182+
end Run
183+
184+
val Zero = Run(' ', 0)
180185
```
181186

182-
### Rewrites
187+
### Settings and Rewrites
188+
189+
Significant indentation is enabled by default. It can be turned off by giving any of the options `-noindent`, `old-syntax` and `language:Scala2`. If indentation is turned off, it is nevertheless checked that indentation conforms to the logical program structure as defined by braces. If that is not the case, the compiler issues an error (or, in the case of `-language:Scala2`, a migration warning).
183190

184191
The Dotty compiler can rewrite source code to indented code and back.
185192
When invoked with options `-rewrite -indent` it will rewrite braces to

0 commit comments

Comments
 (0)