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