Skip to content

Commit 2c44028

Browse files
committed
Rely on hidden sets for use checking
Two changes: 1. We don't distinguish anymore according to separation checking in markFree. We use the same logic regardless of this option. This improves expressiveness because we now admit local reach capabilities that do not refer to parameters. Previously, these were only admitted under separation checking, which is a severe loss of expressiveness. 2. We do trace hidden sets to check that a local reach capability does not refer indirectly through a fresh to a parameter reach. This has to be done after regular capture checking, since hidden sets need to be completely known. This fixes a soundness hole that would have opened once we add `cap.only[Sharable]` capabilities which escape separation checking. Fixes #23579
1 parent af36d6e commit 2c44028

18 files changed

+299
-123
lines changed

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

Lines changed: 92 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import reporting.{trace, Message, OverrideError}
2626
import Annotations.Annotation
2727
import Capabilities.*
2828
import dotty.tools.dotc.cc.CaptureSet.MutAdaptFailure
29+
import dotty.tools.dotc.util.common.alwaysTrue
2930

3031
/** The capture checker */
3132
object CheckCaptures:
@@ -253,8 +254,12 @@ class CheckCaptures extends Recheck, SymTransformer:
253254
*/
254255
private val sepCheckFormals = util.EqHashMap[Tree, Type]()
255256

256-
/** The references used at identifier or application trees */
257-
private val usedSet = util.EqHashMap[Tree, CaptureSet]()
257+
/** The references used at identifier or application trees, including the
258+
* environment at the reference point.
259+
*/
260+
private val useInfos = mutable.ArrayBuffer[(Tree, CaptureSet, Env)]()
261+
262+
private var usedSet = util.EqHashMap[Tree, CaptureSet]()
258263

259264
/** The set of symbols that were rechecked via a completer */
260265
private val completed = new mutable.HashSet[Symbol]
@@ -273,7 +278,7 @@ class CheckCaptures extends Recheck, SymTransformer:
273278
extension [T <: Tree](tree: T)
274279
def needsSepCheck: Boolean = sepCheckFormals.contains(tree)
275280
def formalType: Type = sepCheckFormals.getOrElse(tree, NoType)
276-
def markedFree = usedSet.getOrElse(tree, CaptureSet.empty)
281+
def markedFree: CaptureSet = usedSet.getOrElse(tree, CaptureSet.empty)
277282

278283
/** Instantiate capture set variables appearing contra-variantly to their
279284
* upper approximation.
@@ -462,28 +467,17 @@ class CheckCaptures extends Recheck, SymTransformer:
462467
!sym.isContainedIn(env.owner)
463468
}
464469

465-
/** If capability `c` refers to a parameter that is not @use declared, report an error.
466-
* Exception under deferredReaches: If use comes from a nested closure, accept it.
467-
*/
468-
def checkUseDeclared(c: Capability, env: Env, lastEnv: Env | Null) =
469-
if lastEnv != null && env.nestedClosure.exists && env.nestedClosure == lastEnv.owner then
470-
assert(ccConfig.deferredReaches) // access is from a nested closure under deferredReaches, so it's OK
471-
else c.paramPathRoot match
472-
case ref: NamedType if !ref.symbol.isUseParam =>
473-
val what = if ref.isType then "Capture set parameter" else "Local reach capability"
474-
report.error(
475-
em"""$what $c leaks into capture scope of ${env.ownerString}.
476-
|To allow this, the ${ref.symbol} should be declared with a @use annotation""", tree.srcPos)
477-
case _ =>
478-
479470
/** Avoid locally defined capability by charging the underlying type
480471
* (which may not be cap). This scheme applies only under the deferredReaches setting.
481472
*/
482473
def avoidLocalCapability(c: Capability, env: Env, lastEnv: Env | Null): Unit =
483474
if c.isParamPath then
484475
c match
485476
case Reach(_) | _: TypeRef =>
486-
checkUseDeclared(c, env, lastEnv)
477+
val accessFromNestedClosure =
478+
lastEnv != null && env.nestedClosure.exists && env.nestedClosure == lastEnv.owner
479+
if !accessFromNestedClosure then
480+
checkUseDeclared(c, tree.srcPos)
487481
case _ =>
488482
else
489483
val underlying = c match
@@ -501,32 +495,22 @@ class CheckCaptures extends Recheck, SymTransformer:
501495
* parameter. This is the default.
502496
*/
503497
def avoidLocalReachCapability(c: Capability, env: Env): Unit = c match
504-
case Reach(c1) =>
505-
if c1.isParamPath then
506-
checkUseDeclared(c, env, null)
507-
else
508-
// When a reach capabilty x* where `x` is not a parameter goes out
509-
// of scope, we need to continue with `x`'s underlying deep capture set.
510-
// It is an error if that set contains cap.
511-
// The same is not an issue for normal capabilities since in a local
512-
// definition `val x = e`, the capabilities of `e` have already been charged.
513-
// Note: It's not true that the underlying capture set of a reach capability
514-
// is always cap. Reach capabilities over paths depend on the prefix, which
515-
// might turn a cap into something else.
516-
// The path-use.scala neg test contains an example.
517-
val underlying = CaptureSet.ofTypeDeeply(c1.widen)
518-
capt.println(i"Widen reach $c to $underlying in ${env.owner}")
519-
if sepChecksEnabled then
520-
recur(underlying.filter(!_.isTerminalCapability), env, null)
521-
// we don't want to disallow underlying Fresh instances, since these are typically locally created
522-
// fresh capabilities. We don't need to also follow the hidden set since separation
523-
// checking makes ure that locally hidden references need to go to @consume parameters.
524-
else
525-
underlying.disallowRootCapability(ctx.owner): () =>
526-
report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", tree.srcPos)
527-
recur(underlying, env, null)
528-
case c: TypeRef if c.isParamPath =>
529-
checkUseDeclared(c, env, null)
498+
case Reach(c1) if !c1.isParamPath =>
499+
// Parameter reaches are rejected in checkEscapingUses.
500+
// When a reach capabilty x* where `x` is not a parameter goes out
501+
// of scope, we need to continue with `x`'s underlying deep capture set.
502+
// It is an error if that set contains cap.
503+
// The same is not an issue for normal capabilities since in a local
504+
// definition `val x = e`, the capabilities of `e` have already been charged.
505+
// Note: It's not true that the underlying capture set of a reach capability
506+
// is always cap. Reach capabilities over paths depend on the prefix, which
507+
// might turn a cap into something else.
508+
// The path-use.scala neg test contains an example.
509+
val underlying = CaptureSet.ofTypeDeeply(c1.widen)
510+
capt.println(i"Widen reach $c to $underlying in ${env.owner}")
511+
recur(underlying.filter(!_.isTerminalCapability), env, null)
512+
// we don't want to disallow underlying Fresh instances, since these are typically locally created
513+
// fresh capabilities. We do check that they hide no parameter reach caps in checkEscapingUses
530514
case _ =>
531515

532516
def recur(cs: CaptureSet, env: Env, lastEnv: Env | Null): Unit =
@@ -542,15 +526,28 @@ class CheckCaptures extends Recheck, SymTransformer:
542526
isVisible
543527
checkSubset(included, env.captured, tree.srcPos, provenance(env))
544528
capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}")
545-
if !isOfNestedMethod(env) || true then
529+
if !isOfNestedMethod(env) then
546530
recur(included, nextEnvToCharge(env, !_.owner.isStaticOwner), env)
547-
// Don't propagate out of methods inside terms. The use set of these methods
548-
// will be charged when that method is called.
531+
// Under deferredReaches, don't propagate out of methods inside terms.
532+
// The use set of these methods will be charged when that method is called.
549533

550534
recur(cs, curEnv, null)
551-
usedSet(tree) = tree.markedFree ++ cs
535+
useInfos += ((tree, cs, curEnv))
552536
end markFree
553537

538+
/** If capability `c` refers to a parameter that is not @use declared, report an error.
539+
*/
540+
def checkUseDeclared(c: Capability, pos: SrcPos)(using Context): Unit =
541+
c.paramPathRoot match
542+
case ref: NamedType if !ref.symbol.isUseParam =>
543+
val what = if ref.isType then "Capture set parameter" else "Local reach capability"
544+
val owner = ref.symbol.owner
545+
val ownerStr = if owner.isAnonymousFunction then "enclosing function" else owner.show
546+
report.error(
547+
em"""$what $c leaks into capture scope of $ownerStr.
548+
|To allow this, the ${ref.symbol} should be declared with a @use annotation""", pos)
549+
case _ =>
550+
554551
/** Include references captured by the called method in the current environment stack */
555552
def includeCallCaptures(sym: Symbol, resType: Type, tree: Tree)(using Context): Unit = resType match
556553
case _: MethodOrPoly => // wait until method is fully applied
@@ -1992,6 +1989,48 @@ class CheckCaptures extends Recheck, SymTransformer:
19921989
traverseChildren(t)
19931990
check.traverse(tp)
19941991

1992+
/** Check that no uses refer to reach capabilities of parameters of enclosing
1993+
* methods or classes.
1994+
*/
1995+
def checkEscapingUses()(using Context) =
1996+
for (tree, uses, env) <- useInfos do
1997+
val seen = util.EqHashSet[Capability]()
1998+
1999+
// The owner of the innermost environment of kind Boxed
2000+
def boxedOwner(env: Env): Symbol =
2001+
if env.kind == EnvKind.Boxed then env.owner
2002+
else if isOfNestedMethod(env) then env.owner.owner
2003+
else if env.owner.isStaticOwner then NoSymbol
2004+
else boxedOwner(nextEnvToCharge(env, alwaysTrue))
2005+
2006+
def checkUseUnlessBoxed(c: Capability, croot: NamedType) =
2007+
if !boxedOwner(env).isContainedIn(croot.symbol.owner) then
2008+
checkUseDeclared(c, tree.srcPos)
2009+
2010+
def check(cs: CaptureSet): Unit = cs.elems.foreach(checkElem)
2011+
2012+
def checkElem(c: Capability): Unit =
2013+
if !seen.contains(c) then
2014+
seen += c
2015+
c match
2016+
case Reach(c1) =>
2017+
c1.paramPathRoot match
2018+
case croot: NamedType => checkUseUnlessBoxed(c, croot)
2019+
case _ => check(CaptureSet.ofTypeDeeply(c1.widen))
2020+
case c: TypeRef =>
2021+
c.paramPathRoot match
2022+
case croot: NamedType => checkUseUnlessBoxed(c, croot)
2023+
case _ =>
2024+
case c: DerivedCapability =>
2025+
checkElem(c.underlying)
2026+
case c: FreshCap =>
2027+
check(c.hiddenSet)
2028+
case _ =>
2029+
2030+
check(uses)
2031+
end for
2032+
end checkEscapingUses
2033+
19952034
/** Check that arguments of TypeApplys and AppliedTypes conform to their bounds.
19962035
*/
19972036
def postCheck(unit: tpd.Tree)(using Context): Unit =
@@ -2020,7 +2059,11 @@ class CheckCaptures extends Recheck, SymTransformer:
20202059
end checker
20212060

20222061
checker.traverse(unit)(using ctx.withOwner(defn.RootClass))
2023-
if sepChecksEnabled then SepCheck(this).traverse(unit)
2062+
checkEscapingUses()
2063+
if sepChecksEnabled then
2064+
for (tree, cs, env) <- useInfos do
2065+
usedSet(tree) = tree.markedFree ++ cs
2066+
SepCheck(this).traverse(unit)
20242067
if !ctx.reporter.errorsReported then
20252068
// We dont report errors here if previous errors were reported, because other
20262069
// errors often result in bad applied types, but flagging these bad types gives
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
-- Error: tests/neg-custom-args/captures/dcs-tvar.scala:6:15 -----------------------------------------------------------
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/dcs-tvar.scala:6:2 ---------------------------------------
22
6 | () => runOps(xs) // error
3-
| ^^
4-
| Local reach capability xs* leaks into capture scope of method f.
5-
| To allow this, the parameter xs should be declared with a @use annotation
6-
-- Error: tests/neg-custom-args/captures/dcs-tvar.scala:9:15 -----------------------------------------------------------
3+
| ^^^^^^^^^^^^^^^^
4+
| Found: () ->{xs*} Unit
5+
| Required: () -> Unit
6+
|
7+
| longer explanation available when compiling with `-explain`
8+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/dcs-tvar.scala:9:2 ---------------------------------------
79
9 | () => runOps(xs) // error
8-
| ^^
9-
| Local reach capability xs* leaks into capture scope of method g.
10-
| To allow this, the parameter xs should be declared with a @use annotation
10+
| ^^^^^^^^^^^^^^^^
11+
| Found: () ->{xs*} Unit
12+
| Required: () -> Unit
13+
|
14+
| longer explanation available when compiling with `-explain`

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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
-- Error: tests/neg-custom-args/captures/delayedRunops.scala:17:13 -----------------------------------------------------
2-
17 | runOps(ops1) // error
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/delayedRunops.scala:15:4 ---------------------------------
2+
15 | () => // error
3+
| ^
4+
| Found: () ->{ops*} Unit
5+
| Required: () -> Unit
6+
16 | val ops1 = ops
7+
17 | runOps(ops1)
8+
|
9+
| longer explanation available when compiling with `-explain`
10+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/delayedRunops.scala:27:4 ---------------------------------
11+
27 | () => // error
12+
| ^
13+
| Found: () ->{ops*} Unit
14+
| Required: () -> Unit
15+
28 | val ops1: List[() ->{ops*} Unit] = ops
16+
29 | runOps(ops1)
17+
|
18+
| longer explanation available when compiling with `-explain`
19+
-- Error: tests/neg-custom-args/captures/delayedRunops.scala:23:13 -----------------------------------------------------
20+
23 | runOps(ops1) // error
321
| ^^^^
4-
| Local reach capability ops* leaks into capture scope of method delayedRunOps1.
5-
| To allow this, the parameter ops should be declared with a @use annotation
6-
-- Error: tests/neg-custom-args/captures/delayedRunops.scala:29:13 -----------------------------------------------------
7-
29 | runOps(ops1) // error
8-
| ^^^^
9-
| Local reach capability ops* leaks into capture scope of method delayedRunOps3.
22+
| Local reach capability ops* leaks into capture scope of method delayedRunOps2.
1023
| To allow this, the parameter ops should be declared with a @use annotation
1124
-- Error: tests/neg-custom-args/captures/delayedRunops.scala:22:16 -----------------------------------------------------
1225
22 | val ops1: List[() => Unit] = ops // error

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ import caps.{use, consume}
1212

1313
// unsound: impure operation pretended pure
1414
def delayedRunOps1(ops: List[() => Unit]): () ->{} Unit =
15-
() =>
15+
() => // error
1616
val ops1 = ops
17-
runOps(ops1) // error
17+
runOps(ops1)
1818

1919
// unsound: impure operation pretended pure
2020
def delayedRunOps2(@consume ops: List[() => Unit]): () ->{} Unit =
2121
() =>
2222
val ops1: List[() => Unit] = ops // error
23-
runOps(ops1) // was error
23+
runOps(ops1) // error
2424

2525
// unsound: impure operation pretended pure
2626
def delayedRunOps3(ops: List[() => Unit]): () ->{} Unit =
27-
() =>
27+
() => // error
2828
val ops1: List[() ->{ops*} Unit] = ops
29-
runOps(ops1) // error
29+
runOps(ops1)

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
| ^^^^^^^
44
| Local reach capability x.unbox* leaks into capture scope of method foo.
55
| To allow this, the parameter x should be declared with a @use annotation
6+
-- Error: tests/neg-custom-args/captures/i21442.scala:18:14 ------------------------------------------------------------
7+
18 | val io = x1.unbox // error
8+
| ^^^^^^^^
9+
| Local reach capability x* leaks into capture scope of method bar.
10+
| To allow this, the parameter x should be declared with a @use annotation
611
-- Error: tests/neg-custom-args/captures/i21442.scala:17:10 ------------------------------------------------------------
712
17 | val x1: Boxed[IO^] = x // error
813
| ^^^^^^^^^^

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ def foo(x: Boxed[IO^]): Unit =
1515
// But, no type error reported.
1616
def bar(x: Boxed[IO^]): Unit =
1717
val x1: Boxed[IO^] = x // error
18-
val io = x1.unbox // was error
18+
val io = x1.unbox // error
1919
io.use()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- Error: tests/neg-custom-args/captures/localReaches.scala:24:30 ------------------------------------------------------
2+
24 | var x: () ->{xs*} Unit = ys.head // error
3+
| ^^^^^^^
4+
| Local reach capability ops* leaks into capture scope of method localReach3.
5+
| To allow this, the parameter ops should be declared with a @use annotation
6+
-- Error: tests/neg-custom-args/captures/localReaches.scala:27:11 ------------------------------------------------------
7+
27 | x = ys.head // error
8+
| ^^^^^^^
9+
| Local reach capability ops* leaks into capture scope of method localReach3.
10+
| To allow this, the parameter ops should be declared with a @use annotation
11+
-- Error: tests/neg-custom-args/captures/localReaches.scala:14:10 ------------------------------------------------------
12+
14 | val xs: List[() => Unit] = op :: Nil // error
13+
| ^^^^^^^^^^^^^^^^
14+
| Separation failure: value xs's type List[() => Unit] hides parameter op.
15+
| The parameter needs to be annotated with @consume to allow this.
16+
-- Error: tests/neg-custom-args/captures/localReaches.scala:22:10 ------------------------------------------------------
17+
22 | val xs: List[() => Unit] = ops // error
18+
| ^^^^^^^^^^^^^^^^
19+
| Separation failure: value xs's type List[() => Unit] hides parameter ops.
20+
| The parameter needs to be annotated with @consume to allow this.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import language.experimental.captureChecking
2+
// no separation checking
3+
import caps.consume
4+
5+
def localReach() =
6+
val xs: List[() => Unit] = ???
7+
var ys: List[() ->{xs*} Unit] = xs
8+
var x: () ->{xs*} Unit = ys.head
9+
while ys.nonEmpty do
10+
ys = ys.tail
11+
x = ys.head
12+
13+
def localReach2(op: () => Unit) =
14+
val xs: List[() => Unit] = op :: Nil // error
15+
var ys: List[() ->{xs*} Unit] = xs
16+
var x: () ->{xs*} Unit = ys.head
17+
while ys.nonEmpty do
18+
ys = ys.tail
19+
x = ys.head
20+
21+
def localReach3(ops: List[() => Unit]) =
22+
val xs: List[() => Unit] = ops // error
23+
var ys: List[() ->{xs*} Unit] = xs
24+
var x: () ->{xs*} Unit = ys.head // error
25+
while ys.nonEmpty do
26+
ys = ys.tail
27+
x = ys.head // error
28+

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,16 @@
9090
72 | f1
9191
|
9292
| longer explanation available when compiling with `-explain`
93-
-- Error: tests/neg-custom-args/captures/reaches.scala:88:10 -----------------------------------------------------------
93+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:88:10 --------------------------------------
9494
88 | ps.map((x, y) => compose1(x, y)) // error
95-
| ^
96-
| Local reach capability ps* leaks into capture scope of method mapCompose.
97-
| To allow this, the parameter ps should be declared with a @use annotation
95+
| ^^^^^^^^^^^^^^^^^^^^^^^
96+
|Found: (x$1: (A^ ->? A^?, A^ ->? A^?)^?) ->? A^? ->? A^?
97+
|Required: ((A ->{ps*} A, A ->{ps*} A)) => A^? ->? A^?
98+
|
99+
|where: => refers to a fresh root capability created in method mapCompose when checking argument to parameter f of method map
100+
| ^ refers to the universal root capability
101+
|
102+
| longer explanation available when compiling with `-explain`
98103
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:91:10 --------------------------------------
99104
91 | ps.map((x, y) => compose1(x, y)) // error
100105
| ^^^^^^^^^^^^^^^^^^^^^^^
@@ -105,3 +110,8 @@
105110
| ^ refers to the universal root capability
106111
|
107112
| longer explanation available when compiling with `-explain`
113+
-- Error: tests/neg-custom-args/captures/reaches.scala:39:31 -----------------------------------------------------------
114+
39 | val next: () => Unit = cur.head // error
115+
| ^^^^^^^^
116+
| Local reach capability xs* leaks into capture scope of method runAll2.
117+
| To allow this, the parameter xs should be declared with a @use annotation

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def runAll1(@use xs: List[Proc]): Unit =
3636
def runAll2(@consume xs: List[Proc]): Unit =
3737
var cur: List[Proc] = xs
3838
while cur.nonEmpty do
39-
val next: () => Unit = cur.head // was error, now OK
39+
val next: () => Unit = cur.head // error
4040
next()
4141
cur = cur.tail
4242

0 commit comments

Comments
 (0)