Skip to content

Commit fcd29f6

Browse files
committed
Switch level checking for capsets to owner-based scheme
The previous scheme was too coarse since it treated all vaks in a method the same.
1 parent b952e3a commit fcd29f6

20 files changed

+177
-109
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,12 @@ object Capabilities:
484484
case self: FreshCap => self.hiddenSet.owner
485485
case _ /* : GlobalCap | ResultCap | ParamRef */ => NoSymbol
486486

487+
final def visibility(using Context): Symbol = this match
488+
case self: FreshCap => ccOwner.enclosingMethodOrClass
489+
case _ =>
490+
val vis = ccOwner
491+
if vis.is(Param) then vis.owner else vis
492+
487493
/** The symbol that represents the level closest-enclosing ccOwner.
488494
* Symbols representing levels are
489495
* - class symbols, but not inner (non-static) module classes

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ object CaptureSet:
660660

661661
/** If true emit info when var with id debugTarget is created or gets a new element */
662662
inline val debugVars = false
663-
inline val debugTarget = 22
663+
inline val debugTarget = 1745
664664

665665
/** The subclass of captureset variables with given initial elements */
666666
class Var(initialOwner: Symbol = NoSymbol, initialElems: Refs = emptyRefs, val level: Level = undefinedLevel, underBox: Boolean = false)(using /*@constructorOnly*/ ictx: Context) extends CaptureSet:
@@ -690,6 +690,7 @@ object CaptureSet:
690690

691691
if debugVars && id == debugTarget then
692692
println(i"###INIT ELEMS of $id to $initialElems")
693+
assert(false)
693694

694695
def elems: Refs = myElems
695696
def elems_=(refs: Refs): Unit =
@@ -832,7 +833,7 @@ object CaptureSet:
832833
case _ => foldOver(b, t)
833834
find(false, binder)
834835

835-
def levelOK(elem: Capability)(using Context): Boolean = elem match
836+
def levelOKold(elem: Capability)(using Context): Boolean = elem match
836837
case _: FreshCap =>
837838
!level.isDefined
838839
|| ccState.symLevel(elem.ccOwner) <= level
@@ -865,6 +866,29 @@ object CaptureSet:
865866
case _ =>
866867
true
867868

869+
def levelOKnew(elem: Capability)(using Context): Boolean = elem match
870+
case elem @ ResultCap(binder) =>
871+
rootLimit == null && (this.isInstanceOf[BiMapped] || isPartOf(binder.resType))
872+
case GlobalCap =>
873+
rootLimit == null
874+
case elem: ParamRef =>
875+
this.isInstanceOf[BiMapped] || isPartOf(elem.binder.resType)
876+
case _ =>
877+
if owner.exists then
878+
val elemVis = elem.visibility
879+
!elemVis.isProperlyContainedIn(owner)
880+
else true
881+
882+
inline val debugLevelChecking = false
883+
884+
def levelOK(elem: Capability)(using Context): Boolean =
885+
val now = levelOKnew(elem)
886+
if debugLevelChecking then
887+
val was = levelOKold(elem)
888+
if was != now then
889+
println(i"LEVEL DIFF for $this / $getClass in ${owner.ownersIterator.toList} and elem $elem in ${elem.ccOwner}, was $was")
890+
now
891+
868892
def addDependent(cs: CaptureSet)(using Context, VarState): Boolean =
869893
(cs eq this)
870894
|| cs.isUniversal

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

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import reporting.{trace, Message, OverrideError}
2828
import Annotations.Annotation
2929
import Capabilities.*
3030
import util.common.alwaysTrue
31+
import scala.annotation.constructorOnly
3132

3233
/** The capture checker */
3334
object CheckCaptures:
@@ -56,7 +57,13 @@ object CheckCaptures:
5657
kind: EnvKind,
5758
captured: CaptureSet,
5859
outer0: Env | Null,
59-
nestedClosure: Symbol = NoSymbol):
60+
nestedClosure: Symbol = NoSymbol)(using @constructorOnly ictx: Context):
61+
62+
assert(definesEnv(owner))
63+
captured match
64+
case captured: CaptureSet.Var => assert(captured.owner == owner,
65+
i"owner discrepancy env owner = $owner but its captureset $captured has owner ${captured.owner}")
66+
case _ =>
6067

6168
def outer = outer0.nn
6269

@@ -71,6 +78,9 @@ object CheckCaptures:
7178
res
7279
end Env
7380

81+
def definesEnv(sym: Symbol)(using Context): Boolean =
82+
sym.is(Method) || sym.isClass
83+
7484
/** Similar normal substParams, but this is an approximating type map that
7585
* maps parameters in contravariant capture sets to the empty set.
7686
*/
@@ -364,21 +374,28 @@ class CheckCaptures extends Recheck, SymTransformer:
364374
assert(cs1.subCaptures(cs2), i"$cs1 is not a subset of $cs2")
365375

366376
/** If `res` is not CompareResult.OK, report an error */
367-
def checkOK(res: TypeComparer.CompareResult, prefix: => String, added: Capability | CaptureSet, target: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context): Unit =
377+
def checkOK(res: TypeComparer.CompareResult,
378+
prefix: => String,
379+
added: Capability | CaptureSet,
380+
target: CaptureSet,
381+
pos: SrcPos,
382+
provenance: => String = "")(using Context): Unit =
368383
res match
369384
case TypeComparer.CompareResult.Fail(notes) =>
370-
val ((res: IncludeFailure) :: Nil, otherNotes) =
371-
notes.partition(_.isInstanceOf[IncludeFailure]): @unchecked
385+
val (includeFailures, otherNotes) = notes.partition(_.isInstanceOf[IncludeFailure])
386+
val realTarget = includeFailures match
387+
case (fail: IncludeFailure) :: _ => fail.cs
388+
case _ => target
372389
def msg(provisional: Boolean) =
373390
def toAdd: String = errorNotes(otherNotes).toAdd.mkString
374391
def descr: String =
375-
val d = res.cs.description
392+
val d = realTarget.description
376393
if d.isEmpty then provenance else ""
377394
def kind = if provisional then "previously estimated\n" else "allowed "
378-
em"$prefix included in the ${kind}capture set ${res.cs}$descr$toAdd"
395+
em"$prefix included in the ${kind}capture set $realTarget$descr$toAdd"
379396
target match
380397
case target: CaptureSet.Var
381-
if res.cs.isProvisionallySolved =>
398+
if realTarget.isProvisionallySolved =>
382399
report.warning(
383400
msg(provisional = true)
384401
.prepend(i"Another capture checking run needs to be scheduled because\n"),
@@ -413,7 +430,7 @@ class CheckCaptures extends Recheck, SymTransformer:
413430
def capturedVars(sym: Symbol)(using Context): CaptureSet =
414431
myCapturedVars.getOrElseUpdate(sym,
415432
if sym.isTerm || !sym.owner.isStaticOwner
416-
then CaptureSet.Var(sym.owner, level = ccState.symLevel(sym))
433+
then CaptureSet.Var(sym, level = ccState.symLevel(sym))
417434
else CaptureSet.empty)
418435

419436
// ---- Record Uses with MarkFree ----------------------------------------------------
@@ -535,6 +552,7 @@ class CheckCaptures extends Recheck, SymTransformer:
535552
then avoidLocalCapability(c, env, lastEnv)
536553
else avoidLocalReachCapability(c, env)
537554
isVisible
555+
//println(i"Include call or box capture $included from $cs in ${env.owner}/${env.captured}/${env.captured.owner}/${env.kind}")
538556
checkSubset(included, env.captured, tree.srcPos, provenance(env))
539557
capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}")
540558
if !isOfNestedMethod(env) then
@@ -1151,11 +1169,13 @@ class CheckCaptures extends Recheck, SymTransformer:
11511169
.map(e => (e.owner, e))
11521170
.toMap
11531171
def restoreEnvFor(sym: Symbol): Env =
1154-
val localSet = capturedVars(sym)
1155-
if localSet eq CaptureSet.empty then rootEnv
1156-
else envForOwner.get(sym) match
1157-
case Some(e) => e
1158-
case None => Env(sym, EnvKind.Regular, localSet, restoreEnvFor(sym.owner))
1172+
if definesEnv(sym) then
1173+
val localSet = capturedVars(sym)
1174+
if localSet eq CaptureSet.empty then rootEnv
1175+
else envForOwner.get(sym) match
1176+
case Some(e) => e
1177+
case None => Env(sym, EnvKind.Regular, localSet, restoreEnvFor(sym.owner))
1178+
else restoreEnvFor(sym.owner)
11591179
curEnv = restoreEnvFor(sym.owner)
11601180
capt.println(i"Complete $sym in ${curEnv.outersIterator.toList.map(_.owner)}")
11611181
try recheckDef(tree, sym)

tests/neg-custom-args/captures/boundary.check

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
| cap is a fresh root capability classified as Control created in value local when constructing Capability instance scala.util.boundary.Label[Object^'s1]
1313
| cap² is the universal root capability
1414
6 | boundary[Unit]: l2 ?=>
15-
7 | boundary.break(l2)(using l1)
15+
7 | boundary.break(l2)(using l1) // error
1616
8 | ???
1717
|--------------------------------------------------------------------------------------------------------------------
1818
|Inline stack trace
@@ -23,6 +23,21 @@
2323
--------------------------------------------------------------------------------------------------------------------
2424
|
2525
| longer explanation available when compiling with `-explain`
26+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/boundary.scala:7:33 --------------------------------------
27+
7 | boundary.break(l2)(using l1) // error
28+
| ^^
29+
|Found: (local : scala.util.boundary.Label[Object^])
30+
|Required: scala.util.boundary.Label[scala.util.boundary.Label[Unit]^²]^³
31+
|
32+
|Note that capability cap is not included in capture set {cap²}.
33+
|
34+
|where: ^ refers to the universal root capability
35+
| ^² refers to a fresh root capability classified as Control in the type of value local²
36+
| ^³ refers to a fresh root capability classified as Control created in package <empty> when checking argument to parameter label of method break
37+
| cap is a fresh root capability classified as Control in the type of value local
38+
| cap² is a fresh root capability classified as Control created in package <empty> when checking argument to parameter label of method break
39+
|
40+
| longer explanation available when compiling with `-explain`
2641
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/boundary.scala:5:4 ---------------------------------------
2742
4 | boundary[AnyRef^]:
2843
5 | l1 ?=> // error // error
@@ -37,7 +52,7 @@
3752
| cap is a fresh root capability created in package <empty>
3853
| cap² is the universal root capability
3954
6 | boundary[Unit]: l2 ?=>
40-
7 | boundary.break(l2)(using l1)
55+
7 | boundary.break(l2)(using l1) // error
4156
8 | ???
4257
|--------------------------------------------------------------------------------------------------------------------
4358
|Inline stack trace

tests/neg-custom-args/captures/boundary.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ object test:
44
boundary[AnyRef^]:
55
l1 ?=> // error // error
66
boundary[Unit]: l2 ?=>
7-
boundary.break(l2)(using l1)
7+
boundary.break(l2)(using l1) // error
88
???

tests/neg-custom-args/captures/branding.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ def main() =
2222
val untrustedLogger: Logger^ = ???
2323
val untrustedChannel: Channel[String]^ = ???
2424

25-
runSecure: () =>
25+
runSecure: () => // error (???, arose after changing level checking)
2626
trustedLogger.log("Hello from trusted code") // ok
2727

28-
runSecure: () =>
28+
runSecure: () => // error (???, arose after changing level checking)
2929
trustedChannel.send("I can send")
3030
trustedLogger.log(trustedChannel.recv()) // ok
3131

32-
runSecure: () =>
32+
runSecure: () => // error (???, arose after changing level checking)
3333
"I am pure" // ok
3434

3535
runSecure: () => // error

tests/neg-custom-args/captures/capset-members4.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ def test =
1111
type C^ >: {z,x} <: {x,y,z}
1212

1313
val foo: Foo = ???
14-
onlyWithZ[{foo.C}]
15-
onlyWithZ[{z}]
16-
onlyWithZ[{x,z}]
17-
onlyWithZ[{x,y,z}]
18-
onlyWithZ[{x,y}] // error
14+
onlyWithZ[{foo.C}] // error
15+
onlyWithZ[{z}] // error
16+
onlyWithZ[{x,z}] // error
17+
onlyWithZ[{x,y,z}] // error
18+
onlyWithZ[{x,y}] // error

tests/neg-custom-args/captures/i15772.check

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
-- Error: tests/neg-custom-args/captures/i15772.scala:22:46 ------------------------------------------------------------
22
22 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error
33
| ^^^^^^^
4-
|C^ => Unit cannot be box-converted to C{val arg: C^²}^{c} ->{cap, c} Unit
5-
|since the additional capture set {c} resulting from box conversion is not allowed in C{val arg: C^²}^{c} => Unit
4+
|C^ => Unit cannot be box-converted to C{val arg: C^²}^{c} ->{cap, x} Unit
5+
|since the additional capture set {x} resulting from box conversion is not allowed in C{val arg: C^²}^{c} => Unit
66
|
77
|where: => refers to the universal root capability
88
| ^ refers to the universal root capability
@@ -11,8 +11,8 @@
1111
-- Error: tests/neg-custom-args/captures/i15772.scala:29:35 ------------------------------------------------------------
1212
29 | val boxed2 : Observe[C^] = box2(c) // error
1313
| ^^^^^^^
14-
|C^ => Unit cannot be box-converted to C{val arg: C^²}^{c} ->{cap, c} Unit
15-
|since the additional capture set {c} resulting from box conversion is not allowed in C{val arg: C^²}^{c} => Unit
14+
|C^ => Unit cannot be box-converted to C{val arg: C^²}^{c} ->{cap, x} Unit
15+
|since the additional capture set {x} resulting from box conversion is not allowed in C{val arg: C^²}^{c} => Unit
1616
|
1717
|where: => refers to the universal root capability
1818
| ^ refers to the universal root capability

tests/neg-custom-args/captures/i15922.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ def withCap[X](op: (Cap^) => X): X = {
1313
def leaking(c: Cap^): Id[Cap^{c}] = mkId(c)
1414

1515
def test =
16-
val ll = (c: Cap^) => leaking(c)
17-
val bad1 = withCap(ll) // error
16+
val ll = (c: Cap^) => leaking(c) // error
17+
val bad1 = withCap(ll)
1818
val bad2 = withCap(leaking) // error
Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:12:33 ---------------------------------------
2-
12 | files.map((f: F) => new Logger(f)) // error, Q: can we make this pass (see #19076)?
2+
12 | files.map((f: F) => new Logger(f)) // error // error, Q: can we make this pass (see #19076)?
33
| ^
44
|Found: (f : F)
55
|Required: File^
@@ -11,16 +11,25 @@
1111
| cap² is a fresh root capability classified as SharedCapability created in anonymous function of type (f²: F): Logger when checking argument to parameter f of constructor Logger
1212
|
1313
| longer explanation available when compiling with `-explain`
14+
-- Error: tests/neg-custom-args/captures/i21614.scala:12:8 -------------------------------------------------------------
15+
12 | files.map((f: F) => new Logger(f)) // error // error, Q: can we make this pass (see #19076)?
16+
| ^^^^^^^^^
17+
|Type variable U of method map cannot be instantiated to Logger{val f: File^'s1}^{cap.rd} since
18+
|that type captures the root capability `cap`.
19+
|This is often caused by a local capability in an argument of method map
20+
|leaking as part of its result.
21+
|
22+
|where: cap is a root capability associated with the result type of (f²: F^{files*}): Logger{val f: File^'s1}^'s2
1423
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i21614.scala:15:12 ---------------------------------------
1524
15 | files.map(new Logger(_)) // error, Q: can we improve the error message?
1625
| ^^^^^^^^^^^^^
17-
|Found: (_$1: File^'s1) ->{C} Logger{val f: File^{_$1}}^{cap.rd, _$1}
18-
|Required: File^{C} => Logger{val f: File^'s2}^'s3
26+
|Found: (_$1: File^'s3) ->{C} Logger{val f: File^{_$1}}^{cap.rd, _$1}
27+
|Required: File^{C} => Logger{val f: File^'s4}^'s5
1928
|
2029
|Note that capability C is not classified as trait SharedCapability, therefore it
21-
|cannot be included in capture set 's1 of parameter _$1 of SharedCapability elements.
30+
|cannot be included in capture set 's3 of parameter _$1 of SharedCapability elements.
2231
|
2332
|where: => refers to a fresh root capability created in method mkLoggers2 when checking argument to parameter f of method map
24-
| cap is a root capability associated with the result type of (_$1: File^'s1): Logger{val f: File^{_$1}}^{cap.rd, _$1}
33+
| cap is a root capability associated with the result type of (_$1: File^'s3): Logger{val f: File^{_$1}}^{cap.rd, _$1}
2534
|
2635
| longer explanation available when compiling with `-explain`

0 commit comments

Comments
 (0)