Skip to content

Allow infix operators at start of line #7031

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 1 commit into from
Aug 25, 2019
Merged
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
10 changes: 9 additions & 1 deletion compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ object desugar {
*/
val CheckIrrefutable: Property.Key[MatchCheck] = Property.StickyKey()

/** A multi-line infix operation with the infix operator starting a new line.
* Used for explaining potential errors.
*/
val MultiLineInfix: Property.Key[Unit] = Property.StickyKey()

/** What static check should be applied to a Match? */
enum MatchCheck {
case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom
Expand Down Expand Up @@ -1194,7 +1199,10 @@ object desugar {
case Tuple(args) => args.mapConserve(assignToNamedArg)
case _ => arg :: Nil
}
Apply(Select(fn, op.name).withSpan(selectPos), args)
val sel = Select(fn, op.name).withSpan(selectPos)
if (left.sourcePos.endLine < op.sourcePos.startLine)
sel.pushAttachment(MultiLineInfix, ())
Apply(sel, args)
}

if (isLeftAssoc(op.name))
Expand Down
48 changes: 39 additions & 9 deletions compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ object Scanners {
class Scanner(source: SourceFile, override val startFrom: Offset = 0)(implicit ctx: Context) extends ScannerCommon(source)(ctx) {
val keepComments: Boolean = !ctx.settings.YdropComments.value

/** A switch whether operators at the start of lines can be infix operators */
private var allowLeadingInfixOperators = true

/** All doc comments kept by their end position in a `Map` */
private[this] var docstringMap: SortedMap[Int, Comment] = SortedMap.empty

Expand Down Expand Up @@ -265,12 +268,12 @@ object Scanners {
else IDENTIFIER
}

private class TokenData0 extends TokenData
def newTokenData: TokenData = new TokenData {}

/** We need one token lookahead and one token history
*/
val next : TokenData = new TokenData0
private val prev : TokenData = new TokenData0
val next = newTokenData
private val prev = newTokenData

/** a stack of tokens which indicates whether line-ends can be statement separators
* also used for keeping track of nesting levels.
Expand Down Expand Up @@ -378,6 +381,30 @@ object Scanners {
next.token = EMPTY
}

def insertNL(nl: Token): Unit = {
next.copyFrom(this)
// todo: make offset line-end of previous line?
offset = if (lineStartOffset <= offset) lineStartOffset else lastLineStartOffset
token = nl
}


/** A leading symbolic or backquoted identifier is treated as an infix operator
* if it is followed by at least one ' ' and a token on the same line
* that can start an expression.
*/
def isLeadingInfixOperator =
allowLeadingInfixOperators &&
(token == BACKQUOTED_IDENT ||
token == IDENTIFIER && isOperatorPart(name(name.length - 1))) &&
(ch == ' ') && {
val lookahead = lookaheadScanner
lookahead.allowLeadingInfixOperators = false
// force a NEWLINE a after current token if it is on its own line
lookahead.nextToken()
canStartExpressionTokens.contains(lookahead.token)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we remove the field allowLeadingInfixOperators, all the tests pass. However, the field makes a semantic difference for the following program:

@main def Test = {
  val a = 5
  val x = 1
    + //
    `a` * 6

  assert(x == 1)
}

Maybe add the code above as a run test.


/** Insert NEWLINE or NEWLINES if
* - we are after a newline
* - we are within a { ... } or on toplevel (wrt sepRegions)
Expand All @@ -389,10 +416,15 @@ object Scanners {
(canStartStatTokens contains token) &&
(sepRegions.isEmpty || sepRegions.head == RBRACE ||
sepRegions.head == ARROW && token == CASE)) {
next copyFrom this
// todo: make offset line-end of previous line?
offset = if (lineStartOffset <= offset) lineStartOffset else lastLineStartOffset
token = if (pastBlankLine()) NEWLINES else NEWLINE
if (pastBlankLine())
insertNL(NEWLINES)
else if (!isLeadingInfixOperator)
insertNL(NEWLINE)
else if (isScala2Mode)
ctx.warning(em"""Line starts with an operator;
|it is now treated as a continuation of the expression on the previous line,
|not as a separate statement.""",
source.atSpan(Span(offset)))
}

postProcessToken()
Expand Down Expand Up @@ -1087,8 +1119,6 @@ object Scanners {
case _ => showToken(token)
}

// (does not seem to be needed) def flush = { charOffset = offset; nextChar(); this }

/* Resume normal scanning after XML */
def resume(lastToken: Token): Unit = {
token = lastToken
Expand Down
8 changes: 7 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,13 @@ trait TypeAssigner {
|An extension method was tried, but could not be fully constructed:
|
| ${failure.tree.show.replace("\n", "\n ")}"""
case _ => ""
case _ =>
if (tree.hasAttachment(desugar.MultiLineInfix))
i""".
|Note that `$name` is treated as an infix operator in Scala 3.
|If you do not want that, insert a `;` or empty line in front
|or drop any spaces behind the operator."""
else ""
}
errorType(NotAMember(qualType, name, kind, addendum), tree.sourcePos)
}
Expand Down
15 changes: 15 additions & 0 deletions tests/neg/multiLineOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
val x = 1
+ 2
+3 // error: Expected a toplevel definition

val b1 = {
22
* 22 // ok
*/*one more*/22 // error: end of statement expected
} // error: ';' expected, but '}' found

val b2: Boolean = {
println(x)
! "hello".isEmpty // error: value ! is not a member of Unit
}

26 changes: 26 additions & 0 deletions tests/pos/multiLineOps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
val x = 1
+ 2
+ 3

class Channel {
def ! (msg: String): Channel = this
def send_! (msg: String): Channel = this
}

val c = Channel()

def send() =
c ! "hello"
! "world"
send_! "!"

val b: Boolean =
"hello".isEmpty
&& true &&
!"hello".isEmpty

val b2: Boolean = {
println(x)
!"hello".isEmpty
???
}