Skip to content

Reimplemented everything using Continuation#284

Open
djspiewak wants to merge 10 commits intomainfrom
experiment/continuation
Open

Reimplemented everything using Continuation#284
djspiewak wants to merge 10 commits intomainfrom
experiment/continuation

Conversation

@djspiewak
Copy link
Member

This basically yeets out the existing compile-time mechanism and replaces it with one based on Continuation (the JVM basis for virtual threads). Note that this requires some runtime shenanigans from users, but it's not too bad and the benefits are quite significant (e.g. no more issues with higher order functions or other transformation failures).

Haven't implemented yet for Scala Native (we'll probably use something like this https://dl.acm.org/doi/10.1145/3679005.3685979) or Scala.js (idk @armanbilge had some idea here) but it should be possible in principle.

}
}

final class Await[F[_]] private[direct] (private[direct] val scope: ContinuationScope) {
Copy link
Member

Choose a reason for hiding this comment

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

We could even extends ContinuationScope but maybe don't want to leak that?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I would rather not leak it, especially if they change the API.


final class Await[F[_]] private[direct] (private[direct] val scope: ContinuationScope) {
private[direct] var next: F[Any] = _
private[direct] var result: Any = _
Copy link
Member

Choose a reason for hiding this comment

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

@jducoeur raised a good point about error handling. We should also have a channel for Throwables ...

Suggested change
private[direct] var result: Any = _
private[direct] var result: Any = _
private[direct] var error: Throwable = null

Comment on lines 63 to 65
await.next = self.asInstanceOf[F[Any]]
Continuation.`yield`(await.scope)
await.result.asInstanceOf[A]
Copy link
Member

Choose a reason for hiding this comment

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

... that is thrown like this:

Suggested change
await.next = self.asInstanceOf[F[Any]]
Continuation.`yield`(await.scope)
await.result.asInstanceOf[A]
await.next = self.asInstanceOf[F[Any]]
await.error = null
Continuation.`yield`(await.scope)
val e = await.error
if (e ne null) throw e else await.result.asInstanceOf[A]

@djspiewak
Copy link
Member Author

One challenge that I realized this morning with this approach: auto-ceding. In theory it should work since we're only crossing thread boundaries while the continuation is suspended, but it needs some testing. Additionally, the semantics here may be a bit different between JVM and LLVM.

@djspiewak
Copy link
Member Author

The Scala Native version is Scala 3 only, which is a little frustrating since it doesn't need to be. Additionally, it doesn't work. I keep getting the following error:

cats.effect.AsyncAwaitSuite:
ScalaNative: Unrecoverable NullPointerException in user thread
	at StackTrace_PrintStackTrace
	at stackOverflowHandler
	at _sigtramp
	at handler_split_at
[warn] Force close java.lang.RuntimeException: Process /Users/daniel/Development/Scala/cats-effect-cps/core/native/target/scala-3.3.6/cats-effect-direct-test finished with non-zero value 11 (0xb)

@durban
Copy link

durban commented Dec 14, 2025

@djspiewak About the SN problem: I definitely don't understand, how this magic works, but this code seems suspicious to me:

  private[effect] def asyncImpl[F[_]: Sync, A](body: Await[F] => A): F[A] =
    boundary[F[A]] {
      Sync[F].delay {
        val await = new Await[F] {
          type Result = A
          val label = implicitly[BoundaryLabel[F[A]]]
        }

        body(await)
      }
    }

This seems to return an F[A] data structure, which closes over a BoundaryLabel. The scaladoc for BoundaryLabel says this:

the passed-in BoundaryLabel cannot be used outside of the scope of the body. Suspending to a BoundaryLabel not created by a boundary call higher on the same call stack is undefined behaviour.

Which is exactly what asyncImpl seems to be doing. The undefined behavior, that is.

@djspiewak
Copy link
Member Author

@durban Oh it's a lazy vs eager execution issue! That makes a lot of sense. So then this encoding just isn't going to work. I'll ponder that a bit more.

I think it's quite ironic that the typeful and more functional encoding of this concept (in Scala Native) is harder to work with for this goal than the imperative and untyped version in the JVM.

@djspiewak
Copy link
Member Author

I think what we've concluded is that I like long jumps and I cannot lie.

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.

4 participants