Skip to content

Commit 7db1d43

Browse files
committed
Fix special capture set handling in recheckApply, Step 2 revised
Step 2: Change the logic. The previous one was unsound. The new logic is makes use of the distinction between regular and unboxed parameters.
1 parent d8ace30 commit 7db1d43

File tree

5 files changed

+91
-76
lines changed

5 files changed

+91
-76
lines changed

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

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -547,36 +547,47 @@ class CheckCaptures extends Recheck, SymTransformer:
547547
protected override
548548
def recheckArg(arg: Tree, formal: Type)(using Context): Type =
549549
val argType = recheck(arg, formal)
550-
if unboxedArgs.remove(arg) && ccConfig.useUnboxedParams then
550+
if unboxedArgs.contains(arg) && ccConfig.useUnboxedParams then
551551
capt.println(i"charging deep capture set of $arg: ${argType} = ${CaptureSet.deepCaptureSet(argType)}")
552552
markFree(CaptureSet.deepCaptureSet(argType), arg.srcPos)
553553
argType
554554

555555
/** A specialized implementation of the apply rule.
556556
*
557-
* E |- f: Ra ->Cf Rr^Cr
558-
* E |- a: Ra^Ca
557+
* E |- q: Tq^Cq
558+
* E |- q.f: Ta^Ca ->Cf Tr^Cr
559+
* E |- a: Ta
559560
* ---------------------
560-
* E |- f a: Rr^C
561+
* E |- f(a): Tr^C
561562
*
562-
* The implementation picks as `C` one of `{f, a}` or `Cr`, depending on the
563-
* outcome of a `mightSubcapture` test. It picks `{f, a}` if this might subcapture Cr
564-
* and Cr otherwise.
563+
* If the function `f` does not have an `@unboxed` parameter, then
564+
* any unboxing it does would be charged to the environment of the function
565+
* so they have to appear in Cq. Since any capabilities of the result of the
566+
* application must already be present in the application, an upper
567+
* approximation of the result capture set is Cq \union Ca, where `Ca`
568+
* is the capture set of the argument.
569+
* If the function `f` does have an `@unboxed` parameter, then it could in addition
570+
* unbox reach capabilities over its formal parameter. Therefore, the approximation
571+
* would be `Cq \union dcs(Ca)` instead.
572+
* If the approximation is known to subcapture the declared result Cr, we pick it for C
573+
* otherwise we pick Cr.
565574
*/
566575
protected override
567576
def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type =
568-
Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes)) match
577+
val appType = Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes))
578+
val qualCaptures = qualType.captureSet
579+
val argCaptures =
580+
for (arg, argType) <- tree.args.lazyZip(argTypes) yield
581+
if unboxedArgs.remove(arg) // need to ensure the remove happens, that's why argCaptures is computed even if not needed.
582+
then CaptureSet.deepCaptureSet(argType)
583+
else argType.captureSet
584+
appType match
569585
case appType @ CapturingType(appType1, refs)
570586
if qualType.exists
571587
&& !tree.fun.symbol.isConstructor
572-
&& !qualType.isBoxedCapturing // TODO: This is not strng enough, we also have
573-
// to exclude existentials in function results
574-
&& !argTypes.exists(_.isBoxedCapturing)
575-
&& qualType.captureSet.mightSubcapture(refs)
576-
&& argTypes.forall(_.captureSet.mightSubcapture(refs))
577-
=>
578-
val callCaptures = tree.args.foldLeft(qualType.captureSet): (cs, arg) =>
579-
cs ++ arg.tpe.captureSet
588+
&& qualCaptures.mightSubcapture(refs)
589+
&& argCaptures.forall(_.mightSubcapture(refs)) =>
590+
val callCaptures = argCaptures.foldLeft(qualCaptures)(_ ++ _)
580591
appType.derivedCapturingType(appType1, callCaptures)
581592
.showing(i"narrow $tree: $appType, refs = $refs, qual-cs = ${qualType.captureSet} = $result", capt)
582593
case appType =>
Lines changed: 27 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:24:11 --------------------------------------
2-
24 | cur = (() => f.write()) :: Nil // error since {f*} !<: {xs*}
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:22:11 --------------------------------------
2+
22 | cur = (() => f.write()) :: Nil // error
33
| ^^^^^^^^^^^^^^^^^^^^^^^
44
| Found: List[box () ->{f} Unit]
55
| Required: List[box () ->{xs*} Unit]
66
|
77
| longer explanation available when compiling with `-explain`
8-
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:35:7 ---------------------------------------
9-
35 | (() => f.write()) :: Nil // error since {f*} !<: {xs*}
8+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:33:7 ---------------------------------------
9+
33 | (() => f.write()) :: Nil // error
1010
| ^^^^^^^^^^^^^^^^^^^^^^^
1111
| Found: List[box () ->{f} Unit]
1212
| Required: box List[box () ->{xs*} Unit]^?
@@ -15,52 +15,34 @@
1515
| cannot be included in outer capture set {xs*} of value cur
1616
|
1717
| longer explanation available when compiling with `-explain`
18-
-- Error: tests/neg-custom-args/captures/reaches.scala:38:6 ------------------------------------------------------------
19-
38 | var cur: List[Proc] = xs // error: Illegal type for var
20-
| ^
21-
| Mutable variable cur cannot have type List[box () => Unit] since
22-
| the part box () => Unit of that type captures the root capability `cap`.
23-
-- Error: tests/neg-custom-args/captures/reaches.scala:45:15 -----------------------------------------------------------
24-
45 | val cur = Ref[List[Proc]](xs) // error: illegal type for type argument to Ref
25-
| ^^^^^^^^^^^^^^^
26-
| Sealed type variable T cannot be instantiated to List[box () => Unit] since
27-
| the part box () => Unit of that type captures the root capability `cap`.
28-
| This is often caused by a local capability in an argument of constructor Ref
29-
| leaking as part of its result.
30-
-- Error: tests/neg-custom-args/captures/reaches.scala:55:31 -----------------------------------------------------------
31-
55 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error
32-
| ^^^^^^^^^^^^^^^^^^^^
33-
| Sealed type variable A cannot be instantiated to box () => Unit since
34-
| that type captures the root capability `cap`.
35-
| This is often caused by a local capability in an argument of constructor Id
36-
| leaking as part of its result.
37-
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:57:6 ---------------------------------------
38-
57 | id(() => f.write()) // error
39-
| ^^^^^^^^^^^^^^^^^^^
40-
| Found: () => Unit
41-
| Required: () ->? Unit
42-
|
43-
| Note that the universal capability `cap`
44-
| cannot be included in capture set ?
45-
|
46-
| longer explanation available when compiling with `-explain`
47-
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:64:27 --------------------------------------
48-
64 | val f1: File^{id*} = id(f) // error, since now id(f): File^
18+
-- Error: tests/neg-custom-args/captures/reaches.scala:38:31 -----------------------------------------------------------
19+
38 | val next: () => Unit = cur.head // error
20+
| ^^^^^^^^
21+
| The expression's type box () => Unit is not allowed to capture the root capability `cap`.
22+
| This usually means that a capability persists longer than its allowed lifetime.
23+
-- Error: tests/neg-custom-args/captures/reaches.scala:45:35 -----------------------------------------------------------
24+
45 | val next: () => Unit = cur.get.head // error
25+
| ^^^^^^^^^^^^
26+
| The expression's type box () => Unit is not allowed to capture the root capability `cap`.
27+
| This usually means that a capability persists longer than its allowed lifetime.
28+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:62:27 --------------------------------------
29+
62 | val f1: File^{id*} = id(f) // error, since now id(f): File^
4930
| ^^^^^
50-
| Found: File^{id, f}
31+
| Found: File^{f}
5132
| Required: File^{id*}
5233
|
5334
| longer explanation available when compiling with `-explain`
54-
-- Error: tests/neg-custom-args/captures/reaches.scala:81:5 ------------------------------------------------------------
55-
81 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error
56-
| ^^^^^^
57-
| Reach capability cap and universal capability cap cannot both
58-
| appear in the type [B](f: ((box A ->{ps*} A, box A ->{ps*} A)) => B): List[B] of this expression
59-
-- Error: tests/neg-custom-args/captures/reaches.scala:81:10 -----------------------------------------------------------
60-
81 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error
35+
-- Error: tests/neg-custom-args/captures/reaches.scala:79:10 -----------------------------------------------------------
36+
79 | ps.map((x, y) => compose1(x, y)) // error // error
6137
| ^
6238
| Local reach capability ps* leaks into capture scope of method mapCompose
63-
-- Error: tests/neg-custom-args/captures/reaches.scala:81:13 -----------------------------------------------------------
64-
81 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error
39+
-- Error: tests/neg-custom-args/captures/reaches.scala:79:13 -----------------------------------------------------------
40+
79 | ps.map((x, y) => compose1(x, y)) // error // error
6541
| ^
6642
| Local reach capability ps* leaks into capture scope of method mapCompose
43+
-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:53:51 --------------------------------------
44+
53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error
45+
| ^
46+
| Type argument () -> Unit does not conform to lower bound () => Unit
47+
|
48+
| longer explanation available when compiling with `-explain`

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

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
//> using options -source 3.4
2-
// (to make sure we use the sealed policy)
31
import caps.unboxed
42
class File:
53
def write(): Unit = ???
@@ -14,14 +12,14 @@ class Ref[T](init: T):
1412
def set(y: T) = { x = y }
1513

1614
def runAll0(@unboxed xs: List[Proc]): Unit =
17-
var cur: List[() ->{xs*} Unit] = xs // OK, by revised VAR
15+
var cur: List[() ->{xs*} Unit] = xs
1816
while cur.nonEmpty do
1917
val next: () ->{xs*} Unit = cur.head
2018
next()
2119
cur = cur.tail: List[() ->{xs*} Unit]
2220

2321
usingFile: f =>
24-
cur = (() => f.write()) :: Nil // error since {f*} !<: {xs*}
22+
cur = (() => f.write()) :: Nil // error
2523

2624
def runAll1(@unboxed xs: List[Proc]): Unit =
2725
val cur = Ref[List[() ->{xs*} Unit]](xs) // OK, by revised VAR
@@ -32,19 +30,19 @@ def runAll1(@unboxed xs: List[Proc]): Unit =
3230

3331
usingFile: f =>
3432
cur.set:
35-
(() => f.write()) :: Nil // error since {f*} !<: {xs*}
33+
(() => f.write()) :: Nil // error
3634

3735
def runAll2(xs: List[Proc]): Unit =
38-
var cur: List[Proc] = xs // error: Illegal type for var
36+
var cur: List[Proc] = xs
3937
while cur.nonEmpty do
40-
val next: () => Unit = cur.head
38+
val next: () => Unit = cur.head // error
4139
next()
4240
cur = cur.tail
4341

4442
def runAll3(xs: List[Proc]): Unit =
45-
val cur = Ref[List[Proc]](xs) // error: illegal type for type argument to Ref
43+
val cur = Ref[List[Proc]](xs)
4644
while cur.get.nonEmpty do
47-
val next: () => Unit = cur.get.head
45+
val next: () => Unit = cur.get.head // error
4846
next()
4947
cur.set(cur.get.tail: List[Proc])
5048

@@ -54,7 +52,7 @@ class Id[-A, +B >: A]():
5452
def test =
5553
val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error
5654
usingFile: f =>
57-
id(() => f.write()) // error
55+
id(() => f.write())
5856

5957
def attack2 =
6058
val id: File^ -> File^ = x => x
@@ -78,4 +76,7 @@ def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C =
7876
z => g(f(z))
7977

8078
def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] =
81-
ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error
79+
ps.map((x, y) => compose1(x, y)) // error // error
80+
81+
def mapCompose2[A](@unboxed ps: List[(A => A, A => A)]): List[A ->{ps*} A] =
82+
ps.map((x, y) => compose1(x, y))
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import language.experimental.captureChecking
2+
3+
// Extract of the problem in collection.mutable.Buffers
4+
trait Buffer[A]:
5+
6+
def apply(i: Int): A = ???
7+
8+
def flatMapInPlace(f: A => IterableOnce[A]^): this.type = {
9+
val g = f
10+
val s = 10
11+
// capture checking: we need the copy since we box/unbox on g* on the next line
12+
// TODO: This looks fishy, need to investigate
13+
// Alternative would be to mark `f` as @unboxed. It's not inferred
14+
// since `^ appears in a function result, not under a box.
15+
val newElems = new Array[(IterableOnce[A]^{f})](s)
16+
var i = 0
17+
while i < s do
18+
val x = g(this(i))
19+
newElems(i) = x
20+
i += 1
21+
this
22+
}

tests/pos-custom-args/captures/opaque-inline-problem.scala

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ trait Async extends caps.Capability:
44
object Async:
55
inline def current(using async: Async): async.type = async
66
opaque type Spawn <: Async = Async
7-
def blocking[T](f: Spawn => T): T = ???
7+
def blocking[T](f: Spawn ?=> T): T = ???
88

99
def main() =
10-
Async.blocking: spawn =>
11-
val c = Async.current(using spawn)
12-
val a = c.group
10+
Async.blocking:
11+
val a = Async.current.group

0 commit comments

Comments
 (0)