packages feed

stm-ringbuffer-0.1.0.0: src/Control/Concurrent/STM/TRingBuffer.hs

-- |
-- Module      :  Control.Concurrent.STM.TRingBuffer
-- Copyright   :  (c) Greg Baimetov 2026
-- License     :  BSD-style (see the file LICENSE)
--
-- Stability   :  experimental
-- Portability :  non-portable (requires STM)
--
-- 'TRingBuffer' is an STM ring buffer. The implementation allows for
-- simultaneous operations on the front and back of the buffer.

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE InstanceSigs #-}

module Control.Concurrent.STM.TRingBuffer
  ( TRingBuffer,
    new,
    newIO,
    length,
    isEmpty,
    isFull,
    size,
    pushFront,
    pushBack,
    popFront,
    popBack,
    peekFront,
    peekBack,
    flushFront,
    flushBack,
  )
where

import Control.Concurrent.STM
  ( STM,
    TArray,
    TVar,
    newTVar,
    newTVarIO,
    readTVar,
    retry,
    writeTVar,
  )
import Control.Concurrent.STM.TArray ()
import Data.Array.Base
  ( MArray (newArray, unsafeRead, unsafeWrite),
  )
import Data.Array.MArray ()
import Data.DList as DList (empty, snoc, toList)
import Prelude hiding (length, null)

-- | STM ring buffer.
data TRingBuffer a
  = MkTRingBuffer
  { -- | Maximum number of elements the ring buffer can hold.
    size :: {-# UNPACK #-} !Int,
    -- Underlying TArray
    -- index of frontmost item item
    front :: {-# UNPACK #-} !(TVar Int),
    -- index of rearmost item
    back :: {-# UNPACK #-} !(TVar Int),
    arr :: {-# UNPACK #-} !(TArray Int (Maybe a))
  }

instance Eq (TRingBuffer a) where
  (==) :: TRingBuffer a -> TRingBuffer a -> Bool
  a == b = front a == front b

-- | Initialize a ring buffer in STM. O(size)
new :: Int -> STM (TRingBuffer a)
new size = check size $ do
  arr <- newArray (0, size - 1) Nothing
  front <- newTVar $! incr size 0
  back <- newTVar 0
  pure $! MkTRingBuffer {arr, front, back, size}

-- | Initialize a ring buffer in IO. This may be used with UnsafePerformIO. O(size)
newIO :: Int -> IO (TRingBuffer a)
newIO size = check size $ do
  arr <- newArray (0, size - 1) Nothing
  front <- newTVarIO $! incr size 0
  back <- newTVarIO 0
  pure $! MkTRingBuffer {arr, front, back, size}

check :: (Ord a, Num a) => a -> p -> p
check n a =
  if n >= 0
    then a
    else error "Attempted to initialize RingBuffer with size < 0"

-- | Current length. O(1)
length :: TRingBuffer a -> STM Int
length rb@MkTRingBuffer {front, back, size} = do
  rbIsFull <- isFull rb
  if rbIsFull
    then pure size
    else do
      f <- readTVar front
      b <- readTVar back
      pure $ (b + 1 - f) `mod` size

-- | Is the ring buffer empty?
isEmpty :: TRingBuffer a -> STM Bool
isEmpty MkTRingBuffer {arr, front, size} = testPure size True $ do
  ix <- readTVar front
  res <- unsafeRead arr ix
  pure $ case res of
    Nothing -> True
    Just _ -> False

-- | Is the ring buffer full?
isFull :: TRingBuffer a -> STM Bool
isFull MkTRingBuffer {arr, front, size} = testPure size True $ do
  ix <- decr size <$> readTVar front
  res <- unsafeRead arr ix
  pure $ case res of
    Nothing -> False
    Just _ -> True

-- | Push an element to the front of the buffer. Retry if the buffer is full. O(1)
pushFront :: TRingBuffer a -> a -> STM ()
pushFront MkTRingBuffer {arr, front, size} e = testRetry size $ do
  ix <- decr size <$> readTVar front
  cur <- unsafeRead arr ix
  case cur of
    Just _ -> retry
    Nothing -> do
      unsafeWrite arr ix (Just e)
      writeTVar front ix

-- | Push an element to the back of the buffer. Retry if the buffer is full. O(1)
pushBack :: TRingBuffer a -> a -> STM ()
pushBack MkTRingBuffer {arr, back, size} e = testRetry size $ do
  ix <- incr size <$> readTVar back
  cur <- unsafeRead arr ix
  case cur of
    Just _ -> retry
    Nothing -> do
      unsafeWrite arr ix (Just e)
      writeTVar back ix

-- | Pop an element from the front of the buffer. Retry if the buffer is empty. O(1)
popFront :: TRingBuffer b -> STM b
popFront MkTRingBuffer {arr, front, size} = testRetry size $ do
  ix <- readTVar front
  cur <- unsafeRead arr ix
  case cur of
    Nothing -> retry
    Just e -> do
      unsafeWrite arr ix Nothing
      writeTVar front $! incr size ix
      pure e

-- | Pop an element from the back of the buffer. Retry if the buffer is empty. O(1)
popBack :: TRingBuffer b -> STM b
popBack MkTRingBuffer {arr, back, size} = testRetry size $ do
  ix <- readTVar back
  cur <- unsafeRead arr ix
  case cur of
    Nothing -> retry
    Just e -> do
      unsafeWrite arr ix Nothing
      writeTVar back $! decr size ix
      pure e

-- | Peek at the frontmost element of the buffer. Retry if the buffer is empty. O(1)
peekFront :: TRingBuffer b -> STM b
peekFront MkTRingBuffer {arr, front, size} = testRetry size $ do
  ix <- readTVar front
  cur <- unsafeRead arr ix
  maybe retry pure cur

-- | Peek at the backmost element of the buffer. Retry if the buffer is empty. O(1)
peekBack :: TRingBuffer b -> STM b
peekBack MkTRingBuffer {arr, back, size} = testRetry size $ do
  ix <- readTVar back
  cur <- unsafeRead arr ix
  maybe retry pure cur

-- | Pop all elements from the front of the buffer. This operation may succeed even if new elements are pushed to the back in the meantime. O(length)
flushFront :: TRingBuffer a -> STM [a]
flushFront MkTRingBuffer {arr, front, size} = testPure size [] $ do
  initIx <- readTVar front
  go initIx DList.empty
  where
    go !ix !xs = do
      cur <- unsafeRead arr ix
      case cur of
        Nothing -> do
          writeTVar front ix
          pure (toList xs)
        Just x -> do
          unsafeWrite arr ix Nothing
          go (incr size ix) (snoc xs x)

-- | Pop all elements from the back of the buffer. This operation may succeed even if new elements are pushed to the front in the meantime. O(length)
flushBack :: TRingBuffer a -> STM [a]
flushBack MkTRingBuffer {arr, back, size} = testPure size [] $ do
  initIx <- readTVar back
  go initIx DList.empty
  where
    go !ix !xs = do
      cur <- unsafeRead arr ix
      case cur of
        Nothing -> do
          writeTVar back ix
          pure (toList xs)
        Just x -> do
          unsafeWrite arr ix Nothing
          go (decr size ix) (snoc xs x)

-- Functions for moving pointer in ring buffer
-- For n /= 0, these are equivalent to (x \pm 1) `mod` n
-- For n == 0, these are equivalent to (x \pm 1)

incr :: (Eq a, Num a) => a -> a -> a
incr n x = if x == (n - 1) then 0 else x + 1

decr :: (Eq a, Num a) => a -> a -> a
decr n x = if x == 0 then n - 1 else x - 1

-- Functions for handling the case where the buffer's length is zero.
-- Overhead should be negligible when the buffer length is never actually zero.

testRetry :: (Eq a1, Num a1) => a1 -> STM a2 -> STM a2
testRetry !size m = if size == 0 then retry else m

testPure :: (Eq a1, Num a1, Applicative f) => a1 -> a2 -> f a2 -> f a2
testPure !size default' m = if size == 0 then pure default' else m