From c30eafab10b284a972eb11239d4187b946792ec8 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 23 Sep 2019 11:30:18 +0200 Subject: [PATCH 1/6] Make `then` optional at the end of line `then` is treated like `;`: It is inferred at the end of a line, if - a new line would otherwise be inferred - the next line is indented Disambiguation with Scala-2 syntax is as follows: The question is whether to classify if (A) B C as a condition A followed by a statement B (old-style), or as a condition (A) B followed by a then part C, and an inferred `then` in-between (new-style) (new-style) is chosen if B cannot start a statement, or starts with a leading infix operator. Otherwise put, (new-style) is chosen if in ``` (A) B ``` no newline would be inserted. (old-style) is chosen otherwise. This means that some otherwise legal conditions still need a `then` at the end of a line. If (old-style) is eventually deprecated and removed, we can drop this restriction. --- .../tools/backend/jvm/BCodeHelpers.scala | 2 +- .../dotty/tools/dotc/parsing/Parsers.scala | 89 +++++++++++-------- .../dotty/tools/dotc/parsing/Scanners.scala | 65 +++++++------- .../languageserver/DottyLanguageServer.scala | 30 +++---- .../dotty/tools/languageserver/Memory.scala | 2 +- tests/neg/indent.scala | 9 ++ tests/pos/indent.scala | 19 ++++ 7 files changed, 133 insertions(+), 83 deletions(-) create mode 100644 tests/neg/indent.scala diff --git a/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala b/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala index e7986638e558..d67ac2d25ddd 100644 --- a/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala +++ b/compiler/src/dotty/tools/backend/jvm/BCodeHelpers.scala @@ -220,7 +220,7 @@ trait BCodeHelpers extends BCodeIdiomatic with BytecodeWriters { if (sym == NothingClass) RT_NOTHING else if (sym == NullClass) RT_NULL else { - val r = classBTypeFromSymbol(sym) + val r = classBTypeFromSymbol(sym) if (r.isNestedClass) innerClassBufferASM += r r } diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 384659030c51..19f4aff10559 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -69,7 +69,7 @@ object Parsers { * if not, the AST will be supplemented. */ def parser(source: SourceFile)(implicit ctx: Context): Parser = - if (source.isSelfContained) new ScriptParser(source) + if source.isSelfContained then new ScriptParser(source) else new Parser(source) abstract class ParserCommon(val source: SourceFile)(implicit ctx: Context) { @@ -354,7 +354,7 @@ object Parsers { case NEWLINE | NEWLINES => in.nextToken() case SEMI => in.nextToken() case _ => - syntaxError(i"end of statement expected but $in found") + syntaxError(i"end of statement expected but ${showToken(in.token)} found") in.nextToken() // needed to ensure progress; otherwise we might cycle forever accept(SEMI) } @@ -813,20 +813,20 @@ object Parsers { else span } - /** Drop current token, which is assumed to be `then` or `do`. */ - def dropTerminator(): Unit = { - var startOffset = in.offset - var endOffset = in.lastCharOffset - if (in.isAfterLineEnd) { - if (testChar(endOffset, ' ')) - endOffset += 1 - } - else - if (testChar(startOffset - 1, ' ') && - !overlapsPatch(source, Span(startOffset - 1, endOffset))) - startOffset -= 1 - patch(source, widenIfWholeLine(Span(startOffset, endOffset)), "") - } + /** Drop current token, if it is a `then` or `do`. */ + def dropTerminator(): Unit = + if in.token == THEN || in.token == DO then + var startOffset = in.offset + var endOffset = in.lastCharOffset + if (in.isAfterLineEnd) { + if (testChar(endOffset, ' ')) + endOffset += 1 + } + else + if (testChar(startOffset - 1, ' ') && + !overlapsPatch(source, Span(startOffset - 1, endOffset))) + startOffset -= 1 + patch(source, widenIfWholeLine(Span(startOffset, endOffset)), "") /** rewrite code with (...) around the source code of `t` */ def revertToParens(t: Tree): Unit = @@ -841,7 +841,8 @@ object Parsers { /** In the tokens following the current one, does `query` precede any of the tokens that * - must start a statement, or * - separate two statements, or - * - continue a statement (e.g. `else`, catch`)? + * - continue a statement (e.g. `else`, catch`), or + * - terminate the current scope? */ def followedByToken(query: Token): Boolean = { val lookahead = in.LookaheadScanner() @@ -1251,7 +1252,7 @@ object Parsers { } def possibleTemplateStart(): Unit = { - in.observeIndented(noIndentTemplateTokens, nme.derives) + in.observeIndented(unless = noIndentTemplateTokens, unlessSoftKW = nme.derives) newLineOptWhenFollowedBy(LBRACE) } @@ -1650,35 +1651,51 @@ object Parsers { /* ----------- EXPRESSIONS ------------------------------------------------ */ + /** Does the current conditional expression continue after + * the initially parsed (...) region? + */ + def toBeContinued(altToken: Token): Boolean = + if in.token == altToken || in.isNewLine || in.isScala2Mode then + false // a newline token means the expression is finished + else if !canStartStatTokens.contains(in.token) + || in.isLeadingInfixOperator(inConditional = true) + then + true + else + followedByToken(altToken) // scan ahead to see whether we find a `then` or `do` + def condExpr(altToken: Token): Tree = - if (in.token == LPAREN) { + if in.token == LPAREN then var t: Tree = atSpan(in.offset) { Parens(inParens(exprInParens())) } - if (in.token != altToken && followedByToken(altToken)) - t = inSepRegion(LPAREN, RPAREN) { - newLineOpt() + val newSyntax = toBeContinued(altToken) + if newSyntax then + t = inSepRegion(LBRACE, RBRACE) { expr1Rest(postfixExprRest(simpleExprRest(t)), Location.ElseWhere) } - if (in.token == altToken) { - if (rewriteToOldSyntax()) revertToParens(t) + if in.token == altToken then + if rewriteToOldSyntax() then revertToParens(t) in.nextToken() - } - else { - in.observeIndented(noIndentAfterConditionTokens) + else + if (altToken == THEN || !newSyntax) && in.isNewLine then + in.observeIndented() + if newSyntax && in.token != INDENT then accept(altToken) if (rewriteToNewSyntax(t.span)) dropParensOrBraces(t.span.start, s"${tokenString(altToken)}") - } t - } - else { + else val t = - if (in.isNestedStart) + if in.isNestedStart then try expr() finally newLinesOpt() else - inSepRegion(LPAREN, RPAREN)(expr()) - if (rewriteToOldSyntax(t.span.startPos)) revertToParens(t) - accept(altToken) + inSepRegion(LBRACE, RBRACE)(expr()) + if rewriteToOldSyntax(t.span.startPos) then + revertToParens(t) + if altToken == THEN && in.isNewLine then + // don't require a `then` at the end of a line + in.observeIndented() + if in.token != INDENT then accept(altToken) t - } + end condExpr /** Expr ::= [`implicit'] FunParams =>' Expr * | Expr1 @@ -2327,7 +2344,7 @@ object Parsers { dropParensOrBraces(start, if (in.token == YIELD || in.token == DO) "" else "do") } } - in.observeIndented(noIndentAfterEnumeratorTokens) + in.observeIndented(unless = noIndentAfterEnumeratorTokens) res } else { diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index d6c3e39cec62..6c79803db1f0 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -374,8 +374,8 @@ object Scanners { * If a leading infix operator is found and -language:Scala2 or -old-syntax is set, * emit a change warning. */ - def isLeadingInfixOperator() = ( - allowLeadingInfixOperators + def isLeadingInfixOperator(inConditional: Boolean = true) = ( + allowLeadingInfixOperators && ( token == BACKQUOTED_IDENT || token == IDENTIFIER && isOperatorPart(name(name.length - 1))) && ch == ' ' @@ -388,9 +388,12 @@ object Scanners { canStartExpressionTokens.contains(lookahead.token) } && { - if (isScala2Mode || oldSyntax && !rewrite) - ctx.warning(em"""Line starts with an operator; - |it is now treated as a continuation of the expression on the previous line, + if isScala2Mode || oldSyntax && !rewrite then + val (what, previous) = + if inConditional then ("Rest of line", "previous expression in parentheses") + else ("Line", "expression on the previous line") + ctx.warning(em"""$what starts with an operator; + |it is now treated as a continuation of the $previous, |not as a separate statement.""", source.atSpan(Span(offset))) true @@ -538,11 +541,13 @@ object Scanners { |Previous indent : $lastWidth |Latest indent : $nextWidth""" - def observeIndented(unless: BitSet, unlessSoftKW: TermName = EmptyTermName): Unit = + def observeIndented( + unless: BitSet = BitSet.empty, + unlessSoftKW: TermName = EmptyTermName): Unit + = if (indentSyntax && isAfterLineEnd && token != INDENT) { - val newLineInserted = token == NEWLINE || token == NEWLINES - val nextOffset = if (newLineInserted) next.offset else offset - val nextToken = if (newLineInserted) next.token else token + val nextOffset = if (isNewLine) next.offset else offset + val nextToken = if (isNewLine) next.token else token val nextWidth = indentWidth(nextOffset) val lastWidth = currentRegion match { case r: Indented => r.width @@ -554,7 +559,7 @@ object Scanners { && !unless.contains(nextToken) && (unlessSoftKW.isEmpty || token != IDENTIFIER || name != unlessSoftKW)) { currentRegion = Indented(nextWidth, Set(), COLONEOL, currentRegion) - if (!newLineInserted) next.copyFrom(this) + if (!isNewLine) next.copyFrom(this) offset = nextOffset token = INDENT } @@ -880,38 +885,35 @@ object Scanners { print("la:") super.printState() } - - /** Skip matching pairs of `(...)` or `[...]` parentheses. - * @pre The current token is `(` or `[` - */ - final def skipParens(): Unit = { - val opening = token - nextToken() - while token != EOF && token != opening + 1 do - if token == opening then skipParens() else nextToken() - nextToken() - } } + /** Skip matching pairs of `(...)` or `[...]` parentheses. + * @pre The current token is `(` or `[` + */ + final def skipParens(multiple: Boolean = true): Unit = + val opening = token + nextToken() + while token != EOF && token != opening + 1 do + if token == opening && multiple then skipParens() else nextToken() + nextToken() + /** Is the token following the current one in `tokens`? */ def lookaheadIn(tokens: BitSet): Boolean = { val lookahead = LookaheadScanner() - while ({ + while lookahead.nextToken() - lookahead.token == NEWLINE || lookahead.token == NEWLINES - }) - () + lookahead.isNewLine + do () tokens.contains(lookahead.token) } /** Is the current token in a position where a modifier is allowed? */ def inModifierPosition(): Boolean = { val lookahead = LookaheadScanner() - while ({ + while lookahead.nextToken() - lookahead.token == NEWLINE || lookahead.token == NEWLINES || lookahead.isSoftModifier - }) - () + lookahead.isNewLine || lookahead.isSoftModifier + do () modifierFollowers.contains(lookahead.token) } @@ -1003,6 +1005,8 @@ object Scanners { def isSoftModifierInParamModifierPosition: Boolean = isSoftModifier && !lookaheadIn(BitSet(COLON)) + def isNewLine = token == NEWLINE || token == NEWLINES + def isNestedStart = token == LBRACE || token == INDENT def isNestedEnd = token == RBRACE || token == OUTDENT @@ -1273,7 +1277,8 @@ object Scanners { override def toString: String = showTokenDetailed(token) + { - if ((identifierTokens contains token) || (literalTokens contains token)) " " + name + if identifierTokens.contains(token) then name + else if literalTokens.contains(token) then strVal else "" } diff --git a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala index 475f118d6667..4d146fb60e48 100644 --- a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -66,7 +66,7 @@ class DottyLanguageServer extends LanguageServer private[this] var myDependentProjects: mutable.Map[ProjectConfig, mutable.Set[ProjectConfig]] = _ def drivers: Map[ProjectConfig, InteractiveDriver] = thisServer.synchronized { - if (myDrivers == null) { + if myDrivers == null assert(rootUri != null, "`drivers` cannot be called before `initialize`") val configFile = new File(new URI(rootUri + '/' + IDE_CONFIG_FILE)) val configs: List[ProjectConfig] = (new ObjectMapper).readValue(configFile, classOf[Array[ProjectConfig]]).toList @@ -78,7 +78,7 @@ class DottyLanguageServer extends LanguageServer implicit class updateDeco(ss: List[String]) { def update(pathKind: String, pathInfo: String) = { val idx = ss.indexOf(pathKind) - val ss1 = if (idx >= 0) ss.take(idx) ++ ss.drop(idx + 2) else ss + val ss1 = if idx >= 0 then ss.take(idx) ++ ss.drop(idx + 2) else ss ss1 ++ List(pathKind, pathInfo) } } @@ -91,7 +91,6 @@ class DottyLanguageServer extends LanguageServer "-scansource" myDrivers(config) = new InteractiveDriver(settings) } - } myDrivers } @@ -105,12 +104,13 @@ class DottyLanguageServer extends LanguageServer System.gc() for ((_, driver, opened) <- driverConfigs; (uri, source) <- opened) driver.run(uri, source) - if (Memory.isCritical()) + if Memory.isCritical() println(s"WARNING: Insufficient memory to run Scala language server on these projects.") } private def checkMemory() = - if (Memory.isCritical()) CompletableFutures.computeAsync { _ => restart() } + if Memory.isCritical() + CompletableFutures.computeAsync { _ => restart() } /** The configuration of the project that owns `uri`. */ def configFor(uri: URI): ProjectConfig = thisServer.synchronized { @@ -138,7 +138,7 @@ class DottyLanguageServer extends LanguageServer implicit class updateDeco(ss: List[String]) { def update(pathKind: String, pathInfo: String) = { val idx = ss.indexOf(pathKind) - val ss1 = if (idx >= 0) ss.take(idx) ++ ss.drop(idx + 2) else ss + val ss1 = if idx >= 0 then ss.take(idx) ++ ss.drop(idx + 2) else ss ss1 ++ List(pathKind, pathInfo) } } @@ -151,7 +151,7 @@ class DottyLanguageServer extends LanguageServer /** A mapping from project `p` to the set of projects that transitively depend on `p`. */ def dependentProjects: Map[ProjectConfig, Set[ProjectConfig]] = thisServer.synchronized { - if (myDependentProjects == null) { + if myDependentProjects == null val idToConfig = drivers.keys.map(k => k.id -> k).toMap val allProjects = drivers.keySet @@ -165,7 +165,6 @@ class DottyLanguageServer extends LanguageServer dependency <- transitiveDependencies(project) } { myDependentProjects(dependency) += project } - } myDependentProjects } @@ -195,7 +194,7 @@ class DottyLanguageServer extends LanguageServer throw ex } } - if (synchronize) + if synchronize thisServer.synchronized { computation() } else computation() @@ -814,15 +813,15 @@ object DottyLanguageServer { def completionItemKind(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItemKind = { import lsp4j.{CompletionItemKind => CIK} - if (sym.is(Package) || sym.is(Module)) + if sym.is(Package) || sym.is(Module) CIK.Module // No CompletionItemKind.Package (https://github.com/Microsoft/language-server-protocol/issues/155) - else if (sym.isConstructor) + else if sym.isConstructor CIK.Constructor - else if (sym.isClass) + else if sym.isClass CIK.Class - else if (sym.is(Mutable)) + else if sym.is(Mutable) CIK.Variable - else if (sym.is(Method)) + else if sym.is(Method) CIK.Method else CIK.Field @@ -846,7 +845,8 @@ object DottyLanguageServer { } def markupContent(content: String): lsp4j.MarkupContent = { - if (content.isEmpty) null + if content.isEmpty + null else { val markup = new lsp4j.MarkupContent markup.setKind("markdown") diff --git a/language-server/src/dotty/tools/languageserver/Memory.scala b/language-server/src/dotty/tools/languageserver/Memory.scala index a2869614e276..5de6ddbcb695 100644 --- a/language-server/src/dotty/tools/languageserver/Memory.scala +++ b/language-server/src/dotty/tools/languageserver/Memory.scala @@ -31,7 +31,7 @@ object Memory { def free = runtime.freeMemory def used = total - free def usedIsCloseToMax = - if (maximal == Long.MaxValue) free.toDouble / used < FreeThreshold + if maximal == Long.MaxValue then free.toDouble / used < FreeThreshold else used.toDouble / maximal > UsedThreshold usedIsCloseToMax && { runtime.gc(); usedIsCloseToMax } } diff --git a/tests/neg/indent.scala b/tests/neg/indent.scala new file mode 100644 index 000000000000..b38ff81221f1 --- /dev/null +++ b/tests/neg/indent.scala @@ -0,0 +1,9 @@ +object Test { + + def (x: Int) gt (y: Int) = x > y + val y3 = + if (1) max 10 gt 0 // error: end of statement expected but integer literal found // error // error // error + 1 + else // error: end of statement expected but 'else' found + 2 // error +} diff --git a/tests/pos/indent.scala b/tests/pos/indent.scala index e77f16b79134..c15a85cb4e82 100644 --- a/tests/pos/indent.scala +++ b/tests/pos/indent.scala @@ -18,6 +18,25 @@ object Test else println("world") 33 + val y1 = + if x > 0 + 1 + else + 2 + val y2 = + if (y > 0) && y < 0 + 1 + else + 2 + def (x: Int) gt (y: Int) = x > y + val y3 = + if (1) max 10 gt 0 + + then + 1 + else + 2 + if (true) println (2) val z = 22 x + y + z end f From b1e3f30bae7160980c439b525ebf1a65a1aa8907 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 23 Sep 2019 11:33:22 +0200 Subject: [PATCH 2/6] Make use of isNewLine and isIdent methods in Scanner --- .../dotty/tools/dotc/parsing/Parsers.scala | 24 +++++++------------ .../dotty/tools/dotc/parsing/Scanners.scala | 1 + 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 19f4aff10559..f11a0b685b11 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -181,7 +181,7 @@ object Parsers { /* -------------- TOKEN CLASSES ------------------------------------------- */ - def isIdent = in.token == IDENTIFIER || in.token == BACKQUOTED_IDENT + def isIdent = in.isIdent def isIdent(name: Name) = in.token == IDENTIFIER && in.name == name def isSimpleLiteral = simpleLiteralTokens contains in.token def isLiteral = literalTokens contains in.token @@ -215,8 +215,7 @@ object Parsers { (allowedMods `contains` in.token) || in.isSoftModifierInModifierPosition && !excludedSoftModifiers.contains(in.name) - def isStatSep: Boolean = - in.token == NEWLINE || in.token == NEWLINES || in.token == SEMI + def isStatSep: Boolean = in.isNewLine || in.token == SEMI /** A '$' identifier is treated as a splice if followed by a `{`. * A longer identifier starting with `$` is treated as a splice/id combination @@ -341,10 +340,8 @@ object Parsers { /** semi = nl {nl} | `;' * nl = `\n' // where allowed */ - def acceptStatSep(): Unit = in.token match { - case NEWLINE | NEWLINES => in.nextToken() - case _ => accept(SEMI) - } + def acceptStatSep(): Unit = + if in.isNewLine then in.nextToken() else accept(SEMI) def acceptStatSepUnlessAtEnd(altEnd: Token = EOF): Unit = if (!isStatSeqEnd) @@ -603,9 +600,7 @@ object Parsers { val t = body() // Therefore, make sure there would be a matching def nextIndentWidth = in.indentWidth(in.next.offset) - if (in.token == NEWLINE || in.token == NEWLINES) - && !(nextIndentWidth < startIndentWidth) - then + if in.isNewLine && !(nextIndentWidth < startIndentWidth) then warning( if startIndentWidth <= nextIndentWidth then i"""Line is indented too far to the right, or a `{' is missing before: @@ -623,7 +618,7 @@ object Parsers { * statement that's indented relative to the current region. */ def checkNextNotIndented(): Unit = in.currentRegion match - case r: InBraces if in.token == NEWLINE || in.token == NEWLINES => + case r: InBraces if in.isNewLine => val nextIndentWidth = in.indentWidth(in.next.offset) if r.indentWidth < nextIndentWidth then warning(i"Line is indented too far to the right, or a `{' is missing", in.next.offset) @@ -876,7 +871,7 @@ object Parsers { } if (lookahead.token == LARROW) false // it's a pattern - else if (lookahead.token != IDENTIFIER && lookahead.token != BACKQUOTED_IDENT) + else if (lookahead.isIdent) true // it's not a pattern since token cannot be an infix operator else followedByToken(LARROW) // `<-` comes before possible statement starts @@ -904,7 +899,7 @@ object Parsers { */ def followingIsGivenSig() = val lookahead = in.LookaheadScanner() - if lookahead.token == IDENTIFIER || lookahead.token == BACKQUOTED_IDENT then + if lookahead.isIdent then lookahead.nextToken() while lookahead.token == LPAREN || lookahead.token == LBRACKET do lookahead.skipParens() @@ -1230,8 +1225,7 @@ object Parsers { if (in.token == NEWLINE) in.nextToken() def newLinesOpt(): Unit = - if (in.token == NEWLINE || in.token == NEWLINES) - in.nextToken() + if in.isNewLine then in.nextToken() def newLineOptWhenFollowedBy(token: Int): Unit = // note: next is defined here because current == NEWLINE diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 6c79803db1f0..ee8f532cd266 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -1006,6 +1006,7 @@ object Scanners { isSoftModifier && !lookaheadIn(BitSet(COLON)) def isNewLine = token == NEWLINE || token == NEWLINES + def isIdent = token == IDENTIFIER || token == BACKQUOTED_IDENT def isNestedStart = token == LBRACE || token == INDENT def isNestedEnd = token == RBRACE || token == OUTDENT From f33ade701adc3cb3bff70e97549a6058974672c5 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 23 Sep 2019 11:36:15 +0200 Subject: [PATCH 3/6] Make treatment of `do` language version dependent `do` can start an expression or a statement only under -language:Scala2. Maintaining this distinction is important in order not to insert spurious newlines in front of `do` in a `while` or `for`. --- .../dotty/tools/dotc/parsing/Parsers.scala | 34 ++++++++------- .../dotty/tools/dotc/parsing/Scanners.scala | 41 +++++++++---------- .../src/dotty/tools/dotc/parsing/Tokens.scala | 14 +++---- compiler/src/dotty/tools/dotc/util/kwords.sc | 4 +- tests/pos/indent4.scala | 11 +++++ 5 files changed, 59 insertions(+), 45 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index f11a0b685b11..c8d048383b61 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -207,7 +207,7 @@ object Parsers { } && !in.isSoftModifierInModifierPosition def isExprIntro: Boolean = - canStartExpressionTokens.contains(in.token) && !in.isSoftModifierInModifierPosition + in.canStartExprTokens.contains(in.token) && !in.isSoftModifierInModifierPosition def isDefIntro(allowedMods: BitSet, excludedSoftModifiers: Set[TermName] = Set.empty): Boolean = in.token == AT || @@ -1231,6 +1231,10 @@ object Parsers { // note: next is defined here because current == NEWLINE if (in.token == NEWLINE && in.next.token == token) in.nextToken() + def newLinesOptWhenFollowedBy(name: Name): Unit = + if in.isNewLine && in.next.token == IDENTIFIER && in.next.name == name then + in.nextToken() + def newLineOptWhenFollowing(p: Int => Boolean): Unit = // note: next is defined here because current == NEWLINE if (in.token == NEWLINE && p(in.next.token)) newLineOpt() @@ -1246,7 +1250,7 @@ object Parsers { } def possibleTemplateStart(): Unit = { - in.observeIndented(unless = noIndentTemplateTokens, unlessSoftKW = nme.derives) + in.observeIndented() newLineOptWhenFollowedBy(LBRACE) } @@ -1651,7 +1655,7 @@ object Parsers { def toBeContinued(altToken: Token): Boolean = if in.token == altToken || in.isNewLine || in.isScala2Mode then false // a newline token means the expression is finished - else if !canStartStatTokens.contains(in.token) + else if !in.canStartStatTokens.contains(in.token) || in.isLeadingInfixOperator(inConditional = true) then true @@ -1852,20 +1856,20 @@ object Parsers { } } case _ => - if (isIdent(nme.inline) && !in.inModifierPosition() && in.lookaheadIn(canStartExpressionTokens)) { + if isIdent(nme.inline) + && !in.inModifierPosition() + && in.lookaheadIn(in.canStartExprTokens) + then val start = in.skipToken() - in.token match { + in.token match case IF => ifExpr(start, InlineIf) case _ => val t = postfixExpr() if (in.token == MATCH) matchExpr(t, start, InlineMatch) - else { + else syntaxErrorOrIncomplete(i"`match` or `if` expected but ${in.token} found") t - } - } - } else expr1Rest(postfixExpr(), location) } @@ -2012,7 +2016,7 @@ object Parsers { def postfixExpr(): Tree = postfixExprRest(prefixExpr()) def postfixExprRest(t: Tree): Tree = - infixOps(t, canStartExpressionTokens, prefixExpr, maybePostfix = true) + infixOps(t, in.canStartExprTokens, prefixExpr, maybePostfix = true) /** PrefixExpr ::= [`-' | `+' | `~' | `!'] SimpleExpr */ @@ -2209,7 +2213,7 @@ object Parsers { lookahead.nextToken() lookahead.token != COLON } - else canStartExpressionTokens.contains(lookahead.token) + else in.canStartExprTokens.contains(lookahead.token) } } if (in.token == LPAREN && (!inClassConstrAnnots || isLegalAnnotArg)) @@ -2338,7 +2342,7 @@ object Parsers { dropParensOrBraces(start, if (in.token == YIELD || in.token == DO) "" else "do") } } - in.observeIndented(unless = noIndentAfterEnumeratorTokens) + in.observeIndented() res } else { @@ -2484,7 +2488,7 @@ object Parsers { /** InfixPattern ::= SimplePattern {id [nl] SimplePattern} */ def infixPattern(): Tree = - infixOps(simplePattern(), canStartExpressionTokens, simplePattern, isOperator = in.name != nme.raw.BAR) + infixOps(simplePattern(), in.canStartExprTokens, simplePattern, isOperator = in.name != nme.raw.BAR) /** SimplePattern ::= PatVar * | Literal @@ -3470,6 +3474,7 @@ object Parsers { /** Template ::= InheritClauses [TemplateBody] */ def template(constr: DefDef, isEnum: Boolean = false): Template = { + newLinesOptWhenFollowedBy(nme.derives) val (parents, derived) = inheritClauses() possibleTemplateStart() if (isEnum) { @@ -3482,10 +3487,11 @@ object Parsers { /** TemplateOpt = [Template] */ def templateOpt(constr: DefDef): Template = - possibleTemplateStart() + newLinesOptWhenFollowedBy(nme.derives) if in.token == EXTENDS || isIdent(nme.derives) then template(constr) else + possibleTemplateStart() if in.isNestedStart then template(constr) else diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index ee8f532cd266..b5e45898bcef 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -385,7 +385,7 @@ object Scanners { lookahead.allowLeadingInfixOperators = false // force a NEWLINE a after current token if it is on its own line lookahead.nextToken() - canStartExpressionTokens.contains(lookahead.token) + canStartExprTokens.contains(lookahead.token) } && { if isScala2Mode || oldSyntax && !rewrite then @@ -495,10 +495,11 @@ object Scanners { indentPrefix = LBRACE case _ => } - if (newlineIsSeparating && - canEndStatTokens.contains(lastToken) && - canStartStatTokens.contains(token) && - !isLeadingInfixOperator()) + if newlineIsSeparating + && canEndStatTokens.contains(lastToken) + && canStartStatTokens.contains(token) + && !isLeadingInfixOperator() + then insert(if (pastBlankLine) NEWLINES else NEWLINE, lineOffset) else if indentIsSignificant then if nextWidth < lastWidth @@ -541,29 +542,19 @@ object Scanners { |Previous indent : $lastWidth |Latest indent : $nextWidth""" - def observeIndented( - unless: BitSet = BitSet.empty, - unlessSoftKW: TermName = EmptyTermName): Unit - = - if (indentSyntax && isAfterLineEnd && token != INDENT) { - val nextOffset = if (isNewLine) next.offset else offset - val nextToken = if (isNewLine) next.token else token - val nextWidth = indentWidth(nextOffset) - val lastWidth = currentRegion match { + def observeIndented(): Unit = + if indentSyntax && isNewLine then + val nextWidth = indentWidth(next.offset) + val lastWidth = currentRegion match case r: Indented => r.width case r: InBraces => r.width case _ => nextWidth - } - if (lastWidth < nextWidth - && !unless.contains(nextToken) - && (unlessSoftKW.isEmpty || token != IDENTIFIER || name != unlessSoftKW)) { + if lastWidth < nextWidth then currentRegion = Indented(nextWidth, Set(), COLONEOL, currentRegion) - if (!isNewLine) next.copyFrom(this) - offset = nextOffset + offset = next.offset token = INDENT - } - } + end observeIndented /** - Join CASE + CLASS => CASECLASS, CASE + OBJECT => CASEOBJECT, SEMI + ELSE => ELSE, COLON + => COLONEOL * - Insert missing OUTDENTs at EOF @@ -1011,6 +1002,12 @@ object Scanners { def isNestedStart = token == LBRACE || token == INDENT def isNestedEnd = token == RBRACE || token == OUTDENT + def canStartStatTokens = + if isScala2Mode then canStartStatTokens2 else canStartStatTokens3 + + def canStartExprTokens = + if isScala2Mode then canStartExprTokens2 else canStartExprTokens3 + // Literals ----------------------------------------------------------------- private def getStringLit() = { diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 92684c9c3293..72bde459335a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -219,8 +219,10 @@ object Tokens extends TokensCommon { final val atomicExprTokens: TokenSet = literalTokens | identifierTokens | BitSet( USCORE, NULL, THIS, SUPER, TRUE, FALSE, RETURN, QUOTEID, XMLSTART) - final val canStartExpressionTokens: TokenSet = atomicExprTokens | BitSet( - LBRACE, LPAREN, INDENT, QUOTE, IF, DO, WHILE, FOR, NEW, TRY, THROW) + final val canStartExprTokens3: TokenSet = atomicExprTokens | BitSet( + LBRACE, LPAREN, INDENT, QUOTE, IF, WHILE, FOR, NEW, TRY, THROW) + + final val canStartExprTokens2: TokenSet = canStartExprTokens3 | BitSet(DO) final val canStartTypeTokens: TokenSet = literalTokens | identifierTokens | BitSet( THIS, SUPER, USCORE, LPAREN, AT) @@ -247,7 +249,9 @@ object Tokens extends TokensCommon { /** Is token only legal as start of statement (eof also included)? */ final val mustStartStatTokens: TokenSet = defIntroTokens | modifierTokens | BitSet(IMPORT, EXPORT, PACKAGE) - final val canStartStatTokens: TokenSet = canStartExpressionTokens | mustStartStatTokens | BitSet( + final val canStartStatTokens2: TokenSet = canStartExprTokens2 | mustStartStatTokens | BitSet( + AT, CASE) + final val canStartStatTokens3: TokenSet = canStartExprTokens3 | mustStartStatTokens | BitSet( AT, CASE) final val canEndStatTokens: TokenSet = atomicExprTokens | BitSet( @@ -280,10 +284,6 @@ object Tokens extends TokensCommon { */ final val startParamOrGivenTypeTokens: BitSet = startParamTokens | BitSet(GIVEN, ERASED) - final val noIndentTemplateTokens = BitSet(EXTENDS) - final val noIndentAfterConditionTokens = BitSet(THEN, DO) - final val noIndentAfterEnumeratorTokens = BitSet(YIELD, DO) - final val scala3keywords = BitSet(ENUM, ERASED, GIVEN) final val softModifierNames = Set(nme.inline, nme.opaque) diff --git a/compiler/src/dotty/tools/dotc/util/kwords.sc b/compiler/src/dotty/tools/dotc/util/kwords.sc index 94c17eaf455e..961be3b0aa23 100644 --- a/compiler/src/dotty/tools/dotc/util/kwords.sc +++ b/compiler/src/dotty/tools/dotc/util/kwords.sc @@ -13,6 +13,6 @@ object kwords { //| atch, lazy, then, forSome, _, :, =, <-, =>, ';', ';', <:, >:, #, @, <%) keywords.toList.filter(kw => tokenString(kw) == null) //> res1: List[Int] = List() - canStartStatTokens contains CASE //> res2: Boolean = false - + canStartStatTokens3 contains CASE //> res2: Boolean = false + } \ No newline at end of file diff --git a/tests/pos/indent4.scala b/tests/pos/indent4.scala index 50eef34ff3e7..415e4637adfd 100644 --- a/tests/pos/indent4.scala +++ b/tests/pos/indent4.scala @@ -22,3 +22,14 @@ object testindent val y = x println(y) + while true + do println(1) + + for i <- List(1, 2, 3) + do println(i) + + while (true) + do println(1) + + for (i <- List(1, 2, 3)) + do println(i) From 3463d17b36ed3a3a6d3bf02282e4accb458ded98 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 23 Sep 2019 14:51:30 +0200 Subject: [PATCH 4/6] Check indentation also at TopLevel Issue "too far to the right" errors also at toplevel. Previously this was done only inside braces. --- .../src/dotty/tools/dotc/parsing/Parsers.scala | 2 +- .../src/dotty/tools/dotc/parsing/Scanners.scala | 14 +++++++++----- tests/neg-custom-args/indentRight.scala | 5 +++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index c8d048383b61..018fa0cb7658 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -618,7 +618,7 @@ object Parsers { * statement that's indented relative to the current region. */ def checkNextNotIndented(): Unit = in.currentRegion match - case r: InBraces if in.isNewLine => + case r: IndentSignificantRegion if in.isNewLine => val nextIndentWidth = in.indentWidth(in.next.offset) if r.indentWidth < nextIndentWidth then warning(i"Line is indented too far to the right, or a `{' is missing", in.next.offset) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index b5e45898bcef..eedaacd5cc6d 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -546,8 +546,7 @@ object Scanners { if indentSyntax && isNewLine then val nextWidth = indentWidth(next.offset) val lastWidth = currentRegion match - case r: Indented => r.width - case r: InBraces => r.width + case r: IndentSignificantRegion => r.indentWidth case _ => nextWidth if lastWidth < nextWidth then @@ -1331,12 +1330,16 @@ object Scanners { def enclosing: Region = outer.asInstanceOf[Region] /** If this is an InBraces or Indented region, its indentation width, or Zero otherwise */ - def indentWidth = IndentWidth.Zero + def indentWidth: IndentWidth = IndentWidth.Zero } case class InString(multiLine: Boolean, outer: Region) extends Region case class InParens(prefix: Token, outer: Region) extends Region - case class InBraces(var width: IndentWidth | Null, outer: Region) extends Region { + + abstract class IndentSignificantRegion extends Region + + case class InBraces(var width: IndentWidth | Null, outer: Region) + extends IndentSignificantRegion { override def indentWidth = width } @@ -1345,7 +1348,8 @@ object Scanners { * @param others Other indendation widths > width of lines in the same region * @param prefix The token before the initial of the region */ - case class Indented(width: IndentWidth, others: Set[IndentWidth], prefix: Token, outer: Region | Null) extends Region { + case class Indented(width: IndentWidth, others: Set[IndentWidth], prefix: Token, outer: Region | Null) + extends IndentSignificantRegion { override def indentWidth = width } diff --git a/tests/neg-custom-args/indentRight.scala b/tests/neg-custom-args/indentRight.scala index f852208d339d..f00bec12fbff 100644 --- a/tests/neg-custom-args/indentRight.scala +++ b/tests/neg-custom-args/indentRight.scala @@ -1,3 +1,7 @@ +trait A + case class B() extends A // error: Line is indented too far to the right + case object C extends A // error: Line is indented too far to the right + object Test { if (true) @@ -27,3 +31,4 @@ object Test { println("!") // error: expected a toplevel definition } + From 912c672cb4fd9f0d7e0d7ed194b884007348a2ee Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 23 Sep 2019 15:04:14 +0200 Subject: [PATCH 5/6] Update docs - Update current reference docs to what's implemented in 0.19. - Add links to new pages describing what will come in 0.20. --- .../other-new-features/control-syntax-new.md | 43 +++ .../other-new-features/control-syntax.md | 3 + .../other-new-features/indentation-new.md | 249 ++++++++++++++++++ .../other-new-features/indentation.md | 87 +++--- docs/sidebar.yml | 2 +- 5 files changed, 346 insertions(+), 38 deletions(-) create mode 100644 docs/docs/reference/other-new-features/control-syntax-new.md create mode 100644 docs/docs/reference/other-new-features/indentation-new.md diff --git a/docs/docs/reference/other-new-features/control-syntax-new.md b/docs/docs/reference/other-new-features/control-syntax-new.md new file mode 100644 index 000000000000..e6d1a2bb2d46 --- /dev/null +++ b/docs/docs/reference/other-new-features/control-syntax-new.md @@ -0,0 +1,43 @@ +--- +layout: doc-page +title: New Control Syntax +--- + +Scala 3 has a new "quiet" syntax for control expressions that does not rely in +enclosing the condition in parentheses, and also allows to drop parentheses or braces +around the generators of a `for`-expression. Examples: +```scala +if x < 0 + "negative" +else if x == 0 + "zero" +else + "positive" + +if x < 0 then -x else x + +while x >= 0 do x = f(x) + +for x <- xs if x > 0 +yield x * x + +for + x <- xs + y <- ys +do + println(x + y) +``` + +The rules in detail are: + + - The condition of an `if`-expression can be written without enclosing parentheses if it is followed by a `then` + or some [indented](./indentation.html) code on a following line. + - The condition of a `while`-loop can be written without enclosing parentheses if it is followed by a `do`. + - The enumerators of a `for`-expression can be written without enclosing parentheses or braces if they are followed by a `yield` or `do`. + - A `do` in a `for`-expression expresses a `for`-loop. + + +### Rewrites + +The Dotty compiler can rewrite source code from old syntax and new syntax and back. +When invoked with options `-rewrite -new-syntax` it will rewrite from old to new syntax, dropping parentheses and braces in conditions and enumerators. When invoked with with options `-rewrite -old-syntax` it will rewrite in the reverse direction, inserting parentheses and braces as needed. diff --git a/docs/docs/reference/other-new-features/control-syntax.md b/docs/docs/reference/other-new-features/control-syntax.md index 455474171eca..49c9aa97b5f0 100644 --- a/docs/docs/reference/other-new-features/control-syntax.md +++ b/docs/docs/reference/other-new-features/control-syntax.md @@ -3,6 +3,9 @@ layout: doc-page title: New Control Syntax --- +**Note** The syntax described in this section is currently under revision. +[Here is the new version which will be implemented in Dotty 0.20](./control-syntax-new.html). + Scala 3 has a new "quiet" syntax for control expressions that does not rely in enclosing the condition in parentheses, and also allows to drop parentheses or braces around the generators of a `for`-expression. Examples: diff --git a/docs/docs/reference/other-new-features/indentation-new.md b/docs/docs/reference/other-new-features/indentation-new.md new file mode 100644 index 000000000000..0b9c710431a4 --- /dev/null +++ b/docs/docs/reference/other-new-features/indentation-new.md @@ -0,0 +1,249 @@ +--- +layout: doc-page +title: Optional Braces +--- + +As an experimental feature, Scala 3 treats indentation as significant and allows +some occurrences of braces `{...}` to be optional. + + - First, some badly indented programs are ruled out, which means they are flagged with warnings. + - Second, some occurrences of braces `{...}` are made optional. Generally, the rule + is that adding a pair of optional braces will not change the meaning of a well-indented program. + +### Indentation Rules + +The compiler enforces three rules for well-indented programs, flagging violations as warnings. + + 1. In a brace-delimited region, no statement is allowed to start to the left + of the first statement after the opening brace that starts a new line. + + This rule is helpful for finding missing closing braces. It prevents errors like: + + ```scala + if (x < 0) { + println(1) + println(2) + + println("done") // error: indented too far to the left + ``` + + 2. If significant indentation is turned off (i.e. under Scala-2 mode or under `-noindent`) and we are at the start of an indented sub-part of an expression, and the indented part ends in a newline, the next statement must start at an indentation width less than the sub-part. This prevents errors where an opening brace was forgotten, as in + + ```scala + if (x < 0) + println(1) + println(2) // error: missing `{` + ``` + + 3. If significant indentation is turned off, code that follows a class or object definition (or similar) lacking a `{...}` body may not be indented more than that definition. This prevents misleading situations like: + + ```scala + trait A + case class B() extends A // error: indented too far to the right + ``` + It requires that the case class `C` to be written instead at the same level of indentation as the trait `A`. + +These rules still leave a lot of leeway how programs should be indented. For instance, they do not impose +any restrictions on indentation within expressions, nor do they require that all statements of an indentation block line up exactly. + +The rules are generally helpful in pinpointing the root cause of errors related to missing opening or closing braces. These errors are often quite hard to diagnose, in particular in large programs. + +### Optional Braces + +The compiler will insert `` or `` +tokens at certain line breaks. Grammatically, pairs of `` and `` tokens have the same effect as pairs of braces `{` and `}`. + +The algorithm makes use of a stack `IW` of previously encountered indentation widths. The stack initially holds a single element with a zero indentation width. The _current indentation width_ is the indentation width of the top of the stack. + +There are two rules: + + 1. An `` is inserted at a line break, if + + - An indentation region can start at the current position in the source, and + - the first token on the next line has an indentation width strictly greater + than the current indentation width + + An indentation region can start + + - after the condition of an `if-else`, or + - at points where a set of definitions enclosed in braces is expected in a + class, object, given, or enum definition, in an enum case, or after a package clause, or + - after one of the following tokens: + ``` + = => <- if then else while do try catch finally for yield match + ``` + If an `` is inserted, the indentation width of the token on the next line + is pushed onto `IW`, which makes it the new current indentation width. + + 2. An `` is inserted at a line break, if + + - the first token on the next line has an indentation width strictly less + than the current indentation width, and + - the first token on the next line is not a + [leading infix operator](../changed-features/operators.html). + + If an `` is inserted, the top element if popped from `IW`. + 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 `` tokens + may be inserted in a row. + +It is an error if the indentation width of the token following an `` 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 + else // error: `else` does not align correctly + x +``` +Indentation tokens are only inserted in regions where newline statement separators are also inferred: +at the toplevel, inside braces `{...}`, but not inside parentheses `(...)`, patterns or types. + +### Spaces vs Tabs + +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. + +### Indentation and Braces + +Indentation can be mixed freely with braces. For interpreting indentation inside braces, the following rules apply. + + 1. The assumed indentation width of a multiline region enclosed in braces is the + indentation width of the first token that starts a new line after the opening brace. + + 2. On encountering a closing brace `}`, as many `` tokens as necessary are + inserted to close all open indentation regions inside the pair of braces. + +### Special Treatment of Case Clauses + +The indentation rules for `match` expressions and `catch` clauses are refined as follows: + + - An indentation region is opened after a `match` or `catch` also if the following `case` + appears at the indentation width that's current for the `match` itself. + - In that case, the indentation region closes at the first token at that + same indentation width that is not a `case`, or at any token with a smaller + indentation width, whichever comes first. + +The rules allow to write `match` expressions where cases are not indented themselves, as in the example below: +```scala +x match +case 1 => print("I") +case 2 => print("II") +case 3 => print("III") +case 4 => print("IV") +case 5 => print("V") + +println(".") +``` + +### The End Marker + +Indentation-based syntax has many advantages over other conventions. But one possible problem is that it makes it hard to discern when a large indentation region ends, since there is no specific token that delineates the end. Braces are not much better since a brace by itself also contains no information about what region is closed. + +To solve this problem, Scala 3 offers an optional `end` marker. Example +```scala +def largeMethod(...) = + ... + if ... then ... + else + ... // a large block + end if + ... // more code +end largeMethod +``` +An `end` marker consists of the identifier `end` which follows an `` token, and is in turn followed on the same line by exactly one other token, which is either an identifier or one of the reserved words +```scala +if while for match try new +``` +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 +that defines _id_ or a package clause that refers to _id_. + +`end` itself is a soft keyword. It is only treated as an `end` marker if it +occurs at the start of a line and is followed by an identifier or one of the reserved words above. + +It is recommended that `end` markers are used for code where the extent of an indentation region is not immediately apparent "at a glance". Typically this is the case if an indentation region spans 20 lines or more. + +### Example + +Here is a (somewhat meta-circular) example of code using indentation. It provides a concrete representation of indentation widths as defined above together with efficient operations for constructing and comparing indentation widths. + +```scala +enum IndentWidth + case Run(ch: Char, n: Int) + case Conc(l: IndentWidth, r: Run) + + def <= (that: IndentWidth): Boolean = + this match + case Run(ch1, n1) => + that match + case Run(ch2, n2) => n1 <= n2 && (ch1 == ch2 || n1 == 0) + case Conc(l, r) => this <= l + case Conc(l1, r1) => + that match + case Conc(l2, r2) => l1 == l2 && r1 <= r2 + case _ => false + + def < (that: IndentWidth): Boolean = + this <= that && !(that <= this) + + override def toString: String = + this match + case Run(ch, n) => + val kind = ch match + case ' ' => "space" + case '\t' => "tab" + case _ => s"'$ch'-character" + val suffix = if n == 1 then "" else "s" + s"$n $kind$suffix" + case Conc(l, r) => + s"$l, $r" + +object IndentWidth + private inline val MaxCached = 40 + + private val spaces = IArray.tabulate(MaxCached + 1)(new Run(' ', _)) + private val tabs = IArray.tabulate(MaxCached + 1)(new Run('\t', _)) + + def Run(ch: Char, n: Int): Run = + if n <= MaxCached && ch == ' ' + spaces(n) + else if n <= MaxCached && ch == '\t' + tabs(n) + else + new Run(ch, n) + end Run + + val Zero = Run(' ', 0) +end IndentWidth +``` + +### Settings and Rewrites + +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 a warning. + +The Dotty compiler can rewrite source code to indented code and back. +When invoked with options `-rewrite -indent` it will rewrite braces to +indented regions where possible. When invoked with with options `-rewrite -noindent` it will rewrite in the reverse direction, inserting braces for indentation regions. +The `-indent` option only works on [new-style syntax](./control-syntax.html). So to go from old-style syntax to new-style indented code one has to invoke the compiler twice, first with options `-rewrite -new-syntax`, then again with options +`-rewrite-indent`. To go in the opposite direction, from indented code to old-style syntax, it's `-rewrite -noindent`, followed by `-rewrite -old-syntax`. + +### Variant: Indentation Marker `:` + +Generally, the possible indentation regions coincide with those regions where braces `{...}` are also legal, no matter whether the braces enclose an expression or a set of definitions. There is one exception, though: Arguments to function can be enclosed in braces but they cannot be simply indented instead. Making indentation always significant for function arguments would be too restrictive and fragile. + +To allow such arguments to be written without braces, a variant of the indentation scheme is implemented under +option `-Yindent-colons`. This variant is more contentious and less stable than the rest of the significant indentation scheme. In this variant, a colon `:` at the end of a line is also one of the possible tokens that opens an indentation region. Examples: + +```scala +times(10): + println("ah") + println("ha") +``` +or +```scala +xs.map: + x => + val y = x - 1 + y * y +``` +Colons at the end of lines are their own token, distinct from normal `:`. +The Scala grammar is changed in this variant so that colons at end of lines are accepted at all points +where an opening brace enclosing a function argument is legal. 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. + diff --git a/docs/docs/reference/other-new-features/indentation.md b/docs/docs/reference/other-new-features/indentation.md index 3b6ef4dfd504..054176d21428 100644 --- a/docs/docs/reference/other-new-features/indentation.md +++ b/docs/docs/reference/other-new-features/indentation.md @@ -1,11 +1,15 @@ --- layout: doc-page -title: Significant Indentation +title: Optional Braces --- -As an experimental feature, Scala 3 treats indentation as significant. Indentation is significant everywhere except inside regions delineated by brackets `[...]` or parentheses `(...)`. +**Note** The syntax described in this section is currently under revision. +[Here is the new version which will be implemented in Dotty 0.20](./indentation-new.html). -Where indentation is significant, the compiler will insert `` or `` +As an experimental feature, Scala 3 treats indentation as significant and allows +some occurrences of braces `{...}` to be optional. + +The compiler will insert `` or `` tokens at certain line breaks. Grammatically, pairs of `` and `` tokens have the same effect as pairs of braces `{` and `}`. The algorithm makes use of a stack `IW` of previously encountered indentation widths. The stack initially holds a single element with a zero indentation width. The _current indentation width_ is the indentation width of the top of the stack. @@ -14,17 +18,20 @@ There are two rules: 1. An `` is inserted at a line break, if + - An indentation region can start at the current position in the source, and - the first token on the next line has an indentation width strictly greater - than the current indentation width, and - - the last token on the previous line can start an indentation region. + than the current indentation width + + An indentation region can start - The following tokens can start an indentation region: + - at points where a set of definitions enclosed in braces is expected in a + class, object, given, or enum definition, in an enum case, or after a package clause, or + - after one of the following tokens: ``` - : = => <- if then else while do try catch finally for yield match + = => <- if then else while do try catch finally for yield match ``` - - If an `` is inserted, the indentation width of the token on the next line - is pushed onto `IW`, which makes it the new current indentation width. + If an `` is inserted, the indentation width of the token on the next line + is pushed onto `IW`, which makes it the new current indentation width. 2. An `` is inserted at a line break, if @@ -45,6 +52,8 @@ if x < 0 then else // error: `else` does not align correctly x ``` +Indentation tokens are only inserted in regions where newline statement separators are also inferred: +at the toplevel, inside braces `{...}`, but not inside parentheses `(...)`, patterns or types. ### Spaces vs Tabs @@ -60,26 +69,6 @@ Indentatation can be mixed freely with braces. For interpreting indentation ins 2. On encountering a closing brace `}`, as many `` tokens as necessary are inserted to close all open indentation regions inside the pair of braces. -### Indentation Marker `:` - -A colon `:` at the end of a line is one of the possible tokens that opens an indentation region. Examples: - -```scala -times(10): - println("ah") - println("ha") -``` -or -```scala -xs.map: - x => - val y = x - 1 - y * y -``` -Colons at the end of lines are their own token, distinct from normal `:`. -The Scala grammar is changed so that colons at end of lines are accepted at all points -where an opening brace is legal, except if the previous token can already start an -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. ### Special Treatment of Case Clauses @@ -135,7 +124,7 @@ It is recommended that `end` markers are used for code where the extent of an in Here is a (somewhat meta-circular) example of code using indentation. It provides a concrete representation of indentation widths as defined above together with efficient operations for constructing and comparing indentation widths. ```scala -enum IndentWidth: +enum IndentWidth case Run(ch: Char, n: Int) case Conc(l: IndentWidth, r: Run) @@ -150,7 +139,8 @@ enum IndentWidth: case Conc(l2, r2) => l1 == l2 && r1 <= r2 case _ => false - def < (that: IndentWidth): Boolean = this <= that && !(that <= this) + def < (that: IndentWidth): Boolean = + this <= that && !(that <= this) override def toString: String = this match @@ -164,13 +154,11 @@ enum IndentWidth: case Conc(l, r) => s"$l, $r" -object IndentWidth: +object IndentWidth private inline val MaxCached = 40 - private val spaces = IArray.tabulate(MaxCached + 1): - new Run(' ', _) - private val tabs = IArray.tabulate(MaxCached + 1): - new Run('\t', _) + private val spaces = IArray.tabulate(MaxCached + 1)(new Run(' ', _)) + private val tabs = IArray.tabulate(MaxCached + 1)(new Run('\t', _)) def Run(ch: Char, n: Int): Run = if n <= MaxCached && ch == ' ' then @@ -182,6 +170,7 @@ object IndentWidth: end Run val Zero = Run(' ', 0) +end IndentWidth ``` ### Settings and Rewrites @@ -193,3 +182,27 @@ When invoked with options `-rewrite -indent` it will rewrite braces to indented regions where possible. When invoked with with options `-rewrite -noindent` it will rewrite in the reverse direction, inserting braces for indentation regions. The `-indent` option only works on [new-style syntax](./control-syntax.html). So to go from old-style syntax to new-style indented code one has to invoke the compiler twice, first with options `-rewrite -new-syntax`, then again with options `-rewrite-indent`. To go in the opposite direction, from indented code to old-style syntax, it's `-rewrite -noindent`, followed by `-rewrite -old-syntax`. + +### Variant: Indentation Marker `:` + +Generally, the possible indentation regions coincide with those regions where braces `{...}` are also legal, no matter whether the braces enclose an expression or a set of definitions. There is one exception, though: Arguments to function can be enclosed in braces but they cannot be simply indented instead. Making indentation always significant for function arguments would be too restrictive and fragile. + +To allow such arguments to be written without braces, a variant of the indentation scheme is implemented under +option `-Yindent-colons`. This variant is more contentious and less stable than the rest of the significant indentation scheme. In this variant, a colon `:` at the end of a line is also one of the possible tokens that opens an indentation region. Examples: + +```scala +times(10): + println("ah") + println("ha") +``` +or +```scala +xs.map: + x => + val y = x - 1 + y * y +``` +Colons at the end of lines are their own token, distinct from normal `:`. +The Scala grammar is changed in this variant so that colons at end of lines are accepted at all points +where an opening brace enclosing a function argument is legal. 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. + diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 05a7bb5e49e6..ff9cf1912bb5 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -103,7 +103,7 @@ sidebar: url: docs/reference/other-new-features/threadUnsafe-annotation.html - title: New Control Syntax url: docs/reference/other-new-features/control-syntax.html - - title: Significant Indentation + - title: Optional Braces url: docs/reference/other-new-features/indentation.html - title: Other Changed Features subsection: From 4d470ddd90f3e8265ca58ab73c5b2f27e5bf930a Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 25 Sep 2019 17:41:53 +0200 Subject: [PATCH 6/6] Address review comments --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 8 ++++---- .../reference/other-new-features/control-syntax-new.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 018fa0cb7658..a9f9750408f6 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1665,8 +1665,8 @@ object Parsers { def condExpr(altToken: Token): Tree = if in.token == LPAREN then var t: Tree = atSpan(in.offset) { Parens(inParens(exprInParens())) } - val newSyntax = toBeContinued(altToken) - if newSyntax then + val enclosedInParens = !toBeContinued(altToken) + if !enclosedInParens then t = inSepRegion(LBRACE, RBRACE) { expr1Rest(postfixExprRest(simpleExprRest(t)), Location.ElseWhere) } @@ -1674,9 +1674,9 @@ object Parsers { if rewriteToOldSyntax() then revertToParens(t) in.nextToken() else - if (altToken == THEN || !newSyntax) && in.isNewLine then + if (altToken == THEN || enclosedInParens) && in.isNewLine then in.observeIndented() - if newSyntax && in.token != INDENT then accept(altToken) + if !enclosedInParens && in.token != INDENT then accept(altToken) if (rewriteToNewSyntax(t.span)) dropParensOrBraces(t.span.start, s"${tokenString(altToken)}") t diff --git a/docs/docs/reference/other-new-features/control-syntax-new.md b/docs/docs/reference/other-new-features/control-syntax-new.md index e6d1a2bb2d46..01987c1e0652 100644 --- a/docs/docs/reference/other-new-features/control-syntax-new.md +++ b/docs/docs/reference/other-new-features/control-syntax-new.md @@ -3,7 +3,7 @@ layout: doc-page title: New Control Syntax --- -Scala 3 has a new "quiet" syntax for control expressions that does not rely in +Scala 3 has a new "quiet" syntax for control expressions that does not rely on enclosing the condition in parentheses, and also allows to drop parentheses or braces around the generators of a `for`-expression. Examples: ```scala