-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Go back towards a pure capability system #21764
Conversation
This is done for comparing old with new
Add the path cases without changing the whole logic
If we refer to a path `a.b`, we should mark `a.b` as used, which is better than marking `a`.
Needed to make stdlib2-cc go through. There were two errors. One in LayListIterable required a type annotation and a tweak to markFree. The other in Vieew.scala required a cast, but this could be fixed with better handling of pattern matching. path-patmat-should-be-pos.scala is a minimization.
Previously, we violated that assumption is we too the deep capture set of a capture reference wiht singleton type.
Count in dcs exactly those locations where a cap gets replaced by a reach capability.
Also: In Recheck-apply, use deep capture sets of arguments in computing the result alternative. Drop restrictions on leaking reach capabilities in markFree. Revise visibility criterion for paths. Together these changes now implement a classical capability system with reach capabilities. References that are used later after passing some arguments are already recorded in capture sets of earlier stages (exception: closure results).
Soundness issue found: import language.experimental.captureChecking
// ok
def runOps(ops: List[() => Unit]): Unit =
ops.foreach(op => op())
// ok
def delayedRunOps(ops: List[() => Unit]): () ->{ops*} Unit =
() => runOps(ops)
// unsound: impure operation pretended pure
def delayedRunOps1(ops: List[() => Unit]): () ->{} Unit =
() =>
val ops1 = ops
runOps(ops1) |
Another soundness issue found: import language.experimental.captureChecking
def runOps(ops: List[() => Unit]): Unit =
ops.foreach(op => op())
def app[T, U](x: T, op: T => U): () ->{op} U =
() => op(x)
def unsafeRunOps(ops: List[() => Unit]): () ->{} Unit =
app[List[() ->{ops*} Unit], Unit](ops, runOps) |
Good points. The first soundness issue is in the deep capture set computation. We think the deep capture set of ops1 is ops1* since ops1 is a capability. But that's true only if all capabilities in the underlying set are shorter-lived than ops1. In this case, that's not the case, sothe deep capture set of ops1 should be ops*, not ops1*. |
The second unsoundness example has the same root issue. |
How is the second one sharing the root cause with the first? My idea behind that example was that given (x:T) where T is a type parameter with no upper bound we can show {x*} <: {}. Then this can be broken by instantiating T. |
A deep capture set should not be shortened to a reach capability `x*` if there are elements in the underlying set that live longer than `x`.
@Linyxus Both examples are fixed by the same change to dcs computation. For the second example we get now:
|
I see that in import language.experimental.captureChecking
def runOps(ops1: List[() => Unit]): Unit =
ops1.foreach(op => op())
def app[T, U](x: T, op: T => U): () ->{op} U =
() => op(x)
def unsafeRunOps(ops: List[() => Unit]): () ->{} Unit =
app[List[() => Unit], Unit](ops, runOps) |
Also, the error in the second example depends on the fact that
When marking-free But this does not really fix the problem. For instance, this can be by passed by wrapping import language.experimental.captureChecking
def runOps(ops: List[() => Unit]): Unit =
ops.foreach(op => op())
trait Consumer[-T]:
def take(x: T): Unit
class OpsRunner extends Consumer[List[() => Unit]]:
def take(x: List[() => Unit]) = runOps(x)
def app[T](x: T, op: Consumer[T]^): () ->{op} Unit =
() => op.take(x)
def unsafeRunOps(ops: List[() => Unit]): () ->{} Unit =
val x: Consumer[List[() ->{ops*} Unit]]^{} = new OpsRunner
app[List[() ->{ops*} Unit]](ops, x) The semantics stays the same, yet we bypass the capture checker. |
IMO, the root cause of the second issue is in: def app[T, U](x: T, op: T => U): () ->{op} U =
() => op(x) The closure returned from this function now only captures |
In a similar manner, the first example bypasses the capture checker if we do: import language.experimental.captureChecking
// ok
def runOps(ops: List[() => Unit]): Unit =
ops.foreach(op => op())
// ok
def delayedRunOps(ops: List[() => Unit]): () ->{ops*} Unit =
() => runOps(ops)
// unsound: impure operation pretended pure
def delayedRunOps1(ops: List[() => Unit]): () ->{} Unit =
() =>
val ops1: List[() => Unit] = ops
runOps(ops1) The change is on the second but last line: we added a type ascription to the |
Yes, but this will make a total mess of parametric reasoning. I fear this would be far too restrictive if we have to assume cap for every time variable reference. We need something better. But what? |
I think the kernel of the first issue: def delayedRunOps1(ops: List[() => Unit]): () ->{} Unit =
() =>
val ops1: List[() ->{ops*} Unit] = ops
runOps(ops1) is that the reach capabilities term parameters and local definitions should be treated differently. What happened when we type But it is different for local definitions. There is no compensation for them. When a reach capability of them, like One possibility is to widen it, similar to what is already implemented: we see that the type of |
And when the dcs of the type of def delayedRunOps1(ops: List[() => Unit]): () ->{} Unit =
() =>
val ops1: List[() => Unit] = ops
runOps(ops1) We can understand it from a Capless perspective. def delayedRunOps1(ops: List[() => Unit]): () ->{} Unit =
() =>
val (ops1*, ops1) = (ops : Exists C. List[() ->{C^} Unit])
// this is an existential unpack; ops1* is the capture variable, and ops1 the term variable.
runOps(ops1) Then there is no way to approximate |
If we instead adopt the above mechanism in place of |
I see how this would fix the delayedRunops1 example. But what about the example with |
For def app[T, U](x: T, op: T => U): () ->{x*,op} U =
() => op(x) and then |
Not sure. dcs[T] being cap looks very problematic. I was hoping we could avoid that, but it seems not. |
It could be possible to avoid that, if we either:
|
Here's a modified version of def app2[T, U](x: T, op: T => U): () ->{op} U =
() =>
def y: T = x
op(y) We could solve this by allowing regular type variables in capture sets. E.g. a sound version of def app2[T, U](x: T, op: T => U): () ->{T^, op} U =
() =>
def y: T = x
op(y) Looks a bit like like a version of cc that we had early on. |
The idea is that |
When substituting U for T, we replace every occurrence of |
Another simplification could be to admit |
Given that in most cases, we write the following form: def f[T](x: T): ... where type parameters are followed by term ones and become the type of the term parameters, would it be possible to simply use |
The correct point to address charging reach capabilities is in markFree itself: When a reach capability goes out of scope, and that capability is not a parameter, we need to continue with the underlying capture set. With this fix, we don't need any special provisions to compute deep capture sets anymore.
This is an attempt to fix the problem explified in the `delayedRunops*.scala` tests. We can treat it as a baseline that fixes the immediate problem of the interaction of reach capabilities and type variables. There might be better ways to do this by using a proper adapation rule for function types instead of adding implied captures post-hoc.
Superseded by #21863 |
1 similar comment
Superseded by #21863 |
@unbox
annotation, don't replace with@use
.cv
.The aim of this PR is to go to a stable system for handling reach capabilities in a classical way before embarking on a more effect-system like treatment where we distinguish use later from use now.