Skip to content

Commit a61d2bc

Browse files
committed
Allow renamings as N in context bounds
Also, provide the possibility to use the parameter name for single context bounds. This is controlled by a Config setting, which is off by default.
1 parent 598c6ad commit a61d2bc

File tree

5 files changed

+146
-76
lines changed

5 files changed

+146
-76
lines changed

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

+111-69
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Annotations.Annotation
1010
import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName}
1111
import typer.{Namer, Checking}
1212
import util.{Property, SourceFile, SourcePosition, SrcPos, Chars}
13-
import config.Feature.{sourceVersion, migrateTo3, enabled}
13+
import config.{Feature, Config}
1414
import config.SourceVersion.*
1515
import collection.mutable
1616
import reporting.*
@@ -46,6 +46,11 @@ object desugar {
4646
*/
4747
val UntupledParam: Property.Key[Unit] = Property.StickyKey()
4848

49+
/** An attachment key to indicate that a ValDef is an evidence parameter
50+
* for a context bound.
51+
*/
52+
val ContextBoundParam: Property.Key[Unit] = Property.StickyKey()
53+
4954
/** What static check should be applied to a Match? */
5055
enum MatchCheck {
5156
case None, Exhaustive, IrrefutablePatDef, IrrefutableGenFrom
@@ -195,17 +200,6 @@ object desugar {
195200
else vdef1
196201
end valDef
197202

198-
def makeImplicitParameters(
199-
tpts: List[Tree], implicitFlag: FlagSet,
200-
mkParamName: Int => TermName,
201-
forPrimaryConstructor: Boolean = false
202-
)(using Context): List[ValDef] =
203-
for (tpt, i) <- tpts.zipWithIndex yield {
204-
val paramFlags: FlagSet = if (forPrimaryConstructor) LocalParamAccessor else Param
205-
val epname = mkParamName(i)
206-
ValDef(epname, tpt, EmptyTree).withFlags(paramFlags | implicitFlag)
207-
}
208-
209203
def mapParamss(paramss: List[ParamClause])
210204
(mapTypeParam: TypeDef => TypeDef)
211205
(mapTermParam: ValDef => ValDef)(using Context): List[ParamClause] =
@@ -232,34 +226,57 @@ object desugar {
232226
private def defDef(meth: DefDef, isPrimaryConstructor: Boolean = false)(using Context): Tree =
233227
addDefaultGetters(elimContextBounds(meth, isPrimaryConstructor))
234228

235-
private def elimContextBounds(meth: DefDef, isPrimaryConstructor: Boolean)(using Context): DefDef =
236-
val DefDef(_, paramss, tpt, rhs) = meth
237-
val evidenceParamBuf = mutable.ListBuffer[ValDef]()
229+
private def desugarContextBounds(
230+
tdef: TypeDef,
231+
evidenceBuf: mutable.ListBuffer[ValDef],
232+
flags: FlagSet,
233+
freshName: untpd.Tree => TermName,
234+
allParamss: List[ParamClause])(using Context): TypeDef =
238235

239-
var seenContextBounds: Int = 0
240-
def desugarContextBounds(rhs: Tree): Tree = rhs match
236+
val evidenceNames = mutable.ListBuffer[TermName]()
237+
238+
def desugarRhs(rhs: Tree): Tree = rhs match
241239
case ContextBounds(tbounds, cxbounds) =>
242-
val iflag = if sourceVersion.isAtLeast(`future`) then Given else Implicit
243-
evidenceParamBuf ++= makeImplicitParameters(
244-
cxbounds, iflag,
245-
// Just like with `makeSyntheticParameter` on nameless parameters of
246-
// using clauses, we only need names that are unique among the
247-
// parameters of the method since shadowing does not affect
248-
// implicit resolution in Scala 3.
249-
mkParamName = i =>
250-
val index = seenContextBounds + 1 // Start at 1 like FreshNameCreator.
251-
val ret = ContextBoundParamName(EmptyTermName, index)
252-
seenContextBounds += 1
253-
ret,
254-
forPrimaryConstructor = isPrimaryConstructor)
240+
for bound <- cxbounds do
241+
val evidenceName = bound match
242+
case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty =>
243+
ownName
244+
case _ if Config.nameSingleContextBounds && cxbounds.tail.isEmpty
245+
&& Feature.enabled(Feature.modularity) =>
246+
tdef.name.toTermName
247+
case _ =>
248+
freshName(bound)
249+
evidenceNames += evidenceName
250+
val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags)
251+
evidenceParam.pushAttachment(ContextBoundParam, ())
252+
evidenceBuf += evidenceParam
255253
tbounds
256254
case LambdaTypeTree(tparams, body) =>
257-
cpy.LambdaTypeTree(rhs)(tparams, desugarContextBounds(body))
255+
cpy.LambdaTypeTree(rhs)(tparams, desugarRhs(body))
258256
case _ =>
259257
rhs
258+
259+
cpy.TypeDef(tdef)(rhs = desugarRhs(tdef.rhs))
260+
end desugarContextBounds
261+
262+
private def elimContextBounds(meth: DefDef, isPrimaryConstructor: Boolean)(using Context): DefDef =
263+
val DefDef(_, paramss, tpt, rhs) = meth
264+
val evidenceParamBuf = mutable.ListBuffer[ValDef]()
265+
266+
var seenContextBounds: Int = 0
267+
def freshName(unused: Tree) =
268+
seenContextBounds += 1 // Start at 1 like FreshNameCreator.
269+
ContextBoundParamName(EmptyTermName, seenContextBounds)
270+
// Just like with `makeSyntheticParameter` on nameless parameters of
271+
// using clauses, we only need names that are unique among the
272+
// parameters of the method since shadowing does not affect
273+
// implicit resolution in Scala 3.
274+
260275
val paramssNoContextBounds =
276+
val iflag = if Feature.sourceVersion.isAtLeast(`future`) then Given else Implicit
277+
val flags = if isPrimaryConstructor then iflag | LocalParamAccessor else iflag | Param
261278
mapParamss(paramss) {
262-
tparam => cpy.TypeDef(tparam)(rhs = desugarContextBounds(tparam.rhs))
279+
tparam => desugarContextBounds(tparam, evidenceParamBuf, flags, freshName, paramss)
263280
}(identity)
264281

265282
rhs match
@@ -399,43 +416,70 @@ object desugar {
399416
(Nil, tree)
400417

401418
/** Add all evidence parameters in `params` as implicit parameters to `meth`.
402-
* If the parameters of `meth` end in an implicit parameter list or using clause,
403-
* evidence parameters are added in front of that list. Otherwise they are added
404-
* as a separate parameter clause.
419+
* The position of the added parameters is determined as follows:
420+
*
421+
* - If there is an existing parameter list that refers to one of the added
422+
* parameters in one of its parameter types, add the new parameters
423+
* in front of the first such parameter list.
424+
* - Otherwise, if the last parameter list consists implicit or using parameters,
425+
* join the new parameters in front of this parameter list, creating one
426+
* parameter list (this is equilavent to Scala 2's scheme).
427+
* - Otherwise, add the new parameter list at the end as a separate parameter clause.
405428
*/
406429
private def addEvidenceParams(meth: DefDef, params: List[ValDef])(using Context): DefDef =
407-
params match
430+
if params.isEmpty then return meth
431+
432+
val boundNames = params.map(_.name).toSet
433+
434+
//println(i"add ev params ${meth.name}, ${boundNames.toList}")
435+
436+
def references(vdef: ValDef): Boolean =
437+
vdef.tpt.existsSubTree:
438+
case Ident(name: TermName) => boundNames.contains(name)
439+
case _ => false
440+
441+
def recur(mparamss: List[ParamClause]): List[ParamClause] = mparamss match
442+
case ValDefs(mparams) :: _ if mparams.exists(references) =>
443+
params :: mparamss
444+
case ValDefs(mparams @ (mparam :: _)) :: Nil if mparam.mods.isOneOf(GivenOrImplicit) =>
445+
(params ++ mparams) :: Nil
446+
case mparams :: mparamss1 =>
447+
mparams :: recur(mparamss1)
408448
case Nil =>
409-
meth
410-
case evidenceParams =>
411-
val paramss1 = meth.paramss.reverse match
412-
case ValDefs(vparams @ (vparam :: _)) :: rparamss if vparam.mods.isOneOf(GivenOrImplicit) =>
413-
((evidenceParams ++ vparams) :: rparamss).reverse
414-
case _ =>
415-
meth.paramss :+ evidenceParams
416-
cpy.DefDef(meth)(paramss = paramss1)
449+
params :: Nil
450+
451+
cpy.DefDef(meth)(paramss = recur(meth.paramss))
452+
end addEvidenceParams
417453

418454
/** The parameters generated from the contextual bounds of `meth`, as generated by `desugar.defDef` */
419455
private def evidenceParams(meth: DefDef)(using Context): List[ValDef] =
420456
meth.paramss.reverse match {
421457
case ValDefs(vparams @ (vparam :: _)) :: _ if vparam.mods.isOneOf(GivenOrImplicit) =>
422-
vparams.takeWhile(_.name.is(ContextBoundParamName))
458+
vparams.takeWhile(_.hasAttachment(ContextBoundParam))
423459
case _ =>
424460
Nil
425461
}
426462

427463
@sharable private val synthetic = Modifiers(Synthetic)
428464

429-
private def toDefParam(tparam: TypeDef, keepAnnotations: Boolean): TypeDef = {
430-
var mods = tparam.rawMods
431-
if (!keepAnnotations) mods = mods.withAnnotations(Nil)
465+
/** Filter annotations in `mods` according to `keep` */
466+
private def filterAnnots(mods: Modifiers, keep: Boolean)(using Context) =
467+
if keep then mods else mods.withAnnotations(Nil)
468+
469+
private def toDefParam(tparam: TypeDef, keepAnnotations: Boolean)(using Context): TypeDef =
470+
val mods = filterAnnots(tparam.rawMods, keepAnnotations)
432471
tparam.withMods(mods & EmptyFlags | Param)
433-
}
434-
private def toDefParam(vparam: ValDef, keepAnnotations: Boolean, keepDefault: Boolean): ValDef = {
435-
var mods = vparam.rawMods
436-
if (!keepAnnotations) mods = mods.withAnnotations(Nil)
472+
473+
private def toDefParam(vparam: ValDef, keepAnnotations: Boolean, keepDefault: Boolean)(using Context): ValDef = {
474+
val mods = filterAnnots(vparam.rawMods, keepAnnotations)
437475
val hasDefault = if keepDefault then HasDefault else EmptyFlags
438-
vparam.withMods(mods & (GivenOrImplicit | Erased | hasDefault | Tracked) | Param)
476+
// Need to ensure that tree is duplicated since term parameters can be watched
477+
// and cloning a term parameter will copy its watchers to the clone, which means
478+
// we'd get cross-talk between the original parameter and the clone.
479+
ValDef(vparam.name, vparam.tpt, vparam.rhs)
480+
.withSpan(vparam.span)
481+
.withAttachmentsFrom(vparam)
482+
.withMods(mods & (GivenOrImplicit | Erased | hasDefault | Tracked) | Param)
439483
}
440484

441485
def mkApply(fn: Tree, paramss: List[ParamClause])(using Context): Tree =
@@ -609,6 +653,11 @@ object desugar {
609653
case _ => false
610654
}
611655

656+
def isRepeated(tree: Tree): Boolean = stripByNameType(tree) match {
657+
case PostfixOp(_, Ident(tpnme.raw.STAR)) => true
658+
case _ => false
659+
}
660+
612661
def appliedRef(tycon: Tree, tparams: List[TypeDef] = constrTparams, widenHK: Boolean = false) = {
613662
val targs = for (tparam <- tparams) yield {
614663
val targ = refOfDef(tparam)
@@ -625,11 +674,6 @@ object desugar {
625674
appliedTypeTree(tycon, targs)
626675
}
627676

628-
def isRepeated(tree: Tree): Boolean = stripByNameType(tree) match {
629-
case PostfixOp(_, Ident(tpnme.raw.STAR)) => true
630-
case _ => false
631-
}
632-
633677
// a reference to the class type bound by `cdef`, with type parameters coming from the constructor
634678
val classTypeRef = appliedRef(classTycon)
635679

@@ -667,7 +711,7 @@ object desugar {
667711
}
668712
ensureApplied(nu)
669713

670-
val copiedAccessFlags = if migrateTo3 then EmptyFlags else AccessFlags
714+
val copiedAccessFlags = if Feature.migrateTo3 then EmptyFlags else AccessFlags
671715

672716
// Methods to add to a case class C[..](p1: T1, ..., pN: Tn)(moreParams)
673717
// def _1: T1 = this.p1
@@ -850,12 +894,11 @@ object desugar {
850894
Nil
851895
}
852896
else {
853-
val defParamss = constrVparamss match {
897+
val defParamss = constrVparamss match
854898
case Nil :: paramss =>
855899
paramss // drop leading () that got inserted by class
856900
// TODO: drop this once we do not silently insert empty class parameters anymore
857901
case paramss => paramss
858-
}
859902
val finalFlag = if ctx.settings.YcompileScala2Library.value then EmptyFlags else Final
860903
// implicit wrapper is typechecked in same scope as constructor, so
861904
// we can reuse the constructor parameters; no derived params are needed.
@@ -1681,14 +1724,13 @@ object desugar {
16811724
.collect:
16821725
case vd: ValDef => vd
16831726

1684-
def makeContextualFunction(formals: List[Tree], paramNamesOrNil: List[TermName], body: Tree, erasedParams: List[Boolean])(using Context): Function = {
1685-
val mods = Given
1686-
val params = makeImplicitParameters(formals, mods,
1687-
mkParamName = i =>
1688-
if paramNamesOrNil.isEmpty then ContextFunctionParamName.fresh()
1689-
else paramNamesOrNil(i))
1690-
FunctionWithMods(params, body, Modifiers(mods), erasedParams)
1691-
}
1727+
def makeContextualFunction(formals: List[Tree], paramNamesOrNil: List[TermName], body: Tree, erasedParams: List[Boolean])(using Context): Function =
1728+
val paramNames =
1729+
if paramNamesOrNil.nonEmpty then paramNamesOrNil
1730+
else formals.map(_ => ContextFunctionParamName.fresh())
1731+
val params = for (tpt, pname) <- formals.zip(paramNames) yield
1732+
ValDef(pname, tpt, EmptyTree).withFlags(Given | Param)
1733+
FunctionWithMods(params, body, Modifiers(Given), erasedParams)
16921734

16931735
private def derivedValDef(original: Tree, named: NameTree, tpt: Tree, rhs: Tree, mods: Modifiers)(using Context) = {
16941736
val vdef = ValDef(named.name.asTermName, tpt, rhs)

compiler/src/dotty/tools/dotc/config/Config.scala

+7
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,11 @@ object Config {
235235
*/
236236
inline val checkLevelsOnConstraints = false
237237
inline val checkLevelsOnInstantiation = true
238+
239+
/** If a type parameter `X` has a single context bound `X: C`, should the
240+
* witness parameter be named `X`? This would prevent the creation of a
241+
* context bound companion.
242+
*/
243+
inline val nameSingleContextBounds = false
238244
}
245+

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

+12-4
Original file line numberDiff line numberDiff line change
@@ -2196,9 +2196,9 @@ object Parsers {
21962196
if (in.token == tok) { in.nextToken(); toplevelTyp() }
21972197
else EmptyTree
21982198

2199-
/** TypeParamBounds ::= TypeBounds {`<%' Type} {`:' Type}
2199+
/** TypeAndCtxBounds ::= TypeBounds [`:` ContextBounds]
22002200
*/
2201-
def typeParamBounds(pname: TypeName): Tree = {
2201+
def typeAndCtxBounds(pname: TypeName): Tree = {
22022202
val t = typeBounds()
22032203
val cbs = contextBounds(pname)
22042204
if (cbs.isEmpty) t
@@ -2207,8 +2207,16 @@ object Parsers {
22072207

22082208
/** ContextBound ::= Type [`as` id] */
22092209
def contextBound(pname: TypeName): Tree =
2210-
ContextBoundTypeTree(toplevelTyp(), pname, EmptyTermName)
2210+
val t = toplevelTyp()
2211+
val ownName =
2212+
if isIdent(nme.as) && in.featureEnabled(Feature.modularity) then
2213+
in.nextToken()
2214+
ident()
2215+
else EmptyTermName
2216+
ContextBoundTypeTree(t, pname, ownName)
22112217

2218+
/** ContextBounds ::= ContextBound | `{` ContextBound {`,` ContextBound} `}`
2219+
*/
22122220
def contextBounds(pname: TypeName): List[Tree] =
22132221
if in.isColon then
22142222
in.nextToken()
@@ -3411,7 +3419,7 @@ object Parsers {
34113419
}
34123420
else ident().toTypeName
34133421
val hkparams = typeParamClauseOpt(ParamOwner.Type)
3414-
val bounds = if (isAbstractOwner) typeBounds() else typeParamBounds(name)
3422+
val bounds = if (isAbstractOwner) typeBounds() else typeAndCtxBounds(name)
34153423
TypeDef(name, lambdaAbstract(hkparams, bounds)).withMods(mods)
34163424
}
34173425
}

docs/_docs/internals/syntax.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ IntoTargetType ::= Type
221221
TypeArgs ::= ‘[’ Types ‘]’ ts
222222
Refinement ::= :<<< [RefineDcl] {semi [RefineDcl]} >>> ds
223223
TypeBounds ::= [‘>:’ Type] [‘<:’ Type] TypeBoundsTree(lo, hi)
224-
TypeParamBounds ::= TypeBounds {‘:’ Type} ContextBounds(typeBounds, tps)
224+
TypeAndCtxBounds ::= TypeBounds [‘:’ ContextBounds] ContextBounds(typeBounds, tps)
225+
ContextBounds ::= ContextBound | '{' ContextBound {',' ContextBound} '}'
226+
ContextBound ::= Type ['as' id]
225227
Types ::= Type {‘,’ Type}
226228
NamesAndTypes ::= NameAndType {‘,’ NameAndType}
227229
NameAndType ::= id ':' Type
@@ -359,7 +361,7 @@ ArgumentPatterns ::= ‘(’ [Patterns] ‘)’
359361
```ebnf
360362
ClsTypeParamClause::= ‘[’ ClsTypeParam {‘,’ ClsTypeParam} ‘]’
361363
ClsTypeParam ::= {Annotation} [‘+’ | ‘-’] TypeDef(Modifiers, name, tparams, bounds)
362-
id [HkTypeParamClause] TypeParamBounds Bound(below, above, context)
364+
id [HkTypeParamClause] TypeAndCtxBounds Bound(below, above, context)
363365
364366
TypTypeParamClause::= ‘[’ TypTypeParam {‘,’ TypTypeParam} ‘]’
365367
TypTypeParam ::= {Annotation} id [HkTypeParamClause] TypeBounds
@@ -384,7 +386,7 @@ TypelessClause ::= DefTermParamClause
384386
| UsingParamClause
385387
386388
DefTypeParamClause::= [nl] ‘[’ DefTypeParam {‘,’ DefTypeParam} ‘]’
387-
DefTypeParam ::= {Annotation} id [HkTypeParamClause] TypeParamBounds
389+
DefTypeParam ::= {Annotation} id [HkTypeParamClause] TypeAndCtxBounds
388390
DefTermParamClause::= [nl] ‘(’ [DefTermParams] ‘)’
389391
UsingParamClause ::= [nl] ‘(’ ‘using’ (DefTermParams | FunArgTypes) ‘)’
390392
DefImplicitClause ::= [nl] ‘(’ ‘implicit’ DefTermParams ‘)’

tests/pos/FromString-named.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//> using options -language:experimental.modularity -source future
2+
3+
trait FromString[A]:
4+
def fromString(s: String): A
5+
6+
given FromString[Int] = _.toInt
7+
8+
given FromString[Double] = _.toDouble
9+
10+
def add[N: {FromString as N, Numeric as num}](a: String, b: String): N =
11+
num.plus(N.fromString(a), N.fromString(b))

0 commit comments

Comments
 (0)