Skip to content

Fix right associativity of extension methods #19203

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

Closed
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
11 changes: 8 additions & 3 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,14 @@ object desugar {

rightParam match
case ValDefs(vparam :: Nil) =>
if !vparam.mods.is(Given) then
if vparam.mods.is(Given) then
badRightAssoc("cannot start with using clause")
else if mdef.mods.is(Infix) then
// New encoding:
// we keep the extension method as is and rely on the swap of arguments at call site
extParamss ++ mdef.paramss
else
// Old encoding:
// we merge the extension parameters with the method parameters,
// swapping the operator arguments:
// e.g.
Expand All @@ -1012,8 +1019,6 @@ object desugar {
// If you change the names of the clauses below, also change them in right-associative-extension-methods.md
val (leftTyParamsAndLeadingUsing, leftParamAndTrailingUsing) = extParamss.span(isUsingOrTypeParamClause)
leftTyParamsAndLeadingUsing ::: rightTyParams ::: rightParam :: leftParamAndTrailingUsing ::: paramss1
else
badRightAssoc("cannot start with using clause")
case _ =>
badRightAssoc("must start with a single parameter")
case _ =>
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/Positioned.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import util.{SourceFile, SourcePosition, SrcPos}
import core.Contexts.*
import core.Decorators.*
import core.NameOps.*
import core.Flags.{JavaDefined, ExtensionMethod}
import core.Flags.{JavaDefined, ExtensionMethod, Infix}
import core.StdNames.nme
import ast.Trees.mods
import annotation.constructorOnly
Expand Down Expand Up @@ -215,7 +215,7 @@ abstract class Positioned(implicit @constructorOnly src: SourceFile) extends Src
check(tree.trailingParamss)
case tree: DefDef if tree.mods.is(ExtensionMethod) =>
tree.paramss match
case vparams1 :: vparams2 :: rest if tree.name.isRightAssocOperatorName =>
case vparams1 :: vparams2 :: rest if tree.name.isRightAssocOperatorName && !tree.mods.is(Infix) =>
// omit check for right-associatiove extension methods; their parameters were swapped
case _ =>
check(tree.paramss)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -933,7 +933,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
val coreSig =
if isExtension then
val paramss =
if tree.name.isRightAssocOperatorName then
if tree.name.isRightAssocOperatorName && !tree.mods.is(Infix) && !tree.symbol.is(Infix) then
// If you change the names of the clauses below, also change them in right-associative-extension-methods.md
// we have the following encoding of tree.paramss:
// (leftTyParams ++ leadingUsing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,22 @@ single explicit term parameter (in other words, `rightParam` is present). In the

The Scala compiler pre-processes a right-associative infix operation such as `x +: xs`
to `xs.+:(x)` if `x` is a pure expression or a call-by-name parameter and to `val y = x; xs.+:(y)` otherwise. This is necessary since a regular right-associative infix method
is defined in the class of its right operand. To make up for this swap,
is defined in the class of its right operand.

### Natural Order Right-Associative Extension Methods
If the right-associative extension methods is defined as infix, then the extension is used in its natural order. The `leftParam` is the receiver and
the `rightParam` is the argument. The order of the parameters is kept consistent with the order of the arguments at call site after desugaring.
For instance:

```scala
extension [T](xs: List[T])
infix def +:: (x: T): List[T] = ...

y +:: ys // ys.::y // +::(ys)(y)
```

### Inverted Right-Associative Extension Methods
To make up for the swap in the order at call site,
the expansion of right-associative extension methods performs the inverse parameter swap. More precisely, if `rightParam` is present, the total parameter sequence
of the extension method's expansion is:

Expand All @@ -59,6 +74,27 @@ For instance, the `+::` method above would become
```

This expansion has to be kept in mind when writing right-associative extension
methods with inter-parameter dependencies.
methods with inter-parameter dependencies. To avoid this limitation use _natural order right-associative extension methods_.

This expansion also introduces some inconsistencies when calling the extension methods in non infix form. The user needs to invert the order of the arguments at call site manually. For instance:

```scala
extension [T](x: T)
def *:(xs: List[T]): List[T] = ...

y.*:(ys) // error when following the parameter definition order
ys.*:(y)

*:(y)(ys) // error when following the parameter definition order
*:(ys)(y)
```

An overall simpler design could be obtained if right-associative operators could _only_ be defined as extension methods, and would be disallowed as normal methods. In that case neither arguments nor parameters would have to be swapped. Future versions of Scala should strive to achieve this simplification.
Another limitation of this representation is that it is impossible to pass the
type parameters of the `def` explicitly. For instance:

```scala
extension (x: Int)
def *:[T](xs: List[T]): List[T] = ???

xs.*:[Int](1) // error when trying to set T explicitly
```
13 changes: 13 additions & 0 deletions tests/pos/IArrayToCons.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import IArray.{+:, ++:}
def test(arr: IArray[Int]): Unit =
1 +: arr;
arr.+:(1);
+:(arr)(1);

arr ++: arr;
arr.++:(arr);
++:(arr)(arr);

Nil ++: arr;
arr.++:(Nil);
++:(arr)(Nil);
6 changes: 6 additions & 0 deletions tests/pos/i19197.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
extension (tuple: Tuple)
infix def **:[T >: tuple.type <: Tuple, H](x: H): H *: T = ???

def test1: (Int, String, Char) = 1 **: ("a", 'b')
def test2: (Int, String, Char) = ("a", 'b').**:(1)
def test3: (Int, String, Char) = **:("a", 'b')(1)