Skip to content

Question: desugaring of right associative extenstion methods #22170

Open
@Florian3k

Description

@Florian3k

With the way right associative extension methods are desugared (https://docs.scala-lang.org/scala3/reference/contextual/right-associative-extension-methods.html), some different methods end up with the same desugared form.
Both of those methods:

extension (x: X) def +:: [T](y: Y)
extension [T](x: X) def +:: (y: Y)

are desugared to:

<extension> def +:: [T](y: Y)(x: X)

It's important that the desugaring is reversible for some tools in the ecosystem, to display correct signature.
Scaladoc hacked solved this by sorting the parameters by their source positions, but the problem remains in ShortenedTypePrinter and RefinedPrinter:

if gsym.name.isRightAssocOperatorName then
val (leadingTyParamss, rest1) = paramss match
case fst :: tail if isTypeParamClause(fst) => (List(fst), tail)
case other => (List(), other)
val (leadingUsing, rest2) = rest1.span(isUsingClause)
val (rightTyParamss, rest3) = rest2.span(isTypeParamClause)
val (rightParamss, rest4) = rest3.splitAt(1)
val (leftParamss, rest5) = rest4.splitAt(1)
val (trailingUsing, rest6) = rest5.span(isUsingClause)
if leftParamss.nonEmpty then
leadingTyParamss ::: leadingUsing ::: leftParamss ::: rightTyParamss ::: rightParamss ::: trailingUsing ::: rest6
else paramss // it wasn't a binary operator, after all.

if tree.name.isRightAssocOperatorName 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
// ++ rightTyParams ++ rightParam
// ++ leftParam ++ trailingUsing ++ rest)
// e.g.
// extension [A](using B)(c: C)(using D)
// def %:[E](f: F)(g: G)(using H): Res = ???
// will have the following values:
// - leftTyParams = List(`[A]`)
// - leadingUsing = List(`(using B)`)
// - rightTyParams = List(`[E]`)
// - rightParam = List(`(f: F)`)
// - leftParam = List(`(c: C)`)
// - trailingUsing = List(`(using D)`)
// - rest = List(`(g: G)`, `(using H)`)
// we need to swap (rightTyParams ++ rightParam) with (leftParam ++ trailingUsing)
val (leftTyParams, rest1) = tree.paramss match
case fst :: tail if isTypeParamClause(fst) => (List(fst), tail)
case other => (List(), other)
val (leadingUsing, rest2) = rest1.span(isUsingClause)
val (rightTyParams, rest3) = rest2.span(isTypeParamClause)
val (rightParam, rest4) = rest3.splitAt(1)
val (leftParam, rest5) = rest4.splitAt(1)
val (trailingUsing, rest6) = rest5.span(isUsingClause)
if leftParam.nonEmpty then
leftTyParams ::: leadingUsing ::: leftParam ::: trailingUsing ::: rightTyParams ::: rightParam ::: rest6
else
tree.paramss // it wasn't a binary operator, after all.

This bug in ShortenedTypePrinter can be seen in Metals:
image

My question is, can we add some additional information in desugar so that the desugared form can be reversed easily to original signature?

This could be a flag added to type parameters that are next to extension keyword, maybe the third flag here (?):

/** An extension method, or a collective extension instance */
val (Extension @ _, ExtensionMethod @ _, _) = newFlags(28, "<extension>")

Or this could be an annotation attached to such type params:
extension [T](x: X) def +:: (y: Y) -> <extension> def +:: [@extensionTypeParam T](y: Y)(x: X)
extension (x: X) def +:: [T](y: Y) -> <extension> def +:: [T](y: Y)(x: X)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions