diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,6 @@
+
+# Changelog for circular
+
+
+## Unreleased changes
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Dominik Schrempf (c) 2020
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Dominik Schrempf nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,25 @@
+
+# Circular
+
+<p align="center"><img src="https://travis-ci.org/dschrempf/circular.svg?branch=master"/></p>
+
+Circular fixed-sized stacks.
+
+Circular stacks with fxed maximum size are just normal vectors with a
+pointer to the last element. They are useful because
+
+-   memory usage is constant
+-   they are fast, especially when summary statistics need to be
+    computed across the stack
+-   they can be saved, and restored using JSON format
+
+When the stack is full, new elements pushed on the stack replace the oldest
+(deepest) elements on the stack. Complex circular behavior can arise when pushes
+and pops are mixed. QuickCheck and unit tests with HSpec give promising results
+&#x2014; have a look yourself.
+
+I use them, for example, as the data type for traces of Markov chains.
+
+`Circular` is actively developed and functions may be removed, renamed, or
+changed. New ideas are welcome!
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/circular.cabal b/circular.cabal
new file mode 100644
--- /dev/null
+++ b/circular.cabal
@@ -0,0 +1,60 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.34.2.
+--
+-- see: https://github.com/sol/hpack
+
+name:           circular
+version:        0.1.0
+synopsis:       Circular fixed-sized mutable vectors
+description:    Please see the README on GitHub at <https://github.com/dschrempf/circular#readme>
+category:       Math, Data Structures
+homepage:       https://github.com/dschrempf/circular#readme
+bug-reports:    https://github.com/dschrempf/circular/issues
+author:         Dominik Schrempf
+maintainer:     dominik.schrempf@gmail.com
+copyright:      Dominik Schrempf (2020)
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://github.com/dschrempf/circular
+
+library
+  exposed-modules:
+      Data.Stack.Circular
+  other-modules:
+      Paths_circular
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      aeson
+    , base >=4.7 && <5
+    , vector
+  default-language: Haskell2010
+
+test-suite circular-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Data.Stack.CircularSpec
+      Paths_circular
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      QuickCheck
+    , aeson
+    , base >=4.7 && <5
+    , circular
+    , hspec
+    , hspec-discover
+    , quickcheck-instances
+    , vector
+  default-language: Haskell2010
diff --git a/src/Data/Stack/Circular.hs b/src/Data/Stack/Circular.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Stack/Circular.hs
@@ -0,0 +1,261 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- |
+-- Module      :  Data.Stack.Circular
+-- Description :  Circular stacks of fixed size
+-- Copyright   :  (c) Dominik Schrempf, 2020
+-- License     :  GPL-3.0-or-later
+--
+-- Maintainer  :  dominik.schrempf@gmail.com
+-- Stability   :  unstable
+-- Portability :  portable
+--
+-- Creation date: Thu Jun 18 10:00:28 2020.
+module Data.Stack.Circular
+  ( -- * Boxed circular stacks
+    CStack (..),
+
+    -- * Construction
+    empty,
+
+    -- * Conversion
+    toVector,
+    toVectorN,
+    fromVector,
+
+    -- * Accessors
+    get,
+    pop,
+    push,
+    unsafePush,
+
+    -- * Queries
+    isFull,
+
+    -- * Folding
+    --
+    -- Here all fold functions should be provided, but I am too lazy. Instead,
+    -- let's just provide some optimized functions to compute summary statistics
+    -- across all values on the stack.
+    --
+    -- For reasons of efficiency, __commutativity__ of the combining function is
+    -- __assumed__ for fold-like functions provided in this section! That is,
+    -- the order of elements of the stack must not matter.
+    foldl1',
+    sum,
+    mean,
+    product,
+  )
+where
+
+import Control.Monad.ST
+import Data.Aeson
+import Data.Aeson.Types
+import qualified Data.Vector.Generic as V
+import Data.Vector.Generic (Vector)
+import qualified Data.Vector.Generic.Mutable as M
+import Prelude hiding (product, sum)
+
+-- | Circular stacks with fxed maximum size are just normal vectors with a
+-- pointer to the last element.
+--
+-- Construction of 'CStack's is done with 'empty' and subsequent 'push'es, or
+-- the provided type conversion functions so that the index and bounds are
+-- updated and checked consistently. The data constructor 'CStack' is exported
+-- only to enable creation of orphan instances such as Arbitrary (QuickCheck).
+--
+-- When denoting the efficiency of the functions @m@ refers to the current size
+-- of the stack, and @n@ to the maximum size.
+data CStack v a = CStack
+  { stack :: v a,
+    index :: !Int,
+    curSize :: !Int
+  }
+
+instance (Eq (v a), Vector v a) => Eq (CStack v a) where
+  (CStack v1 i1 m1) == (CStack v2 i2 m2) = (v1 == v2) && (i1 == i2) && (m1 == m2)
+
+instance (Show (v a), Vector v a) => Show (CStack v a) where
+  show c@(CStack _ i m) = "CStack {" ++ show (toVector c) ++ ", " ++ show i ++ ", " ++ show m ++ "}"
+
+-- | We have @c /= FromJSON $ ToJSON c@, but the elements on the stack and their
+-- order are correctly saved and restored.
+instance (ToJSON a, ToJSON (v a), Vector v a) => ToJSON (CStack v a) where
+  toJSON c = object ["stack" .= toVector c, "maxSize" .= n]
+    where
+      n = V.length $ stack c
+  toEncoding c = pairs ("stack" .= toVector c <> "maxSize" .= n)
+    where
+      n = V.length $ stack c
+
+instance (FromJSON a, FromJSON (v a), Vector v a) => FromJSON (CStack v a) where
+  parseJSON = withObject "CStack" fromObject
+
+fromObject :: forall v a. (FromJSON (v a), Vector v a) => Object -> Parser (CStack v a)
+fromObject o = do
+  v <- o .: "stack" :: Parser (v a)
+  n <- o .: "maxSize" :: Parser Int
+  let c = empty n
+  pure $ V.foldr' unsafePush c v
+
+-- Calculate the start index of the stack.
+--
+-- (startIndex + m - 1) `mod` n = i
+startIndex :: Int -> Int -> Int -> Int
+startIndex i m n
+  | m == 0 = error "startIndex: empty stack"
+  | m <= i + 1 = i + 1 - m
+  | otherwise = i + 1 - m + n
+
+-- | A circular stack without an element but of a given maximum size. At this
+-- state, it is not very useful :). O(n).
+empty :: Vector v a => Int -> CStack v a
+empty n
+  | n <= 0 = error "empty: maximum size must be 1 or larger"
+  | otherwise = CStack (V.create $ M.unsafeNew n) 0 0
+
+-- | Convert a circular stack to a vector. The first element of the returned
+-- vector is the deepest (oldest) element of the stack, the last element of the
+-- returned vector is the current (newest) element of the stack.
+--
+-- This is a relatively expensive operation. O(m).
+toVector :: Vector v a => CStack v a -> v a
+toVector (CStack v i m)
+  | m == 0 = V.empty
+  | i' + m <= n = V.unsafeSlice i' m v
+  | otherwise = V.unsafeDrop i' v V.++ V.unsafeTake (i + 1) v
+  where
+    n = V.length v
+    i' = startIndex i m n
+
+-- | Convert the last N elements of a circular stack to a vector. The first
+-- element of the returned vector is the deepest (oldest) element of the stack,
+-- the last element of the returned vector is the current (newest) element of
+-- the stack.
+--
+-- The size of the stack must be larger than N.
+--
+-- This is a relatively expensive operation. O(N).
+toVectorN :: Vector v a => Int -> CStack v a -> v a
+toVectorN k (CStack v i m)
+  | k < 0 = error "toVectorN: negative n"
+  | k > m = error "toVectorN: stack too small"
+  | k == 0 = V.empty
+  | i' + k <= n = V.unsafeSlice i' k v
+  | otherwise = V.unsafeDrop i' v V.++ V.unsafeTake (i + 1) v
+  where
+    n = V.length v
+    i' = startIndex i k n
+
+-- | Convert a vector to a circular stack. The first element of the vector is
+-- the deepest (oldest) element of the stack, the last element of the vector is
+-- the current (newest) element of the stack. O(n).
+--
+-- The vector must be non-empty.
+fromVector :: Vector v a => v a -> CStack v a
+fromVector v
+  | V.null v = error "fromVector: empty vector"
+  | otherwise = CStack v (n - 1) n
+  where
+    n = V.length v
+
+-- | Get the last element without changing the stack. O(1).
+get :: Vector v a => CStack v a -> a
+get (CStack v i _) = V.unsafeIndex v i
+
+-- Select the previous element without changing the stack.
+previous :: Vector v a => CStack v a -> CStack v a
+previous (CStack v i m)
+  | m == 0 = error "previous: empty stack"
+  | i == 0 = CStack v (n - 1) (m - 1)
+  | otherwise = CStack v (i - 1) (m - 1)
+  where
+    n = V.length v
+
+-- | Get the last element and remove it from the stack. O(1).
+--
+-- The stack must be non-empty.
+pop :: Vector v a => CStack v a -> (a, CStack v a)
+pop c = (get c, previous c)
+
+-- Replace an element in a vector.
+set :: Vector v a => Int -> a -> v a -> v a
+set i x = V.modify (\v -> M.write v i x)
+{-# INLINE set #-}
+
+-- Replace the last element.
+put :: Vector v a => a -> CStack v a -> CStack v a
+put x (CStack v i m) = CStack (set i x v) i m
+
+-- Select the next element without changing the stack.
+next :: Vector v a => CStack v a -> CStack v a
+next (CStack v i m)
+  | i == (n - 1) = CStack v 0 (min (m + 1) n)
+  | otherwise = CStack v (i + 1) (min (m + 1) n)
+  where
+    n = V.length v
+
+-- | Push an element on the stack. O(n).
+push :: Vector v a => a -> CStack v a -> CStack v a
+push x c = put x $ next c
+
+unsafeSet :: Vector v a => Int -> a -> v a -> v a
+unsafeSet i x v = runST $ do
+  mv <- V.unsafeThaw v
+  M.unsafeWrite mv i x
+  V.unsafeFreeze mv
+
+-- Replace the last element. O(1).
+unsafePut :: Vector v a => a -> CStack v a -> CStack v a
+unsafePut x (CStack v i m) = CStack (unsafeSet i x v) i m
+
+-- | Push an element on the stack. O(1).
+--
+-- Be careful; the internal vector is mutated! The immutable circular stack may
+-- not be used after this operation.
+unsafePush :: Vector v a => a -> CStack v a -> CStack v a
+unsafePush x c = unsafePut x $ next c
+
+-- | Check if the stack is full.
+isFull :: Vector v a => CStack v a -> Bool
+isFull (CStack v _ m) = V.length v == m
+
+-- | Compute summary statistics of the elements on the stack using a custom
+-- commutative `mappend` function.
+foldl1' :: Vector v a => (a -> a -> a) -> CStack v a -> a
+foldl1' f (CStack v i m)
+  | m == n = V.foldl1' f v
+  | i' + m <= n = V.foldl1' f $ V.unsafeSlice i' m v
+  | otherwise = f (V.foldl1' f (V.unsafeDrop i' v)) (V.foldl1' f (V.unsafeTake (i + 1) v))
+  where
+    n = V.length v
+    i' = startIndex i m n
+
+-- | Compute the sum of the elements on the stack.
+sum :: (Num a, Vector v a) => CStack v a -> a
+sum (CStack v i m)
+  | m == n = V.sum v
+  | i' + m <= n = V.sum $ V.unsafeSlice i' m v
+  | otherwise = V.sum (V.unsafeDrop i' v) + V.sum (V.unsafeTake (i + 1) v)
+  where
+    n = V.length v
+    i' = startIndex i m n
+
+-- | Compute the mean of the elements on the stack.
+mean :: (Real a, Vector v a, Fractional b) => CStack v a -> b
+mean c = realToFrac (sum c) / fromIntegral (curSize c)
+
+-- | Compute the product of the elements on the stack.
+--
+-- For reasons of efficiency, commutativity of the combining function is
+-- assumed. That is, the order of elements of the stack must not matter.
+product :: (Num a, Vector v a) => CStack v a -> a
+product (CStack v i m)
+  | m == n = V.product v
+  | i' + m <= n = V.product $ V.unsafeSlice i' m v
+  | otherwise = V.product (V.unsafeDrop i' v) * V.product (V.unsafeTake (i + 1) v)
+  where
+    n = V.length v
+    i' = startIndex i m n
diff --git a/test/Data/Stack/CircularSpec.hs b/test/Data/Stack/CircularSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Stack/CircularSpec.hs
@@ -0,0 +1,120 @@
+{-# LANGUAGE FlexibleInstances #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+-- |
+--   Module      :  Data.Stack.CircularSpec
+--   Description :  Unit tests for Data.Stack.Circular
+--   Copyright   :  (c) Dominik Schrempf, 2020
+--   License     :  GPL-3.0-or-later
+--
+--   Maintainer  :  dominik.schrempf@gmail.com
+--   Stability   :  unstable
+--   Portability :  portable
+--
+-- Creation date: Thu Jun 18 10:21:28 2020.
+module Data.Stack.CircularSpec
+  ( spec,
+  )
+where
+
+import Control.Exception
+import Data.Aeson
+import Data.Stack.Circular as C
+import Data.Vector (Vector)
+import qualified Data.Vector as V
+import Test.Hspec
+import Test.Hspec.QuickCheck
+import Test.QuickCheck hiding (Success, Result)
+import Test.QuickCheck.Instances.Vector ()
+import Prelude hiding (sum, product)
+
+instance Arbitrary (CStack Vector Int) where
+  arbitrary = do
+    s <- getSize
+    n <- choose (1, s+1)
+    v <- V.fromList <$> vector n
+    i <- choose (0, n-1)
+    m <- choose (1, n)
+    return $ CStack v i m
+
+se :: CStack Vector Int
+se = empty 10
+
+ss :: CStack Vector Int
+ss = push 13 se
+
+prop_from_to_id :: Vector Int -> Bool
+prop_from_to_id v
+  | V.length v == 0 = True
+  | otherwise = toVector (fromVector v) == v
+
+prop_pop :: Vector Int -> Bool
+prop_pop v
+  | V.length v == 0 = True
+  | otherwise = toVector (snd $ pop $ fromVector v) == V.init v
+
+prop_push_pop :: Int -> Vector Int -> Bool
+prop_push_pop x v
+  | V.length v == 0 = True
+  | otherwise = toVector (snd $ pop $ push x $ fromVector v) == V.tail v
+
+prop_push :: Int -> Vector Int -> Bool
+prop_push x v
+  | V.length v == 0 = True
+  | otherwise = toVector (push x $ fromVector v) == V.tail v V.++ V.singleton x
+
+prop_many_pushes :: [Int] -> Vector Int -> Bool
+prop_many_pushes xs v
+  | V.length v == 0 = True
+  | length xs <= V.length v = True
+  | otherwise =
+    toVector (foldr push (fromVector v) xs)
+      == V.fromList (reverse $ take (V.length v) xs)
+
+prop_length :: CStack Vector Int -> Bool
+prop_length c = V.length (toVector c) == curSize c
+
+jsonId :: CStack Vector Int -> Result (CStack Vector Int)
+jsonId c = fromJSON $ toJSON c
+
+prop_json_sum :: CStack Vector Int -> Bool
+prop_json_sum c = (sum <$> jsonId c) == Success (sum c)
+
+prop_json_product :: CStack Vector Int -> Bool
+prop_json_product c = (product <$> jsonId c) == Success (product c)
+
+-- Check current size and max size.
+prop_json_misc :: CStack Vector Int -> Bool
+prop_json_misc c = ((curSize <$> jsonId c) == Success (curSize c)) &&
+                   ((V.length . stack <$> jsonId c) == Success (V.length $ stack c))
+
+spec :: Spec
+spec = do
+  describe "construction" $ it "doesn't choke on weird inputs" $ do
+    print ss
+    toVector se `shouldBe` V.empty
+    toVector (snd $ pop ss) `shouldBe` V.empty
+
+  describe "conversion identities" $ do
+    it "correctly converts partly filled stacks" $
+      toVector ss `shouldBe` V.singleton 13
+    prop "toVector . fromVector is identity" (prop_from_to_id :: Vector Int -> Bool)
+
+  describe "conversion failure" $
+    it "fails to convert empty vectors" $
+      evaluate (fromVector V.empty) `shouldThrow` anyErrorCall
+
+  describe "properties" $ do
+    prop "pop" prop_pop
+    prop "push" prop_push
+    prop "push pop" prop_push_pop
+    prop "many pushed" prop_many_pushes
+    prop "length" prop_length
+    prop "json sum" prop_json_sum
+    prop "json product" prop_json_product
+    prop "json misc" prop_json_misc
+
+  describe "laziness" $
+    it "should not conflict with intuition" $
+    toVector ss `shouldNotBe` toVector (push 10 ss)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
