Skip to content

Commit 23f5e32

Browse files
authored
Changes to Fresh and Separation Checking (#24112)
In this PR, I originally only wanted to change the scheme handling fresh caps: Capture sets in the type of a val or the result type of a def should only have a single fresh that had the val or def as its origin. Any other fresh caps that were inferred before in those sets should instead be hidden by the unique fresh. But doing this disturbed the system sufficiently to cause new problems in separation checking. These problems were caused by incorrect algorithms that had to be fixed first. The current status of the separation checker is "improved relative to the state before, but still a way to go".
2 parents 9f70200 + c492a63 commit 23f5e32

24 files changed

+400
-289
lines changed

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

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import CCState.*
1212
import Periods.{NoRunId, RunWidth}
1313
import compiletime.uninitialized
1414
import StdNames.nme
15-
import CaptureSet.VarState
15+
import CaptureSet.{Refs, emptyRefs, VarState}
1616
import Annotations.Annotation
1717
import Flags.*
1818
import config.Printers.capt
@@ -147,7 +147,7 @@ object Capabilities:
147147
* @param origin an indication where and why the FreshCap was created, used
148148
* for diagnostics
149149
*/
150-
case class FreshCap private (owner: Symbol, origin: Origin)(using @constructorOnly ctx: Context) extends RootCapability:
150+
case class FreshCap (owner: Symbol, origin: Origin)(using @constructorOnly ctx: Context) extends RootCapability:
151151
val hiddenSet = CaptureSet.HiddenSet(owner, this: @unchecked)
152152
// fails initialization check without the @unchecked
153153

@@ -547,6 +547,23 @@ object Capabilities:
547547
captureSetValid = currentId
548548
computed
549549

550+
/** The elements hidden by this capability, if this is a FreshCap
551+
* or a derived version of one. Read-only status and restrictions
552+
* are transferred from the capability to its hidden set.
553+
*/
554+
def hiddenSet(using Context): Refs = computeHiddenSet(identity)
555+
556+
/** Compute result based on hidden set of this capability.
557+
* Restrictions and read-only status transfer from the capability to its
558+
* hidden set.
559+
* @param f a function that gets applied to all detected hidden sets
560+
*/
561+
def computeHiddenSet(f: Refs => Refs)(using Context): Refs = this match
562+
case self: FreshCap => f(self.hiddenSet.elems)
563+
case Restricted(elem1, cls) => elem1.computeHiddenSet(f).map(_.restrict(cls))
564+
case ReadOnly(elem1) => elem1.computeHiddenSet(f).map(_.readOnly)
565+
case _ => emptyRefs
566+
550567
/** The transitive classifiers of this capability. */
551568
def transClassifiers(using Context): Classifiers =
552569
def toClassifiers(cls: ClassSymbol): Classifiers =
@@ -753,25 +770,44 @@ object Capabilities:
753770
* x covers x
754771
* x covers y ==> x covers y.f
755772
* x covers y ==> x* covers y*, x? covers y?
773+
* x covers y ==> <fresh hiding x> covers y
774+
* x covers y ==> x.only[C] covers y, x covers y.only[C]
775+
*
756776
* TODO what other clauses from subsumes do we need to port here?
777+
* The last clause is a conservative over-approximation: basically, we can't achieve
778+
* separation by having different classifiers for now. It would be good to
779+
* have a test that would expect such separation, then we can try to refine
780+
* the clause to make the test pass.
757781
*/
758782
final def covers(y: Capability)(using Context): Boolean =
759-
(this eq y)
760-
|| y.match
761-
case y @ TermRef(ypre: Capability, _) =>
762-
this.covers(ypre)
763-
case Reach(y1) =>
764-
this match
765-
case Reach(x1) => x1.covers(y1)
766-
case _ => false
767-
case Maybe(y1) =>
768-
this match
769-
case Maybe(x1) => x1.covers(y1)
770-
case _ => false
771-
case y: FreshCap =>
772-
y.hiddenSet.superCaps.exists(this.covers(_))
773-
case _ =>
774-
false
783+
val seen: util.EqHashSet[FreshCap] = new util.EqHashSet
784+
785+
def recur(x: Capability, y: Capability): Boolean =
786+
(x eq y)
787+
|| y.match
788+
case y @ TermRef(ypre: Capability, _) =>
789+
recur(x, ypre)
790+
case Reach(y1) =>
791+
x match
792+
case Reach(x1) => recur(x1, y1)
793+
case _ => false
794+
case Maybe(y1) =>
795+
x match
796+
case Maybe(x1) => recur(x1, y1)
797+
case _ => false
798+
case Restricted(y1, _) =>
799+
recur(x, y1)
800+
case _ =>
801+
false
802+
|| x.match
803+
case x: FreshCap if !seen.contains(x) =>
804+
seen.add(x)
805+
x.hiddenSet.exists(recur(_, y))
806+
case Restricted(x1, _) => recur(x1, y)
807+
case _ => false
808+
809+
recur(this, y)
810+
end covers
775811

776812
def assumedContainsOf(x: TypeRef)(using Context): SimpleIdentitySet[Capability] =
777813
CaptureSet.assumedContains.getOrElse(x, SimpleIdentitySet.empty)

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

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,21 +151,37 @@ extension (tp: Type)
151151
case tp: ObjectCapability => tp.captureSetOfInfo
152152
case _ => CaptureSet.ofType(tp, followResult = false)
153153

154-
/** The deep capture set of a type. This is by default the union of all
154+
/** Compute a captureset by traversing parts of this type. This is by default the union of all
155155
* covariant capture sets embedded in the widened type, as computed by
156156
* `CaptureSet.ofTypeDeeply`. If that set is nonempty, and the type is
157157
* a singleton capability `x` or a reach capability `x*`, the deep capture
158158
* set can be narrowed to`{x*}`.
159+
* @param includeTypevars if true, return a new FreshCap for every type parameter
160+
* or abstract type with an Any upper bound. Types with
161+
* defined upper bound are always mapped to the dcs of their bound
162+
* @param includeBoxed if true, include capture sets found in boxed parts of this type
159163
*/
160-
def deepCaptureSet(includeTypevars: Boolean)(using Context): CaptureSet =
161-
val dcs = CaptureSet.ofTypeDeeply(tp.widen.stripCapturing, includeTypevars)
164+
def computeDeepCaptureSet(includeTypevars: Boolean, includeBoxed: Boolean = true)(using Context): CaptureSet =
165+
val dcs = CaptureSet.ofTypeDeeply(tp.widen.stripCapturing, includeTypevars, includeBoxed)
162166
if dcs.isAlwaysEmpty then tp.captureSet
163167
else tp match
164168
case tp: ObjectCapability if tp.isTrackableRef => tp.reach.singletonCaptureSet
165169
case _ => tp.captureSet ++ dcs
166170

171+
/** The deep capture set of a type. This is by default the union of all
172+
* covariant capture sets embedded in the widened type, as computed by
173+
* `CaptureSet.ofTypeDeeply`. If that set is nonempty, and the type is
174+
* a singleton capability `x` or a reach capability `x*`, the deep capture
175+
* set can be narrowed to`{x*}`.
176+
*/
167177
def deepCaptureSet(using Context): CaptureSet =
168-
deepCaptureSet(includeTypevars = false)
178+
computeDeepCaptureSet(includeTypevars = false)
179+
180+
/** The span capture set of a type. This is analogous to deepCaptureSet but ignoring
181+
* capture sets in boxed parts.
182+
*/
183+
def spanCaptureSet(using Context): CaptureSet =
184+
computeDeepCaptureSet(includeTypevars = false, includeBoxed = false)
169185

170186
/** A type capturing `ref` */
171187
def capturing(ref: Capability)(using Context): Type =
@@ -362,7 +378,7 @@ extension (tp: Type)
362378
*/
363379
def derivesFromCapTraitDeeply(cls: ClassSymbol)(using Context): Boolean =
364380
val accumulate = new DeepTypeAccumulator[Boolean]:
365-
def capturingCase(acc: Boolean, parent: Type, refs: CaptureSet) =
381+
def capturingCase(acc: Boolean, parent: Type, refs: CaptureSet, boxed: Boolean) =
366382
this(acc, parent)
367383
&& (parent.derivesFromCapTrait(cls)
368384
|| refs.isConst && refs.elems.forall(_.derivesFromCapTrait(cls)))
@@ -734,15 +750,15 @@ object ContainsParam:
734750
abstract class DeepTypeAccumulator[T](using Context) extends TypeAccumulator[T]:
735751
val seen = util.HashSet[Symbol]()
736752

737-
protected def capturingCase(acc: T, parent: Type, refs: CaptureSet): T
753+
protected def capturingCase(acc: T, parent: Type, refs: CaptureSet, boxed: Boolean): T
738754

739755
protected def abstractTypeCase(acc: T, t: TypeRef, upperBound: Type): T
740756

741757
def apply(acc: T, t: Type) =
742758
if variance < 0 then acc
743759
else t.dealias match
744760
case t @ CapturingType(parent, cs) =>
745-
capturingCase(acc, parent, cs)
761+
capturingCase(acc, parent, cs, t.isBoxed)
746762
case t: TypeRef if t.symbol.isAbstractOrParamType && !seen.contains(t.symbol) =>
747763
seen += t.symbol
748764
abstractTypeCase(acc, t, t.info.bounds.hi)

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

Lines changed: 76 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,7 @@ object CaptureSet:
777777
assert(elem.subsumes(elem1),
778778
i"Skipped map ${tm.getClass} maps newly added $elem to $elem1 in $this")
779779

780-
protected def includeElem(elem: Capability)(using Context): Unit =
780+
protected def includeElem(elem: Capability)(using Context, VarState): Unit =
781781
if !elems.contains(elem) then
782782
if debugVars && id == debugTarget then
783783
println(i"###INCLUDE $elem in $this")
@@ -803,7 +803,10 @@ object CaptureSet:
803803
// id == 108 then assert(false, i"trying to add $elem to $this")
804804
assert(elem.isWellformed, elem)
805805
assert(!this.isInstanceOf[HiddenSet] || summon[VarState].isSeparating, summon[VarState])
806-
includeElem(elem)
806+
try includeElem(elem)
807+
catch case ex: AssertionError =>
808+
println(i"error for incl $elem in $this, ${summon[VarState].toString}")
809+
throw ex
807810
if isBadRoot(elem) then
808811
rootAddedHandler()
809812
val normElem = if isMaybeSet then elem else elem.stripMaybe
@@ -947,12 +950,64 @@ object CaptureSet:
947950
override def toString = s"Var$id$elems"
948951
end Var
949952

950-
/** Variables that represent refinements of class parameters can have the universal
951-
* capture set, since they represent only what is the result of the constructor.
952-
* Test case: Without that tweak, logger.scala would not compile.
953-
*/
954-
class RefiningVar(owner: Symbol)(using Context) extends Var(owner):
955-
override def disallowBadRoots(upto: Symbol)(handler: () => Context ?=> Unit)(using Context) = this
953+
/** Variables created in types of inferred type trees */
954+
class ProperVar(override val owner: Symbol, initialElems: Refs = emptyRefs, nestedOK: Boolean = true, isRefining: Boolean)(using /*@constructorOnly*/ ictx: Context)
955+
extends Var(owner, initialElems, nestedOK):
956+
957+
/** Make sure that capset variables in types of vals and result types of
958+
* non-anonymous functions contain only a single FreshCap, and furthermore
959+
* that that FreshCap has as origin InDecl(owner), where owner is the val
960+
* or def for which the type is defined.
961+
* Note: This currently does not apply to classified or read-only fresh caps.
962+
*/
963+
override def includeElem(elem: Capability)(using ctx: Context, vs: VarState): Unit = elem match
964+
case elem: FreshCap
965+
if !nestedOK
966+
&& !elems.contains(elem)
967+
&& !owner.isAnonymousFunction
968+
&& ccConfig.newScheme =>
969+
def fail = i"attempting to add $elem to $this"
970+
def hideIn(fc: FreshCap): Unit =
971+
assert(elem.tryClassifyAs(fc.hiddenSet.classifier), fail)
972+
if !isRefining then
973+
// If a variable is added by addCaptureRefinements in a synthetic
974+
// refinement of a class type, don't do level checking. The problem is
975+
// that the variable might be matched against a type that does not have
976+
// a refinement, in which case FreshCaps of the class definition would
977+
// leak out in the corresponding places. This will fail level checking.
978+
// The disallowBadRoots override below has a similar reason.
979+
// TODO: We should instead mark the variable as impossible to instantiate
980+
// and drop the refinement later in the inferred type.
981+
// Test case is drop-refinement.scala.
982+
assert(fc.acceptsLevelOf(elem),
983+
i"level failure, cannot add $elem with ${elem.levelOwner} to $owner / $getClass / $fail")
984+
fc.hiddenSet.add(elem)
985+
val isSubsumed = (false /: elems): (isSubsumed, prev) =>
986+
prev match
987+
case prev: FreshCap =>
988+
hideIn(prev)
989+
true
990+
case _ => isSubsumed
991+
if !isSubsumed then
992+
if elem.origin != Origin.InDecl(owner) || elem.hiddenSet.isConst then
993+
val fc = new FreshCap(owner, Origin.InDecl(owner))
994+
assert(fc.tryClassifyAs(elem.hiddenSet.classifier), fail)
995+
hideIn(fc)
996+
super.includeElem(fc)
997+
else
998+
super.includeElem(elem)
999+
case _ =>
1000+
super.includeElem(elem)
1001+
1002+
/** Variables that represent refinements of class parameters can have the universal
1003+
* capture set, since they represent only what is the result of the constructor.
1004+
* Test case: Without that tweak, logger.scala would not compile.
1005+
*/
1006+
override def disallowBadRoots(upto: Symbol)(handler: () => Context ?=> Unit)(using Context) =
1007+
if isRefining then this
1008+
else super.disallowBadRoots(upto)(handler)
1009+
1010+
end ProperVar
9561011

9571012
/** A variable that is derived from some other variable via a map or filter. */
9581013
abstract class DerivedVar(owner: Symbol, initialElems: Refs)(using @constructorOnly ctx: Context)
@@ -1163,6 +1218,9 @@ object CaptureSet:
11631218
*/
11641219
class HiddenSet(initialOwner: Symbol, val owningCap: FreshCap)(using @constructorOnly ictx: Context)
11651220
extends Var(initialOwner):
1221+
1222+
// Updated by anchorCaps in CheckCaptures, but owner can be changed only
1223+
// if it was NoSymbol before.
11661224
var givenOwner: Symbol = initialOwner
11671225

11681226
override def owner = givenOwner
@@ -1171,62 +1229,9 @@ object CaptureSet:
11711229

11721230
description = i"of elements subsumed by a fresh cap in $initialOwner"
11731231

1174-
private def aliasRef: FreshCap | Null =
1175-
if myElems.size == 1 then
1176-
myElems.nth(0) match
1177-
case alias: FreshCap if deps.contains(alias.hiddenSet) => alias
1178-
case _ => null
1179-
else null
1180-
1181-
private def aliasSet: HiddenSet =
1182-
if myElems.size == 1 then
1183-
myElems.nth(0) match
1184-
case alias: FreshCap if deps.contains(alias.hiddenSet) => alias.hiddenSet
1185-
case _ => this
1186-
else this
1187-
1188-
def superCaps: List[FreshCap] =
1189-
deps.toList.map(_.asInstanceOf[HiddenSet].owningCap)
1190-
1191-
override def elems: Refs =
1192-
val al = aliasSet
1193-
if al eq this then super.elems else al.elems
1194-
1195-
override def elems_=(refs: Refs) =
1196-
val al = aliasSet
1197-
if al eq this then super.elems_=(refs) else al.elems_=(refs)
1198-
1199-
/** Add element to hidden set. Also add it to all supersets (as indicated by
1200-
* deps of this set). Follow aliases on both hidden set and added element
1201-
* before adding. If the added element is also a Fresh instance with
1202-
* hidden set H which is a superset of this set, then make this set an
1203-
* alias of H.
1204-
*/
1232+
/** Add element to hidden set. */
12051233
def add(elem: Capability)(using ctx: Context, vs: VarState): Unit =
1206-
val alias = aliasSet
1207-
if alias ne this then alias.add(elem)
1208-
else
1209-
def addToElems() =
1210-
assert(!isConst)
1211-
includeElem(elem)
1212-
deps.foreach: dep =>
1213-
assert(dep != this)
1214-
vs.addHidden(dep.asInstanceOf[HiddenSet], elem)
1215-
elem match
1216-
case elem: FreshCap =>
1217-
if this ne elem.hiddenSet then
1218-
val alias = elem.hiddenSet.aliasRef
1219-
if alias != null then
1220-
add(alias)
1221-
else if deps.contains(elem.hiddenSet) then // make this an alias of elem
1222-
capt.println(i"Alias $this to ${elem.hiddenSet}")
1223-
elems = SimpleIdentitySet(elem)
1224-
deps = SimpleIdentitySet(elem.hiddenSet)
1225-
else
1226-
addToElems()
1227-
elem.hiddenSet.includeDep(this)
1228-
case _ =>
1229-
addToElems()
1234+
includeElem(elem)
12301235

12311236
/** Apply function `f` to `elems` while setting `elems` to empty for the
12321237
* duration. This is used to escape infinite recursions if two Freshs
@@ -1553,7 +1558,7 @@ object CaptureSet:
15531558
/** The capture set of the type underlying the capability `c` */
15541559
def ofInfo(c: Capability)(using Context): CaptureSet = c match
15551560
case Reach(c1) =>
1556-
c1.widen.deepCaptureSet(includeTypevars = true)
1561+
c1.widen.computeDeepCaptureSet(includeTypevars = true)
15571562
.showing(i"Deep capture set of $c: ${c1.widen} = ${result}", capt)
15581563
case Restricted(c1, cls) =>
15591564
if cls == defn.NothingClass then CaptureSet.empty
@@ -1615,13 +1620,17 @@ object CaptureSet:
16151620
/** The deep capture set of a type is the union of all covariant occurrences of
16161621
* capture sets. Nested existential sets are approximated with `cap`.
16171622
*/
1618-
def ofTypeDeeply(tp: Type, includeTypevars: Boolean = false)(using Context): CaptureSet =
1623+
def ofTypeDeeply(tp: Type, includeTypevars: Boolean = false, includeBoxed: Boolean = true)(using Context): CaptureSet =
16191624
val collect = new DeepTypeAccumulator[CaptureSet]:
1620-
def capturingCase(acc: CaptureSet, parent: Type, refs: CaptureSet) =
1621-
this(acc, parent) ++ refs
1625+
1626+
def capturingCase(acc: CaptureSet, parent: Type, refs: CaptureSet, boxed: Boolean) =
1627+
if includeBoxed || !boxed then this(acc, parent) ++ refs
1628+
else this(acc, parent)
1629+
16221630
def abstractTypeCase(acc: CaptureSet, t: TypeRef, upperBound: Type) =
16231631
if includeTypevars && upperBound.isExactlyAny then fresh(Origin.DeepCS(t))
16241632
else this(acc, upperBound)
1633+
16251634
collect(CaptureSet.empty, tp)
16261635

16271636
type AssumedContains = immutable.Map[TypeRef, SimpleIdentitySet[Capability]]

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1376,7 +1376,10 @@ class CheckCaptures extends Recheck, SymTransformer:
13761376
* where local capture roots are instantiated to root variables.
13771377
*/
13781378
override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type =
1379-
testAdapted(actual, expected, tree, addenda)(err.typeMismatch)
1379+
try testAdapted(actual, expected, tree, addenda)(err.typeMismatch)
1380+
catch case ex: AssertionError =>
1381+
println(i"error while checking $tree: $actual against $expected")
1382+
throw ex
13801383

13811384
@annotation.tailrec
13821385
private def findImpureUpperBound(tp: Type)(using Context): Type = tp match

0 commit comments

Comments
 (0)