Skip to content

Commit ce09ef3

Browse files
committed
Implement context bound companions
1 parent 96fbf29 commit ce09ef3

25 files changed

+496
-48
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 =
@@ -323,9 +332,9 @@ object desugar {
323332

324333
def getterParamss(n: Int): List[ParamClause] =
325334
mapParamss(takeUpTo(paramssNoRHS, n)) {
326-
tparam => dropContextBounds(toDefParam(tparam, keepAnnotations = true))
335+
tparam => dropContextBounds(toDefParam(tparam, KeepAnnotations.All))
327336
} {
328-
vparam => toDefParam(vparam, keepAnnotations = true, keepDefault = false)
337+
vparam => toDefParam(vparam, KeepAnnotations.All, keepDefault = false)
329338
}
330339

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

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

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

@@ -463,16 +477,26 @@ object desugar {
463477

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

480+
/** Which annotations to keep in derived parameters */
481+
private enum KeepAnnotations:
482+
case None, All, WitnessOnly
483+
466484
/** Filter annotations in `mods` according to `keep` */
467-
private def filterAnnots(mods: Modifiers, keep: Boolean)(using Context) =
468-
if keep then mods else mods.withAnnotations(Nil)
485+
private def filterAnnots(mods: Modifiers, keep: KeepAnnotations)(using Context) = keep match
486+
case KeepAnnotations.None => mods.withAnnotations(Nil)
487+
case KeepAnnotations.All => mods
488+
case KeepAnnotations.WitnessOnly =>
489+
mods.withAnnotations:
490+
mods.annotations.filter:
491+
case WitnessNamesAnnot(_) => true
492+
case _ => false
469493

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

474-
private def toDefParam(vparam: ValDef, keepAnnotations: Boolean, keepDefault: Boolean)(using Context): ValDef = {
475-
val mods = filterAnnots(vparam.rawMods, keepAnnotations)
498+
private def toDefParam(vparam: ValDef, keep: KeepAnnotations, keepDefault: Boolean)(using Context): ValDef = {
499+
val mods = filterAnnots(vparam.rawMods, keep)
476500
val hasDefault = if keepDefault then HasDefault else EmptyFlags
477501
// Need to ensure that tree is duplicated since term parameters can be watched
478502
// and cloning a term parameter will copy its watchers to the clone, which means
@@ -573,7 +597,7 @@ object desugar {
573597
// Annotations on class _type_ parameters are set on the derived parameters
574598
// but not on the constructor parameters. The reverse is true for
575599
// annotations on class _value_ parameters.
576-
val constrTparams = impliedTparams.map(toDefParam(_, keepAnnotations = false))
600+
val constrTparams = impliedTparams.map(toDefParam(_, KeepAnnotations.WitnessOnly))
577601
val constrVparamss =
578602
if (originalVparamss.isEmpty) { // ensure parameter list is non-empty
579603
if (isCaseClass)
@@ -584,7 +608,7 @@ object desugar {
584608
report.error(CaseClassMissingNonImplicitParamList(cdef), namePos)
585609
ListOfNil
586610
}
587-
else originalVparamss.nestedMap(toDefParam(_, keepAnnotations = true, keepDefault = true))
611+
else originalVparamss.nestedMap(toDefParam(_, KeepAnnotations.All, keepDefault = true))
588612
val derivedTparams =
589613
constrTparams.zipWithConserve(impliedTparams)((tparam, impliedParam) =>
590614
derivedTypeParam(tparam).withAnnotations(impliedParam.mods.annotations))
@@ -606,7 +630,7 @@ object desugar {
606630
defDef(
607631
addEvidenceParams(
608632
cpy.DefDef(ddef)(paramss = joinParams(constrTparams, ddef.paramss)),
609-
evidenceParams(constr1).map(toDefParam(_, keepAnnotations = false, keepDefault = false)))))
633+
evidenceParams(constr1).map(toDefParam(_, KeepAnnotations.None, keepDefault = false)))))
610634
case stat =>
611635
stat
612636
}

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

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

385416
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))
@@ -1062,6 +1069,7 @@ class Definitions {
10621069
@tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName")
10631070
@tu lazy val RetainsArgAnnot: ClassSymbol = requiredClass("scala.annotation.retainsArg")
10641071
@tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary")
1072+
@tu lazy val WitnessNamesAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WitnessNames")
10651073

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

@@ -2158,6 +2166,7 @@ class Definitions {
21582166
NullClass,
21592167
NothingClass,
21602168
SingletonClass,
2169+
CBCompanion,
21612170
MaybeCapabilityAnnot)
21622171

21632172
@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$"
@@ -396,6 +397,7 @@ object StdNames {
396397
val TypeApply: N = "TypeApply"
397398
val TypeRef: N = "TypeRef"
398399
val UNIT : N = "UNIT"
400+
val WitnessNames: N = "WitnessNames"
399401
val acc: N = "acc"
400402
val adhocExtensions: N = "adhocExtensions"
401403
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
@@ -1138,6 +1138,7 @@ class TreeUnpickler(reader: TastyReader,
11381138
})
11391139
defn.patchStdLibClass(cls)
11401140
NamerOps.addConstructorProxies(cls)
1141+
NamerOps.addContextBoundCompanions(cls)
11411142
setSpan(start,
11421143
untpd.Template(constr, mappedParents, self, lazyStats)
11431144
.withType(localDummy.termRef))

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

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

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

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

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,9 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
208208
case UnstableInlineAccessorID // errorNumber: 192
209209
case VolatileOnValID // errorNumber: 193
210210
case ExtensionNullifiedByMemberID // errorNumber: 194
211-
case InlinedAnonClassWarningID // errorNumber: 195
211+
case ConstructorProxyNotValueID // errorNumber: 195
212+
case ContextBoundCompanionNotValueID // errorNumber: 196
213+
case InlinedAnonClassWarningID // errorNumber: 197
212214

213215
def errorNumber = ordinal - 1
214216

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

+36
Original file line numberDiff line numberDiff line change
@@ -3203,3 +3203,39 @@ class VolatileOnVal()(using Context)
32033203
extends SyntaxMsg(VolatileOnValID):
32043204
protected def msg(using Context): String = "values cannot be volatile"
32053205
protected def explain(using Context): String = ""
3206+
3207+
class ConstructorProxyNotValue(sym: Symbol)(using Context)
3208+
extends TypeMsg(ConstructorProxyNotValueID):
3209+
protected def msg(using Context): String =
3210+
i"constructor proxy $sym cannot be used as a value"
3211+
protected def explain(using Context): String =
3212+
i"""A constructor proxy is a symbol made up by the compiler to represent a non-existent
3213+
|factory method of a class. For instance, in
3214+
|
3215+
| class C(x: Int)
3216+
|
3217+
|C does not have an apply method since it is not a case class. Yet one can
3218+
|still create instances with applications like `C(3)` which expand to `new C(3)`.
3219+
|The `C` in this call is a constructor proxy. It can only be used as applications
3220+
|but not as a stand-alone value."""
3221+
3222+
class ContextBoundCompanionNotValue(sym: Symbol)(using Context)
3223+
extends TypeMsg(ConstructorProxyNotValueID):
3224+
protected def msg(using Context): String =
3225+
i"context bound companion $sym cannot be used as a value"
3226+
protected def explain(using Context): String =
3227+
i"""A context bound companion is a symbol made up by the compiler to represent the
3228+
|witness or witnesses generated for the context bound(s) of a type parameter or type.
3229+
|For instance, in
3230+
|
3231+
| class Monoid extends SemiGroup:
3232+
| type Self
3233+
| def unit: Self
3234+
|
3235+
| type A: Monoid
3236+
|
3237+
|there is just a type `A` declared but not a value `A`. Nevertheless, one can write
3238+
|the selection `A.unit`, which works because the compiler created a context bound
3239+
|companion value with the (term-)name `A`. However, these context bound companions
3240+
|are not values themselves, they can only be referred to in selections."""
3241+

0 commit comments

Comments
 (0)