Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions compiler/src/dotty/tools/dotc/cc/Capability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,20 @@ object Capabilities:
else if cls2.isSubClass(cls1) then cls2
else defn.NothingClass

/** The least classifier that both `cls1` and `cls2` extend, or `AnyClass`,
* if `cls1` and `cls2` don't have a common ancestor classifier. It is
* assumed that each of `cls1` and `cls2` is either a classifier class or
* is equal to AnyClass.
*/
def greatestClassifier(cls1: ClassSymbol, cls2: ClassSymbol)(using Context): ClassSymbol =
if cls1.isSubClass(cls2) then cls1
else if cls2.isSubClass(cls1) then cls2
else
cls1.classDenot.baseClasses
.find: bc1 =>
bc1.isClassifiedCapabilityClass && cls2.isSubClass(bc1)
.getOrElse(defn.AnyClass)

/** The smallest list D of class symbols in cs1 and cs2 such that
* every class symbol in cs1 and cs2 is a subclass of a class symbol in D
*/
Expand Down
57 changes: 31 additions & 26 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -570,26 +570,34 @@ extension (cls: ClassSymbol) {
* @return the implied capture set, and the list of fields contributing to it
*/
def capturesImpliedByFields(core: Type)(using Context): (refs: CaptureSet, fields: List[Symbol]) = {
var infos: List[String] = Nil
def pushInfo(msg: => String) =
if ctx.settings.YccVerbose.value then infos = msg :: infos

def knownFields(cls: ClassSymbol) =
ccState.fieldsWithExplicitTypes // pick fields with explicit types for classes in this compilation unit
.getOrElse(cls, cls.info.decls.toList) // pick all symbols in class scope for other classes

/** The classifiers of the LocalCaps in the span capture sets of all fields
* in the given class `cls`.
def commonAncestor(clss: List[ClassSymbol]): Symbol =
if clss.isEmpty then NoSymbol
else clss.reduce(greatestClassifier)

/** The implied classifier of the LocalCap of the class instance, derived from
* - the clasifiers of the LocalCaps in the span capture sets of all fields
* - the implied classifiers of the parent classes
* - if `cls` is a stateful class, the classifier of `cls` itself
* @return The implied classidier, or NoSymbol is there is no LocalCap
* to be generated for the instance.
*/
def impliedClassifiers(cls: Symbol): List[ClassSymbol] = cls match
def impliedClassifier(cls: Symbol): Symbol = cls match
case cls: ClassSymbol =>
var fieldClassifiers = knownFields(cls).flatMap(classifiersOfLocalCapsInType)
val fieldClassifiers =
knownFields(cls).flatMap(classifiersOfLocalCapsInType)
val parentClassifiers =
cls.parentSyms.map(impliedClassifiers).filter(_.nonEmpty)
if fieldClassifiers.isEmpty && parentClassifiers.isEmpty
then Nil
else parentClassifiers.foldLeft(fieldClassifiers.distinct)(dominators)
case _ => Nil
cls.parentSyms.map(impliedClassifier).collect:
case cl: ClassSymbol => cl
val stateClassifiers =
if cls.typeRef.isStatefulType(varsOnly = true)
then cls.classifier :: Nil
else Nil
commonAncestor(fieldClassifiers ++ parentClassifiers ++ stateClassifiers)
case _ => NoSymbol

def contributingFields(cls: Symbol): List[Symbol] = cls match
case cls: ClassSymbol =>
Expand All @@ -606,20 +614,17 @@ extension (cls: ClassSymbol) {
def localCap(fields: List[Symbol]) =
LocalCap(Origin.NewInstance(core, fields))

var implied = impliedClassifiers(cls)
if cls.typeRef.isStatefulType(varsOnly = true) then
implied = dominators(cls.classifier :: Nil, implied)
val fields = contributingFields(cls)
val impliedSet = ccState.localCapClassifiersAndFieldsCache.getOrElseUpdate(cls, (implied, fields)) match
case (Nil, _) =>
val impliedClr = impliedClassifier(cls)
val contributing = contributingFields(cls)
val impliedSet = impliedClr match
case impliedClr: ClassSymbol =>
val result = localCap(contributing)
if impliedClr != defn.AnyClass then
result.hiddenSet.adoptClassifier(impliedClr)
maybeRO(result, contributing).singletonCaptureSet
case _ =>
CaptureSet.empty
case (cl :: Nil, fields) =>
val result = localCap(fields)
result.hiddenSet.adoptClassifier(cl)
maybeRO(result, fields).singletonCaptureSet
case (_, fields) =>
maybeRO(localCap(fields), fields).singletonCaptureSet
(impliedSet, fields)
(impliedSet, contributing)
}

def creationCapset(using Context)(core: Type = cls.appliedRef): CaptureSet =
Expand Down
58 changes: 40 additions & 18 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import NameOps.isReplWrapperName
import reporting.*
import reporting.Message.Note
import Annotations.Annotation
import Constants.Constant
import Capabilities.*
import Mutability.*
import util.common.alwaysTrue
Expand Down Expand Up @@ -197,11 +198,6 @@ object CheckCaptures:
check.traverse(tp)
}

private def contributesLocalCapToClass(sym: Symbol)(using Context): Boolean =
sym.isField
&& !sym.isOneOf(DeferredOrTermParamOrAccessor)
&& !sym.hasAnnotation(defn.UntrackedCapturesAnnot)

trait CheckerAPI:
/** Complete symbol info of a val or a def */
def completeDef(tree: ValOrDefDef, sym: Symbol, completer: LazyType)(using Context): Unit
Expand Down Expand Up @@ -991,14 +987,37 @@ class CheckCaptures extends Recheck, SymTransformer:
/** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`.
* This means
* - Instantiate result type with actual arguments
* - if `sym` is a constructor, refine its type with `refineInstanceType`
* - if `sym` is a constructor, refine its type with `refineConstructorInstance`
*/
override def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type =
val ownType =
if !mt.isResultDependent then mt.resType
else SubstParamsMap(mt, argTypes)(mt.resType)
def instCls = ownType.finalResultType.classSymbol.asClass
if sym.needsResultRefinement then
refineConstructorInstance(ownType, mt, argTypes, ownType.finalResultType.classSymbol.asClass)
refineConstructorInstance(ownType, mt, argTypes, instCls)
else if sym.isSecondaryConstructor then
// Refine primary constructor instance with a list of arguments of matching
// length that is constructed as follows:
// - If the argument is for a primary constructor parameter named `x`
// and there is a secondary constructor parameter carrying an
// annotation `@caps.internal.paramAlias("x")`, pick the actual argument
// in `argTypes` that corresponds to this secondary constructor parameter.
// We assume there can be at most one such secondary constructor parameter.
// - Otherwise the argument is NoType.
instCls.primaryConstructor.info.stripPoly match
case primaryMt: MethodType =>
var aliasMap = Map.empty[Name, Type]
for (param, argType) <- sym.paramSymss.flatten.filter(_.isTerm).lazyZip(argTypes) do
for
ann <- param.getAnnotation(defn.ParamAliasAnnot)
name <- ann.argumentConstantString(0)
do
aliasMap = aliasMap.updated(name.toTermName, argType)
val aliasedArgs = instCls.paramGetters.map: param =>
aliasMap.getOrElse(param.name, NoType)
refineConstructorInstance(ownType, primaryMt, aliasedArgs, instCls)
case _ => ownType
else ownType

/** Refine the type returned from a constructor as follows:
Expand Down Expand Up @@ -1028,16 +1047,19 @@ class CheckCaptures extends Recheck, SymTransformer:
for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do
val getter = cls.refiningGetterNamed(getterName)
if !getter.is(Private) && getter.hasTrackedParts then
if !getter.is(Tracked) then
refined = refined.refinedOverride(getterName, argType.unboxed)
// We can assume unboxed since the use set contributed by field selection is also the capture set
// So unboxing will not add anything to the use sets.
// This trick is also the principal reason why we can't make refineConstructorInstance
// an operation to work on the declared constructor types. We would miss the necessary unboxed that way.
if getter.hasAnnotation(defn.ConsumeAnnot) then
() // We make sure in checkClassDef, point (6), that consume parameters don't
// contribute to the class capture set
else allCaptures ++= argType.captureSet
if argType.exists then
if !getter.is(Tracked) then
refined = refined.refinedOverride(getterName, argType.unboxed)
// We can assume unboxed since the use set contributed by field selection is also the capture set
// So unboxing will not add anything to the use sets.
// This trick is also the principal reason why we can't make refineConstructorInstance
// an operation to work on the declared constructor types. We would miss the necessary unboxed that way.
if getter.hasAnnotation(defn.ConsumeAnnot) then
() // We make sure in checkClassDef, point (6), that consume parameters don't
// contribute to the class capture set
else allCaptures ++= argType.captureSet
else
allCaptures ++= cls.mapClassCaptures(core, getter.info.captureSet)
(refined, allCaptures)

/** Augment result type of constructor with refinements and captures.
Expand Down Expand Up @@ -1475,7 +1497,7 @@ class CheckCaptures extends Recheck, SymTransformer:
cls.isPackageObject && cls.enclosingPackageClass.isEmptyPackage
if sym.owner.isClass
&& !isToplevelDefsInEmptyPackage(sym.owner)
&& contributesLocalCapToClass(sym)
&& sym.contributesLocalCapsToClass
&& !CaptureSet.isAssumedPure(sym)
then
todoAtPostCheck += { () =>
Expand Down
33 changes: 33 additions & 0 deletions compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import CheckCaptures.CheckerAPI
import NamerOps.methodType
import NameOps.isSelectorName
import NameKinds.{CanThrowEvidenceName, TryOwnerName, DefaultGetterName}
import Constants.Constant
import Capabilities.*

/** Operations accessed from CheckCaptures */
Expand Down Expand Up @@ -77,6 +78,38 @@ object Setup:
case _ => false
case _ => None

/** Add `caps.internal.paramAlias annotation("x")` to secondoray constructor
* parameters that get forwarded in the constructor's super call to a primary
* constructor parameter named "x". Example:
*
* class A(x: B^, y: Int):
* def this(xx: B^) = this(xx, 0)
*
* Here we add `@caps.internal.paramAlias("x")` s annotation to parameter `xx`.
* The forward could also be indirect, that is the argument gets forwarded
* to a secondary constructor parameter that itself has a @paramAlias annotation.
* In that case the @paramAlias annotation is copied to the argument.
*/
def recordParamAliases(constr: Symbol, superCall: Apply)(using Context): Unit = {

def addParamAlias(param: Symbol, name: String) =
val ann = Annotation(defn.ParamAliasAnnot, Literal(Constant(name)), param.span)
param.addAnnotation(ann)
capt.println(i"added $ann to $param of $constr")

val target = superCall.fun.symbol
for case (param, arg: Ident) <- target.paramSymss.flatten.filter(_.isTerm).lazyZip(superCall.args) do
if arg.symbol.is(Param) && arg.symbol.owner == constr then
if target == constr.owner.primaryConstructor then
addParamAlias(arg.symbol, param.name.toString)
else
for
ann <- param.getAnnotation(defn.ParamAliasAnnot)
name <- ann.argumentConstantString(0)
do
addParamAlias(arg.symbol, name)
}

end Setup
import Setup.*

Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,7 @@ class Definitions {
@tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName")
@tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs")
@tu lazy val ReachCapabilityAnnot = requiredClass("scala.annotation.internal.reachCapability")
@tu lazy val ParamAliasAnnot: ClassSymbol = requiredClass("scala.caps.internal.paramAlias")
@tu lazy val InferredAnnot = requiredClass("scala.caps.internal.inferred")
@tu lazy val ReadOnlyCapabilityAnnot = requiredClass("scala.annotation.internal.readOnlyCapability")
@tu lazy val OnlyCapabilityAnnot = requiredClass("scala.annotation.internal.onlyCapability")
Expand Down
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,10 @@ object SymDenotations {
final def isPrimaryConstructor(using Context): Boolean =
isConstructor && owner.primaryConstructor == symbol

/** Does this symbol denote a secondary constructor for its enclosing class? */
def isSecondaryConstructor(using Context): Boolean =
isConstructor && owner.primaryConstructor != symbol

/** Does this symbol denote the static constructor of its enclosing class? */
final def isStaticConstructor(using Context): Boolean =
name.isStaticConstructorName
Expand Down
13 changes: 8 additions & 5 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import staging.StagingLevel
import reporting.*
import Nullables.*
import NullOpsDecorator.*
import cc.{CheckCaptures, isRetainsLike, derivesFromCapSet}
import cc.{Setup, CheckCaptures, isRetainsLike, derivesFromCapSet}
import config.MigrationVersion
import transform.CheckUnused.OriginalName

Expand Down Expand Up @@ -3171,7 +3171,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
val rhsToInline = PrepareInlineable.wrapRHS(ddef, tpt1, rhs1)
PrepareInlineable.registerInlineInfo(sym, rhsToInline)

if sym.isConstructor then
if sym.isConstructor then {
if sym.is(Inline) then
report.error("constructors cannot be `inline`", ddef)

Expand All @@ -3186,7 +3186,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
do
if defn.isContextFunctionType(param.tpt.tpe) then
report.error("case class element cannot be a context function", param.srcPos)
else
else {
for params <- paramss1; param <- params do
checkRefsLegal(param, sym.owner, (name, sym) => sym.is(TypeParam), "secondary constructor")

Expand All @@ -3199,14 +3199,17 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
&& tree.span.exists && !tree.span.isSynthetic
then
report.error("secondary constructor must call a preceding constructor", app.srcPos)
if Feature.ccEnabled then
Setup.recordParamAliases(sym, app)

case Block(call :: _, expr) =>
checkThisConstrCall(call)
checkThisConstrCall(expr)
case _ =>

checkThisConstrCall(rhs1)
end if
end if
}
}

if sym.is(Method) && sym.owner.denot.isRefinementClass then
for annot <- sym.paramSymss.flatten.filter(_.isTerm).flatMap(_.getAnnotation(defn.ImplicitNotFoundAnnot)) do
Expand Down
7 changes: 7 additions & 0 deletions library/src/scala/caps/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ object internal:
@deprecated
final class refineOverride extends annotation.StaticAnnotation

/** An internal annotation placed on a parameter of a secondary constructor
* that gets forwarded indirectly or directly to a parameter of the
* corresponding primary constructor.
* @param parmName the name of the primary constructor parameter
*/
final class paramAlias(paramName: String) extends annotation.StaticAnnotation

/** An erasedValue issued internally by the compiler. Unlike the
* user-accessible compiletime.erasedValue, this version is assumed
* to be a pure expression, hence capability safe. The compiler generates this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,9 @@ object LazyListIterable extends IterableFactory[LazyListIterable] {
@inline private def newLL[A](state: => LazyListIterable[A]^): LazyListIterable[A]^{state} = new LazyListIterable[A](() => state)

/** Creates a new LazyListIterable with evaluated `head` and `tail`. */
@inline private def eagerCons[A](hd: A, tl: LazyListIterable[A]^): LazyListIterable[A]^{tl} = new LazyListIterable[A](hd, tl)
private def eagerCons[A](hd: A, tl: LazyListIterable[A]^): LazyListIterable[A]^{tl} =
new LazyListIterable[A](hd, tl).asInstanceOf[LazyListIterable[A]^{tl}]
// SAFETY: cc gets confused by private the secondary constructor here

private val anyToMarker: Any -> Any = _ => Statics.pfMarker

Expand Down
8 changes: 4 additions & 4 deletions tests/neg-custom-args/captures/fresh-fields.check
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/fresh-fields.scala:33:13 ---------------------------------
33 | val _: F = f // error
| ^
| Found: (f : F^{any})
| Required: F
|Found: (f : F^{any})
|Required: F
|
| Note that capability `any` cannot flow into capture set {}.
|Note that capability `any` cannot flow into capture set {}.
|
| where: any is a root capability in the type of value f with contributing fields value b, value e
|where: any is a root capability classified as SharedCapability in the type of value f with contributing fields value b, value e
|
| longer explanation available when compiling with `-explain`
27 changes: 27 additions & 0 deletions tests/neg-custom-args/captures/rand.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/rand.scala:14:14 -----------------------------------------
14 |val _: Rand = rand // error
| ^^^^
| Found: Rand{val self: JavaRand^{rand*}}^{rand}
| Required: Rand
|
| Note that capability `rand` cannot flow into capture set {}.
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/rand.scala:17:14 -----------------------------------------
17 |val _: Rand = rand1 // error
| ^^^^^
| Found: (rand1 : Rand{val self: JavaRand^{jrand}}^{jrand})
| Required: Rand
|
| Note that capability `jrand` cannot flow into capture set {}.
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/rand.scala:20:14 -----------------------------------------
20 |val _: Rand = rand2 // error
| ^^^^^
| Found: (rand2 : Rand{val self: JavaRand^{jrand}}^{jrand})
| Required: Rand
|
| Note that capability `jrand` cannot flow into capture set {}.
|
| longer explanation available when compiling with `-explain`
Loading
Loading