Skip to content
Draft
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
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: [[email protected], adopt@11, adopt@14, [email protected]]
scala: [0.27.0-RC1, 3.0.0-M1, 2.12.12, 2.13.3]
java:
- [email protected]
- [email protected]
- [email protected]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout current branch (full)
Expand All @@ -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 }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/clean.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "$@"; }
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ target/

# Ignore [ce]tags files
tags

.bsp
21 changes: 9 additions & 12 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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("[email protected]", "adopt@11", "adopt@14", "[email protected]")
// Restore running the CI on Java 15 (https://github.com/lampepfl/dotty/issues/10131).
ThisBuild / githubWorkflowJavaVersions := Seq("[email protected]", "[email protected]", "[email protected]")

Global / homepage := Some(url("https://github.com/typelevel/coop"))

Global / scmInfo := Some(
ScmInfo(
url("https://github.com/typelevel/coop"),
"[email protected]:typelevel/coop.git"))
"[email protected]:typelevel/coop.git"
)
)

lazy val root = project.in(file(".")).aggregate(core.jvm, core.js)
.settings(noPublishSettings)
Expand All @@ -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))
)

37 changes: 36 additions & 1 deletion core/shared/src/main/scala/coop/ApplicativeThread.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down Expand Up @@ -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))
}
}
}
89 changes: 44 additions & 45 deletions core/shared/src/main/scala/coop/MVar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]())
}
36 changes: 19 additions & 17 deletions core/shared/src/test/scala/coop/MVarSpecs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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))
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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
}
Loading