Skip to content

Commit defea02

Browse files
committed
Implement context bound companions
1 parent 86b4cc2 commit defea02

26 files changed

+552
-67
lines changed

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

+36-27
Original file line numberDiff line numberDiff line change
@@ -227,30 +227,41 @@ object desugar {
227227
addDefaultGetters(elimContextBounds(meth, isPrimaryConstructor))
228228

229229
private def desugarContextBounds(
230-
tname: TypeName, rhs: Tree,
230+
tdef: TypeDef,
231231
evidenceBuf: ListBuffer[ValDef],
232232
flags: FlagSet,
233-
freshName: => TermName)(using Context): Tree = rhs match
234-
case ContextBounds(tbounds, cxbounds) =>
235-
val isMember = flags.isAllOf(DeferredGivenFlags)
236-
for bound <- cxbounds do
237-
val evidenceName = bound match
238-
case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty =>
239-
ownName
240-
case _ if !isMember && cxbounds.tail.isEmpty && Feature.enabled(Feature.modularity) =>
241-
tname.toTermName
242-
case _ =>
243-
if isMember then inventGivenOrExtensionName(bound)
244-
else freshName
245-
val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags)
246-
evidenceParam.pushAttachment(ContextBoundParam, ())
247-
evidenceBuf += evidenceParam
248-
tbounds
249-
case LambdaTypeTree(tparams, body) =>
250-
cpy.LambdaTypeTree(rhs)(tparams,
251-
desugarContextBounds(tname, body, evidenceBuf, flags, freshName))
252-
case _ =>
253-
rhs
233+
freshName: => TermName)(using Context): TypeDef =
234+
235+
val evidenceNames = ListBuffer[TermName]()
236+
237+
def desugarRhs(rhs: Tree): Tree = rhs match
238+
case ContextBounds(tbounds, cxbounds) =>
239+
val isMember = flags.isAllOf(DeferredGivenFlags)
240+
for bound <- cxbounds do
241+
val evidenceName = bound match
242+
case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty =>
243+
ownName
244+
case _ if !isMember && cxbounds.tail.isEmpty && Feature.enabled(Feature.modularity) =>
245+
tdef.name.toTermName
246+
case _ =>
247+
if isMember then inventGivenOrExtensionName(bound)
248+
else freshName
249+
evidenceNames += evidenceName
250+
val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags)
251+
evidenceParam.pushAttachment(ContextBoundParam, ())
252+
evidenceBuf += evidenceParam
253+
tbounds
254+
case LambdaTypeTree(tparams, body) =>
255+
cpy.LambdaTypeTree(rhs)(tparams, desugarRhs(body))
256+
case _ =>
257+
rhs
258+
259+
val tdef1 = cpy.TypeDef(tdef)(rhs = desugarRhs(tdef.rhs))
260+
if evidenceNames.nonEmpty && !evidenceNames.contains(tdef.name.toTermName) then
261+
val witnessNamesAnnot = WitnessNamesAnnot(evidenceNames.toList).withSpan(tdef.span)
262+
tdef1.withAddedAnnotation(witnessNamesAnnot)
263+
else
264+
tdef1
254265
end desugarContextBounds
255266

256267
private def elimContextBounds(meth: DefDef, isPrimaryConstructor: Boolean)(using Context): DefDef =
@@ -269,8 +280,7 @@ object desugar {
269280
val iflag = if Feature.sourceVersion.isAtLeast(`future`) then Given else Implicit
270281
val flags = if isPrimaryConstructor then iflag | LocalParamAccessor else iflag | Param
271282
mapParamss(paramss) {
272-
tparam => cpy.TypeDef(tparam)(rhs =
273-
desugarContextBounds(tparam.name, tparam.rhs, evidenceParamBuf, flags, freshName))
283+
tparam => desugarContextBounds(tparam, evidenceParamBuf, flags, freshName)
274284
}(identity)
275285

276286
rhs match
@@ -485,9 +495,8 @@ object desugar {
485495

486496
def typeDef(tdef: TypeDef)(using Context): Tree =
487497
val evidenceBuf = new ListBuffer[ValDef]
488-
val result = cpy.TypeDef(tdef)(rhs =
489-
desugarContextBounds(tdef.name, tdef.rhs, evidenceBuf,
490-
(tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, EmptyTermName))
498+
val result = desugarContextBounds(tdef, evidenceBuf,
499+
(tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, EmptyTermName)
491500
if evidenceBuf.isEmpty then result else Thicket(result :: evidenceBuf.toList)
492501

493502
/** The expansion of a class definition. See inline comments for what is involved */

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/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

+59
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:
@@ -248,4 +250,61 @@ object NamerOps:
248250
rhsCtx.gadtState.addBound(psym, tr, isUpper = true)
249251
}
250252

253+
/** Create a context-bound companion for type symbol `tsym` unless it
254+
* would clash with another parameter. `tsym` is a context-bound symbol
255+
* that defined a set of witnesses with names `witnessNames`.
256+
*
257+
* @param paramSymss If `tsym` is a type parameter, the other parameter symbols,
258+
* including witnesses, of the method containing `tsym`.
259+
* If `tsym` is an abstract type member, `paramSymss` is the
260+
* empty list.
261+
*
262+
* The context-bound companion has as name the name of `tsym` translated to
263+
* a term name. We create a synthetic val of the form
264+
*
265+
* val A: CBCompanion[witnessRef1 | ... | witnessRefN]
266+
*
267+
* where
268+
*
269+
* CBCompanion is the <context-bound-companion> type created in Definitions
270+
* withnessRefK is a refence to the K'the witness.
271+
*
272+
* The companion has the same access flags as the original type.
273+
*/
274+
def maybeAddContextBoundCompanionFor(tsym: Symbol, witnessNames: List[TermName], paramSymss: List[List[Symbol]])(using Context): Unit =
275+
val prefix = ctx.owner.thisType
276+
val companionName = tsym.name.toTermName
277+
val witnessRefs =
278+
if paramSymss.nonEmpty then
279+
if paramSymss.nestedExists(_.name == companionName) then Nil
280+
else
281+
witnessNames.map: witnessName =>
282+
prefix.select(paramSymss.nestedFind(_.name == witnessName).get)
283+
else
284+
witnessNames.map(prefix.select)
285+
if witnessRefs.nonEmpty then
286+
val cbtype = defn.CBCompanion.typeRef.appliedTo:
287+
witnessRefs.reduce[Type](OrType(_, _, soft = false))
288+
val cbc = newSymbol(
289+
ctx.owner, companionName,
290+
(tsym.flagsUNSAFE & AccessFlags) | Synthetic,
291+
cbtype)
292+
typr.println(i"contetx bpund companion created $cbc: $cbtype in ${ctx.owner}")
293+
ctx.enter(cbc)
294+
end maybeAddContextBoundCompanionFor
295+
296+
/** Add context bound companions to all context-bound types declared in
297+
* this class. This assumes that these types already have their
298+
* WitnessNames annotation set even before they are completed. This is
299+
* the case for unpickling but currently not for Namer. So the method
300+
* is only called during unpickling, and is not part of NamerOps.
301+
*/
302+
def addContextBoundCompanions(cls: ClassSymbol)(using Context): Unit =
303+
for sym <- cls.info.decls do
304+
if sym.isType && !sym.isClass then
305+
for ann <- sym.annotationsUNSAFE do
306+
if ann.symbol == defn.WitnessNamesAnnot then
307+
ann.tree match
308+
case ast.tpd.WitnessNamesAnnot(witnessNames) =>
309+
maybeAddContextBoundCompanionFor(sym, witnessNames, Nil)
251310
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$"
@@ -394,6 +395,7 @@ object StdNames {
394395
val TypeApply: N = "TypeApply"
395396
val TypeRef: N = "TypeRef"
396397
val UNIT : N = "UNIT"
398+
val WitnessNames: N = "WitnessNames"
397399
val acc: N = "acc"
398400
val adhocExtensions: N = "adhocExtensions"
399401
val andThen: N = "andThen"

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1207,8 +1207,8 @@ object SymDenotations {
12071207
*/
12081208
final def isEffectivelySealed(using Context): Boolean =
12091209
isOneOf(FinalOrSealed)
1210-
|| isClass && (!isOneOf(EffectivelyOpenFlags)
1211-
|| isLocalToCompilationUnit)
1210+
|| isClass
1211+
&& (!isOneOf(EffectivelyOpenFlags) || isLocalToCompilationUnit)
12121212

12131213
final def isLocalToCompilationUnit(using Context): Boolean =
12141214
is(Private)

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.info.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/printing/RefinedPrinter.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
640640
def toTextAnnot =
641641
toTextLocal(arg) ~~ annotText(annot.symbol.enclosingClass, annot)
642642
def toTextRetainsAnnot =
643-
try changePrec(GlobalPrec)(toText(arg) ~ "^" ~ toTextCaptureSet(captureSet))
643+
try changePrec(GlobalPrec)(toTextLocal(arg) ~ "^" ~ toTextCaptureSet(captureSet))
644644
catch case ex: IllegalCaptureRef => toTextAnnot
645645
if annot.symbol.maybeOwner.isRetains
646646
&& Feature.ccEnabled && !printDebug

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+

compiler/src/dotty/tools/dotc/transform/PostTyper.scala

+16-6
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,13 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
261261
}
262262
}
263263

264-
def checkNoConstructorProxy(tree: Tree)(using Context): Unit =
264+
def checkUsableAsValue(tree: Tree)(using Context): Unit =
265+
def unusable(msg: Symbol => Message) =
266+
report.error(msg(tree.symbol), tree.srcPos)
265267
if tree.symbol.is(ConstructorProxy) then
266-
report.error(em"constructor proxy ${tree.symbol} cannot be used as a value", tree.srcPos)
268+
unusable(ConstructorProxyNotValue(_))
269+
if tree.symbol.isContextBoundCompanion then
270+
unusable(ContextBoundCompanionNotValue(_))
267271

268272
def checkStableSelection(tree: Tree)(using Context): Unit =
269273
def check(qual: Tree) =
@@ -293,7 +297,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
293297
if tree.isType then
294298
checkNotPackage(tree)
295299
else
296-
checkNoConstructorProxy(tree)
300+
checkUsableAsValue(tree)
297301
registerNeedsInlining(tree)
298302
tree.tpe match {
299303
case tpe: ThisType => This(tpe.cls).withSpan(tree.span)
@@ -305,7 +309,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
305309
Checking.checkRealizable(qual.tpe, qual.srcPos)
306310
withMode(Mode.Type)(super.transform(checkNotPackage(tree)))
307311
else
308-
checkNoConstructorProxy(tree)
312+
checkUsableAsValue(tree)
309313
transformSelect(tree, Nil)
310314
case tree: Apply =>
311315
val methType = tree.fun.tpe.widen.asInstanceOf[MethodType]
@@ -437,8 +441,14 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
437441
val relativePath = util.SourceFile.relativePath(ctx.compilationUnit.source, reference)
438442
sym.addAnnotation(Annotation(defn.SourceFileAnnot, Literal(Constants.Constant(relativePath)), tree.span))
439443
else
440-
if !sym.is(Param) && !sym.owner.isOneOf(AbstractOrTrait) then
441-
Checking.checkGoodBounds(tree.symbol)
444+
if !sym.is(Param) then
445+
if !sym.owner.isOneOf(AbstractOrTrait) then
446+
Checking.checkGoodBounds(tree.symbol)
447+
if sym.owner.isClass && sym.hasAnnotation(defn.WitnessNamesAnnot) then
448+
val decls = sym.owner.info.decls
449+
for cbCompanion <- decls.lookupAll(sym.name.toTermName) do
450+
if cbCompanion.isContextBoundCompanion then
451+
decls.openForMutations.unlink(cbCompanion)
442452
(tree.rhs, sym.info) match
443453
case (rhs: LambdaTypeTree, bounds: TypeBounds) =>
444454
VarianceChecker.checkLambda(rhs, bounds)

0 commit comments

Comments
 (0)