Skip to content

Commit 93fd8db

Browse files
committed
Allow infix operators at start of line
1 parent 2c12b93 commit 93fd8db

File tree

5 files changed

+96
-11
lines changed

5 files changed

+96
-11
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ object desugar {
3939
*/
4040
val CheckIrrefutable: Property.Key[MatchCheck] = Property.StickyKey()
4141

42+
/** A multi-line infix operation with the infix operator starting a new line.
43+
* Used for explaining potential errors.
44+
*/
45+
val MultiLineInfix: Property.Key[Unit] = Property.StickyKey()
46+
4247
/** What static check should be applied to a Match? */
4348
enum MatchCheck {
4449
case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom
@@ -1194,7 +1199,10 @@ object desugar {
11941199
case Tuple(args) => args.mapConserve(assignToNamedArg)
11951200
case _ => arg :: Nil
11961201
}
1197-
Apply(Select(fn, op.name).withSpan(selectPos), args)
1202+
val sel = Select(fn, op.name).withSpan(selectPos)
1203+
if (left.sourcePos.endLine < op.sourcePos.startLine)
1204+
sel.pushAttachment(MultiLineInfix, ())
1205+
Apply(sel, args)
11981206
}
11991207

12001208
if (isLeftAssoc(op.name))

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ object Scanners {
219219
class Scanner(source: SourceFile, override val startFrom: Offset = 0)(implicit ctx: Context) extends ScannerCommon(source)(ctx) {
220220
val keepComments: Boolean = !ctx.settings.YdropComments.value
221221

222+
/** A switch whether operators at the start of lines can be infix operators */
223+
private var allowLeadingInfixOperators = true
224+
222225
/** All doc comments kept by their end position in a `Map` */
223226
private[this] var docstringMap: SortedMap[Int, Comment] = SortedMap.empty
224227

@@ -265,12 +268,12 @@ object Scanners {
265268
else IDENTIFIER
266269
}
267270

268-
private class TokenData0 extends TokenData
271+
def newTokenData: TokenData = new TokenData {}
269272

270273
/** We need one token lookahead and one token history
271274
*/
272-
val next : TokenData = new TokenData0
273-
private val prev : TokenData = new TokenData0
275+
val next = newTokenData
276+
private val prev = newTokenData
274277

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

384+
def insertNL(nl: Token): Unit = {
385+
next.copyFrom(this)
386+
// todo: make offset line-end of previous line?
387+
offset = if (lineStartOffset <= offset) lineStartOffset else lastLineStartOffset
388+
token = nl
389+
}
390+
391+
392+
/** A leading symbolic or backquoted identifier is treated as an infix operator
393+
* if it is followed by at least one ' ' and a token on the same line
394+
* that can start an expression.
395+
*/
396+
def isLeadingInfixOperator =
397+
allowLeadingInfixOperators &&
398+
(token == BACKQUOTED_IDENT ||
399+
token == IDENTIFIER && isOperatorPart(name(name.length - 1))) &&
400+
(ch == ' ') && {
401+
val lookahead = lookaheadScanner
402+
lookahead.allowLeadingInfixOperators = false
403+
// force a NEWLINE a after current token if it is on its own line
404+
lookahead.nextToken()
405+
canStartExpressionTokens.contains(lookahead.token)
406+
}
407+
381408
/** Insert NEWLINE or NEWLINES if
382409
* - we are after a newline
383410
* - we are within a { ... } or on toplevel (wrt sepRegions)
@@ -389,10 +416,15 @@ object Scanners {
389416
(canStartStatTokens contains token) &&
390417
(sepRegions.isEmpty || sepRegions.head == RBRACE ||
391418
sepRegions.head == ARROW && token == CASE)) {
392-
next copyFrom this
393-
// todo: make offset line-end of previous line?
394-
offset = if (lineStartOffset <= offset) lineStartOffset else lastLineStartOffset
395-
token = if (pastBlankLine()) NEWLINES else NEWLINE
419+
if (pastBlankLine())
420+
insertNL(NEWLINES)
421+
else if (!isLeadingInfixOperator)
422+
insertNL(NEWLINE)
423+
else if (isScala2Mode)
424+
ctx.warning(em"""Line starts with an operator;
425+
|it is now treated as a continuation of the expression on the previous line,
426+
|not as a separate statement.""",
427+
source.atSpan(Span(offset)))
396428
}
397429

398430
postProcessToken()
@@ -1087,8 +1119,6 @@ object Scanners {
10871119
case _ => showToken(token)
10881120
}
10891121

1090-
// (does not seem to be needed) def flush = { charOffset = offset; nextChar(); this }
1091-
10921122
/* Resume normal scanning after XML */
10931123
def resume(lastToken: Token): Unit = {
10941124
token = lastToken

compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,13 @@ trait TypeAssigner {
271271
|An extension method was tried, but could not be fully constructed:
272272
|
273273
| ${failure.tree.show.replace("\n", "\n ")}"""
274-
case _ => ""
274+
case _ =>
275+
if (tree.hasAttachment(desugar.MultiLineInfix))
276+
i""".
277+
|Note that `$name` is treated as an infix operator in Scala 3.
278+
|If you do not want that, insert a `;` or empty line in front
279+
|or drop any spaces behind the operator."""
280+
else ""
275281
}
276282
errorType(NotAMember(qualType, name, kind, addendum), tree.sourcePos)
277283
}

tests/neg/multiLineOps.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
val x = 1
2+
+ 2
3+
+3 // error: Expected a toplevel definition
4+
5+
val b1 = {
6+
22
7+
* 22 // ok
8+
*/*one more*/22 // error: end of statement expected
9+
} // error: ';' expected, but '}' found
10+
11+
val b2: Boolean = {
12+
println(x)
13+
! "hello".isEmpty // error: value ! is not a member of Unit
14+
}
15+

tests/pos/multiLineOps.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
val x = 1
2+
+ 2
3+
+ 3
4+
5+
class Channel {
6+
def ! (msg: String): Channel = this
7+
def send_! (msg: String): Channel = this
8+
}
9+
10+
val c = Channel()
11+
12+
def send() =
13+
c ! "hello"
14+
! "world"
15+
send_! "!"
16+
17+
val b: Boolean =
18+
"hello".isEmpty
19+
&& true &&
20+
!"hello".isEmpty
21+
22+
val b2: Boolean = {
23+
println(x)
24+
!"hello".isEmpty
25+
???
26+
}

0 commit comments

Comments
 (0)