Skip to content

Commit

Permalink
Implement context bound companions
Browse files Browse the repository at this point in the history
  • Loading branch information
odersky committed Mar 8, 2024
1 parent 8cce649 commit dfb59a1
Show file tree
Hide file tree
Showing 26 changed files with 552 additions and 67 deletions.
63 changes: 36 additions & 27 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -227,30 +227,41 @@ object desugar {
addDefaultGetters(elimContextBounds(meth, isPrimaryConstructor))

private def desugarContextBounds(
tname: TypeName, rhs: Tree,
tdef: TypeDef,
evidenceBuf: ListBuffer[ValDef],
flags: FlagSet,
freshName: => TermName)(using Context): Tree = rhs match
case ContextBounds(tbounds, cxbounds) =>
val isMember = flags.isAllOf(DeferredGivenFlags)
for bound <- cxbounds do
val evidenceName = bound match
case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty =>
ownName
case _ if !isMember && cxbounds.tail.isEmpty && Feature.enabled(Feature.modularity) =>
tname.toTermName
case _ =>
if isMember then inventGivenOrExtensionName(bound)
else freshName
val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags)
evidenceParam.pushAttachment(ContextBoundParam, ())
evidenceBuf += evidenceParam
tbounds
case LambdaTypeTree(tparams, body) =>
cpy.LambdaTypeTree(rhs)(tparams,
desugarContextBounds(tname, body, evidenceBuf, flags, freshName))
case _ =>
rhs
freshName: => TermName)(using Context): TypeDef =

val evidenceNames = ListBuffer[TermName]()

def desugarRhs(rhs: Tree): Tree = rhs match
case ContextBounds(tbounds, cxbounds) =>
val isMember = flags.isAllOf(DeferredGivenFlags)
for bound <- cxbounds do
val evidenceName = bound match
case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty =>
ownName
case _ if !isMember && cxbounds.tail.isEmpty && Feature.enabled(Feature.modularity) =>
tdef.name.toTermName
case _ =>
if isMember then inventGivenOrExtensionName(bound)
else freshName
evidenceNames += evidenceName
val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags)
evidenceParam.pushAttachment(ContextBoundParam, ())
evidenceBuf += evidenceParam
tbounds
case LambdaTypeTree(tparams, body) =>
cpy.LambdaTypeTree(rhs)(tparams, desugarRhs(body))
case _ =>
rhs

val tdef1 = cpy.TypeDef(tdef)(rhs = desugarRhs(tdef.rhs))
if evidenceNames.nonEmpty && !evidenceNames.contains(tdef.name.toTermName) then
val witnessNamesAnnot = WitnessNamesAnnot(evidenceNames.toList).withSpan(tdef.span)
tdef1.withAddedAnnotation(witnessNamesAnnot)
else
tdef1
end desugarContextBounds

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

rhs match
Expand Down Expand Up @@ -485,9 +495,8 @@ object desugar {

def typeDef(tdef: TypeDef)(using Context): Tree =
val evidenceBuf = new ListBuffer[ValDef]
val result = cpy.TypeDef(tdef)(rhs =
desugarContextBounds(tdef.name, tdef.rhs, evidenceBuf,
(tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, EmptyTermName))
val result = desugarContextBounds(tdef, evidenceBuf,
(tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, EmptyTermName)
if evidenceBuf.isEmpty then result else Thicket(result :: evidenceBuf.toList)

/** The expansion of a class definition. See inline comments for what is involved */
Expand Down
31 changes: 31 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package ast
import core.*
import Flags.*, Trees.*, Types.*, Contexts.*
import Names.*, StdNames.*, NameOps.*, Symbols.*
import Annotations.Annotation
import NameKinds.ContextBoundParamName
import typer.ConstFold
import reporting.trace

Expand Down Expand Up @@ -376,6 +378,35 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
case _ =>
tree.tpe.isInstanceOf[ThisType]
}

/** Extractor for annotation.internal.WitnessNames(name_1, ..., name_n)`
* represented as an untyped or typed tree.
*/
object WitnessNamesAnnot:
def apply(names0: List[TermName])(using Context): untpd.Tree =
untpd.TypedSplice(tpd.New(
defn.WitnessNamesAnnot.typeRef,
tpd.SeqLiteral(names0.map(n => tpd.Literal(Constant(n.toString))), tpd.TypeTree(defn.StringType)) :: Nil
))

def unapply(tree: Tree)(using Context): Option[List[TermName]] =
def isWitnessNames(tp: Type) = tp match
case tp: TypeRef =>
tp.name == tpnme.WitnessNames && tp.symbol == defn.WitnessNamesAnnot
case _ =>
false
unsplice(tree) match
case Apply(
Select(New(tpt: tpd.TypeTree), nme.CONSTRUCTOR),
SeqLiteral(elems, _) :: Nil
) if isWitnessNames(tpt.tpe) =>
Some:
elems.map:
case Literal(Constant(str: String)) =>
ContextBoundParamName.unmangle(str.toTermName.asSimpleName)
case _ =>
None
end WitnessNamesAnnot
}

trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] =>
Expand Down
9 changes: 9 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,13 @@ class Definitions {
@tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _))
@tu lazy val orType: TypeSymbol = enterBinaryAlias(tpnme.OR, OrType(_, _, soft = false))

@tu lazy val CBCompanion: TypeSymbol = // type `<context-bound-companion>`[-Refs]
enterPermanentSymbol(tpnme.CBCompanion,
TypeBounds(NothingType,
HKTypeLambda(tpnme.syntheticTypeParamName(0) :: Nil, Contravariant :: Nil)(
tl => TypeBounds.empty :: Nil,
tl => AnyType))).asType

/** Method representing a throw */
@tu lazy val throwMethod: TermSymbol = enterMethod(OpsPackageClass, nme.THROWkw,
MethodType(List(ThrowableType), NothingType))
Expand Down Expand Up @@ -1064,6 +1071,7 @@ class Definitions {
@tu lazy val RetainsCapAnnot: ClassSymbol = requiredClass("scala.annotation.retainsCap")
@tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName")
@tu lazy val PublicInBinaryAnnot: ClassSymbol = requiredClass("scala.annotation.publicInBinary")
@tu lazy val WitnessNamesAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WitnessNames")

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

Expand Down Expand Up @@ -2141,6 +2149,7 @@ class Definitions {
NullClass,
NothingClass,
SingletonClass,
CBCompanion,
MaybeCapabilityAnnot)

@tu lazy val syntheticCoreClasses: List[Symbol] = syntheticScalaClasses ++ List(
Expand Down
59 changes: 59 additions & 0 deletions compiler/src/dotty/tools/dotc/core/NamerOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ package core

import Contexts.*, Symbols.*, Types.*, Flags.*, Scopes.*, Decorators.*, Names.*, NameOps.*
import SymDenotations.{LazyType, SymDenotation}, StdNames.nme
import ContextOps.enter
import TypeApplications.EtaExpansion
import collection.mutable
import config.Printers.typr

/** Operations that are shared between Namer and TreeUnpickler */
object NamerOps:
Expand Down Expand Up @@ -248,4 +250,61 @@ object NamerOps:
rhsCtx.gadtState.addBound(psym, tr, isUpper = true)
}

/** Create a context-bound companion for type symbol `tsym` unless it
* would clash with another parameter. `tsym` is a context-bound symbol
* that defined a set of witnesses with names `witnessNames`.
*
* @param paramSymss If `tsym` is a type parameter, the other parameter symbols,
* including witnesses, of the method containing `tsym`.
* If `tsym` is an abstract type member, `paramSymss` is the
* empty list.
*
* The context-bound companion has as name the name of `tsym` translated to
* a term name. We create a synthetic val of the form
*
* val A: CBCompanion[witnessRef1 | ... | witnessRefN]
*
* where
*
* CBCompanion is the <context-bound-companion> type created in Definitions
* withnessRefK is a refence to the K'the witness.
*
* The companion has the same access flags as the original type.
*/
def maybeAddContextBoundCompanionFor(tsym: Symbol, witnessNames: List[TermName], paramSymss: List[List[Symbol]])(using Context): Unit =
val prefix = ctx.owner.thisType
val companionName = tsym.name.toTermName
val witnessRefs =
if paramSymss.nonEmpty then
if paramSymss.nestedExists(_.name == companionName) then Nil
else
witnessNames.map: witnessName =>
prefix.select(paramSymss.nestedFind(_.name == witnessName).get)
else
witnessNames.map(prefix.select)
if witnessRefs.nonEmpty then
val cbtype = defn.CBCompanion.typeRef.appliedTo:
witnessRefs.reduce[Type](OrType(_, _, soft = false))
val cbc = newSymbol(
ctx.owner, companionName,
(tsym.flagsUNSAFE & AccessFlags) | Synthetic,
cbtype)
typr.println(i"contetx bpund companion created $cbc: $cbtype in ${ctx.owner}")
ctx.enter(cbc)
end maybeAddContextBoundCompanionFor

/** Add context bound companions to all context-bound types declared in
* this class. This assumes that these types already have their
* WitnessNames annotation set even before they are completed. This is
* the case for unpickling but currently not for Namer. So the method
* is only called during unpickling, and is not part of NamerOps.
*/
def addContextBoundCompanions(cls: ClassSymbol)(using Context): Unit =
for sym <- cls.info.decls do
if sym.isType && !sym.isClass then
for ann <- sym.annotationsUNSAFE do
if ann.symbol == defn.WitnessNamesAnnot then
ann.tree match
case ast.tpd.WitnessNamesAnnot(witnessNames) =>
maybeAddContextBoundCompanionFor(sym, witnessNames, Nil)
end NamerOps
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ object StdNames {

// Compiler-internal
val CAPTURE_ROOT: N = "cap"
val CBCompanion: N = "<context-bound-companion>"
val CONSTRUCTOR: N = "<init>"
val STATIC_CONSTRUCTOR: N = "<clinit>"
val EVT2U: N = "evt2u$"
Expand Down Expand Up @@ -394,6 +395,7 @@ object StdNames {
val TypeApply: N = "TypeApply"
val TypeRef: N = "TypeRef"
val UNIT : N = "UNIT"
val WitnessNames: N = "WitnessNames"
val acc: N = "acc"
val adhocExtensions: N = "adhocExtensions"
val andThen: N = "andThen"
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1207,8 +1207,8 @@ object SymDenotations {
*/
final def isEffectivelySealed(using Context): Boolean =
isOneOf(FinalOrSealed)
|| isClass && (!isOneOf(EffectivelyOpenFlags)
|| isLocalToCompilationUnit)
|| isClass
&& (!isOneOf(EffectivelyOpenFlags) || isLocalToCompilationUnit)

final def isLocalToCompilationUnit(using Context): Boolean =
is(Private)
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/core/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ class SymUtils:
!d.isPrimitiveValueClass
}

def isContextBoundCompanion(using Context): Boolean =
self.is(Synthetic) && self.info.typeSymbol == defn.CBCompanion

/** Is this a case class for which a product mirror is generated?
* Excluded are value classes, abstract classes and case classes with more than one
* parameter section.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
def toTextAnnot =
toTextLocal(arg) ~~ annotText(annot.symbol.enclosingClass, annot)
def toTextRetainsAnnot =
try changePrec(GlobalPrec)(toText(arg) ~ "^" ~ toTextCaptureSet(captureSet))
try changePrec(GlobalPrec)(toTextLocal(arg) ~ "^" ~ toTextCaptureSet(captureSet))
catch case ex: IllegalCaptureRef => toTextAnnot
if annot.symbol.maybeOwner.isRetains
&& Feature.ccEnabled && !printDebug
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
case MatchTypeLegacyPatternID // errorNumber: 191
case UnstableInlineAccessorID // errorNumber: 192
case VolatileOnValID // errorNumber: 193
case ConstructorProxyNotValueID // errorNumber: 194
case ContextBoundCompanionNotValueID // errorNumber: 195

def errorNumber = ordinal - 1

Expand Down
36 changes: 36 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3159,3 +3159,39 @@ class VolatileOnVal()(using Context)
extends SyntaxMsg(VolatileOnValID):
protected def msg(using Context): String = "values cannot be volatile"
protected def explain(using Context): String = ""

class ConstructorProxyNotValue(sym: Symbol)(using Context)
extends TypeMsg(ConstructorProxyNotValueID):
protected def msg(using Context): String =
i"constructor proxy $sym cannot be used as a value"
protected def explain(using Context): String =
i"""A constructor proxy is a symbol made up by the compiler to represent a non-existent
|factory method of a class. For instance, in
|
| class C(x: Int)
|
|C does not have an apply method since it is not a case class. Yet one can
|still create instances with applications like `C(3)` which expand to `new C(3)`.
|The `C` in this call is a constructor proxy. It can only be used as applications
|but not as a stand-alone value."""

class ContextBoundCompanionNotValue(sym: Symbol)(using Context)
extends TypeMsg(ConstructorProxyNotValueID):
protected def msg(using Context): String =
i"context bound companion $sym cannot be used as a value"
protected def explain(using Context): String =
i"""A context bound companion is a symbol made up by the compiler to represent the
|witness or witnesses generated for the context bound(s) of a type parameter or type.
|For instance, in
|
| class Monoid extends SemiGroup:
| type Self
| def unit: Self
|
| type A: Monoid
|
|there is just a type `A` declared but not a value `A`. Nevertheless, one can write
|the selection `A.unit`, which works because the compiler created a context bound
|companion value with the (term-)name `A`. However, these context bound companions
|are not values themselves, they can only be referred to in selections."""

22 changes: 16 additions & 6 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,13 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
}
}

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

def checkStableSelection(tree: Tree)(using Context): Unit =
def check(qual: Tree) =
Expand Down Expand Up @@ -293,7 +297,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
if tree.isType then
checkNotPackage(tree)
else
checkNoConstructorProxy(tree)
checkUsableAsValue(tree)
registerNeedsInlining(tree)
tree.tpe match {
case tpe: ThisType => This(tpe.cls).withSpan(tree.span)
Expand All @@ -305,7 +309,7 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
Checking.checkRealizable(qual.tpe, qual.srcPos)
withMode(Mode.Type)(super.transform(checkNotPackage(tree)))
else
checkNoConstructorProxy(tree)
checkUsableAsValue(tree)
transformSelect(tree, Nil)
case tree: Apply =>
val methType = tree.fun.tpe.widen.asInstanceOf[MethodType]
Expand Down Expand Up @@ -437,8 +441,14 @@ class PostTyper extends MacroTransform with InfoTransformer { thisPhase =>
val relativePath = util.SourceFile.relativePath(ctx.compilationUnit.source, reference)
sym.addAnnotation(Annotation(defn.SourceFileAnnot, Literal(Constants.Constant(relativePath)), tree.span))
else
if !sym.is(Param) && !sym.owner.isOneOf(AbstractOrTrait) then
Checking.checkGoodBounds(tree.symbol)
if !sym.is(Param) then
if !sym.owner.isOneOf(AbstractOrTrait) then
Checking.checkGoodBounds(tree.symbol)
if sym.owner.isClass && sym.hasAnnotation(defn.WitnessNamesAnnot) then
val decls = sym.owner.info.decls
for cbCompanion <- decls.lookupAll(sym.name.toTermName) do
if cbCompanion.isContextBoundCompanion then
decls.openForMutations.unlink(cbCompanion)
(tree.rhs, sym.info) match
case (rhs: LambdaTypeTree, bounds: TypeBounds) =>
VarianceChecker.checkLambda(rhs, bounds)
Expand Down
Loading

0 comments on commit dfb59a1

Please sign in to comment.