diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 3386dc7d7a6c..22ba1fb698b6 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -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. @@ -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 _ => diff --git a/compiler/src/dotty/tools/dotc/ast/Positioned.scala b/compiler/src/dotty/tools/dotc/ast/Positioned.scala index d8017783f47f..4dd171393d92 100644 --- a/compiler/src/dotty/tools/dotc/ast/Positioned.scala +++ b/compiler/src/dotty/tools/dotc/ast/Positioned.scala @@ -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 @@ -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) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 8ad1188a3e7e..bc7060df72d5 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -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 diff --git a/docs/_docs/reference/contextual/right-associative-extension-methods.md b/docs/_docs/reference/contextual/right-associative-extension-methods.md index 61f0beece6ed..5227ee44533a 100644 --- a/docs/_docs/reference/contextual/right-associative-extension-methods.md +++ b/docs/_docs/reference/contextual/right-associative-extension-methods.md @@ -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: @@ -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 +``` diff --git a/tests/pos/IArrayToCons.scala b/tests/pos/IArrayToCons.scala new file mode 100644 index 000000000000..6a0acce88391 --- /dev/null +++ b/tests/pos/IArrayToCons.scala @@ -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); diff --git a/tests/pos/i19197.scala b/tests/pos/i19197.scala new file mode 100644 index 000000000000..826c80269f74 --- /dev/null +++ b/tests/pos/i19197.scala @@ -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)