Skip to content
14 changes: 11 additions & 3 deletions compiler/src/dotty/tools/dotc/cc/Capability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ object Capabilities:
def acceptsLevelOf(ref: Capability)(using Context): Boolean =
if ccConfig.useFreshLevels && !CCState.collapseFresh then
val refOwner = ref.levelOwner
refOwner.isStaticOwner || ccOwner.isContainedIn(refOwner)
ccOwner.isContainedIn(refOwner)
|| classifier.derivesFrom(defn.Caps_Unscoped)
else ref.core match
case ResultCap(_) | _: ParamRef => false
case _ => true
Expand Down Expand Up @@ -432,11 +433,18 @@ object Capabilities:
core.isInstanceOf[RootCapability]

/** Is the reference tracked? This is true if it can be tracked and the capture
* set of the underlying type is not always empty.
* set of the underlying type is not always empty. Also excluded are references
* that come from source files that were not capture checked and that have
* `Fluid` capture sets.
*/
final def isTracked(using Context): Boolean = this.core match
case _: RootCapability => true
case tp: CoreCapability => tp.isTrackableRef && !captureSetOfInfo.isAlwaysEmpty
case tp: CoreCapability =>
tp.isTrackableRef
&& {
val cs = captureSetOfInfo
!cs.isAlwaysEmpty && cs != CaptureSet.Fluid
}

/** An exclusive capability is a capability that derives
* indirectly from a maximal capability without going through
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ extension (tp: Type)
!tp.underlying.exists // might happen during construction of lambdas with annotations on parameters
||
((tp.prefix eq NoPrefix)
|| tp.symbol.isField && !tp.symbol.isStatic && tp.prefix.isTrackableRef
|| tp.symbol.isField && tp.prefix.isTrackableRef
) && !tp.symbol.isOneOf(UnstableValueFlags)
case tp: TypeRef =>
tp.symbol.isType && tp.derivesFrom(defn.Caps_CapSet)
Expand Down
14 changes: 13 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/CaptureSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import compiletime.uninitialized
import Capabilities.*
import Names.Name
import NameKinds.CapsetName
import StdNames.nme

/** A class for capture sets. Capture sets can be constants or variables.
* Capture sets support inclusion constraints <:< where <:< is subcapturing.
Expand Down Expand Up @@ -1678,6 +1679,8 @@ object CaptureSet:
// might happen during construction of lambdas, assume `{cap}` in this case so that
// `ref` will not seem subsumed by other capabilities in a `++`.
universal
case c: TermRef if isAssumedPure(c.symbol) =>
CaptureSet.empty
case c: CoreCapability =>
ofType(c.underlying, followResult = ccConfig.useSpanCapset)

Expand Down Expand Up @@ -1723,7 +1726,7 @@ object CaptureSet:
/** The deep capture set of a type is the union of all covariant occurrences of
* capture sets. Nested existential sets are approximated with `cap`.
*/
def ofTypeDeeply(tp: Type, includeTypevars: Boolean = false, includeBoxed: Boolean = true)(using Context): CaptureSet =
def ofTypeDeeply(tp: Type, includeTypevars: Boolean = false, includeBoxed: Boolean = true)(using Context): CaptureSet = {
val collect = new DeepTypeAccumulator[CaptureSet]:

def capturingCase(acc: CaptureSet, parent: Type, refs: CaptureSet, boxed: Boolean) =
Expand All @@ -1736,6 +1739,15 @@ object CaptureSet:
else this(acc, upperBound)

collect(CaptureSet.empty, tp)
}

/** Is `sym` assumed to be pure even though it would have a non-empty capture
* set by the normal rules?
*/
def isAssumedPure(sym: Symbol)(using Context): Boolean =
sym.name == nme.DOLLAR_VALUES // sym is an enum $values array, which for backwards
&& sym.owner.is(Module) // compatible reasons is an array, but should really be an IArray.
&& sym.owner.companionClass.is(Enum)

type AssumedContains = immutable.Map[TypeRef, SimpleIdentitySet[Capability]]
val AssumedContains: Property.Key[AssumedContains] = Property.Key()
Expand Down
77 changes: 32 additions & 45 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ object CheckCaptures:
* @param nestedClosure under deferredReaches: If this is an env of a method with an anonymous function or
* anonymous class as RHS, the symbol of that function or class. NoSymbol in all other cases.
*/
case class Env(
owner: Symbol,
kind: EnvKind,
captured: CaptureSet,
outer0: Env | Null,
nestedClosure: Symbol = NoSymbol)(using @constructorOnly ictx: Context):
class Env(
val owner: Symbol,
val kind: EnvKind,
val captured: CaptureSet,
outer0: Env | Null,
val nestedClosure: Symbol = NoSymbol)(using @constructorOnly ictx: Context) {

assert(definesEnv(owner))
captured match
Expand All @@ -71,16 +71,16 @@ object CheckCaptures:

def outer = outer0.nn

def isOutermost = outer0 == null
def isRoot(using Context) = owner.is(Package)

def outersIterator: Iterator[Env] = new:
def outersIterator(using Context): Iterator[Env] = new:
private var cur = Env.this
def hasNext = !cur.isOutermost
def hasNext = !cur.isRoot
def next(): Env =
val res = cur
cur = cur.outer
res
end Env
}

def definesEnv(sym: Symbol)(using Context): Boolean =
sym.isOneOf(MethodOrLazy) || sym.isClass
Expand Down Expand Up @@ -389,6 +389,9 @@ class CheckCaptures extends Recheck, SymTransformer:

/** If `tpt` is an inferred type, interpolate capture set variables appearing contra-
* variantly in it. Also anchor Fresh instances with anchorCaps.
* Note: module vals don't have inferred types but still hold capture set variables.
* These capture set variables are interpolated after the associated module class
* has been rechecked.
*/
private def interpolateIfInferred(tpt: Tree, sym: Symbol)(using Context): Unit =
if tpt.isInstanceOf[InferredTypeTree] then
Expand Down Expand Up @@ -465,18 +468,17 @@ class CheckCaptures extends Recheck, SymTransformer:
catch case ex: IllegalCaptureRef =>
report.error(em"Illegal capture reference: ${ex.getMessage}", sym.srcPos)
CaptureSet.empty
case _ =>
if sym.isTerm || !sym.owner.isStaticOwner || sym.is(Lazy)
then CaptureSet.Var(sym, nestedOK = false)
else CaptureSet.empty)
case _ if sym.is(Package) => CaptureSet.empty
case _ => CaptureSet.Var(sym, nestedOK = false)
)

// ---- Record Uses with MarkFree ----------------------------------------------------

/** The next environment enclosing `env` that needs to be charged
* with free references.
*/
def nextEnvToCharge(env: Env)(using Context): Env | Null =
if env.owner.isConstructor then env.outer.outer0
def nextEnvToCharge(env: Env)(using Context): Env =
if env.owner.isConstructor then env.outer.outer
else env.outer

/** A description where this environment comes from */
Expand All @@ -495,9 +497,8 @@ class CheckCaptures extends Recheck, SymTransformer:
* Does the given environment belong to a method that is (a) nested in a term
* and (b) not the method of an anonymous function?
*/
def isOfNestedMethod(env: Env | Null)(using Context) =
def isOfNestedMethod(env: Env)(using Context) =
ccConfig.deferredReaches
&& env != null
&& env.owner.is(Method)
&& env.owner.owner.isTerm
&& !env.owner.isAnonymousFunction
Expand Down Expand Up @@ -587,7 +588,7 @@ class CheckCaptures extends Recheck, SymTransformer:
tree.srcPos)

def recur(cs: CaptureSet, env: Env, lastEnv: Env | Null): Unit =
if env.kind != EnvKind.Boxed && !env.owner.isStaticOwner && !cs.isAlwaysEmpty then
if env.kind != EnvKind.Boxed && !cs.isAlwaysEmpty then
// Only captured references that are visible from the environment
// should be included.
val included = cs.filter: c =>
Expand All @@ -602,7 +603,7 @@ class CheckCaptures extends Recheck, SymTransformer:
capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}")
if !isOfNestedMethod(env) then
val nextEnv = nextEnvToCharge(env)
if nextEnv != null && !nextEnv.owner.isStaticOwner then
if !nextEnv.isRoot then
if nextEnv.owner != env.owner
&& env.owner.isReadOnlyMember
&& env.owner.owner.derivesFrom(defn.Caps_Stateful)
Expand Down Expand Up @@ -731,7 +732,7 @@ class CheckCaptures extends Recheck, SymTransformer:
// For fields it's not a problem since `c` would already have been
// charged for the prefix `p` in `p.x`.
markFree(sym.info.captureSet, tree)
if sym.exists && !sym.isStatic then
if sym.exists then
markPathFree(sym.termRef, pt, tree)
mapResultRoots(super.recheckIdent(tree, pt), tree.symbol)

Expand Down Expand Up @@ -761,7 +762,7 @@ class CheckCaptures extends Recheck, SymTransformer:
* a PathSelectionProto.
*/
override def selectionProto(tree: Select, pt: Type)(using Context): Type =
if tree.symbol.isStatic then super.selectionProto(tree, pt)
if tree.symbol.is(Package) then super.selectionProto(tree, pt)
else PathSelectionProto(tree, pt)

/** A specialized implementation of the selection rule.
Expand Down Expand Up @@ -1261,22 +1262,6 @@ class CheckCaptures extends Recheck, SymTransformer:

if runInConstructor && savedEnv.owner.isClass then
markFree(declaredCaptures, tree, addUseInfo = false)

if sym.owner.isStaticOwner
&& !declaredCaptures.elems.isEmpty
&& sym != defn.captureRoot
&& !(sym.is(ModuleVal) && sym.owner.is(Package))
// global modules that don't derive from capability can have captures
// only if their fields have them, and then the field was already reported.
then
def where =
if sym.effectiveOwner.is(Package) then "top-level definition"
else i"member of static ${sym.owner}"
report.warning(
em"""$sym has a non-empty capture set but will not be added as
|a capability to computed capture sets since it is globally accessible
|as a $where. Global values cannot be capabilities.""",
tree.namePos)
end recheckValDef

/** Recheck method definitions:
Expand Down Expand Up @@ -1400,8 +1385,9 @@ class CheckCaptures extends Recheck, SymTransformer:
// does not interfere with normal rechecking by constraining capture set variables.
}
// Test point (2) of doc comment above
if sym.owner.isClass && !sym.owner.isStaticOwner
if sym.owner.isClass
&& contributesFreshToClass(sym)
&& !CaptureSet.isAssumedPure(sym)
then
todoAtPostCheck += { () =>
val cls = sym.owner.asClass
Expand Down Expand Up @@ -1474,7 +1460,7 @@ class CheckCaptures extends Recheck, SymTransformer:

def checkExplicitUses(sym: Symbol): Unit = capturedVars(sym) match
case cs: CaptureSet.Var
if !cs.elems.isEmpty && !isExemptFromExplicitChecks(sym) =>
if cs.elems.exists(!_.isTerminalCapability) && !isExemptFromExplicitChecks(sym) =>
val usesStr = if sym.isClass then "uses" else "uses_init"
report.error(
em"""Publicly visible $sym uses external capabilities $cs.
Expand Down Expand Up @@ -1518,6 +1504,8 @@ class CheckCaptures extends Recheck, SymTransformer:
finally
checkExplicitUses(cls)
checkExplicitUses(cls.primaryConstructor)
if cls.is(ModuleClass) then
interpolate(cls.sourceModule.info, cls.sourceModule)
completed += cls
curEnv = saved
end recheckClassDef
Expand Down Expand Up @@ -1838,11 +1826,11 @@ class CheckCaptures extends Recheck, SymTransformer:
private def debugShowEnvs()(using Context): Unit =
def showEnv(env: Env): String = i"Env(${env.owner}, ${env.kind}, ${env.captured})"
val sb = StringBuilder()
@annotation.tailrec def walk(env: Env | Null): Unit =
if env != null then
@annotation.tailrec def walk(env: Env): Unit =
if !env.isRoot then
sb ++= showEnv(env)
sb ++= "\n"
walk(env.outer0)
walk(env.outer)
sb ++= "===== Current Envs ======\n"
walk(curEnv)
sb ++= "===== End ======\n"
Expand Down Expand Up @@ -2240,10 +2228,9 @@ class CheckCaptures extends Recheck, SymTransformer:
def boxedOwner(env: Env): Symbol =
if env.kind == EnvKind.Boxed then env.owner
else if isOfNestedMethod(env) then env.owner.owner
else if env.owner.isStaticOwner then NoSymbol
else
val nextEnv = nextEnvToCharge(env)
if nextEnv == null then NoSymbol else boxedOwner(nextEnv)
if nextEnv.isRoot then NoSymbol else boxedOwner(nextEnv)

def checkUseUnlessBoxed(c: Capability, croot: NamedType) =
if !boxedOwner(env).isContainedIn(croot.symbol.owner) then
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/cc/Mutability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ object Mutability:
else if sym.owner == cls then
if sym.isConstructor then OK
else NotInUpdateMethod(sym, cls)
else if sym.isStatic then OutsideClass(cls)
else if sym.isRoot then OutsideClass(cls)
else sym.owner.inExclusivePartOf(cls)

extension (tp: Type)
Expand Down
17 changes: 12 additions & 5 deletions compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -723,28 +723,35 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo

// Compute new self type
def isInnerModule = cls.is(ModuleClass) && !cls.isStatic
val selfInfo1 =
if (selfInfo ne NoType) && !isInnerModule then
if (selfInfo ne NoType) && !cls.is(ModuleClass) then
// if selfInfo is explicitly given then use that one, except if
// self info applies to non-static modules, these still need to be inferred
// self info applies to a module class, these still need to be inferred
selfInfo
else if cls.isPureClass then
// is cls is known to be pure, nothing needs to be added to self type
selfInfo
else if !cls.isEffectivelySealed && !cls.baseClassHasExplicitNonUniversalSelfType then
// assume {cap} for completely unconstrained self types of publicly extensible classes
CapturingType(cinfo.selfType, CaptureSet.universal)
else
else {
// Infer the self type for the rest, which is all classes without explicit
// self types (to which we also add nested module classes), provided they are
// neither pure, nor are publicily extensible with an unconstrained self type.
val cs = CaptureSet.ProperVar(cls, CaptureSet.emptyRefs, nestedOK = false, isRefining = false)

if cls.derivesFrom(defn.Caps_Capability) then
// If cls is a capability class, we need to add a fresh capability to ensure
// we cannot treat the class as pure.
CaptureSet.fresh(cls, cls.thisType, Origin.InDecl(cls)).subCaptures(cs)
CapturingType(cinfo.selfType, cs)

// Add capture set variable `cs`, burying it under any refinements
// that might contain infos of opaque type aliases
def addCs(tp: Type): Type = tp match
case tp @ RefinedType(parent, _, _) => tp.derivedRefinedType(parent = addCs(parent))
case _ => CapturingType(tp, cs)
addCs(cinfo.selfType)
}

// Compute new parent types
val ps1 = inContext(ctx.withOwner(cls)):
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/SymDenotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1391,7 +1391,7 @@ object SymDenotations {
* containing object.
*/
def opaqueAlias(using Context): Type = {
def recur(tp: Type): Type = tp match {
def recur(tp: Type): Type = tp.stripAnnots match {
case RefinedType(parent, rname, TypeAlias(alias)) =>
if rname == name then alias.stripLazyRef else recur(parent)
case _ =>
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/SymUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ class SymUtils:
}

def isField(using Context): Boolean =
self.isTerm && !self.isOneOf(Method | PhantomSymbol | NonMember)
self.isTerm && !self.isOneOf(Method | PhantomSymbol | NonMember | Package)

def isEnumCase(using Context): Boolean =
self.isAllOf(EnumCase, butNot = JavaDefined)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2486,7 +2486,7 @@ object Types extends TypeUtils {
util.Stats.record("NamedType.computeDenot")

def finish(d: Denotation) = {
if (d.exists)
if d.exists then
// Avoid storing NoDenotations in the cache - we will not be able to recover from
// them. The situation might arise that a type has NoDenotation in some later
// phase but a defined denotation earlier (e.g. a TypeRef to an abstract type
Expand Down
2 changes: 1 addition & 1 deletion library/src/scala/collection/StringOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ object StringOps {
}

/** Avoid an allocation in [[collect]]. */
private val fallback: Any => Any = _ => fallback
private val fallback: Any -> Any = _ => fallback
}

/** Provides extension methods for strings.
Expand Down
2 changes: 1 addition & 1 deletion library/src/scala/collection/mutable/LongMap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ object LongMap {
private final val VacantBit = 0x4000_0000
private final val MissVacant = 0xC000_0000

private val exceptionDefault: Long => Nothing = (k: Long) => throw new NoSuchElementException(k.toString)
private val exceptionDefault: Long -> Nothing = (k: Long) => throw new NoSuchElementException(k.toString)

/** A builder for instances of `LongMap`.
*
Expand Down
2 changes: 1 addition & 1 deletion tests/neg-custom-args/captures/boundary.check
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
| Capability cap outlives its scope: it leaks into outer capture set 's1 which is owned by value local.
| The leakage occurred when trying to match the following types:
|
| Found: scala.util.boundary.Label[Object^'s1]
| Found: scala.util.boundary.Label[Object^'s1]^'s2
| Required: scala.util.boundary.Label[Object^]^²
|
| where: ^ and cap refer to the universal root capability
Expand Down
13 changes: 13 additions & 0 deletions tests/neg-custom-args/captures/box-unsoundness-global.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class CanIO { def use(): Unit = () }
def use[X](x: X): (op: X -> Unit) -> Unit = op => op(x)
val io: CanIO^ = CanIO()
def test: Unit =
val f: (CanIO^ => Unit) -> Unit = use[CanIO^](io) // error
val _: (CanIO^ => Unit) -> Unit = f

val g1 = () => f(x => x.use())

val a1 = f(x => x.use())
val a2 = () => f(x => x.use())
val g2: () -> Unit = a2
// was UNSOUND: g uses the capability io but has an empty capture set
Loading
Loading