Skip to content

Commit a21791b

Browse files
authored
Fixes to make dotc compile with capture checking (#16254)
This PR contains all fixes and enhancements that were needed to make the compiler itself pass capture checking. A separate PR #16292 adds a capture-checkable version of the compiler as a test case.
2 parents d876acf + dc4d6c9 commit a21791b

Some content is hidden

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

58 files changed

+662
-211
lines changed

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,15 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
164164
private var finalizeActions = mutable.ListBuffer[() => Unit]()
165165

166166
/** Will be set to true if any of the compiled compilation units contains
167-
* a pureFunctions or captureChecking language import.
167+
* a pureFunctions language import.
168168
*/
169169
var pureFunsImportEncountered = false
170170

171+
/** Will be set to true if any of the compiled compilation units contains
172+
* a captureChecking language import.
173+
*/
174+
var ccImportEncountered = false
175+
171176
def compile(files: List[AbstractFile]): Unit =
172177
try
173178
val sources = files.map(runContext.getSource(_))
@@ -229,6 +234,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
229234
def runPhases(using Context) = {
230235
var lastPrintedTree: PrintedTree = NoPrintedTree
231236
val profiler = ctx.profiler
237+
var phasesWereAdjusted = false
232238

233239
for (phase <- ctx.base.allPhases)
234240
if (phase.isRunnable)
@@ -247,6 +253,11 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
247253
Stats.record(s"retained typed trees at end of $phase", unit.tpdTree.treeSize)
248254
ctx.typerState.gc()
249255
}
256+
if !phasesWereAdjusted then
257+
phasesWereAdjusted = true
258+
if !Feature.ccEnabledSomewhere then
259+
ctx.base.unlinkPhaseAsDenotTransformer(Phases.checkCapturesPhase.prev)
260+
ctx.base.unlinkPhaseAsDenotTransformer(Phases.checkCapturesPhase)
250261

251262
profiler.finished()
252263
}

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,49 @@ extension (tp: Type)
166166
case CapturingType(_, _) => true
167167
case _ => false
168168

169+
/** Is type known to be always pure by its class structure,
170+
* so that adding a capture set to it would not make sense?
171+
*/
172+
def isAlwaysPure(using Context): Boolean = tp.dealias match
173+
case tp: (TypeRef | AppliedType) =>
174+
val sym = tp.typeSymbol
175+
if sym.isClass then sym.isPureClass
176+
else tp.superType.isAlwaysPure
177+
case CapturingType(parent, refs) =>
178+
parent.isAlwaysPure || refs.isAlwaysEmpty
179+
case tp: TypeProxy =>
180+
tp.superType.isAlwaysPure
181+
case tp: AndType =>
182+
tp.tp1.isAlwaysPure || tp.tp2.isAlwaysPure
183+
case tp: OrType =>
184+
tp.tp1.isAlwaysPure && tp.tp2.isAlwaysPure
185+
case _ =>
186+
false
187+
188+
extension (cls: ClassSymbol)
189+
190+
def pureBaseClass(using Context): Option[Symbol] =
191+
cls.baseClasses.find(bc =>
192+
defn.pureBaseClasses.contains(bc)
193+
|| {
194+
val selfType = bc.givenSelfType
195+
selfType.exists && selfType.captureSet.isAlwaysEmpty
196+
})
197+
169198
extension (sym: Symbol)
170199

200+
/** A class is pure if:
201+
* - one its base types has an explicitly declared self type with an empty capture set
202+
* - or it is a value class
203+
* - or it is an exception
204+
* - or it is one of Nothing, Null, or String
205+
*/
206+
def isPureClass(using Context): Boolean = sym match
207+
case cls: ClassSymbol =>
208+
cls.pureBaseClass.isDefined || defn.pureSimpleClasses.contains(cls)
209+
case _ =>
210+
false
211+
171212
/** Does this symbol allow results carrying the universal capability?
172213
* Currently this is true only for function type applies (since their
173214
* results are unboxed) and `erasedValue` since this function is magic in

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ sealed abstract class CaptureSet extends Showable:
271271
map(Substituters.SubstParamsMap(tl, to))
272272

273273
/** Invoke handler if this set has (or later aquires) the root capability `*` */
274-
def disallowRootCapability(handler: () => Unit)(using Context): this.type =
274+
def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type =
275275
if isUniversal then handler()
276276
this
277277

@@ -373,7 +373,7 @@ object CaptureSet:
373373
def isAlwaysEmpty = false
374374

375375
/** A handler to be invoked if the root reference `*` is added to this set */
376-
var addRootHandler: () => Unit = () => ()
376+
var rootAddedHandler: () => Context ?=> Unit = () => ()
377377

378378
var description: String = ""
379379

@@ -404,7 +404,7 @@ object CaptureSet:
404404
def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult =
405405
if !isConst && recordElemsState() then
406406
elems ++= newElems
407-
if isUniversal then addRootHandler()
407+
if isUniversal then rootAddedHandler()
408408
// assert(id != 2 || elems.size != 2, this)
409409
(CompareResult.OK /: deps) { (r, dep) =>
410410
r.andAlso(dep.tryInclude(newElems, this))
@@ -421,8 +421,8 @@ object CaptureSet:
421421
else
422422
CompareResult.fail(this)
423423

424-
override def disallowRootCapability(handler: () => Unit)(using Context): this.type =
425-
addRootHandler = handler
424+
override def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type =
425+
rootAddedHandler = handler
426426
super.disallowRootCapability(handler)
427427

428428
private var computingApprox = false

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

Lines changed: 80 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ import config.Printers.{capt, recheckr}
1010
import config.{Config, Feature}
1111
import ast.{tpd, untpd, Trees}
1212
import Trees.*
13-
import typer.RefChecks.{checkAllOverrides, checkParents}
13+
import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents}
14+
import typer.Checking.{checkBounds, checkAppliedTypesIn}
1415
import util.{SimpleIdentitySet, EqHashMap, SrcPos}
1516
import transform.SymUtils.*
1617
import transform.{Recheck, PreRecheck}
1718
import Recheck.*
1819
import scala.collection.mutable
1920
import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap}
2021
import StdNames.nme
22+
import NameKinds.DefaultGetterName
2123
import reporting.trace
2224

2325
/** The capture checker */
@@ -335,12 +337,21 @@ class CheckCaptures extends Recheck, SymTransformer:
335337
override def recheckApply(tree: Apply, pt: Type)(using Context): Type =
336338
val meth = tree.fun.symbol
337339
includeCallCaptures(meth, tree.srcPos)
338-
if meth == defn.Caps_unsafeBox || meth == defn.Caps_unsafeUnbox then
340+
def mapArgUsing(f: Type => Type) =
339341
val arg :: Nil = tree.args: @unchecked
340-
val argType0 = recheckStart(arg, pt)
341-
.forceBoxStatus(boxed = meth == defn.Caps_unsafeBox)
342+
val argType0 = f(recheckStart(arg, pt))
342343
val argType = super.recheckFinish(argType0, arg, pt)
343344
super.recheckFinish(argType, tree, pt)
345+
346+
if meth == defn.Caps_unsafeBox then
347+
mapArgUsing(_.forceBoxStatus(true))
348+
else if meth == defn.Caps_unsafeUnbox then
349+
mapArgUsing(_.forceBoxStatus(false))
350+
else if meth == defn.Caps_unsafeBoxFunArg then
351+
mapArgUsing {
352+
case defn.FunctionOf(paramtpe :: Nil, restpe, isContectual, isErased) =>
353+
defn.FunctionOf(paramtpe.forceBoxStatus(true) :: Nil, restpe, isContectual, isErased)
354+
}
344355
else
345356
super.recheckApply(tree, pt) match
346357
case appType @ CapturingType(appType1, refs) =>
@@ -432,7 +443,8 @@ class CheckCaptures extends Recheck, SymTransformer:
432443
block match
433444
case closureDef(mdef) =>
434445
pt.dealias match
435-
case defn.FunctionOf(ptformals, _, _, _) if ptformals.forall(_.captureSet.isAlwaysEmpty) =>
446+
case defn.FunctionOf(ptformals, _, _, _)
447+
if ptformals.nonEmpty && ptformals.forall(_.captureSet.isAlwaysEmpty) =>
436448
// Redo setup of the anonymous function so that formal parameters don't
437449
// get capture sets. This is important to avoid false widenings to `*`
438450
// when taking the base type of the actual closures's dependent function
@@ -442,46 +454,32 @@ class CheckCaptures extends Recheck, SymTransformer:
442454
// First, undo the previous setup which installed a completer for `meth`.
443455
atPhase(preRecheckPhase.prev)(meth.denot.copySymDenotation())
444456
.installAfter(preRecheckPhase)
457+
445458
// Next, update all parameter symbols to match expected formals
446459
meth.paramSymss.head.lazyZip(ptformals).foreach { (psym, pformal) =>
447-
psym.copySymDenotation(info = pformal).installAfter(preRecheckPhase)
460+
psym.updateInfoBetween(preRecheckPhase, thisPhase, pformal.mapExprType)
448461
}
449462
// Next, update types of parameter ValDefs
450463
mdef.paramss.head.lazyZip(ptformals).foreach { (param, pformal) =>
451464
val ValDef(_, tpt, _) = param: @unchecked
452465
tpt.rememberTypeAlways(pformal)
453466
}
454467
// Next, install a new completer reflecting the new parameters for the anonymous method
468+
val mt = meth.info.asInstanceOf[MethodType]
455469
val completer = new LazyType:
456470
def complete(denot: SymDenotation)(using Context) =
457-
denot.info = MethodType(ptformals, mdef.tpt.knownType)
471+
denot.info = mt.companion(ptformals, mdef.tpt.knownType)
458472
.showing(i"simplify info of $meth to $result", capt)
459473
recheckDef(mdef, meth)
460-
meth.copySymDenotation(info = completer, initFlags = meth.flags &~ Touched)
461-
.installAfter(preRecheckPhase)
474+
meth.updateInfoBetween(preRecheckPhase, thisPhase, completer)
462475
case _ =>
463476
case _ =>
464477
super.recheckBlock(block, pt)
465478

466-
/** If `rhsProto` has `*` as its capture set, wrap `rhs` in a `unsafeBox`.
467-
* Used to infer `unsafeBox` for expressions that get assigned to variables
468-
* that have universal capture set.
469-
*/
470-
def maybeBox(rhs: Tree, rhsProto: Type)(using Context): Tree =
471-
if rhsProto.captureSet.isUniversal then
472-
ref(defn.Caps_unsafeBox).appliedToType(rhsProto).appliedTo(rhs)
473-
else rhs
474-
475-
override def recheckAssign(tree: Assign)(using Context): Type =
476-
val rhsProto = recheck(tree.lhs).widen
477-
recheck(maybeBox(tree.rhs, rhsProto), rhsProto)
478-
defn.UnitType
479-
480479
override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit =
481480
try
482481
if !sym.is(Module) then // Modules are checked by checking the module class
483-
if sym.is(Mutable) then recheck(maybeBox(tree.rhs, sym.info), sym.info)
484-
else super.recheckValDef(tree, sym)
482+
super.recheckValDef(tree, sym)
485483
finally
486484
if !sym.is(Param) then
487485
// Parameters with inferred types belong to anonymous methods. We need to wait
@@ -503,7 +501,8 @@ class CheckCaptures extends Recheck, SymTransformer:
503501
/** Class-specific capture set relations:
504502
* 1. The capture set of a class includes the capture sets of its parents.
505503
* 2. The capture set of the self type of a class includes the capture set of the class.
506-
* 3. The capture set of the self type of a class includes the capture set of every class parameter.
504+
* 3. The capture set of the self type of a class includes the capture set of every class parameter,
505+
* unless the parameter is marked @constructorOnly.
507506
*/
508507
override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type =
509508
val saved = curEnv
@@ -515,7 +514,12 @@ class CheckCaptures extends Recheck, SymTransformer:
515514
val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls")
516515
checkSubset(localSet, thisSet, tree.srcPos) // (2)
517516
for param <- cls.paramGetters do
518-
checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3)
517+
if !param.hasAnnotation(defn.ConstructorOnlyAnnot) then
518+
checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3)
519+
for pureBase <- cls.pureBaseClass do
520+
checkSubset(thisSet,
521+
CaptureSet.empty.withDescription(i"of pure base class $pureBase"),
522+
tree.srcPos)
519523
super.recheckClassDef(tree, impl, cls)
520524
finally
521525
curEnv = saved
@@ -772,7 +776,8 @@ class CheckCaptures extends Recheck, SymTransformer:
772776
// We can't box/unbox the universal capability. Leave `actual` as it is
773777
// so we get an error in checkConforms. This tends to give better error
774778
// messages than disallowing the root capability in `criticalSet`.
775-
capt.println(i"cannot box/unbox $actual vs $expected")
779+
if ctx.settings.YccDebug.value then
780+
println(i"cannot box/unbox $actual vs $expected")
776781
actual
777782
else
778783
// Disallow future addition of `*` to `criticalSet`.
@@ -845,13 +850,21 @@ class CheckCaptures extends Recheck, SymTransformer:
845850
cls => !parentTrees(cls).exists(ptree => parentTrees.contains(ptree.tpe.classSymbol))
846851
}
847852
assert(roots.nonEmpty)
848-
for root <- roots do
849-
checkParents(root, parentTrees(root))
853+
for case root: ClassSymbol <- roots do
854+
checkSelfAgainstParents(root, root.baseClasses)
850855
val selfType = root.asClass.classInfo.selfType
851856
interpolator(startingVariance = -1).traverse(selfType)
852857
if !root.isEffectivelySealed then
858+
def matchesExplicitRefsInBaseClass(refs: CaptureSet, cls: ClassSymbol): Boolean =
859+
cls.baseClasses.tail.exists { psym =>
860+
val selfType = psym.asClass.givenSelfType
861+
selfType.exists && selfType.captureSet.elems == refs.elems
862+
}
853863
selfType match
854-
case CapturingType(_, refs: CaptureSet.Var) if !refs.isUniversal =>
864+
case CapturingType(_, refs: CaptureSet.Var)
865+
if !refs.isUniversal && !matchesExplicitRefsInBaseClass(refs, root) =>
866+
// Forbid inferred self types unless they are already implied by an explicit
867+
// self type in a parent.
855868
report.error(
856869
i"""$root needs an explicitly declared self type since its
857870
|inferred self type $selfType
@@ -867,6 +880,7 @@ class CheckCaptures extends Recheck, SymTransformer:
867880
* - Check that externally visible `val`s or `def`s have empty capture sets. If not,
868881
* suggest an explicit type. This is so that separate compilation (where external
869882
* symbols have empty capture sets) gives the same results as joint compilation.
883+
* - Check that arguments of TypeApplys and AppliedTypes conform to their bounds.
870884
*/
871885
def postCheck(unit: tpd.Tree)(using Context): Unit =
872886
unit.foreachSubTree {
@@ -885,15 +899,23 @@ class CheckCaptures extends Recheck, SymTransformer:
885899
val isLocal =
886900
sym.owner.ownersIterator.exists(_.isTerm)
887901
|| sym.accessBoundary(defn.RootClass).isContainedIn(sym.topLevelClass)
888-
889-
// The following classes of definitions need explicit capture types ...
890-
if !isLocal // ... since external capture types are not inferred
891-
|| sym.owner.is(Trait) // ... since we do OverridingPairs checking before capture inference
892-
|| sym.allOverriddenSymbols.nonEmpty // ... since we do override checking before capture inference
893-
then
902+
def canUseInferred = // If canUseInferred is false, all capturing types in the type of `sym` need to be given explicitly
903+
sym.is(Private) // private symbols can always have inferred types
904+
|| sym.name.is(DefaultGetterName) // default getters are exempted since otherwise it would be
905+
// too annoying. This is a hole since a defualt getter's result type
906+
// might leak into a type variable.
907+
|| // non-local symbols cannot have inferred types since external capture types are not inferred
908+
isLocal // local symbols still need explicit types if
909+
&& !sym.owner.is(Trait) // they are defined in a trait, since we do OverridingPairs checking before capture inference
910+
def isNotPureThis(ref: CaptureRef) = ref match {
911+
case ref: ThisType => !ref.cls.isPureClass
912+
case _ => true
913+
}
914+
if !canUseInferred then
894915
val inferred = t.tpt.knownType
895916
def checkPure(tp: Type) = tp match
896-
case CapturingType(_, refs) if !refs.elems.isEmpty =>
917+
case CapturingType(_, refs)
918+
if !refs.elems.filter(isNotPureThis).isEmpty =>
897919
val resultStr = if t.isInstanceOf[DefDef] then " result" else ""
898920
report.error(
899921
em"""Non-local $sym cannot have an inferred$resultStr type
@@ -902,8 +924,27 @@ class CheckCaptures extends Recheck, SymTransformer:
902924
|The type needs to be declared explicitly.""", t.srcPos)
903925
case _ =>
904926
inferred.foreachPart(checkPure, StopAt.Static)
927+
case t @ TypeApply(fun, args) =>
928+
fun.knownType.widen match
929+
case tl: PolyType =>
930+
val normArgs = args.lazyZip(tl.paramInfos).map { (arg, bounds) =>
931+
arg.withType(arg.knownType.forceBoxStatus(
932+
bounds.hi.isBoxedCapturing | bounds.lo.isBoxedCapturing))
933+
}
934+
checkBounds(normArgs, tl)
935+
case _ =>
905936
case _ =>
906937
}
907-
938+
if !ctx.reporter.errorsReported then
939+
// We dont report errors here if previous errors were reported, because other
940+
// errors often result in bad applied types, but flagging these bad types gives
941+
// often worse error messages than the original errors.
942+
val checkApplied = new TreeTraverser:
943+
def traverse(t: Tree)(using Context) = t match
944+
case tree: InferredTypeTree =>
945+
case tree: New =>
946+
case tree: TypeTree => checkAppliedTypesIn(tree.withKnownType)
947+
case _ => traverseChildren(t)
948+
checkApplied.traverse(unit)
908949
end CaptureChecker
909950
end CheckCaptures

0 commit comments

Comments
 (0)