-
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
Consolidate CC #21863
Consolidate CC #21863
Conversation
9865f93
to
70af983
Compare
At this point, I feel that curried capture for reach capabilities makes sense no more, given that
Besides, functions like Given the above, shall we drop the special curried capture mechanism for reach capability completely? Reach capabilities, like other capabilities, bubbles all the way up to the top, unless stopped by a boxed environment. Any function that uses the reach capability of its parameter is then automatically be required to be marked as |
In fact that's what is currently implemented. There's a def test3(xs: List[() => Unit]): () ->{xs*} Unit = () =>
println(xs.head) // error, ok under deferredReaches Then [C^] -> (xs: List[() ->{C^} Unit]) -> () ->{C^} Unit` That's a pure function, so we have some justification to treat |
I see, I agree. Environment avoidance can make deferred reaches sound, since it is basically a mechanism for charging effects on subsequent arrows of a function spine correctly. (note that since effects can not only be those of function itself, but can also be some effect variable instantiated by the application of the function, like capture variables or reach capabilities) |
It turns out that the subcapturing rule between a reach capability and its dcs is not sound. The problem is that type substitution breaks it. In other words, this rule is unsound:
Type preservation does not preserve it. As an example, given the following context:
Note that
Therefore, under this context, one may derive:
And this will not be preserved by a type substitution, for instance, [X := Op^{io}]. After applying the type substitution, the context becomes:
and we have
Therefore, under this context, we can only derive:
And the relation `{x*} <: {async}` is falsified after the type substitution! In other words, type application could break typing. Based on this idea, we could construct the following example:
The |
Yes, that's bad. The alternative would be to always take import language.experimental.captureChecking
import caps.use
class Box[T](items: Seq[T^]):
def getOne: T^{items*} = ???
object Box:
def getOne[T](@use items: Seq[T^]): T^{items*} =
val bx = Box(items)
bx.getOne we'd get with that change:
Other tests failing in similar ways are:
What can we do to accept these, but rule out the unsound one? |
In the previous example, val bx: Box[T]{val items: Seq[T^{items*}]} |
In your example, where would you expect to see an error? |
In the status quo, the reach capabilities are purely a device for polymorphism. That makes a lot of stuff counter-intuitive. Among them, one is that given Now let's consider an example. Given
what should A tempting answer maybe: this term charges But actually NO. Let's translate the function type:
and the application should then be in the form Therefore the answer can be: the application should charge More generally, when type checking the application
we should charge Going back to the example, I think it is def compose(op: List[(() ->{cap} Unit, () ->{cap} Unit)]): List[() ->{op*} Unit] = ...
def compose1(op: List[(() ->{async} Unit, () ->{io} Unit)]): List[() ->{op*} Unit] =
compose(op) The result type of |
In fact, the problem shown by the unsoundness example was something completely different. Function def foo[X](op: (xs: List[(X, () ->{io} Unit)]) => List[() ->{xs*} Unit])
: (xs: List[(X, () ->{io} Unit)]) => List[() ->{} Unit] =
op // error This should have given a type error, but it passed since the encoding of a function type with reach capabilities in the result was wrong. It took me quite a long time to find the cause. It's because we unfortunately have this super-complex encoding of dependent function types as refinement types with a parametric function type as parent. The generation of this parametric parent function type was wrong for results containing reach capabilities. So, it would be good to find a test that demonstrates the original unsoundness argument. |
It would be really good if we could use a different function type encoding in the style of PolyTypes. That will require quite a lot of engineering, in particular for backwards compatibility but it would be a big simplification. So if somebody wants to take this on, this would be much appreciated. |
We currently have have for any reference
Since colltest5 fails, the stdlib is likely to fail as well. The problem is demonstrated in this extract of colltest5: trait Iterator[+A]:
self: Iterator[A]^ =>
trait View[+A]:
def iterator: Iterator[A]^{this}
object View:
def fromIterator[A](it: => Iterator[A]^): View[A]^{it} = new View[A]:
def iterator: Iterator[A]^{this} = it When we change the rules it fails with:
The root cause is that given So I think until we work out span captures we are stuck on this one. |
About adding environment avoidance. I tried that as well, but a great number of tests (>40) break. One problem is that we often treat def f() =
val r: Ref^ = newRef(1)
r.get But that causes a failure when we use |
I could give it a try after the submission. It's been some while since my last compiler hacking. |
That's right. This leads me to the realisation that right now with the addition of environment avoidance existential types are useless in Capless:
This seems really bad. |
Here it is! import language.experimental.captureChecking
import caps.{cap, use}
trait IO
trait Async
def main(io: IO^, async: Async^) =
def bad[X](ops: List[(X, () ->{io} Unit)])(f: () ->{ops*} Unit): () ->{io} Unit = f
def runOps(@use ops: List[(() => Unit, () => Unit)]): () ->{ops*} Unit =
() => ops.foreach((f1, f2) => { f1(); f2() })
def delayOps(@use ops: List[(() ->{async} Unit, () ->{io} Unit)]): () ->{io} Unit =
val runner: () ->{ops*} Unit = runOps(ops)
val badRunner: () ->{io} Unit = bad[() ->{async} Unit](ops)(runner)
// it uses both async and io, but we losed track of async.
badRunner (ps: Sorry for accidentally closing this PR, I misclicked) |
This gives better error messages. Previously we thought this would make reach capabilities unsound, but I don't see an issue with the latest design.
With our current @use scheme, this is unsound. We leave the possibility to re-enable as a Config option which is disabled by default and comes with a warning that enabling it would be unsound.
Check that type parameters of methods and parent traits don't get instantiated with types containing a `cap` anywhere in covariant or invariant position.
Also: add test that reach capabilities are contained inside boxes
We can use the dcs only if there are no type variables.
The additional purity in the asInstanceOf target is not needed
Retain only caps.unsafe.unsafeAssumePure
Don't show an `(ex$n: Exists) ->` if the bound variable does not appear in the result. The full type will be shown under -Ycc-debug. Also, avoid spurious ineffective mappings in widenReach.
A by-name Closure node, which is produced by phase ElimByName gets a target type to indicate it's a contextual zero parameter closure. But for the purposes of rechecking and capture checking, it needs to be treated like a function. In particular the type of the closure needs to be derived from the result type of the anonymous function. Fixes scala#21920
508bdbf
to
353f80b
Compare
@noti0na1 All suggestions implemented, waiting for your approval. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
A refactored and consolidated capture checker without any drastic changes to the algorithm. The main changes are:
cap
instead of checking that we do not box or unboxcap
.@unbox
to@use
Based on #21861