Skip to content

Commit 16f86f0

Browse files
committed
Always charge deep capture set of function arguments to cv
Also: In Recheck-apply, use deep capture sets of arguments in computing the result alternative. Drop restrictions on leaking reach capabilities in markFree. Revise visibility criterion for paths. Together these changes now implement a classical capability system with reach capabilities. References that are used later after passing some arguments are already recorded in capture sets of earlier stages (exception: closure results).
1 parent 7693722 commit 16f86f0

33 files changed

+174
-383
lines changed

compiler/src/dotty/tools/dotc/cc/CaptureOps.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,11 @@ extension (tp: Type)
274274
case _ =>
275275
tp
276276

277+
/** The first element of this path type */
278+
final def pathRoot(using Context): Type = tp.dealias match
279+
case tp1: NamedType if tp1.symbol.owner.isClass => tp1.prefix.pathRoot
280+
case _ => tp
281+
277282
/** If this is a unboxed capturing type with nonempty capture set, its boxed version.
278283
* Or, if type is a TypeBounds of capturing types, the version where the bounds are boxed.
279284
* The identity for all other types.

compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala

Lines changed: 30 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -385,34 +385,8 @@ class CheckCaptures extends Recheck, SymTransformer:
385385
// Only captured references that are visible from the environment
386386
// should be included.
387387
val included = cs.filter: c =>
388-
c.stripReach match
389-
case ref: NamedType =>
390-
val refSym = ref.symbol
391-
val refOwner = refSym.owner
392-
val isVisible = isVisibleFromEnv(refOwner, env)
393-
if isVisible && !ref.isRootCapability then
394-
ref match
395-
case ref: TermRef if ref.prefix `ne` NoPrefix =>
396-
// If c is a path of a class defined outside the environment,
397-
// we check the capture set of its info.
398-
checkSubsetEnv(ref.captureSetOfInfo, env)
399-
case _ =>
400-
if !isVisible
401-
&& (c.isReach || ref.isType)
402-
&& (!ccConfig.useSealed || refSym.is(Param))
403-
&& refOwner == env.owner
404-
then
405-
if refSym.hasAnnotation(defn.UnboxAnnot) then
406-
capt.println(i"exempt: $ref in $refOwner")
407-
else
408-
// Reach capabilities that go out of scope have to be approximated
409-
// by their underlying capture set, which cannot be universal.
410-
// Reach capabilities of @unboxed parameters are exempted.
411-
val cs = CaptureSet.ofInfo(c)
412-
cs.disallowRootCapability: () =>
413-
report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos)
414-
checkSubset(cs, env.captured, pos, provenance(env))
415-
isVisible
388+
c.stripReach.pathRoot match
389+
case ref: NamedType => isVisibleFromEnv(ref.symbol.owner, env)
416390
case ref: ThisType => isVisibleFromEnv(ref.cls, env)
417391
case _ => false
418392
checkSubset(included, env.captured, pos, provenance(env))
@@ -424,48 +398,14 @@ class CheckCaptures extends Recheck, SymTransformer:
424398
end markFree
425399

426400
/** Include references captured by the called method in the current environment stack */
427-
def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit =
428-
if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos)
429-
430-
private val prefixCalls = util.EqHashSet[GenericApply]()
431-
private val unboxedArgs = util.EqHashSet[Tree]()
432-
433-
def handleCall(meth: Symbol, call: GenericApply, eval: () => Type)(using Context): Type =
434-
if prefixCalls.remove(call) then return eval()
435-
436-
val unboxedParamNames =
437-
meth.rawParamss.flatMap: params =>
438-
params.collect:
439-
case param if param.hasAnnotation(defn.UnboxAnnot) =>
440-
param.name
441-
.toSet
442-
443-
def markUnboxedArgs(call: GenericApply): Unit = call.fun.tpe.widen match
444-
case MethodType(pnames) =>
445-
for (pname, arg) <- pnames.lazyZip(call.args) do
446-
if unboxedParamNames.contains(pname) then
447-
unboxedArgs.add(arg)
448-
case _ =>
449-
450-
def markPrefixCalls(tree: Tree): Unit = tree match
451-
case tree: GenericApply =>
452-
prefixCalls.add(tree)
453-
markUnboxedArgs(tree)
454-
markPrefixCalls(tree.fun)
455-
case _ =>
456-
457-
markUnboxedArgs(call)
458-
markPrefixCalls(call.fun)
459-
val res = eval()
460-
includeCallCaptures(meth, call.srcPos)
461-
res
462-
end handleCall
401+
def includeCallCaptures(sym: Symbol, resType: Type, pos: SrcPos)(using Context): Unit = resType match
402+
case _: MethodOrPoly => // wait until method is fully applied
403+
case _ =>
404+
if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos)
463405

464406
override def recheckIdent(tree: Ident, pt: Type)(using Context): Type =
465407
if tree.symbol.is(Method) then
466-
if tree.symbol.info.isParameterless then
467-
// there won't be an apply; need to include call captures now
468-
includeCallCaptures(tree.symbol, tree.srcPos)
408+
includeCallCaptures(tree.symbol, tree.symbol.info, tree.srcPos)
469409
else if !tree.symbol.isStatic then
470410
//debugShowEnvs()
471411
def addSelects(ref: TermRef, pt: Type): TermRef = pt match
@@ -570,15 +510,16 @@ class CheckCaptures extends Recheck, SymTransformer:
570510
tp.derivedCapturingType(forceBox(parent), refs)
571511
mapArgUsing(forceBox)
572512
else
573-
handleCall(meth, tree, () => super.recheckApply(tree, pt))
513+
val res = super.recheckApply(tree, pt)
514+
includeCallCaptures(meth, res, tree.srcPos)
515+
res
574516
end recheckApply
575517

576518
protected override
577519
def recheckArg(arg: Tree, formal: Type)(using Context): Type =
578520
val argType = recheck(arg, formal)
579-
if unboxedArgs.contains(arg) then
580-
capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}")
581-
markFree(argType.deepCaptureSet, arg.srcPos)
521+
capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}")
522+
markFree(argType.deepCaptureSet, arg.srcPos)
582523
argType
583524

584525
/** A specialized implementation of the apply rule.
@@ -589,27 +530,18 @@ class CheckCaptures extends Recheck, SymTransformer:
589530
* ---------------------
590531
* E |- f(a): Tr^C
591532
*
592-
* If the function `f` does not have an `@unboxed` parameter, then
593-
* any unboxing it does would be charged to the environment of the function
594-
* so they have to appear in Cq. Since any capabilities of the result of the
595-
* application must already be present in the application, an upper
596-
* approximation of the result capture set is Cq \union Ca, where `Ca`
597-
* is the capture set of the argument.
598-
* If the function `f` does have an `@unboxed` parameter, then it could in addition
599-
* unbox reach capabilities over its formal parameter. Therefore, the approximation
600-
* would be `Cq \union dcs(Ca)` instead.
533+
* If the type of the function `f` does not mention any formal parameters
534+
* any capabilities of the result of the application must already be present in
535+
* the application. So an upper approximation of the result capture set is Cq \union Ca,
536+
* where `Ca` is the deep capture set of the argument.
601537
* If the approximation is known to subcapture the declared result Cr, we pick it for C
602-
* otherwise we pick Cr.
538+
* otherwise we pick Cr. ???
603539
*/
604540
protected override
605541
def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type =
606542
val appType = Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes))
607543
val qualCaptures = qualType.captureSet
608-
val argCaptures =
609-
for (arg, argType) <- tree.args.lazyZip(argTypes) yield
610-
if unboxedArgs.remove(arg) // need to ensure the remove happens, that's why argCaptures is computed even if not needed.
611-
then argType.deepCaptureSet
612-
else argType.captureSet
544+
val argCaptures = argTypes.map(_.deepCaptureSet)
613545
appType match
614546
case appType @ CapturingType(appType1, refs)
615547
if qualType.exists
@@ -704,8 +636,10 @@ class CheckCaptures extends Recheck, SymTransformer:
704636
i"Sealed type variable $pname", "be instantiated to",
705637
i"This is often caused by a local capability$where\nleaking as part of its result.",
706638
tree.srcPos)
707-
try handleCall(meth, tree, () => Existential.toCap(super.recheckTypeApply(tree, pt)))
708-
finally checkContains(tree)
639+
val res = Existential.toCap(super.recheckTypeApply(tree, pt))
640+
includeCallCaptures(meth, res, tree.srcPos)
641+
checkContains(tree)
642+
res
709643
end recheckTypeApply
710644

711645
/** Faced with a tree of form `caps.contansImpl[CS, r.type]`, check that `R` is a tracked
@@ -1156,12 +1090,7 @@ class CheckCaptures extends Recheck, SymTransformer:
11561090
(erefs /: erefs.elems): (erefs, eref) =>
11571091
eref match
11581092
case eref: ThisType if isPureContext(ctx.owner, eref.cls) =>
1159-
1160-
def pathRoot(aref: Type): Type = aref match
1161-
case aref: NamedType if aref.symbol.owner.isClass => pathRoot(aref.prefix)
1162-
case _ => aref
1163-
1164-
def isOuterRef(aref: Type): Boolean = pathRoot(aref) match
1093+
def isOuterRef(aref: Type): Boolean = aref.pathRoot match
11651094
case aref: NamedType => eref.cls.isProperlyContainedIn(aref.symbol.owner)
11661095
case aref: ThisType => eref.cls.isProperlyContainedIn(aref.cls)
11671096
case _ => false
@@ -1171,7 +1100,7 @@ class CheckCaptures extends Recheck, SymTransformer:
11711100
// Include implicitly added outer references in the capture set of the class of `eref`.
11721101
for outerRef <- outerRefs.elems do
11731102
if !erefs.elems.contains(outerRef)
1174-
&& !pathRoot(outerRef).isInstanceOf[ThisType]
1103+
&& !outerRef.pathRoot.isInstanceOf[ThisType]
11751104
// we don't need to add outer ThisTypes as these are anyway added as path
11761105
// prefixes at the use site. And this exemption is required since capture sets
11771106
// of non-local classes are always empty, so we can't add an outer this to them.
@@ -1328,6 +1257,12 @@ class CheckCaptures extends Recheck, SymTransformer:
13281257

13291258
/** If actual is a tracked CaptureRef `a` and widened is a capturing type T^C,
13301259
* improve `T^C` to `T^{a}`, following the VAR rule of CC.
1260+
* TODO: We probably should do this also for other top-level occurrences of captures
1261+
* E.g.
1262+
* class Foo { def a: C^{io}; val def: C^{async} }
1263+
* val foo: Foo^{io, async}
1264+
* Then
1265+
* foo: Foo { def a: C^{foo}; def b: C^{foo} }^{foo}
13311266
*/
13321267
private def improveCaptures(widened: Type, actual: Type)(using Context): Type = actual match
13331268
case ref: CaptureRef if ref.isTracked =>
@@ -1388,21 +1323,6 @@ class CheckCaptures extends Recheck, SymTransformer:
13881323
!setup.isPreCC(overriding) && !setup.isPreCC(overridden)
13891324

13901325
override def checkInheritedTraitParameters: Boolean = false
1391-
1392-
/** Check that overrides don't change the @unbox status of their parameters */
1393-
override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit =
1394-
for
1395-
(params1, params2) <- member.rawParamss.lazyZip(other.rawParamss)
1396-
(param1, param2) <- params1.lazyZip(params2)
1397-
do
1398-
if param1.hasAnnotation(defn.UnboxAnnot) != param2.hasAnnotation(defn.UnboxAnnot) then
1399-
report.error(
1400-
OverrideError(
1401-
i"has a parameter ${param1.name} with different @unbox status than the corresponding parameter in the overridden definition",
1402-
self, member, other, self.memberInfo(member), self.memberInfo(other)
1403-
),
1404-
if member.owner == clazz then member.srcPos else clazz.srcPos
1405-
)
14061326
end OverridingPairsCheckerCC
14071327

14081328
def traverse(t: Tree)(using Context) =

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1057,7 +1057,6 @@ class Definitions {
10571057
@tu lazy val ExperimentalAnnot: ClassSymbol = requiredClass("scala.annotation.experimental")
10581058
@tu lazy val ThrowsAnnot: ClassSymbol = requiredClass("scala.throws")
10591059
@tu lazy val TransientAnnot: ClassSymbol = requiredClass("scala.transient")
1060-
@tu lazy val UnboxAnnot: ClassSymbol = requiredClass("scala.caps.unbox")
10611060
@tu lazy val UncheckedAnnot: ClassSymbol = requiredClass("scala.unchecked")
10621061
@tu lazy val UncheckedStableAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedStable")
10631062
@tu lazy val UncheckedVarianceAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedVariance")

compiler/src/dotty/tools/dotc/core/Types.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1796,7 +1796,7 @@ object Types extends TypeUtils {
17961796

17971797
/** Is this either not a method at all, or a parameterless method? */
17981798
final def isParameterless(using Context): Boolean = stripPoly match {
1799-
case mt: MethodType => false
1799+
case mt: MethodOrPoly => false
18001800
case _ => true
18011801
}
18021802

scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz
384384
* @param suffix The collection that gets appended to this lazy list
385385
* @return The lazy list containing elements of this lazy list and the iterable object.
386386
*/
387-
def lazyAppendedAll[B >: A](suffix: => collection.IterableOnce[B]^): LazyListIterable[B]^{this, suffix} =
387+
def lazyAppendedAll[B >: A](suffix: => collection.IterableOnce[B]^): LazyListIterable[B]^{this, suffix*} =
388388
newLL {
389389
if (isEmpty) suffix match {
390390
case lazyList: LazyListIterable[B] => lazyList.state // don't recompute the LazyListIterable
@@ -497,7 +497,7 @@ final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => Laz
497497
*
498498
* $preservesLaziness
499499
*/
500-
def prepended[B >: A](elem: B): LazyListIterable[B] = newLL(sCons(elem, this))
500+
def prepended[B >: A](elem: B): LazyListIterable[B]^{this} = newLL(sCons(elem, this))
501501

502502
/** @inheritdoc
503503
*
@@ -1137,7 +1137,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] {
11371137
/** Construct a LazyListIterable consisting of the concatenation of the given LazyListIterable and
11381138
* another LazyListIterable.
11391139
*/
1140-
def #:::[B >: A](prefix: LazyListIterable[B]^): LazyListIterable[B]^{prefix, l} = prefix lazyAppendedAll l
1140+
def #:::[B >: A](prefix: LazyListIterable[B]^): LazyListIterable[B]^{prefix, l*} = prefix lazyAppendedAll l
11411141

11421142
object #:: {
11431143
def unapply[A](s: LazyListIterable[A]^): Option[(A, LazyListIterable[A]^{s})] =
@@ -1155,7 +1155,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] {
11551155
/** Creates a State from an Iterator, with another State appended after the Iterator
11561156
* is empty.
11571157
*/
1158-
private def stateFromIteratorConcatSuffix[A](it: Iterator[A]^)(suffix: => State[A]^): State[A]^{it, suffix} =
1158+
private def stateFromIteratorConcatSuffix[A](it: Iterator[A]^)(suffix: => State[A]^): State[A]^{it, suffix*} =
11591159
if (it.hasNext) sCons(it.next(), newLL(stateFromIteratorConcatSuffix(it)(suffix)))
11601160
else suffix
11611161

tests/neg-custom-args/captures/cc-selftype-unsound.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ def magic(l: Logger^): Logger =
1111
Boxed[Logger^{this}](l) // error
1212
val x = new Foo
1313
val y = x.foo.unbox // y: Logger^{x}
14-
val z: Logger = y // now the capability becomes pure
14+
val z: Logger = y // error
1515
z

tests/neg-custom-args/captures/effect-swaps-explicit.check

Lines changed: 0 additions & 29 deletions
This file was deleted.

tests/neg-custom-args/captures/effect-swaps-explicit.scala

Lines changed: 0 additions & 76 deletions
This file was deleted.

0 commit comments

Comments
 (0)