Description
The capture checker employs a global type inference scheme using a propagation constraint solver. Capture sets are constrained by subcapturing constraints or are defined as results of type maps of other capture sets. This can lead to long chains of maps that are hard to debug and that make formulation of capture checking algorithms more difficult.
Can we use a more local scheme where we "solve" constraints of capture sets for each inferred result type? That would largely avoid the need for stored type maps. But it would work only if we can compute the type of each definition only from its local scope. The following example shows that this is not always possible:
case class Box[T](x: T)
def test(io: Object^): Unit =
def foo(): Unit = bar()
def bar(): Unit =
val x = () =>
foo()
val y = Box(io)
println(y.x)
If we print this with -Xprint:cc
, we get:
def test(io: Object^): Unit =
{
def foo(): Unit = bar()
def bar(): Unit =
{
val x: () ->{io} Unit = () => foo()
val y: Box[box Object^{io}]^? = Box.apply[box Object^{io}](io)
println(y.x)
}
()
}
The interesting bit is the type of x
. How did the checker find out it captures io
? Here's how:
x
's right hand side callsfoo
, so the capture set of the closure includes the use set offoo
.foo
callsbar
so the use set offoo
includes the use set ofbar
.io
is used as an argument toBox
inbar
but this is in boxed form, so this does not contribute to the use set ofbar
.- The inferred type of
y
ends up to beBox[box Object^{io}]
. bar
unboxesy.x
of typey*
in the finalprintln
.- This means we charge the deep capture set of
y
's type to the use set ofbar
. - The deep capture set is
{io}
, so that set ends up as the use set ofbar
andfoo
and as the capture set of the type ofx
.
This means that to compute the type of x
we have to know the use sets of foo
and bar
, which means we have to also analyze the rest of bar
after the definition of x
. What's more, this computation involves the full
capture checking machinery including reach captures and their underlying deep capture sets. So there does not seem to be an easy way to have a simpler use set computation followed by local capture set inference.
On the other hand, maybe we could use a standard fixed point iteration approach:
- Keep track of which definitions are currently traversed.
- When including call captures, compute the use set of the callee before proceeding, providing the callee is not currently analyzed.
- If the callee is currently analyzed we have a cycle like the one in the example. Pick the current use set of the callee and mark that set as "observed".
- If an observed use set acquires new elements, set a flag that capture set inference needs to be repeated.
- If at the end of the inference the flag is set, we do a new iteration, with the use sets computed so far.
In the example above, this would lead to the following steps:
- When capture-checking
foo
, we need the use set ofbar
, so we capture-checkbar
. - When capture checking
bar
, we need to check the RHS ofx
, which means we need the use set offoo
. - Since
foo
is currently checked, we assume its use set is as computed so far, i..e. it is empty, and we mark it as observed. - This gives the wrong type
() -> Unit
forx
. - We complete checking of
bar
which gives a use set of{io}
. - We complete checking of
foo
which gives again a use set of{io}
. We remark that this use set has changed after it was observed. - We restart the whole capture checking of
test
with use sets of{io}
forbar
andfoo
.
I believe cycles will be quite rare, so computationally this might be a win.