Skip to content
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

Experiment: Allow indented regions after operators #19099

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*/
Expand Down
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,10 @@ object Parsers {
*/
def nextCanFollowOperator(leadingOperandTokens: BitSet): Boolean =
leadingOperandTokens.contains(in.lookahead.token)
|| 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)
|| in.postfixOpsEnabled
|| in.lookahead.token == COLONop
|| in.lookahead.token == EOF // important for REPL completions
Expand Down
42 changes: 27 additions & 15 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,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
}
Expand Down Expand Up @@ -160,9 +156,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 =
Expand Down Expand Up @@ -194,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)
Expand All @@ -209,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
Expand Down Expand Up @@ -387,9 +383,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()
Expand All @@ -406,6 +403,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
Expand All @@ -416,7 +417,7 @@ object Scanners {
*/
def isLeadingInfixOperator(nextWidth: IndentWidth = indentWidth(offset), inConditional: Boolean = true) =
allowLeadingInfixOperators
&& isOperator
&& isOperator(token, name)
&& (isWhitespace(ch) || ch == LF)
&& !pastBlankLine
&& {
Expand All @@ -434,15 +435,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)
|| indentAfterOperatorEnabled
&& indentWidth(offset) < indentWidth(lookahead.next.offset))
}
&& {
currentRegion match
Expand Down Expand Up @@ -547,7 +550,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
Expand Down Expand Up @@ -576,7 +579,11 @@ object Scanners {
*/
inline def isContinuing =
lastWidth < nextWidth
&& (openParensTokens.contains(token) || lastToken == RETURN)
&& ( openParensTokens.contains(token)
|| lastToken == RETURN
|| indentAfterOperatorEnabled
&& isOperator(lastToken, lastName) && lastLineOffset >= 0
)
&& !pastBlankLine
&& !migrateTo3
&& !noindentSyntax
Expand Down Expand Up @@ -631,7 +638,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)
|| indentAfterOperatorEnabled
&& isOperator(lastToken, lastName)
&& lastLineOffset >= 0
&& canStartStatTokens3.contains(token)
then
currentRegion = Indented(nextWidth, lastToken, currentRegion)
insert(INDENT, offset)
else if lastToken == SELFARROW then
Expand Down Expand Up @@ -1088,7 +1100,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
}

Expand Down
14 changes: 14 additions & 0 deletions docs/_docs/reference/changed-features/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,20 @@ 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 `<indent>` token is inserted [^1] after the `||`. Since `<indent>` 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.
Expand Down
7 changes: 6 additions & 1 deletion docs/_docs/reference/other-new-features/indentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 [^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`.

If an `<indent>` is inserted, the indentation width of the token on the next line
Expand Down Expand Up @@ -143,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.
Expand Down Expand Up @@ -199,6 +203,7 @@ Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>>
Packaging ::= ‘package’ QualId :<<< TopStats >>>
```


## Optional Braces for Method Arguments

Starting with Scala 3.3, a `<colon>` token is also recognized where a function argument would be expected. Examples:
Expand Down
2 changes: 1 addition & 1 deletion library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions tests/pos/indent-ops.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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 =
firstValue < secondValue
||
val thirdValue = firstValue * secondValue
thirdValue > 100
||
def avg(x: Double, y: Double) = (x + y)/2
avg(firstValue, secondValue) > 0.0
||
firstValue
* secondValue
*
val firstSquare = firstValue * firstValue
firstSquare + firstSquare
<=
firstValue `max` secondValue
Loading