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 Apr 2, 2024
1 parent 153b958 commit f38d3c7
Show file tree
Hide file tree
Showing 25 changed files with 492 additions and 47 deletions.
50 changes: 37 additions & 13 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,16 @@ object desugar {
case _ =>
rhs

cpy.TypeDef(tdef)(rhs = desugarRhs(tdef.rhs))
val tdef1 = cpy.TypeDef(tdef)(rhs = desugarRhs(tdef.rhs))
if Feature.enabled(Feature.modularity)
&& evidenceNames.nonEmpty
&& !evidenceNames.contains(tdef.name.toTermName)
&& !allParamss.nestedExists(_.name == tdef.name.toTermName)
then
tdef1.withAddedAnnotation:
WitnessNamesAnnot(evidenceNames.toList).withSpan(tdef.span)
else
tdef1
end desugarContextBounds

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

def getterParamss(n: Int): List[ParamClause] =
mapParamss(takeUpTo(paramssNoRHS, n)) {
tparam => dropContextBounds(toDefParam(tparam, keepAnnotations = true))
tparam => dropContextBounds(toDefParam(tparam, KeepAnnotations.All))
} {
vparam => toDefParam(vparam, keepAnnotations = true, keepDefault = false)
vparam => toDefParam(vparam, KeepAnnotations.All, keepDefault = false)
}

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

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

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

Expand Down Expand Up @@ -462,16 +476,26 @@ object desugar {

@sharable private val synthetic = Modifiers(Synthetic)

/** Which annotations to keep in derived parameters */
private enum KeepAnnotations:
case None, All, WitnessOnly

/** Filter annotations in `mods` according to `keep` */
private def filterAnnots(mods: Modifiers, keep: Boolean)(using Context) =
if keep then mods else mods.withAnnotations(Nil)
private def filterAnnots(mods: Modifiers, keep: KeepAnnotations)(using Context) = keep match
case KeepAnnotations.None => mods.withAnnotations(Nil)
case KeepAnnotations.All => mods
case KeepAnnotations.WitnessOnly =>
mods.withAnnotations:
mods.annotations.filter:
case WitnessNamesAnnot(_) => true
case _ => false

private def toDefParam(tparam: TypeDef, keepAnnotations: Boolean)(using Context): TypeDef =
val mods = filterAnnots(tparam.rawMods, keepAnnotations)
private def toDefParam(tparam: TypeDef, keep: KeepAnnotations)(using Context): TypeDef =
val mods = filterAnnots(tparam.rawMods, keep)
tparam.withMods(mods & EmptyFlags | Param)

private def toDefParam(vparam: ValDef, keepAnnotations: Boolean, keepDefault: Boolean)(using Context): ValDef = {
val mods = filterAnnots(vparam.rawMods, keepAnnotations)
private def toDefParam(vparam: ValDef, keep: KeepAnnotations, keepDefault: Boolean)(using Context): ValDef = {
val mods = filterAnnots(vparam.rawMods, keep)
val hasDefault = if keepDefault then HasDefault else EmptyFlags
// Need to ensure that tree is duplicated since term parameters can be watched
// and cloning a term parameter will copy its watchers to the clone, which means
Expand Down Expand Up @@ -572,7 +596,7 @@ object desugar {
// Annotations on class _type_ parameters are set on the derived parameters
// but not on the constructor parameters. The reverse is true for
// annotations on class _value_ parameters.
val constrTparams = impliedTparams.map(toDefParam(_, keepAnnotations = false))
val constrTparams = impliedTparams.map(toDefParam(_, KeepAnnotations.WitnessOnly))
val constrVparamss =
if (originalVparamss.isEmpty) { // ensure parameter list is non-empty
if (isCaseClass)
Expand All @@ -583,7 +607,7 @@ object desugar {
report.error(CaseClassMissingNonImplicitParamList(cdef), namePos)
ListOfNil
}
else originalVparamss.nestedMap(toDefParam(_, keepAnnotations = true, keepDefault = true))
else originalVparamss.nestedMap(toDefParam(_, KeepAnnotations.All, keepDefault = true))
val derivedTparams =
constrTparams.zipWithConserve(impliedTparams)((tparam, impliedParam) =>
derivedTypeParam(tparam).withAnnotations(impliedParam.mods.annotations))
Expand All @@ -605,7 +629,7 @@ object desugar {
defDef(
addEvidenceParams(
cpy.DefDef(ddef)(paramss = joinParams(constrTparams, ddef.paramss)),
evidenceParams(constr1).map(toDefParam(_, keepAnnotations = false, keepDefault = false)))))
evidenceParams(constr1).map(toDefParam(_, KeepAnnotations.None, keepDefault = false)))))
case stat =>
stat
}
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
13 changes: 8 additions & 5 deletions compiler/src/dotty/tools/dotc/core/Contexts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Symbols.*
import Scopes.*
import Uniques.*
import ast.Trees.*
import Flags.ParamAccessor
import ast.untpd
import util.{NoSource, SimpleIdentityMap, SourceFile, HashSet, ReusableInstance}
import typer.{Implicits, ImportInfo, SearchHistory, SearchRoot, TypeAssigner, Typer, Nullables}
Expand Down Expand Up @@ -399,7 +400,8 @@ object Contexts {
*
* - as owner: The primary constructor of the class
* - as outer context: The context enclosing the class context
* - as scope: The parameter accessors in the class context
* - as scope: type parameters, the parameter accessors, and
* the context bound companions in the class context,
*
* The reasons for this peculiar choice of attributes are as follows:
*
Expand All @@ -413,10 +415,11 @@ object Contexts {
* context see the constructor parameters instead, but then we'd need a final substitution step
* from constructor parameters to class parameter accessors.
*/
def superCallContext: Context = {
val locals = newScopeWith(owner.typeParams ++ owner.asClass.paramAccessors*)
superOrThisCallContext(owner.primaryConstructor, locals)
}
def superCallContext: Context =
val locals = owner.typeParams
++ owner.asClass.unforcedDecls.filter: sym =>
sym.is(ParamAccessor) || sym.isContextBoundCompanion
superOrThisCallContext(owner.primaryConstructor, newScopeWith(locals*))

/** The context for the arguments of a this(...) constructor call.
* The context is computed from the local auxiliary constructor context.
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
53 changes: 53 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 @@ -256,4 +258,55 @@ object NamerOps:
rhsCtx.gadtState.addBound(psym, tr, isUpper = true)
}

/** Create a context-bound companion for type symbol `tsym`, which has a context
* bound that defines a set of witnesses with names `witnessNames`.
*
* @param parans If `tsym` is a type parameter, a list of parameter symbols
* that include all witnesses, otherwise 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: `<context-bound-companion>`[witnessRef1 | ... | witnessRefN]
*
* where
*
* <context-bound-companion> is the CBCompanion type created in Definitions
* withnessRefK is a refence to the K'th witness.
*
* The companion has the same access flags as the original type.
*/
def addContextBoundCompanionFor(tsym: Symbol, witnessNames: List[TermName], params: List[Symbol])(using Context): Unit =
val prefix = ctx.owner.thisType
val companionName = tsym.name.toTermName
val witnessRefs =
if params.nonEmpty then
witnessNames.map: witnessName =>
prefix.select(params.find(_.name == witnessName).get)
else
witnessNames.map(TermRef(prefix, _))
val cbtype = defn.CBCompanion.typeRef.appliedTo:
witnessRefs.reduce[Type](OrType(_, _, soft = false))
val cbc = newSymbol(
ctx.owner, companionName,
(tsym.flagsUNSAFE & (AccessFlags)).toTermFlags | Synthetic,
cbtype)
typr.println(s"context bound companion created $cbc for $witnessNames in ${ctx.owner}")
ctx.enter(cbc)
end addContextBoundCompanionFor

/** 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) =>
addContextBoundCompanionFor(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 @@ -393,6 +394,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
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.infoOrCompleter.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 @@ -1127,6 +1127,7 @@ class TreeUnpickler(reader: TastyReader,
})
defn.patchStdLibClass(cls)
NamerOps.addConstructorProxies(cls)
NamerOps.addContextBoundCompanions(cls)
setSpan(start,
untpd.Template(constr, mappedParents, self, lazyStats)
.withType(localDummy.termRef))
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -430,11 +430,11 @@ class PlainPrinter(_ctx: Context) extends Printer {
sym.isEffectiveRoot || sym.isAnonymousClass || sym.name.isReplWrapperName

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

protected def decomposeLambdas(bounds: TypeBounds): (Text, TypeBounds) =
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."""

Loading

0 comments on commit f38d3c7

Please sign in to comment.