Skip to content

Commit 68044f0

Browse files
committed
Level-based capture checking for try/catch
1 parent 50419ac commit 68044f0

18 files changed

+192
-134
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ class CCState:
4949
* the reference could not be added to the set due to a level conflict.
5050
*/
5151
var levelError: Option[(CaptureRef, CaptureSet)] = None
52+
53+
/** Under saferExceptions: The <try block> symbol generated for a try.
54+
* Installed by Setup, removed by CheckCaptures.
55+
*/
56+
val tryBlockOwner: mutable.HashMap[Try, Symbol] = new mutable.HashMap
5257
end CCState
5358

5459
/** Property key for capture checking state */

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,15 @@ object CheckCaptures:
163163
* Note: We need to perform the check on the original annotation rather than its
164164
* capture set since the conversion to a capture set already eliminates redundant elements.
165165
*/
166-
def warnIfRedundantCaptureSet(ann: Tree)(using Context): Unit =
166+
def warnIfRedundantCaptureSet(ann: Tree, tpt: Tree)(using Context): Unit =
167167
var retained = retainedElems(ann).toArray
168168
for i <- 0 until retained.length do
169169
val ref = retained(i).toCaptureRef
170170
val others = for j <- 0 until retained.length if j != i yield retained(j).toCaptureRef
171171
val remaining = CaptureSet(others*)
172172
if remaining.accountsFor(ref) then
173-
report.warning(em"redundant capture: $remaining already accounts for $ref", ann.srcPos)
173+
val srcTree = if ann.span.exists then ann else tpt
174+
report.warning(em"redundant capture: $remaining already accounts for $ref", srcTree.srcPos)
174175

175176
/** Report an error if some part of `tp` contains the root capability in its capture set */
176177
def disallowRootCapabilitiesIn(tp: Type, what: String, have: String, addendum: String, pos: SrcPos)(using Context) =
@@ -689,7 +690,14 @@ class CheckCaptures extends Recheck, SymTransformer:
689690
super.recheckTyped(tree)
690691

691692
override def recheckTry(tree: Try, pt: Type)(using Context): Type =
692-
val tp = super.recheckTry(tree, pt)
693+
val tryOwner = ccState.tryBlockOwner.remove(tree).getOrElse(ctx.owner)
694+
val saved = curEnv
695+
curEnv = Env(tryOwner, EnvKind.Regular, CaptureSet.Var(curEnv.owner), curEnv)
696+
val tp = try
697+
inContext(ctx.withOwner(tryOwner)):
698+
super.recheckTry(tree, pt)
699+
finally
700+
curEnv = saved
693701
if allowUniversalInBoxed && Feature.enabled(Feature.saferExceptions) then
694702
disallowRootCapabilitiesIn(tp,
695703
"Result of `try`", "have type",
@@ -1192,7 +1200,7 @@ class CheckCaptures extends Recheck, SymTransformer:
11921200
checkWellformedPost(tp, tree.srcPos)
11931201
tp match
11941202
case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot =>
1195-
warnIfRedundantCaptureSet(annot.tree)
1203+
warnIfRedundantCaptureSet(annot.tree, tree)
11961204
case _ =>
11971205
}
11981206
case t: ValOrDefDef

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import core._
66
import Phases.*, DenotTransformers.*, SymDenotations.*
77
import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.*
88
import Types.*, StdNames.*
9+
import Annotations.Annotation
10+
import config.Feature
911
import config.Printers.capt
1012
import ast.tpd
1113
import transform.Recheck.*
1214
import CaptureSet.IdentityCaptRefMap
1315
import Synthetics.isExcluded
1416
import util.Property
15-
import dotty.tools.dotc.core.Annotations.Annotation
1617

1718
/** A tree traverser that prepares a compilation unit to be capture checked.
1819
* It does the following:
@@ -289,13 +290,27 @@ extends tpd.TreeTraverser:
289290
def inverse = thisMap
290291
end SubstParams
291292

293+
/** If the outer context directly enclosing the definition of `sym`
294+
* has a <try block> owner, that owner, otherwise null.
295+
*/
296+
def newOwnerFor(sym: Symbol)(using Context): Symbol | Null =
297+
var octx = ctx
298+
while octx.owner == sym do octx = octx.outer
299+
if octx.owner.name == nme.TRY_BLOCK then octx.owner else null
300+
292301
/** Update info of `sym` for CheckCaptures phase only */
293302
private def updateInfo(sym: Symbol, info: Type)(using Context) =
294-
sym.updateInfoBetween(preRecheckPhase, thisPhase, info)
303+
sym.updateInfoBetween(preRecheckPhase, thisPhase, info, newOwnerFor(sym))
295304
sym.namedType match
296305
case ref: CaptureRef => ref.invalidateCaches()
297306
case _ =>
298307

308+
/** Update only the owner part fo info if necessary. A symbol should be updated
309+
* only once by either updateInfo or updateOwner.
310+
*/
311+
private def updateOwner(sym: Symbol)(using Context) =
312+
if newOwnerFor(sym) != null then updateInfo(sym, sym.info)
313+
299314
def traverse(tree: Tree)(using Context): Unit =
300315
tree match
301316
case tree: DefDef =>
@@ -367,11 +382,24 @@ extends tpd.TreeTraverser:
367382
case tree: Template =>
368383
inContext(ctx.withOwner(tree.symbol.owner)):
369384
traverseChildren(tree)
385+
case tree: Try if Feature.enabled(Feature.saferExceptions) =>
386+
val tryOwner = newSymbol(ctx.owner, nme.TRY_BLOCK, SyntheticMethod, MethodType(Nil, defn.UnitType))
387+
ccState.tryBlockOwner(tree) = tryOwner
388+
inContext(ctx.withOwner(tryOwner)):
389+
traverseChildren(tree)
370390
case _ =>
371391
traverseChildren(tree)
372392
postProcess(tree)
373393
end traverse
374394

395+
override def apply(x: Unit, trees: List[Tree])(using Context): Unit = trees match
396+
case (imp: Import) :: rest =>
397+
traverse(rest)(using ctx.importContext(imp, imp.symbol))
398+
case tree :: rest =>
399+
traverse(tree)
400+
traverse(rest)
401+
case Nil =>
402+
375403
def postProcess(tree: Tree)(using Context): Unit = tree match
376404
case tree: TypeTree =>
377405
transformTT(tree, boxed = false, exact = false,
@@ -451,6 +479,9 @@ extends tpd.TreeTraverser:
451479
// are checked on depand
452480
denot.info = newInfo
453481
recheckDef(tree, sym))
482+
else updateOwner(sym)
483+
else if !sym.is(Module) then updateOwner(sym) // Modules are updated with their module classes
484+
454485
case tree: Bind =>
455486
val sym = tree.symbol
456487
updateInfo(sym, transformInferredType(sym.info, boxed = false, mapRoots = true))
@@ -482,11 +513,14 @@ extends tpd.TreeTraverser:
482513
val modul = cls.sourceModule
483514
updateInfo(modul, CapturingType(modul.info, newSelfType.captureSet))
484515
modul.termRef.invalidateCaches()
516+
else
517+
updateOwner(cls)
518+
if cls.is(ModuleClass) then updateOwner(cls.sourceModule)
485519
case _ =>
486520
val info = atPhase(preRecheckPhase)(tree.symbol.info)
487521
val newInfo = transformExplicitType(info, boxed = false, mapRoots = !ctx.owner.isStaticOwner)
522+
updateInfo(tree.symbol, newInfo)
488523
if newInfo ne info then
489-
updateInfo(tree.symbol, newInfo)
490524
capt.println(i"update info of ${tree.symbol} from $info to $newInfo")
491525
case _ =>
492526
end postProcess

compiler/src/dotty/tools/dotc/core/StdNames.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ object StdNames {
299299
val SELF: N = "$this"
300300
val SKOLEM: N = "<skolem>"
301301
val TRAIT_CONSTRUCTOR: N = "$init$"
302+
val TRY_BLOCK: N = "<try block>"
302303
val THROWS: N = "$throws"
303304
val U2EVT: N = "u2evt$"
304305
val ALLARGS: N = "$allArgs"

compiler/src/dotty/tools/dotc/transform/Recheck.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ object Recheck:
4949
extension (sym: Symbol)
5050

5151
/** Update symbol's info to newInfo from prevPhase.next to lastPhase.
52-
* Reset to previous info for phases after lastPhase.
52+
* Also update owner to newOwnerOrNull if it is not null.
53+
* Reset to previous info and owner for phases after lastPhase.
5354
*/
54-
def updateInfoBetween(prevPhase: DenotTransformer, lastPhase: DenotTransformer, newInfo: Type)(using Context): Unit =
55-
if sym.info ne newInfo then
55+
def updateInfoBetween(prevPhase: DenotTransformer, lastPhase: DenotTransformer, newInfo: Type, newOwnerOrNull: Symbol | Null = null)(using Context): Unit =
56+
val newOwner = if newOwnerOrNull == null then sym.owner else newOwnerOrNull
57+
if (sym.info ne newInfo) || (sym.owner ne newOwner) then
5658
val flags = sym.flags
5759
sym.copySymDenotation(
5860
initFlags =
@@ -61,6 +63,7 @@ object Recheck:
6163
else flags
6264
).installAfter(lastPhase) // reset
6365
sym.copySymDenotation(
66+
owner = newOwner,
6467
info = newInfo,
6568
initFlags =
6669
if newInfo.isInstanceOf[LazyType] then flags &~ Touched

tests/neg-custom-args/captures/capt-test.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ def raise[E <: Exception](e: E): Nothing throws E = throw e
1414
def foo(x: Boolean): Int throws Fail =
1515
if x then 1 else raise(Fail())
1616

17-
def handle[E <: Exception, sealed R <: Top](op: (CanThrow[E]) => R)(handler: E => R): R =
18-
val x: CanThrow[E] = ???
17+
def handle[E <: Exception, R <: Top](op: (lcap: caps.Root) ?-> (CT[E] @retains(lcap)) => R)(handler: E => R): R =
18+
val x: CT[E] = ???
1919
try op(x)
2020
catch case ex: E => handler(ex)
2121

2222
def test: Unit =
23-
val b = handle[Exception, () => Nothing] { // error
24-
(x: CanThrow[Exception]) => () => raise(new Exception)(using x)
23+
val b = handle[Exception, () => Nothing] {
24+
(x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error
2525
} {
2626
(ex: Exception) => ???
2727
}
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
-- Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:36:2 -----------------------------------------------
2-
36 | try // error
3-
| ^
4-
| Result of `try` cannot have type LazyList[Int]^ since
5-
| that type captures the root capability `cap`.
6-
| This is often caused by a locally generated exception capability leaking as part of its result.
7-
37 | tabulate(10) { i =>
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:37:4 --------------------------
2+
37 | tabulate(10) { i => // error
3+
| ^
4+
| Found: LazyList[Int]^
5+
| Required: LazyList[Int]^?
6+
|
7+
| Note that reference (cap : caps.Root), defined at level 2
8+
| cannot be included in outer capture set ?, defined at level 1 in method problem
89
38 | if i > 9 then throw Ex1()
910
39 | i * i
1011
40 | }
11-
41 | catch case ex: Ex1 => LazyNil
12+
|
13+
| longer explanation available when compiling with `-explain`

tests/neg-custom-args/captures/lazylists-exceptions.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ def tabulate[A](n: Int)(gen: Int => A): LazyList[A]^{gen} =
3333
class Ex1 extends Exception
3434

3535
def problem =
36-
try // error
37-
tabulate(10) { i =>
36+
try
37+
tabulate(10) { i => // error
3838
if i > 9 then throw Ex1()
3939
i * i
4040
}
Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
1-
-- [E129] Potential Issue Warning: tests/neg-custom-args/captures/real-try.scala:30:4 ----------------------------------
2-
30 | b.x
1+
-- [E129] Potential Issue Warning: tests/neg-custom-args/captures/real-try.scala:36:4 ----------------------------------
2+
36 | b.x
33
| ^^^
44
| A pure expression does nothing in statement position; you may be omitting necessary parentheses
55
|
66
| longer explanation available when compiling with `-explain`
7-
-- Error: tests/neg-custom-args/captures/real-try.scala:12:2 -----------------------------------------------------------
8-
12 | try // error
9-
| ^
10-
| Result of `try` cannot have type () => Unit since
11-
| that type captures the root capability `cap`.
12-
| This is often caused by a locally generated exception capability leaking as part of its result.
13-
13 | () => foo(1)
14-
14 | catch
15-
15 | case _: Ex1 => ???
16-
16 | case _: Ex2 => ???
17-
-- Error: tests/neg-custom-args/captures/real-try.scala:18:2 -----------------------------------------------------------
18-
18 | try // error
19-
| ^
20-
| Result of `try` cannot have type () => Cell[Unit]^? since
21-
| that type captures the root capability `cap`.
22-
| This is often caused by a locally generated exception capability leaking as part of its result.
23-
19 | () => Cell(foo(1))
24-
20 | catch
25-
21 | case _: Ex1 => ???
26-
22 | case _: Ex2 => ???
27-
-- Error: tests/neg-custom-args/captures/real-try.scala:24:10 ----------------------------------------------------------
28-
24 | val b = try // error
29-
| ^
30-
| Result of `try` cannot have type Cell[box () => Unit]^? since
31-
| the part box () => Unit of that type captures the root capability `cap`.
32-
| This is often caused by a locally generated exception capability leaking as part of its result.
33-
25 | Cell(() => foo(1))//: Cell[box {ev} () => Unit] <: Cell[box {cap} () => Unit]
34-
26 | catch
35-
27 | case _: Ex1 => ???
36-
28 | case _: Ex2 => ???
7+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/real-try.scala:19:4 --------------------------------------
8+
19 | () => foo(1) // error
9+
| ^^^^^^^^^^^^
10+
| Found: () => Unit
11+
| Required: () ->? Unit
12+
|
13+
| Note that reference (cap : caps.Root), defined at level 2
14+
| cannot be included in outer capture set ?, defined at level 1 in method test
15+
|
16+
| longer explanation available when compiling with `-explain`
17+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/real-try.scala:25:4 --------------------------------------
18+
25 | () => Cell(foo(1)) // error
19+
| ^^^^^^^^^^^^^^^^^^
20+
| Found: () => Cell[Unit]^?
21+
| Required: () ->? Cell[Unit]^?
22+
|
23+
| Note that reference (cap : caps.Root), defined at level 2
24+
| cannot be included in outer capture set ?, defined at level 1 in method test
25+
|
26+
| longer explanation available when compiling with `-explain`
27+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/real-try.scala:31:4 --------------------------------------
28+
31 | Cell(() => foo(1))// // error
29+
| ^^^^^^^^^^^^^^^^^^
30+
| Found: Cell[box () => Unit]^?
31+
| Required: Cell[() ->? Unit]^?
32+
|
33+
| Note that reference (cap : caps.Root), defined at level 2
34+
| cannot be included in outer capture set ?, defined at level 1 in method test
35+
|
36+
| longer explanation available when compiling with `-explain`

tests/neg-custom-args/captures/real-try.scala

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,26 @@ def foo(i: Int): (CanThrow[Ex1], CanThrow[Ex2]) ?-> Unit =
99
class Cell[+T](val x: T)
1010

1111
def test(): Unit =
12-
try // error
13-
() => foo(1)
12+
try
13+
() => foo(1) // no error, since result type is Unit
1414
catch
1515
case _: Ex1 => ???
1616
case _: Ex2 => ???
1717

18-
try // error
19-
() => Cell(foo(1))
18+
val x = try
19+
() => foo(1) // error
2020
catch
2121
case _: Ex1 => ???
2222
case _: Ex2 => ???
2323

24-
val b = try // error
25-
Cell(() => foo(1))//: Cell[box {ev} () => Unit] <: Cell[box {cap} () => Unit]
24+
val y = try
25+
() => Cell(foo(1)) // error
26+
catch
27+
case _: Ex1 => ???
28+
case _: Ex2 => ???
29+
30+
val b = try
31+
Cell(() => foo(1))// // error
2632
catch
2733
case _: Ex1 => ???
2834
case _: Ex2 => ???
Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
-- Error: tests/neg-custom-args/captures/try.scala:23:16 ---------------------------------------------------------------
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:23:49 ------------------------------------------
22
23 | val a = handle[Exception, CanThrow[Exception]] { // error
3-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4-
| Sealed type variable R cannot be instantiated to box CT[Exception]^ since
5-
| that type captures the root capability `cap`.
6-
| This is often caused by a local capability in an argument of method handle
7-
| leaking as part of its result.
3+
| ^
4+
|Found: (lcap: caps.Root) ?->? (x$0: CT[Exception]^{lcap}) ->? box CT[Exception]^{lcap}
5+
|Required: (lcap: caps.Root) ?-> CT[Exception]^{lcap} ->{'cap[..test](from instantiating handle)} box CT[Exception]^
6+
24 | (x: CanThrow[Exception]) => x
7+
25 | }{
8+
|
9+
| longer explanation available when compiling with `-explain`
810
-- Error: tests/neg-custom-args/captures/try.scala:30:65 ---------------------------------------------------------------
911
30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error
1012
| ^
11-
| (x : CanThrow[Exception]) cannot be referenced here; it is not included in the allowed capture set {}
12-
| of an enclosing function literal with expected type () ->? Nothing
13+
| (x : CT[Exception]^{lcap}) cannot be referenced here; it is not included in the allowed capture set {}
14+
| of an enclosing function literal with expected type () ->? Nothing
1315
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:52:2 -------------------------------------------
1416
47 |val global: () -> Int = handle {
1517
48 | (x: CanThrow[Exception]) =>
@@ -18,19 +20,13 @@
1820
51 | 22
1921
52 |} { // error
2022
| ^
21-
| Found: () ->{x$0, x$0²} Int
23+
| Found: () ->{x$0, lcap} Int
2224
| Required: () -> Int
23-
|
24-
| where: x$0 is a reference to a value parameter
25-
| x$0² is a reference to a value parameter
2625
53 | (ex: Exception) => () => 22
2726
54 |}
2827
|
2928
| longer explanation available when compiling with `-explain`
3029
-- Error: tests/neg-custom-args/captures/try.scala:35:11 ---------------------------------------------------------------
3130
35 | val xx = handle { // error
3231
| ^^^^^^
33-
| Sealed type variable R cannot be instantiated to box () => Int since
34-
| that type captures the root capability `cap`.
35-
| This is often caused by a local capability in an argument of method handle
36-
| leaking as part of its result.
32+
| escaping local reference lcap.type

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ def raise[E <: Exception](e: E): Nothing throws E = throw e
1414
def foo(x: Boolean): Int throws Fail =
1515
if x then 1 else raise(Fail())
1616

17-
def handle[E <: Exception, sealed R <: Top](op: CanThrow[E] => R)(handler: E => R): R =
18-
val x: CanThrow[E] = ???
17+
def handle[E <: Exception, R <: Top](op: (lcap: caps.Root) ?-> CT[E]^{lcap} => R)(handler: E => R): R =
18+
val x: CT[E] = ???
1919
try op(x)
2020
catch case ex: E => handler(ex)
2121

0 commit comments

Comments
 (0)