Skip to content
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

Refine withReachCaptures #18968

Closed
wants to merge 1 commit into from

Conversation

Linyxus
Copy link
Contributor

@Linyxus Linyxus commented Nov 17, 2023

Follow-up of this comment.

Previously, we avoid refining co-variant caps to reach capabilities as long as there exist contra-variant caps. This limits expressivity. One counter-example is church-encodings: types like [R] -> (op: (IO^, IO^) => R) -> R cannot be refined, since the parameter op has a covariant cap.

This PR refines the treatment: only when a contra-variant cap is possibly reached by a co-variant cap do we refuse the refinement. Examples:

  • The co-variant cap in (c: IO^{cap}) -> IO^{cap} cannot be refined, as it could refer to the impure contra-variant parameter c.
  • p : [R] -> (op: (IO^{cap}, IO^{cap}) => R) -> R can be safely refined to [R] -> (op: (IO^{p*}, IO^{p*}) => R) -> R, as op is not possibly reached from the two co-variant caps.

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the status of this wrt soundness?

@Linyxus
Copy link
Contributor Author

Linyxus commented Nov 28, 2023

I found a deeper unsoundness issue of this approach.

To recap, the underlying principle of this approach is to refine co-variant caps to both the reach capability and deeply-impure contra-variant dependent parameters in scope. For instance,

f : (x: Box[IO^]) -> Box[IO^]

should be refined to

f : (x: Box[IO^]) -> Box[IO^{f*, x*}]

In practice, augmenting the capture set with reachable parameters (like x* here) might be over-complicated. So, in the implementation we do not refine a cap if there is any impure dependent parameter in scope. So in this example, f will not be refined.

Similarly, the following function will not be refined either:

f : (x: Box[IO^]) -> [R] -> (op: Box[IO^] => R) -> R

Since based on the underlying principle, it should be refined to the complex form

f : (x: Box[IO^]) -> [R] -> (op: Box[IO^{f*, x*}] => R) -> R

But such a scheme falls apart quickly, for instance, just swapping the order of the first three arguments, the function is refined by our scheme:

g : [R] -> (op: Box[IO^] => R) -> (x: Box[IO^]) -> R

gets refined to

g : [R] -> (op: Box[IO^{g*}] => R) -> (x: Box[IO^]) -> R

We assume that x, an argument introduced after op, cannot possibly flow into the first cap. This is false:

val g = [R] => op => x => op(x)

If we were to refine g, we would get the following unsound code:

val g = [R] => op => x => op(x)
withIO: localIO =>
  val g1 : [R] -> (op: Box[IO^{g*}] => R) -> (x: Box[IO^]) -> R = g
  val g2 : (op: Box[IO^{g*}] => Box[IO^{g*]]) -> (x: Box[IO^]) -> Box[IO^{g*}] = g1[Box[IO^{g*}]]
  val bad : Box[IO^{g*}] = g2(x => x)(localIO)
  bad  // leaked

Essentially, in the scheme in this PR, when we see a cap at a covariant position in a function:

f : (x1: T1) -> (x2: T2) -> ... -> (xk: Tk) -> (xs: List[IO^{cap}]) -> U

we assume that cap indicates something reachable from f* and from x1 ... xk. But this is false: cap could well indicate capabilities flowing into the closure in U.

There is no easy way to patch this scheme. Ideally we should refine g as:

g : [R] -> (op: Box[IO^{g*, x*}] => R) -> (x: Box[IO^]) -> R

but this is ill-formed: we cannot name x in op.

@Linyxus
Copy link
Contributor Author

Linyxus commented Dec 2, 2023

Turns out to be unsound. Closed.

@Linyxus Linyxus closed this Dec 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants