Skip to content

Commit

Permalink
Refinements for context-bound companions
Browse files Browse the repository at this point in the history
Add a config setting whether or not to use the type name for unary context bounds
as default name. It's on by default. If it is off, context bound companions are created
instead.

After fixing several problems, the test suite was verified to compile with the setting set to off.
  • Loading branch information
odersky committed Mar 11, 2024
1 parent dfb59a1 commit ec3b7de
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 94 deletions.
79 changes: 52 additions & 27 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Annotations.Annotation
import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName}
import typer.{Namer, Checking}
import util.{Property, SourceFile, SourcePosition, Chars}
import config.Feature
import config.{Feature, Config}
import config.SourceVersion.*
import collection.mutable.ListBuffer
import reporting.*
Expand Down Expand Up @@ -230,7 +230,8 @@ object desugar {
tdef: TypeDef,
evidenceBuf: ListBuffer[ValDef],
flags: FlagSet,
freshName: => TermName)(using Context): TypeDef =
freshName: untpd.Tree => TermName,
allParamss: List[ParamClause])(using Context): TypeDef =

val evidenceNames = ListBuffer[TermName]()

Expand All @@ -241,11 +242,11 @@ object desugar {
val evidenceName = bound match
case ContextBoundTypeTree(_, _, ownName) if !ownName.isEmpty =>
ownName
case _ if !isMember && cxbounds.tail.isEmpty && Feature.enabled(Feature.modularity) =>
case _ if !isMember && cxbounds.tail.isEmpty
&& Feature.enabled(Feature.modularity) && Config.nameSingleContextBounds =>
tdef.name.toTermName
case _ =>
if isMember then inventGivenOrExtensionName(bound)
else freshName
freshName(bound)
evidenceNames += evidenceName
val evidenceParam = ValDef(evidenceName, bound, EmptyTree).withFlags(flags)
evidenceParam.pushAttachment(ContextBoundParam, ())
Expand All @@ -257,9 +258,13 @@ object desugar {
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)
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
Expand All @@ -268,7 +273,7 @@ object desugar {
val DefDef(_, paramss, tpt, rhs) = meth
val evidenceParamBuf = ListBuffer[ValDef]()
var seenContextBounds: Int = 0
def freshName =
def freshName(unused: Tree) =
seenContextBounds += 1 // Start at 1 like FreshNameCreator.
ContextBoundParamName(EmptyTermName, seenContextBounds)
// Just like with `makeSyntheticParameter` on nameless parameters of
Expand All @@ -280,7 +285,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 => desugarContextBounds(tparam, evidenceParamBuf, flags, freshName)
tparam => desugarContextBounds(tparam, evidenceParamBuf, flags, freshName, paramss)
}(identity)

rhs match
Expand Down Expand Up @@ -326,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 @@ -433,7 +438,14 @@ 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}")

def references(vdef: ValDef): Boolean =
vdef.tpt.existsSubTree:
Expand Down Expand Up @@ -464,15 +476,26 @@ object desugar {

@sharable private val synthetic = Modifiers(Synthetic)

private def toDefParam(tparam: TypeDef, keepAnnotations: Boolean): TypeDef = {
var mods = tparam.rawMods
if (!keepAnnotations) mods = mods.withAnnotations(Nil)
/** 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: 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, 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 = {
var mods = vparam.rawMods
if (!keepAnnotations) mods = mods.withAnnotations(Nil)
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 All @@ -495,8 +518,10 @@ object desugar {

def typeDef(tdef: TypeDef)(using Context): Tree =
val evidenceBuf = new ListBuffer[ValDef]
val result = desugarContextBounds(tdef, evidenceBuf,
(tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags, EmptyTermName)
val result = desugarContextBounds(
tdef, evidenceBuf,
(tdef.mods.flags.toTermFlags & AccessFlags) | Lazy | DeferredGivenFlags,
inventGivenOrExtensionName, Nil)
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 Expand Up @@ -571,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))
def defVparamss =
if (originalVparamss.isEmpty) { // ensure parameter list is non-empty
if (isCaseClass)
Expand All @@ -582,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 constrVparamss = defVparamss
// defVparamss also needed as separate tree nodes in implicitWrappers below.
// Need to be separate because they are `watch`ed in addParamRefinements.
Expand All @@ -608,7 +633,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 Expand Up @@ -914,9 +939,9 @@ object desugar {
}
else {
val defParamss = defVparamss.nestedMapConserve: param =>
// for named context bound parameters, we assume that they might have embedded types
// for context bound parameters, we assume that they might have embedded types
// so they should be treated as tracked.
if param.hasAttachment(ContextBoundParam) && !param.name.is(ContextBoundParamName)
if param.hasAttachment(ContextBoundParam) && Feature.enabled(Feature.modularity)
then param.withFlags(param.mods.flags | Tracked)
else param
match
Expand Down
7 changes: 7 additions & 0 deletions compiler/src/dotty/tools/dotc/config/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,11 @@ object Config {
*/
inline val checkLevelsOnConstraints = false
inline val checkLevelsOnInstantiation = true

/** If a type parameter `X` has a single context bounf `X: C`, should the
* witness parameter be named `X`? This would prevent the creation of a
* context bound companion.
*/
inline val nameSingleContextBounds = true
}

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
50 changes: 22 additions & 28 deletions compiler/src/dotty/tools/dotc/core/NamerOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -250,48 +250,42 @@ 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`.
/** Create a context-bound companion for type symbol `tsym`, which has a context
* bound that defines 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.
* @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: CBCompanion[witnessRef1 | ... | witnessRefN]
* val A: `<context-bound-companion>`[witnessRef1 | ... | witnessRefN]
*
* where
*
* CBCompanion is the <context-bound-companion> type created in Definitions
* withnessRefK is a refence to the K'the witness.
* <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 maybeAddContextBoundCompanionFor(tsym: Symbol, witnessNames: List[TermName], paramSymss: List[List[Symbol]])(using Context): Unit =
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 paramSymss.nonEmpty then
if paramSymss.nestedExists(_.name == companionName) then Nil
else
witnessNames.map: witnessName =>
prefix.select(paramSymss.nestedFind(_.name == witnessName).get)
if params.nonEmpty then
witnessNames.map: witnessName =>
prefix.select(params.find(_.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
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
Expand All @@ -306,5 +300,5 @@ object NamerOps:
if ann.symbol == defn.WitnessNamesAnnot then
ann.tree match
case ast.tpd.WitnessNamesAnnot(witnessNames) =>
maybeAddContextBoundCompanionFor(sym, witnessNames, Nil)
addContextBoundCompanionFor(sym, witnessNames, Nil)
end NamerOps
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class SymUtils:
}

def isContextBoundCompanion(using Context): Boolean =
self.is(Synthetic) && self.info.typeSymbol == defn.CBCompanion
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,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
Loading

0 comments on commit ec3b7de

Please sign in to comment.