Skip to content

Change fewerbraces to always use a colon, even before lambdas #15273

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 16 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/parsing/JavaScanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ object JavaScanners {
nextChar()

case ':' =>
token = COLON
token = COLONop
nextChar()

case '@' =>
Expand Down
262 changes: 163 additions & 99 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Large diffs are not rendered by default.

75 changes: 45 additions & 30 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,8 @@ object Scanners {
def isNestedStart = token == LBRACE || token == INDENT
def isNestedEnd = token == RBRACE || token == OUTDENT

/** Is token a COLON, after having converted COLONEOL to COLON?
* The conversion means that indentation is not significant after `:`
* anymore. So, warning: this is a side-effecting operation.
*/
def isColon() =
if token == COLONEOL then token = COLON
token == COLON
def isColon =
token == COLONop || token == COLONfollow || token == COLONeol

/** Is current token first one after a newline? */
def isAfterLineEnd: Boolean = lineOffset >= 0
Expand Down Expand Up @@ -189,7 +184,10 @@ object Scanners {
val indentSyntax =
((if (Config.defaultIndent) !noindentSyntax else ctx.settings.indent.value)
|| rewriteNoIndent)
&& !isInstanceOf[LookaheadScanner]
&& { this match
case self: LookaheadScanner => self.allowIndent
case _ => true
}

if (rewrite) {
val s = ctx.settings
Expand All @@ -206,12 +204,22 @@ object Scanners {
def featureEnabled(name: TermName) = Feature.enabled(name)(using languageImportContext)
def erasedEnabled = featureEnabled(Feature.erasedDefinitions)

private inline val fewerBracesByDefault = false
// turn on to study impact on codebase if `fewerBraces` was the default

private var fewerBracesEnabledCache = false
private var fewerBracesEnabledCtx: Context = NoContext

def fewerBracesEnabled =
if fewerBracesEnabledCtx ne myLanguageImportContext then
fewerBracesEnabledCache = featureEnabled(Feature.fewerBraces)
fewerBracesEnabledCache =
featureEnabled(Feature.fewerBraces)
|| fewerBracesByDefault && indentSyntax && !migrateTo3
// ensure that fewer braces is not the default for 3.0-migration since
// { x: T =>
// expr
// }
// would be ambiguous
fewerBracesEnabledCtx = myLanguageImportContext
fewerBracesEnabledCache

Expand Down Expand Up @@ -386,10 +394,11 @@ object Scanners {
*/
def nextToken(): Unit =
val lastToken = token
val lastName = name
adjustSepRegions(lastToken)
getNextToken(lastToken)
if isAfterLineEnd then handleNewLine(lastToken)
postProcessToken()
postProcessToken(lastToken, lastName)
printState()

final def printState() =
Expand Down Expand Up @@ -420,7 +429,7 @@ object Scanners {
&& {
// Is current lexeme assumed to start an expression?
// This is the case if the lexime is one of the tokens that
// starts an expression or it is a COLONEOL. Furthermore, if
// starts an expression or it is a COLONeol. Furthermore, if
// the previous token is in backticks, the lexeme may not be a binary operator.
// I.e. in
//
Expand All @@ -431,7 +440,7 @@ object Scanners {
// in backticks and is a binary operator. Hence, `x` is not classified as a
// leading infix operator.
def assumeStartsExpr(lexeme: TokenData) =
(canStartExprTokens.contains(lexeme.token) || lexeme.token == COLONEOL)
(canStartExprTokens.contains(lexeme.token) || lexeme.token == COLONeol)
&& (!lexeme.isOperator || nme.raw.isUnary(lexeme.name))
val lookahead = LookaheadScanner()
lookahead.allowLeadingInfixOperators = false
Expand Down Expand Up @@ -607,12 +616,11 @@ object Scanners {
currentRegion match
case r: Indented =>
insert(OUTDENT, offset)
if next.token != COLON then
handleNewIndentWidth(r.enclosing, ir =>
errorButContinue(
i"""The start of this line does not match any of the previous indentation widths.
|Indentation width of current line : $nextWidth
|This falls between previous widths: ${ir.width} and $lastWidth"""))
handleNewIndentWidth(r.enclosing, ir =>
errorButContinue(
i"""The start of this line does not match any of the previous indentation widths.
|Indentation width of current line : $nextWidth
|This falls between previous widths: ${ir.width} and $lastWidth"""))
case r =>
if skipping then
if r.enclosing.isClosedByUndentAt(nextWidth) then
Expand All @@ -629,7 +637,7 @@ object Scanners {
currentRegion.knownWidth = nextWidth
else if (lastWidth != nextWidth)
errorButContinue(spaceTabMismatchMsg(lastWidth, nextWidth))
if token != OUTDENT || next.token == COLON then
if token != OUTDENT then
handleNewIndentWidth(currentRegion, _.otherIndentWidths += nextWidth)
end handleNewLine

Expand All @@ -638,19 +646,22 @@ object Scanners {
|Previous indent : $lastWidth
|Latest indent : $nextWidth"""

def observeColonEOL(): Unit =
if token == COLON then
def observeColonEOL(inTemplate: Boolean): Unit =
val enabled =
if inTemplate then token == COLONop || token == COLONfollow
else token == COLONfollow && fewerBracesEnabled
if enabled then
lookAhead()
val atEOL = isAfterLineEnd || token == EOF
reset()
if atEOL then token = COLONEOL
if atEOL then token = COLONeol

def observeIndented(): Unit =
if indentSyntax && isNewLine then
val nextWidth = indentWidth(next.offset)
val lastWidth = currentRegion.indentWidth
if lastWidth < nextWidth then
currentRegion = Indented(nextWidth, COLONEOL, currentRegion)
currentRegion = Indented(nextWidth, COLONeol, currentRegion)
offset = next.offset
token = INDENT
end observeIndented
Expand Down Expand Up @@ -683,10 +694,10 @@ object Scanners {
case _ =>

/** - Join CASE + CLASS => CASECLASS, CASE + OBJECT => CASEOBJECT
* SEMI + ELSE => ELSE, COLON + <EOL> => COLONEOL
* SEMI + ELSE => ELSE, COLON following id/)/] => COLONfollow
* - Insert missing OUTDENTs at EOF
*/
def postProcessToken(): Unit = {
def postProcessToken(lastToken: Token, lastName: SimpleName): Unit = {
def fuse(tok: Int) = {
token = tok
offset = prev.offset
Expand Down Expand Up @@ -721,8 +732,10 @@ object Scanners {
reset()
case END =>
if !isEndMarker then token = IDENTIFIER
case COLON =>
if fewerBracesEnabled then observeColonEOL()
case COLONop =>
if lastToken == IDENTIFIER && lastName != null && isIdentifierStart(lastName.head)
|| colonEOLPredecessors.contains(lastToken)
then token = COLONfollow
case RBRACE | RPAREN | RBRACKET =>
closeIndented()
case EOF =>
Expand Down Expand Up @@ -1067,7 +1080,7 @@ object Scanners {
reset()
next

class LookaheadScanner() extends Scanner(source, offset) {
class LookaheadScanner(val allowIndent: Boolean = false) extends Scanner(source, offset) {
override def languageImportContext = Scanner.this.languageImportContext
}

Expand Down Expand Up @@ -1179,7 +1192,7 @@ object Scanners {
isSoftModifier && inModifierPosition()

def isSoftModifierInParamModifierPosition: Boolean =
isSoftModifier && lookahead.token != COLON
isSoftModifier && !lookahead.isColon

def isErased: Boolean = isIdent(nme.erased) && erasedEnabled

Expand Down Expand Up @@ -1518,7 +1531,9 @@ object Scanners {
case NEWLINE => ";"
case NEWLINES => ";;"
case COMMA => ","
case _ => showToken(token)
case COLONfollow | COLONeol => "':'"
case _ =>
if debugTokenStream then showTokenDetailed(token) else showToken(token)
}

/* Resume normal scanning after XML */
Expand Down
29 changes: 17 additions & 12 deletions compiler/src/dotty/tools/dotc/parsing/Tokens.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@ abstract class TokensCommon {

def tokenRange(lo: Int, hi: Int): TokenSet = BitSet(lo to hi: _*)

def showTokenDetailed(token: Int): String = debugString(token)

def showToken(token: Int): String = {
val str = tokenString(token)
if (isKeyword(token)) s"'$str'" else str
}

val tokenString, debugString: Array[String] = new Array[String](maxToken + 1)

def enter(token: Int, str: String, debugStr: String = ""): Unit = {
Expand Down Expand Up @@ -107,7 +100,7 @@ abstract class TokensCommon {

/** special keywords */
//inline val USCORE = 73; enter(USCORE, "_")
inline val COLON = 74; enter(COLON, ":")
inline val COLONop = 74; enter(COLONop, ":") // a stand-alone `:`, see also COLONfollow
inline val EQUALS = 75; enter(EQUALS, "=")
//inline val LARROW = 76; enter(LARROW, "<-")
//inline val ARROW = 77; enter(ARROW, "=>")
Expand Down Expand Up @@ -204,8 +197,11 @@ object Tokens extends TokensCommon {

inline val QUOTE = 87; enter(QUOTE, "'")

inline val COLONEOL = 88; enter(COLONEOL, ":", ": at eol")
inline val SELFARROW = 89; enter(SELFARROW, "=>") // reclassified ARROW following self-type
inline val COLONfollow = 88; enter(COLONfollow, ":")
// A `:` following an alphanumeric identifier or one of the tokens in colonEOLPredecessors
inline val COLONeol = 89; enter(COLONeol, ":", ": at eol")
// A `:` recognized as starting an indentation block
inline val SELFARROW = 90; enter(SELFARROW, "=>") // reclassified ARROW following self-type

/** XML mode */
inline val XMLSTART = 99; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate
Expand Down Expand Up @@ -233,7 +229,7 @@ object Tokens extends TokensCommon {
final val canStartExprTokens2: TokenSet = canStartExprTokens3 | BitSet(DO)

final val canStartTypeTokens: TokenSet = literalTokens | identifierTokens | BitSet(
THIS, SUPER, USCORE, LPAREN, AT)
THIS, SUPER, USCORE, LPAREN, LBRACE, AT)

final val templateIntroTokens: TokenSet = BitSet(CLASS, TRAIT, OBJECT, ENUM, CASECLASS, CASEOBJECT)

Expand Down Expand Up @@ -276,7 +272,7 @@ object Tokens extends TokensCommon {
final val closingRegionTokens = BitSet(RBRACE, RPAREN, RBRACKET, CASE) | statCtdTokens

final val canStartIndentTokens: BitSet =
statCtdTokens | BitSet(COLONEOL, WITH, EQUALS, ARROW, CTXARROW, LARROW, WHILE, TRY, FOR, IF, THROW, RETURN)
statCtdTokens | BitSet(COLONeol, WITH, EQUALS, ARROW, CTXARROW, LARROW, WHILE, TRY, FOR, IF, THROW, RETURN)

/** Faced with the choice between a type and a formal parameter, the following
* tokens determine it's a formal parameter.
Expand All @@ -287,7 +283,16 @@ object Tokens extends TokensCommon {

final val endMarkerTokens = identifierTokens | BitSet(IF, WHILE, FOR, MATCH, TRY, NEW, THROW, GIVEN, VAL, THIS)

final val colonEOLPredecessors = BitSet(RPAREN, RBRACKET, BACKQUOTED_IDENT, THIS, SUPER, QUOTEID, STRINGLIT)

final val closingParens = BitSet(RPAREN, RBRACKET, RBRACE)

final val softModifierNames = Set(nme.inline, nme.opaque, nme.open, nme.transparent, nme.infix)

def showTokenDetailed(token: Int): String = debugString(token)

def showToken(token: Int): String = {
val str = tokenString(token)
if isKeyword(token) || token == COLONfollow || token == COLONeol then s"'$str'" else str
}
}
12 changes: 6 additions & 6 deletions docs/_docs/internals/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,13 @@ SimpleExpr ::= SimpleRef
| SimpleExpr ‘.’ MatchClause
| SimpleExpr TypeArgs TypeApply(expr, args)
| SimpleExpr ArgumentExprs Apply(expr, args)
| SimpleExpr ‘:’ IndentedExpr -- under language.experimental.fewerBraces
| SimpleExpr FunParams (‘=>’ | ‘?=>’) IndentedExpr -- under language.experimental.fewerBraces
| SimpleExpr ‘:’ ColonArgument -- under language.experimental.fewerBraces
| SimpleExpr ‘_’ PostfixOp(expr, _) (to be dropped)
| XmlExpr -- to be dropped
IndentedExpr ::= indent CaseClauses | Block outdent
Quoted ::= ‘'’ ‘{’ Block ‘}’
| ‘'’ ‘[’ Type ‘]’
| XmlExpr -- to be dropped
ColonArgument ::= indent CaseClauses | Block outdent
| FunParams (‘=>’ | ‘?=>’) ColonArgBody
| HkTypeParamClause ‘=>’ ColonArgBody
ColonArgBody ::= indent (CaseClauses | Block) outdent
ExprSplice ::= spliceId -- if inside quoted block
| ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern
| ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern
Expand Down
36 changes: 17 additions & 19 deletions docs/_docs/reference/experimental/fewer-braces.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import language.experimental.fewerBraces
```
Alternatively, it can be enabled with command line option `-language:experimental.fewerBraces`.

This variant is more contentious and less stable than the rest of the significant indentation scheme. It allows to replace a function argument in braces by a `:` at the end of a line and indented code, similar to the convention for class bodies. It also allows to leave out braces around arguments that are multi-line function values.
This variant is more contentious and less stable than the rest of the significant indentation scheme. It allows to replace a function argument in braces by a `:` at the end of a line and indented code, similar to the convention for class bodies. The `:` can
optionally be followed by the parameter part of a function literal.

## Using `:` At End Of Line

Expand Down Expand Up @@ -50,34 +51,31 @@ val firstLine = files.get(fileName).fold:

## Lambda Arguments Without Braces

Braces can also be omitted around multiple line function value arguments:
The `:` can optionally be followed by the parameter part of a function literal:
```scala
val xs = elems.map x =>
val xs = elems.map: x =>
val y = x - 1
y * y
xs.foldLeft (x, y) =>
x + y
```
Braces can be omitted if the lambda starts with a parameter list and `=>` or `=>?` at the end of one line and it has an indented body on the following lines.
Braces can be omitted if the lambda starts with a parameter list and an arrow symbol `=>` or `?=>`.
The arrow is followed on the next line(s) by the body of the functional literal which must be indented
relative to the previous line.

## Syntax Changes

As a lexical change, a `:` at the end of a line is now always treated as a
"colon at end of line" token.

The context free grammar changes as follows:
```
SimpleExpr ::= ...
| SimpleExpr `:` IndentedArgument
| SimpleExpr FunParams (‘=>’ | ‘?=>’) IndentedArgument
InfixExpr ::= ...
| InfixExpr id `:` IndentedArgument
IndentedArgument ::= indent (CaseClauses | Block) outdent
```

Note that a lambda argument must have the `=>` at the end of a line for braces
to be optional. For instance, the following would also be incorrect:
| SimpleExpr ‘:’ ColonArgument

```scala
xs.map x => x + 1 // error: braces or parentheses are required
```
The lambda has to be enclosed in braces or parentheses:
```scala
xs.map(x => x + 1) // ok
| SimpleExpr FunParams (‘=>’ | ‘?=>’) IndentedArgument
ColonArgument ::= indent CaseClauses | Block outdent
| FunParams (‘=>’ | ‘?=>’) ColonArgBody
| HkTypeParamClause ‘=>’ ColonArgBody
ColonArgBody ::= indent (CaseClauses | Block) outdent
```
10 changes: 5 additions & 5 deletions tests/neg-custom-args/nowarn/nowarn.check
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Matching filters for @nowarn or -Wconf:
| ^
| method f is deprecated
-- Deprecation Warning: tests/neg-custom-args/nowarn/nowarn.scala:47:10 ------------------------------------------------
47 |def t7c = f: // warning (deprecation)
47 |def t7c = f // warning (deprecation)
| ^
| method f is deprecated
-- Unchecked Warning: tests/neg-custom-args/nowarn/nowarn.scala:53:7 ---------------------------------------------------
Expand All @@ -78,10 +78,10 @@ Matching filters for @nowarn or -Wconf:
40 |@nowarn("msg=fish") def t6d = f // error (unused nowarn), warning (deprecation)
|^^^^^^^^^^^^^^^^^^^
|@nowarn annotation does not suppress any warnings
-- Error: tests/neg-custom-args/nowarn/nowarn.scala:48:3 ---------------------------------------------------------------
48 | @nowarn("msg=fish") // error (unused nowarn)
| ^^^^^^^^^^^^^^^^^^^
| @nowarn annotation does not suppress any warnings
-- Error: tests/neg-custom-args/nowarn/nowarn.scala:48:5 ---------------------------------------------------------------
48 | : @nowarn("msg=fish") // error (unused nowarn)
| ^^^^^^^^^^^^^^^^^^^
| @nowarn annotation does not suppress any warnings
-- Error: tests/neg-custom-args/nowarn/nowarn.scala:60:0 ---------------------------------------------------------------
60 |@nowarn def t9a = { 1: @nowarn; 2 } // error (outer @nowarn is unused)
|^^^^^^^
Expand Down
8 changes: 4 additions & 4 deletions tests/neg-custom-args/nowarn/nowarn.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ def t6a = f // warning (refchecks, deprecation)
@nowarn def t6f = f

def t7a = f: @nowarn("cat=deprecation")
def t7b = f:
@nowarn("msg=deprecated")
def t7c = f: // warning (deprecation)
@nowarn("msg=fish") // error (unused nowarn)
def t7b = f
: @nowarn("msg=deprecated")
def t7c = f // warning (deprecation)
: @nowarn("msg=fish") // error (unused nowarn)
def t7d = f: @nowarn("")
def t7e = f: @nowarn

Expand Down
Loading