Skip to content

Allow indentation inside (...) #8659

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -650,12 +650,11 @@ object Parsers {

/** Check that this is not the start of a statement that's indented relative to the current region.
*/
def checkNextNotIndented(): Unit = in.currentRegion match
case r: IndentSignificantRegion if in.isNewLine =>
def checkNextNotIndented(): Unit =
if in.isNewLine then
val nextIndentWidth = in.indentWidth(in.next.offset)
if r.indentWidth < nextIndentWidth then
if in.currentRegion.indentWidth < nextIndentWidth then
warning(i"Line is indented too far to the right, or a `{` or `:` is missing", in.next.offset)
case _ =>

/* -------- REWRITES ----------------------------------------------------------- */

Expand Down
109 changes: 52 additions & 57 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ object Scanners {
/** Are we in a `${ }` block? such that RBRACE exits back into multiline string. */
private def inMultiLineInterpolatedExpression =
currentRegion match {
case InBraces(_, InString(true, _)) => true
case InBraces(InString(true, _)) => true
case _ => false
}

Expand All @@ -305,7 +305,7 @@ object Scanners {
case LPAREN | LBRACKET =>
currentRegion = InParens(lastToken, currentRegion)
case LBRACE =>
currentRegion = InBraces(null, currentRegion)
currentRegion = InBraces(currentRegion)
case RBRACE =>
def dropBraces(): Unit = currentRegion match {
case r: InBraces =>
Expand Down Expand Up @@ -340,7 +340,7 @@ object Scanners {
if (next.token == EMPTY) {
lastOffset = lastCharOffset
currentRegion match {
case InString(multiLine, _) => fetchStringPart(multiLine)
case InString(multiLine, _) if lastToken != STRINGPART => fetchStringPart(multiLine)
case _ => fetchToken()
}
if (token == ERROR) adjustSepRegions(STRINGLIT) // make sure we exit enclosing string literal
Expand All @@ -360,6 +360,7 @@ object Scanners {

/** Insert `token` at assumed `offset` in front of current one. */
def insert(token: Token, offset: Int) = {
assert(next.token == EMPTY, next)
next.copyFrom(this)
this.offset = offset
this.token = token
Expand Down Expand Up @@ -518,26 +519,24 @@ object Scanners {
* I.e. `a <= b` iff `b.startsWith(a)`. If indentation is significant it is considered an error
* if the current indentation width and the indentation of the current token are incomparable.
*/
def handleNewLine(lastToken: Token) = {
def handleNewLine(lastToken: Token) =
var indentIsSignificant = false
var newlineIsSeparating = false
var lastWidth = IndentWidth.Zero
var indentPrefix = EMPTY
val nextWidth = indentWidth(offset)
currentRegion match {
currentRegion match
case r: Indented =>
indentIsSignificant = indentSyntax
lastWidth = r.width
newlineIsSeparating = lastWidth <= nextWidth || r.isOutermost
indentPrefix = r.prefix
case r: InBraces =>
case r =>
indentIsSignificant = indentSyntax
if (r.width == null) r.width = nextWidth
lastWidth = r.width
newlineIsSeparating = true
indentPrefix = LBRACE
case _ =>
}
if (r.knownWidth == null) r.knownWidth = nextWidth
lastWidth = r.knownWidth
newlineIsSeparating = r.isInstanceOf[InBraces]

if newlineIsSeparating
&& canEndStatTokens.contains(lastToken)
&& canStartStatTokens.contains(token)
Expand Down Expand Up @@ -580,7 +579,7 @@ object Scanners {
currentRegion = Indented(curWidth, others + nextWidth, prefix, outer)
case _ =>
}
}
end handleNewLine

def spaceTabMismatchMsg(lastWidth: IndentWidth, nextWidth: IndentWidth) =
i"""Incompatible combinations of tabs and spaces in indentation prefixes.
Expand All @@ -597,10 +596,7 @@ object Scanners {
def observeIndented(): Unit =
if indentSyntax && isNewLine then
val nextWidth = indentWidth(next.offset)
val lastWidth = currentRegion match
case r: IndentSignificantRegion => r.indentWidth
case _ => nextWidth

val lastWidth = currentRegion.indentWidth
if lastWidth < nextWidth then
currentRegion = Indented(nextWidth, Set(), COLONEOL, currentRegion)
offset = next.offset
Expand Down Expand Up @@ -650,15 +646,27 @@ object Scanners {
lookahead()
if (token != ELSE) reset()
case COMMA =>
lookahead()
if (isAfterLineEnd && (token == RPAREN || token == RBRACKET || token == RBRACE || token == OUTDENT)) {
/* skip the trailing comma */
} else if (token == EOF) { // e.g. when the REPL is parsing "val List(x, y, _*,"
/* skip the trailing comma */
} else reset()
def isEnclosedInParens(r: Region): Boolean = r match
case r: Indented => isEnclosedInParens(r.outer)
case _: InParens => true
case _ => false
currentRegion match
case r: Indented if isEnclosedInParens(r.outer) =>
insert(OUTDENT, offset)
currentRegion = r.outer
case _ =>
lookahead()
if isAfterLineEnd
&& (token == RPAREN || token == RBRACKET || token == RBRACE || token == OUTDENT)
then
() /* skip the trailing comma */
else if token == EOF then // e.g. when the REPL is parsing "val List(x, y, _*,"
() /* skip the trailing comma */
else
reset()
case COLON =>
if colonSyntax then observeColonEOL()
case EOF | RBRACE =>
case EOF | RBRACE | RPAREN | RBRACKET =>
currentRegion match {
case r: Indented if !r.isOutermost =>
insert(OUTDENT, offset)
Expand Down Expand Up @@ -1097,13 +1105,7 @@ object Scanners {
getRawStringLit()
}

@annotation.tailrec private def getStringPart(multiLine: Boolean): Unit = {
def finishStringPart() = {
setStrVal()
token = STRINGPART
next.lastOffset = charOffset - 1
next.offset = charOffset - 1
}
@annotation.tailrec private def getStringPart(multiLine: Boolean): Unit =
if (ch == '"')
if (multiLine) {
nextRawChar()
Expand All @@ -1127,18 +1129,19 @@ object Scanners {
getStringPart(multiLine)
}
else if (ch == '{') {
finishStringPart()
nextRawChar()
next.token = LBRACE
setStrVal()
token = STRINGPART
}
else if (Character.isUnicodeIdentifierStart(ch) || ch == '_') {
finishStringPart()
while ({
setStrVal()
token = STRINGPART
next.lastOffset = charOffset - 1
next.offset = charOffset - 1
while
putChar(ch)
nextRawChar()
ch != SU && Character.isUnicodeIdentifierPart(ch)
})
()
do ()
finishNamed(target = next)
}
else
Expand All @@ -1157,7 +1160,7 @@ object Scanners {
getStringPart(multiLine)
}
}
}
end getStringPart

private def fetchStringPart(multiLine: Boolean) = {
offset = charOffset - 1
Expand Down Expand Up @@ -1379,7 +1382,7 @@ object Scanners {
* InBraces a pair of braces { ... }
* Indented a pair of <indent> ... <outdent> tokens
*/
abstract class Region {
abstract class Region:
/** The region enclosing this one, or `null` for the outermost region */
def outer: Region | Null

Expand All @@ -1389,32 +1392,24 @@ object Scanners {
/** The enclosing region, which is required to exist */
def enclosing: Region = outer.asInstanceOf[Region]

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

/** The indentation width, Zero if not known */
final def indentWidth: IndentWidth =
if knownWidth == null then IndentWidth.Zero else knownWidth
end Region

case class InString(multiLine: Boolean, outer: Region) extends Region
case class InParens(prefix: Token, outer: Region) extends Region

abstract class IndentSignificantRegion extends Region

case class InBraces(var width: IndentWidth | Null, outer: Region)
extends IndentSignificantRegion {
// The indent width starts out as `null` when the opening brace is encountered
// It is then adjusted when the next token on a new line is encountered.
override def indentWidth: IndentWidth =
if width == null then IndentWidth.Zero else width
}
case class InBraces(outer: Region) extends Region

/** A class describing an indentation region.
* @param width The principal indendation width
* @param others Other indendation widths > width of lines in the same region
* @param prefix The token before the initial <indent> of the region
*/
case class Indented(width: IndentWidth, others: Set[IndentWidth], prefix: Token, outer: Region | Null)
extends IndentSignificantRegion {
override def indentWidth = width
}
case class Indented(width: IndentWidth, others: Set[IndentWidth], prefix: Token, outer: Region | Null) extends Region:
knownWidth = width

enum IndentWidth {
case Run(ch: Char, n: Int)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/parsing/Tokens.scala
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ object Tokens extends TokensCommon {

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

final val closingRegionTokens = BitSet(RBRACE, CASE) | statCtdTokens
final val closingRegionTokens = BitSet(RBRACE, RPAREN, RBRACKET, CASE) | statCtdTokens

final val canStartIndentTokens: BitSet =
statCtdTokens | BitSet(COLONEOL, EQUALS, ARROW, LARROW, WHILE, TRY, FOR, IF)
Expand Down
9 changes: 5 additions & 4 deletions docs/docs/reference/other-new-features/indentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: Optional Braces
---

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

- First, some badly indented programs are flagged with warnings.
Expand Down Expand Up @@ -78,10 +78,11 @@ There are two rules:
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
may be inserted in a row.

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`.
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`.

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.
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

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.
```scala
if x < 0
-x
Expand Down
2 changes: 1 addition & 1 deletion tests/neg/parser-stability-19.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
object x0 {
case class x0[](): // error
def x0( ) ] // error
def x0 ( x0:x0 ):x0.type = x1 x0 // error // error
def x0 ( x0:x0 ):x0.type = x1 x0 // error // error // error
// error
38 changes: 38 additions & 0 deletions tests/pos/indent-in-parens.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
def f(x: Int) =
assert(
if x > 0 then
true
else
false
)
assert(
if x > 0 then
true
else
false)
assert(
if x > 0 then
true
else
false, "fail")
assert(
if x > 0 then
true
else
if x < 0 then
true
else
false, "fail")
(
if x > 0 then
println(x)
x
else
s"""foo${
if x > 0 then
println(x)
x
else
-x
}"""
)
67 changes: 33 additions & 34 deletions tests/run/LazyValsLongs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,41 +23,40 @@ object Test {
def main(args: Array[String]): Unit = {
val c = new I
import c._
val l1 = List(A1,
A2,
A3,
A4,
A5,
A6,
A7,
A8,
A9,
A10,
A11,
A12,
A13,
A14,
A15,
A16,
A17,
val l1 = List(A1,
A2,
A3,
A4,
A5, A6,
A7,
A8,
A9,
A10,
A11,
A12,
A13,
A14,
A15,
A16,
A17,
A18)
val l2 = List(A1,
A2,
A3,
A4,
A5,
A6,
A7,
A8,
A9,
A10,
A11,
A12,
A13,
A14,
A15,
A16,
A17,
val l2 = List(A1,
A2,
A3,
A4,
A5,
A6,
A7,
A8,
A9,
A10,
A11,
A12,
A13,
A14,
A15,
A16,
A17,
A18)
assert(l1.mkString == l2.mkString)
assert(!l1.contains(null)) // @odersky - 2.12 encoding seems wonky here as well
Expand Down