diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 829e6f1a19..7469265d37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] scala: [3.1.2, 2.12.17, 2.13.8] java: [temurin@8, temurin@11, temurin@17, graalvm@11] - ci: [ciJVM, ciJS, ciFirefox, ciChrome] + ci: [ciJVM, ciNative, ciJS, ciFirefox, ciChrome] exclude: - scala: 3.1.2 java: temurin@11 @@ -83,6 +83,20 @@ jobs: ci: ciChrome - os: macos-latest ci: ciChrome + - ci: ciNative + java: temurin@11 + - ci: ciNative + java: temurin@17 + - ci: ciNative + java: graalvm@11 + - os: windows-latest + ci: ciNative + - os: macos-latest + ci: ciNative + scala: 2.12.17 + - os: macos-latest + ci: ciNative + scala: 3.1.2 - os: windows-latest java: graalvm@11 runs-on: ${{ matrix.os }} @@ -210,6 +224,11 @@ jobs: shell: bash run: example/test-js.sh ${{ matrix.scala }} + - name: Test Example Native App Using Binary + if: matrix.ci == 'ciNative' && matrix.os == 'ubuntu-latest' + shell: bash + run: example/test-native.sh ${{ matrix.scala }} + - name: Scalafix tests if: matrix.scala == '2.13.8' && matrix.ci == 'ciJVM' && matrix.os == 'ubuntu-latest' shell: bash diff --git a/build.sbt b/build.sbt index 315dc35972..c6aed16d39 100644 --- a/build.sbt +++ b/build.sbt @@ -112,10 +112,11 @@ val PrimaryOS = "ubuntu-latest" val Windows = "windows-latest" val MacOS = "macos-latest" +val Scala212 = "2.12.17" val Scala213 = "2.13.8" val Scala3 = "3.1.2" -ThisBuild / crossScalaVersions := Seq(Scala3, "2.12.17", Scala213) +ThisBuild / crossScalaVersions := Seq(Scala3, Scala212, Scala213) ThisBuild / tlVersionIntroduced := Map("3" -> "3.1.1") ThisBuild / tlJdkRelease := Some(8) @@ -127,6 +128,7 @@ val OldGuardJava = JavaSpec.temurin("8") val LTSJava = JavaSpec.temurin("11") val LatestJava = JavaSpec.temurin("17") val ScalaJSJava = OldGuardJava +val ScalaNativeJava = OldGuardJava val GraalVM = JavaSpec.graalvm("11") ThisBuild / githubWorkflowJavaVersions := Seq(OldGuardJava, LTSJava, LatestJava, GraalVM) @@ -167,6 +169,11 @@ ThisBuild / githubWorkflowBuild := Seq( name = Some("Test Example JavaScript App Using Node"), cond = Some(s"matrix.ci == 'ciJS' && matrix.os == '$PrimaryOS'") ), + WorkflowStep.Run( + List("example/test-native.sh ${{ matrix.scala }}"), + name = Some("Test Example Native App Using Binary"), + cond = Some(s"matrix.ci == 'ciNative' && matrix.os == '$PrimaryOS'") + ), WorkflowStep.Run( List("cd scalafix", "sbt test"), name = Some("Scalafix tests"), @@ -209,12 +216,28 @@ ThisBuild / githubWorkflowBuildMatrixExclusions := { MatrixExclude(Map("os" -> MacOS, "ci" -> ci))) } + val nativeJavaAndOSFilters = { + val ci = CI.Native.command + + val javaFilters = + (ThisBuild / githubWorkflowJavaVersions).value.filterNot(Set(ScalaNativeJava)).map { + java => MatrixExclude(Map("ci" -> ci, "java" -> java.render)) + } + + javaFilters ++ Seq( + MatrixExclude(Map("os" -> Windows, "ci" -> ci)), + MatrixExclude(Map("os" -> MacOS, "ci" -> ci, "scala" -> Scala212)), + // keep a native+2.13+macos job + MatrixExclude(Map("os" -> MacOS, "ci" -> ci, "scala" -> Scala3)) + ) + } + // Nice-to-haves but unreliable in CI val flakyFilters = Seq( MatrixExclude(Map("os" -> Windows, "java" -> GraalVM.render)) ) - scalaJavaFilters ++ windowsAndMacScalaFilters ++ jsScalaFilters ++ jsJavaAndOSFilters ++ flakyFilters + scalaJavaFilters ++ windowsAndMacScalaFilters ++ jsScalaFilters ++ jsJavaAndOSFilters ++ nativeJavaAndOSFilters ++ flakyFilters } lazy val useJSEnv = @@ -265,6 +288,7 @@ tlReplaceCommandAlias("ci", CI.AllCIs.map(_.toString).mkString) addCommandAlias("release", "tlRelease") addCommandAlias(CI.JVM.command, CI.JVM.toString) +addCommandAlias(CI.Native.command, CI.Native.toString) addCommandAlias(CI.JS.command, CI.JS.toString) addCommandAlias(CI.Firefox.command, CI.Firefox.toString) addCommandAlias(CI.Chrome.command, CI.Chrome.toString) @@ -276,12 +300,27 @@ addCommandAlias( val jsProjects: Seq[ProjectReference] = Seq(kernel.js, kernelTestkit.js, laws.js, core.js, testkit.js, testsJS, std.js, example.js) +val nativeProjects: Seq[ProjectReference] = + Seq( + kernel.native, + kernelTestkit.native, + laws.native, + core.native, + testkit.native, + tests.native, + std.native, + example.native) + val undocumentedRefs = - jsProjects ++ Seq[ProjectReference](benchmarks, example.jvm, tests.jvm, tests.js) + jsProjects ++ nativeProjects ++ Seq[ProjectReference]( + benchmarks, + example.jvm, + tests.jvm, + tests.js) lazy val root = project .in(file(".")) - .aggregate(rootJVM, rootJS) + .aggregate(rootJVM, rootJS, rootNative) .enablePlugins(NoPublishPlugin) .enablePlugins(ScalaUnidocPlugin) .settings( @@ -306,11 +345,13 @@ lazy val rootJVM = project lazy val rootJS = project.aggregate(jsProjects: _*).enablePlugins(NoPublishPlugin) +lazy val rootNative = project.aggregate(nativeProjects: _*).enablePlugins(NoPublishPlugin) + /** * The core abstractions and syntax. This is the most general definition of Cats Effect, without * any concrete implementations. This is the "batteries not included" dependency. */ -lazy val kernel = crossProject(JSPlatform, JVMPlatform) +lazy val kernel = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("kernel")) .settings( name := "cats-effect-kernel", @@ -322,12 +363,15 @@ lazy val kernel = crossProject(JSPlatform, JVMPlatform) .jsSettings( libraryDependencies += "org.scala-js" %%% "scala-js-macrotask-executor" % MacrotaskExecutorVersion % Test ) + .nativeSettings( + libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.4.0" + ) /** * Reference implementations (including a pure ConcurrentBracket), generic ScalaCheck * generators, and useful tools for testing code written against Cats Effect. */ -lazy val kernelTestkit = crossProject(JSPlatform, JVMPlatform) +lazy val kernelTestkit = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("kernel-testkit")) .dependsOn(kernel) .settings( @@ -357,7 +401,7 @@ lazy val kernelTestkit = crossProject(JSPlatform, JVMPlatform) * dependency issues. As a consequence of this split, some things which are defined in * kernelTestkit are *tested* in the Test scope of this project. */ -lazy val laws = crossProject(JSPlatform, JVMPlatform) +lazy val laws = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("laws")) .dependsOn(kernel, kernelTestkit % Test) .settings( @@ -372,7 +416,7 @@ lazy val laws = crossProject(JSPlatform, JVMPlatform) * contains some general datatypes built on top of IO which are useful in their own right, as * well as some utilities (such as IOApp). This is the "batteries included" dependency. */ -lazy val core = crossProject(JSPlatform, JVMPlatform) +lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("core")) .dependsOn(kernel, std) .settings( @@ -711,7 +755,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) * Test support for the core project, providing various helpful instances like ScalaCheck * generators for IO and SyncIO. */ -lazy val testkit = crossProject(JSPlatform, JVMPlatform) +lazy val testkit = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("testkit")) .dependsOn(core, kernelTestkit) .settings( @@ -725,7 +769,7 @@ lazy val testkit = crossProject(JSPlatform, JVMPlatform) /** * Unit tests for the core project, utilizing the support provided by testkit. */ -lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform) +lazy val tests: CrossProject = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("tests")) .dependsOn(core, laws % Test, kernelTestkit % Test, testkit % Test) .enablePlugins(BuildInfoPlugin, NoPublishPlugin) @@ -773,7 +817,7 @@ lazy val testsJVM = tests * implementations will require IO, and thus those tests will be located within the core * project. */ -lazy val std = crossProject(JSPlatform, JVMPlatform) +lazy val std = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("std")) .dependsOn(kernel) .settings( @@ -826,7 +870,7 @@ lazy val std = crossProject(JSPlatform, JVMPlatform) * A trivial pair of trivial example apps primarily used to show that IOApp works as a practical * runtime on both target platforms. */ -lazy val example = crossProject(JSPlatform, JVMPlatform) +lazy val example = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("example")) .dependsOn(core) .enablePlugins(NoPublishPlugin) diff --git a/core/js/src/main/scala/cats/effect/IOFiberConstants.scala b/core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala similarity index 100% rename from core/js/src/main/scala/cats/effect/IOFiberConstants.scala rename to core/js-native/src/main/scala/cats/effect/IOFiberConstants.scala diff --git a/core/js/src/main/scala/cats/effect/IOFiberPlatform.scala b/core/js-native/src/main/scala/cats/effect/IOFiberPlatform.scala similarity index 100% rename from core/js/src/main/scala/cats/effect/IOFiberPlatform.scala rename to core/js-native/src/main/scala/cats/effect/IOFiberPlatform.scala diff --git a/core/js/src/main/scala/cats/effect/SyncIOConstants.scala b/core/js-native/src/main/scala/cats/effect/SyncIOConstants.scala similarity index 100% rename from core/js/src/main/scala/cats/effect/SyncIOConstants.scala rename to core/js-native/src/main/scala/cats/effect/SyncIOConstants.scala diff --git a/core/js/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala b/core/js-native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala similarity index 100% rename from core/js/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala rename to core/js-native/src/main/scala/cats/effect/unsafe/IORuntimeBuilderPlatform.scala diff --git a/core/js/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala b/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala similarity index 75% rename from core/js/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala rename to core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala index a20376708d..d8cf2e01b8 100644 --- a/core/js/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala +++ b/core/js-native/src/main/scala/cats/effect/unsafe/WorkStealingThreadPool.scala @@ -28,4 +28,12 @@ private[effect] sealed abstract class WorkStealingThreadPool private () def reportFailure(cause: Throwable): Unit private[effect] def reschedule(runnable: Runnable): Unit private[effect] def canExecuteBlockingCode(): Boolean + private[unsafe] def liveFibers() + : (Set[Runnable], Map[WorkerThread, (Option[Runnable], Set[Runnable])], Set[Runnable]) +} + +private[unsafe] sealed abstract class WorkerThread private () extends Thread { + private[unsafe] def isOwnedBy(threadPool: WorkStealingThreadPool): Boolean + private[unsafe] def monitor(fiber: Runnable): WeakBag.Handle + private[unsafe] def index: Int } diff --git a/core/js/src/main/scala/cats/effect/IOApp.scala b/core/js/src/main/scala/cats/effect/IOApp.scala index 45cb5241a1..96b66f029a 100644 --- a/core/js/src/main/scala/cats/effect/IOApp.scala +++ b/core/js/src/main/scala/cats/effect/IOApp.scala @@ -55,10 +55,10 @@ import scala.util.Try * produce an exit code of 1. * * Note that exit codes are an implementation-specific feature of the underlying runtime, as are - * process arguments. Naturally, all JVMs support these functions, as does NodeJS, but some - * JavaScript execution environments will be unable to replicate these features (or they simply - * may not make sense). In such cases, exit codes may be ignored and/or argument lists may be - * empty. + * process arguments. Naturally, all JVMs support these functions, as does Node.js and Scala + * Native, but some JavaScript execution environments will be unable to replicate these features + * (or they simply may not make sense). In such cases, exit codes may be ignored and/or argument + * lists may be empty. * * Note that in the case of the above example, we would actually be better off using * [[IOApp.Simple]] rather than `IOApp` directly, since we are neither using `args` nor are we diff --git a/core/jvm/src/main/scala/cats/effect/SyncIOCompanionPlatform.scala b/core/jvm-native/src/main/scala/cats/effect/SyncIOCompanionPlatform.scala similarity index 100% rename from core/jvm/src/main/scala/cats/effect/SyncIOCompanionPlatform.scala rename to core/jvm-native/src/main/scala/cats/effect/SyncIOCompanionPlatform.scala diff --git a/core/jvm/src/main/scala/cats/effect/syntax/DispatcherSyntax.scala b/core/jvm-native/src/main/scala/cats/effect/syntax/DispatcherSyntax.scala similarity index 100% rename from core/jvm/src/main/scala/cats/effect/syntax/DispatcherSyntax.scala rename to core/jvm-native/src/main/scala/cats/effect/syntax/DispatcherSyntax.scala diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala similarity index 100% rename from core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala rename to core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala index 823977461e..aee5a45e96 100644 --- a/core/jvm/src/main/scala/cats/effect/unsafe/FiberMonitor.scala +++ b/core/jvm-native/src/main/scala/cats/effect/unsafe/FiberMonitor.scala @@ -218,14 +218,14 @@ private[effect] object FiberMonitor { } } + private[FiberMonitor] final val BagReferences + : ConcurrentLinkedQueue[WeakReference[WeakBag[Runnable]]] = + new ConcurrentLinkedQueue() + private[FiberMonitor] final val Bags: ThreadLocal[WeakBag[Runnable]] = ThreadLocal.withInitial { () => val bag = new WeakBag[Runnable]() BagReferences.offer(new WeakReference(bag)) bag } - - private[FiberMonitor] final val BagReferences - : ConcurrentLinkedQueue[WeakReference[WeakBag[Runnable]]] = - new ConcurrentLinkedQueue() } diff --git a/core/jvm/src/main/scala/cats/effect/unsafe/ref/package.scala b/core/jvm-native/src/main/scala/cats/effect/unsafe/ref/package.scala similarity index 100% rename from core/jvm/src/main/scala/cats/effect/unsafe/ref/package.scala rename to core/jvm-native/src/main/scala/cats/effect/unsafe/ref/package.scala diff --git a/core/jvm/src/main/scala/cats/effect/IOApp.scala b/core/jvm/src/main/scala/cats/effect/IOApp.scala index f6e151283c..38e2abb46b 100644 --- a/core/jvm/src/main/scala/cats/effect/IOApp.scala +++ b/core/jvm/src/main/scala/cats/effect/IOApp.scala @@ -59,10 +59,10 @@ import java.util.concurrent.atomic.AtomicInteger * produce an exit code of 1. * * Note that exit codes are an implementation-specific feature of the underlying runtime, as are - * process arguments. Naturally, all JVMs support these functions, as does NodeJS, but some - * JavaScript execution environments will be unable to replicate these features (or they simply - * may not make sense). In such cases, exit codes may be ignored and/or argument lists may be - * empty. + * process arguments. Naturally, all JVMs support these functions, as does Node.js and Scala + * Native, but some JavaScript execution environments will be unable to replicate these features + * (or they simply may not make sense). In such cases, exit codes may be ignored and/or argument + * lists may be empty. * * Note that in the case of the above example, we would actually be better off using * [[IOApp.Simple]] rather than `IOApp` directly, since we are neither using `args` nor are we diff --git a/core/native/src/main/scala/cats/effect/IOApp.scala b/core/native/src/main/scala/cats/effect/IOApp.scala new file mode 100644 index 0000000000..270f9dc3b7 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/IOApp.scala @@ -0,0 +1,251 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect + +import scala.concurrent.CancellationException +import scala.concurrent.duration._ + +/** + * The primary entry point to a Cats Effect application. Extend this trait rather than defining + * your own `main` method. This avoids the need to run [[IO.unsafeRunAsync]] (or similar) on + * your own. + * + * `IOApp` takes care of the messy details of properly setting up (and tearing down) the + * [[unsafe.IORuntime]] needed to run the [[IO]] which represents your application. All of the + * associated thread pools (if relevant) will be configured with the assumption that your + * application is fully contained within the `IO` produced by the [[run]] method. Note that the + * exact details of how the runtime will be configured are very platform-specific. Part of the + * point of `IOApp` is to insulate users from the details of the underlying runtime (whether JVM + * or JavaScript). + * + * {{{ + * object MyApplication extends IOApp { + * def run(args: List[String]) = + * for { + * _ <- IO.print("Enter your name: ") + * name <- IO.readln + * _ <- IO.println("Hello, " + name) + * } yield ExitCode.Success + * } + * }}} + * + * In the above example, `MyApplication` will be a runnable class with a `main` method, visible + * to Sbt, IntelliJ, or plain-old `java`. When run externally, it will print, read, and print in + * the obvious way, producing a final process exit code of 0. Any exceptions thrown within the + * `IO` will be printed to standard error and the exit code will be set to 1. In the event that + * the main [[Fiber]] (represented by the `IO` returned by `run`) is canceled, the runtime will + * produce an exit code of 1. + * + * Note that exit codes are an implementation-specific feature of the underlying runtime, as are + * process arguments. Naturally, all JVMs support these functions, as does Node.js and Scala + * Native, but some JavaScript execution environments will be unable to replicate these features + * (or they simply may not make sense). In such cases, exit codes may be ignored and/or argument + * lists may be empty. + * + * Note that in the case of the above example, we would actually be better off using + * [[IOApp.Simple]] rather than `IOApp` directly, since we are neither using `args` nor are we + * explicitly producing a custom [[ExitCode]]: + * + * {{{ + * object MyApplication extends IOApp.Simple { + * val run = + * for { + * _ <- IO.print("Enter your name: ") + * name <- IO.readln + * _ <- IO.println(s"Hello, " + name) + * } yield () + * } + * }}} + * + * It is valid to define `val run` rather than `def run` because `IO`'s evaluation is lazy: it + * will only run when the `main` method is invoked by the runtime. + * + * In the event that the process receives an interrupt signal (`SIGINT`) due to Ctrl-C (or any + * other mechanism), it will immediately `cancel` the main fiber. Assuming this fiber is not + * within an `uncancelable` region, this will result in interrupting any current activities and + * immediately invoking any finalizers (see: [[IO.onCancel]] and [[IO.bracket]]). The process + * will not shut down until the finalizers have completed. For example: + * + * {{{ + * object InterruptExample extends IOApp.Simple { + * val run = + * IO.bracket(startServer)( + * _ => IO.never)( + * server => IO.println("shutting down") *> server.close) + * } + * }}} + * + * If we assume the `startServer` function has type `IO[Server]` (or similar), this kind of + * pattern is very common. When this process receives a `SIGINT`, it will immediately print + * "shutting down" and run the `server.close` effect. + * + * One consequence of this design is it is possible to build applications which will ignore + * process interrupts. For example, if `server.close` runs forever, the process will ignore + * interrupts and will need to be cleaned up using `SIGKILL` (i.e. `kill -9`). This same + * phenomenon can be demonstrated by using [[IO.uncancelable]] to suppress all interruption + * within the application itself: + * + * {{{ + * object Zombie extends IOApp.Simple { + * val run = IO.never.uncancelable + * } + * }}} + * + * The above process will run forever and ignore all interrupts. The only way it will shut down + * is if it receives `SIGKILL`. + * + * It is possible (though not necessary) to override various platform-specific runtime + * configuration options, such as `computeWorkerThreadCount` (which only exists on the JVM). + * Please note that the default configurations have been extensively benchmarked and are optimal + * (or close to it) in most conventional scenarios. + * + * However, with that said, there really is no substitute to benchmarking your own application. + * Every application and scenario is unique, and you will always get the absolute best results + * by performing your own tuning rather than trusting someone else's defaults. `IOApp`'s + * defaults are very ''good'', but they are not perfect in all cases. One common example of this + * is applications which maintain network or file I/O worker threads which are under heavy load + * in steady-state operations. In such a performance profile, it is usually better to reduce the + * number of compute worker threads to "make room" for the I/O workers, such that they all sum + * to the number of physical threads exposed by the kernel. + * + * @note + * The Scala Native runtime has several limitations compared to its JVM and JS counterparts + * and should generally be considered experimental at this stage. Limitations include: + * - No blocking threadpool: [[IO.blocking]] will simply block the main thread + * - No support for graceful termination: finalizers will not run on external cancelation + * - No support for tracing or fiber dumps + * + * @see + * [[IO]] + * @see + * [[run]] + * @see + * [[ResourceApp]] + * @see + * [[IOApp.Simple]] + */ +trait IOApp { + + private[this] var _runtime: unsafe.IORuntime = null + + /** + * The runtime which will be used by `IOApp` to evaluate the [[IO]] produced by the `run` + * method. This may be overridden by `IOApp` implementations which have extremely specialized + * needs, but this is highly unlikely to ever be truly needed. As an example, if an + * application wishes to make use of an alternative compute thread pool (such as + * `Executors.fixedThreadPool`), it is almost always better to leverage [[IO.evalOn]] on the + * value produced by the `run` method, rather than directly overriding `runtime`. + * + * In other words, this method is made available to users, but its use is strongly discouraged + * in favor of other, more precise solutions to specific use-cases. + * + * This value is guaranteed to be equal to [[unsafe.IORuntime.global]]. + */ + protected def runtime: unsafe.IORuntime = _runtime + + /** + * The configuration used to initialize the [[runtime]] which will evaluate the [[IO]] + * produced by `run`. It is very unlikely that users will need to override this method. + */ + protected def runtimeConfig: unsafe.IORuntimeConfig = unsafe.IORuntimeConfig() + + /** + * The entry point for your application. Will be called by the runtime when the process is + * started. If the underlying runtime supports it, any arguments passed to the process will be + * made available in the `args` parameter. The numeric value within the resulting [[ExitCode]] + * will be used as the exit code when the process terminates unless terminated exceptionally + * or by interrupt. + * + * @param args + * The arguments passed to the process, if supported by the underlying runtime. For example, + * `java com.company.MyApp --foo --bar baz` or `node com-mycompany-fastopt.js --foo --bar + * baz` would each result in `List("--foo", "--bar", "baz")`. + * @see + * [[IOApp.Simple!.run:cats\.effect\.IO[Unit]*]] + */ + def run(args: List[String]): IO[ExitCode] + + final def main(args: Array[String]): Unit = { + if (runtime == null) { + import unsafe.IORuntime + + val installed = IORuntime installGlobal { + IORuntime( + IORuntime.defaultComputeExecutionContext, + IORuntime.defaultComputeExecutionContext, + IORuntime.defaultScheduler, + () => (), + runtimeConfig) + } + + if (!installed) { + System + .err + .println( + "WARNING: Cats Effect global runtime already initialized; custom configurations will be ignored") + } + + _runtime = IORuntime.global + } + + // An infinite heartbeat to keep main alive. This is similar to + // `IO.never`, except `IO.never` doesn't schedule any tasks and is + // insufficient to keep main alive. The tick is fast enough that + // it isn't silently discarded, as longer ticks are, but slow + // enough that we don't interrupt often. 1 hour was chosen + // empirically. + lazy val keepAlive: IO[Nothing] = + IO.sleep(1.hour) >> keepAlive + + Spawn[IO] + .raceOutcome[ExitCode, Nothing](run(args.toList), keepAlive) + .flatMap { + case Left(Outcome.Canceled()) => + IO.raiseError(new CancellationException("IOApp main fiber was canceled")) + case Left(Outcome.Errored(t)) => IO.raiseError(t) + case Left(Outcome.Succeeded(code)) => code + case Right(Outcome.Errored(t)) => IO.raiseError(t) + case Right(_) => sys.error("impossible") + } + .unsafeRunFiber( + System.exit(0), + t => { + t.printStackTrace() + System.exit(1) + }, + c => System.exit(c.code) + )(runtime) + + () + } + +} + +object IOApp { + + /** + * A simplified version of [[IOApp]] for applications which ignore their process arguments and + * always produces [[ExitCode.Success]] (unless terminated exceptionally or interrupted). + * + * @see + * [[IOApp]] + */ + trait Simple extends IOApp { + def run: IO[Unit] + final def run(args: List[String]): IO[ExitCode] = run.as(ExitCode.Success) + } +} diff --git a/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala new file mode 100644 index 0000000000..71e71c7003 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/IOCompanionPlatform.scala @@ -0,0 +1,65 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect + +import cats.effect.std.Console + +import java.time.Instant + +private[effect] abstract class IOCompanionPlatform { this: IO.type => + + private[this] val TypeDelay = Sync.Type.Delay + + def blocking[A](thunk: => A): IO[A] = + // do our best to mitigate blocking + IO.cede *> apply(thunk).guarantee(IO.cede) + + private[effect] def interruptible[A](many: Boolean, thunk: => A): IO[A] = { + val _ = many + blocking(thunk) + } + + def interruptible[A](thunk: => A): IO[A] = interruptible(false, thunk) + + def interruptibleMany[A](thunk: => A): IO[A] = interruptible(true, thunk) + + def suspend[A](hint: Sync.Type)(thunk: => A): IO[A] = + if (hint eq TypeDelay) + apply(thunk) + else + blocking(thunk) + + def realTimeInstant: IO[Instant] = asyncForIO.realTimeInstant + + /** + * Reads a line as a string from the standard input using the platform's default charset, as + * per `java.nio.charset.Charset.defaultCharset()`. + * + * The effect can raise a `java.io.EOFException` if no input has been consumed before the EOF + * is observed. This should never happen with the standard input, unless it has been replaced + * with a finite `java.io.InputStream` through `java.lang.System#setIn` or similar. + * + * @see + * `cats.effect.std.Console#readLineWithCharset` for reading using a custom + * `java.nio.charset.Charset` + * + * @return + * an IO effect that describes reading the user's input from the standard input as a string + */ + def readLine: IO[String] = + Console[IO].readLine +} diff --git a/std/jvm/src/main/scala/cats/effect/std/RandomCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/IOPlatform.scala similarity index 87% rename from std/jvm/src/main/scala/cats/effect/std/RandomCompanionPlatform.scala rename to core/native/src/main/scala/cats/effect/IOPlatform.scala index a5da2dc9a7..50edeb36d6 100644 --- a/std/jvm/src/main/scala/cats/effect/std/RandomCompanionPlatform.scala +++ b/core/native/src/main/scala/cats/effect/IOPlatform.scala @@ -14,7 +14,6 @@ * limitations under the License. */ -package cats.effect.std +package cats.effect -// Vestigial shim -private[std] trait RandomCompanionPlatform +abstract private[effect] class IOPlatform[+A] diff --git a/core/native/src/main/scala/cats/effect/tracing/TracingConstants.scala b/core/native/src/main/scala/cats/effect/tracing/TracingConstants.scala new file mode 100644 index 0000000000..3c57fdcd3e --- /dev/null +++ b/core/native/src/main/scala/cats/effect/tracing/TracingConstants.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect +package tracing + +private[effect] object TracingConstants { + + final val isCachedStackTracing = false + + final val isFullStackTracing = false + + final val isStackTracing = isFullStackTracing || isCachedStackTracing +} diff --git a/core/native/src/main/scala/cats/effect/tracing/TracingPlatform.scala b/core/native/src/main/scala/cats/effect/tracing/TracingPlatform.scala new file mode 100644 index 0000000000..f214f0a832 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/tracing/TracingPlatform.scala @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect.tracing + +import scala.annotation.nowarn + +private[tracing] abstract class TracingPlatform { self: Tracing.type => + + @nowarn("msg=never used") + def calculateTracingEvent(key: Any): TracingEvent = null + + @nowarn("msg=never used") + private[tracing] def applyStackTraceFilter( + callSiteClassName: String, + callSiteMethodName: String, + callSiteFileName: String): Boolean = false + + private[tracing] def decodeMethodName(name: String): String = name + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala new file mode 100644 index 0000000000..42c0d19b1c --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect.unsafe + +import scala.concurrent.ExecutionContext + +private[unsafe] abstract class IORuntimeCompanionPlatform { this: IORuntime.type => + + def defaultComputeExecutionContext: ExecutionContext = QueueExecutorScheduler + + def defaultScheduler: Scheduler = QueueExecutorScheduler + + private[this] var _global: IORuntime = null + + private[effect] def installGlobal(global: => IORuntime): Boolean = { + if (_global == null) { + _global = global + true + } else { + false + } + } + + private[effect] def resetGlobal(): Unit = + _global = null + + lazy val global: IORuntime = { + if (_global == null) { + installGlobal { + IORuntime( + defaultComputeExecutionContext, + defaultComputeExecutionContext, + defaultScheduler, + () => (), + IORuntimeConfig()) + } + } + + _global + } + + private[effect] def registerFiberMonitorMBean(fiberMonitor: FiberMonitor): () => Unit = { + val _ = fiberMonitor + () => () + } +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/IORuntimeConfigCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeConfigCompanionPlatform.scala new file mode 100644 index 0000000000..d363aada8b --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/IORuntimeConfigCompanionPlatform.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect +package unsafe + +import scala.concurrent.duration.Duration +import scala.util.Try + +private[unsafe] abstract class IORuntimeConfigCompanionPlatform { this: IORuntimeConfig.type => + // TODO make the cancelation and auto-yield properties have saner names + protected final val Default: IORuntimeConfig = { + val cancelationCheckThreshold = + Option(System.getenv("CATS_EFFECT_CANCELATION_CHECK_THRESHOLD")) + .flatMap(x => Try(x.toInt).toOption) + .getOrElse(512) + + val autoYieldThreshold = + Option(System.getenv("CATS_EFFECT_AUTO_YIELD_THRESHOLD_MULTIPLIER")) + .flatMap(x => Try(x.toInt).toOption) + .getOrElse(2) * cancelationCheckThreshold + + val enhancedExceptions = Option(System.getenv("CATS_EFFECT_TRACING_EXCEPTIONS_ENHANCED")) + .flatMap(x => Try(x.toBoolean).toOption) + .getOrElse(DefaultEnhancedExceptions) + + val traceBufferSize = Option(System.getenv("CATS_EFFECT_TRACING_BUFFER_SIZE")) + .flatMap(x => Try(x.toInt).toOption) + .getOrElse(DefaultTraceBufferSize) + + val shutdownHookTimeout = Option(System.getenv("CATS_EFFECT_SHUTDOWN_HOOK_TIMEOUT")) + .flatMap(x => Try(Duration(x)).toOption) + .getOrElse(DefaultShutdownHookTimeout) + + apply( + cancelationCheckThreshold, + autoYieldThreshold, + enhancedExceptions, + traceBufferSize, + shutdownHookTimeout) + } +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala new file mode 100644 index 0000000000..c949e2b8c6 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/PollingExecutorScheduler.scala @@ -0,0 +1,154 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect +package unsafe + +import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} +import scala.concurrent.duration._ +import scala.scalanative.libc.errno +import scala.scalanative.meta.LinktimeInfo +import scala.scalanative.unsafe._ +import scala.util.control.NonFatal + +import java.util.{ArrayDeque, PriorityQueue} + +abstract class PollingExecutorScheduler(pollEvery: Int) + extends ExecutionContextExecutor + with Scheduler { + + private[this] var needsReschedule: Boolean = true + + private[this] val executeQueue: ArrayDeque[Runnable] = new ArrayDeque + private[this] val sleepQueue: PriorityQueue[SleepTask] = new PriorityQueue + + private[this] val noop: Runnable = () => () + + private[this] def scheduleIfNeeded(): Unit = if (needsReschedule) { + ExecutionContext.global.execute(() => loop()) + needsReschedule = false + } + + final def execute(runnable: Runnable): Unit = { + scheduleIfNeeded() + executeQueue.addLast(runnable) + } + + final def sleep(delay: FiniteDuration, task: Runnable): Runnable = + if (delay <= Duration.Zero) { + execute(task) + noop + } else { + scheduleIfNeeded() + val now = monotonicNanos() + val sleepTask = new SleepTask(now + delay.toNanos, task) + sleepQueue.offer(sleepTask) + sleepTask + } + + def reportFailure(t: Throwable): Unit = t.printStackTrace() + + def nowMillis() = System.currentTimeMillis() + + override def nowMicros(): Long = + if (LinktimeInfo.isFreeBSD || LinktimeInfo.isLinux || LinktimeInfo.isMac) { + import scala.scalanative.posix.time._ + import scala.scalanative.posix.timeOps._ + val ts = stackalloc[timespec]() + if (clock_gettime(CLOCK_REALTIME, ts) != 0) + throw new RuntimeException(s"clock_gettime: ${errno.errno}") + ts.tv_sec * 1000000 + ts.tv_nsec / 1000 + } else { + super.nowMicros() + } + + def monotonicNanos() = System.nanoTime() + + /** + * @param timeout + * the maximum duration for which to block. ''However'', if `timeout == Inf` and there are + * no remaining events to poll for, this method should return `false` immediately. This is + * unfortunate but necessary so that this `ExecutionContext` can yield to the Scala Native + * global `ExecutionContext` which is currently hard-coded into every test framework, + * including JUnit, MUnit, and specs2. + * + * @return + * whether poll should be called again (i.e., there are more events to be polled) + */ + protected def poll(timeout: Duration): Boolean + + private[this] def loop(): Unit = { + needsReschedule = false + + var continue = true + + while (continue) { + // execute the timers + val now = monotonicNanos() + while (!sleepQueue.isEmpty() && sleepQueue.peek().at <= now) { + val task = sleepQueue.poll() + try task.runnable.run() + catch { + case NonFatal(t) => reportFailure(t) + case t: Throwable => IOFiber.onFatalFailure(t) + } + } + + // do up to pollEvery tasks + var i = 0 + while (i < pollEvery && !executeQueue.isEmpty()) { + val runnable = executeQueue.poll() + try runnable.run() + catch { + case NonFatal(t) => reportFailure(t) + case t: Throwable => IOFiber.onFatalFailure(t) + } + i += 1 + } + + // finally we poll + val timeout = + if (!executeQueue.isEmpty()) + Duration.Zero + else if (!sleepQueue.isEmpty()) + Math.max(sleepQueue.peek().at - monotonicNanos(), 0).nanos + else + Duration.Inf + + val needsPoll = poll(timeout) + + continue = needsPoll || !executeQueue.isEmpty() || !sleepQueue.isEmpty() + } + + needsReschedule = true + } + + private[this] final class SleepTask( + val at: Long, + val runnable: Runnable + ) extends Runnable + with Comparable[SleepTask] { + + def run(): Unit = { + sleepQueue.remove(this) + () + } + + def compareTo(that: SleepTask): Int = + java.lang.Long.compare(this.at, that.at) + } + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/QueueExecutorScheduler.scala b/core/native/src/main/scala/cats/effect/unsafe/QueueExecutorScheduler.scala new file mode 100644 index 0000000000..c53036b5dc --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/QueueExecutorScheduler.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect.unsafe + +import scala.concurrent.duration._ + +// JVM WSTP sets ExternalQueueTicks = 64 so we steal it here +private[effect] object QueueExecutorScheduler extends PollingExecutorScheduler(64) { + + def poll(timeout: Duration): Boolean = { + if (timeout != Duration.Zero && timeout.isFinite) { + val nanos = timeout.toNanos + Thread.sleep(nanos / 1000000, (nanos % 1000000).toInt) + } + false + } + +} diff --git a/core/native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala b/core/native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala new file mode 100644 index 0000000000..f6e4964808 --- /dev/null +++ b/core/native/src/main/scala/cats/effect/unsafe/SchedulerCompanionPlatform.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect.unsafe + +private[unsafe] abstract class SchedulerCompanionPlatform { this: Scheduler.type => + + def createDefaultScheduler(): (Scheduler, () => Unit) = (QueueExecutorScheduler, () => ()) + +} diff --git a/example/js/src/main/scala/cats/effect/example/Example.scala b/example/js-native/src/main/scala/cats/effect/example/Example.scala similarity index 100% rename from example/js/src/main/scala/cats/effect/example/Example.scala rename to example/js-native/src/main/scala/cats/effect/example/Example.scala diff --git a/example/test-native.sh b/example/test-native.sh new file mode 100755 index 0000000000..79b6318fa5 --- /dev/null +++ b/example/test-native.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +# This script mostly just ensures that we can use native to run an example application. + +set -euo pipefail # STRICT MODE +IFS=$'\n\t' # http://redsymbol.net/articles/unofficial-bash-strict-mode/ + +cd $(dirname $0)/.. + +sbt ++$1 exampleNative/nativeLink + +output=$(mktemp) +expected=$(mktemp) + +cd example/native/target/scala-$(echo $1 | sed -E 's/^(2\.[0-9]+)\.[0-9]+$/\1/')/ + +set +e +./cats-effect-example-out left right > $output +result=$? +set -e + +if [[ $result -ne 2 ]]; then + exit 1 +fi + +echo $'left +left +left +left +left +right +right +right +right +right +left +left +left +left +left +right +right +right +right +right' > $expected + +exec diff $output $expected diff --git a/kernel/jvm/src/main/scala/cats/effect/kernel/ClockPlatform.scala b/kernel/jvm-native/src/main/scala/cats/effect/kernel/ClockPlatform.scala similarity index 100% rename from kernel/jvm/src/main/scala/cats/effect/kernel/ClockPlatform.scala rename to kernel/jvm-native/src/main/scala/cats/effect/kernel/ClockPlatform.scala diff --git a/kernel/native/src/main/scala/cats/effect/kernel/AsyncPlatform.scala b/kernel/native/src/main/scala/cats/effect/kernel/AsyncPlatform.scala new file mode 100644 index 0000000000..d05bcc3700 --- /dev/null +++ b/kernel/native/src/main/scala/cats/effect/kernel/AsyncPlatform.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect.kernel + +private[kernel] trait AsyncPlatform[F[_]] diff --git a/kernel/native/src/main/scala/cats/effect/kernel/ResourcePlatform.scala b/kernel/native/src/main/scala/cats/effect/kernel/ResourcePlatform.scala new file mode 100644 index 0000000000..8ea619eed5 --- /dev/null +++ b/kernel/native/src/main/scala/cats/effect/kernel/ResourcePlatform.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect.kernel + +private[effect] trait ResourcePlatform extends Serializable diff --git a/laws/shared/src/test/scala/cats/effect/laws/ReaderWriterStateTFreeSyncSpec.scala b/laws/shared/src/test/scala/cats/effect/laws/ReaderWriterStateTFreeSyncSpec.scala index ed817b6d0f..ab827850a0 100644 --- a/laws/shared/src/test/scala/cats/effect/laws/ReaderWriterStateTFreeSyncSpec.scala +++ b/laws/shared/src/test/scala/cats/effect/laws/ReaderWriterStateTFreeSyncSpec.scala @@ -26,6 +26,7 @@ import cats.laws.discipline.MiniInt import cats.laws.discipline.arbitrary._ import org.specs2.mutable._ +import org.specs2.scalacheck._ import org.typelevel.discipline.specs2.mutable.Discipline class ReaderWriterStateTFreeSyncSpec @@ -36,6 +37,12 @@ class ReaderWriterStateTFreeSyncSpec import FreeSyncGenerators._ import SyncTypeGenerators._ + implicit val params: Parameters = + if (cats.platform.Platform.isNative) + Parameters(minTestsOk = 5) + else + Parameters(minTestsOk = 100) + implicit val scala_2_12_is_buggy : Eq[FreeT[Eval, Either[Throwable, *], Either[Int, Either[Throwable, Int]]]] = eqFreeSync[Either[Throwable, *], Either[Int, Either[Throwable, Int]]] diff --git a/project/CI.scala b/project/CI.scala index af82753d19..7469822edd 100644 --- a/project/CI.scala +++ b/project/CI.scala @@ -65,6 +65,16 @@ object CI { suffixCommands = List("exampleJS/compile") ) + case object Native + extends CI( + command = "ciNative", + rootProject = "rootNative", + jsEnv = None, + testCommands = List("test"), + mimaReport = true, + suffixCommands = List("exampleNative/compile") + ) + case object Firefox extends CI( command = "ciFirefox", @@ -99,5 +109,5 @@ object CI { ) val AllJSCIs: List[CI] = List(JS, Firefox, Chrome) - val AllCIs: List[CI] = JVM :: AllJSCIs + val AllCIs: List[CI] = JVM :: Native :: AllJSCIs } diff --git a/project/Common.scala b/project/Common.scala index 93b5bcf1bf..8b574c501c 100644 --- a/project/Common.scala +++ b/project/Common.scala @@ -19,7 +19,10 @@ import sbt._, Keys._ import de.heikoseeberger.sbtheader.HeaderPlugin.autoImport._ import org.typelevel.sbt.TypelevelPlugin import org.typelevel.sbt.TypelevelKernelPlugin.autoImport._ +import org.typelevel.sbt.TypelevelMimaPlugin.autoImport._ import scalafix.sbt.ScalafixPlugin, ScalafixPlugin.autoImport._ +import sbtcrossproject.CrossPlugin.autoImport._ +import scalanativecrossproject.NativePlatform object Common extends AutoPlugin { @@ -33,6 +36,12 @@ object Common extends AutoPlugin { ), ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.6.0", ThisBuild / semanticdbEnabled := !tlIsScala3.value, - ThisBuild / semanticdbVersion := scalafixSemanticdb.revision + ThisBuild / semanticdbVersion := scalafixSemanticdb.revision, + tlVersionIntroduced ++= { + if (crossProjectPlatform.?.value.contains(NativePlatform)) + List("2.12", "2.13", "3").map(_ -> "3.4.0").toMap + else + Map.empty + } ) } diff --git a/project/plugins.sbt b/project/plugins.sbt index ed94418979..3c71aef1c4 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,6 +4,8 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.5.0-M2") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.7") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.2.0") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.3") diff --git a/std/js/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala b/std/js-native/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala similarity index 100% rename from std/js/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala rename to std/js-native/src/main/scala/cats/effect/std/MapRefCompanionPlatform.scala diff --git a/std/js/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala b/std/js-native/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala similarity index 100% rename from std/js/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala rename to std/js-native/src/main/scala/cats/effect/std/UUIDGenCompanionPlatform.scala diff --git a/std/jvm/src/main/scala/cats/effect/std/Console.scala b/std/jvm-native/src/main/scala/cats/effect/std/Console.scala similarity index 100% rename from std/jvm/src/main/scala/cats/effect/std/Console.scala rename to std/jvm-native/src/main/scala/cats/effect/std/Console.scala diff --git a/std/jvm/src/main/scala/cats/effect/std/EnvCompanionPlatform.scala b/std/jvm-native/src/main/scala/cats/effect/std/EnvCompanionPlatform.scala similarity index 100% rename from std/jvm/src/main/scala/cats/effect/std/EnvCompanionPlatform.scala rename to std/jvm-native/src/main/scala/cats/effect/std/EnvCompanionPlatform.scala diff --git a/std/js/src/main/scala/cats/effect/std/RandomCompanionPlatform.scala b/std/native/src/main/scala/cats/effect/std/DispatcherPlatform.scala similarity index 91% rename from std/js/src/main/scala/cats/effect/std/RandomCompanionPlatform.scala rename to std/native/src/main/scala/cats/effect/std/DispatcherPlatform.scala index a5da2dc9a7..b0bbdbfbfe 100644 --- a/std/js/src/main/scala/cats/effect/std/RandomCompanionPlatform.scala +++ b/std/native/src/main/scala/cats/effect/std/DispatcherPlatform.scala @@ -16,5 +16,4 @@ package cats.effect.std -// Vestigial shim -private[std] trait RandomCompanionPlatform +private[std] trait DispatcherPlatform[F[_]] diff --git a/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala b/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala new file mode 100644 index 0000000000..a69db02c97 --- /dev/null +++ b/std/native/src/main/scala/cats/effect/std/SecureRandomCompanionPlatform.scala @@ -0,0 +1,67 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect.std + +import scala.annotation.nowarn +import scala.scalanative.libc.errno +import scala.scalanative.unsafe._ +import scala.scalanative.unsigned._ + +private[std] trait SecureRandomCompanionPlatform { + + private[std] class JavaSecureRandom() extends java.util.Random(0L) { + + override def setSeed(x: Long): Unit = () + + override def nextBytes(bytes: Array[Byte]): Unit = { + val len = bytes.length + val buffer = stackalloc[Byte](256) + var i = 0 + while (i < len) { + val n = Math.min(256, len - i) + if (sysrandom.getentropy(buffer, n.toULong) < 0) + throw new RuntimeException(s"getentropy: ${errno.errno}") + + var j = 0L + while (j < n) { + bytes(i) = buffer(j) + i += 1 + j += 1 + } + } + } + + override protected final def next(numBits: Int): Int = { + if (numBits <= 0) { + 0 // special case because the formula on the last line is incorrect for numBits == 0 + } else { + val bytes = stackalloc[CInt]() + sysrandom.getentropy(bytes.asInstanceOf[Ptr[Byte]], sizeof[CInt]) + val rand32: Int = !bytes + rand32 & (-1 >>> (32 - numBits)) // Clear the (32 - numBits) higher order bits + } + } + + } + +} + +@extern +@nowarn +private[std] object sysrandom { + def getentropy(buf: Ptr[Byte], buflen: CSize): Int = extern +} diff --git a/std/shared/src/main/scala/cats/effect/std/Random.scala b/std/shared/src/main/scala/cats/effect/std/Random.scala index dd80bfabec..a45817e293 100644 --- a/std/shared/src/main/scala/cats/effect/std/Random.scala +++ b/std/shared/src/main/scala/cats/effect/std/Random.scala @@ -614,3 +614,6 @@ object Random extends RandomCompanionPlatform { } } + +// Vestigial shim +private[std] trait RandomCompanionPlatform diff --git a/std/shared/src/main/scala/cats/effect/std/SecureRandom.scala b/std/shared/src/main/scala/cats/effect/std/SecureRandom.scala index 3a8d7c0ba2..8332d2bcfa 100644 --- a/std/shared/src/main/scala/cats/effect/std/SecureRandom.scala +++ b/std/shared/src/main/scala/cats/effect/std/SecureRandom.scala @@ -109,6 +109,10 @@ object SecureRandom extends SecureRandomCompanionPlatform { ]: SecureRandom[IndexedReaderWriterStateT[F, E, L, S, S, *]] = SecureRandom[F].mapK(IndexedReaderWriterStateT.liftK) + /** + * @see + * implementation notes at [[[javaSecuritySecureRandom[F[_]](implicit*]]]. + */ def javaSecuritySecureRandom[F[_]: Sync](n: Int): F[SecureRandom[F]] = for { ref <- Ref[F].of(0) @@ -119,6 +123,19 @@ object SecureRandom extends SecureRandomCompanionPlatform { new ScalaRandom[F](selectRandom) with SecureRandom[F] {} } + /** + * On the JVM, delegates to [[java.security.SecureRandom]]. + * + * In browsers, delegates to the + * [[https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API Web Crypto API]]. + * + * In Node.js, delegates to the [[https://nodejs.org/api/crypto.html crypto module]]. + * + * On Native, delegates to + * [[https://man7.org/linux/man-pages/man3/getentropy.3.html getentropy]] which is supported + * on Linux, macOS, and BSD. Unsupported platforms such as Windows will encounter link-time + * errors. + */ def javaSecuritySecureRandom[F[_]: Sync]: F[SecureRandom[F]] = Sync[F] .delay(new JavaSecureRandom()) diff --git a/tests/shared/src/test/scala/cats/effect/IOFiberSpec.scala b/tests/js-jvm/src/test/scala/cats/effect/IOFiberSpec.scala similarity index 100% rename from tests/shared/src/test/scala/cats/effect/IOFiberSpec.scala rename to tests/js-jvm/src/test/scala/cats/effect/IOFiberSpec.scala diff --git a/tests/shared/src/test/scala/cats/effect/tracing/TraceSpec.scala b/tests/js-jvm/src/test/scala/cats/effect/tracing/TraceSpec.scala similarity index 100% rename from tests/shared/src/test/scala/cats/effect/tracing/TraceSpec.scala rename to tests/js-jvm/src/test/scala/cats/effect/tracing/TraceSpec.scala diff --git a/tests/shared/src/test/scala/cats/effect/tracing/TracingSpec.scala b/tests/js-jvm/src/test/scala/cats/effect/tracing/TracingSpec.scala similarity index 100% rename from tests/shared/src/test/scala/cats/effect/tracing/TracingSpec.scala rename to tests/js-jvm/src/test/scala/cats/effect/tracing/TracingSpec.scala diff --git a/tests/js/src/test/scala/cats/effect/ContSpecBasePlatform.scala b/tests/js-native/src/test/scala/cats/effect/ContSpecBasePlatform.scala similarity index 100% rename from tests/js/src/test/scala/cats/effect/ContSpecBasePlatform.scala rename to tests/js-native/src/test/scala/cats/effect/ContSpecBasePlatform.scala diff --git a/tests/js/src/main/scala/cats/effect/DetectPlatform.scala b/tests/js/src/main/scala/cats/effect/DetectPlatform.scala index f33ebb9821..8cce4b95c3 100644 --- a/tests/js/src/main/scala/cats/effect/DetectPlatform.scala +++ b/tests/js/src/main/scala/cats/effect/DetectPlatform.scala @@ -36,4 +36,5 @@ trait DetectPlatform { } def isJS: Boolean = true + def isNative: Boolean = false } diff --git a/tests/jvm/src/test/scala/cats/effect/SyncIOPlatformSpecification.scala b/tests/jvm-native/src/test/scala/cats/effect/SyncIOPlatformSpecification.scala similarity index 100% rename from tests/jvm/src/test/scala/cats/effect/SyncIOPlatformSpecification.scala rename to tests/jvm-native/src/test/scala/cats/effect/SyncIOPlatformSpecification.scala diff --git a/tests/jvm/src/test/scala/cats/effect/DetectPlatform.scala b/tests/jvm/src/test/scala/cats/effect/DetectPlatform.scala index 321fe8ac34..8131a2c7ec 100644 --- a/tests/jvm/src/test/scala/cats/effect/DetectPlatform.scala +++ b/tests/jvm/src/test/scala/cats/effect/DetectPlatform.scala @@ -19,4 +19,5 @@ package cats.effect trait DetectPlatform { def isWSL: Boolean = System.getProperty("os.version").contains("-WSL") def isJS: Boolean = false + def isNative: Boolean = false } diff --git a/tests/native/src/test/scala/cats/effect/DetectPlatform.scala b/tests/native/src/test/scala/cats/effect/DetectPlatform.scala new file mode 100644 index 0000000000..252b674256 --- /dev/null +++ b/tests/native/src/test/scala/cats/effect/DetectPlatform.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect + +trait DetectPlatform { + def isWSL: Boolean = System.getProperty("os.version").contains("-WSL") + def isJS: Boolean = false + def isNative: Boolean = true +} diff --git a/tests/native/src/test/scala/cats/effect/IOPlatformSpecification.scala b/tests/native/src/test/scala/cats/effect/IOPlatformSpecification.scala new file mode 100644 index 0000000000..2aecef7f76 --- /dev/null +++ b/tests/native/src/test/scala/cats/effect/IOPlatformSpecification.scala @@ -0,0 +1,34 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect + +import org.specs2.ScalaCheck + +trait IOPlatformSpecification { self: BaseSpec with ScalaCheck => + + def platformSpecs = "platform" should { + "realTimeInstant should return an Instant constructed from realTime" in ticked { + implicit ticker => + val op = for { + now <- IO.realTimeInstant + realTime <- IO.realTime + } yield now.toEpochMilli == realTime.toMillis + + op must completeAs(true) + } + } +} diff --git a/tests/native/src/test/scala/cats/effect/RunnersPlatform.scala b/tests/native/src/test/scala/cats/effect/RunnersPlatform.scala new file mode 100644 index 0000000000..5aaa1d4418 --- /dev/null +++ b/tests/native/src/test/scala/cats/effect/RunnersPlatform.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect + +import cats.effect.unsafe._ + +trait RunnersPlatform { + protected def runtime(): IORuntime = IORuntime.global +} diff --git a/tests/native/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala b/tests/native/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala new file mode 100644 index 0000000000..c55d919868 --- /dev/null +++ b/tests/native/src/test/scala/cats/effect/unsafe/SchedulerSpec.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2022 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cats.effect +package unsafe + +class SchedulerSpec extends BaseSpec { + + "Default scheduler" should { + "use high-precision time" in real { + for { + start <- IO.realTime + times <- IO.realTime.replicateA(100) + deltas = times.map(_ - start) + } yield deltas.exists(_.toMicros % 1000 != 0) + } + "correctly calculate real time" in real { + IO.realTime.product(IO(System.currentTimeMillis())).map { + case (realTime, currentTime) => + (realTime.toMillis - currentTime) should be_<=(1L) + } + } + } + +} diff --git a/tests/shared/src/test/scala/cats/effect/kernel/ParallelFSpec.scala b/tests/shared/src/test/scala/cats/effect/kernel/ParallelFSpec.scala index 36bfab8aa1..ab9a14a4ee 100644 --- a/tests/shared/src/test/scala/cats/effect/kernel/ParallelFSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/kernel/ParallelFSpec.scala @@ -25,9 +25,16 @@ import cats.laws.discipline.{AlignTests, CommutativeApplicativeTests, ParallelTe import cats.laws.discipline.arbitrary.catsLawsCogenForIor import cats.syntax.all._ +import org.specs2.scalacheck._ import org.typelevel.discipline.specs2.mutable.Discipline -class ParallelFSpec extends BaseSpec with Discipline { +class ParallelFSpec extends BaseSpec with Discipline with DetectPlatform { + + implicit val params: Parameters = + if (isNative) + Parameters(minTestsOk = 5) + else + Parameters(minTestsOk = 100) def alleyEq[E, A: Eq]: Eq[PureConc[E, A]] = { (x, y) => import Outcome._ diff --git a/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala b/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala index b9a8c04652..9b8d026adf 100644 --- a/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/QueueSpec.scala @@ -124,7 +124,7 @@ class BoundedQueueSpec extends BaseSpec with QueueTests[Queue] with DetectPlatfo } "offer/take at high contention" in real { - val size = if (isJS) 10000 else 100000 + val size = if (isJS || isNative) 10000 else 100000 val action = constructor(size) flatMap { q => def par(action: IO[Unit], num: Int): IO[Unit] = diff --git a/tests/shared/src/test/scala/cats/effect/std/SecureRandomSpec.scala b/tests/shared/src/test/scala/cats/effect/std/SecureRandomSpec.scala index 4d3f43c626..aff8b08940 100644 --- a/tests/shared/src/test/scala/cats/effect/std/SecureRandomSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/std/SecureRandomSpec.scala @@ -26,7 +26,17 @@ class SecureRandomSpec extends BaseSpec { bytes1 <- random1.nextBytes(128) random2 <- SecureRandom.javaSecuritySecureRandom[IO](2) bytes2 <- random2.nextBytes(256) - } yield bytes1.length == 128 && bytes2.length == 256 + bytes3 <- random2.nextBytes(1024) + } yield bytes1.length == 128 && bytes2.length == 256 && bytes3.length == 1024 + } + + "overrides nextInt" in real { + for { + secureRandom <- SecureRandom.javaSecuritySecureRandom[IO] + secureInts <- secureRandom.nextInt.replicateA(3) + insecureRandom <- Random.scalaUtilRandomSeedInt[IO](0) + insecureInts <- insecureRandom.nextInt.replicateA(3) + } yield secureInts != insecureInts } }