Skip to content

Commit 1254e14

Browse files
authored
Add capture checking to some standard library classes (#18192)
Convert some standard library classes to capture checking. The converted classes are represented as tests. Initially, the `Iterator` and `IterableOnce` classes are converted. UPDATE: By now, we have quite a few more, including List, ListBuffer, View, and all their super classes and traits. Also change capture checker to facilitate the conversion. The main changes are about better interop with classes that are not (yet) capture checked. Based on #18131
2 parents 492f777 + 7a8fae7 commit 1254e14

File tree

93 files changed

+11576
-171
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+11576
-171
lines changed

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import config.Feature
1515
private val Captures: Key[CaptureSet] = Key()
1616
private val BoxedType: Key[BoxedTypeCache] = Key()
1717

18+
/** Switch whether unpickled function types and byname types should be mapped to
19+
* impure types. With the new gradual typing using Fluid capture sets, this should
20+
* be no longer needed. Also, it has bad interactions with pickling tests.
21+
*/
22+
private val adaptUnpickledFunctionTypes = false
23+
1824
/** The arguments of a @retains or @retainsByName annotation */
1925
private[cc] def retainedElems(tree: Tree)(using Context): List[Tree] = tree match
2026
case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems
@@ -49,7 +55,7 @@ extension (tree: Tree)
4955
* a by name parameter type, turning the latter into an impure by name parameter type.
5056
*/
5157
def adaptByNameArgUnderPureFuns(using Context): Tree =
52-
if Feature.pureFunsEnabledSomewhere then
58+
if adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere then
5359
val rbn = defn.RetainsByNameAnnot
5460
Annotated(tree,
5561
New(rbn.typeRef).select(rbn.primaryConstructor).appliedTo(
@@ -145,7 +151,7 @@ extension (tp: Type)
145151
*/
146152
def adaptFunctionTypeUnderPureFuns(using Context): Type = tp match
147153
case AppliedType(fn, args)
148-
if Feature.pureFunsEnabledSomewhere && defn.isFunctionClass(fn.typeSymbol) =>
154+
if adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere && defn.isFunctionClass(fn.typeSymbol) =>
149155
val fname = fn.typeSymbol.name
150156
defn.FunctionType(
151157
fname.functionArity,

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

+15-1
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,20 @@ object CaptureSet:
356356
override def toString = elems.toString
357357
end Const
358358

359+
/** A special capture set that gets added to the types of symbols that were not
360+
* themselves capture checked, in order to admit arbitrary corresponding capture
361+
* sets in subcapturing comparisons. Similar to platform types for explicit
362+
* nulls, this provides more lenient checking against compilation units that
363+
* were not yet compiled with capture checking on.
364+
*/
365+
object Fluid extends Const(emptySet):
366+
override def isAlwaysEmpty = false
367+
override def addNewElems(elems: Refs, origin: CaptureSet)(using Context, VarState) = CompareResult.OK
368+
override def accountsFor(x: CaptureRef)(using Context): Boolean = true
369+
override def mightAccountFor(x: CaptureRef)(using Context): Boolean = true
370+
override def toString = "<fluid>"
371+
end Fluid
372+
359373
/** The subclass of captureset variables with given initial elements */
360374
class Var(initialElems: Refs = emptySet) extends CaptureSet:
361375

@@ -863,7 +877,7 @@ object CaptureSet:
863877
case CapturingType(parent, refs) =>
864878
recur(parent) ++ refs
865879
case tpd @ RefinedType(parent, _, rinfo: MethodType)
866-
if followResult && defn.isFunctionType(tpd) =>
880+
if followResult && defn.isFunctionNType(tpd) =>
867881
ofType(parent, followResult = false) // pick up capture set from parent type
868882
++ (recur(rinfo.resType) // add capture set of result
869883
-- CaptureSet(rinfo.paramRefs.filter(_.isTracked)*)) // but disregard bound parameters

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

+54-7
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,46 @@ class CheckCaptures extends Recheck, SymTransformer:
290290
def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit =
291291
if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos)
292292

293+
private def handleBackwardsCompat(tp: Type, sym: Symbol, initialVariance: Int = 1)(using Context): Type =
294+
val fluidify = new TypeMap with IdempotentCaptRefMap:
295+
variance = initialVariance
296+
def apply(t: Type): Type = t match
297+
case t: MethodType =>
298+
mapOver(t)
299+
case t: TypeLambda =>
300+
t.derivedLambdaType(resType = this(t.resType))
301+
case CapturingType(_, _) =>
302+
t
303+
case _ =>
304+
val t1 = t match
305+
case t @ RefinedType(parent, rname, rinfo: MethodType) if defn.isFunctionType(t) =>
306+
t.derivedRefinedType(parent, rname, this(rinfo))
307+
case _ =>
308+
mapOver(t)
309+
if variance > 0 then t1
310+
else Setup.decorate(t1, Function.const(CaptureSet.Fluid))
311+
312+
def isPreCC(sym: Symbol): Boolean =
313+
sym.isTerm && sym.maybeOwner.isClass
314+
&& !sym.owner.is(CaptureChecked)
315+
&& !defn.isFunctionSymbol(sym.owner)
316+
317+
if isPreCC(sym) then
318+
val tpw = tp.widen
319+
val fluidTp = fluidify(tpw)
320+
if fluidTp eq tpw then tp
321+
else fluidTp.showing(i"fluid for ${sym.showLocated}, ${sym.is(JavaDefined)}: $tp --> $result", capt)
322+
else tp
323+
end handleBackwardsCompat
324+
293325
override def recheckIdent(tree: Ident)(using Context): Type =
294326
if tree.symbol.is(Method) then
295327
if tree.symbol.info.isParameterless then
296328
// there won't be an apply; need to include call captures now
297329
includeCallCaptures(tree.symbol, tree.srcPos)
298330
else
299331
markFree(tree.symbol, tree.srcPos)
300-
super.recheckIdent(tree)
332+
handleBackwardsCompat(super.recheckIdent(tree), tree.symbol)
301333

302334
/** A specialized implementation of the selection rule.
303335
*
@@ -327,7 +359,7 @@ class CheckCaptures extends Recheck, SymTransformer:
327359
val selType = recheckSelection(tree, qualType, name, disambiguate)
328360
val selCs = selType.widen.captureSet
329361
if selCs.isAlwaysEmpty || selType.widen.isBoxedCapturing || qualType.isBoxedCapturing then
330-
selType
362+
handleBackwardsCompat(selType, tree.symbol)
331363
else
332364
val qualCs = qualType.captureSet
333365
capt.println(i"pick one of $qualType, ${selType.widen}, $qualCs, $selCs in $tree")
@@ -362,7 +394,16 @@ class CheckCaptures extends Recheck, SymTransformer:
362394
val argType0 = f(recheckStart(arg, pt))
363395
val argType = super.recheckFinish(argType0, arg, pt)
364396
super.recheckFinish(argType, tree, pt)
365-
if meth == defn.Caps_unsafeBox then
397+
398+
if meth == defn.Caps_unsafeAssumePure then
399+
val arg :: Nil = tree.args: @unchecked
400+
val argType0 = recheck(arg, pt.capturing(CaptureSet.universal))
401+
val argType =
402+
if argType0.captureSet.isAlwaysEmpty then argType0
403+
else argType0.widen.stripCapturing
404+
capt.println(i"rechecking $arg with ${pt.capturing(CaptureSet.universal)}: $argType")
405+
super.recheckFinish(argType, tree, pt)
406+
else if meth == defn.Caps_unsafeBox then
366407
mapArgUsing(_.forceBoxStatus(true))
367408
else if meth == defn.Caps_unsafeUnbox then
368409
mapArgUsing(_.forceBoxStatus(false))
@@ -662,8 +703,10 @@ class CheckCaptures extends Recheck, SymTransformer:
662703
/** Turn `expected` into a dependent function when `actual` is dependent. */
663704
private def alignDependentFunction(expected: Type, actual: Type)(using Context): Type =
664705
def recur(expected: Type): Type = expected.dealias match
665-
case expected @ CapturingType(eparent, refs) =>
666-
CapturingType(recur(eparent), refs, boxed = expected.isBoxed)
706+
case expected0 @ CapturingType(eparent, refs) =>
707+
val eparent1 = recur(eparent)
708+
if eparent1 eq eparent then expected
709+
else CapturingType(eparent1, refs, boxed = expected0.isBoxed)
667710
case expected @ defn.FunctionOf(args, resultType, isContextual)
668711
if defn.isNonRefinedFunction(expected) && defn.isFunctionNType(actual) && !defn.isNonRefinedFunction(actual) =>
669712
val expected1 = toDepFun(args, resultType, isContextual)
@@ -883,7 +926,7 @@ class CheckCaptures extends Recheck, SymTransformer:
883926
* But maybe we can then elide the check during the RefChecks phase under captureChecking?
884927
*/
885928
def checkOverrides = new TreeTraverser:
886-
class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, srcPos: SrcPos)(using Context) extends OverridingPairsChecker(clazz, self) {
929+
class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, srcPos: SrcPos)(using Context) extends OverridingPairsChecker(clazz, self):
887930
/** Check subtype with box adaptation.
888931
* This function is passed to RefChecks to check the compatibility of overriding pairs.
889932
* @param sym symbol of the field definition that is being checked
@@ -905,7 +948,11 @@ class CheckCaptures extends Recheck, SymTransformer:
905948
case _ => adapted
906949
finally curEnv = saved
907950
actual1 frozen_<:< expected1
908-
}
951+
952+
override def adjustInfo(tp: Type, member: Symbol)(using Context): Type =
953+
handleBackwardsCompat(tp, member, initialVariance = 0)
954+
//.showing(i"adjust $other: $tp --> $result")
955+
end OverridingPairsCheckerCC
909956

910957
def traverse(t: Tree)(using Context) =
911958
t match

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

+97-86
Original file line numberDiff line numberDiff line change
@@ -114,88 +114,6 @@ extends tpd.TreeTraverser:
114114
case _ => tp
115115
case _ => tp
116116

117-
private def superTypeIsImpure(tp: Type): Boolean = {
118-
tp.dealias match
119-
case CapturingType(_, refs) =>
120-
!refs.isAlwaysEmpty
121-
case tp: (TypeRef | AppliedType) =>
122-
val sym = tp.typeSymbol
123-
if sym.isClass then
124-
sym == defn.AnyClass
125-
// we assume Any is a shorthand of {cap} Any, so if Any is an upper
126-
// bound, the type is taken to be impure.
127-
else superTypeIsImpure(tp.superType)
128-
case tp: (RefinedOrRecType | MatchType) =>
129-
superTypeIsImpure(tp.underlying)
130-
case tp: AndType =>
131-
superTypeIsImpure(tp.tp1) || needsVariable(tp.tp2)
132-
case tp: OrType =>
133-
superTypeIsImpure(tp.tp1) && superTypeIsImpure(tp.tp2)
134-
case _ =>
135-
false
136-
}.showing(i"super type is impure $tp = $result", capt)
137-
138-
/** Should a capture set variable be added on type `tp`? */
139-
def needsVariable(tp: Type): Boolean = {
140-
tp.typeParams.isEmpty && tp.match
141-
case tp: (TypeRef | AppliedType) =>
142-
val tp1 = tp.dealias
143-
if tp1 ne tp then needsVariable(tp1)
144-
else
145-
val sym = tp1.typeSymbol
146-
if sym.isClass then
147-
!sym.isPureClass && sym != defn.AnyClass
148-
else superTypeIsImpure(tp1)
149-
case tp: (RefinedOrRecType | MatchType) =>
150-
needsVariable(tp.underlying)
151-
case tp: AndType =>
152-
needsVariable(tp.tp1) && needsVariable(tp.tp2)
153-
case tp: OrType =>
154-
needsVariable(tp.tp1) || needsVariable(tp.tp2)
155-
case CapturingType(parent, refs) =>
156-
needsVariable(parent)
157-
&& refs.isConst // if refs is a variable, no need to add another
158-
&& !refs.isUniversal // if refs is {cap}, an added variable would not change anything
159-
case _ =>
160-
false
161-
}.showing(i"can have inferred capture $tp = $result", capt)
162-
163-
/** Add a capture set variable to `tp` if necessary, or maybe pull out
164-
* an embedded capture set variable from a part of `tp`.
165-
*/
166-
def addVar(tp: Type) = tp match
167-
case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) =>
168-
CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed)
169-
case tp: RecType =>
170-
tp.parent match
171-
case parent @ CapturingType(parent1, refs) =>
172-
CapturingType(tp.derivedRecType(parent1), refs, parent.isBoxed)
173-
case _ =>
174-
tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created
175-
// by `mapInferred`. Hence if the underlying type admits capture variables
176-
// a variable was already added, and the first case above would apply.
177-
case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) =>
178-
assert(refs1.asVar.elems.isEmpty)
179-
assert(refs2.asVar.elems.isEmpty)
180-
assert(tp1.isBoxed == tp2.isBoxed)
181-
CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed)
182-
case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) =>
183-
assert(refs1.asVar.elems.isEmpty)
184-
assert(refs2.asVar.elems.isEmpty)
185-
assert(tp1.isBoxed == tp2.isBoxed)
186-
CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed)
187-
case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) =>
188-
CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed)
189-
case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) =>
190-
CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed)
191-
case _ if needsVariable(tp) =>
192-
val cs = tp.dealias match
193-
case CapturingType(_, refs) => CaptureSet.Var(refs.elems)
194-
case _ => CaptureSet.Var()
195-
CapturingType(tp, cs)
196-
case _ =>
197-
tp
198-
199117
private var isTopLevel = true
200118

201119
private def mapNested(ts: List[Type]): List[Type] =
@@ -246,7 +164,7 @@ extends tpd.TreeTraverser:
246164
resType = this(tp.resType))
247165
case _ =>
248166
mapOver(tp)
249-
addVar(addCaptureRefinements(tp1))
167+
Setup.addVar(addCaptureRefinements(tp1))
250168
end apply
251169
end mapInferred
252170

@@ -385,9 +303,9 @@ extends tpd.TreeTraverser:
385303
val polyType = fn.tpe.widen.asInstanceOf[TypeLambda]
386304
for case (arg: TypeTree, pinfo, pname) <- args.lazyZip(polyType.paramInfos).lazyZip((polyType.paramNames)) do
387305
if pinfo.bounds.hi.hasAnnotation(defn.Caps_SealedAnnot) then
388-
def where = if fn.symbol.exists then i" in the body of ${fn.symbol}" else ""
306+
def where = if fn.symbol.exists then i" in an argument of ${fn.symbol}" else ""
389307
CheckCaptures.disallowRootCapabilitiesIn(arg.knownType,
390-
i"Sealed type variable $pname", " be instantiated to",
308+
i"Sealed type variable $pname", "be instantiated to",
391309
i"This is often caused by a local capability$where\nleaking as part of its result.",
392310
tree.srcPos)
393311
case _ =>
@@ -428,7 +346,7 @@ extends tpd.TreeTraverser:
428346
if prevLambdas.isEmpty then restp
429347
else SubstParams(prevPsymss, prevLambdas)(restp)
430348

431-
if tree.tpt.hasRememberedType && !sym.isConstructor then
349+
if sym.exists && tree.tpt.hasRememberedType && !sym.isConstructor then
432350
val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil)
433351
.showing(i"update info $sym: ${sym.info} --> $result", capt)
434352
if newInfo ne sym.info then
@@ -474,4 +392,97 @@ object Setup:
474392

475393
def isDuringSetup(using Context): Boolean =
476394
ctx.property(IsDuringSetupKey).isDefined
395+
396+
private def superTypeIsImpure(tp: Type)(using Context): Boolean = {
397+
tp.dealias match
398+
case CapturingType(_, refs) =>
399+
!refs.isAlwaysEmpty
400+
case tp: (TypeRef | AppliedType) =>
401+
val sym = tp.typeSymbol
402+
if sym.isClass then
403+
sym == defn.AnyClass
404+
// we assume Any is a shorthand of {cap} Any, so if Any is an upper
405+
// bound, the type is taken to be impure.
406+
else superTypeIsImpure(tp.superType)
407+
case tp: (RefinedOrRecType | MatchType) =>
408+
superTypeIsImpure(tp.underlying)
409+
case tp: AndType =>
410+
superTypeIsImpure(tp.tp1) || needsVariable(tp.tp2)
411+
case tp: OrType =>
412+
superTypeIsImpure(tp.tp1) && superTypeIsImpure(tp.tp2)
413+
case _ =>
414+
false
415+
}.showing(i"super type is impure $tp = $result", capt)
416+
417+
/** Should a capture set variable be added on type `tp`? */
418+
def needsVariable(tp: Type)(using Context): Boolean = {
419+
tp.typeParams.isEmpty && tp.match
420+
case tp: (TypeRef | AppliedType) =>
421+
val sym = tp.typeSymbol
422+
if sym.isClass then
423+
!sym.isPureClass && sym != defn.AnyClass
424+
else
425+
sym != defn.FromJavaObjectSymbol
426+
// For capture checking, we assume Object from Java is the same as Any
427+
&& {
428+
val tp1 = tp.dealias
429+
if tp1 ne tp then needsVariable(tp1)
430+
else superTypeIsImpure(tp1)
431+
}
432+
case tp: (RefinedOrRecType | MatchType) =>
433+
needsVariable(tp.underlying)
434+
case tp: AndType =>
435+
needsVariable(tp.tp1) && needsVariable(tp.tp2)
436+
case tp: OrType =>
437+
needsVariable(tp.tp1) || needsVariable(tp.tp2)
438+
case CapturingType(parent, refs) =>
439+
needsVariable(parent)
440+
&& refs.isConst // if refs is a variable, no need to add another
441+
&& !refs.isUniversal // if refs is {cap}, an added variable would not change anything
442+
case _ =>
443+
false
444+
}.showing(i"can have inferred capture $tp = $result", capt)
445+
446+
/** Add a capture set variable to `tp` if necessary, or maybe pull out
447+
* an embedded capture set variable from a part of `tp`.
448+
*/
449+
def decorate(tp: Type, addedSet: Type => CaptureSet)(using Context): Type = tp match
450+
case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) =>
451+
CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed)
452+
case tp: RecType =>
453+
tp.parent match
454+
case parent @ CapturingType(parent1, refs) =>
455+
CapturingType(tp.derivedRecType(parent1), refs, parent.isBoxed)
456+
case _ =>
457+
tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created
458+
// by `mapInferred`. Hence if the underlying type admits capture variables
459+
// a variable was already added, and the first case above would apply.
460+
case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) =>
461+
assert(refs1.elems.isEmpty)
462+
assert(refs2.elems.isEmpty)
463+
assert(tp1.isBoxed == tp2.isBoxed)
464+
CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed)
465+
case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) =>
466+
assert(refs1.elems.isEmpty)
467+
assert(refs2.elems.isEmpty)
468+
assert(tp1.isBoxed == tp2.isBoxed)
469+
CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed)
470+
case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) =>
471+
CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed)
472+
case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) =>
473+
CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed)
474+
case _ if needsVariable(tp) =>
475+
CapturingType(tp, addedSet(tp))
476+
case _ =>
477+
tp
478+
479+
/** Add a capture set variable to `tp` if necessary, or maybe pull out
480+
* an embedded capture set variable from a part of `tp`.
481+
*/
482+
def addVar(tp: Type)(using Context): Type =
483+
decorate(tp,
484+
addedSet = _.dealias.match
485+
case CapturingType(_, refs) => CaptureSet.Var(refs.elems)
486+
case _ => CaptureSet.Var())
487+
477488
end Setup

0 commit comments

Comments
 (0)