Skip to content

Commit 9af2ec5

Browse files
committed
Allow exports in extension clauses
This is part of a long term strategy to get deprecate and remove general implicit conversions in Scala. In the thread https://contributors.scala-lang.org/t/can-we-wean-scala-off-implicit-conversions/4388 we identified two areas where implicit conversions or implicit classes were still essential. One was adding methods to new types in bulk. This can be achieved by defining an implicit class that inherits from some other classes that define the methods. Exports in extension methods provide a similar functionality without relying on implicit conversions under the covers.
1 parent 97493c7 commit 9af2ec5

File tree

14 files changed

+655
-482
lines changed

14 files changed

+655
-482
lines changed

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

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,16 @@ object desugar {
393393
vparam.withMods(mods & (GivenOrImplicit | Erased | hasDefault) | Param)
394394
}
395395

396+
def mkApply(fn: Tree, paramss: List[ParamClause])(using Context): Tree =
397+
paramss.foldLeft(fn) { (fn, params) => params match
398+
case TypeDefs(params) =>
399+
TypeApply(fn, params.map(refOfDef))
400+
case (vparam: ValDef) :: _ if vparam.mods.is(Given) =>
401+
Apply(fn, params.map(refOfDef)).setApplyKind(ApplyKind.Using)
402+
case _ =>
403+
Apply(fn, params.map(refOfDef))
404+
}
405+
396406
/** The expansion of a class definition. See inline comments for what is involved */
397407
def classDef(cdef: TypeDef)(using Context): Tree = {
398408
val impl @ Template(constr0, _, self, _) = cdef.rhs
@@ -588,22 +598,13 @@ object desugar {
588598
}
589599

590600
// new C[Ts](paramss)
591-
lazy val creatorExpr = {
592-
val vparamss = constrVparamss match {
601+
lazy val creatorExpr =
602+
val vparamss = constrVparamss match
593603
case (vparam :: _) :: _ if vparam.mods.isOneOf(GivenOrImplicit) => // add a leading () to match class parameters
594604
Nil :: constrVparamss
595605
case _ =>
596606
constrVparamss
597-
}
598-
val nu = vparamss.foldLeft(makeNew(classTypeRef)) { (nu, vparams) =>
599-
val app = Apply(nu, vparams.map(refOfDef))
600-
vparams match {
601-
case vparam :: _ if vparam.mods.is(Given) => app.setApplyKind(ApplyKind.Using)
602-
case _ => app
603-
}
604-
}
605-
ensureApplied(nu)
606-
}
607+
ensureApplied(mkApply(makeNew(classTypeRef), vparamss))
607608

608609
val copiedAccessFlags = if migrateTo3 then EmptyFlags else AccessFlags
609610

@@ -888,48 +889,50 @@ object desugar {
888889
}
889890
}
890891

892+
def extMethod(mdef: DefDef, extParamss: List[ParamClause])(using Context): DefDef =
893+
cpy.DefDef(mdef)(
894+
name = normalizeName(mdef, mdef.tpt).asTermName,
895+
paramss =
896+
if mdef.name.isRightAssocOperatorName then
897+
val (typaramss, paramss) = mdef.paramss.span(isTypeParamClause) // first extract type parameters
898+
899+
paramss match
900+
case params :: paramss1 => // `params` must have a single parameter and without `given` flag
901+
902+
def badRightAssoc(problem: String) =
903+
report.error(i"right-associative extension method $problem", mdef.srcPos)
904+
extParamss ++ mdef.paramss
905+
906+
params match
907+
case ValDefs(vparam :: Nil) =>
908+
if !vparam.mods.is(Given) then
909+
// we merge the extension parameters with the method parameters,
910+
// swapping the operator arguments:
911+
// e.g.
912+
// extension [A](using B)(c: C)(using D)
913+
// def %:[E](f: F)(g: G)(using H): Res = ???
914+
// will be encoded as
915+
// def %:[A](using B)[E](f: F)(c: C)(using D)(g: G)(using H): Res = ???
916+
val (leadingUsing, otherExtParamss) = extParamss.span(isUsingOrTypeParamClause)
917+
leadingUsing ::: typaramss ::: params :: otherExtParamss ::: paramss1
918+
else
919+
badRightAssoc("cannot start with using clause")
920+
case _ =>
921+
badRightAssoc("must start with a single parameter")
922+
case _ =>
923+
// no value parameters, so not an infix operator.
924+
extParamss ++ mdef.paramss
925+
else
926+
extParamss ++ mdef.paramss
927+
).withMods(mdef.mods | ExtensionMethod)
928+
891929
/** Transform extension construct to list of extension methods */
892930
def extMethods(ext: ExtMethods)(using Context): Tree = flatTree {
893-
for mdef <- ext.methods yield
894-
defDef(
895-
cpy.DefDef(mdef)(
896-
name = normalizeName(mdef, ext).asTermName,
897-
paramss =
898-
if mdef.name.isRightAssocOperatorName then
899-
val (typaramss, paramss) = mdef.paramss.span(isTypeParamClause) // first extract type parameters
900-
901-
paramss match
902-
case params :: paramss1 => // `params` must have a single parameter and without `given` flag
903-
904-
def badRightAssoc(problem: String) =
905-
report.error(i"right-associative extension method $problem", mdef.srcPos)
906-
ext.paramss ++ mdef.paramss
907-
908-
params match
909-
case ValDefs(vparam :: Nil) =>
910-
if !vparam.mods.is(Given) then
911-
// we merge the extension parameters with the method parameters,
912-
// swapping the operator arguments:
913-
// e.g.
914-
// extension [A](using B)(c: C)(using D)
915-
// def %:[E](f: F)(g: G)(using H): Res = ???
916-
// will be encoded as
917-
// def %:[A](using B)[E](f: F)(c: C)(using D)(g: G)(using H): Res = ???
918-
val (leadingUsing, otherExtParamss) = ext.paramss.span(isUsingOrTypeParamClause)
919-
leadingUsing ::: typaramss ::: params :: otherExtParamss ::: paramss1
920-
else
921-
badRightAssoc("cannot start with using clause")
922-
case _ =>
923-
badRightAssoc("must start with a single parameter")
924-
case _ =>
925-
// no value parameters, so not an infix operator.
926-
ext.paramss ++ mdef.paramss
927-
else
928-
ext.paramss ++ mdef.paramss
929-
).withMods(mdef.mods | ExtensionMethod)
930-
)
931+
ext.methods map {
932+
case exp: Export => exp
933+
case mdef: DefDef => defDef(extMethod(mdef, ext.paramss))
934+
}
931935
}
932-
933936
/** Transforms
934937
*
935938
* <mods> type t >: Low <: Hi

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
117117
case class GenAlias(pat: Tree, expr: Tree)(implicit @constructorOnly src: SourceFile) extends Tree
118118
case class ContextBounds(bounds: TypeBoundsTree, cxBounds: List[Tree])(implicit @constructorOnly src: SourceFile) extends TypTree
119119
case class PatDef(mods: Modifiers, pats: List[Tree], tpt: Tree, rhs: Tree)(implicit @constructorOnly src: SourceFile) extends DefTree
120-
case class ExtMethods(paramss: List[ParamClause], methods: List[DefDef])(implicit @constructorOnly src: SourceFile) extends Tree
120+
case class ExtMethods(paramss: List[ParamClause], methods: List[Tree])(implicit @constructorOnly src: SourceFile) extends Tree
121121
case class MacroTree(expr: Tree)(implicit @constructorOnly src: SourceFile) extends Tree
122122

123123
case class ImportSelector(imported: Ident, renamed: Tree = EmptyTree, bound: Tree = EmptyTree)(implicit @constructorOnly src: SourceFile) extends Tree {
@@ -639,7 +639,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
639639
case tree: PatDef if (mods eq tree.mods) && (pats eq tree.pats) && (tpt eq tree.tpt) && (rhs eq tree.rhs) => tree
640640
case _ => finalize(tree, untpd.PatDef(mods, pats, tpt, rhs)(tree.source))
641641
}
642-
def ExtMethods(tree: Tree)(paramss: List[ParamClause], methods: List[DefDef])(using Context): Tree = tree match
642+
def ExtMethods(tree: Tree)(paramss: List[ParamClause], methods: List[Tree])(using Context): Tree = tree match
643643
case tree: ExtMethods if (paramss eq tree.paramss) && (methods == tree.methods) => tree
644644
case _ => finalize(tree, untpd.ExtMethods(paramss, methods)(tree.source))
645645
def ImportSelector(tree: Tree)(imported: Ident, renamed: Tree, bound: Tree)(using Context): Tree = tree match {

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3123,7 +3123,7 @@ object Parsers {
31233123
/** Import ::= `import' ImportExpr {‘,’ ImportExpr}
31243124
* Export ::= `export' ImportExpr {‘,’ ImportExpr}
31253125
*/
3126-
def importClause(leading: Token, mkTree: ImportConstr): List[Tree] = {
3126+
def importOrExportClause(leading: Token, mkTree: ImportConstr): List[Tree] = {
31273127
val offset = accept(leading)
31283128
commaSeparated(importExpr(mkTree)) match {
31293129
case t :: rest =>
@@ -3136,6 +3136,12 @@ object Parsers {
31363136
}
31373137
}
31383138

3139+
def exportClause() =
3140+
importOrExportClause(EXPORT, Export(_,_))
3141+
3142+
def importClause(outermost: Boolean = false) =
3143+
importOrExportClause(IMPORT, mkImport(outermost))
3144+
31393145
/** Create an import node and handle source version imports */
31403146
def mkImport(outermost: Boolean = false): ImportConstr = (tree, selectors) =>
31413147
val imp = Import(tree, selectors)
@@ -3676,8 +3682,10 @@ object Parsers {
36763682
if in.isColon() then
36773683
syntaxError("no `:` expected here")
36783684
in.nextToken()
3679-
val methods =
3680-
if isDefIntro(modifierTokens) then
3685+
val methods: List[Tree] =
3686+
if in.token == EXPORT then
3687+
exportClause()
3688+
else if isDefIntro(modifierTokens) then
36813689
extMethod(nparams) :: Nil
36823690
else
36833691
in.observeIndented()
@@ -3687,12 +3695,13 @@ object Parsers {
36873695
val result = atSpan(start)(ExtMethods(joinParams(tparams, leadParamss.toList), methods))
36883696
val comment = in.getDocComment(start)
36893697
if comment.isDefined then
3690-
for meth <- methods do
3698+
for case meth: DefDef <- methods do
36913699
if !meth.rawComment.isDefined then meth.setComment(comment)
36923700
result
36933701
end extension
36943702

36953703
/** ExtMethod ::= {Annotation [nl]} {Modifier} ‘def’ DefDef
3704+
* | Export
36963705
*/
36973706
def extMethod(numLeadParams: Int): DefDef =
36983707
val start = in.offset
@@ -3702,16 +3711,18 @@ object Parsers {
37023711

37033712
/** ExtMethods ::= ExtMethod | [nl] ‘{’ ExtMethod {semi ExtMethod ‘}’
37043713
*/
3705-
def extMethods(numLeadParams: Int): List[DefDef] = checkNoEscapingPlaceholders {
3706-
val meths = new ListBuffer[DefDef]
3714+
def extMethods(numLeadParams: Int): List[Tree] = checkNoEscapingPlaceholders {
3715+
val meths = new ListBuffer[Tree]
37073716
while
37083717
val start = in.offset
3709-
val mods = defAnnotsMods(modifierTokens)
3710-
in.token != EOF && {
3711-
accept(DEF)
3712-
meths += defDefOrDcl(start, mods, numLeadParams)
3713-
in.token != EOF && statSepOrEnd(meths, what = "extension method")
3714-
}
3718+
if in.token == EXPORT then
3719+
meths ++= exportClause()
3720+
else
3721+
val mods = defAnnotsMods(modifierTokens)
3722+
if in.token != EOF then
3723+
accept(DEF)
3724+
meths += defDefOrDcl(start, mods, numLeadParams)
3725+
in.token != EOF && statSepOrEnd(meths, what = "extension method")
37153726
do ()
37163727
if meths.isEmpty then syntaxErrorOrIncomplete("`def` expected")
37173728
meths.toList
@@ -3859,9 +3870,9 @@ object Parsers {
38593870
else stats += packaging(start)
38603871
}
38613872
else if (in.token == IMPORT)
3862-
stats ++= importClause(IMPORT, mkImport(outermost))
3873+
stats ++= importClause(outermost)
38633874
else if (in.token == EXPORT)
3864-
stats ++= importClause(EXPORT, Export(_,_))
3875+
stats ++= exportClause()
38653876
else if isIdent(nme.extension) && followingIsExtension() then
38663877
stats += extension()
38673878
else if isDefIntro(modifierTokens) then
@@ -3907,9 +3918,9 @@ object Parsers {
39073918
while
39083919
var empty = false
39093920
if (in.token == IMPORT)
3910-
stats ++= importClause(IMPORT, mkImport())
3921+
stats ++= importClause()
39113922
else if (in.token == EXPORT)
3912-
stats ++= importClause(EXPORT, Export(_,_))
3923+
stats ++= exportClause()
39133924
else if isIdent(nme.extension) && followingIsExtension() then
39143925
stats += extension()
39153926
else if (isDefIntro(modifierTokensOrCase))
@@ -3985,7 +3996,7 @@ object Parsers {
39853996
while
39863997
var empty = false
39873998
if (in.token == IMPORT)
3988-
stats ++= importClause(IMPORT, mkImport())
3999+
stats ++= importClause()
39894000
else if (isExprIntro)
39904001
stats += expr(Location.InBlock)
39914002
else if in.token == IMPLICIT && !in.inModifierPosition() then

0 commit comments

Comments
 (0)