diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9852f8..fe04789 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,11 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [0.26.0, 0.27.0-RC1, 2.12.11, 2.13.2] - java: [adopt@1.8, adopt@11, adopt@14, graalvm@20.1.0] + scala: [0.27.0-RC1, 3.0.0-M1, 2.12.12, 2.13.3] + java: + - adopt@1.8 + - adopt@1.11 + - graalvm-ce-java8@20.2.0 runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (full) @@ -32,7 +35,7 @@ jobs: fetch-depth: 0 - name: Setup Java and Scala - uses: olafurpg/setup-scala@v5 + uses: olafurpg/setup-scala@v10 with: java-version: ${{ matrix.java }} diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml index 9c4729e..b535fcc 100644 --- a/.github/workflows/clean.yml +++ b/.github/workflows/clean.yml @@ -19,7 +19,7 @@ jobs: - name: Delete artifacts run: | # Customize those three lines with your repository and credentials: - REPO=https://api.github.com/repos/$1 + REPO=${GITHUB_API_URL}/repos/${{ github.repository }} # A shortcut to call GitHub API. ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } diff --git a/.gitignore b/.gitignore index 2f1e8cf..735b268 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ target/ # Ignore [ce]tags files tags + +.bsp diff --git a/build.sbt b/build.sbt index 0f5b894..707bff7 100644 --- a/build.sbt +++ b/build.sbt @@ -24,16 +24,19 @@ ThisBuild / publishFullName := "Daniel Spiewak" ThisBuild / strictSemVer := false -ThisBuild / crossScalaVersions := Seq("0.26.0", "0.27.0-RC1", "2.12.11", "2.13.2") +ThisBuild / crossScalaVersions := Seq("0.27.0-RC1", "3.0.0-M1", "2.12.12", "2.13.3") -ThisBuild / githubWorkflowJavaVersions := Seq("adopt@1.8", "adopt@11", "adopt@14", "graalvm@20.1.0") +// Restore running the CI on Java 15 (https://github.com/lampepfl/dotty/issues/10131). +ThisBuild / githubWorkflowJavaVersions := Seq("adopt@1.8", "adopt@1.11", "graalvm-ce-java8@20.2.0") Global / homepage := Some(url("https://github.com/typelevel/coop")) Global / scmInfo := Some( ScmInfo( url("https://github.com/typelevel/coop"), - "git@github.com:typelevel/coop.git")) + "git@github.com:typelevel/coop.git" + ) +) lazy val root = project.in(file(".")).aggregate(core.jvm, core.js) .settings(noPublishSettings) @@ -45,13 +48,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform).in(file("core")) libraryDependencies ++= Seq( "org.typelevel" %%% "cats-free" % "2.2.0", "org.typelevel" %%% "cats-mtl" % "1.0.0", - - "org.specs2" %%% "specs2-core" % "4.10.3" % Test), - - mimaPreviousArtifacts := { - val old = mimaPreviousArtifacts.value - if (isDotty.value) Set() else old - }) - .settings(dottyLibrarySettings) - .settings(dottyJsSettings(ThisBuild / crossScalaVersions)) + "org.specs2" %%% "specs2-core" % "4.10.5" % Test + ).map(_.withDottyCompat(scalaVersion.value)) + ) diff --git a/core/shared/src/main/scala/coop/ApplicativeThread.scala b/core/shared/src/main/scala/coop/ApplicativeThread.scala index cd7f865..aea8328 100644 --- a/core/shared/src/main/scala/coop/ApplicativeThread.scala +++ b/core/shared/src/main/scala/coop/ApplicativeThread.scala @@ -17,7 +17,7 @@ package coop import cats.{Applicative, InjectK, Monad} -import cats.data.{EitherT, Kleisli} +import cats.data.{EitherT, Kleisli, StateT} import cats.free.FreeT import cats.syntax.all._ @@ -150,4 +150,39 @@ object ApplicativeThread { def annotate[A](name: String, indent: Boolean)(body: EitherT[F, E, A]): EitherT[F, E, A] = EitherT(thread.annotate(name, indent)(body.value)) } + + implicit def forStateT[F[_]: Monad: ApplicativeThread, E]: ApplicativeThread[StateT[F, E, *]] = + new ApplicativeThread[StateT[F, E, *]] { + private val thread = ApplicativeThread[F] + + val applicative = Applicative[StateT[F, E, *]] + + def fork[A](left: => A, right: => A): StateT[F, E, A] = + StateT.liftF(thread.fork(left, right)) + + val cede: StateT[F, E, Unit] = + StateT.liftF(thread.cede) + + def done[A]: StateT[F, E, A] = + StateT.liftF(thread.done[A]) + + val monitor: StateT[F, E, MonitorId] = + StateT.liftF(thread.monitor) + + def await(id: MonitorId): StateT[F, E, Unit] = + StateT.liftF(thread.await(id)) + + def notify(id: MonitorId): StateT[F, E, Unit] = + StateT.liftF(thread.notify(id)) + + def start[A](child: StateT[F, E, A]): StateT[F, E, Unit] = + StateT { s => + thread.start(child.run(s)).as((s, ())) + } + + def annotate[A](name: String, indent: Boolean)(body: StateT[F, E, A]): StateT[F, E, A] = + StateT { s => + thread.annotate(name, indent)(body.run(s)) + } + } } diff --git a/core/shared/src/main/scala/coop/MVar.scala b/core/shared/src/main/scala/coop/MVar.scala index d4579ec..2bf14f3 100644 --- a/core/shared/src/main/scala/coop/MVar.scala +++ b/core/shared/src/main/scala/coop/MVar.scala @@ -16,105 +16,104 @@ package coop -import cats.{Functor, Monad} -import cats.data.Kleisli -import cats.implicits._ -import cats.mtl.Ask +import cats.{Applicative, FlatMap, Monad} +import cats.data.StateT +import cats.syntax.all._ import ThreadF.MonitorId final class MVar[A] private (monitor: MonitorId) { outer => + import MVar.Action + private[this] val Key = this.asInstanceOf[MVar[Any]] - def tryRead[F[_]: Functor: MVar.Ask]: F[Option[A]] = getU[F] + def tryRead[F[_]: Applicative]: Action[F, Option[A]] = getU[F] - def read[F[_]: Monad: ApplicativeThread: MVar.Ask]: F[A] = + def read[F[_]: Monad: ApplicativeThread]: Action[F, A] = tryRead[F] flatMap { - case Some(a) => a.pure[F] - case None => ApplicativeThread[F].await(monitor) >> read[F] + case Some(a) => StateT.pure(a) + case None => StateT.liftF[F, Map[MVar[Any], Any], Unit](ApplicativeThread[F].await(monitor)) >> read[F] } - def tryPut[F[_]: Monad: ApplicativeThread: MVar.Ask](a: A): F[Boolean] = + def tryPut[F[_]: Monad: ApplicativeThread](a: A): Action[F, Boolean] = getU[F] flatMap { case Some(_) => - false.pure[F] + StateT.pure(false) case None => setU[F](a).as(true) } - def put[F[_]: Monad: ApplicativeThread: MVar.Ask](a: A): F[Unit] = - tryPut[F](a).ifM(().pure[F], ApplicativeThread[F].await(monitor) >> put[F](a)) + def put[F[_]: Monad: ApplicativeThread](a: A): Action[F, Unit] = + tryPut[F](a).ifM( + StateT.pure(()), + StateT.liftF[F, Map[MVar[Any], Any], Unit](ApplicativeThread[F].await(monitor)) >> put[F](a) + ) - def tryTake[F[_]: Monad: ApplicativeThread: MVar.Ask]: F[Option[A]] = + def tryTake[F[_]: Monad: ApplicativeThread]: Action[F, Option[A]] = getU[F] flatMap { case Some(a) => removeU[F].as(Some(a): Option[A]) case None => - (None: Option[A]).pure[F] + StateT.pure(None) } - def take[F[_]: Monad: ApplicativeThread: MVar.Ask]: F[A] = + def take[F[_]: Monad: ApplicativeThread]: Action[F, A] = tryTake[F] flatMap { - case Some(a) => a.pure[F] - case None => ApplicativeThread[F].await(monitor) >> take[F] + case Some(a) => StateT.pure(a) + case None => StateT.liftF[F, Map[MVar[Any], Any], Unit](ApplicativeThread[F].await(monitor)) >> take[F] } - def swap[F[_]: Monad: ApplicativeThread: MVar.Ask](a: A): F[A] = + def swap[F[_]: Monad: ApplicativeThread](a: A): Action[F, A] = getU[F] flatMap { case Some(oldA) => setU[F](a).as(oldA) case None => - ApplicativeThread[F].await(monitor) >> swap[F](a) + StateT.liftF[F, Map[MVar[Any], Any], Unit](ApplicativeThread[F].await(monitor)) >> swap[F](a) } - def apply[F[_]: Monad: ApplicativeThread: MVar.Ask]: MVarPartiallyApplied[F] = + def apply[F[_]: Monad: ApplicativeThread]: MVarPartiallyApplied[F] = new MVarPartiallyApplied[F] - class MVarPartiallyApplied[F[_]: Monad: ApplicativeThread: MVar.Ask] { + class MVarPartiallyApplied[F[_]: Monad: ApplicativeThread] { - val tryRead: F[Option[A]] = outer.tryRead[F] + val tryRead: Action[F, Option[A]] = outer.tryRead[F] - val read: F[A] = outer.read[F] + val read: Action[F, A] = outer.read[F] - def tryPut(a: A): F[Boolean] = outer.tryPut[F](a) + def tryPut(a: A): Action[F, Boolean] = outer.tryPut[F](a) - def put(a: A): F[Unit] = outer.put[F](a) + def put(a: A): Action[F, Unit] = outer.put[F](a) - val tryTake: F[Option[A]] = outer.tryTake[F] + val tryTake: Action[F, Option[A]] = outer.tryTake[F] - val take: F[A] = outer.take[F] + val take: Action[F, A] = outer.take[F] - def swap(a: A): F[A] = outer.swap[F](a) + def swap(a: A): Action[F, A] = outer.swap[F](a) } - private[this] def getU[F[_]: Functor: MVar.Ask]: F[Option[A]] = - Ask[F, MVar.Universe].ask.map(_().get(Key).map(_.asInstanceOf[A])) + private[this] def getU[F[_]: Applicative]: Action[F, Option[A]] = + StateT.get[F, Map[MVar[Any], Any]].map(_.get(Key).map(_.asInstanceOf[A])) - private[this] def setU[F[_]: Monad: MVar.Ask: ApplicativeThread](a: A): F[Unit] = - Ask[F, MVar.Universe].ask.map(_() += (Key -> a.asInstanceOf[Any])) >> - ApplicativeThread[F].notify(monitor) + private[this] def setU[F[_]: Monad: ApplicativeThread](a: A): Action[F, Unit] = + StateT.modify[F, Map[MVar[Any], Any]](_ + (Key -> a)) >> StateT.liftF(ApplicativeThread[F].notify(monitor)) - private[this] def removeU[F[_]: Monad: MVar.Ask: ApplicativeThread]: F[Unit] = - Ask[F, MVar.Universe].ask.map(_() -= Key) >> - ApplicativeThread[F].notify(monitor) + private[this] def removeU[F[_]: Monad: ApplicativeThread]: Action[F, Unit] = + StateT.modify[F, Map[MVar[Any], Any]](_ - Key) >> StateT.liftF(ApplicativeThread[F].notify(monitor)) } object MVar { - // we use a kleisli of a ref of a map here rather than StateT to avoid issues with zeros in F - // the Any(s) are required due to the existentiality of the A types - type Universe = UnsafeRef[Map[MVar[Any], Any]] - type Ask[F[_]] = cats.mtl.Ask[F, Universe] + type Action[F[_], A] = StateT[F, Map[MVar[Any], Any], A] - def empty[F[_]: Functor: ApplicativeThread, A]: F[MVar[A]] = - ApplicativeThread[F].monitor.map(new MVar[A](_)) // not actually pure due to object identity, but whatevs + def empty[F[_]: Applicative: ApplicativeThread, A]: Action[F, MVar[A]] = + StateT.liftF(ApplicativeThread[F].monitor.map(new MVar[A](_))) // not actually pure due to object identity, but whatevs - def apply[F[_]: Monad: ApplicativeThread: Ask, A](a: A): F[MVar[A]] = + def apply[F[_]: Monad: ApplicativeThread, A](a: A): Action[F, MVar[A]] = empty[F, A].flatMap(mv => mv.put[F](a).as(mv)) - def resolve[F[_], A](mvt: Kleisli[F, Universe, A]): F[A] = - mvt.run(new UnsafeRef(Map[MVar[Any], Any]())) + def resolve[F[_]: FlatMap, A](mvt: Action[F, A]): F[A] = + mvt.runA(Map[MVar[Any], Any]()) } diff --git a/core/shared/src/test/scala/coop/MVarSpecs.scala b/core/shared/src/test/scala/coop/MVarSpecs.scala index f9b3092..3b5569a 100644 --- a/core/shared/src/test/scala/coop/MVarSpecs.scala +++ b/core/shared/src/test/scala/coop/MVarSpecs.scala @@ -17,16 +17,16 @@ package coop import cats.{Eval, Monoid} -import cats.data.{Kleisli, State} -import cats.implicits._ +import cats.data.{State, StateT} import cats.mtl.Stateful +import cats.syntax.all._ import org.specs2.mutable.Specification class MVarSpecs extends Specification { import FreeTInstances._ - type F[S, A] = Kleisli[ThreadT[State[S, *], *], MVar.Universe, A] + type F[S, A] = ThreadT[State[S, *], A] "mvar" should { "put and read values" in { @@ -37,7 +37,7 @@ class MVarSpecs extends Specification { _ <- v.put(42) i <- v.read - _ <- Stateful[F[Int, *], Int].set(i) + _ <- StateT.liftF(Stateful[F[Int, *], Int].set(i)) } yield () runToCompletionEmpty(eff) mustEqual 42 @@ -47,7 +47,7 @@ class MVarSpecs extends Specification { val eff = for { v <- MVar.empty[F[Option[Int], *], Int] r <- v.tryRead[F[Option[Int], *]] - _ <- Stateful[F[Option[Int], *], Option[Int]].set(r) + _ <- StateT.liftF(Stateful[F[Option[Int], *], Option[Int]].set(r)) } yield () runToCompletionEmpty(eff) must beNone @@ -57,7 +57,7 @@ class MVarSpecs extends Specification { val eff = for { v <- MVar[F[Boolean, *], Int](42) r <- v.tryPut[F[Boolean, *]](12) - _ <- Stateful[F[Boolean, *], Boolean].set(r) + _ <- StateT.liftF(Stateful[F[Boolean, *], Boolean].set(r)) } yield () runToCompletion(true, eff) must beFalse @@ -71,7 +71,7 @@ class MVarSpecs extends Specification { r1 <- v.take r2 <- v.tryRead - _ <- Stateful[F[(Int, Option[Int]), *], (Int, Option[Int])].set((r1, r2)) + _ <- StateT.liftF(Stateful[F[(Int, Option[Int]), *], (Int, Option[Int])].set((r1, r2))) } yield () runToCompletionEmpty(eff) mustEqual ((42, None)) @@ -85,14 +85,14 @@ class MVarSpecs extends Specification { r1 <- v.swap(24) r2 <- v.read - _ <- Stateful[F[(Int, Int), *], (Int, Int)].set((r1, r2)) + _ <- StateT.liftF(Stateful[F[(Int, Int), *], (Int, Int)].set((r1, r2))) } yield () runToCompletionEmpty(eff) mustEqual ((42, 24)) } "resolve a race condition" in { - val thread = ApplicativeThread[F[(Either[Int, Int], Int), *]] + val thread = ApplicativeThread[MVar.Action[F[(Either[Int, Int], Int), *], *]] val state = Stateful[F[(Either[Int, Int], Int), *], (Either[Int, Int], Int)] val eff = for { @@ -104,20 +104,22 @@ class MVarSpecs extends Specification { _ <- thread start { v.tryPut(5).ifM( - ().pure[F[(Either[Int, Int], Int), *]], - results.put(Left(5))) + StateT.pure(()), + results.put(Left(5)) + ) } _ <- thread start { v.tryPut(8).ifM( - ().pure[F[(Either[Int, Int], Int), *]], - results.put(Right(8))) + StateT.pure(()), + results.put(Right(8)) + ) } r1 <- v.read r2 <- results.read - _ <- state.set((r2, r1)) + _ <- StateT.liftF(state.set((r2, r1))) } yield () val results = runToCompletionEmpty(eff) @@ -126,16 +128,16 @@ class MVarSpecs extends Specification { } "detect a deadlock" in { - type F[A] = Kleisli[ThreadT[Eval, *], MVar.Universe, A] + type F[A] = ThreadT[Eval, A] val eff = MVar.empty[F, Unit].flatMap(_.read[F]) ThreadT.roundRobin(MVar.resolve(eff)).value must beFalse } } - def runToCompletionEmpty[S: Monoid](fa: F[S, _]): S = + def runToCompletionEmpty[S: Monoid](fa: MVar.Action[F[S, *], _]): S = runToCompletion(Monoid[S].empty, fa) - def runToCompletion[S](init: S, fa: F[S, _]): S = + def runToCompletion[S](init: S, fa: MVar.Action[F[S, *], _]): S = ThreadT.roundRobin(MVar.resolve(fa)).runS(init).value } diff --git a/project/build.properties b/project/build.properties index 0837f7a..c19c768 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.3.13 +sbt.version=1.4.2 diff --git a/project/plugins.sbt b/project/plugins.sbt index f91b912..7057fe5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,3 @@ -addSbtPlugin("com.codecommit" % "sbt-spiewak-sonatype" % "0.15.4") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.2.0") +addSbtPlugin("com.codecommit" % "sbt-spiewak-sonatype" % "0.17.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.3.0") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0")