Skip to content

Commit 5a65e4b

Browse files
committed
Implement context bound companions
1 parent 153b958 commit 5a65e4b

25 files changed

+492
-47
lines changed

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

+37-13
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,16 @@ object desugar {
257257
case _ =>
258258
rhs
259259

260-
cpy.TypeDef(tdef)(rhs = desugarRhs(tdef.rhs))
260+
val tdef1 = cpy.TypeDef(tdef)(rhs = desugarRhs(tdef.rhs))
261+
if Feature.enabled(Feature.modularity)
262+
&& evidenceNames.nonEmpty
263+
&& !evidenceNames.contains(tdef.name.toTermName)
264+
&& !allParamss.nestedExists(_.name == tdef.name.toTermName)
265+
then
266+
tdef1.withAddedAnnotation:
267+
WitnessNamesAnnot(evidenceNames.toList).withSpan(tdef.span)
268+
else
269+
tdef1
261270
end desugarContextBounds
262271

263272
private def elimContextBounds(meth: DefDef, isPrimaryConstructor: Boolean)(using Context): DefDef =
@@ -322,9 +331,9 @@ object desugar {
322331

323332
def getterParamss(n: Int): List[ParamClause] =
324333
mapParamss(takeUpTo(paramssNoRHS, n)) {
325-
tparam => dropContextBounds(toDefParam(tparam, keepAnnotations = true))
334+
tparam => dropContextBounds(toDefParam(tparam, KeepAnnotations.All))
326335
} {
327-
vparam => toDefParam(vparam, keepAnnotations = true, keepDefault = false)
336+
vparam => toDefParam(vparam, KeepAnnotations.All, keepDefault = false)
328337
}
329338

330339
def defaultGetters(paramss: List[ParamClause], n: Int): List[DefDef] = paramss match
@@ -429,7 +438,12 @@ object desugar {
429438
private def addEvidenceParams(meth: DefDef, params: List[ValDef])(using Context): DefDef =
430439
if params.isEmpty then return meth
431440

432-
val boundNames = params.map(_.name).toSet
441+
var boundNames = params.map(_.name).toSet
442+
for mparams <- meth.paramss; mparam <- mparams do
443+
mparam match
444+
case tparam: TypeDef if tparam.mods.annotations.exists(WitnessNamesAnnot.unapply(_).isDefined) =>
445+
boundNames += tparam.name.toTermName
446+
case _ =>
433447

434448
//println(i"add ev params ${meth.name}, ${boundNames.toList}")
435449

@@ -462,16 +476,26 @@ object desugar {
462476

463477
@sharable private val synthetic = Modifiers(Synthetic)
464478

479+
/** Which annotations to keep in derived parameters */
480+
private enum KeepAnnotations:
481+
case None, All, WitnessOnly
482+
465483
/** 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)
484+
private def filterAnnots(mods: Modifiers, keep: KeepAnnotations)(using Context) = keep match
485+
case KeepAnnotations.None => mods.withAnnotations(Nil)
486+
case KeepAnnotations.All => mods
487+
case KeepAnnotations.WitnessOnly =>
488+
mods.withAnnotations:
489+
mods.annotations.filter:
490+
case WitnessNamesAnnot(_) => true
491+
case _ => false
468492

469-
private def toDefParam(tparam: TypeDef, keepAnnotations: Boolean)(using Context): TypeDef =
470-
val mods = filterAnnots(tparam.rawMods, keepAnnotations)
493+
private def toDefParam(tparam: TypeDef, keep: KeepAnnotations)(using Context): TypeDef =
494+
val mods = filterAnnots(tparam.rawMods, keep)
471495
tparam.withMods(mods & EmptyFlags | Param)
472496

473-
private def toDefParam(vparam: ValDef, keepAnnotations: Boolean, keepDefault: Boolean)(using Context): ValDef = {
474-
val mods = filterAnnots(vparam.rawMods, keepAnnotations)
497+
private def toDefParam(vparam: ValDef, keep: KeepAnnotations, keepDefault: Boolean)(using Context): ValDef = {
498+
val mods = filterAnnots(vparam.rawMods, keep)
475499
val hasDefault = if keepDefault then HasDefault else EmptyFlags
476500
// Need to ensure that tree is duplicated since term parameters can be watched
477501
// and cloning a term parameter will copy its watchers to the clone, which means
@@ -572,7 +596,7 @@ object desugar {
572596
// Annotations on class _type_ parameters are set on the derived parameters
573597
// but not on the constructor parameters. The reverse is true for
574598
// annotations on class _value_ parameters.
575-
val constrTparams = impliedTparams.map(toDefParam(_, keepAnnotations = false))
599+
val constrTparams = impliedTparams.map(toDefParam(_, KeepAnnotations.WitnessOnly))
576600
val constrVparamss =
577601
if (originalVparamss.isEmpty) { // ensure parameter list is non-empty
578602
if (isCaseClass)
@@ -583,7 +607,7 @@ object desugar {
583607
report.error(CaseClassMissingNonImplicitParamList(cdef), namePos)
584608
ListOfNil
585609
}
586-
else originalVparamss.nestedMap(toDefParam(_, keepAnnotations = true, keepDefault = true))
610+
else originalVparamss.nestedMap(toDefParam(_, KeepAnnotations.All, keepDefault = true))
587611
val derivedTparams =
588612
constrTparams.zipWithConserve(impliedTparams)((tparam, impliedParam) =>
589613
derivedTypeParam(tparam).withAnnotations(impliedParam.mods.annotations))
@@ -605,7 +629,7 @@ object desugar {
605629
defDef(
606630
addEvidenceParams(
607631
cpy.DefDef(ddef)(paramss = joinParams(constrTparams, ddef.paramss)),
608-
evidenceParams(constr1).map(toDefParam(_, keepAnnotations = false, keepDefault = false)))))
632+
evidenceParams(constr1).map(toDefParam(_, KeepAnnotations.None, keepDefault = false)))))
609633
case stat =>
610634
stat
611635
}

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

+31
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package ast
55
import core.*
66
import Flags.*, Trees.*, Types.*, Contexts.*
77
import Names.*, StdNames.*, NameOps.*, Symbols.*
8+
import Annotations.Annotation
9+
import NameKinds.ContextBoundParamName
810
import typer.ConstFold
911
import reporting.trace
1012

@@ -376,6 +378,35 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
376378
case _ =>
377379
tree.tpe.isInstanceOf[ThisType]
378380
}
381+
382+
/** Extractor for annotation.internal.WitnessNames(name_1, ..., name_n)`
383+
* represented as an untyped or typed tree.
384+
*/
385+
object WitnessNamesAnnot:
386+
def apply(names0: List[TermName])(using Context): untpd.Tree =
387+
untpd.TypedSplice(tpd.New(
388+
defn.WitnessNamesAnnot.typeRef,
389+
tpd.SeqLiteral(names0.map(n => tpd.Literal(Constant(n.toString))), tpd.TypeTree(defn.StringType)) :: Nil
390+
))
391+
392+
def unapply(tree: Tree)(using Context): Option[List[TermName]] =
393+
def isWitnessNames(tp: Type) = tp match
394+
case tp: TypeRef =>
395+
tp.name == tpnme.WitnessNames && tp.symbol == defn.WitnessNamesAnnot
396+
case _ =>
397+
false
398+
unsplice(tree) match
399+
case Apply(
400+
Select(New(tpt: tpd.TypeTree), nme.CONSTRUCTOR),
401+
SeqLiteral(elems, _) :: Nil
402+
) if isWitnessNames(tpt.tpe) =>
403+
Some:
404+
elems.map:
405+
case Literal(Constant(str: String)) =>
406+
ContextBoundParamName.unmangle(str.toTermName.asSimpleName)
407+
case _ =>
408+
None
409+
end WitnessNamesAnnot
379410
}
380411

381412
trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] =>

compiler/src/dotty/tools/dotc/core/Contexts.scala

+8-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Symbols.*
1212
import Scopes.*
1313
import Uniques.*
1414
import ast.Trees.*
15+
import Flags.ParamAccessor
1516
import ast.untpd
1617
import util.{NoSource, SimpleIdentityMap, SourceFile, HashSet, ReusableInstance}
1718
import typer.{Implicits, ImportInfo, SearchHistory, SearchRoot, TypeAssigner, Typer, Nullables}
@@ -399,7 +400,8 @@ object Contexts {
399400
*
400401
* - as owner: The primary constructor of the class
401402
* - as outer context: The context enclosing the class context
402-
* - as scope: The parameter accessors in the class context
403+
* - as scope: type parameters, the parameter accessors, and
404+
* the context bound companions in the class context,
403405
*
404406
* The reasons for this peculiar choice of attributes are as follows:
405407
*
@@ -413,10 +415,11 @@ object Contexts {
413415
* context see the constructor parameters instead, but then we'd need a final substitution step
414416
* from constructor parameters to class parameter accessors.
415417
*/
416-
def superCallContext: Context = {
417-
val locals = newScopeWith(owner.typeParams ++ owner.asClass.paramAccessors*)
418-
superOrThisCallContext(owner.primaryConstructor, locals)
419-
}
418+
def superCallContext: Context =
419+
val locals = owner.typeParams
420+
++ owner.asClass.unforcedDecls.filter: sym =>
421+
sym.is(ParamAccessor) || sym.isContextBoundCompanion
422+
superOrThisCallContext(owner.primaryConstructor, newScopeWith(locals*))
420423

421424
/** The context for the arguments of a this(...) constructor call.
422425
* The context is computed from the local auxiliary constructor context.

compiler/src/dotty/tools/dotc/core/Definitions.scala

+9
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,13 @@ class Definitions {
459459
@tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _))
460460
@tu lazy val orType: TypeSymbol = enterBinaryAlias(tpnme.OR, OrType(_, _, soft = false))
461461

462+
@tu lazy val CBCompanion: TypeSymbol = // type `<context-bound-companion>`[-Refs]
463+
enterPermanentSymbol(tpnme.CBCompanion,
464+
TypeBounds(NothingType,
465+
HKTypeLambda(tpnme.syntheticTypeParamName(0) :: Nil, Contravariant :: Nil)(
466+
tl => TypeBounds.empty :: Nil,
467+
tl => AnyType))).asType
468+
462469
/** Method representing a throw */
463470
@tu lazy val throwMethod: TermSymbol = enterMethod(OpsPackageClass, nme.THROWkw,
464471
MethodType(List(ThrowableType), NothingType))
@@ -1064,6 +1071,7 @@ class Definitions {
10641071
@tu lazy val RetainsCapAnnot: ClassSymbol = requiredClass("scala.annotation.retainsCap")
10651072
@tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName")
10661073
@tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary")
1074+
@tu lazy val WitnessNamesAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WitnessNames")
10671075

10681076
@tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable")
10691077

@@ -2141,6 +2149,7 @@ class Definitions {
21412149
NullClass,
21422150
NothingClass,
21432151
SingletonClass,
2152+
CBCompanion,
21442153
MaybeCapabilityAnnot)
21452154

21462155
@tu lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List(

compiler/src/dotty/tools/dotc/core/NamerOps.scala

+53
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ package core
44

55
import Contexts.*, Symbols.*, Types.*, Flags.*, Scopes.*, Decorators.*, Names.*, NameOps.*
66
import SymDenotations.{LazyType, SymDenotation}, StdNames.nme
7+
import ContextOps.enter
78
import TypeApplications.EtaExpansion
89
import collection.mutable
10+
import config.Printers.typr
911

1012
/** Operations that are shared between Namer and TreeUnpickler */
1113
object NamerOps:
@@ -256,4 +258,55 @@ object NamerOps:
256258
rhsCtx.gadtState.addBound(psym, tr, isUpper = true)
257259
}
258260

261+
/** Create a context-bound companion for type symbol `tsym`, which has a context
262+
* bound that defines a set of witnesses with names `witnessNames`.
263+
*
264+
* @param parans If `tsym` is a type parameter, a list of parameter symbols
265+
* that include all witnesses, otherwise the empty list.
266+
*
267+
* The context-bound companion has as name the name of `tsym` translated to
268+
* a term name. We create a synthetic val of the form
269+
*
270+
* val A: `<context-bound-companion>`[witnessRef1 | ... | witnessRefN]
271+
*
272+
* where
273+
*
274+
* <context-bound-companion> is the CBCompanion type created in Definitions
275+
* withnessRefK is a refence to the K'th witness.
276+
*
277+
* The companion has the same access flags as the original type.
278+
*/
279+
def addContextBoundCompanionFor(tsym: Symbol, witnessNames: List[TermName], params: List[Symbol])(using Context): Unit =
280+
val prefix = ctx.owner.thisType
281+
val companionName = tsym.name.toTermName
282+
val witnessRefs =
283+
if params.nonEmpty then
284+
witnessNames.map: witnessName =>
285+
prefix.select(params.find(_.name == witnessName).get)
286+
else
287+
witnessNames.map(TermRef(prefix, _))
288+
val cbtype = defn.CBCompanion.typeRef.appliedTo:
289+
witnessRefs.reduce[Type](OrType(_, _, soft = false))
290+
val cbc = newSymbol(
291+
ctx.owner, companionName,
292+
(tsym.flagsUNSAFE & (AccessFlags)).toTermFlags | Synthetic,
293+
cbtype)
294+
typr.println(s"context bound companion created $cbc for $witnessNames in ${ctx.owner}")
295+
ctx.enter(cbc)
296+
end addContextBoundCompanionFor
297+
298+
/** Add context bound companions to all context-bound types declared in
299+
* this class. This assumes that these types already have their
300+
* WitnessNames annotation set even before they are completed. This is
301+
* the case for unpickling but currently not for Namer. So the method
302+
* is only called during unpickling, and is not part of NamerOps.
303+
*/
304+
def addContextBoundCompanions(cls: ClassSymbol)(using Context): Unit =
305+
for sym <- cls.info.decls do
306+
if sym.isType && !sym.isClass then
307+
for ann <- sym.annotationsUNSAFE do
308+
if ann.symbol == defn.WitnessNamesAnnot then
309+
ann.tree match
310+
case ast.tpd.WitnessNamesAnnot(witnessNames) =>
311+
addContextBoundCompanionFor(sym, witnessNames, Nil)
259312
end NamerOps

compiler/src/dotty/tools/dotc/core/StdNames.scala

+2
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ object StdNames {
288288

289289
// Compiler-internal
290290
val CAPTURE_ROOT: N = "cap"
291+
val CBCompanion: N = "<context-bound-companion>"
291292
val CONSTRUCTOR: N = "<init>"
292293
val STATIC_CONSTRUCTOR: N = "<clinit>"
293294
val EVT2U: N = "evt2u$"
@@ -393,6 +394,7 @@ object StdNames {
393394
val TypeApply: N = "TypeApply"
394395
val TypeRef: N = "TypeRef"
395396
val UNIT : N = "UNIT"
397+
val WitnessNames: N = "WitnessNames"
396398
val acc: N = "acc"
397399
val adhocExtensions: N = "adhocExtensions"
398400
val andThen: N = "andThen"

compiler/src/dotty/tools/dotc/core/SymUtils.scala

+3
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ class SymUtils:
8787
!d.isPrimitiveValueClass
8888
}
8989

90+
def isContextBoundCompanion(using Context): Boolean =
91+
self.is(Synthetic) && self.infoOrCompleter.typeSymbol == defn.CBCompanion
92+
9093
/** Is this a case class for which a product mirror is generated?
9194
* Excluded are value classes, abstract classes and case classes with more than one
9295
* parameter section.

compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala

+1
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,7 @@ class TreeUnpickler(reader: TastyReader,
11271127
})
11281128
defn.patchStdLibClass(cls)
11291129
NamerOps.addConstructorProxies(cls)
1130+
NamerOps.addContextBoundCompanions(cls)
11301131
setSpan(start,
11311132
untpd.Template(constr, mappedParents, self, lazyStats)
11321133
.withType(localDummy.termRef))

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -430,11 +430,11 @@ class PlainPrinter(_ctx: Context) extends Printer {
430430
sym.isEffectiveRoot || sym.isAnonymousClass || sym.name.isReplWrapperName
431431

432432
/** String representation of a definition's type following its name,
433-
* if symbol is completed, "?" otherwise.
433+
* if symbol is completed, ": ?" otherwise.
434434
*/
435435
protected def toTextRHS(optType: Option[Type]): Text = optType match {
436436
case Some(tp) => toTextRHS(tp)
437-
case None => "?"
437+
case None => ": ?"
438438
}
439439

440440
protected def decomposeLambdas(bounds: TypeBounds): (Text, TypeBounds) =

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

+2
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
207207
case MatchTypeLegacyPatternID // errorNumber: 191
208208
case UnstableInlineAccessorID // errorNumber: 192
209209
case VolatileOnValID // errorNumber: 193
210+
case ConstructorProxyNotValueID // errorNumber: 194
211+
case ContextBoundCompanionNotValueID // errorNumber: 195
210212

211213
def errorNumber = ordinal - 1
212214

compiler/src/dotty/tools/dotc/reporting/messages.scala

+36
Original file line numberDiff line numberDiff line change
@@ -3159,3 +3159,39 @@ class VolatileOnVal()(using Context)
31593159
extends SyntaxMsg(VolatileOnValID):
31603160
protected def msg(using Context): String = "values cannot be volatile"
31613161
protected def explain(using Context): String = ""
3162+
3163+
class ConstructorProxyNotValue(sym: Symbol)(using Context)
3164+
extends TypeMsg(ConstructorProxyNotValueID):
3165+
protected def msg(using Context): String =
3166+
i"constructor proxy $sym cannot be used as a value"
3167+
protected def explain(using Context): String =
3168+
i"""A constructor proxy is a symbol made up by the compiler to represent a non-existent
3169+
|factory method of a class. For instance, in
3170+
|
3171+
| class C(x: Int)
3172+
|
3173+
|C does not have an apply method since it is not a case class. Yet one can
3174+
|still create instances with applications like `C(3)` which expand to `new C(3)`.
3175+
|The `C` in this call is a constructor proxy. It can only be used as applications
3176+
|but not as a stand-alone value."""
3177+
3178+
class ContextBoundCompanionNotValue(sym: Symbol)(using Context)
3179+
extends TypeMsg(ConstructorProxyNotValueID):
3180+
protected def msg(using Context): String =
3181+
i"context bound companion $sym cannot be used as a value"
3182+
protected def explain(using Context): String =
3183+
i"""A context bound companion is a symbol made up by the compiler to represent the
3184+
|witness or witnesses generated for the context bound(s) of a type parameter or type.
3185+
|For instance, in
3186+
|
3187+
| class Monoid extends SemiGroup:
3188+
| type Self
3189+
| def unit: Self
3190+
|
3191+
| type A: Monoid
3192+
|
3193+
|there is just a type `A` declared but not a value `A`. Nevertheless, one can write
3194+
|the selection `A.unit`, which works because the compiler created a context bound
3195+
|companion value with the (term-)name `A`. However, these context bound companions
3196+
|are not values themselves, they can only be referred to in selections."""
3197+

0 commit comments

Comments
 (0)