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

Tests for capture checking #67

Open
odersky opened this issue Apr 17, 2024 · 8 comments
Open

Tests for capture checking #67

odersky opened this issue Apr 17, 2024 · 8 comments

Comments

@odersky
Copy link
Contributor

odersky commented Apr 17, 2024

When we get capture checked Gears it would be good that the following works as expected:

val good1: List[Future[Result[T]]] -> Future[Result[List[T]]] = frs =>
  Future:
    Result:
      frs.map(_.await.ok)

val good2: Result[Future[T]] -> Future[Result[T]] = rf =>
  Future:
    Result:
      rf.ok.await  // OK, Future argument has type Result[T]

On the other hand, the following case should fail:

val fail3: Future[Result[T]] -> Result[Future[T]] = fr =>
  Result: 
    Future:
      fr.await.ok // error, escaping label from Result

This fails because it is expanded to

val fail3: Future[Result[T]] -> Result[Future[T]] = fr =>
  Result: lbl ?=> 
    Future:
      fr.await.ok(using lbl) // error, escaping label from Result

and the Future block has type Future[T]^{async, lbl}.
A transform having that type can be constructed but would be useless since we take a future of a value:

val useless4: Future[Result[T]] -> Result[Future[T]] = fr =>
  fr.await.map(Future(_))

The difference between good2 and fail3 is that Result is strict and Future is not.

natsukagami added a commit to natsukagami/async that referenced this issue Apr 19, 2024
natsukagami added a commit to natsukagami/async that referenced this issue Apr 19, 2024
@odersky
Copy link
Contributor Author

odersky commented Apr 20, 2024

The following minimization gives the correct error, even if the error message could certainly be imrpoved:

import scala.util.boundary, boundary.{Label, break}

class Async
class Future[+T]:
  this: Future[T]^ =>
  def await(using Async^): T = ???
object Future:
  def apply[T](op: Async^ ?=> T)(using Async): Future[T]^{op} = ???

abstract class Result[+T, +E]
case class Ok[+T](value: T)  extends Result[T, Nothing]
case class Err[+E](value: E) extends Result[Nothing, E]

object Result:
  extension [T, E](r: Result[T, E])

    /** `_.ok` propagates Err to current Label */
    def ok(using Label[Err[E]]^): T = r match
      case r: Ok[_] => r.value
      case err => break(err.asInstanceOf[Err[E]])

  def apply[T, E](body: Label[Err[E]]^ ?=> T): Result[T, E] =
    boundary:
      val result = body
      Ok(result)

end Result

def test[T, E](using Async) =
  val good1: List[Future[Result[T, E]]] => Future[Result[List[T], E]] = frs =>
    Future:
      Result:
        frs.map(_.await.ok)

  val good2: Result[Future[T], E] => Future[Result[T, E]] = rf =>
    Future:
      Result:
        rf.ok.await  // OK, Future argument has type Result[T]

  def fail3(fr: Future[Result[T, E]]^) =
    Result:
      Future:
        fr.await.ok // error, escaping label from Result

We get:

-- Error: effect-swaps.scala:41:4 ----------------------------------------------
41 |    Result:
   |    ^^^^^^
   |local reference contextual$6 leaks into outer capture set of type parameter T of method apply
1 error found

But it does give a weirder error message if the apply method in Result is declared inline with inline arguments:

  inline def apply[T, E](inline body: Label[Err[E]]^ ?=> T): Result[T, E] =
    boundary:
      val result = body
      Ok(result)

In this case we get:

-- [E007] Type Mismatch Error: effect-swaps.scala:42:6 -------------------------
41 |    Result:
42 |      Future:
   |    ^
   |    Found:    Result[box Future[box T^?]^{fr}, box E^?]^?
   |    Required: Result[Future[T], E]
43 |        fr.await.ok // error, escaping label from Result
   |----------------------------------------------------------------------------
   |Inline stack trace
   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   |This location contains code that was inlined from effect-swaps.scala:24
23 |    boundary:
24 |      val result = body
   |    ^
25 |      Ok(result)
    ----------------------------------------------------------------------------
   |
   | longer explanation available when compiling with `-explain`
1 error found

And it compiles without errors if apply is declared as transparent inline.

@odersky
Copy link
Contributor Author

odersky commented Apr 20, 2024

I think the false positives came because boundary itself was not capture checked. I now added it to the test as follows:

import annotation.capability

object boundary:

  @capability final class Label[-T]

  /** Abort current computation and instead return `value` as the value of
   *  the enclosing `boundary` call that created `label`.
   */
  def break[T](value: T)(using label: Label[T]): Nothing = ???

  def apply[T](body: Label[T] ?=> T): T = ???
end boundary

import boundary.{Label, break}

class Async
class Future[+T]:
  this: Future[T]^ =>
  def await(using Async^): T = ???
object Future:
  def apply[T](op: Async^ ?=> T)(using Async): Future[T]^{op} = ???

abstract class Result[+T, +E]
case class Ok[+T](value: T)  extends Result[T, Nothing]
case class Err[+E](value: E) extends Result[Nothing, E]

object Result:
  extension [T, E](r: Result[T, E])

    /** `_.ok` propagates Err to current Label */
    def ok(using Label[Err[E]]): T = r match
      case r: Ok[_] => r.value
      case err => break(err.asInstanceOf[Err[E]])

  transparent inline def apply[T, E](inline body: Label[Err[E]] ?=> T): Result[T, E] =
    boundary:
      val result = body
      Ok(result)

end Result

def test[T, E](using Async) =
  val good1: List[Future[Result[T, E]]] => Future[Result[List[T], E]] = frs =>
    Future:
      Result:
        frs.map(_.await.ok)

  val good2: Result[Future[T], E] => Future[Result[T, E]] = rf =>
    Future:
      Result:
        rf.ok.await  // OK, Future argument has type Result[T]

  def fail3(fr: Future[Result[T, E]]^) =
    Result:
      Future:
        fr.await.ok // error, escaping label from Result

And that gives the expected error:

-- Error: effect-swaps.scala:56:6 ----------------------------------------------
55 |    Result:
56 |      Future:
   |    ^
   |local reference contextual$1 leaks into outer capture set of type parameter T of method apply
57 |        fr.await.ok // error, escaping label from Result
   |----------------------------------------------------------------------------
   |Inline stack trace
   |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
   |This location contains code that was inlined from effect-swaps.scala:37
37 |    boundary:
   |    ^^^^^^^^
    ----------------------------------------------------------------------------
1 error found

I am still not sure why the experiment with actual Gears code inferred Result[Any, Any].

@natsukagami
Copy link
Contributor

natsukagami commented Apr 20, 2024

I wrote down the relevant parts here. This is code not using any Gears implementation, and should still compile

import language.experimental.captureChecking

import scala.annotation.capability

@capability trait Async

object Async:
  def blocking[T](body: Async ?=> T): T = ???

object boundary:
  @capability final class Label[-T]

  /** Abort current computation and instead return `value` as the value of
   *  the enclosing `boundary` call that created `label`.
   */
  def break[T](value: T)(using label: Label[T]): Nothing = ???

  def apply[T](body: Label[T] ?=> T): T = ???
end boundary

enum Result[+T, +E]:
  case Ok[+T](value: T) extends Result[T, Nothing]
  case Err[+E](error: E) extends Result[Nothing, E]

object Result:
  import boundary.Label

  def apply[T, E](body: Label[Result[T, E]] ?=> T): Result[T, E] =
    boundary(Ok(body))

  extension [U, E](r: Result[U, E]^)(using Label[Err[E]])
    def ok: U = r match
      case Err(value)  => boundary.break[Err[E]](Err(value))
      case Ok(value) => value

class Future[+T]():
  def await(using Async): T = ???
  // def awaitResult(using Async): scala.util.Try[T] = ???
object Future:
  def apply[T](body: Async ?=> T)(using spawn: Async): Future[T]^{body} = ???

def main() =
  import Result.*
  Async.blocking: async ?=>
    def fail3[T, E](fr: Future[Result[T, E]]^) =
      Result: label ?=>
        val f: Future[T]^{async, fr, label} = Future: fut ?=>
          fr.await.ok(using label) // error, escaping label from Result
        f

@odersky

@natsukagami
Copy link
Contributor

natsukagami commented Apr 20, 2024

It seems that changing

object Result:
  def apply[T, E](body: Label[Result[T, E]] ?=> T): Result[T, E] = 

to

object Result:
  def apply[T, E](body: Label[Err[E]] ?=> T): Result[T, E] = 

will make the compiler emit the error you had.

We do need to have a Label[Result[T, E]] however, because we do want the user to emit a boundary.break with a Result (for tail calls).

@odersky
Copy link
Contributor Author

odersky commented Apr 21, 2024

We do need to have a Label[Result[T, E]] however, because we do want the user to emit a boundary.break with a Result (for tail calls).

In #20244 I have a minimization along these lines. But I don't get the error you see, for me it's still a "local reference leaks into outer capture set" message.

@natsukagami
Copy link
Contributor

Does compiling the minimization at #67 (comment) give you the "local reference leaks" error?

I'm getting no compiler errors using sbt scalac Test.scala from scala/scala3#20241 branch.

@odersky
Copy link
Contributor Author

odersky commented Apr 21, 2024

That minimization also compiles for me without errors and a Result[Any, Any] label. I chased it down to the following difference: If we write fail3 like this, it compiles:

   def fail3[T, E](fr: Future[Result[T, E]]^) =
      Result: label ?=>
        val f = Future: fut ?=>
          fr.await.ok // error, escaping label from Result
        f
 Result: label ?=>

But if we drop the label ?=> and use an implicit context funciton parameter instead, we get the leak error:

    def fail3[T, E](fr: Future[Result[T, E]]^) =
      Result:
        val f = Future: fut ?=>
          fr.await.ok // error, escaping label from Result
        f

-- Error: effect-swaps-2.scala:46:6 --------------------------------------------
46 |      Result: // contextual$2 ?=>
   |      ^^^^^^
   |local reference contextual$2 from (using contextual$2:
   |  boundary.Label[
   |    Result[box Future[box T^?]^{fr, contextual$2, contextual$2}, box E^?]]^
   |): box Future[box T^?]^{fr, contextual$2, contextual$2} leaks into outer capture set of type parameter T of method apply in object Result
1 error found

I

@odersky
Copy link
Contributor Author

odersky commented Apr 21, 2024

And it turns out that difference is already present after typer. With explicit label, it infers Result[Any, Any] for the result type of fail3, whereas without implicit label it infers Result[Future[T], E].

natsukagami added a commit to natsukagami/async that referenced this issue May 8, 2024
natsukagami added a commit to natsukagami/async that referenced this issue Jul 10, 2024
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

No branches or pull requests

2 participants