Skip to content

Commit 7b0aeca

Browse files
committed
New scheme for infix operators
- An infix operation `x op (y, z)` now unconditionally interprets `(y, z)` as a tuple, unless in Scala-2 mode. - In Scala-2 mode, the pattern will give a migration warning with option to rewrite in most cases. Excluded from rewrite are operator assignments and right-associative infix operations. - Auto-tupling is only enabled in `Scala-2` mode, and will give a migration warning with option to rewrite in that mode.
1 parent 024e4ba commit 7b0aeca

File tree

6 files changed

+88
-40
lines changed

6 files changed

+88
-40
lines changed

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -776,18 +776,16 @@ object desugar {
776776
* ==> r.op(l) if op is right-associative
777777
*/
778778
def binop(left: Tree, op: Ident, right: Tree)(implicit ctx: Context): Apply = {
779-
def assignToNamedArg(arg: Tree) = arg match {
779+
780+
def assignToNamedArg(arg: Tree): Tree = arg match {
780781
case Assign(Ident(name), rhs) => cpy.NamedArg(arg)(name, rhs)
782+
case Parens(arg1) => untpd.cpy.Parens(arg)(assignToNamedArg(arg1))
783+
case Tuple(args) => untpd.cpy.Tuple(arg)(args.mapConserve(assignToNamedArg))
781784
case _ => arg
782785
}
783-
def makeOp(fn: Tree, arg: Tree, selectPos: Position) = {
784-
val args: List[Tree] = arg match {
785-
case Parens(arg) => assignToNamedArg(arg) :: Nil
786-
case Tuple(args) => args.mapConserve(assignToNamedArg)
787-
case _ => arg :: Nil
788-
}
789-
Apply(Select(fn, op.name).withPos(selectPos), args)
790-
}
786+
787+
def makeOp(fn: Tree, arg: Tree, selectPos: Position) =
788+
Apply(Select(fn, op.name).withPos(selectPos), assignToNamedArg(arg))
791789

792790
if (isLeftAssoc(op.name))
793791
makeOp(left, right, Position(left.pos.start, op.pos.end, op.pos.start))

compiler/src/dotty/tools/dotc/transform/Erasure.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ object Erasure {
481481
/** Besides normal typing, this method collects all arguments
482482
* to a compacted function into a single argument of array type.
483483
*/
484-
override def typedApply(tree: untpd.Apply, pt: Type)(implicit ctx: Context): Tree = {
484+
override def typedApply(tree: untpd.Apply, pt: Type, scala2InfixOp: Boolean)(implicit ctx: Context): Tree = {
485485
val Apply(fun, args) = tree
486486
if (fun.symbol == defn.cbnArg)
487487
typedUnadapted(args.head, pt)

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

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import StdNames._
2323
import NameKinds.DefaultGetterName
2424
import ProtoTypes._
2525
import Inferencing._
26+
import rewrite.Rewrites.patch
2627

2728
import collection.mutable
2829
import config.Printers.{overload, typr, unapp}
@@ -242,7 +243,7 @@ trait Applications extends Compatibility { self: Typer with Dynamic =>
242243
def success = ok
243244

244245
protected def methodType = methType.asInstanceOf[MethodType]
245-
private def methString: String = i"${methRef.symbol}: ${methType.show}"
246+
private def methString: String = i"${methRef.symbol.showLocated}: ${methType.show}"
246247

247248
/** Re-order arguments to correctly align named arguments */
248249
def reorder[T >: Untyped](args: List[Trees.Tree[T]]): List[Trees.Tree[T]] = {
@@ -669,10 +670,19 @@ trait Applications extends Compatibility { self: Typer with Dynamic =>
669670
* or, if application is an operator assignment, also an `Assign` or
670671
* Block node.
671672
*/
672-
def typedApply(tree: untpd.Apply, pt: Type)(implicit ctx: Context): Tree = {
673-
673+
def typedApply(tree: untpd.Apply, pt: Type, scala2InfixOp: Boolean = false)(implicit ctx: Context): Tree = {
674674
def realApply(implicit ctx: Context): Tree = track("realApply") {
675-
val originalProto = new FunProto(tree.args, IgnoredProto(pt), this)(argCtx(tree))
675+
var originalProto = new FunProto(tree.args, IgnoredProto(pt), this)(argCtx(tree))
676+
677+
// In an infix operation `x op (y, z)` under Scala-2 mode, rewrite tuple on the
678+
// right hand side to multi-parameters, i.e `x.op(y, z)`, relying on auto-tupling
679+
// to revert this if `op` takes a single argument.
680+
tree.args match {
681+
case untpd.Tuple(elems) :: Nil if scala2InfixOp =>
682+
originalProto = new FunProto(elems, IgnoredProto(pt), this)(argCtx(tree))
683+
originalProto.scala2InfixOp = true
684+
case _ =>
685+
}
676686
val fun1 = typedExpr(tree.fun, originalProto)
677687

678688
// Warning: The following lines are dirty and fragile. We record that auto-tupling was demanded as
@@ -682,7 +692,41 @@ trait Applications extends Compatibility { self: Typer with Dynamic =>
682692
// otherwise we would get possible cross-talk between different `adapt` calls using the same
683693
// prototype. A cleaner alternative would be to return a modified prototype from `adapt` together with
684694
// a modified tree but this would be more convoluted and less efficient.
685-
val proto = if (originalProto.isTupled) originalProto.tupled else originalProto
695+
// All of this is provisional in the long run, since it is only needed under Scala-2 mode.
696+
val proto =
697+
if (originalProto.isTupled) originalProto.tupled
698+
else {
699+
// If we see a multi-parameter infix operation `x op (y, z)` under Scala-2 mode,
700+
// issue a migration warning and propose a rewrite to `x .op (y, z)` (or
701+
// `(x) .op (y, z)` is `x` is itself an infix operation) where this is possible.
702+
// Not possible are rewrites of operator assignments and right-associative operators.
703+
tree.fun match {
704+
case sel @ Select(qual, opName)
705+
if originalProto.scala2InfixOp && !fun1.tpe.widen.isErroneous =>
706+
val opEndOffset = sel.pos.point + opName.toString.length
707+
val isOpAssign = ctx.source(opEndOffset) == '='
708+
val isRightAssoc = !isLeftAssoc(opName)
709+
val addendum =
710+
if (isOpAssign) "\nThis requires a manual rewrite since it is part of an operator assignment."
711+
else if (isRightAssoc) "\nThis requires a manual rewrite since the operator is right-associative."
712+
else "\nThis can be fixed automatically using -rewrite."
713+
ctx.migrationWarning(
714+
em"""infix operator takes exactly one argument,
715+
|use method call syntax with `.` instead.$addendum""", fun1.pos)
716+
val Select(qual, _) = tree.fun
717+
if (!isOpAssign && !isRightAssoc) {
718+
patch(Position(sel.pos.point), ".")
719+
qual match {
720+
case qual: untpd.InfixOp =>
721+
patch(qual.pos.startPos, "(")
722+
patch(qual.pos.endPos, ")")
723+
case _ =>
724+
}
725+
}
726+
case _ =>
727+
}
728+
originalProto
729+
}
686730

687731
// If some of the application's arguments are function literals without explicitly declared
688732
// parameter types, relate the normalized result type of the application with the
@@ -750,12 +794,25 @@ trait Applications extends Compatibility { self: Typer with Dynamic =>
750794
* { val xs = es; e' = e' + args }
751795
*/
752796
def typedOpAssign(implicit ctx: Context): Tree = track("typedOpAssign") {
753-
val Apply(Select(lhs, name), rhss) = tree
797+
val Apply(sel @ Select(lhs, name), rhss) = tree
754798
val lhs1 = typedExpr(lhs)
755799
val liftedDefs = new mutable.ListBuffer[Tree]
756800
val lhs2 = untpd.TypedSplice(LiftComplex.liftAssigned(liftedDefs, lhs1))
757-
val assign = untpd.Assign(lhs2,
758-
untpd.Apply(untpd.Select(lhs2, name.asSimpleName.dropRight(1)), rhss))
801+
val opName = name.asSimpleName.dropRight(1)
802+
803+
// If original operation was an infix operation under Scala-2 mode, generate
804+
// again an infix operation insteadof an application, so that the logic does
805+
// the right thing for migrating from auto-tupling and multi-parameter infix ops.
806+
val app = rhss match {
807+
case (rhs: untpd.Tuple) :: Nil if scala2InfixOp =>
808+
val opEndOffset = sel.pos.point + opName.length
809+
assert(ctx.source(opEndOffset) == '=')
810+
val op = untpd.Ident(opName).withPos(Position(sel.pos.point, opEndOffset))
811+
untpd.InfixOp(lhs2, op, rhs)
812+
case _ =>
813+
untpd.Apply(untpd.Select(lhs2, opName), rhss)
814+
}
815+
val assign = untpd.Assign(lhs2, app)
759816
wrapDefs(liftedDefs, typed(assign))
760817
}
761818

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -564,13 +564,13 @@ class Inliner(call: tpd.Tree, rhs: tpd.Tree)(implicit ctx: Context) {
564564
}
565565
}
566566

567-
override def typedApply(tree: untpd.Apply, pt: Type)(implicit ctx: Context) =
567+
override def typedApply(tree: untpd.Apply, pt: Type, scala2InfixOp: Boolean)(implicit ctx: Context) =
568568
tree.asInstanceOf[tpd.Tree] match {
569569
case Apply(Select(InlineableArg(closure(_, fn, _)), nme.apply), args) =>
570570
inlining.println(i"reducing $tree with closure $fn")
571571
typed(fn.appliedToArgs(args), pt)
572572
case _ =>
573-
super.typedApply(tree, pt)
573+
super.typedApply(tree, pt, scala2InfixOp)
574574
}
575575
}
576576
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@ object ProtoTypes {
288288
def typeOfArg(arg: untpd.Tree)(implicit ctx: Context): Type =
289289
myTypedArg(arg).tpe
290290

291+
// `scala2InfixOp` and `tupled` only needed for Scala-2 compatibility and migration
292+
293+
/** FunProto applies to an infix operation under -language:Scala2 */
294+
var scala2InfixOp = false
295+
291296
private[this] var myTupled: Type = NoType
292297

293298
/** The same proto-type but with all arguments combined in a single tuple */

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

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,7 +1709,7 @@ class Typer extends Namer
17091709
*/
17101710
def typedInfixOp(tree: untpd.InfixOp, pt: Type)(implicit ctx: Context): Tree = {
17111711
val untpd.InfixOp(l, op, r) = tree
1712-
val app = typedApply(desugar.binop(l, op, r), pt)
1712+
val app = typedApply(desugar.binop(l, op, r), pt, scala2InfixOp = ctx.scala2Mode)
17131713
if (untpd.isLeftAssoc(op.name)) app
17141714
else {
17151715
val defs = new mutable.ListBuffer[Tree]
@@ -2037,22 +2037,13 @@ class Typer extends Namer
20372037
def canAutoTuple(args: List[untpd.Tree])(implicit ctx: Context): Boolean = {
20382038
val pos = Position(args.map(_.pos.start).min, args.map(_.pos.end).max)
20392039
ctx.testScala2Mode(
2040-
i"""auto-tupling is no longer supported in this case,
2041-
|arguments now need to be enclosed in parentheses (...).""",
2040+
i"""auto-tupling is no longer supported,
2041+
|arguments now need to be enclosed in parentheses (...).""",
20422042
pos, { patch(pos.startPos, "("); patch(pos.endPos, ")") })
20432043
}
20442044

2045-
def takesAutoTupledArgs(tp: Type, sym: Symbol, args: List[untpd.Tree])(implicit ctx: Context): Boolean = tp match {
2046-
case tp: MethodicType =>
2047-
tp.firstParamTypes match {
2048-
case ptype :: Nil => !ptype.isRepeatedParam && (sym.name.isOpName || canAutoTuple(args))
2049-
case _ => false
2050-
}
2051-
case tp: TermRef =>
2052-
tp.denot.alternatives.forall(alt => takesAutoTupledArgs(alt.info, alt.symbol, args))
2053-
case _ =>
2054-
false
2055-
}
2045+
def takesAutoTupledArgs(tp: Type, pt: FunProto)(implicit ctx: Context): Boolean =
2046+
pt.args.lengthCompare(1) > 0 && isUnary(tp) && (pt.scala2InfixOp || canAutoTuple(pt.args))
20562047

20572048
/** Perform the following adaptations of expression, pattern or type `tree` wrt to
20582049
* given prototype `pt`:
@@ -2143,10 +2134,8 @@ class Typer extends Namer
21432134

21442135
def adaptToArgs(wtp: Type, pt: FunProto): Tree = wtp match {
21452136
case _: MethodOrPoly =>
2146-
if (pt.args.lengthCompare(1) > 0 && takesAutoTupledArgs(wtp, tree.symbol, pt.args))
2147-
adapt(tree, pt.tupled, locked)
2148-
else
2149-
tree
2137+
if (takesAutoTupledArgs(wtp, pt)) adapt(tree, pt.tupled, locked)
2138+
else tree
21502139
case _ => tryInsertApplyOrImplicit(tree, pt, locked) {
21512140
errorTree(tree, MethodDoesNotTakeParameters(tree, methPart(tree).tpe)(err))
21522141
}
@@ -2511,8 +2500,7 @@ class Typer extends Namer
25112500
tree
25122501
case ref: TermRef => // this case can happen in case tree.tpe is overloaded
25132502
pt match {
2514-
case pt: FunProto
2515-
if pt.args.lengthCompare(1) > 0 && takesAutoTupledArgs(ref, ref.symbol, pt.args) =>
2503+
case pt: FunProto if takesAutoTupledArgs(ref, pt) =>
25162504
adapt(tree, pt.tupled, locked)
25172505
case _ =>
25182506
adaptOverloaded(ref)

0 commit comments

Comments
 (0)