Skip to content

Commit 64c3138

Browse files
authored
New capture escape checking based on levels (#18463)
A new scope restriction scheme for capture checking based on levels. The idea is to have a stack of capture roots where inner capture roots are super-captures of outer roots. Refines and supersedes #18348
2 parents 08f2faf + 4a45939 commit 64c3138

File tree

102 files changed

+2126
-968
lines changed

Some content is hidden

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

102 files changed

+2126
-968
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,6 @@ docs/_spec/.jekyll-metadata
9999
# scaladoc related
100100
scaladoc/output/
101101

102+
#coverage
103+
coverage/
104+

compiler/src/dotty/tools/dotc/Run.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,9 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
247247
profiler.afterPhase(phase, profileBefore)
248248
if (ctx.settings.Xprint.value.containsPhase(phase))
249249
for (unit <- units)
250-
lastPrintedTree =
251-
printTree(lastPrintedTree)(using ctx.fresh.setPhase(phase.next).setCompilationUnit(unit))
250+
def printCtx(unit: CompilationUnit) = phase.printingContext(
251+
ctx.fresh.setPhase(phase.next).setCompilationUnit(unit))
252+
lastPrintedTree = printTree(lastPrintedTree)(using printCtx(unit))
252253
report.informTime(s"$phase ", start)
253254
Stats.record(s"total trees at end of $phase", ast.Trees.ntrees)
254255
for (unit <- units)

compiler/src/dotty/tools/dotc/ast/Desugar.scala

+8-5
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,12 @@ object desugar {
205205

206206
def makeImplicitParameters(
207207
tpts: List[Tree], implicitFlag: FlagSet,
208-
mkParamName: () => TermName,
208+
mkParamName: Int => TermName,
209209
forPrimaryConstructor: Boolean = false
210210
)(using Context): List[ValDef] =
211211
for (tpt, i) <- tpts.zipWithIndex yield {
212212
val paramFlags: FlagSet = if (forPrimaryConstructor) LocalParamAccessor else Param
213-
val epname = mkParamName()
213+
val epname = mkParamName(i)
214214
ValDef(epname, tpt, EmptyTree).withFlags(paramFlags | implicitFlag)
215215
}
216216

@@ -254,7 +254,7 @@ object desugar {
254254
// using clauses, we only need names that are unique among the
255255
// parameters of the method since shadowing does not affect
256256
// implicit resolution in Scala 3.
257-
mkParamName = () =>
257+
mkParamName = i =>
258258
val index = seenContextBounds + 1 // Start at 1 like FreshNameCreator.
259259
val ret = ContextBoundParamName(EmptyTermName, index)
260260
seenContextBounds += 1
@@ -1602,9 +1602,12 @@ object desugar {
16021602
case vd: ValDef => vd
16031603
}
16041604

1605-
def makeContextualFunction(formals: List[Tree], body: Tree, erasedParams: List[Boolean])(using Context): Function = {
1605+
def makeContextualFunction(formals: List[Tree], paramNamesOrNil: List[TermName], body: Tree, erasedParams: List[Boolean])(using Context): Function = {
16061606
val mods = Given
1607-
val params = makeImplicitParameters(formals, mods, mkParamName = () => ContextFunctionParamName.fresh())
1607+
val params = makeImplicitParameters(formals, mods,
1608+
mkParamName = i =>
1609+
if paramNamesOrNil.isEmpty then ContextFunctionParamName.fresh()
1610+
else paramNamesOrNil(i))
16081611
FunctionWithMods(params, body, Modifiers(mods), erasedParams)
16091612
}
16101613

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

+36
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,17 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
376376
case _ =>
377377
tree.tpe.isInstanceOf[ThisType]
378378
}
379+
380+
/** Under capture checking, an extractor for qualified roots `cap[Q]`.
381+
*/
382+
object QualifiedRoot:
383+
384+
def unapply(tree: Apply)(using Context): Option[String] = tree match
385+
case Apply(fn, Literal(lit) :: Nil) if fn.symbol == defn.Caps_capIn =>
386+
Some(lit.value.asInstanceOf[String])
387+
case _ =>
388+
None
389+
end QualifiedRoot
379390
}
380391

381392
trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] =>
@@ -799,12 +810,37 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
799810
}
800811
}
801812

813+
/** An extractor for def of a closure contained the block of the closure,
814+
* possibly with type ascriptions.
815+
*/
816+
object possiblyTypedClosureDef:
817+
def unapply(tree: Tree)(using Context): Option[DefDef] = tree match
818+
case Typed(expr, _) => unapply(expr)
819+
case _ => closureDef.unapply(tree)
820+
802821
/** If tree is a closure, its body, otherwise tree itself */
803822
def closureBody(tree: Tree)(using Context): Tree = tree match {
804823
case closureDef(meth) => meth.rhs
805824
case _ => tree
806825
}
807826

827+
/** Is `mdef` an eta-expansion of a method reference? To recognize this, we use
828+
* the following criterion: A method definition is an eta expansion, if
829+
* it contains at least one term paramter, the parameter has a zero extent span,
830+
* and the right hand side is either an application or a closure with'
831+
* an anonymous method that's itself characterized as an eta expansion.
832+
*/
833+
def isEtaExpansion(mdef: DefDef)(using Context): Boolean =
834+
!rhsOfEtaExpansion(mdef).isEmpty
835+
836+
def rhsOfEtaExpansion(mdef: DefDef)(using Context): Tree = mdef.paramss match
837+
case (param :: _) :: _ if param.asInstanceOf[Tree].span.isZeroExtent =>
838+
mdef.rhs match
839+
case rhs: Apply => rhs
840+
case closureDef(mdef1) => rhsOfEtaExpansion(mdef1)
841+
case _ => EmptyTree
842+
case _ => EmptyTree
843+
808844
/** The variables defined by a pattern, in reverse order of their appearance. */
809845
def patVars(tree: Tree)(using Context): List[Symbol] = {
810846
val acc = new TreeAccumulator[List[Symbol]] { outer =>

compiler/src/dotty/tools/dotc/ast/untpd.scala

+7-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,10 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
149149
case Floating
150150
}
151151

152-
/** {x1, ..., xN} T (only relevant under captureChecking) */
152+
/** {x1, ..., xN} T (only relevant under captureChecking)
153+
* Created when parsing function types so that capture set and result type
154+
* is combined in a single node.
155+
*/
153156
case class CapturesAndResult(refs: List[Tree], parent: Tree)(implicit @constructorOnly src: SourceFile) extends TypTree
154157

155158
/** A type tree appearing somewhere in the untyped DefDef of a lambda, it will be typed using `tpFun`.
@@ -512,6 +515,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
512515
def captureRoot(using Context): Select =
513516
Select(scalaDot(nme.caps), nme.CAPTURE_ROOT)
514517

518+
def captureRootIn(using Context): Select =
519+
Select(scalaDot(nme.caps), nme.capIn)
520+
515521
def makeRetaining(parent: Tree, refs: List[Tree], annotName: TypeName)(using Context): Annotated =
516522
Annotated(parent, New(scalaAnnotationDot(annotName), List(refs)))
517523

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

+166-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import config.SourceVersion
1010
import config.Printers.capt
1111
import util.Property.Key
1212
import tpd.*
13+
import StdNames.nme
1314
import config.Feature
15+
import collection.mutable
1416

1517
private val Captures: Key[CaptureSet] = Key()
1618
private val BoxedType: Key[BoxedTypeCache] = Key()
@@ -21,6 +23,11 @@ private val BoxedType: Key[BoxedTypeCache] = Key()
2123
*/
2224
private val adaptUnpickledFunctionTypes = false
2325

26+
/** Switch whether we constrain a root var that includes the source of a
27+
* root map to be an alias of that source (so that it can be mapped)
28+
*/
29+
private val constrainRootsWhenMapping = true
30+
2431
/** The arguments of a @retains or @retainsByName annotation */
2532
private[cc] def retainedElems(tree: Tree)(using Context): List[Tree] = tree match
2633
case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems
@@ -32,12 +39,82 @@ def allowUniversalInBoxed(using Context) =
3239
/** An exception thrown if a @retains argument is not syntactically a CaptureRef */
3340
class IllegalCaptureRef(tpe: Type) extends Exception
3441

42+
/** Capture checking state, which is stored in a context property */
43+
class CCState:
44+
45+
val rhsClosure: mutable.HashSet[Symbol] = new mutable.HashSet
46+
47+
val levelOwners: mutable.HashSet[Symbol] = new mutable.HashSet
48+
49+
/** Associates certain symbols (the nesting level owners) with their ccNestingLevel */
50+
val nestingLevels: mutable.HashMap[Symbol, Int] = new mutable.HashMap
51+
52+
/** Associates nesting level owners with the local roots valid in their scopes. */
53+
val localRoots: mutable.HashMap[Symbol, Symbol] = new mutable.HashMap
54+
55+
/** The last pair of capture reference and capture set where
56+
* the reference could not be added to the set due to a level conflict.
57+
*/
58+
var levelError: Option[(CaptureRef, CaptureSet)] = None
59+
60+
/** Under saferExceptions: The <try block> symbol generated for a try.
61+
* Installed by Setup, removed by CheckCaptures.
62+
*/
63+
val tryBlockOwner: mutable.HashMap[Try, Symbol] = new mutable.HashMap
64+
end CCState
65+
66+
/** Property key for capture checking state */
67+
val ccStateKey: Key[CCState] = Key()
68+
69+
/** The currently valid CCState */
70+
def ccState(using Context) = ctx.property(ccStateKey).get
71+
72+
trait FollowAliases extends TypeMap:
73+
def mapOverFollowingAliases(t: Type): Type = t match
74+
case t: LazyRef =>
75+
val t1 = this(t.ref)
76+
if t1 ne t.ref then t1 else t
77+
case _ =>
78+
val t1 = t.dealiasKeepAnnots
79+
if t1 ne t then
80+
val t2 = this(t1)
81+
if t2 ne t1 then return t2
82+
mapOver(t)
83+
84+
class mapRoots(from: CaptureRoot, to: CaptureRoot)(using Context) extends BiTypeMap, FollowAliases:
85+
thisMap =>
86+
87+
def apply(t: Type): Type =
88+
if t eq from then to
89+
else t match
90+
case t: CaptureRoot.Var =>
91+
val ta = t.followAlias
92+
if ta ne t then apply(ta)
93+
else from match
94+
case from: TermRef
95+
if t.upperLevel >= from.symbol.ccNestingLevel
96+
&& constrainRootsWhenMapping // next two lines do the constraining
97+
&& CaptureRoot.isEnclosingRoot(from, t)
98+
&& CaptureRoot.isEnclosingRoot(t, from) => to
99+
case from: CaptureRoot.Var if from.followAlias eq t => to
100+
case _ => t
101+
case _ =>
102+
mapOverFollowingAliases(t)
103+
104+
def inverse = mapRoots(to, from)
105+
end mapRoots
106+
35107
extension (tree: Tree)
36108

37109
/** Map tree with CaptureRef type to its type, throw IllegalCaptureRef otherwise */
38-
def toCaptureRef(using Context): CaptureRef = tree.tpe match
39-
case ref: CaptureRef => ref
40-
case tpe => throw IllegalCaptureRef(tpe)
110+
def toCaptureRef(using Context): CaptureRef = tree match
111+
case QualifiedRoot(outer) =>
112+
ctx.owner.levelOwnerNamed(outer)
113+
.orElse(defn.captureRoot) // non-existing outer roots are reported in Setup's checkQualifiedRoots
114+
.localRoot.termRef
115+
case _ => tree.tpe match
116+
case ref: CaptureRef => ref
117+
case tpe => throw IllegalCaptureRef(tpe) // if this was compiled from cc syntax, problem should have been reported at Typer
41118

42119
/** Convert a @retains or @retainsByName annotation tree to the capture set it represents.
43120
* For efficience, the result is cached as an Attachment on the tree.
@@ -164,7 +241,7 @@ extension (tp: Type)
164241
* a by name parameter type, turning the latter into an impure by name parameter type.
165242
*/
166243
def adaptByNameArgUnderPureFuns(using Context): Type =
167-
if Feature.pureFunsEnabledSomewhere then
244+
if adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere then
168245
AnnotatedType(tp,
169246
CaptureAnnotation(CaptureSet.universal, boxed = false)(defn.RetainsByNameAnnot))
170247
else
@@ -253,6 +330,91 @@ extension (sym: Symbol)
253330
&& sym != defn.Caps_unsafeBox
254331
&& sym != defn.Caps_unsafeUnbox
255332

333+
def isLevelOwner(using Context): Boolean = ccState.levelOwners.contains(sym)
334+
335+
/** The owner of the current level. Qualifying owners are
336+
* - methods other than constructors and anonymous functions
337+
* - anonymous functions, provided they either define a local
338+
* root of type caps.Cap, or they are the rhs of a val definition.
339+
* - classes, if they are not staticOwners
340+
* - _root_
341+
*/
342+
def levelOwner(using Context): Symbol =
343+
if !sym.exists || sym.isRoot || sym.isStaticOwner then defn.RootClass
344+
else if sym.isLevelOwner then sym
345+
else sym.owner.levelOwner
346+
347+
/** The nesting level of `sym` for the purposes of `cc`,
348+
* -1 for NoSymbol
349+
*/
350+
def ccNestingLevel(using Context): Int =
351+
if sym.exists then
352+
val lowner = sym.levelOwner
353+
ccState.nestingLevels.getOrElseUpdate(lowner,
354+
if lowner.isRoot then 0 else lowner.owner.ccNestingLevel + 1)
355+
else -1
356+
357+
/** Optionally, the nesting level of `sym` for the purposes of `cc`, provided
358+
* a capture checker is running.
359+
*/
360+
def ccNestingLevelOpt(using Context): Option[Int] =
361+
if ctx.property(ccStateKey).isDefined then Some(ccNestingLevel) else None
362+
363+
/** The parameter with type caps.Cap in the leading term parameter section,
364+
* or NoSymbol, if none exists.
365+
*/
366+
def definedLocalRoot(using Context): Symbol =
367+
sym.paramSymss.dropWhile(psyms => psyms.nonEmpty && psyms.head.isType) match
368+
case psyms :: _ => psyms.find(_.info.typeSymbol == defn.Caps_Cap).getOrElse(NoSymbol)
369+
case _ => NoSymbol
370+
371+
/** The local root corresponding to sym's level owner */
372+
def localRoot(using Context): Symbol =
373+
val owner = sym.levelOwner
374+
assert(owner.exists)
375+
def newRoot = newSymbol(if owner.isClass then newLocalDummy(owner) else owner,
376+
nme.LOCAL_CAPTURE_ROOT, Synthetic, defn.Caps_Cap.typeRef, nestingLevel = owner.ccNestingLevel)
377+
def lclRoot =
378+
if owner.isTerm then owner.definedLocalRoot.orElse(newRoot)
379+
else newRoot
380+
ccState.localRoots.getOrElseUpdate(owner, lclRoot)
381+
382+
/** The level owner enclosing `sym` which has the given name, or NoSymbol if none exists.
383+
* If name refers to a val that has a closure as rhs, we return the closure as level
384+
* owner.
385+
*/
386+
def levelOwnerNamed(name: String)(using Context): Symbol =
387+
def recur(owner: Symbol, prev: Symbol): Symbol =
388+
if owner.name.toString == name then
389+
if owner.isLevelOwner then owner
390+
else if owner.isTerm && !owner.isOneOf(Method | Module) && prev.exists then prev
391+
else NoSymbol
392+
else if owner == defn.RootClass then
393+
NoSymbol
394+
else
395+
val prev1 = if owner.isAnonymousFunction && owner.isLevelOwner then owner else NoSymbol
396+
recur(owner.owner, prev1)
397+
recur(sym, NoSymbol)
398+
.showing(i"find outer $sym [ $name ] = $result", capt)
399+
400+
def maxNested(other: Symbol)(using Context): Symbol =
401+
if sym.ccNestingLevel < other.ccNestingLevel then other else sym
402+
/* does not work yet, we do mix sets with different levels, for instance in cc-this.scala.
403+
else if sym.ccNestingLevel > other.ccNestingLevel then sym
404+
else
405+
assert(sym == other, i"conflicting symbols at same nesting level: $sym, $other")
406+
sym
407+
*/
408+
409+
def minNested(other: Symbol)(using Context): Symbol =
410+
if sym.ccNestingLevel > other.ccNestingLevel then other else sym
411+
412+
extension (tp: TermRef | ThisType)
413+
/** The nesting level of this reference as defined by capture checking */
414+
def ccNestingLevel(using Context): Int = tp match
415+
case tp: TermRef => tp.symbol.ccNestingLevel
416+
case tp: ThisType => tp.cls.ccNestingLevel
417+
256418
extension (tp: AnnotatedType)
257419
/** Is this a boxed capturing type? */
258420
def isBoxed(using Context): Boolean = tp.annot match

0 commit comments

Comments
 (0)