Skip to content

Commit 4cc6c06

Browse files
committed
New style syntax for control expressions
Allow drop drop parens or braces in control expressions Use for-do, for-yield, while-do, if-then-else instead.
1 parent 93fd8db commit 4cc6c06

File tree

10 files changed

+483
-28
lines changed

10 files changed

+483
-28
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ class ScalaSettings extends Settings.SettingGroup {
4848
val silentWarnings: Setting[Boolean] = BooleanSetting("-nowarn", "Silence all warnings.") withAbbreviation "--no-warnings"
4949
val fromTasty: Setting[Boolean] = BooleanSetting("-from-tasty", "Compile classes from tasty in classpath. The arguments are used as class names.") withAbbreviation "--from-tasty"
5050

51+
val newSyntax: Setting[Boolean] = BooleanSetting("-new-syntax", "Require `then` and `do` in control expressions")
52+
val oldSyntax: Setting[Boolean] = BooleanSetting("-old-syntax", "Require `(...)` around conditions")
53+
5154
/** Decompiler settings */
5255
val printTasty: Setting[Boolean] = BooleanSetting("-print-tasty", "Prints the raw tasty.") withAbbreviation "--print-tasty"
5356
val printLines: Setting[Boolean] = BooleanSetting("-print-lines", "Show source code line numbers.") withAbbreviation "--print-lines"

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 228 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import Constants._
2222
import Symbols.defn
2323
import ScriptParsers._
2424
import Decorators._
25-
import scala.internal.Chars.isIdentifierStart
25+
import scala.internal.Chars
2626
import scala.annotation.{tailrec, switch}
2727
import rewrites.Rewrites.patch
2828

@@ -351,6 +351,31 @@ object Parsers {
351351
accept(SEMI)
352352
}
353353

354+
def rewriteNotice(additionalOption: String = "") = {
355+
val optionStr = if (additionalOption.isEmpty) "" else " " ++ additionalOption
356+
i"\nThis construct can be rewritten automatically under$optionStr -rewrite."
357+
}
358+
359+
def syntaxVersionError(option: String, span: Span) = {
360+
syntaxError(em"""This construct is not allowed under $option.${rewriteNotice(option)}""", span)
361+
}
362+
363+
def rewriteToNewSyntax(span: Span = Span(in.offset)): Boolean = {
364+
if (in.newSyntax) {
365+
if (in.rewrite) return true
366+
syntaxVersionError("-new-syntax", span)
367+
}
368+
false
369+
}
370+
371+
def rewriteToOldSyntax(span: Span = Span(in.offset)): Boolean = {
372+
if (in.oldSyntax) {
373+
if (in.rewrite) return true
374+
syntaxVersionError("-old-syntax", span)
375+
}
376+
false
377+
}
378+
354379
def errorTermTree: Literal = atSpan(in.offset) { Literal(Constant(null)) }
355380

356381
private[this] var inFunReturnType = false
@@ -525,6 +550,131 @@ object Parsers {
525550

526551
def commaSeparated[T](part: () => T): List[T] = tokenSeparated(COMMA, part)
527552

553+
def inSepRegion[T](opening: Token, closing: Token)(op: => T): T = {
554+
in.adjustSepRegions(opening)
555+
try op finally in.adjustSepRegions(closing)
556+
}
557+
558+
/* -------- REWRITES ----------------------------------------------------------- */
559+
560+
/** A list of pending patches, to be issued if we can rewrite all enclosing braces to
561+
* indentation regions.
562+
*/
563+
var pendingPatches: List[() => Unit] = Nil
564+
565+
def testChar(idx: Int, p: Char => Boolean): Boolean = {
566+
val txt = source.content
567+
idx < txt.length && p(txt(idx))
568+
}
569+
570+
def testChar(idx: Int, c: Char): Boolean = {
571+
val txt = source.content
572+
idx < txt.length && txt(idx) == c
573+
}
574+
575+
def testChars(from: Int, str: String): Boolean =
576+
str.isEmpty ||
577+
testChar(from, str.head) && testChars(from + 1, str.tail)
578+
579+
def skipBlanks(idx: Int, step: Int = 1): Int =
580+
if (testChar(idx, c => c == ' ' || c == '\t' || c == Chars.CR)) skipBlanks(idx + step, step)
581+
else idx
582+
583+
def skipLineCommentsRightOf(idx: Int, column: Int): Int = {
584+
val j = skipBlanks(idx)
585+
if (testChar(j, '/') && testChar(j + 1, '/') && source.column(j) > column)
586+
skipLineCommentsRightOf(source.nextLine(j), column)
587+
else idx
588+
}
589+
590+
/** The region to eliminate when replacing a closing `)` or `}` that starts
591+
* a new line
592+
*/
593+
def closingElimRegion(): (Offset, Offset) = {
594+
val skipped = skipBlanks(in.lastOffset)
595+
if (testChar(skipped, Chars.LF)) // if `}` is on a line by itself
596+
(source.startOfLine(in.lastOffset), skipped + 1) // skip the whole line
597+
else // else
598+
(in.lastOffset - 1, skipped) // move the following text up to where the `}` was
599+
}
600+
601+
/** Drop (...) or { ... }, replacing the closing element with `endStr` */
602+
def dropParensOrBraces(start: Offset, endStr: String): Unit = {
603+
patch(source, Span(start, start + 1),
604+
if (testChar(start - 1, Chars.isIdentifierPart)) " " else "")
605+
val closingStartsLine = testChar(skipBlanks(in.lastOffset - 2, -1), Chars.LF)
606+
val preFill = if (closingStartsLine || endStr.isEmpty) "" else " "
607+
val postFill = if (in.lastOffset == in.offset) " " else ""
608+
val (startClosing, endClosing) =
609+
if (closingStartsLine && endStr.isEmpty) closingElimRegion()
610+
else (in.lastOffset - 1, in.lastOffset)
611+
patch(source, Span(startClosing, endClosing), s"$preFill$endStr$postFill")
612+
}
613+
614+
/** Drop current token, which is assumed to be `then` or `do`. */
615+
def dropTerminator(): Unit = {
616+
var startOffset = in.offset
617+
var endOffset = in.lastCharOffset
618+
if (in.isAfterLineEnd()) {
619+
if (testChar(endOffset, ' ')) endOffset += 1
620+
}
621+
else {
622+
if (testChar(startOffset - 1, ' ')) startOffset -= 1
623+
}
624+
patch(source, Span(startOffset, endOffset), "")
625+
}
626+
627+
/** rewrite code with (...) around the source code of `t` */
628+
def revertToParens(t: Tree): Unit =
629+
if (t.span.exists) {
630+
patch(source, t.span.startPos, "(")
631+
patch(source, t.span.endPos, ")")
632+
dropTerminator()
633+
}
634+
635+
/** In the tokens following the current one, does `query` precede any of the tokens that
636+
* - must start a statement, or
637+
* - separate two statements, or
638+
* - continue a statement (e.g. `else`, catch`)?
639+
*/
640+
def followedByToken(query: Token): Boolean = {
641+
val lookahead = in.lookaheadScanner
642+
var braces = 0
643+
while (true) {
644+
val token = lookahead.token
645+
if (braces == 0) {
646+
if (token == query) return true
647+
if (stopScanTokens.contains(token) || lookahead.token == RBRACE) return false
648+
}
649+
else if (token == EOF)
650+
return false
651+
else if (lookahead.token == RBRACE)
652+
braces -= 1
653+
if (lookahead.token == LBRACE) braces += 1
654+
lookahead.nextToken()
655+
}
656+
false
657+
}
658+
659+
/** A the generators of a for-expression enclosed in (...)? */
660+
def parensEncloseGenerators: Boolean = {
661+
val lookahead = in.lookaheadScanner
662+
var parens = 1
663+
lookahead.nextToken()
664+
while (parens != 0 && lookahead.token != EOF) {
665+
val token = lookahead.token
666+
if (token == LPAREN) parens += 1
667+
else if (token == RPAREN) parens -= 1
668+
lookahead.nextToken()
669+
}
670+
if (lookahead.token == LARROW)
671+
false // it's a pattern
672+
else if (lookahead.token != IDENTIFIER && lookahead.token != BACKQUOTED_IDENT)
673+
true // it's not a pattern since token cannot be an infix operator
674+
else
675+
followedByToken(LARROW) // `<-` comes before possible statement starts
676+
}
677+
528678
/* --------- OPERAND/OPERATOR STACK --------------------------------------- */
529679

530680
var opStack: List[OpInfo] = Nil
@@ -758,7 +908,7 @@ object Parsers {
758908
}
759909
else atSpan(negOffset) {
760910
if (in.token == QUOTEID) {
761-
if ((staged & StageKind.Spliced) != 0 && isIdentifierStart(in.name(0))) {
911+
if ((staged & StageKind.Spliced) != 0 && Chars.isIdentifierStart(in.name(0))) {
762912
val t = atSpan(in.offset + 1) {
763913
val tok = in.toToken(in.name)
764914
tok match {
@@ -844,7 +994,7 @@ object Parsers {
844994

845995
def newLineOptWhenFollowedBy(token: Int): Unit = {
846996
// note: next is defined here because current == NEWLINE
847-
if (in.token == NEWLINE && in.next.token == token) newLineOpt()
997+
if (in.token == NEWLINE && in.next.token == token) in.nextToken()
848998
}
849999

8501000
def newLineOptWhenFollowing(p: Int => Boolean): Unit = {
@@ -1235,11 +1385,22 @@ object Parsers {
12351385

12361386
def condExpr(altToken: Token): Tree = {
12371387
if (in.token == LPAREN) {
1238-
val t = atSpan(in.offset) { Parens(inParens(exprInParens())) }
1239-
if (in.token == altToken) in.nextToken()
1388+
var t: Tree = atSpan(in.offset) { Parens(inParens(exprInParens())) }
1389+
if (in.token != altToken && followedByToken(altToken))
1390+
t = inSepRegion(LPAREN, RPAREN) {
1391+
newLineOpt()
1392+
expr1Rest(postfixExprRest(simpleExprRest(t)), Location.ElseWhere)
1393+
}
1394+
if (in.token == altToken) {
1395+
if (rewriteToOldSyntax()) revertToParens(t)
1396+
in.nextToken()
1397+
}
1398+
else if (rewriteToNewSyntax(t.span))
1399+
dropParensOrBraces(t.span.start, s"${tokenString(altToken)}")
12401400
t
12411401
} else {
1242-
val t = expr()
1402+
val t = inSepRegion(LPAREN, RPAREN)(expr())
1403+
if (rewriteToOldSyntax(t.span.startPos)) revertToParens(t)
12431404
accept(altToken)
12441405
t
12451406
}
@@ -1333,7 +1494,7 @@ object Parsers {
13331494
in.errorOrMigrationWarning(
13341495
i"""`do <body> while <cond>' is no longer supported,
13351496
|use `while ({<body> ; <cond>}) ()' instead.
1336-
|The statement can be rewritten automatically under -language:Scala2 -migration -rewrite.
1497+
|${rewriteNotice("-language:Scala2")}
13371498
""")
13381499
val start = in.skipToken()
13391500
atSpan(start) {
@@ -1342,7 +1503,7 @@ object Parsers {
13421503
val whileStart = in.offset
13431504
accept(WHILE)
13441505
val cond = expr()
1345-
if (ctx.settings.migration.value) {
1506+
if (in.isScala2Mode) {
13461507
patch(source, Span(start, start + 2), "while ({")
13471508
patch(source, Span(whileStart, whileStart + 5), ";")
13481509
cond match {
@@ -1576,8 +1737,10 @@ object Parsers {
15761737
* | InfixExpr id [nl] InfixExpr
15771738
* | InfixExpr ‘given’ (InfixExpr | ParArgumentExprs)
15781739
*/
1579-
def postfixExpr(): Tree =
1580-
infixOps(prefixExpr(), canStartExpressionTokens, prefixExpr, maybePostfix = true)
1740+
def postfixExpr(): Tree = postfixExprRest(prefixExpr())
1741+
1742+
def postfixExprRest(t: Tree): Tree =
1743+
infixOps(t, canStartExpressionTokens, prefixExpr, maybePostfix = true)
15811744

15821745
/** PrefixExpr ::= [`-' | `+' | `~' | `!'] SimpleExpr
15831746
*/
@@ -1799,8 +1962,13 @@ object Parsers {
17991962
def enumerators(): List[Tree] = generator() :: enumeratorsRest()
18001963

18011964
def enumeratorsRest(): List[Tree] =
1802-
if (isStatSep) { in.nextToken(); enumerator() :: enumeratorsRest() }
1803-
else if (in.token == IF) guard() :: enumeratorsRest()
1965+
if (isStatSep) {
1966+
in.nextToken()
1967+
if (in.token == DO || in.token == YIELD || in.token == RBRACE) Nil
1968+
else enumerator() :: enumeratorsRest()
1969+
}
1970+
else if (in.token == IF)
1971+
guard() :: enumeratorsRest()
18041972
else Nil
18051973

18061974
/** Enumerator ::= Generator
@@ -1838,37 +2006,72 @@ object Parsers {
18382006
*/
18392007
def forExpr(): Tree = atSpan(in.skipToken()) {
18402008
var wrappedEnums = true
2009+
val start = in.offset
2010+
val forEnd = in.lastOffset
2011+
val leading = in.token
18412012
val enums =
1842-
if (in.token == LBRACE) inBraces(enumerators())
1843-
else if (in.token == LPAREN) {
1844-
val lparenOffset = in.skipToken()
1845-
openParens.change(LPAREN, 1)
2013+
if (leading == LBRACE || leading == LPAREN && parensEncloseGenerators) {
2014+
in.nextToken()
2015+
openParens.change(leading, 1)
18462016
val res =
1847-
if (in.token == CASE) enumerators()
2017+
if (leading == LBRACE || in.token == CASE)
2018+
enumerators()
18482019
else {
18492020
val pats = patternsOpt()
18502021
val pat =
18512022
if (in.token == RPAREN || pats.length > 1) {
18522023
wrappedEnums = false
18532024
accept(RPAREN)
18542025
openParens.change(LPAREN, -1)
1855-
atSpan(lparenOffset) { makeTupleOrParens(pats) } // note: alternatives `|' need to be weeded out by typer.
2026+
atSpan(start) { makeTupleOrParens(pats) } // note: alternatives `|' need to be weeded out by typer.
18562027
}
18572028
else pats.head
18582029
generatorRest(pat, casePat = false) :: enumeratorsRest()
18592030
}
18602031
if (wrappedEnums) {
1861-
accept(RPAREN)
1862-
openParens.change(LPAREN, -1)
2032+
val closingOnNewLine = in.isAfterLineEnd()
2033+
accept(leading + 1)
2034+
openParens.change(leading, -1)
2035+
def hasMultiLineEnum =
2036+
res.exists { t =>
2037+
val pos = t.sourcePos
2038+
pos.startLine < pos.endLine
2039+
}
2040+
if (rewriteToNewSyntax(Span(start)) && (leading == LBRACE || !hasMultiLineEnum)) {
2041+
// Don't rewrite if that could change meaning of newlines
2042+
newLinesOpt()
2043+
dropParensOrBraces(start, if (in.token == YIELD || in.token == DO) "" else "do")
2044+
}
18632045
}
18642046
res
1865-
} else {
2047+
}
2048+
else {
18662049
wrappedEnums = false
1867-
enumerators()
2050+
2051+
/*if (in.token == INDENT) inBracesOrIndented(enumerators()) else*/
2052+
val ts = inSepRegion(LBRACE, RBRACE)(enumerators())
2053+
if (rewriteToOldSyntax(Span(start)) && ts.nonEmpty) {
2054+
if (ts.length > 1 && ts.head.sourcePos.startLine != ts.last.sourcePos.startLine) {
2055+
patch(source, Span(forEnd), " {")
2056+
patch(source, Span(in.offset), "} ")
2057+
}
2058+
else {
2059+
patch(source, ts.head.span.startPos, "(")
2060+
patch(source, ts.last.span.endPos, ")")
2061+
}
2062+
}
2063+
ts
18682064
}
18692065
newLinesOpt()
1870-
if (in.token == YIELD) { in.nextToken(); ForYield(enums, expr()) }
1871-
else if (in.token == DO) { in.nextToken(); ForDo(enums, expr()) }
2066+
if (in.token == YIELD) {
2067+
in.nextToken()
2068+
ForYield(enums, expr())
2069+
}
2070+
else if (in.token == DO) {
2071+
if (rewriteToOldSyntax()) dropTerminator()
2072+
in.nextToken()
2073+
ForDo(enums, expr())
2074+
}
18722075
else {
18732076
if (!wrappedEnums) syntaxErrorOrIncomplete(YieldOrDoExpectedInForComprehension())
18742077
ForDo(enums, expr())
@@ -2675,7 +2878,7 @@ object Parsers {
26752878
}
26762879

26772880
/** ConstrExpr ::= SelfInvocation
2678-
* | ConstrBlock
2881+
* | `{' SelfInvocation {semi BlockStat} `}'
26792882
*/
26802883
def constrExpr(): Tree =
26812884
if (in.token == LBRACE) constrBlock()

compiler/src/dotty/tools/dotc/parsing/Scanners.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,10 @@ object Scanners {
222222
/** A switch whether operators at the start of lines can be infix operators */
223223
private var allowLeadingInfixOperators = true
224224

225+
val rewrite = ctx.settings.rewrite.value.isDefined
226+
val oldSyntax = ctx.settings.oldSyntax.value
227+
val newSyntax = ctx.settings.newSyntax.value
228+
225229
/** All doc comments kept by their end position in a `Map` */
226230
private[this] var docstringMap: SortedMap[Int, Comment] = SortedMap.empty
227231

@@ -236,7 +240,7 @@ object Scanners {
236240
def nextPos: Int = (lookahead.getc(): @switch) match {
237241
case ' ' | '\t' => nextPos
238242
case CR | LF | FF =>
239-
// if we encounter line delimitng whitespace we don't count it, since
243+
// if we encounter line delimiting whitespace we don't count it, since
240244
// it seems not to affect positions in source
241245
nextPos - 1
242246
case _ => lookahead.charOffset - 1
@@ -420,7 +424,7 @@ object Scanners {
420424
insertNL(NEWLINES)
421425
else if (!isLeadingInfixOperator)
422426
insertNL(NEWLINE)
423-
else if (isScala2Mode)
427+
else if (isScala2Mode || oldSyntax)
424428
ctx.warning(em"""Line starts with an operator;
425429
|it is now treated as a continuation of the expression on the previous line,
426430
|not as a separate statement.""",

0 commit comments

Comments
 (0)