Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import org.scalajs.jsenv.selenium.SeleniumJSEnv

import JSEnv._

ThisBuild / baseVersion := "3.3"
ThisBuild / baseVersion := "3.4"

ThisBuild / organization := "org.typelevel"
ThisBuild / organizationName := "Typelevel"
Expand Down
15 changes: 15 additions & 0 deletions core/js/src/main/scala/cats/effect/IOPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package cats.effect

import scala.concurrent.Future
import scala.scalajs.js.{|, Function1, JavaScriptException, Promise, Thenable}

abstract private[effect] class IOPlatform[+A] { self: IO[A] =>
Expand All @@ -35,4 +36,18 @@ abstract private[effect] class IOPlatform[+A] { self: IO[A] =>
resolve(value)
()
})

def unsafeRunSyncToFuture()(implicit runtime: unsafe.IORuntime): Future[A] =
self.syncStep(runtime.config.autoYieldThreshold).attempt.unsafeRunSync() match {
case Left(t) => Future.failed(t)
case Right(Left(ioa)) => ioa.unsafeToFuture()
case Right(Right(a)) => Future.successful(a)
}

def unsafeRunSyncToPromise()(implicit runtime: unsafe.IORuntime): Promise[A] =
self.syncStep(runtime.config.autoYieldThreshold).attempt.unsafeRunSync() match {
case Left(t) => Promise.reject(t)
case Right(Left(ioa)) => ioa.unsafeToPromise()
case Right(Right(a)) => Promise.resolve[A](a)
}
}
85 changes: 48 additions & 37 deletions core/shared/src/main/scala/cats/effect/IO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -827,52 +827,63 @@ sealed abstract class IO[+A] private () extends IOPlatform[A] {
fiber
}

@deprecated("use syncStep(Int) instead", "3.4.0")
def syncStep: SyncIO[Either[IO[A], A]] = syncStep(Int.MaxValue)

/**
* Translates this `IO[A]` into a `SyncIO` value which, when evaluated, runs the original `IO`
* to its completion, or until the first asynchronous, boundary, whichever is encountered
* first.
*/
def syncStep: SyncIO[Either[IO[A], A]] = {
def interpret[B](io: IO[B]): SyncIO[Either[IO[B], B]] =
io match {
case IO.Pure(a) => SyncIO.pure(Right(a))
case IO.Error(t) => SyncIO.raiseError(t)
case IO.Delay(thunk, _) => SyncIO.delay(thunk()).map(Right(_))
case IO.RealTime => SyncIO.realTime.map(Right(_))
case IO.Monotonic => SyncIO.monotonic.map(Right(_))

case IO.Map(ioe, f, _) =>
interpret(ioe).map {
case Left(_) => Left(io)
case Right(a) => Right(f(a))
}

case IO.FlatMap(ioe, f, _) =>
interpret(ioe).flatMap {
case Left(_) => SyncIO.pure(Left(io))
case Right(a) => interpret(f(a))
}

case IO.Attempt(ioe) =>
interpret(ioe)
.map {
* to its completion, the `limit` number of stages, or until the first asynchronous boundary,
* whichever is encountered first.
*
* @param limit
* The number of stages to evaluate prior to forcibly yielding to `IO`
*/
def syncStep(limit: Int): SyncIO[Either[IO[A], A]] = {
def interpret[B](io: IO[B], limit: Int): SyncIO[Either[IO[B], (B, Int)]] = {
if (limit <= 0) {
SyncIO.pure(Left(io))
} else {
io match {
case IO.Pure(a) => SyncIO.pure(Right((a, limit)))
case IO.Error(t) => SyncIO.raiseError(t)
case IO.Delay(thunk, _) => SyncIO.delay(thunk()).map(a => Right((a, limit)))
case IO.RealTime => SyncIO.realTime.map(a => Right((a, limit)))
case IO.Monotonic => SyncIO.monotonic.map(a => Right((a, limit)))

case IO.Map(ioe, f, _) =>
interpret(ioe, limit - 1).map {
case Left(_) => Left(io)
case Right(a) => Right(a.asRight[Throwable])
case Right((a, limit)) => Right((f(a), limit))
}
.handleError(t => Right(t.asLeft[IO[B]]))

case IO.HandleErrorWith(ioe, f, _) =>
interpret(ioe)
.map {
case Left(_) => Left(io)
case Right(a) => Right(a)
case IO.FlatMap(ioe, f, _) =>
interpret(ioe, limit - 1).flatMap {
case Left(_) => SyncIO.pure(Left(io))
case Right((a, limit)) => interpret(f(a), limit - 1)
}
.handleErrorWith(t => interpret(f(t)))

case _ => SyncIO.pure(Left(io))
case IO.Attempt(ioe) =>
interpret(ioe, limit - 1)
.map {
case Left(_) => Left(io)
case Right((a, limit)) => Right((a.asRight[Throwable], limit))
}
.handleError(t => Right((t.asLeft[IO[B]], limit - 1)))

case IO.HandleErrorWith(ioe, f, _) =>
interpret(ioe, limit - 1)
.map {
case Left(_) => Left(io)
case r @ Right(_) => r
}
.handleErrorWith(t => interpret(f(t), limit - 1))

case _ => SyncIO.pure(Left(io))
}
}
}

interpret(this)
interpret(this, limit).map(_.map(_._1))
}

/**
Expand Down
25 changes: 22 additions & 3 deletions tests/shared/src/test/scala/cats/effect/IOSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1283,7 +1283,7 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification {
.flatMap { _ => IO.pure(42) }
}

io.syncStep.map {
io.syncStep(1024).map {
case Left(_) => throw new RuntimeException("Boom!")
case Right(n) => n
} must completeAsSync(42)
Expand All @@ -1295,7 +1295,7 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification {
case object TestException extends RuntimeException
val io = IO.raiseError[Unit](TestException)

io.syncStep.map {
io.syncStep(1024).map {
case Left(_) => throw new RuntimeException("Boom!")
case Right(()) => ()
} must failAsSync(TestException)
Expand Down Expand Up @@ -1325,7 +1325,7 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification {
}
}

io.syncStep.flatMap {
io.syncStep(1024).flatMap {
case Left(remaining) =>
SyncIO.delay {
inDelay must beTrue
Expand All @@ -1342,6 +1342,25 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification {
case Right(_) => SyncIO.raiseError[Unit](new RuntimeException("Boom!"))
} must completeAsSync(())
}

"evaluate up to limit and no further" in {
var first = false
var second = false

val program = IO { first = true } *> IO { second = true }

val test = program.syncStep(2) flatMap { results =>
SyncIO {
first must beTrue
second must beFalse
results must beLeft

()
}
}

test must completeAsSync(())
}
}

"fiber repeated yielding test" in real {
Expand Down