From 1109395626f2157d4be719092108b1acc2d50b80 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 28 Nov 2023 14:08:33 +0100 Subject: [PATCH 1/3] Experiment: Allow indented regions after operators Allow indented regions after operators that appear on their own line. --- .../dotty/tools/dotc/parsing/Parsers.scala | 4 ++ .../dotty/tools/dotc/parsing/Scanners.scala | 42 ++++++++++++------- tests/pos/indent-ops.scala | 20 +++++++++ 3 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 tests/pos/indent-ops.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index aec3ad42feef..7d84a88a5c04 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1003,6 +1003,10 @@ object Parsers { */ def nextCanFollowOperator(leadingOperandTokens: BitSet): Boolean = leadingOperandTokens.contains(in.lookahead.token) + || Scanners.allowIndentAfterInfixOp + && in.lineOffset >= 0 // operator is on its own line + && in.lookahead.lineOffset >= 0 // and next line is indented + && in.currentRegion.indentWidth < in.indentWidth(in.lookahead.offset) || in.postfixOpsEnabled || in.lookahead.token == COLONop || in.lookahead.token == EOF // important for REPL completions diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 44b0c43e545b..74d51f4a502a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -35,6 +35,8 @@ object Scanners { private val identity: IndentWidth => IndentWidth = Predef.identity + val allowIndentAfterInfixOp = true + trait TokenData { /** the next token */ @@ -84,10 +86,6 @@ object Scanners { /** Is current token first one after a newline? */ def isAfterLineEnd: Boolean = lineOffset >= 0 - def isOperator = - token == BACKQUOTED_IDENT - || token == IDENTIFIER && isOperatorPart(name(name.length - 1)) - def isArrow = token == ARROW || token == CTXARROW } @@ -160,9 +158,9 @@ object Scanners { strVal = litBuf.toString litBuf.clear() - @inline def isNumberSeparator(c: Char): Boolean = c == '_' + inline def isNumberSeparator(c: Char): Boolean = c == '_' - @inline def removeNumberSeparators(s: String): String = if (s.indexOf('_') == -1) s else s.replace("_", "") + inline def removeNumberSeparators(s: String): String = if (s.indexOf('_') == -1) s else s.replace("_", "") // disallow trailing numeric separator char, but continue lexing def checkNoTrailingSeparator(): Unit = @@ -387,9 +385,10 @@ object Scanners { def nextToken(): Unit = val lastToken = token val lastName = name + val lastLineOffset = lineOffset adjustSepRegions(lastToken) getNextToken(lastToken) - if isAfterLineEnd then handleNewLine(lastToken) + if isAfterLineEnd then handleNewLine(lastToken, lastName, lastLineOffset) postProcessToken(lastToken, lastName) profile.recordNewToken() printState() @@ -406,6 +405,10 @@ object Scanners { this.token = token } + def isOperator(token: Int, name: SimpleName): Boolean = + token == BACKQUOTED_IDENT + || token == IDENTIFIER && isOperatorPart(name(name.length - 1)) + /** A leading symbolic or backquoted identifier is treated as an infix operator if * - it does not follow a blank line, and * - it is followed by at least one whitespace character and a @@ -416,7 +419,7 @@ object Scanners { */ def isLeadingInfixOperator(nextWidth: IndentWidth = indentWidth(offset), inConditional: Boolean = true) = allowLeadingInfixOperators - && isOperator + && isOperator(token, name) && (isWhitespace(ch) || ch == LF) && !pastBlankLine && { @@ -434,15 +437,17 @@ object Scanners { // leading infix operator. def assumeStartsExpr(lexeme: TokenData) = (canStartExprTokens.contains(lexeme.token) || lexeme.token == COLONeol) - && (!lexeme.isOperator || nme.raw.isUnary(lexeme.name)) + && (!isOperator(lexeme.token, lexeme.name) || nme.raw.isUnary(lexeme.name)) val lookahead = LookaheadScanner() lookahead.allowLeadingInfixOperators = false // force a NEWLINE a after current token if it is on its own line lookahead.nextToken() assumeStartsExpr(lookahead) || lookahead.token == NEWLINE - && assumeStartsExpr(lookahead.next) && indentWidth(offset) <= indentWidth(lookahead.next.offset) + && (assumeStartsExpr(lookahead.next) + || allowIndentAfterInfixOp + && indentWidth(offset) < indentWidth(lookahead.next.offset)) } && { currentRegion match @@ -547,7 +552,7 @@ 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, lastName: SimpleName, lastLineOffset: Offset) = var indentIsSignificant = false var newlineIsSeparating = false var lastWidth = IndentWidth.Zero @@ -576,7 +581,11 @@ object Scanners { */ inline def isContinuing = lastWidth < nextWidth - && (openParensTokens.contains(token) || lastToken == RETURN) + && ( openParensTokens.contains(token) + || lastToken == RETURN + || allowIndentAfterInfixOp + && isOperator(lastToken, lastName) && lastLineOffset >= 0 + ) && !pastBlankLine && !migrateTo3 && !noindentSyntax @@ -631,7 +640,12 @@ object Scanners { else if lastWidth < nextWidth || lastWidth == nextWidth && (lastToken == MATCH || lastToken == CATCH) && token == CASE then - if canStartIndentTokens.contains(lastToken) then + if canStartIndentTokens.contains(lastToken) + || allowIndentAfterInfixOp + && isOperator(lastToken, lastName) + && lastLineOffset >= 0 + && canStartStatTokens3.contains(token) + then currentRegion = Indented(nextWidth, lastToken, currentRegion) insert(INDENT, offset) else if lastToken == SELFARROW then @@ -1088,7 +1102,7 @@ object Scanners { next class LookaheadScanner(val allowIndent: Boolean = false) extends Scanner(source, offset, allowIndent = allowIndent) { - override protected def initialCharBufferSize = 8 + override protected def initialCharBufferSize = 16 override def languageImportContext = Scanner.this.languageImportContext } diff --git a/tests/pos/indent-ops.scala b/tests/pos/indent-ops.scala new file mode 100644 index 000000000000..d90b5b362e6f --- /dev/null +++ b/tests/pos/indent-ops.scala @@ -0,0 +1,20 @@ +def test(b: Boolean, y: Int) = + val first = y + * y + + val result = + y > 0 + || + val z = y + 1 + z > 0 + || + val bb = !b + bb & b + || + y + * y + * + val z = y * y + z + < + y From 9c704da7059e3aa8d46074c7d411dcb04698c28a Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 28 Nov 2023 22:25:50 +0100 Subject: [PATCH 2/3] Update docs --- docs/_docs/reference/changed-features/operators.md | 11 +++++++++++ .../_docs/reference/other-new-features/indentation.md | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/_docs/reference/changed-features/operators.md b/docs/_docs/reference/changed-features/operators.md index 0cf25d77bc11..dcf285f95afb 100644 --- a/docs/_docs/reference/changed-features/operators.md +++ b/docs/_docs/reference/changed-features/operators.md @@ -167,6 +167,17 @@ Another example: This code is recognized as three different statements. `???` is syntactically a symbolic identifier, but neither of its occurrences is followed by a space and a token that can start an expression. +Indentation is significant after an operator that appears on its own line. +For instance, in +```scala +someCondition +|| + val helper = helperDef + anotherCondition(helper) +``` +an `` token is inserted after the `||`. Since `` can start as an expression, the `||` operator is classified as a leading infix operator. +``` + ## Unary operators A unary operator must not have explicit parameter lists even if they are empty. diff --git a/docs/_docs/reference/other-new-features/indentation.md b/docs/_docs/reference/other-new-features/indentation.md index 9963d1ee7577..7d6b38581dc8 100644 --- a/docs/_docs/reference/other-new-features/indentation.md +++ b/docs/_docs/reference/other-new-features/indentation.md @@ -68,8 +68,10 @@ There are two rules: = => ?=> <- catch do else finally for if match return then throw try while yield ``` + , or - - after the closing `)` of a condition in an old-style `if` or `while`. + - after an operator that appears on its own line, or + - after the closing `)` of a condition in an old-style `if` or `while`, or - after the closing `)` or `}` of the enumerations of an old-style `for` loop without a `do`. If an `` is inserted, the indentation width of the token on the next line From cba353a27ddd398bd92a38e04fea57ac49af3566 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 28 Nov 2023 23:29:25 +0100 Subject: [PATCH 3/3] Re-use fewerBraces language import to enable the feature --- .../src/dotty/tools/dotc/config/Feature.scala | 3 ++ .../dotty/tools/dotc/parsing/Parsers.scala | 2 +- .../dotty/tools/dotc/parsing/Scanners.scala | 10 +++--- .../reference/changed-features/operators.md | 5 ++- .../other-new-features/indentation.md | 5 ++- .../runtime/stdLibPatches/language.scala | 2 +- tests/pos/indent-ops.scala | 34 ++++++++++++------- 7 files changed, 38 insertions(+), 23 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index fa262a5880ff..2cdd1ceeb1ce 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -120,6 +120,9 @@ object Feature: def fewerBracesEnabled(using Context) = sourceVersion.isAtLeast(`3.3`) || enabled(fewerBraces) + def indentAfterOperatorEnabled(using Context) = + enabled(fewerBraces) + /** If current source migrates to `version`, issue given warning message * and return `true`, otherwise return `false`. */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 7d84a88a5c04..d47fb7bc298b 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1003,7 +1003,7 @@ object Parsers { */ def nextCanFollowOperator(leadingOperandTokens: BitSet): Boolean = leadingOperandTokens.contains(in.lookahead.token) - || Scanners.allowIndentAfterInfixOp + || in.indentAfterOperatorEnabled && in.lineOffset >= 0 // operator is on its own line && in.lookahead.lineOffset >= 0 // and next line is indented && in.currentRegion.indentWidth < in.indentWidth(in.lookahead.offset) diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 74d51f4a502a..078897cd588d 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -35,8 +35,6 @@ object Scanners { private val identity: IndentWidth => IndentWidth = Predef.identity - val allowIndentAfterInfixOp = true - trait TokenData { /** the next token */ @@ -192,7 +190,6 @@ object Scanners { ((if (Config.defaultIndent) !noindentSyntax else ctx.settings.indent.value) || rewriteNoIndent) && allowIndent - if (rewrite) { val s = ctx.settings val rewriteTargets = List(s.newSyntax, s.oldSyntax, s.indent, s.noindent) @@ -207,6 +204,7 @@ object Scanners { def featureEnabled(name: TermName) = Feature.enabled(name)(using languageImportContext) def erasedEnabled = featureEnabled(Feature.erasedDefinitions) + def indentAfterOperatorEnabled = featureEnabled(Feature.fewerBraces) private var postfixOpsEnabledCache = false private var postfixOpsEnabledCtx: Context = NoContext @@ -446,7 +444,7 @@ object Scanners { || lookahead.token == NEWLINE && indentWidth(offset) <= indentWidth(lookahead.next.offset) && (assumeStartsExpr(lookahead.next) - || allowIndentAfterInfixOp + || indentAfterOperatorEnabled && indentWidth(offset) < indentWidth(lookahead.next.offset)) } && { @@ -583,7 +581,7 @@ object Scanners { lastWidth < nextWidth && ( openParensTokens.contains(token) || lastToken == RETURN - || allowIndentAfterInfixOp + || indentAfterOperatorEnabled && isOperator(lastToken, lastName) && lastLineOffset >= 0 ) && !pastBlankLine @@ -641,7 +639,7 @@ object Scanners { else if lastWidth < nextWidth || lastWidth == nextWidth && (lastToken == MATCH || lastToken == CATCH) && token == CASE then if canStartIndentTokens.contains(lastToken) - || allowIndentAfterInfixOp + || indentAfterOperatorEnabled && isOperator(lastToken, lastName) && lastLineOffset >= 0 && canStartStatTokens3.contains(token) diff --git a/docs/_docs/reference/changed-features/operators.md b/docs/_docs/reference/changed-features/operators.md index dcf285f95afb..4e55951a9653 100644 --- a/docs/_docs/reference/changed-features/operators.md +++ b/docs/_docs/reference/changed-features/operators.md @@ -175,9 +175,12 @@ someCondition val helper = helperDef anotherCondition(helper) ``` -an `` token is inserted after the `||`. Since `` can start as an expression, the `||` operator is classified as a leading infix operator. +an `` token is inserted [^1] after the `||`. Since `` can start as an expression, the `||` operator is classified as a leading infix operator. ``` +[^1]: Currently only enabled with an `experimental.fewerBraces` language import or setting. + + ## Unary operators A unary operator must not have explicit parameter lists even if they are empty. diff --git a/docs/_docs/reference/other-new-features/indentation.md b/docs/_docs/reference/other-new-features/indentation.md index 7d6b38581dc8..7665201a2a49 100644 --- a/docs/_docs/reference/other-new-features/indentation.md +++ b/docs/_docs/reference/other-new-features/indentation.md @@ -70,7 +70,7 @@ There are two rules: ``` , or - - after an operator that appears on its own line, or + - after an operator that appears on its own line [^1], or - after the closing `)` of a condition in an old-style `if` or `while`, or - after the closing `)` or `}` of the enumerations of an old-style `for` loop without a `do`. @@ -145,6 +145,8 @@ else d ``` is parsed as `if x then a + b + c else d`. +[^1]: Currently only enabled with an `experimental.fewerBraces` language import or setting. + ## Optional Braces Around Template Bodies The Scala grammar uses the term _template body_ for the definitions of a class, trait, or object that are normally enclosed in braces. The braces around a template body can also be omitted by means of the following rule. @@ -201,6 +203,7 @@ Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> Packaging ::= ‘package’ QualId :<<< TopStats >>> ``` + ## Optional Braces for Method Arguments Starting with Scala 3.3, a `` token is also recognized where a function argument would be expected. Examples: diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index c2a12cec2ecc..ccacad5259e2 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -51,7 +51,7 @@ object language: /** Experimental support for using indentation for arguments */ @compileTimeOnly("`fewerBraces` can only be used at compile time in import statements") - @deprecated("`fewerBraces` is now standard, no language import is needed", since = "3.3") + //@deprecated("`fewerBraces` is now standard, no language import is needed", since = "3.3") object fewerBraces /** Experimental support for typechecked exception capabilities diff --git a/tests/pos/indent-ops.scala b/tests/pos/indent-ops.scala index d90b5b362e6f..b5e4ca62d1b0 100644 --- a/tests/pos/indent-ops.scala +++ b/tests/pos/indent-ops.scala @@ -1,20 +1,28 @@ -def test(b: Boolean, y: Int) = - val first = y +import language.experimental.fewerBraces + +def test(y: Int) = + val firstValue = y * y + val secondValue = + firstValue + + + if firstValue < 0 then 1 else 0 + + + if y < 0 then y else -y val result = - y > 0 + firstValue < secondValue || - val z = y + 1 - z > 0 + val thirdValue = firstValue * secondValue + thirdValue > 100 || - val bb = !b - bb & b + def avg(x: Double, y: Double) = (x + y)/2 + avg(firstValue, secondValue) > 0.0 || - y - * y + firstValue + * secondValue * - val z = y * y - z - < - y + val firstSquare = firstValue * firstValue + firstSquare + firstSquare + <= + firstValue `max` secondValue