Skip to content

Commit e9df26e

Browse files
committed
Refine encoding of right-associative extension methods
Leading using clauses now scope over both left and right parameter.
1 parent 5155811 commit e9df26e

File tree

6 files changed

+79
-24
lines changed

6 files changed

+79
-24
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,9 @@ object desugar {
883883
ext.vparamss ::: vparamss1
884884
vparams1 match
885885
case vparam :: Nil =>
886-
if !vparam.mods.is(Given) then vparams1 :: ext.vparamss ::: vparamss1
886+
if !vparam.mods.is(Given) then
887+
val (leadingUsing, otherExtParamss) = ext.vparamss.span(isUsingClause)
888+
leadingUsing ::: vparams1 :: otherExtParamss ::: vparamss1
887889
else badRightAssoc("cannot start with using clause")
888890
case _ =>
889891
badRightAssoc("must start with a single parameter")

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

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -794,29 +794,34 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
794794

795795
protected def defDefToText[T >: Untyped](tree: DefDef[T]): Text = {
796796
import untpd._
797-
798-
def splitParams(paramss: List[List[ValDef]]): (List[List[ValDef]], List[List[ValDef]]) =
799-
paramss match
800-
case params1 :: (rest @ (_ :: _)) if tree.name.isRightAssocOperatorName =>
801-
val (leading, trailing) = splitParams(rest)
802-
(leading, params1 :: trailing)
803-
case _ =>
804-
val trailing = paramss
805-
.dropWhile(isUsingClause)
806-
.drop(1)
807-
.dropWhile(isUsingClause)
808-
(paramss.take(paramss.length - trailing.length), trailing)
809-
810797
dclTextOr(tree) {
811798
val defKeyword = modText(tree.mods, tree.symbol, keywordStr("def"), isType = false)
812799
val isExtension = tree.hasType && tree.symbol.is(ExtensionMethod)
813800
withEnclosingDef(tree) {
814801
val (prefix, vparamss) =
815802
if isExtension then
816-
val (leadingParamss, otherParamss) = splitParams(tree.vparamss)
803+
val vparamss =
804+
if tree.name.isRightAssocOperatorName then
805+
// we have the encoding: leadingUsing rightParam trailingUsing leftParam
806+
// we need to swap rightParam and leftParam
807+
val (leadingUsing, rest1) = tree.vparamss.span(isUsingClause)
808+
val (rightParamss, rest2) = rest1.splitAt(1)
809+
val (trailingUsing, rest3) = rest2.span(isUsingClause)
810+
val (leftParamss, rest4) = rest3.splitAt(1)
811+
if leftParamss.nonEmpty then
812+
leadingUsing ::: leftParamss ::: trailingUsing ::: rightParamss ::: rest4
813+
else
814+
tree.vparamss // it wasn't a binary operator, after all.
815+
else
816+
tree.vparamss
817+
val trailingParamss = vparamss
818+
.dropWhile(isUsingClause)
819+
.drop(1)
820+
.dropWhile(isUsingClause)
821+
val leadingParamss = vparamss.take(vparamss.length - trailingParamss.length)
817822
(addVparamssText(keywordStr("extension "), leadingParamss)
818823
~~ (defKeyword ~~ valDefText(nameIdText(tree))).close,
819-
otherParamss)
824+
trailingParamss)
820825
else (defKeyword ~~ valDefText(nameIdText(tree)), tree.vparamss)
821826

822827
addVparamssText(prefix ~ tparamsText(tree.tparams), vparamss) ~

docs/docs/reference/contextual/extension-methods.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,7 @@ The three definitions above translate to
5858
Note the swap of the two parameters `x` and `xs` when translating
5959
the right-associative operator `+:` to an extension method. This is analogous
6060
to the implementation of right binding operators as normal methods. The Scala
61-
compiler preprocesses an infix operation `x +: xs` to `xs.+:(x)`, so the extension
62-
method ends up being applied to the sequence as first argument (in other words,
63-
the two swaps cancel each other out).
64-
61+
compiler preprocesses an infix operation `x +: xs` to `xs.+:(x)`, so the extension method ends up being applied to the sequence as first argument (in other words, the two swaps cancel each other out).
6562
### Generic Extensions
6663

6764
It is also possible to extend generic types by adding type parameters to an extension. For instance:
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
layout: doc-page
3+
title: "Right-Associative Extension Methods: Details"
4+
---
5+
6+
The most general form of leading parameters of an extension method is as follows:
7+
8+
- A possibly empty list of using clauses `leadingUsing`
9+
- A single parameter `extensionParam`
10+
- A possibly empty list of using clauses `trailingUsing`
11+
12+
This is then followed by `def`, the method name, and possibly further parameters
13+
`otherParams`. An example is:
14+
15+
```scala
16+
extension (using a: A, b: B)(using c: C) // <-- leadingUsing
17+
(x: X) // <-- extensionParam
18+
(using d: D) // <-- trailingUsing
19+
def +:: (y: Y)(using e: E)(z: Z) // <-- otherParams
20+
```
21+
An extension method is treated as a right associative operator if
22+
it has a name ending in `:` and is immediately followed by a
23+
single parameter. In the example above, that parameter is `(y: Y)`.
24+
25+
The Scala compiler pre-processes a right-associative infix operation such as `x +: xs`
26+
to `xs.+:(x)` if `x` and `xs` are pure expressions and to `val y = x; xs +: y` otherwise. This is necessary since a regular right-associative infix method
27+
is defined in the class of its right operand. To make up for this swap,
28+
the expansion of right-associative extension methods performs an analogous parameter swap. More precisely, if `otherParams` consists of a single parameter
29+
`rightParam` followed by `remaining`, the total parameter sequence
30+
of the extension method's expansion is:
31+
```
32+
leadingUsing rightParam trailingUsing extensionParam remaining
33+
```
34+
For instance, the `+::` method above would become
35+
```scala
36+
<extension> def +:: (using a: A, b: B)(using c: C)
37+
(y: Y)
38+
(using d: D)
39+
(x: X)
40+
(using e: E)(z: Z)
41+
```
42+
This expansion has to be kept in mind when writing right-associative extension
43+
methods with inter-parameter dependencies.
44+
45+
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.

tests/run/i9530.check

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Expr(123)
2+
123
3+
Expr(123)
4+
123
5+
Expr(1234)
6+
1234

tests/pos/i9530.scala renamed to tests/run/i9530.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ trait Scope:
33
type Value
44
def expr(x: String): Expr
55
def value(e: Expr): Value
6-
def combine(e: Expr, str: String): Expr
6+
def combine(e1: Expr, e2: Expr): Expr
77

88
extension (using s: Scope)(expr: s.Expr)
99
def show = expr.toString
1010
def eval = s.value(expr)
11-
def *: (str: String) = s.combine(expr, str)
11+
def *: (other: s.Expr) = s.combine(expr, other)
1212

1313
def f(using s: Scope)(x: s.Expr): (String, s.Value) =
1414
(x.show, x.eval)
@@ -18,7 +18,7 @@ given scope: Scope with
1818
type Value = Int
1919
def expr(x: String) = Expr(x)
2020
def value(e: Expr) = e.str.toInt
21-
def combine(e: Expr, str: String) = Expr(e.str ++ str)
21+
def combine(e1: Expr, e2: Expr) = Expr(e1.str ++ e2.str)
2222

2323
@main def Test =
2424
val e = scope.Expr("123")
@@ -29,7 +29,7 @@ given scope: Scope with
2929
println(ss)
3030
val vv = e.eval
3131
println(vv)
32-
val e2 = e *: "4"
32+
val e2 = e *: scope.Expr("4")
3333
println(e2.show)
3434
println(e2.eval)
3535

0 commit comments

Comments
 (0)