Skip to content

Commit f467be6

Browse files
authored
Merge pull request #2007 from dotty-staging/ennru_MixedLeftAndRightAssoc
mixed left and right assoc
2 parents f76ffe9 + 6b522cc commit f467be6

File tree

4 files changed

+67
-10
lines changed

4 files changed

+67
-10
lines changed

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -404,14 +404,13 @@ object Parsers {
404404

405405
var opStack: List[OpInfo] = Nil
406406

407-
def checkAssoc(offset: Int, op: Name, leftAssoc: Boolean) =
408-
if (isLeftAssoc(op) != leftAssoc)
409-
syntaxError(
410-
"left- and right-associative operators with same precedence may not be mixed", offset)
407+
def checkAssoc(offset: Token, op1: Name, op2: Name, op2LeftAssoc: Boolean): Unit =
408+
if (isLeftAssoc(op1) != op2LeftAssoc)
409+
syntaxError(MixedLeftAndRightAssociativeOps(op1, op2, op2LeftAssoc), offset)
411410

412-
def reduceStack(base: List[OpInfo], top: Tree, prec: Int, leftAssoc: Boolean): Tree = {
411+
def reduceStack(base: List[OpInfo], top: Tree, prec: Int, leftAssoc: Boolean, op2: Name): Tree = {
413412
if (opStack != base && precedence(opStack.head.operator.name) == prec)
414-
checkAssoc(opStack.head.offset, opStack.head.operator.name, leftAssoc)
413+
checkAssoc(opStack.head.offset, opStack.head.operator.name, op2, leftAssoc)
415414
def recur(top: Tree): Tree = {
416415
if (opStack == base) top
417416
else {
@@ -445,20 +444,20 @@ object Parsers {
445444
var top = first
446445
while (isIdent && in.name != notAnOperator) {
447446
val op = if (isType) typeIdent() else termIdent()
448-
top = reduceStack(base, top, precedence(op.name), isLeftAssoc(op.name))
447+
top = reduceStack(base, top, precedence(op.name), isLeftAssoc(op.name), op.name)
449448
opStack = OpInfo(top, op, in.offset) :: opStack
450449
newLineOptWhenFollowing(canStartOperand)
451450
if (maybePostfix && !canStartOperand(in.token)) {
452451
val topInfo = opStack.head
453452
opStack = opStack.tail
454-
val od = reduceStack(base, topInfo.operand, 0, true)
453+
val od = reduceStack(base, topInfo.operand, 0, true, in.name)
455454
return atPos(startOffset(od), topInfo.offset) {
456455
PostfixOp(od, topInfo.operator)
457456
}
458457
}
459458
top = operand()
460459
}
461-
reduceStack(base, top, 0, true)
460+
reduceStack(base, top, 0, true, in.name)
462461
}
463462

464463
/* -------- IDENTIFIERS AND LITERALS ------------------------------------------- */

compiler/src/dotty/tools/dotc/reporting/diagnostic/ErrorMessageID.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ public enum ErrorMessageID {
4848
OverridesNothingID,
4949
OverridesNothingButNameExistsID,
5050
ForwardReferenceExtendsOverDefinitionID,
51-
ExpectedTokenButFoundID;
51+
ExpectedTokenButFoundID,
52+
MixedLeftAndRightAssociativeOpsID;
5253

5354
public int errorNumber() {
5455
return ordinal() - 2;

compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,4 +1093,38 @@ object messages {
10931093
|""".stripMargin
10941094
}
10951095

1096+
case class MixedLeftAndRightAssociativeOps(op1: Name, op2: Name, op2LeftAssoc: Boolean)(implicit ctx: Context)
1097+
extends Message(MixedLeftAndRightAssociativeOpsID) {
1098+
val kind = "Syntax"
1099+
val op1Asso = if (op2LeftAssoc) "which is right-associative" else "which is left-associative"
1100+
val op2Asso = if (op2LeftAssoc) "which is left-associative" else "which is right-associative"
1101+
val msg = s"`${op1}` (${op1Asso}) and `${op2}` ($op2Asso) have same precedence and may not be mixed"
1102+
val explanation =
1103+
s"""|The operators ${op1} and ${op2} are used as infix operators in the same expression,
1104+
|but they bind to different sides:
1105+
|${op1} is applied to the operand to its ${if (op2LeftAssoc) "right" else "left"}
1106+
|${op2} is applied to the operand to its ${if (op2LeftAssoc) "left" else "right"}
1107+
|As both have the same precedence the compiler can't decide which to apply first.
1108+
|
1109+
|You may use parenthesis to make the application order explicit,
1110+
|or use method application syntax `operand1.${op1}(operand2)`.
1111+
|
1112+
|Operators ending in a colon `:` are right-associative. All other operators are left-associative.
1113+
|
1114+
|Infix operator precedence is determined by the operator's first character. Characters are listed
1115+
|below in increasing order of precedence, with characters on the same line having the same precedence.
1116+
| (all letters)
1117+
| |
1118+
| ^
1119+
| &
1120+
| = !
1121+
| < >
1122+
| :
1123+
| + -
1124+
| * / %
1125+
| (all other special characters)
1126+
|Operators starting with a letter have lowest precedence, followed by operators starting with `|`, etc.
1127+
|""".stripMargin
1128+
}
1129+
10961130
}

compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,27 @@ class ErrorMessagesTests extends ErrorMessagesTest {
136136
""".stripMargin
137137
}
138138
.expectNoErrors
139+
140+
@Test def leftAndRightAssociative =
141+
checkMessagesAfter("frontend") {
142+
"""
143+
|object Ops {
144+
| case class I(j: Int) {
145+
| def +-(i: Int) = i
146+
| def +:(i: Int) = i
147+
| }
148+
| val v = I(1) +- I(4) +: I(4)
149+
|}
150+
""".stripMargin
151+
}
152+
.expect { (ictx, messages) =>
153+
implicit val ctx: Context = ictx
154+
val defn = ictx.definitions
155+
156+
assertMessageCount(1, messages)
157+
val MixedLeftAndRightAssociativeOps(op1, op2, op2LeftAssoc) :: Nil = messages
158+
assertEquals("+-", op1.show)
159+
assertEquals("+:", op2.show)
160+
assertFalse(op2LeftAssoc)
161+
}
139162
}

0 commit comments

Comments
 (0)