Skip to content

Commit b48d62b

Browse files
authored
Rely on hidden sets for use checking (#23580)
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
2 parents 48e173a + 2c44028 commit b48d62b

19 files changed

+305
-127
lines changed

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

Lines changed: 95 additions & 50 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.
@@ -427,11 +432,13 @@ class CheckCaptures extends Recheck, SymTransformer:
427432
else
428433
i"\nof the enclosing ${owner.showLocated}"
429434

430-
/** Does the given environment belong to a method that is (a) nested in a term
435+
/** Under deferredReaches:
436+
* Does the given environment belong to a method that is (a) nested in a term
431437
* and (b) not the method of an anonymous function?
432438
*/
433439
def isOfNestedMethod(env: Env | Null)(using Context) =
434-
env != null
440+
ccConfig.deferredReaches
441+
&& env != null
435442
&& env.owner.is(Method)
436443
&& env.owner.owner.isTerm
437444
&& !env.owner.isAnonymousFunction
@@ -460,28 +467,17 @@ class CheckCaptures extends Recheck, SymTransformer:
460467
!sym.isContainedIn(env.owner)
461468
}
462469

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

530516
def recur(cs: CaptureSet, env: Env, lastEnv: Env | Null): Unit =
@@ -542,13 +528,26 @@ class CheckCaptures extends Recheck, SymTransformer:
542528
capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}")
543529
if !isOfNestedMethod(env) then
544530
recur(included, nextEnvToCharge(env, !_.owner.isStaticOwner), env)
545-
// Don't propagate out of methods inside terms. The use set of these methods
546-
// 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.
547533

548534
recur(cs, curEnv, null)
549-
usedSet(tree) = tree.markedFree ++ cs
535+
useInfos += ((tree, cs, curEnv))
550536
end markFree
551537

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+
552551
/** Include references captured by the called method in the current environment stack */
553552
def includeCallCaptures(sym: Symbol, resType: Type, tree: Tree)(using Context): Unit = resType match
554553
case _: MethodOrPoly => // wait until method is fully applied
@@ -1990,6 +1989,48 @@ class CheckCaptures extends Recheck, SymTransformer:
19901989
traverseChildren(t)
19911990
check.traverse(tp)
19921991

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+
19932034
/** Check that arguments of TypeApplys and AppliedTypes conform to their bounds.
19942035
*/
19952036
def postCheck(unit: tpd.Tree)(using Context): Unit =
@@ -2018,7 +2059,11 @@ class CheckCaptures extends Recheck, SymTransformer:
20182059
end checker
20192060

20202061
checker.traverse(unit)(using ctx.withOwner(defn.RootClass))
2021-
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)
20222067
if !ctx.reporter.errorsReported then
20232068
// We dont report errors here if previous errors were reported, because other
20242069
// 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/method-uses.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ def test(xs: List[() => Unit]) =
22
xs.head // error
33

44
def foo =
5-
xs.head // ok
5+
xs.head // error, ok under deferredReaches
66
def bar() =
7-
xs.head // ok
7+
xs.head // error, ok under deferredReaches
88

99
class Foo:
1010
println(xs.head) // error, but could be OK
@@ -14,7 +14,7 @@ def test(xs: List[() => Unit]) =
1414
Foo() // OK, but could be error
1515

1616
def test2(xs: List[() => Unit]) =
17-
def foo = xs.head // ok
17+
def foo = xs.head // error, ok under deferredReaches
1818
()
1919

2020
def test3(xs: List[() => Unit]): () ->{xs*} Unit = () =>

0 commit comments

Comments
 (0)