Skip to content
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/cc/Capability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,12 @@ object Capabilities:

/** An exclusive capability is a capability that derives
* indirectly from a maximal capability without going through
* a read-only capability first.
* a read-only capability or a capability classified as SharedCapability first.
*/
final def isExclusive(using Context): Boolean =
!isReadOnly && (isTerminalCapability || captureSetOfInfo.isExclusive)
!isReadOnly
&& !classifier.derivesFrom(defn.Caps_SharedCapability)
&& (isTerminalCapability || captureSetOfInfo.isExclusive)

/** Similar to isExlusive, but also includes capabilties with capture
* set variables in their info whose status is still open.
Expand Down
7 changes: 6 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import CaptureSet.VarState
import Capabilities.*
import StdNames.nme
import config.Feature
import dotty.tools.dotc.core.NameKinds.TryOwnerName
import NameKinds.TryOwnerName
import typer.ProtoTypes.WildcardSelectionProto

/** Attachment key for capturing type trees */
private val Captures: Key[CaptureSet] = Key()
Expand Down Expand Up @@ -639,6 +640,10 @@ extension (tp: AnnotatedType)
case ann: CaptureAnnotation => ann.boxed
case _ => false

/** A prototype that indicates selection */
class PathSelectionProto(val select: Select, val pt: Type) extends typer.ProtoTypes.WildcardSelectionProto:
def selector(using Context): Symbol = select.symbol

/** Drop retains annotations in the inferred type if CC is not enabled
* or transform them into RetainingTypes if CC is enabled.
*/
Expand Down
24 changes: 17 additions & 7 deletions compiler/src/dotty/tools/dotc/cc/CaptureSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ sealed abstract class CaptureSet extends Showable:
* - take mutability from the set's sources (for DerivedVars)
* - compute mutability on demand based on mutability of elements (for Consts)
*/
def associateWithMutable()(using Context): Unit
def associateWithMutable()(using Context): CaptureSet

/** Is this capture set constant (i.e. not an unsolved capture variable)?
* Solved capture variables count as constant.
Expand Down Expand Up @@ -297,7 +297,7 @@ sealed abstract class CaptureSet extends Showable:
/** The subcapturing test, using a given VarState */
final def subCaptures(that: CaptureSet)(using ctx: Context, vs: VarState = VarState()): Boolean =
TypeComparer.inNestedLevel:
val this1 = this.adaptMutability(that)
val this1 = if vs.isOpen then this.adaptMutability(that) else this
if this1 == null then false
else if this1 ne this then
capt.println(i"WIDEN ro $this with ${this.mutability} <:< $that with ${that.mutability} to $this1")
Expand Down Expand Up @@ -566,9 +566,16 @@ object CaptureSet:
val emptyRefs: Refs = SimpleIdentitySet.empty

/** The empty capture set `{}` */
@sharable // sharable since the set is empty, so setMutable is a no-op
@sharable // sharable since the set is empty, so mutability won't be set
val empty: CaptureSet.Const = Const(emptyRefs)

/** The empty capture set `{}` of a Mutable type, with Reader status */
@sharable // sharable since the set is empty, so mutability won't be set
val emptyOfMutable: CaptureSet.Const =
val cs = Const(emptyRefs)
cs.mutability = Mutability.Reader
cs

/** The universal capture set `{cap}` */
def universal(using Context): Const =
Const(SimpleIdentitySet(GlobalCap))
Expand Down Expand Up @@ -623,9 +630,11 @@ object CaptureSet:

private var isComplete = true

def associateWithMutable()(using Context): Unit =
if !elems.isEmpty then
def associateWithMutable()(using Context): CaptureSet =
if elems.isEmpty then emptyOfMutable
else
isComplete = false // delay computation of Mutability status
this

override def mutability(using Context): Mutability =
if !isComplete then
Expand Down Expand Up @@ -718,8 +727,9 @@ object CaptureSet:
*/
var deps: Deps = SimpleIdentitySet.empty

def associateWithMutable()(using Context): Unit =
def associateWithMutable()(using Context): CaptureSet =
mutability = Mutable
this

def isConst(using Context) = solved >= ccState.iterationId
def isAlwaysEmpty(using Context) = isConst && elems.isEmpty
Expand Down Expand Up @@ -1036,7 +1046,7 @@ object CaptureSet:
addAsDependentTo(source)

/** Mutability is same as in source, except for readOnly */
override def associateWithMutable()(using Context): Unit = ()
override def associateWithMutable()(using Context): CaptureSet = this

override def mutableToReader(origin: CaptureSet)(using Context): Boolean =
super.mutableToReader(origin)
Expand Down
10 changes: 6 additions & 4 deletions compiler/src/dotty/tools/dotc/cc/CapturingType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ object CapturingType:
*/
def apply(parent: Type, refs: CaptureSet, boxed: Boolean = false)(using Context): Type =
assert(!boxed || !parent.derivesFrom(defn.Caps_CapSet))
if refs.isAlwaysEmpty && !refs.keepAlways then parent
if refs.isAlwaysEmpty && !refs.keepAlways && !parent.derivesFromCapability then
parent
else parent match
case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed =>
apply(parent1, refs ++ refs1, boxed)
case _ =>
if parent.derivesFromMutable then refs.associateWithMutable()
refs.adoptClassifier(parent.inheritedClassifier)
AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot))
val refs1 =
if parent.derivesFromMutable then refs.associateWithMutable() else refs
refs1.adoptClassifier(parent.inheritedClassifier)
AnnotatedType(parent, CaptureAnnotation(refs1, boxed)(defn.RetainsAnnot))

/** An extractor for CapturingTypes. Capturing types are recognized if
* - the annotation is a CaptureAnnotation and we are not past CheckCapturingPhase, or
Expand Down
95 changes: 52 additions & 43 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,6 @@ object CheckCaptures:
override def toString = "SubstParamsMap"
end SubstParamsMap

/** A prototype that indicates selection with an immutable value */
class PathSelectionProto(val select: Select, val pt: Type)(using Context) extends WildcardSelectionProto

/** Check that a @retains annotation only mentions references that can be tracked.
* This check is performed at Typer.
*/
Expand Down Expand Up @@ -573,12 +570,12 @@ class CheckCaptures extends Recheck, SymTransformer:
// fresh capabilities. We do check that they hide no parameter reach caps in checkEscapingUses
case _ =>

def checkReadOnlyMethod(included: CaptureSet, env: Env): Unit =
def checkReadOnlyMethod(included: CaptureSet, meth: Symbol): Unit =
included.checkAddedElems: elem =>
if elem.isExclusive then
report.error(
em"""Read-only ${env.owner} accesses exclusive capability $elem;
|${env.owner} should be declared an update method to allow this.""",
em"""Read-only $meth accesses exclusive capability $elem;
|$meth should be declared an update method to allow this.""",
tree.srcPos)

def recur(cs: CaptureSet, env: Env, lastEnv: Env | Null): Unit =
Expand All @@ -598,8 +595,11 @@ class CheckCaptures extends Recheck, SymTransformer:
if !isOfNestedMethod(env) then
val nextEnv = nextEnvToCharge(env)
if nextEnv != null && !nextEnv.owner.isStaticOwner then
if env.owner.isReadOnlyMethodOrLazyVal && nextEnv.owner != env.owner then
checkReadOnlyMethod(included, env)
if nextEnv.owner != env.owner
&& env.owner.isReadOnlyMember
&& env.owner.owner.derivesFrom(defn.Caps_Mutable)
then
checkReadOnlyMethod(included, env.owner)
recur(included, nextEnv, env)
// Under deferredReaches, don't propagate out of methods inside terms.
// The use set of these methods will be charged when that method is called.
Expand Down Expand Up @@ -702,32 +702,26 @@ class CheckCaptures extends Recheck, SymTransformer:
* type `pt` to `ref`. Expand the marked tree accordingly to take account of
* the added path. Example:
* If we have `x` and the expected type says we select that with `.a.b`
* where `b` is a read-only method, we charge `x.a.b.rd` for tree `x.a.b`
* where `b` is a read-only method, we charge `x.a.rd` for tree `x.a.b`
* instead of just charging `x`.
*/
private def markPathFree(ref: TermRef | ThisType, pt: Type, tree: Tree)(using Context): Unit =
pt match
case pt: PathSelectionProto if ref.isTracked =>
// if `ref` is not tracked then the selection could not give anything new
// class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters.
if pt.select.symbol.isReadOnlyMethodOrLazyVal then
markFree(ref.readOnly, tree)
else
val sel = ref.select(pt.select.symbol).asInstanceOf[TermRef]
markPathFree(sel, pt.pt, pt.select)
case _ =>
markFree(ref.adjustReadOnly(pt), tree)
private def markPathFree(ref: TermRef | ThisType, pt: Type, tree: Tree)(using Context): Unit = pt match
case pt: PathSelectionProto
if ref.isTracked && !pt.selector.isOneOf(MethodOrLazyOrMutable) =>
// if `ref` is not tracked then the selection could not give anything new
// class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters.
val sel = ref.select(pt.selector).asInstanceOf[TermRef]
markPathFree(sel, pt.pt, pt.select)
case _ =>
markFree(ref.adjustReadOnly(pt), tree)

/** The expected type for the qualifier of a selection. If the selection
* could be part of a capability path or is a a read-only method, we return
* a PathSelectionProto.
*/
override def selectionProto(tree: Select, pt: Type)(using Context): Type =
val sym = tree.symbol
if !sym.isOneOf(MethodOrLazyOrMutable) && !sym.isStatic
|| sym.isReadOnlyMethodOrLazyVal
then PathSelectionProto(tree, pt)
else super.selectionProto(tree, pt)
if tree.symbol.isStatic then super.selectionProto(tree, pt)
else PathSelectionProto(tree, pt)

/** A specialized implementation of the selection rule.
*
Expand Down Expand Up @@ -1039,7 +1033,7 @@ class CheckCaptures extends Recheck, SymTransformer:
recheck(tree.rhs, lhsType.widen)
lhsType match
case lhsType @ TermRef(qualType, _)
if (qualType ne NoPrefix) && !lhsType.symbol.is(Transparent) =>
if (qualType ne NoPrefix) && !lhsType.symbol.hasAnnotation(defn.UntrackedCapturesAnnot) =>
checkUpdate(qualType, tree.srcPos)(i"Cannot assign to field ${lhsType.name} of ${qualType.showRef}")
case _ =>
defn.UnitType
Expand Down Expand Up @@ -1131,21 +1125,30 @@ class CheckCaptures extends Recheck, SymTransformer:
try
if sym.is(Module) then sym.info // Modules are checked by checking the module class
else
if sym.is(Mutable) && !sym.hasAnnotation(defn.UncheckedCapturesAnnot) then
val addendum = setup.capturedBy.get(sym) match
case Some(encl) =>
val enclStr =
if encl.isAnonymousFunction then
val location = setup.anonFunCallee.get(encl) match
case Some(meth) if meth.exists => i" argument in a call to $meth"
case _ => ""
s"an anonymous function$location"
else encl.show
i"\n\nNote that $sym does not count as local since it is captured by $enclStr"
case _ =>
""
disallowBadRootsIn(
tree.tpt.nuType, NoSymbol, i"Mutable $sym", "have type", addendum, sym.srcPos)
if sym.is(Mutable) then
if !sym.hasAnnotation(defn.UncheckedCapturesAnnot) then
val addendum = setup.capturedBy.get(sym) match
case Some(encl) =>
val enclStr =
if encl.isAnonymousFunction then
val location = setup.anonFunCallee.get(encl) match
case Some(meth) if meth.exists => i" argument in a call to $meth"
case _ => ""
s"an anonymous function$location"
else encl.show
i"\n\nNote that $sym does not count as local since it is captured by $enclStr"
case _ =>
""
disallowBadRootsIn(
tree.tpt.nuType, NoSymbol, i"Mutable $sym", "have type", addendum, sym.srcPos)
if sepChecksEnabled && false
&& sym.owner.isClass
&& !sym.owner.derivesFrom(defn.Caps_Mutable)
&& !sym.hasAnnotation(defn.UntrackedCapturesAnnot) then
report.error(
em"""Mutable $sym is defined in a class that does not extend `Mutable`.
|The variable needs to be annotated with `untrackedCaptures` to allow this.""",
tree.namePos)

// Lazy vals need their own environment to track captures from their RHS,
// similar to how methods work
Expand Down Expand Up @@ -1481,6 +1484,9 @@ class CheckCaptures extends Recheck, SymTransformer:
else
trace.force(i"rechecking $tree with pt = $pt", recheckr, show = true):
super.recheck(tree, pt)
catch case ex: AssertionError =>
println(i"error while rechecking $tree against $pt")
throw ex
finally curEnv = saved
if tree.isTerm && !pt.isBoxedCapturing && pt != LhsProto then
markFree(res.boxedCaptureSet, tree)
Expand Down Expand Up @@ -1793,7 +1799,10 @@ class CheckCaptures extends Recheck, SymTransformer:

if needsAdaptation && !insertBox then // we are unboxing
val criticalSet = // the set with which we unbox
if covariant then captures // covariant: we box with captures of actual type plus captures leaked by inner adapation
if covariant then
if expected.expectsReadOnly && actual.derivesFromMutable
then captures.readOnly
else captures
else expected.captureSet // contravarant: we unbox with captures of epected type
//debugShowEnvs()
markFree(criticalSet, tree)
Expand Down
41 changes: 25 additions & 16 deletions compiler/src/dotty/tools/dotc/cc/Mutability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Capabilities.*
import util.SrcPos
import config.Printers.capt
import ast.tpd.Tree
import typer.ProtoTypes.LhsProto

/** Handling mutability and read-only access
*/
Expand Down Expand Up @@ -46,25 +47,28 @@ object Mutability:
end Exclusivity

extension (sym: Symbol)
/** An update method is either a method marked with `update` or
* a setter of a non-transparent var.
/** An update method is either a method marked with `update` or a setter
* of a field of a Mutable class that's not annotated with @uncheckedCaptures.
* `update` is implicit for `consume` methods of Mutable classes.
*/
def isUpdateMethod(using Context): Boolean =
sym.isAllOf(Mutable | Method)
&& (!sym.isSetter || sym.field.is(Transparent))
&& (if sym.isSetter then
sym.owner.derivesFrom(defn.Caps_Mutable)
&& !sym.field.hasAnnotation(defn.UntrackedCapturesAnnot)
else true
)

/** A read-only method is a real method (not an accessor) in a type extending
* Mutable that is not an update method. Included are also lazy vals in such types.
*/
def isReadOnlyMethodOrLazyVal(using Context): Boolean =
sym.isOneOf(MethodOrLazy, butNot = Mutable | Accessor)
&& sym.owner.derivesFrom(defn.Caps_Mutable)
/** A read-only member is a lazy val or a method that is not an update method. */
def isReadOnlyMember(using Context): Boolean =
sym.isOneOf(MethodOrLazy) && !sym.isUpdateMethod

private def inExclusivePartOf(cls: Symbol)(using Context): Exclusivity =
import Exclusivity.*
if sym == cls then OK // we are directly in `cls` or in one of its constructors
else if sym.isUpdateMethod then OK
else if sym.owner == cls then
if sym.isUpdateMethod || sym.isConstructor then OK
if sym.isConstructor then OK
else NotInUpdateMethod(sym, cls)
else if sym.isStatic then OutsideClass(cls)
else sym.owner.inExclusivePartOf(cls)
Expand All @@ -77,7 +81,7 @@ object Mutability:
tp.derivesFrom(defn.Caps_Mutable)
&& tp.membersBasedOnFlags(Mutable, EmptyFlags).exists: mbr =>
if mbr.symbol.is(Method) then mbr.symbol.isUpdateMethod
else !mbr.symbol.is(Transparent)
else !mbr.symbol.hasAnnotation(defn.UntrackedCapturesAnnot)

/** OK, except if `tp` extends `Mutable` but `tp`'s capture set is non-exclusive */
private def exclusivity(using Context): Exclusivity =
Expand All @@ -98,19 +102,25 @@ object Mutability:
case _ =>
tp.exclusivity

def expectsReadOnly(using Context): Boolean = tp match
case tp: PathSelectionProto =>
tp.selector.isReadOnlyMember || tp.selector.isMutableVar && tp.pt != LhsProto
case _ => tp.isValueType && !tp.isMutableType

extension (cs: CaptureSet)
private def exclusivity(tp: Type)(using Context): Exclusivity =
if cs.isExclusive then Exclusivity.OK else Exclusivity.ReadOnly(tp)

extension (ref: TermRef | ThisType)
/** Map `ref` to `ref.readOnly` if its type extends Mutble, and one of the
* following is true: it appears in a non-exclusive context, or the expected
* type is a value type that is not a mutable type.
* following is true:
* - it appears in a non-exclusive context,
* - the expected type is a value type that is not a mutable type,
* - the expected type is a read-only selection
*/
def adjustReadOnly(pt: Type)(using Context): Capability =
if ref.derivesFromMutable
&& (pt.isValueType && !pt.isMutableType
|| ref.exclusivityInContext != Exclusivity.OK)
&& (pt.expectsReadOnly || ref.exclusivityInContext != Exclusivity.OK)
then ref.readOnly
else ref

Expand Down Expand Up @@ -142,7 +152,6 @@ object Mutability:
&& expected.isValueType
&& (!expected.derivesFromMutable || expected.captureSet.isAlwaysReadOnly)
&& !expected.isSingleton
&& actual.isBoxedCapturing == expected.isBoxedCapturing
then refs.readOnly
else refs
actual.derivedCapturingType(parent1, refs1)
Expand Down
Loading
Loading