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
90 changes: 48 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 @@ -1793,7 +1796,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
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
case tp: LazyRef if !printDebug =>
try toText(tp.ref)
catch case ex: Throwable => "..."
case sel: cc.PathSelectionProto =>
"?.{ " ~ toText(sel.select.symbol) ~ "}"
case AnySelectionProto =>
"a type that can be selected or applied"
case tp: SelectionProto =>
Expand Down
6 changes: 5 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -995,8 +995,12 @@ class Namer { typer: Typer =>
end if
}

/** Add an implicit Mutable flag to consume methods in Mutable classes. This
* turns the method into an update method.
*/
private def normalizeFlags(denot: SymDenotation)(using Context): Unit =
if denot.is(Method) && denot.hasAnnotation(defn.ConsumeAnnot) then
if denot.is(Method) && denot.hasAnnotation(defn.ConsumeAnnot)
&& denot.owner.derivesFrom(defn.Caps_Mutable) then
denot.setFlag(Mutable)

/** Intentionally left without `using Context` parameter. We need
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,28 @@ sealed trait Capability
trait SharedCapability extends Capability Classifier
trait Control extends SharedCapability, Classifier

trait ExclusiveCapability extends Capability, Classifier
trait Mutable extends ExclusiveCapability, Classifier
trait ExclusiveCapability extends Capability
trait Read extends ExclusiveCapability, Classifier
```
Here is a graph showing the hierarchy of predefined classifier traits:
Here is a graph showing the hierarchy of predefined capability traits. Classifier traits are underlined.
```
Capability
/ \
/ \
/ \
/ \
SharedCapability ExclusiveCapability
----------------
| |
| |
| |
| |
Control Mutable
Control Read
------- ----
```
At the top of the hierarchy, we distinguish between _shared_ and _exclusive_ capabilities in two classifier traits `SharedCapability` and `ExclusiveCapability`. All capability classes we have seen so far are shared.
`ExclusiveCapability` is a base trait for capabilities that allow only un-aliased access to the data they represent, with the rules governed by [separation checking](separation-checking.md). Separation checking is currently an optional extension of capture checking, enabled by a different language import. Since `Capability` is a sealed trait, all capability classes are either shared or exclusive.
At the top of the hierarchy, we distinguish between _shared_ and _exclusive_ capabilities in two traits `SharedCapability` and `ExclusiveCapability`. All capability classes we have seen so far are shared.
`ExclusiveCapability` is a base trait for capabilities that
are checked for anti-aliasing restrictions with the rules governed by [separation checking](separation-checking.md). Separation checking is currently an optional extension of capture checking, enabled by a different language import. Since `Capability` is a sealed trait, all capability classes are either shared or exclusive.

`Control` capabilities are shared. This means they cannot directly or indirectly capture exclusive capabilities such as capabilities that control access to mutable state. Typical `Control` capabilities are:

Expand Down
Loading
Loading