From f9ac85dfe4142d6d8de2028cd0b0e6ad186a2f58 Mon Sep 17 00:00:00 2001 From: Orion Kindel Date: Wed, 1 Nov 2023 20:13:31 -0500 Subject: [PATCH] feat: initial commit --- .gitignore | 13 +++ .prettierrc.cjs | 8 ++ .tool-versions | 3 + bun/fmt.js | 28 ++++++ jsconfig.json | 16 ++++ package.json | 18 ++++ spago.yaml | 15 ++++ src/Control.Monad.Async.State.purs | 112 ++++++++++++++++++++++++ src/Data.Async.Class.purs | 57 ++++++++++++ src/Data.Async.Mutex.purs | 46 ++++++++++ src/Data.Async.RwLock.purs | 136 +++++++++++++++++++++++++++++ test/Test.Main.purs | 60 +++++++++++++ 12 files changed, 512 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierrc.cjs create mode 100644 .tool-versions create mode 100644 bun/fmt.js create mode 100644 jsconfig.json create mode 100644 package.json create mode 100644 spago.yaml create mode 100644 src/Control.Monad.Async.State.purs create mode 100644 src/Data.Async.Class.purs create mode 100644 src/Data.Async.Mutex.purs create mode 100644 src/Data.Async.RwLock.purs create mode 100644 test/Test.Main.purs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebf58ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/bower_components/ +/node_modules/ +/.pulp-cache/ +/output/ +/generated-docs/ +/.psc-package/ +/.psc* +/.purs* +/.psa* +/.spago +.log +.purs-repl +.env diff --git a/.prettierrc.cjs b/.prettierrc.cjs new file mode 100644 index 0000000..b8e1fd1 --- /dev/null +++ b/.prettierrc.cjs @@ -0,0 +1,8 @@ +module.exports = { + tabWidth: 2, + trailingComma: 'all', + singleQuote: true, + semi: false, + arrowParens: 'avoid', + plugins: [], +} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..164b4a0 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,3 @@ +bun 1.0.3 +purescript 0.15.12 +nodejs 20.8.0 diff --git a/bun/fmt.js b/bun/fmt.js new file mode 100644 index 0000000..db578f1 --- /dev/null +++ b/bun/fmt.js @@ -0,0 +1,28 @@ +/** @type {(parser: string, ps: string[]) => import("bun").Subprocess} */ +const prettier = (parser, ps) => + Bun.spawn(['bun', 'x', 'prettier', '--write', '--parser', parser, ...ps], { + stdout: 'inherit', + stderr: 'inherit', + }) + +const procs = [ + prettier('babel', ['./src/**/*.js', './bun/**/*.js', './.prettierrc.cjs']), + prettier('json', ['./package.json', './jsconfig.json']), + prettier('sh', ['./Dockerfile']), + Bun.spawn( + [ + 'bun', + 'x', + 'purs-tidy', + 'format-in-place', + 'src/**/*.purs', + 'test/**/*.purs', + ], + { + stdout: 'inherit', + stderr: 'inherit', + }, + ), +] + +await Promise.all(procs.map(p => p.exited)) diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..e4d3bb6 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "types": ["bun-types"], + "lib": ["esnext"], + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "jsx": "react", + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true + }, + "include": ["tmp.js", "src/**/*.js", "bun/**/*.js"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..80ff14c --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "@ts/scraper", + "private": true, + "module": "index.js", + "type": "module", + "scripts": { + "fmt": "bun bun/fmt.js" + }, + "devDependencies": { + "bun-types": "latest", + "purs-tidy": "^0.10.0", + "spago": "next" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": {} +} diff --git a/spago.yaml b/spago.yaml new file mode 100644 index 0000000..75a7531 --- /dev/null +++ b/spago.yaml @@ -0,0 +1,15 @@ +package: + dependencies: + - aff + - arrays + - avar + - console + name: sync + test: + main: Test.Main + dependencies: + - spec +workspace: + package_set: + url: https://raw.githubusercontent.com/purescript/package-sets/psc-0.15.10-20230930/packages.json + hash: sha256-nTsd44o7/hrTdk0c6dh0wyBqhFFDJJIeKdQU6L1zv/A= diff --git a/src/Control.Monad.Async.State.purs b/src/Control.Monad.Async.State.purs new file mode 100644 index 0000000..a2d1cdf --- /dev/null +++ b/src/Control.Monad.Async.State.purs @@ -0,0 +1,112 @@ +module Control.Monad.State.Async where + +import Prelude + +import Control.Alt (class Alt) +import Control.Alternative (class Alternative, class Plus) +import Control.Monad.Error.Class (class MonadError, class MonadThrow, liftEither, try) +import Control.Monad.Fork.Class (class MonadBracket, class MonadFork, class MonadKill, bracket, kill, never, uninterruptible) +import Control.Monad.Reader (class MonadAsk, ReaderT(..), ask) +import Control.Monad.Trans.Class (class MonadTrans, lift) +import Control.Parallel (class Parallel, parallel, sequential) +import Data.Async.Class (class AsyncState) +import Data.Async.Class as State +import Data.Async.Mutex (Mutex(..)) +import Data.Async.RwLock (RwLock(..)) +import Data.Maybe (Maybe) +import Data.Newtype (class Newtype, unwrap, wrap) +import Data.Traversable (for, for_) +import Data.Tuple (fst, snd) +import Data.Tuple.Nested (type (/\), (/\)) +import Effect.Aff.Class (class MonadAff) +import Effect.Class (class MonadEffect) + +-- | A monadic context with some mutable state `s` +-- | that is in a concurrency-safe `AsyncState` wrapper `w` +-- | +-- | Supports the following operations: +-- | - `asyncTryRead` - Read the state as it is now, returning `Nothing` if the resource is currently locked +-- | - `asyncRead` - Read the state, blocking if the resource is locked +-- | - `asyncWrite` - Map the state's value, blocking if the resource is locked +-- | - `ask` - Direct access to the low-level `AsyncState` +class MonadAsyncState :: (Type -> Type) -> (Type -> Type) -> (Type -> Type) -> Type -> (Type -> Type) -> ((Type -> Type) -> Type -> Type) -> Constraint +class (MonadTrans t, MonadAff (t m), MonadAff m, AsyncState w wwl wrl, MonadAsk (w s) (t m)) <= MonadAsyncState w wwl wrl s m t | w -> wwl, w -> wrl, m -> w, m -> s, t -> m + +asyncTryRead :: forall w wl rl s m t. MonadAsyncState w wl rl s m t => t m (Maybe s) +asyncTryRead = do + state <- ask + readLock :: Maybe (rl s) <- State.tryLock state + snap <- for readLock (State.read state) + for_ readLock (State.unlock state) + pure snap + +asyncWrite :: forall w wl rl s m t a e. MonadError e (t m) => MonadAsyncState w wl rl s m t => (s -> m (a /\ s)) -> t m a +asyncWrite f = do + state <- ask + lock :: wl s <- State.lock state + s <- State.read state lock + e <- try $ lift $ f s + for_ e (State.write state lock <<< snd) + State.unlock state lock + fst <$> liftEither e + +asyncModify :: forall w wl rl s m t e. MonadError e (t m) => MonadAsyncState w wl rl s m t => (s -> m s) -> t m Unit +asyncModify f = asyncWrite (map (unit /\ _) <<< f) + +asyncRead :: forall w wl rl s m t a e. MonadError e (t m) => MonadAsyncState w wl rl s m t => (s -> m a) -> t m a +asyncRead f = do + state <- ask + lock :: rl s <- State.lock state + s <- State.read state lock + e <- try $ lift $ f s + State.unlock state lock + liftEither e + +asyncPut :: forall w wl rl s m t e. MonadError e (t m) => MonadAsyncState w wl rl s m t => s -> t m Unit +asyncPut = asyncWrite <<< const <<< pure <<< (unit /\ _) + +-- | Implementor of `MonadAsyncState` usable with any `AsyncState` structure. +newtype AsyncStateT :: (Type -> Type) -> Type -> (Type -> Type) -> Type -> Type +newtype AsyncStateT w s m ma = AsyncStateT (ReaderT (w s) m ma) + +derive instance Newtype (AsyncStateT w s m ma) _ +derive newtype instance MonadTrans (AsyncStateT w s) +derive newtype instance (Alternative m) => Alternative (AsyncStateT w s m) +derive newtype instance (Alt m) => Alt (AsyncStateT w s m) +derive newtype instance (Plus m) => Plus (AsyncStateT w s m) +derive newtype instance (Applicative m) => Applicative (AsyncStateT w s m) +derive newtype instance (Apply m) => Apply (AsyncStateT w s m) +derive newtype instance (Bind m) => Bind (AsyncStateT w s m) +derive newtype instance (Functor m) => Functor (AsyncStateT w s m) +derive newtype instance (Monad m) => Monad (AsyncStateT w s m) +derive newtype instance (Monad m) => MonadAsk (w s) (AsyncStateT w s m) +derive newtype instance (MonadEffect m) => MonadEffect (AsyncStateT w s m) +derive newtype instance (MonadAff m) => MonadAff (AsyncStateT w s m) +derive newtype instance (MonadThrow e m) => MonadThrow e (AsyncStateT w s m) +derive newtype instance (MonadError e m) => MonadError e (AsyncStateT w s m) +derive newtype instance (MonadFork f m) => MonadFork f (AsyncStateT w s m) +instance (Parallel f m) => Parallel (AsyncStateT w s f) (AsyncStateT w s m) where + parallel a = wrap $ parallel $ unwrap a + sequential a = wrap $ sequential $ unwrap a + +instance (MonadError e m, MonadKill e f m) => MonadKill e f (AsyncStateT w s m) where + kill e f = wrap $ kill e f + +instance (MonadBracket e f m) => MonadBracket e f (AsyncStateT w s m) where + bracket a f c = wrap $ bracket (unwrap a) (\fa fb -> unwrap $ f fa fb) (unwrap <<< c) + uninterruptible a = wrap $ uninterruptible $ unwrap a + never = wrap never + +instance (AsyncState w wl rl, MonadAff m) => MonadAsyncState w wl rl s m (AsyncStateT w s) + +type RwLockStateT s m = AsyncStateT RwLock s m +type MutexStateT s m = AsyncStateT Mutex s m + +runAsyncState :: forall w wl rl s m ma. MonadAff m => AsyncState w wl rl => s -> AsyncStateT w s m ma -> m ma +runAsyncState s (AsyncStateT (ReaderT f)) = f =<< State.boxed s + +runMutexState :: forall s m ma. MonadAff m => s -> MutexStateT s m ma -> m ma +runMutexState s (AsyncStateT (ReaderT f)) = f =<< State.boxed s + +runRwLockState :: forall s m ma. MonadAff m => s -> RwLockStateT s m ma -> m ma +runRwLockState s (AsyncStateT (ReaderT f)) = f =<< State.boxed s diff --git a/src/Data.Async.Class.purs b/src/Data.Async.Class.purs new file mode 100644 index 0000000..2acb98d --- /dev/null +++ b/src/Data.Async.Class.purs @@ -0,0 +1,57 @@ +module Data.Async.Class where + +import Prelude + +import Data.Maybe (Maybe) +import Effect.Aff.Class (class MonadAff) + +-- | Acquire a lock that guarantees access to mutable state +-- | +-- | Care must be taken to release locks when errors are raised +-- | to not cause deadlocks. +-- | +-- | Consider using a high-level structure like `AsyncStateT`, +-- | which will automatically acquire and release locks when errors +-- | occur. +class AsyncStateLock :: (Type -> Type) -> (Type -> Type) -> Constraint +class AsyncStateLock m lock | lock -> m where + -- | Attempt to synchronously acquire a lock, returning `Nothing` + -- | if a call to `lock` would have blocked. + tryLock :: forall async a. MonadAff async => m a -> async (Maybe (lock a)) + + -- | Block the current `Aff`-like computation until a lock may + -- | be acquired. + lock :: forall async a. MonadAff async => m a -> async (lock a) + + -- | Release the lock, unblocking other threads. + -- | + -- | Repeated calls to `unlock` or calls to `unlock` on the wrong + -- | AsyncState `m` may either silently fail or raise an error. + -- | + -- | Implementations must raise an error if using a lock is attempted after + -- | `unlock` has been invoked. + unlock :: forall async a. MonadAff async => m a -> lock a -> async Unit + +-- | A lock type that supports writing data to the mutable state. +-- | +-- | Writable locks must also be Readable. +class AsyncStateWritable :: (Type -> Type) -> (Type -> Type) -> Constraint +class (AsyncStateReadable m lock) <= AsyncStateWritable m lock | lock -> m where + -- | Write a new value into `m` + write :: forall a async. MonadAff async => m a -> lock a -> a -> async Unit + +-- | A lock type that supports reading data from the mutable state +class AsyncStateReadable :: (Type -> Type) -> (Type -> Type) -> Constraint +class AsyncStateReadable m lock | lock -> m where + -- | Read the current value in `m` + read :: forall a async. MonadAff async => m a -> lock a -> async a + +-- | Mutable state that supports concurrent access. +-- | +-- | Implementors may use separate read & write guard types +-- | (ex. `RwLock`) or use a single guard type for both reading +-- | and writing (ex. `Mutex`) +class AsyncState :: (Type -> Type) -> (Type -> Type) -> (Type -> Type) -> Constraint +class (AsyncStateReadable m readLock, AsyncStateReadable m writeLock, AsyncStateWritable m writeLock, AsyncStateLock m writeLock, AsyncStateLock m readLock) <= AsyncState m writeLock readLock | writeLock -> m, readLock -> m, m -> readLock, m -> writeLock where + -- | Create an `m` with initial value `a` + boxed :: forall async a. MonadAff async => a -> async (m a) diff --git a/src/Data.Async.Mutex.purs b/src/Data.Async.Mutex.purs new file mode 100644 index 0000000..2817770 --- /dev/null +++ b/src/Data.Async.Mutex.purs @@ -0,0 +1,46 @@ +module Data.Async.Mutex (Mutex, MutexGuard) where + +import Prelude + +import Control.Monad.Error.Class (liftMaybe, throwError) +import Data.Async.Class (class AsyncState, class AsyncStateLock, class AsyncStateReadable, class AsyncStateWritable, read) +import Data.Maybe (isNothing) +import Data.Traversable (for, for_, traverse) +import Effect.Aff.AVar (AVar) +import Effect.Aff.AVar as AVar +import Effect.Aff.Class (liftAff) +import Effect.Class (liftEffect) +import Effect.Console as Console +import Effect.Exception (error) + +-- | A lock guaranteeing exclusive access to +-- | the data contained within a `Mutex` +data MutexGuard a = MutexGuard (AVar a) + +-- | Mutable state guaranteeing mutually exclusive +-- | access to the data of type `a`. +-- | +-- | There will be at most 1 thread with access to +-- | the data contained in the mutex at a time. +data Mutex a = Mutex (AVar a) + +instance AsyncState Mutex MutexGuard MutexGuard where + boxed = liftAff <<< map Mutex <<< AVar.new + +instance AsyncStateReadable Mutex MutexGuard where + read _ (MutexGuard cell) = liftAff $ liftMaybe (error "MutexGuard used after `unlock`!") =<< AVar.tryRead cell + +instance AsyncStateWritable Mutex MutexGuard where + write _ (MutexGuard cell) s = liftAff $ (const $ AVar.put s cell) =<< liftMaybe (error "MutexGuard used after `unlock`!") =<< AVar.tryTake cell + +instance AsyncStateLock Mutex MutexGuard where + unlock (Mutex stateCell) (MutexGuard localStateCell) = liftAff do + state <- AVar.tryTake localStateCell + when (isNothing state) $ throwError $ error "MutexGuard unlocked already!" + for_ state (flip AVar.put stateCell) + lock (Mutex stateCell) = liftAff do + state <- AVar.take stateCell + MutexGuard <$> AVar.new state + tryLock (Mutex stateCell) = liftAff do + state <- AVar.tryTake stateCell + for state (map MutexGuard <<< AVar.new) diff --git a/src/Data.Async.RwLock.purs b/src/Data.Async.RwLock.purs new file mode 100644 index 0000000..bad1aa8 --- /dev/null +++ b/src/Data.Async.RwLock.purs @@ -0,0 +1,136 @@ +module Data.Async.RwLock (RwLock, ReadGuard, WriteGuard) where + +import Prelude + +import Control.Monad.Error.Class (liftMaybe, throwError) +import Data.Array as Array +import Data.Async.Class (class AsyncState, class AsyncStateLock, class AsyncStateReadable, class AsyncStateWritable, lock) +import Data.Either (Either(..)) +import Data.Maybe (Maybe(..), fromMaybe) +import Data.Newtype (class Newtype, wrap) +import Data.Set (Set) +import Data.Set as Set +import Data.Traversable (for, for_) +import Effect (Effect) +import Effect.Aff (Aff, launchAff_, makeAff) +import Effect.Aff.AVar (AVar) +import Effect.Aff.AVar as AVar +import Effect.Aff.Class (liftAff) +import Effect.Class (liftEffect) +import Effect.Exception (error) + +data ReadGuard :: Type -> Type +data ReadGuard a = ReadGuard ReaderId a + +data WriteGuard :: Type -> Type +data WriteGuard a = WriteGuard (AVar a) + +newtype ReaderId = ReaderId Int + +derive instance Newtype ReaderId _ +derive newtype instance Semiring ReaderId +derive newtype instance Show ReaderId +derive newtype instance Eq ReaderId +derive newtype instance Ord ReaderId + +newtype ReaderCount = ReaderCount Int + +derive instance Newtype ReaderCount _ + +-- | A mutable location in memory with guaranteed exclusive access to +-- | any number of readers or a single writer +data RwLock a = RwLock + { nextReaderId :: AVar ReaderId + , readers :: AVar (Set ReaderId) + , writerQueue :: AVar (Array (Effect Unit)) + , state :: AVar a + } + +makeReadGuard :: forall a. AVar ReaderId -> AVar (Set ReaderId) -> AVar a -> a -> Aff (ReadGuard a) +makeReadGuard nextReaderIdCell readersCell stateCell state = do + nextReaderId <- AVar.take nextReaderIdCell + ids <- AVar.take readersCell + AVar.put (nextReaderId + ReaderId 1) nextReaderIdCell + AVar.put (Set.insert nextReaderId ids) readersCell + AVar.put state stateCell + pure $ ReadGuard nextReaderId state + +mayWrite :: AVar (Array (Effect Unit)) -> Aff Unit +mayWrite writerQCell = do + writerQueue <- AVar.take writerQCell + liftEffect $ for_ (Array.head writerQueue) identity + AVar.put (fromMaybe [] $ Array.tail writerQueue) writerQCell + +instance AsyncState RwLock WriteGuard ReadGuard where + boxed a = liftAff do + state <- AVar.new a + nextReaderId <- AVar.new $ wrap 1 + readers <- AVar.new Set.empty + writerQueue <- AVar.new mempty + pure $ RwLock { state, nextReaderId, readers, writerQueue } + +instance AsyncStateWritable RwLock WriteGuard where + write _ (WriteGuard stateCell) s = + liftAff + $ const (AVar.put s stateCell) + =<< liftMaybe (error "WriteGuard used after `unlock` invoked!") + =<< AVar.tryTake stateCell + +instance AsyncStateReadable RwLock WriteGuard where + read _ (WriteGuard stateCell) = + liftAff + $ liftMaybe (error "WriteGuard used after `unlock` invoked!") + =<< AVar.tryRead stateCell + +instance AsyncStateReadable RwLock ReadGuard where + read (RwLock { readers: readersCell }) (ReadGuard id a) = liftAff do + readers <- AVar.read readersCell + when (not $ Set.member id readers) $ throwError $ error "ReadGuard used after `unlock` invoked!" + pure a + +instance AsyncStateLock RwLock ReadGuard where + unlock (RwLock { readers: readersCell, writerQueue: writerQCell }) (ReadGuard id _) = liftAff do + readers <- Set.delete id <$> AVar.take readersCell + AVar.put readers readersCell + when (readers == Set.empty) $ mayWrite writerQCell + tryLock (RwLock { readers: readersCell, state: stateCell, nextReaderId: nextReaderIdCell }) = liftAff do + stateM <- AVar.tryTake stateCell + for stateM $ makeReadGuard nextReaderIdCell readersCell stateCell + lock (RwLock { readers: readersCell, state: stateCell, nextReaderId: nextReaderIdCell }) = liftAff do + state <- AVar.take stateCell + makeReadGuard nextReaderIdCell readersCell stateCell state + +instance AsyncStateLock RwLock WriteGuard where + unlock (RwLock { writerQueue: writerQCell, state: rwLockStateCell }) (WriteGuard localStateCell) = liftAff do + state <- AVar.take localStateCell + AVar.put state rwLockStateCell + mayWrite writerQCell + tryLock (RwLock { readers: readersCell, state: stateCell }) = liftAff do + readers <- AVar.take readersCell + if readers == Set.empty then do + stateM <- AVar.tryTake stateCell + AVar.put readers readersCell + for stateM (map WriteGuard <<< AVar.new) + else do + AVar.put readers readersCell + pure Nothing + lock rw@(RwLock { writerQueue: writerQCell, readers: readersCell, state: stateCell }) = liftAff do + let + blockOnWritable_ cb = do + launchAff_ do + writerQ <- AVar.take writerQCell + AVar.put (writerQ <> [ cb $ Right unit ]) writerQCell + pure unit + pure mempty + blockOnWritable = makeAff blockOnWritable_ + onWritable = do + state <- AVar.take stateCell + WriteGuard <$> AVar.new state + readers <- AVar.take readersCell + if readers == Set.empty then do + AVar.put readers readersCell + onWritable + else do + AVar.put readers readersCell + blockOnWritable + lock rw diff --git a/test/Test.Main.purs b/test/Test.Main.purs new file mode 100644 index 0000000..1de1440 --- /dev/null +++ b/test/Test.Main.purs @@ -0,0 +1,60 @@ +module Test.Main where + +import Prelude + +import Control.Monad.Error.Class (try) +import Control.Monad.State.Async (AsyncStateT, asyncModify, asyncPut, asyncRead, runAsyncState) +import Control.Monad.Trans.Class (lift) +import Control.Parallel (parallel, sequential) +import Data.Async.Class (class AsyncState) +import Data.Async.Mutex (Mutex) +import Data.Async.RwLock (RwLock) +import Data.Either (isLeft) +import Data.Identity (Identity) +import Data.Newtype (wrap) +import Effect (Effect) +import Effect.Aff (Aff, delay, launchAff_) +import Test.Spec (SpecT, describe, it) +import Test.Spec.Assertions (shouldEqual, shouldSatisfy) +import Test.Spec.Reporter (consoleReporter) +import Test.Spec.Runner (runSpec) + +common :: forall @w wl rl. AsyncState w wl rl => SpecT Aff Unit Identity Unit +common = do + it "does not throw" $ runAsyncState unit (pure @(AsyncStateT w Unit Aff) unit) + it "supports single-threaded state manipulation" do + runAsyncState 0 do + pure @(AsyncStateT w Int Aff) unit + flip shouldSatisfy isLeft =<< try (asyncRead $ shouldEqual 1) + asyncPut 1 + asyncRead $ shouldEqual 1 + asyncModify $ \n -> pure $ n + 2 + asyncRead $ shouldEqual 3 + it "supports concurrent state manipulation" do + let + t1 = parallel do + lift $ delay $ wrap 50.0 + asyncModify + ( \s -> do + delay $ wrap 100.0 + pure $ s <> "john" + ) + pure unit + t2 = parallel do + asyncPut "hello, " + lift $ delay $ wrap 60.0 + asyncModify (pure <<< (_ <> "!")) + pure unit + + runAsyncState "" do + pure @(AsyncStateT w String Aff) unit + sequential (pure (\_ _ -> unit) <*> t1 <*> t2) + asyncRead (shouldEqual "hello, john!") + +main :: Effect Unit +main = launchAff_ $ runSpec [ consoleReporter ] do + describe "AsyncStateT" do + describe "MutexStateT" do + common @Mutex + describe "RwLockStateT" do + common @RwLock