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
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
93 changes: 51 additions & 42 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 @@ -705,29 +705,23 @@ class CheckCaptures extends Recheck, SymTransformer:
* where `b` is a read-only method, we charge `x.a.b.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
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/cc/SepCheck.scala
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
val badParams = mutable.ListBuffer[Symbol]()
def currentOwner = role.dclSym.orElse(ctx.owner)
for hiddenRef <- refsToCheck.deduct(explicitRefs(tpe)) do
if !hiddenRef.isKnownClassifiedAs(defn.Caps_SharedCapability) then
if !hiddenRef.stripReadOnly.isKnownClassifiedAs(defn.Caps_SharedCapability) then
hiddenRef.pathRoot match
case ref: TermRef if ref.symbol != role.dclSym =>
val refSym = ref.symbol
Expand Down Expand Up @@ -675,7 +675,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
role match
case _: TypeRole.Argument | _: TypeRole.Qualifier =>
for ref <- refsToCheck do
if !ref.isKnownClassifiedAs(defn.Caps_SharedCapability) then
if !ref.stripReadOnly.isKnownClassifiedAs(defn.Caps_SharedCapability) then
consumed.put(ref, pos)
case _ =>
end checkConsumedRefs
Expand Down
Loading
Loading