Skip to content

Commit e598bef

Browse files
committed
Fix right associativity of extension methods
This patch removes the special treatment of extension methods while desugaring. Instead of swapping the arguments of the extension method at definition site, we let the usual desugaring take care of it. ```scala extension (xs: List[Int]) def -:(x: Int) = xs.remove(x) 1 -: List(2, 3) // desugars into List(2, 3).-:(1) 1 :: List(2, 3) // desugars into List(2, 3).::(1) ``` This implies that the following makes sense now: ```scala List(2, 3).-:(1) // equivalent to the one bellow -:(List(2, 3))(1) ``` Note that the parameters are passed in order as in the signature, as they should. This used to break intuition in the previous implementation. Futhermore, this fixes parameter scoping and dependencies, as it trivially respects the original signature. An example of this is in the following code example, where `T` is bounded by `tuple.type`, which would have been out of scope in the previous implementation due to the swapping of parameters. ```scala extension (tuple: Tuple) def *:[T >: tuple.type <: Tuple, H](x: H): H *: T = ... ``` The downside is that this is a breaking change as we change the semantics of the extension method definitions. We need to find a way to migrate from one to the other without breaking code. One option might be to have a different syntax for the new semantics, and deprecate the old one. One possibility would be to use `infix def` extensions to mark to mark these methods. One issue encountered in the library is with the `IArray.{++:, +:}` operations. Luckily, it seems that by rewriting them into the new semantics we get a binary and TASTy compatible signature. Fixes #19197
1 parent 7480582 commit e598bef

File tree

23 files changed

+144
-123
lines changed

23 files changed

+144
-123
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -996,7 +996,7 @@ object desugar {
996996

997997
def badRightAssoc(problem: String) =
998998
report.error(em"right-associative extension method $problem", mdef.srcPos)
999-
extParamss ++ mdef.paramss
999+
() // extParamss ++ mdef.paramss
10001000

10011001
rightParam match
10021002
case ValDefs(vparam :: Nil) =>
@@ -1010,15 +1010,16 @@ object desugar {
10101010
// def %:[A](using B)[E](f: F)(c: C)(using D)(g: G)(using H): Res = ???
10111011
//
10121012
// If you change the names of the clauses below, also change them in right-associative-extension-methods.md
1013-
val (leftTyParamsAndLeadingUsing, leftParamAndTrailingUsing) = extParamss.span(isUsingOrTypeParamClause)
1014-
leftTyParamsAndLeadingUsing ::: rightTyParams ::: rightParam :: leftParamAndTrailingUsing ::: paramss1
1013+
// val (leftTyParamsAndLeadingUsing, leftParamAndTrailingUsing) = extParamss.span(isUsingOrTypeParamClause)
1014+
() // leftTyParamsAndLeadingUsing ::: rightTyParams ::: rightParam :: leftParamAndTrailingUsing ::: paramss1
10151015
else
10161016
badRightAssoc("cannot start with using clause")
10171017
case _ =>
10181018
badRightAssoc("must start with a single parameter")
10191019
case _ =>
10201020
// no value parameters, so not an infix operator.
1021-
extParamss ++ mdef.paramss
1021+
() // extParamss ++ mdef.paramss
1022+
extParamss ++ mdef.paramss
10221023
else
10231024
extParamss ++ mdef.paramss
10241025
).withMods(mdef.mods | ExtensionMethod)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,8 @@ abstract class Positioned(implicit @constructorOnly src: SourceFile) extends Src
215215
check(tree.trailingParamss)
216216
case tree: DefDef if tree.mods.is(ExtensionMethod) =>
217217
tree.paramss match
218-
case vparams1 :: vparams2 :: rest if tree.name.isRightAssocOperatorName =>
219-
// omit check for right-associatiove extension methods; their parameters were swapped
218+
// case vparams1 :: vparams2 :: rest if tree.name.isRightAssocOperatorName =>
219+
// // omit check for right-associatiove extension methods; their parameters were swapped
220220
case _ =>
221221
check(tree.paramss)
222222
check(tree.tpt)

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -933,35 +933,35 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
933933
val coreSig =
934934
if isExtension then
935935
val paramss =
936-
if tree.name.isRightAssocOperatorName then
937-
// If you change the names of the clauses below, also change them in right-associative-extension-methods.md
938-
// we have the following encoding of tree.paramss:
939-
// (leftTyParams ++ leadingUsing
940-
// ++ rightTyParams ++ rightParam
941-
// ++ leftParam ++ trailingUsing ++ rest)
942-
// e.g.
943-
// extension [A](using B)(c: C)(using D)
944-
// def %:[E](f: F)(g: G)(using H): Res = ???
945-
// will have the following values:
946-
// - leftTyParams = List(`[A]`)
947-
// - leadingUsing = List(`(using B)`)
948-
// - rightTyParams = List(`[E]`)
949-
// - rightParam = List(`(f: F)`)
950-
// - leftParam = List(`(c: C)`)
951-
// - trailingUsing = List(`(using D)`)
952-
// - rest = List(`(g: G)`, `(using H)`)
953-
// we need to swap (rightTyParams ++ rightParam) with (leftParam ++ trailingUsing)
954-
val (leftTyParams, rest1) = tree.paramss.span(isTypeParamClause)
955-
val (leadingUsing, rest2) = rest1.span(isUsingClause)
956-
val (rightTyParams, rest3) = rest2.span(isTypeParamClause)
957-
val (rightParam, rest4) = rest3.splitAt(1)
958-
val (leftParam, rest5) = rest4.splitAt(1)
959-
val (trailingUsing, rest6) = rest5.span(isUsingClause)
960-
if leftParam.nonEmpty then
961-
leftTyParams ::: leadingUsing ::: leftParam ::: trailingUsing ::: rightTyParams ::: rightParam ::: rest6
962-
else
963-
tree.paramss // it wasn't a binary operator, after all.
964-
else
936+
// if tree.name.isRightAssocOperatorName then
937+
// // If you change the names of the clauses below, also change them in right-associative-extension-methods.md
938+
// // we have the following encoding of tree.paramss:
939+
// // (leftTyParams ++ leadingUsing
940+
// // ++ rightTyParams ++ rightParam
941+
// // ++ leftParam ++ trailingUsing ++ rest)
942+
// // e.g.
943+
// // extension [A](using B)(c: C)(using D)
944+
// // def %:[E](f: F)(g: G)(using H): Res = ???
945+
// // will have the following values:
946+
// // - leftTyParams = List(`[A]`)
947+
// // - leadingUsing = List(`(using B)`)
948+
// // - rightTyParams = List(`[E]`)
949+
// // - rightParam = List(`(f: F)`)
950+
// // - leftParam = List(`(c: C)`)
951+
// // - trailingUsing = List(`(using D)`)
952+
// // - rest = List(`(g: G)`, `(using H)`)
953+
// // we need to swap (rightTyParams ++ rightParam) with (leftParam ++ trailingUsing)
954+
// val (leftTyParams, rest1) = tree.paramss.span(isTypeParamClause)
955+
// val (leadingUsing, rest2) = rest1.span(isUsingClause)
956+
// val (rightTyParams, rest3) = rest2.span(isTypeParamClause)
957+
// val (rightParam, rest4) = rest3.splitAt(1)
958+
// val (leftParam, rest5) = rest4.splitAt(1)
959+
// val (trailingUsing, rest6) = rest5.span(isUsingClause)
960+
// if leftParam.nonEmpty then
961+
// leftTyParams ::: leadingUsing ::: leftParam ::: trailingUsing ::: rightTyParams ::: rightParam ::: rest6
962+
// else
963+
// tree.paramss // it wasn't a binary operator, after all.
964+
// else
965965
tree.paramss
966966
val trailingParamss = paramss
967967
.dropWhile(isUsingOrTypeParamClause)

library/src/scala/IArray.scala

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -319,14 +319,10 @@ object IArray:
319319
def zipAll[T1 >: T, U](that: Iterable[U], thisElem: T1, thatElem: U): IArray[(T1, U)] = genericArrayOps(arr).zipAll(that, thisElem, thatElem)
320320
def zipWithIndex: IArray[(T, Int)] = genericArrayOps(arr).zipWithIndex
321321

322-
extension [T, U >: T: ClassTag](prefix: IterableOnce[T])
323-
def ++:(arr: IArray[U]): IArray[U] = genericArrayOps(arr).prependedAll(prefix)
324-
325-
extension [T, U >: T: ClassTag](prefix: IArray[T])
326-
def ++:(arr: IArray[U]): IArray[U] = genericArrayOps(arr).prependedAll(prefix)
327-
328-
extension [T, U >: T: ClassTag](x: T)
329-
def +:(arr: IArray[U]): IArray[U] = genericArrayOps(arr).prepended(x)
322+
extension [T, U >: T: ClassTag](arr: IArray[U])
323+
def ++:(prefix: IterableOnce[T]): IArray[U] = genericArrayOps(arr).prependedAll(prefix)
324+
def ++:(prefix: IArray[T]): IArray[U] = genericArrayOps(arr).prependedAll(prefix)
325+
def +:(x: T): IArray[U] = genericArrayOps(arr).prepended(x)
330326

331327
// For backwards compatibility with code compiled without -Yexplicit-nulls
332328
private inline def mapNull[A, B](a: A, inline f: B): B =

presentation-compiler/test/dotty/tools/pc/tests/hover/HoverTypeSuite.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ class HoverTypeSuite extends BaseHoverSuite:
152152
|class C
153153
|
154154
|object Foo:
155-
| extension [T](using A)(main: T)(using B)
156-
| def %:[R](res: R)(using C): R = ???
155+
| extension [R](using A)(res: R)(using B)
156+
| def %:[T](main: T)(using C): R = ???
157157
| given A with {}
158158
| given B with {}
159159
| given C with {}
@@ -162,7 +162,7 @@ class HoverTypeSuite extends BaseHoverSuite:
162162
|end Foo
163163
|""".stripMargin,
164164
"""|Int
165-
|extension [T](using A)(main: T) def %:[R](res: R)(using B)(using C): R""".stripMargin.hover
165+
|extension [R](using A)(using B)(res: R) def %:[T](main: T)(using C): R""".stripMargin.hover
166166
)
167167

168168
@Test def `using` =

tests/neg-custom-args/captures/lazylists-exceptions.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ final class LazyCons[+T](val x: T, val xs: () => LazyList[T]^) extends LazyList[
2020
def tail: LazyList[T]^{this} = xs()
2121
end LazyCons
2222

23-
extension [A](x: A)
24-
def #:(xs1: => LazyList[A]^): LazyList[A]^{xs1} =
23+
extension [A](xs1: => LazyList[A]^)
24+
def #:(x: A): LazyList[A]^{xs1} =
2525
LazyCons(x, () => xs1)
2626

2727
def tabulate[A](n: Int)(gen: Int => A): LazyList[A]^{gen} =

tests/neg/i13075.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ object Implementing_Tuples:
88
type *:[H, T <: Tup] = ConsTup[H, T] // for type matching
99
type EmptyTup = EmptyTup.type // for type matching
1010

11-
extension [H](head: H)
12-
def *:[T <: Tup](tail: T) = ConsTup(head, tail)
11+
extension [T <: Tup](tail: T)
12+
def *:[H](head: H) = ConsTup(head, tail)
1313

1414
type Fold[T <: Tup, Seed, F[_,_]] = T match
1515
case EmptyTup => Seed

tests/neg/i9562.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ object Unrelated:
99
def h1: Int = foo // error
1010
def h2: Int = h1 + 1 // OK
1111
def h3: Int = g // error
12-
def ++: (x: Int): Int = h1 + x // OK
12+
extension (x: Int)
13+
def ++:(f: Foo): Int = f.h1 + x // OK

tests/pos-custom-args/captures/lazylists-exceptions.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ extension [A](xs: LzyList[A]^)
4242
if n == 0 then xs else xs.tail.drop(n - 1)
4343
end extension
4444

45-
extension [A](x: A)
46-
def #:(xs1: => LzyList[A]^): LzyList[A]^{xs1} =
45+
extension [A](xs1: => LzyList[A]^)
46+
def #:(x: A): LzyList[A]^{xs1} =
4747
LzyCons(x, () => xs1)
4848

4949
def lazyCons[A](x: A, xs1: => LzyList[A]^): LzyList[A]^{xs1} =

tests/pos-custom-args/captures/logger.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ final class LazyCons[+T](val x: T, val xs: () => LazyList[T]^) extends LazyList[
3232
def tail: LazyList[T]^{this} = xs()
3333
end LazyCons
3434

35-
extension [A](x: A)
36-
def #::(xs1: => LazyList[A]^): LazyList[A]^{xs1} =
35+
extension [A](xs1: => LazyList[A]^)
36+
def #::(x: A): LazyList[A]^{xs1} =
3737
LazyCons(x, () => xs1)
3838

3939
extension [A](xs: LazyList[A]^)

tests/pos-custom-args/captures/strictlists.scala

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,10 @@ extension [A](xs: StrictList[A])
2828
def concat(ys: StrictList[A]): StrictList[A] =
2929
if xs.isEmpty then ys
3030
else xs.head #: xs.tail.concat(ys)
31-
end extension
3231

33-
extension [A](x: A)
34-
def #:(xs1: StrictList[A]): StrictList[A] =
35-
StrictCons(x, xs1)
32+
def #:(x: A): StrictList[A] =
33+
StrictCons(x, xs)
34+
end extension
3635

3736
def tabulate[A](n: Int)(gen: Int => A) =
3837
def recur(i: Int): StrictList[A] =

tests/pos/IArrayToCons.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import IArray.{+:, ++:}
2+
def test(arr: IArray[Int]): Unit =
3+
1 +: arr;
4+
arr.+:(1);
5+
+:(arr)(1);
6+
7+
arr ++: arr;
8+
arr.++:(arr);
9+
++:(arr)(arr);
10+
11+
Nil ++: arr;
12+
arr.++:(Nil);
13+
++:(arr)(Nil);

tests/pos/i19197.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extension (tuple: Tuple)
2+
def **:[T >: tuple.type <: Tuple, H](x: H): H *: T = ???
3+
4+
def test1: (Int, String, Char) = 1 **: ("a", 'b')
5+
def test2: (Int, String, Char) = ("a", 'b').**:(1)
6+
def test3: (Int, String, Char) = **:("a", 'b')(1)

tests/pos/i9562.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ object Unrelated:
88
extension (f: Foo)
99
def h1: Int = 0
1010
def h2: Int = h1 + 1 // OK
11-
def ++: (x: Int): Int = h2 + x // OK
11+
12+
extension (x: Int)
13+
def ++: (f: Foo): Int = f.h2 + x // OK

tests/pos/reference/extension-methods.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ object ExtMethods:
1010
assert(circle.circumference == circumference(circle))
1111

1212
extension (x: String) def < (y: String) = x.compareTo(y) < 0
13-
extension [Elem](x: Elem) def #: (xs: Seq[Elem]) = x +: xs
13+
extension [Elem](xs: Seq[Elem]) def #: (x: Elem) = x +: xs
1414
extension (x: Number) infix def min (y: Number) = x
1515

1616
assert("a" < "bb")

tests/run/errorhandling/Result.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,15 @@ object Result:
3535
case (Err(e), Ok(_)) => Err(e :: Nil)
3636
case (Err(e1), Err(e2)) => Err(e1 :: e2 :: Nil)
3737

38+
end extension
39+
40+
extension [U <: Tuple, E](other: Result[U, List[E]])
3841
/** Validate both `r` and `other`; return a tuple of successes or a list of failures.
3942
* Unlike with `zip`, the right hand side `other` must be a `Result` returning a `Tuple`,
4043
* and the left hand side is added to it. See `Result.empty` for a convenient
4144
* right unit of chains of `*:`s.
4245
*/
43-
def *: [U <: Tuple](other: Result[U, List[E]]): Result[T *: U, List[E]] = (r, other) match
46+
def *: [T](r: Result[T, E]): Result[T *: U, List[E]] = (r, other) match
4447
case (Ok(x), Ok(ys)) => Ok(x *: ys)
4548
case (Ok(_), es: Err[?]) => es
4649
case (Err(e), Ok(_)) => Err(e :: Nil)

tests/run/export-in-extension.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ object O:
1111
export cm.*
1212
def succ: Int = x + 1
1313
def succ2: Int = succ + 1
14-
def ::: (y: Int) = x - y
14+
def ::: (y: Int) = y - x
1515

1616
object O2:
1717
import O.C
@@ -20,7 +20,7 @@ object O2:
2020
export cm.{bar, baz, bam, ::}
2121
def succ: Int = x + 1
2222
def succ2: Int = succ + 1
23-
def ::: (y: Int) = x - y
23+
def ::: (y: Int) = y - x
2424

2525
@main def Test =
2626
import O.*

tests/run/i11583.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ class Env:
1212
// */
1313
// def &&:[T <: ctx.Term](trm: T)(ext: env.Extra): (ctx.Type, T, env.Extra) = (tpe, trm, ext)
1414

15-
extension [Ctx <: Context](using ctx: Ctx)(tpe: String)(using env: Env)
16-
def :#:[T <: Boolean](trm: T)(ext: env.Extra): (String, T, env.Extra) = (tpe, trm, ext)
15+
extension [Ctx <: Context, T <: Boolean](using ctx: Ctx)(trm: T)(using env: Env)
16+
def :#:(tpe: String)(ext: env.Extra): (String, T, env.Extra) = (tpe, trm, ext)
1717

18-
extension [A](a: A)
19-
def :*:[T <: Tuple](t: T): A *: T = a *: t
18+
extension [T <: Tuple](t: T)
19+
def :*:[A](a: A): A *: T = a *: t
2020

2121
@main def Test =
2222

tests/run/i9530.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ trait Scope:
88
extension (using s: Scope)(expr: s.Expr)
99
def show = expr.toString
1010
def eval = s.value(expr)
11-
def *: (other: s.Expr) = s.combine(expr, other)
11+
def *: (other: s.Expr) = s.combine(other, expr)
1212

1313
def f(using s: Scope)(x: s.Expr): (String, s.Value) =
1414
(x.show, x.eval)

tests/run/instances.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ object Test extends App {
3131
extension [T](xs: List[List[T]])
3232
def flattened = xs.foldLeft[List[T]](Nil)(_ ++ _)
3333

34-
extension [T](x: T) def :: (xs: Seq[T]) = x +: xs
34+
extension [T](xs: Seq[T]) def :: (x: T) = x +: xs
3535

3636
val ss: Seq[Int] = List(1, 2, 3)
3737
val ss1 = 0 :: ss

0 commit comments

Comments
 (0)