diff --git a/compiler/src/dotty/tools/dotc/cc/Capability.scala b/compiler/src/dotty/tools/dotc/cc/Capability.scala index f293ff5ddb8c..faaac6450758 100644 --- a/compiler/src/dotty/tools/dotc/cc/Capability.scala +++ b/compiler/src/dotty/tools/dotc/cc/Capability.scala @@ -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 */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 0d5d3b210a44..c2f3b8bc6dcf 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -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 => @@ -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 = diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 6e5c864a78d9..b07a18b57487 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -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 @@ -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 @@ -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: @@ -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. @@ -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 += { () => diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index ad96444917b1..f43ef15b66dd 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -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 */ @@ -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.* diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 0e40149e595b..a8aad5946135 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -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") diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 1aabb85f5919..70c8232b3b94 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -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 diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 64de9f12e44b..818b93461b78 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -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 @@ -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) @@ -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") @@ -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 diff --git a/library/src/scala/caps/package.scala b/library/src/scala/caps/package.scala index b16c2dcf6723..56fc4135b001 100644 --- a/library/src/scala/caps/package.scala +++ b/library/src/scala/caps/package.scala @@ -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 diff --git a/library/src/scala/collection/immutable/LazyListIterable.scala b/library/src/scala/collection/immutable/LazyListIterable.scala index bbedebf3c5d5..1dd30ebc309d 100644 --- a/library/src/scala/collection/immutable/LazyListIterable.scala +++ b/library/src/scala/collection/immutable/LazyListIterable.scala @@ -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 diff --git a/tests/neg-custom-args/captures/fresh-fields.check b/tests/neg-custom-args/captures/fresh-fields.check index 8c66f6ca3396..843576cafd66 100644 --- a/tests/neg-custom-args/captures/fresh-fields.check +++ b/tests/neg-custom-args/captures/fresh-fields.check @@ -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` diff --git a/tests/neg-custom-args/captures/rand.check b/tests/neg-custom-args/captures/rand.check new file mode 100644 index 000000000000..10df04ec2823 --- /dev/null +++ b/tests/neg-custom-args/captures/rand.check @@ -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` diff --git a/tests/neg-custom-args/captures/rand.scala b/tests/neg-custom-args/captures/rand.scala new file mode 100644 index 000000000000..2367c0a0952a --- /dev/null +++ b/tests/neg-custom-args/captures/rand.scala @@ -0,0 +1,23 @@ +import caps.internal.paramAlias + +class JavaRand() + +val jrand: JavaRand^ = JavaRand() + +class Rand(val self: JavaRand^): + def this() = this(jrand) + def this(s: JavaRand^, dummy: Int) = this(s) + def this(d1: Int, s: JavaRand^) = this(s, d1) + +val rand = Rand() +val _: Rand{val self: JavaRand^}^ = rand +val _: Rand = rand // error +val rand1 = Rand(jrand, 0) +val _: Rand{val self: JavaRand^{jrand}}^{jrand} = rand1 +val _: Rand = rand1 // error +val rand2 = Rand(0, jrand) +val _: Rand{val self: JavaRand^{jrand}}^{jrand} = rand2 +val _: Rand = rand2 // error +val rand3 = Rand(JavaRand(): JavaRand^) +val rand4 = Rand(jrand) + diff --git a/tests/neg-custom-args/captures/safemode-4.check b/tests/neg-custom-args/captures/safemode-4.check new file mode 100644 index 000000000000..c661b44e16e8 --- /dev/null +++ b/tests/neg-custom-args/captures/safemode-4.check @@ -0,0 +1,23 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/safemode-4.scala:9:24 ------------------------------------ +9 | val r: Random = Random() // error + | ^^^^^^^^ + | Found: scala.util.Random^{any} + | Required: scala.util.Random + | + | Note that capability `any` cannot flow into capture set {}. + | + | where: any is a root capability in the type of value self + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/safemode-4.scala:12:15 -------------------------------------------------------- +12 | Properties.clearProp("foo") // error + | ^^^^^^^^^^^^^^^^^^^^ + | Cannot refer to method clearProp in trait PropertiesTrait from safe code since it is tagged @rejectSafe +-- Error: tests/neg-custom-args/captures/safemode-4.scala:13:15 -------------------------------------------------------- +13 | Properties.setProp("foo", "invalid") // error + | ^^^^^^^^^^^^^^^^^^ + | Cannot refer to method setProp in trait PropertiesTrait from safe code since it is tagged @rejectSafe +-- Error: tests/neg-custom-args/captures/safemode-4.scala:15:13 -------------------------------------------------------- +15 | Properties.main(Array()) // error + | ^^^^^^^^^^^^^^^ + | Cannot refer to method main in trait PropertiesTrait from safe code since it is tagged @rejectSafe diff --git a/tests/neg-custom-args/captures/safemode-4.scala b/tests/neg-custom-args/captures/safemode-4.scala new file mode 100644 index 000000000000..ae1feb562cb1 --- /dev/null +++ b/tests/neg-custom-args/captures/safemode-4.scala @@ -0,0 +1,16 @@ +package test +import language.experimental.safe +import caps.unsafe.untrackedCaptures +import scala.annotation.unchecked.{uncheckedCaptures, uncheckedVariance} +import scala.util.{Random, Properties} + +object Test: + + val r: Random = Random() // error + + if Properties.propIsSet("foo") then + Properties.clearProp("foo") // error + Properties.setProp("foo", "invalid") // error + + Properties.main(Array()) // error +