{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} ----------------------------------------------------------------------------- -- | -- Module : CircularBuffer -- Copyright : (C) 2017, Xia Li-yao -- License : BSD-style (see the file LICENSE) -- -- Maintainer : Xia Li-yao -- Stability : provisional -- Portability : non-portable (GHC extensions) -- -- This module contains a specification of a circular buffer. Adapted -- from John Hughes' /Experiences with QuickCheck: Testing the hard -- stuff and staying sane/. -- ------------------------------------------------------------------------ module CircularBuffer ( unpropNoSizeCheck , unpropFullIsEmpty , unpropBadRem , unpropStillBadRem , prop_circularBuffer ) where import Control.Applicative (liftA2) import Control.Monad (guard) import Data.Function (on) import Data.Functor.Classes (Eq1) import Data.IORef import Data.Maybe (isJust) import Data.TreeDiff (ToExpr) import Data.Vector.Unboxed.Mutable (IOVector) import qualified Data.Vector.Unboxed.Mutable as V import GHC.Generics (Generic, Generic1) import Prelude hiding (elem) import qualified Prelude as P import Test.QuickCheck (Gen, Positive(..), Property, arbitrary, elements, frequency, shrink, (===)) import Test.QuickCheck.Monadic (monadicIO) import Test.StateMachine import qualified Test.StateMachine.Types.Rank2 as Rank2 ------------------------------------------------------------------------ -- | Sets of bugs in the implementation and specification. type Bugs = [Bug] -- | Possible bugs. -- -- See 'unpropNoSizeCheck', 'unpropFullIsEmpty', 'unpropBadRem', -- and 'unpropStillBadRem'. data Bug = NoSizeCheck | FullIsEmpty | BadRem | StillBadRem deriving (Eq, Enum) -- | Switch to disable or enable testing of the 'lenBuffer' function. data Version = NoLen | YesLen deriving Eq ------------------------------------------------------------------------ -- | An efficient mutable circular buffer. data Buffer = Buffer { top :: IORef Int -- ^ Index to the top: where to 'Put' the next element , bot :: IORef Int -- ^ Index to the bottom: where to 'Get' the next element , arr :: IOVector Int -- ^ Array of elements of fixed capacity } -- | Different buffers are assumed to have disjoint memories, -- so we can use 'V.overlaps' to check equality. instance Eq Buffer where (==) = ((==) `on` top) `also` ((==) `on` bot) `also` (V.overlaps `on` arr) where also = (liftA2 . liftA2) (&&) -- | See 'New'. newBuffer :: Bugs -> Int -> IO Buffer newBuffer bugs n = Buffer <$> newIORef 0 <*> newIORef 0 <*> V.new (if FullIsEmpty `P.elem` bugs then n else n + 1) -- | See 'Put'. putBuffer :: Int -> Buffer -> IO () putBuffer x Buffer{top, arr} = do i <- readIORef top V.write arr i x writeIORef top $! (i + 1) `mod` V.length arr -- | See 'Get'. getBuffer :: Buffer -> IO Int getBuffer Buffer{bot, arr} = do j <- readIORef bot y <- V.read arr j writeIORef bot $! (j + 1) `mod` V.length arr return y -- | See 'Len'. lenBuffer :: Bugs -> Buffer -> IO Int lenBuffer bugs Buffer{top, bot, arr} = do i <- readIORef top j <- readIORef bot return $ if BadRem `P.elem` bugs then (i - j) `rem` V.length arr else if StillBadRem `P.elem` bugs then abs ((i - j) `rem` V.length arr) else (i - j) `mod` V.length arr ------------------------------------------------------------------------ -- | Buffer actions. data Action (r :: * -> *) -- | Create a new buffer of bounded capacity. = New Int -- | Put an element at the top of the buffer. | Put Int (Reference (Opaque Buffer) r) -- | Get an element out of the bottom of the buffer. | Get (Reference (Opaque Buffer) r) -- | Get the number of elements in the buffer. | Len (Reference (Opaque Buffer) r) deriving (Show, Generic1, Rank2.Functor, Rank2.Foldable, Rank2.Traversable) data Response (r :: * -> *) = NewR (Reference (Opaque Buffer) r) | PutR | GetR Int | LenR Int deriving (Show, Generic1, Rank2.Foldable) ------------------------------------------------------------------------ -- | A simple, persistent, inefficient buffer. -- -- The top of the buffer is the head of the list, the bottom is the last -- element. data SpecBuffer = SpecBuffer { specSize :: Int -- ^ Maximum number of elements , specContents :: [Int] -- ^ Contents of the buffer } deriving (Generic, Show, ToExpr) emptySpecBuffer :: Int -> SpecBuffer emptySpecBuffer n = SpecBuffer n [] insertSpecBuffer :: Int -> SpecBuffer -> SpecBuffer insertSpecBuffer x (SpecBuffer n xs) = SpecBuffer n (x : xs) removeSpecBuffer :: SpecBuffer -> (Int, SpecBuffer) removeSpecBuffer (SpecBuffer n xs) = (last xs, SpecBuffer n (init xs)) ------------------------------------------------------------------------ -- | The model is a map from buffer references to their values. newtype Model r = Model [(Reference (Opaque Buffer) r, SpecBuffer)] deriving (Generic, Show) deriving instance ToExpr (Model Concrete) -- | Initially, there are no references to buffers. initModel :: Model v initModel = Model [] precondition :: Bugs -> Model Symbolic -> Action Symbolic -> Logic precondition _ _ (New n) = n .> 0 precondition bugs (Model m) (Put _ buffer) | NoSizeCheck `P.elem` bugs = buffer `elem` map fst m precondition _ (Model m) (Put _ buffer) = Boolean $ isJust $ do specBuffer <- lookup buffer m guard $ length (specContents specBuffer) < specSize specBuffer precondition _ (Model m) (Get buffer) = Boolean $ isJust $ do specBuffer <- lookup buffer m guard $ not (null (specContents specBuffer)) precondition _ (Model m) (Len buffer) = buffer `elem` map fst m transition :: Eq1 r => Model r -> Action r -> Response r -> Model r transition (Model m) (New n) (NewR ref) = Model ((ref, emptySpecBuffer n) : m) transition (Model m) (Put x buffer) _ = case lookup buffer m of Just old -> Model (update buffer (insertSpecBuffer x old) m) Nothing -> error "transition: put" transition (Model m) (Get buffer) _ = case lookup buffer m of Just old -> let (_, new) = removeSpecBuffer old in Model (update buffer new m) Nothing -> error "transition: get" transition m _ _ = m update :: Eq a => a -> b -> [(a, b)] -> [(a, b)] update ref i m = (ref, i) : filter ((/= ref) . fst) m postcondition :: Model Concrete -> Action Concrete -> Response Concrete -> Logic postcondition _ (New _) _ = Top postcondition _ (Put _ _) _ = Top postcondition (Model m) (Get buffer) (GetR y) = case lookup buffer m of Nothing -> Bot Just specBuffer -> let (y', _) = removeSpecBuffer specBuffer in y .== y' postcondition (Model m) (Len buffer) (LenR k) = case lookup buffer m of Nothing -> Bot Just specBuffer -> k .== length (specContents specBuffer) postcondition _ _ _ = error "postcondition" ------------------------------------------------------------------------ genNew :: Gen (Action Symbolic) genNew = do Positive n <- arbitrary return (New n) generator :: Version -> Model Symbolic -> Gen (Action Symbolic) generator _ (Model m) | null m = genNew generator version (Model m) = frequency $ [ (1, genNew) , (4, Put <$> arbitrary <*> (fst <$> elements m)) , (4, Get <$> (fst <$> elements m)) ] ++ [ (4, Len <$> (fst <$> elements m)) | version == YesLen ] shrinker :: Action Symbolic -> [Action Symbolic] shrinker (New n) = [ New n' | n' <- shrink n ] shrinker (Put x buffer) = [ Put x' buffer | x' <- shrink x ] shrinker _ = [] ------------------------------------------------------------------------ semantics :: Bugs -> Action Concrete -> IO (Response Concrete) semantics bugs (New n) = NewR . reference . Opaque <$> newBuffer bugs n semantics _ (Put x buffer) = PutR <$ putBuffer x (opaque buffer) semantics _ (Get buffer) = GetR <$> getBuffer (opaque buffer) semantics bugs (Len buffer) = LenR <$> lenBuffer bugs (opaque buffer) mock :: Model Symbolic -> Action Symbolic -> GenSym (Response Symbolic) mock _ (New _) = NewR <$> genSym mock _ (Put _ _) = pure PutR mock (Model m) (Get buffer) = case lookup buffer m of Nothing -> error "mock: get" Just spec -> case specContents spec of [] -> error "mock: get 2" (i : _) -> pure (GetR i) mock (Model m) (Len buffer) = case lookup buffer m of Nothing -> error "mock: len" Just spec -> pure (LenR (specSize spec)) ------------------------------------------------------------------------ sm :: Version -> Bugs -> StateMachine Model Action IO Response sm version bugs = StateMachine initModel transition (precondition bugs) postcondition Nothing (generator version) Nothing shrinker (semantics bugs) mock -- | Property parameterized by spec version and bugs. prepropcircularBuffer :: Version -> Bugs -> Property prepropcircularBuffer version bugs = forAllCommands sm' Nothing $ \cmds -> monadicIO $ do (hist, _, res) <- runCommands sm' cmds prettyCommands sm' hist $ checkCommandNames cmds (res === Ok) where sm' = sm version bugs -- Adapted from John Hughes' -- /Experiences with QuickCheck: Testing the hard stuff and staying sane/, -- | The first bug. 'NoSizeCheck' -- -- Putting more elements than the capacity of the buffer (set when it is -- constructed using 'New') causes a buffer overflow: new elements overwrite -- older ones that haven't been removed yet. -- A minimal counterexample that reveals the bug is simply: -- -- > buffer <- newBuffer 1 -- > putBuffer 0 buffer -- > putBuffer 1 buffer -- > getBuffer buffer -- > -- > -- Expected: 0 -- > -- Actual: 1 -- -- The mistake is in the specification: it models an unbounded buffer. -- For a bounded buffer, that sequence of calls makes no sense. -- The fix is to add a precondition to forbid 'Put' when the buffer is full. unpropNoSizeCheck :: Property unpropNoSizeCheck = prepropcircularBuffer NoLen [NoSizeCheck ..] -- | The second bug. 'FullIsEmpty' -- -- The top and bottom pointers wrap around when they reach the end of the -- array. We have that @top == bottom@ whenever the buffer is either empty or -- full. -- In other words, a full buffer is undistinguishable from an empty one. -- A minimal counterexample: -- -- > buffer <- newBuffer 1 -- > putBuffer 0 buffer -- > lenBuffer buffer -- > -- > -- Expected: 1 -- > -- Actual: 0 -- -- In this implementation, the length of a buffer is given by the remainder of -- a division by its capacity. When the capacity is one, that remainder is -- always 0. -- The fix is to allocate one more cell when we allocate a 'New' buffer. -- -- In a way, the bug is still there. But to observe it, one has to -- 'Put' one more element than the buffer capacity. Since this violates the -- specification, it's the user's fault! unpropFullIsEmpty :: Property unpropFullIsEmpty = prepropcircularBuffer YesLen [FullIsEmpty ..] -- | The third bug. 'BadRem' -- -- The length of a buffer uses 'rem', which is the remainder of a -- division truncated towards zero (the standard division in many languages, -- such as C, but not Haskell). When the dividend @(top - bottom)@ is negative, -- the remainder is non-positive. -- A minimal counterexample: -- -- > buffer <- newBuffer 1 -- > putBuffer 0 buffer -- > getBuffer buffer -- > putBuffer 0 buffer -- > lenBuffer buffer -- > -- > -- Expected: 1 -- > -- Actual: -1 -- -- The fix is to ensure the remainder is non-negative... unpropBadRem :: Property unpropBadRem = prepropcircularBuffer YesLen [BadRem ..] -- | The fourth bug. 'StillBadRem' -- -- ... One way to obtain a non-negative remainder is to make the dividend -- non-negative. /Clearly/ we should divide by the absolute value instead. -- QuickCheck provides a minimal counterexample to that "obvious" fix: -- -- > buffer <- newBuffer 2 -- > putBuffer 0 buffer -- > getBuffer buffer -- > putBuffer 0 buffer -- > putBuffer 0 buffer -- > lenBuffer len -- > -- > -- Expected: 2 -- > -- Actual: 1 -- -- As an aside, for the first time, the buffer /needs/ to be of capacity two. -- That non-fix fixed buffers of capacity one! -- -- The actual fix is to use 'mod', -- which performs division rounding towards -∞. unpropStillBadRem :: Property unpropStillBadRem = prepropcircularBuffer YesLen [StillBadRem] -- | And now tests pass. prop_circularBuffer :: Property prop_circularBuffer = prepropcircularBuffer YesLen []