diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for construct
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2016, Mario Blažević
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+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,113 @@
+Construct.hs
+============
+
+This is a Haskell implementation of Python's [Construct](https://construct.readthedocs.io/en/latest/intro.html)
+library. It provides a succinct and easy way to specify data formats. Before you get to the succinct part, though,
+you'll probably need a bunch of extensions and imports:
+
+~~~ {.haskell}
+{-# LANGUAGE FlexibleInstances, StandaloneDeriving, TemplateHaskell #-}
+
+module README where
+
+import Data.Functor.Identity (Identity(Identity))
+import Data.Word (Word8)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as ASCII
+import qualified Rank2.TH
+import Text.ParserCombinators.Incremental.LeftBiasedLocal (Parser, completeResults, feed, feedEof)
+import Construct
+
+import Prelude hiding ((*>), (<*))
+~~~
+
+Example
+-------
+
+With that out of the way, let's take the simple example format from the original. Here's what its specification looks
+like in Haskell:
+
+~~~ {.haskell}
+data BitMap f = BitMap{
+   width :: f Word8,
+   height :: f Word8,
+   pixels :: f [[Word8]]
+   }
+deriving instance Show (BitMap Identity)
+$(Rank2.TH.deriveAll ''BitMap)
+
+format :: Format (Parser ByteString) Maybe ByteString (BitMap Identity)
+format = literal (ASCII.pack "BMP") *> mfix (\this-> record
+  BitMap{
+        width= byte,
+        height= byte,
+        pixels= count (fromIntegral $ height this) (count (fromIntegral $ width this) byte)
+        })
+~~~
+
+There are two parts to the specification.
+
+The `data BitMap` declaration specifies the in-memory layout of a simple bitmap. Note that it's declared as a record
+with every field wrapped in a type constructor parameter. This declaration style is sometimes called [Higher-Kinded
+Data](https://reasonablypolymorphic.com/blog/higher-kinded-data/). To regain a regular record, just instantiate the
+parameter to `Identity` &mdash; a `BitMap Identity` contains exactly one value of each field.
+
+The other part of the specification is the `format` definition that specifies the bi-directional mapping between the
+in-memory and the serialized form of the bitmap. A bijection, to be precise. The two definitions are enough to automatically
+serialize the in-memory record form into the binary form:
+
+~~~ {.haskell}
+-- | >>> serialize format BitMap{width= Identity 3, height= Identity 2, pixels= Identity [[7,8,9], [11,12,13]]}
+-- Just "BMP\ETX\STX\a\b\t\v\f\r"
+~~~
+
+and to parse the serialized binary form back into the record structure:
+
+~~~ {.haskell}
+-- | >>> completeResults $ feedEof $ feed (ASCII.pack "BMP" <> ByteString.pack [3, 2, 7, 8, 9, 11, 12, 13]) $ parse format
+-- [(BitMap {width = Identity 3, height = Identity 2, pixels = Identity [[7,8,9],[11,12,13]]},"")]
+~~~
+
+Examples of more complex and realistic formats can be found in the
+[`test`](https://github.com/blamario/construct/tree/master/test) directory.
+
+Acknowledgements
+----------------
+
+I owe the inspiration for this library to Yair Chuchem and his
+[post](https://yairchu.github.io/posts/codecs-as-prisms.html) that introduced me to Construct. I must also express
+gratitude to the authors of the original library of course. And finally, to the authors of the paper [Invertible
+Syntax Descriptions:Unifying Parsing and Pretty
+Printing](https://www.informatik.uni-marburg.de/~rendel/unparse/rendel10invertible.pdf) which I remembered reading
+just in time to avoid following some bad ideas.
+
+Implementation notes
+--------------------
+
+I had to overcome two problems in the course of the implementation. The first difficulty, already touched on in the
+aforementioned blog post, is how to convert a record of formats into a format of the record. As the author of
+[rank2classes](https://hackage.haskell.org/package/rank2classes), I went for an obvious solution: parameterize the
+record as seen in the example, make it an instance of the
+[`Rank2.Traversable`](https://hackage.haskell.org/package/rank2classes-1.3.1.2/docs/Rank2.html#t:Traversable) class,
+and apply [`Rank2.traverse`](https://hackage.haskell.org/package/rank2classes-1.3.1.2/docs/Rank2.html#v:traverse) to
+it.
+
+That little trick alone would have been enough for a nearly complete package, except the Python library also enables a
+record field to refer to any of the preceding fields. This is not a problem when serializing, but when parsing it
+means that a parser must be able to refer to an already-parsed part of the value that's still being parsed.
+
+The standard solution to the problem of accessing the results (parsed values) of a computation (parsing) *while within
+the computation* is known as the
+[`MonadFix`](https://hackage.haskell.org/package/base-4.12.0.0/docs/Control-Monad-Fix.html#t:MonadFix) class, though
+perhaps not as widely as it deserves. You won't find any parsers in the list of its instances, though, and for a good
+reason - it's quite impossible for a parser to obtain a value that it hasn't parsed yet. All *we* need, luckily, is
+access an already parsed part of a partially parsed structure. A limited `MonadFix` instance can do that, provided
+that we also use a specialized form of `Rank2.traverse` capable of preserving the record structure while it's being
+parsed.
+
+As it happens, I have an old parser library named
+[incremental-parser](https://hackage.haskell.org/package/incremental-parser) on Hackage, and the name seems quite
+appropriate for what I'm doing here. I added the necessary functionality there, but another parser combinator library
+should be capable of the same feat. It just needs to implement the `mfix` and `fixSequence` combinators and it can be
+used with the present library.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,6 @@
+module Main where
+
+import Distribution.Extra.Doctest (defaultMainWithDoctests)
+
+main :: IO ()
+main = defaultMainWithDoctests "doctests"
diff --git a/construct.cabal b/construct.cabal
new file mode 100644
--- /dev/null
+++ b/construct.cabal
@@ -0,0 +1,67 @@
+cabal-version:       >=1.10
+name:                construct
+version:             0.1
+synopsis:            Haskell version of the Construct library for easy specification of file formats
+description:
+   A Haskell version of the <https://construct.readthedocs.io/en/latest/intro.html Construct> library for Python. A
+   succinct file format specification provides both a parser and the serializer for the format.
+bug-reports:         https://github.com/blamario/construct/issues
+license:             BSD3
+license-file:        LICENSE
+author:              Mario Blažević
+maintainer:          Mario Blažević <blamario@protonmail.com>
+copyright:           Mario Blažević 2020
+category:            Data, Parsing, Serialization
+build-type:          Custom
+extra-source-files:  CHANGELOG.md, README.md, test/README.lhs
+data-dir:            test/examples/
+data-files:          wmf1.wmf
+custom-setup
+ setup-depends:
+   base >= 4 && <5,
+   Cabal,
+   cabal-doctest >= 1 && <1.1
+ 
+
+library
+  -- other-extensions:
+  hs-source-dirs:      src
+  exposed-modules:     Construct, Construct.Bits, Construct.Classes
+  other-modules:       Construct.Internal
+  build-depends:       base >=4.11 && <4.13,
+                       bytestring >= 0.10 && < 0.11,
+                       text >= 0.10 && < 1.3,
+                       monoid-subclasses >= 1.0 && < 1.1,
+                       incremental-parser >= 0.4 && < 0.5,
+                       parsers >= 0.11 && < 0.13,
+                       attoparsec >= 0.12 && < 0.14,
+                       cereal >= 0.5 && < 0.6,
+                       rank2classes >= 1 && < 1.4
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+
+test-suite             doctests
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  default-language:    Haskell2010
+  main-is:             Doctest.hs
+  other-modules:       README
+  ghc-options:         -threaded -pgmL markdown-unlit
+  build-depends:       base, construct,
+                       bytestring >= 0.10 && < 0.11, incremental-parser >= 0.4 && < 0.5, rank2classes >= 1.0.2 && < 1.4,
+                       doctest >= 0.8
+  build-tool-depends:  markdown-unlit:markdown-unlit >= 0.5 && < 0.6
+  x-doctest-options:   -XTypeApplications
+
+test-suite             examples
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  x-uses-tf:           true
+  build-depends:       base >=4.9 && < 5, construct,
+                       bytestring < 0.11, text < 1.3, cereal, rank2classes >= 1.0.2 && < 1.4,
+                       monoid-subclasses >= 1.0 && < 1.1, incremental-parser < 0.5, attoparsec >= 0.12 && < 0.14,
+                       directory < 2, filepath < 1.5,
+                       tasty >= 0.7, tasty-hunit
+  main-is:             Test.hs
+  other-modules:       MBR, TAR, URI, WMF
+  default-language:    Haskell2010
diff --git a/src/Construct.hs b/src/Construct.hs
new file mode 100644
--- /dev/null
+++ b/src/Construct.hs
@@ -0,0 +1,452 @@
+{-# LANGUAGE FlexibleContexts, FlexibleInstances, GADTs #-}
+
+module Construct
+(
+  -- * The type
+  Format, parse, serialize,
+
+  -- * Combinators
+  (Construct.<$), (Construct.*>), (Construct.<*), (Construct.<|>), (<+>), (<?>),
+  empty, optional, optionWithDefault, pair, deppair, many, some, sepBy, count,
+  -- ** Self-referential record support
+  mfix, record,
+  -- ** Mapping over a 'Format'
+  mapSerialized, mapMaybeSerialized, mapValue, mapMaybeValue,
+  -- ** Constraining a 'Format'
+  satisfy, value, padded, padded1,
+
+  -- * Primitives
+  literal, byte, char,
+  cereal, cereal',
+  Construct.take, Construct.takeWhile, Construct.takeWhile1, Construct.takeCharsWhile, Construct.takeCharsWhile1,
+  -- * Test helpers
+  testParse, testSerialize
+) where
+
+import qualified Control.Applicative as Applicative
+import qualified Control.Monad.Fix as Monad.Fix
+import Control.Applicative (Applicative, Alternative)
+import Control.Monad.Fix (MonadFix)
+import Data.Functor ((<$>), void)
+import qualified Data.Functor.Const as Functor
+import qualified Data.List as List
+import Data.List.NonEmpty (nonEmpty)
+import Data.Maybe (fromMaybe)
+import Data.Semigroup (Semigroup, (<>), sconcat)
+import Data.Word (Word8)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as ByteString
+import qualified Data.Monoid.Factorial as Factorial
+import qualified Data.Monoid.Textual as Textual
+import qualified Data.Monoid.Null as Null
+import Data.Monoid.Factorial (FactorialMonoid)
+import Data.Monoid.Textual (TextualMonoid)
+import Data.String (IsString, fromString)
+import qualified Text.Parser.Combinators as Parser
+import qualified Text.Parser.Char as Parser.Char
+import qualified Text.ParserCombinators.Incremental as Incremental
+import Text.ParserCombinators.Incremental.Symmetric (Symmetric)
+import Data.Serialize (Serialize, Result(Done, Fail, Partial), Get, Putter, runGetPartial, runPut)
+import qualified Data.Serialize as Serialize
+
+import qualified Rank2
+
+import qualified Construct.Classes as Input
+import Construct.Classes (AlternativeFail(failure), InputParsing (ParserInput), InputCharParsing, InputMappableParsing,
+                          FixTraversable, Error, errorString, expectedName)
+import Construct.Internal
+
+import Prelude hiding (pred, take, takeWhile)
+
+-- $setup
+-- >>> import Data.Char (isDigit, isLetter)
+-- >>> import Data.Serialize.Get (getWord16le)
+-- >>> import Data.Serialize.Put (putWord16le)
+-- >>> import Data.Word (Word16)
+-- >>> import Numeric (showInt)
+
+literal  :: (Functor m, InputParsing m, Applicative n, ParserInput m ~ s) => s -> Format m n s ()
+-- | A literal serialized form, such as a magic constant, corresponding to no value
+--
+-- >>> testParse (literal "Hi") "Hi there"
+-- Right [(()," there")]
+literal s = Format{
+   parse = void (Input.string s),
+   serialize = const (pure s)
+   }
+
+-- | Modifies the serialized form of the given format by padding it with the given template if it's any shorter
+--
+-- >>> testParse (padded "----" $ takeCharsWhile isDigit) "12--3---"
+-- Right [("12","3---")]
+-- >>> testSerialize (padded "----" $ takeCharsWhile isDigit) "12"
+-- Right "12--"
+padded :: (Monad m, Functor n, InputParsing m, ParserInput m ~ s, FactorialMonoid s) =>
+          s -> Format m n s s -> Format m n s s
+padded template format = Format{
+   parse = parse format >>= parsePadding,
+   serialize = (padRight <$>) . serialize format
+   }
+   where padRight s = s <> Factorial.drop (Factorial.length s) template
+         parsePadding s = if Null.null padding then pure s else s Applicative.<$ Input.string padding
+            where padding = Factorial.drop (Factorial.length s) template
+
+-- | Modifies the serialized form of the given format by padding it with the given template. The serialized form has
+-- to be shorter than the template before padding.
+padded1 :: (Monad m, Monad n, InputParsing m, ParserInput m ~ s, FactorialMonoid s, Show s, AlternativeFail n) =>
+           s -> Format m n s s -> Format m n s s
+padded1 template format = Format{
+   parse = parse format >>= parsePadding,
+   serialize = \a-> serialize format a >>= padRight
+   }
+   where padRight s = if Null.null padding then expectedName ("padded1 " ++ show template) (failure $ show s)
+                      else pure (s <> padding)
+            where padding = Factorial.drop (Factorial.length s) template
+         parsePadding s = if Null.null padding
+                          then Parser.unexpected (show s) Parser.<?> ("padded1 " ++ show template)
+                          else s Applicative.<$ Input.string padding
+            where padding = Factorial.drop (Factorial.length s) template
+
+-- | Format whose in-memory value is a fixed-size prefix of the serialized value
+--
+-- >>> testParse (take 3) "12345"
+-- Right [("123","45")]
+-- >>> testSerialize (take 3) "123"
+-- Right "123"
+-- >>> testSerialize (take 3) "1234"
+-- Left "expected a value of length 3, encountered \"1234\""
+take :: (InputParsing m, ParserInput m ~ s, FactorialMonoid s, Show s, AlternativeFail n) => Int -> Format m n s s
+take n = Format{
+   parse = Input.take n,
+   serialize = \s-> if Factorial.length s == n then pure s
+                    else expectedName ("a value of length " ++ show n) (failure $ show s)
+   }
+
+-- | Format whose in-memory value is the longest prefix of the serialized value smallest parts of which all satisfy
+-- the given predicate.
+--
+-- >>> testParse (takeWhile (> "b")) "abcd"
+-- Right [("","abcd")]
+-- >>> testParse (takeWhile (> "b")) "dcba"
+-- Right [("dc","ba")]
+-- >>> testSerialize (takeWhile (> "b")) "dcba"
+-- Left "expected takeWhile, encountered \"dcba\""
+-- >>> testSerialize (takeWhile (> "b")) "dc"
+-- Right "dc"
+-- >>> testSerialize (takeWhile (> "b")) ""
+-- Right ""
+takeWhile :: (InputParsing m, ParserInput m ~ s, FactorialMonoid s, Show s, AlternativeFail n) =>
+             (s -> Bool) -> Format m n s s
+takeWhile pred = Format{
+   parse = Input.takeWhile pred,
+   serialize = \s-> if Null.null (Factorial.dropWhile pred s) then pure s
+                    else expectedName "takeWhile" (failure $ show s)
+   }
+
+-- | Format whose in-memory value is the longest non-empty prefix of the serialized value smallest parts of which all
+-- satisfy the given predicate.
+--
+-- >>> testParse (takeWhile1 (> "b")) "abcd"
+-- Left "takeWhile1"
+-- >>> testSerialize (takeWhile1 (> "b")) ""
+-- Left "expected takeWhile1, encountered \"\""
+-- >>> testSerialize (takeWhile1 (> "b")) "dc"
+-- Right "dc"
+takeWhile1 :: (InputParsing m, ParserInput m ~ s, FactorialMonoid s, Show s, AlternativeFail n) =>
+              (s -> Bool) -> Format m n s s
+takeWhile1 pred = Format{
+   parse = Input.takeWhile1 pred,
+   serialize = \s-> if not (Null.null s) && Null.null (Factorial.dropWhile pred s) then pure s
+                    else expectedName "takeWhile1" (failure $ show s)
+   }
+
+-- | Format whose in-memory value is the longest prefix of the serialized value that consists of characters which all
+-- satisfy the given predicate.
+--
+-- >>> testParse (takeCharsWhile isDigit) "a12"
+-- Right [("","a12")]
+-- >>> testParse (takeCharsWhile isDigit) "12a"
+-- Right [("12","a")]
+-- >>> testSerialize (takeCharsWhile isDigit) "12a"
+-- Left "expected takeCharsWhile, encountered \"12a\""
+-- >>> testSerialize (takeCharsWhile isDigit) "12"
+-- Right "12"
+-- >>> testSerialize (takeCharsWhile isDigit) ""
+-- Right ""
+takeCharsWhile :: (InputCharParsing m, ParserInput m ~ s, TextualMonoid s, Show s, AlternativeFail n) =>
+                  (Char -> Bool) -> Format m n s s
+takeCharsWhile pred = Format{
+   parse = Input.takeCharsWhile pred,
+   serialize = \s-> if Null.null (Textual.dropWhile_ False pred s) then pure s
+                    else expectedName "takeCharsWhile" (failure $ show s)
+   }
+
+-- | Format whose in-memory value is the longest non-empty prefix of the serialized value that consists of characters
+-- which all satisfy the given predicate.
+--
+-- >>> testParse (takeCharsWhile1 isDigit) "a12"
+-- Left "takeCharsWhile1 encountered 'a'"
+-- >>> testParse (takeCharsWhile1 isDigit) "12a"
+-- Right [("12","a")]
+-- >>> testSerialize (takeCharsWhile1 isDigit) "12"
+-- Right "12"
+-- >>> testSerialize (takeCharsWhile1 isDigit) ""
+-- Left "expected takeCharsWhile1, encountered \"\""
+takeCharsWhile1 :: (InputCharParsing m, ParserInput m ~ s, TextualMonoid s, Show s, AlternativeFail n) =>
+                   (Char -> Bool) -> Format m n s s
+takeCharsWhile1 pred = Format{
+   parse = Input.takeCharsWhile1 pred,
+   serialize = \s-> if not (Null.null s) && Null.null (Textual.dropWhile_ False pred s) then pure s
+                    else expectedName "takeCharsWhile1" (failure $ show s)
+   }
+
+value :: (Eq a, Show a, Parser.Parsing m, Monad m, Alternative n) => Format m n s a -> a -> Format m n s ()
+-- | A fixed expected value serialized through the argument format
+--
+-- >>> testParse (value char 'a') "bcd"
+-- Left "encountered 'b'"
+-- >>> testParse (value char 'a') "abc"
+-- Right [((),"bc")]
+value f v = Format{
+   parse = void (parse f >>= \x-> if x == v then pure x else Parser.unexpected (show x)),
+   serialize = \()-> serialize f v
+   }
+
+satisfy :: (Parser.Parsing m, Monad m, AlternativeFail n, Show a) => (a -> Bool) -> Format m n s a -> Format m n s a
+-- | Filter the argument format so it only succeeds for values that pass the predicate.
+--
+-- >>> testParse (satisfy isDigit char) "abc"
+-- Left "encountered 'a'"
+-- >>> testParse (satisfy isLetter char) "abc"
+-- Right [('a',"bc")]
+satisfy predicate f = Format{
+   parse = parse f >>= \v-> if predicate v then pure v else Parser.unexpected (show v),
+   serialize = \v-> if predicate v then serialize f v else expectedName "satisfy" (failure $ show v)
+   }
+
+-- | Converts a format for serialized streams of type @s@ so it works for streams of type @t@ instead
+--
+-- >>> testParse (mapSerialized ByteString.unpack ByteString.pack byte) [1,2,3]
+-- Right [(1,[2,3])]
+mapSerialized :: (Monoid s, Monoid t, InputParsing (m s), InputParsing (m t),
+                  s ~ ParserInput (m s), t ~ ParserInput (m t), InputMappableParsing m, Functor n) =>
+                 (s -> t) -> (t -> s) -> Format (m s) n s a -> Format (m t) n t a
+mapSerialized f f' format = Format{
+   parse = Input.mapParserInput f f' (parse format),
+   serialize = (f <$>) . serialize format}
+
+-- | Converts a format for serialized streams of type @s@ so it works for streams of type @t@ instead. The argument
+-- functions may return @Nothing@ to indicate they have insuficient input to perform the conversion.
+mapMaybeSerialized :: (Monoid s, Monoid t, InputParsing (m s), InputParsing (m t),
+                       s ~ ParserInput (m s), t ~ ParserInput (m t), InputMappableParsing m, Functor n) =>
+                      (s -> Maybe t) -> (t -> Maybe s) -> Format (m s) n s a -> Format (m t) n t a
+mapMaybeSerialized f f' format = Format{
+   parse = Input.mapMaybeParserInput f f' (parse format),
+   serialize = (fromMaybe (error "Partial serialization") . f <$>) . serialize format}
+
+-- | Converts a format for in-memory values of type @a@ so it works for values of type @b@ instead.
+--
+-- >>> testParse (mapValue (read @Int) show $ takeCharsWhile1 isDigit) "012 34"
+-- Right [(12," 34")]
+-- >>> testSerialize (mapValue read show $ takeCharsWhile1 isDigit) 12
+-- Right "12"
+mapValue :: Functor m => (a -> b) -> (b -> a) -> Format m n s a -> Format m n s b
+mapValue f f' format = Format{
+   parse = f <$> parse format,
+   serialize = serialize format . f'}
+
+-- | Converts a format for in-memory values of type @a@ so it works for values of type @b@ instead. The argument
+-- functions may signal conversion failure by returning @Nothing@.
+mapMaybeValue :: (Monad m, Parser.Parsing m, Show a, Show b, AlternativeFail n) =>
+                 (a -> Maybe b) -> (b -> Maybe a) -> Format m n s a -> Format m n s b
+mapMaybeValue f f' format = Format{
+   parse = parse format >>= \v-> maybe (Parser.unexpected $ show v) pure (f v),
+   serialize = \v-> maybe (expectedName "mapMaybeValue" (failure $ show v)) (serialize format) (f' v)}
+
+byte :: (InputParsing m, ParserInput m ~ ByteString, Applicative n) => Format m n ByteString Word8
+-- | A trivial format for a single byte in a 'ByteString'
+--
+-- >>> testParse byte (ByteString.pack [1,2,3])
+-- Right [(1,"\STX\ETX")]
+byte = Format{
+   parse = ByteString.head <$> Input.anyToken,
+   serialize = pure . ByteString.singleton}
+
+char     :: (Parser.Char.CharParsing m, ParserInput m ~ s, IsString s, Applicative n) => Format m n s Char
+-- | A trivial format for a single character
+--
+-- >>> testParse char "abc"
+-- Right [('a',"bc")]
+char = Format{
+   parse = Parser.Char.anyChar,
+   serialize = pure . fromString . (:[])}
+
+cereal :: (Serialize a, Monad m, InputParsing m, ParserInput m ~ ByteString, Applicative n) => Format m n ByteString a
+-- | A quick way to format a value that already has an appropriate 'Serialize' instance
+--
+-- >>> testParse (cereal @Word16) (ByteString.pack [1,2,3])
+-- Right [(258,"\ETX")]
+-- >>> testSerialize cereal (1025 :: Word16)
+-- Right "\EOT\SOH"
+cereal = cereal' Serialize.get Serialize.put
+
+cereal' :: (Monad m, InputParsing m, ParserInput m ~ ByteString, Applicative n) =>
+            Get a -> Putter a -> Format m n ByteString a
+-- | Specifying a formatter explicitly using the cereal getter and putter
+--
+-- >>> testParse (cereal' getWord16le putWord16le) (ByteString.pack [1,2,3])
+-- Right [(513,"\ETX")]
+cereal' get put = Format p (pure . runPut . put)
+   where p = go (runGetPartial get mempty)
+            where go (Fail msg _) = fail msg
+                  go (Done r _) = pure r
+                  go (Partial cont) = Input.anyToken >>= go . cont
+
+count :: (Applicative m, AlternativeFail n, Show a, Monoid s) => Int -> Format m n s a -> Format m n s [a]
+-- | Repeats the argument format the given number of times.
+--
+-- >>> testParse (count 4 byte) (ByteString.pack [1,2,3,4,5])
+-- Right [([1,2,3,4],"\ENQ")]
+-- >>> testSerialize (count 4 byte) [1,2,3,4,5]
+-- Left "expected a list of length 4, encountered [1,2,3,4,5]"
+-- >>> testSerialize (count 4 byte) [1,2,3,4]
+-- Right "\SOH\STX\ETX\EOT"
+count n item = Format{
+   parse = Parser.count (fromIntegral n) (parse item),
+   serialize = \as-> if length as == n then mconcat <$> traverse (serialize item) as
+                     else expectedName ("a list of length " ++ show n) (failure $ show as)}
+
+record :: (Rank2.Apply g, Rank2.Traversable g, FixTraversable m, Monoid (n s), Applicative o, Foldable o) =>
+          g (Format m n s) -> Format m n s (g o)
+-- | Converts a record of field formats into a single format of the whole record.
+record formats = Format{
+   parse = Input.fixSequence (parse Rank2.<$> formats),
+   serialize = Rank2.foldMap Functor.getConst . Rank2.liftA2 serializeField formats
+   }
+   where serializeField format xs = Functor.Const (foldMap (serialize format) xs)
+
+infixl 3 <|>
+infixl 4 <$
+infixl 4 <*
+infixl 4 *>
+
+(<$) :: (Eq a, Show a, Functor m, AlternativeFail n) => a -> Format m n s () -> Format m n s a
+-- | Same as the usual 'Data.Functor.<$' except a 'Format' is no 'Functor'.
+a <$ f = Format{
+   parse = a Applicative.<$ parse f,
+   serialize = \b-> if a == b then serialize f () else expectedName (show a) (failure $ show b)}
+
+(*>) :: (Applicative m, Semigroup (n s)) => Format m n s () -> Format m n s a -> Format m n s a
+-- | Same as the usual 'Applicative.*>' except a 'Format' is no 'Functor', let alone 'Applicative'.
+f1 *> f2 = Format{
+   parse = parse f1 Applicative.*> parse f2,
+   serialize = \a-> serialize f1 () <> serialize f2 a}
+
+(<*) :: (Applicative m, Semigroup (n s)) => Format m n s a -> Format m n s () -> Format m n s a
+-- | Same as the usual 'Applicative.<*' except a 'Format' is no 'Functor', let alone 'Applicative'.
+f1 <* f2 = Format{
+   parse = parse f1 Applicative.<* parse f2,
+   serialize = \a-> serialize f1 a <> serialize f2 ()}
+
+(<|>) :: (Alternative m, Alternative n) => Format m n s a -> Format m n s a -> Format m n s a
+-- | Same as the usual 'Applicative.<|>' except a 'Format' is no 'Functor', let alone 'Alternative'.
+f1 <|> f2 = Format{
+   parse = parse f1 Applicative.<|> parse f2,
+   serialize = \a-> serialize f1 a Applicative.<|> serialize f2 a}
+
+(<+>) :: Alternative m => Format m n s a -> Format m n s b -> Format m n s (Either a b)
+-- | A discriminated or tagged choice between two formats.
+f1 <+> f2 = Format{
+   parse = Left <$> parse f1 Applicative.<|> Right <$> parse f2,
+   serialize = either (serialize f1) (serialize f2)}
+
+optional :: (Alternative m, Alternative n, Monoid (n s)) => Format m n s a -> Format m n s (Maybe a)
+-- | Same as the usual 'Applicative.optional' except a 'Format' is no 'Functor', let alone 'Alternative'.
+optional f = Format{
+   parse = Applicative.optional (parse f),
+   serialize = maybe mempty (serialize f)}
+
+-- | Like 'optional' except with arbitrary default serialization for the @Nothing@ value.
+--
+-- > optional = optionWithDefault (literal mempty)
+optionWithDefault :: (Alternative m, Alternative n, Monoid (n s)) =>
+                     Format m n s () -> Format m n s a -> Format m n s (Maybe a)
+optionWithDefault d f = Format{
+   parse = Just <$> parse f Applicative.<|> Nothing Applicative.<$ parse d,
+   serialize = maybe (serialize d ()) (serialize f)}
+
+many :: (Alternative m, Monoid (n s)) => Format m n s a -> Format m n s [a]
+-- | Same as the usual 'Applicative.many' except a 'Format' is no 'Functor', let alone 'Alternative'.
+many f = Format{
+   parse = Applicative.many (parse f),
+   serialize = foldMap (serialize f)}
+
+some :: (Alternative m, AlternativeFail n, Semigroup (n s)) => Format m n s a -> Format m n s [a]
+-- | Same as the usual 'Applicative.some' except a 'Format' is no 'Functor', let alone 'Alternative'.
+some f = Format{
+   parse = Applicative.some (parse f),
+   serialize = maybe (failure "[]") sconcat . nonEmpty . map (serialize f)}
+
+sepBy :: (Alternative m, Applicative n, Monoid s) => Format m n s a -> Format m n s () -> Format m n s [a]
+-- | Represents any number of values formatted using the first argument, separated by the second format argumewnt in
+-- serialized form. Similar to the usual 'Parser.sepBy' combinator.
+--
+-- >>> testParse (takeCharsWhile isLetter `sepBy` literal ",") "foo,bar,baz"
+-- Right [([],"foo,bar,baz"),(["foo"],",bar,baz"),(["foo","bar"],",baz"),(["foo","bar","baz"],"")]
+sepBy format separator = Format{
+   parse = Parser.sepBy (parse format) (parse separator),
+   serialize = \xs-> mconcat <$> sequenceA (List.intersperse (serialize separator ()) $ serialize format <$> xs)}
+
+pair :: (Applicative m, Semigroup (n s)) => Format m n s a -> Format m n s b -> Format m n s (a, b)
+-- | Combines two formats into a format for the pair of their values.
+--
+-- >>> testParse (pair char char) "abc"
+-- Right [(('a','b'),"c")]
+pair f g = Format{
+   parse = (,) <$> parse f <*> parse g,
+   serialize = \(a, b)-> serialize f a <> serialize g b}
+
+deppair :: (Monad m, Semigroup (n s)) => Format m n s a -> (a -> Format m n s b) -> Format m n s (a, b)
+-- | Combines two formats, where the second format depends on the first value, into a format for the pair of their
+-- values.  Similar to '>>=' except 'Format' is no 'Functor' let alone 'Monad'.
+--
+-- >>> testParse (deppair char (\c-> satisfy (==c) char)) "abc"
+-- Left "encountered 'b'"
+-- >>> testParse (deppair char (\c-> satisfy (==c) char)) "aac"
+-- Right [(('a','a'),"c")]
+deppair f g = Format{
+   parse = parse f >>= \a-> parse (g a) >>= \b-> return (a, b),
+   serialize = \(a, b)-> serialize f a <> serialize (g a) b}
+
+empty :: (Alternative m, Alternative n) => Format m n s a
+-- | Same as the usual 'Applicative.empty' except a 'Format' is no 'Functor', let alone 'Alternative'.
+empty = Format{
+   parse = Applicative.empty,
+   serialize = const Applicative.empty}
+
+infixr 0 <?>
+(<?>) :: (Parser.Parsing m, AlternativeFail n) => Format m n s a -> String -> Format m n s a
+-- | Name a format to improve error messages.
+--
+-- >>> testParse (takeCharsWhile1 isDigit <?> "a number") "abc"
+-- Left "expected a number, encountered 'a'"
+-- >>> testSerialize (takeCharsWhile1 isDigit <?> "a number") "abc"
+-- Left "expected a number, encountered \"abc\""
+f <?> name = Format{
+   parse = parse f Parser.<?> name,
+   serialize = expectedName name . serialize f}
+
+mfix :: MonadFix m => (a -> Format m n s a) -> Format m n s a
+-- | Same as the usual 'Control.Monad.Fix.mfix' except a 'Format' is no 'Functor', let alone 'Monad'.
+mfix f = Format{
+   parse = Monad.Fix.mfix (parse . f),
+   serialize = \a-> serialize (f a) a}
+
+-- | Attempts to 'parse' the given input with the format with a constrained type, returns either a failure message or
+-- a list of successes.
+testParse :: Monoid s => Format (Incremental.Parser Symmetric s) (Either Error) s a -> s -> Either String [(a, s)]
+testParse format input = fst <$> Incremental.inspect (Incremental.feedEof $ Incremental.feed input $ parse format)
+
+-- | A less polymorphic wrapper around 'serialize' useful for testing
+testSerialize :: Format (Incremental.Parser Symmetric s) (Either Error) s a -> a -> Either String s
+testSerialize format = either (Left . errorString) Right . serialize format
diff --git a/src/Construct/Bits.hs b/src/Construct/Bits.hs
new file mode 100644
--- /dev/null
+++ b/src/Construct/Bits.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE GADTs #-}
+
+-- | This module exports the primitives and combinators for constructing formats with sub- or cross-byte
+-- components. See @test/MBR.hs@ for an example of its use.
+--
+-- >>> testParse (bigEndianBytesOf $ pair (count 5 bit) (count 3 bit)) (ByteString.pack [9])
+-- Right [(([False,False,False,False,True],[False,False,True]),"")]
+
+module Construct.Bits
+  (Bits, bit,
+   -- * The combinators for converting between 'Bits' and 'ByteString' input streams
+   bigEndianBitsOf, bigEndianBytesOf, littleEndianBitsOf, littleEndianBytesOf) where
+
+import Data.Bits (setBit, testBit)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as ByteString
+import qualified Data.List as List
+import Data.Word (Word8)
+
+import Construct
+import Construct.Classes
+import Construct.Internal
+
+-- | The list of bits
+type Bits = [Bool]
+
+bit :: (Applicative n, InputParsing m, ParserInput m ~ Bits) => Format m n Bits Bool
+bigEndianBitsOf :: (InputParsing (m Bits), InputParsing (m ByteString), InputMappableParsing m, Functor n,
+                    ParserInput (m Bits) ~ Bits, ParserInput (m ByteString) ~ ByteString) =>
+                   Format (m ByteString) n ByteString a -> Format (m Bits) n Bits a
+bigEndianBytesOf :: (InputParsing (m Bits), InputParsing (m ByteString), InputMappableParsing m, Functor n,
+                     ParserInput (m Bits) ~ Bits, ParserInput (m ByteString) ~ ByteString) =>
+                    Format (m Bits) n Bits a -> Format (m ByteString) n ByteString a
+littleEndianBitsOf :: (InputParsing (m Bits), InputParsing (m ByteString), InputMappableParsing m, Functor n,
+                       ParserInput (m Bits) ~ Bits, ParserInput (m ByteString) ~ ByteString) =>
+                      Format (m ByteString) n ByteString a -> Format (m Bits) n Bits a
+littleEndianBytesOf :: (InputParsing (m Bits), InputParsing (m ByteString), InputMappableParsing m, Functor n,
+                        ParserInput (m Bits) ~ Bits, ParserInput (m ByteString) ~ ByteString) =>
+                       Format (m Bits) n Bits a -> Format (m ByteString) n ByteString a
+
+-- | The primitive format of a single bit
+--
+-- >>> testParse bit [True, False, False, True]
+-- Right [(True,[False,False,True])]
+bit = Format{
+   parse = head <$> anyToken,
+   serialize = pure . (:[])}
+
+bigEndianBitsOf = mapMaybeSerialized (Just . enumerateFromMostSignificant) collectFromMostSignificant
+bigEndianBytesOf = mapMaybeSerialized collectFromMostSignificant (Just . enumerateFromMostSignificant)
+littleEndianBitsOf = mapMaybeSerialized (Just . enumerateFromLeastSignificant) collectFromLeastSignificant
+littleEndianBytesOf = mapMaybeSerialized collectFromLeastSignificant (Just . enumerateFromLeastSignificant)
+
+collectFromMostSignificant :: Bits -> Maybe ByteString
+collectFromLeastSignificant :: Bits -> Maybe ByteString
+enumerateFromMostSignificant :: ByteString -> Bits
+enumerateFromLeastSignificant :: ByteString -> Bits
+
+collectFromMostSignificant bits = (ByteString.pack . map toByte) <$> splitEach8 bits
+   where toByte octet = List.foldl' setBit (0 :: Word8) (map snd $ filter fst $ zip octet [7,6..0])
+collectFromLeastSignificant bits = (ByteString.pack . map toByte) <$> splitEach8 bits
+   where toByte octet = List.foldl' setBit (0 :: Word8) (map snd $ filter fst $ zip octet [0..7])
+enumerateFromMostSignificant = ByteString.foldr ((++) . enumerateByte) []
+   where enumerateByte b = [testBit b i | i <- [7,6..0]]
+enumerateFromLeastSignificant = ByteString.foldr ((++) . enumerateByte) []
+   where enumerateByte b = [testBit b i | i <- [0..7]]
+
+splitEach8 :: [a] -> Maybe [[a]]
+splitEach8 [] = Just []
+splitEach8 list
+   | length first8 == 8 = (first8 :) <$> splitEach8 rest
+   | otherwise = Nothing
+   where (first8, rest) = splitAt 8 list
diff --git a/src/Construct/Classes.hs b/src/Construct/Classes.hs
new file mode 100644
--- /dev/null
+++ b/src/Construct/Classes.hs
@@ -0,0 +1,283 @@
+{-# LANGUAGE DefaultSignatures, FlexibleContexts, FlexibleInstances, TypeFamilies,
+             TypeSynonymInstances, UndecidableInstances #-}
+
+-- | The only good reason to import this module is if you intend to add another instance of the classes it exports.
+
+module Construct.Classes where
+
+import qualified Rank2
+import qualified Text.ParserCombinators.Incremental as Incremental
+
+import Control.Applicative (Alternative ((<|>), empty))
+import Control.Monad (void)
+import Data.String (IsString (fromString))
+import Text.ParserCombinators.ReadP (ReadP)
+import qualified Text.ParserCombinators.ReadP as ReadP
+
+import Text.Parser.Char (CharParsing)
+import Text.Parser.Combinators (count, eof, notFollowedBy, try, unexpected)
+import Text.Parser.LookAhead (LookAheadParsing, lookAhead)
+import qualified Text.Parser.Char as Char
+
+import qualified Data.Monoid.Factorial as Factorial
+import qualified Data.Monoid.Null as Null
+import qualified Data.Monoid.Textual as Textual
+import qualified Data.Semigroup.Cancellative as Cancellative
+import Data.Monoid.Factorial (FactorialMonoid)
+import Data.Monoid.Textual (TextualMonoid)
+import Data.Semigroup.Cancellative (LeftReductive)
+
+import Data.ByteString (ByteString)
+import Data.Text (Text)
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as ByteString.Char8
+import qualified Data.Text as Text
+
+import qualified Data.Attoparsec.ByteString as Attoparsec
+import qualified Data.Attoparsec.ByteString.Char8 as Attoparsec.Char8
+import qualified Data.Attoparsec.Text as Attoparsec.Text
+
+import Prelude hiding (take, takeWhile)
+
+-- | Subclass of 'Alternative' that carries an error message in case of failure
+class Alternative m => AlternativeFail m where
+   -- | Equivalent to 'empty' except it takes an error message it may carry or drop on the floor. The grammatical form
+   --  of the argument be a noun representing the unexpected value.
+   failure :: String -> m a
+   -- | Sets or modifies the expected value.
+   expectedName :: String -> m a -> m a
+
+   failure = const empty
+   expectedName = const id
+
+-- | Methods for parsing factorial monoid inputs
+class LookAheadParsing m => InputParsing m where
+   type ParserInput m
+   -- | Always sucessful parser that returns the remaining input without consuming it.
+   getInput :: m (ParserInput m)
+
+   -- | A parser that accepts any single atomic prefix of the input stream.
+   -- > anyToken == satisfy (const True)
+   -- > anyToken == take 1
+   anyToken :: m (ParserInput m)
+   -- | A parser that accepts exactly the given number of input atoms.
+   take :: Int -> m (ParserInput m)
+   -- | A parser that accepts an input atom only if it satisfies the given predicate.
+   satisfy :: (ParserInput m -> Bool) -> m (ParserInput m)
+   -- | A parser that succeeds exactly when satisfy doesn't, equivalent to
+   -- 'Text.Parser.Combinators.notFollowedBy' @. satisfy@
+   notSatisfy :: (ParserInput m -> Bool) -> m ()
+
+   -- | A stateful scanner. The predicate modifies a state argument, and each transformed state is passed to successive
+   -- invocations of the predicate on each token of the input until one returns 'Nothing' or the input ends.
+   --
+   -- This parser does not fail.  It will return an empty string if the predicate returns 'Nothing' on the first
+   -- character.
+   --
+   -- /Note/: Because this parser does not fail, do not use it with combinators such as 'many', because such parsers
+   -- loop until a failure occurs.  Careless use will thus result in an infinite loop.
+   scan :: state -> (state -> ParserInput m -> Maybe state) -> m (ParserInput m)
+   -- | A parser that consumes and returns the given prefix of the input.
+   string :: ParserInput m -> m (ParserInput m)
+
+   -- | A parser accepting the longest sequence of input atoms that match the given predicate; an optimized version of
+   -- 'concatMany . satisfy'.
+   --
+   -- /Note/: Because this parser does not fail, do not use it with combinators such as 'many', because such parsers
+   -- loop until a failure occurs.  Careless use will thus result in an infinite loop.
+   takeWhile :: (ParserInput m -> Bool) -> m (ParserInput m)
+   -- | A parser accepting the longest non-empty sequence of input atoms that match the given predicate; an optimized
+   -- version of 'concatSome . satisfy'.
+   takeWhile1 :: (ParserInput m -> Bool) -> m (ParserInput m)
+   -- | Zero or more argument occurrences like 'many', with concatenated monoidal results.
+   concatMany :: Monoid a => m a -> m a
+
+   anyToken = take 1
+   notSatisfy predicate = try (void $ satisfy $ not . predicate) <|> eof
+   default concatMany :: (Monoid a, Alternative m) => m a -> m a
+   concatMany p = go
+      where go = mappend <$> try p <*> go <|> pure mempty
+
+   default string :: (Monad m, LeftReductive (ParserInput m), FactorialMonoid (ParserInput m), Show (ParserInput m))
+                  => ParserInput m -> m (ParserInput m)
+   string s = do i <- getInput
+                 if s `Cancellative.isPrefixOf` i
+                    then take (Factorial.length s)
+                    else unexpected ("string " <> show s)
+   default scan :: (Monad m, FactorialMonoid (ParserInput m)) =>
+                   state -> (state -> ParserInput m -> Maybe state) -> m (ParserInput m)
+   scan state f = do i <- getInput
+                     let (prefix, _suffix, _state) = Factorial.spanMaybe' state f i
+                     take (Factorial.length prefix)
+   default takeWhile :: (Monad m, FactorialMonoid (ParserInput m)) => (ParserInput m -> Bool) -> m (ParserInput m)
+   takeWhile predicate = do i <- getInput
+                            take (Factorial.length $ Factorial.takeWhile predicate i)
+   default takeWhile1 :: (Monad m, FactorialMonoid (ParserInput m)) => (ParserInput m -> Bool) -> m (ParserInput m)
+   takeWhile1 predicate = do x <- takeWhile predicate
+                             if Null.null x then unexpected "takeWhile1" else pure x
+   {-# INLINE concatMany #-}
+
+-- | Methods for parsing textual monoid inputs
+class (CharParsing m, InputParsing m) => InputCharParsing m where
+   -- | Specialization of 'satisfy' on textual inputs, accepting an input character only if it satisfies the given
+   -- predicate, and returning the input atom that represents the character. Equivalent to @fmap singleton
+   -- . Char.satisfy@
+   satisfyCharInput :: (Char -> Bool) -> m (ParserInput m)
+   -- | A parser that succeeds exactly when satisfy doesn't, equivalent to @notFollowedBy . Char.satisfy@
+   notSatisfyChar :: (Char -> Bool) -> m ()
+
+   -- | Stateful scanner like `scan`, but specialized for 'TextualMonoid' inputs.
+   scanChars :: state -> (state -> Char -> Maybe state) -> m (ParserInput m)
+
+   -- | Specialization of 'takeWhile' on 'TextualMonoid' inputs, accepting the longest sequence of input characters that
+   -- match the given predicate; an optimized version of @fmap fromString  . many . Char.satisfy@.
+   --
+   -- /Note/: Because this parser does not fail, do not use it with combinators such as 'many', because such parsers
+   -- loop until a failure occurs.  Careless use will thus result in an infinite loop.
+   takeCharsWhile :: (Char -> Bool) -> m (ParserInput m)
+   -- | Specialization of 'takeWhile1' on 'TextualMonoid' inputs, accepting the longest sequence of input characters
+   -- that match the given predicate; an optimized version of @fmap fromString  . some . Char.satisfy@.
+   takeCharsWhile1 :: (Char -> Bool) -> m (ParserInput m)
+
+   default satisfyCharInput :: IsString (ParserInput m) => (Char -> Bool) -> m (ParserInput m)
+   satisfyCharInput = fmap (fromString . (:[])) . Char.satisfy
+   notSatisfyChar = notFollowedBy . Char.satisfy
+   default scanChars :: (Monad m, TextualMonoid (ParserInput m)) =>
+                        state -> (state -> Char -> Maybe state) -> m (ParserInput m)
+   scanChars state f = do i <- getInput
+                          let (prefix, _suffix, _state) = Textual.spanMaybe' state (const $ const Nothing) f i
+                          take (Factorial.length prefix)
+
+   default takeCharsWhile :: (Monad m, TextualMonoid (ParserInput m)) => (Char -> Bool) -> m (ParserInput m)
+   takeCharsWhile predicate = do i <- getInput
+                                 take (Factorial.length $ Textual.takeWhile_ False predicate i)
+   default takeCharsWhile1 :: (Monad m, TextualMonoid (ParserInput m)) => (Char -> Bool) -> m (ParserInput m)
+   takeCharsWhile1 predicate = do x <- takeCharsWhile predicate
+                                  if Null.null x then unexpected "takeCharsWhile1" else pure x
+
+-- | A subclass of 'InputParsing' for parsers that can switch the input stream type
+class InputMappableParsing m where
+   -- | Converts a parser accepting one input stream type to another. The functions @forth@ and @back@ must be inverses of
+   -- each other and they must distribute through '<>':
+   --
+   -- > f (s1 <> s2) == f s1 <> f s2
+   mapParserInput :: (InputParsing (m s), s ~ ParserInput (m s), Monoid s, Monoid s') =>
+                     (s -> s') -> (s' -> s) -> m s a -> m s' a
+   -- | Converts a parser accepting one input stream type to another just like 'mapParserInput', except the argument
+   -- functions can return @Nothing@ to indicate they need more input.
+   
+   mapMaybeParserInput :: (InputParsing (m s), s ~ ParserInput (m s), Monoid s, Monoid s') =>
+                          (s -> Maybe s') -> (s' -> Maybe s) -> m s a -> m s' a
+
+-- | A subclass of 'MonadFix' for monads that can fix a function that handles higher-kinded data
+class Monad m => FixTraversable m where
+   -- | This specialized form of 'Rank2.traverse' can be used inside 'mfix'.
+   fixSequence :: (Rank2.Traversable g, Applicative n) => g m -> m (g n)
+   fixSequence = Rank2.traverse (pure <$>)
+
+------------------------------------------------------------
+--                       Instances
+------------------------------------------------------------
+
+data Error = Error [String] (Maybe String) deriving (Eq, Show)
+
+instance Semigroup Error where
+   Error expected1 encountered1 <> Error expected2 encountered2 =
+      Error (expected1 <> expected2) (maybe encountered2 Just encountered1)
+
+instance AlternativeFail Maybe
+
+instance AlternativeFail []
+
+instance {-# OVERLAPS #-} Alternative (Either Error) where
+   empty = Left (Error [] Nothing)
+   Right a <|> _ = Right a
+   _ <|> Right a = Right a
+   Left e1 <|> Left e2 = Left (e1 <> e2)
+
+instance AlternativeFail (Either Error) where
+   failure encountered = Left (Error [] (Just encountered))
+   expectedName expected (Left (Error _ encountered)) = Left (Error [expected] encountered)
+   expectedName _ success = success
+
+errorString :: Error -> String
+errorString (Error ex Nothing) = maybe "" ("expected " <>) (concatExpected ex)
+errorString (Error [] (Just en)) = "encountered " <> en
+errorString (Error ex (Just en)) = maybe "" ("expected " <>) (concatExpected ex) <> ", encountered " <> en
+
+concatExpected :: [String] -> Maybe String
+concatExpected [] = Nothing
+concatExpected [e] = Just e
+concatExpected [e1, e2] = Just (e1 <> " or " <> e2)
+concatExpected (e:es) = Just (oxfordComma e es)
+
+oxfordComma :: String -> [String] -> String
+oxfordComma e [] = "or " <> e
+oxfordComma e (e':es) = e <> ", " <> oxfordComma e' es
+
+instance InputParsing ReadP where
+   type ParserInput ReadP = String
+   getInput = ReadP.look
+   take n = count n ReadP.get
+   anyToken = pure <$> ReadP.get
+   satisfy predicate = pure <$> ReadP.satisfy (predicate . pure)
+   string = ReadP.string
+
+instance InputCharParsing ReadP where
+   satisfyCharInput predicate = pure <$> ReadP.satisfy predicate
+
+instance InputParsing Attoparsec.Parser where
+   type ParserInput Attoparsec.Parser = ByteString
+   getInput = lookAhead Attoparsec.takeByteString
+   anyToken = Attoparsec.take 1
+   take = Attoparsec.take
+   satisfy predicate = Attoparsec.satisfyWith ByteString.singleton predicate
+   string = Attoparsec.string
+
+instance InputCharParsing Attoparsec.Parser where
+   satisfyCharInput predicate = ByteString.Char8.singleton <$> Attoparsec.Char8.satisfy predicate
+   scanChars = Attoparsec.Char8.scan
+   takeCharsWhile = Attoparsec.Char8.takeWhile
+   takeCharsWhile1 = Attoparsec.Char8.takeWhile1
+
+instance InputParsing Attoparsec.Text.Parser where
+   type ParserInput Attoparsec.Text.Parser = Text
+   getInput = lookAhead Attoparsec.Text.takeText
+   anyToken = Attoparsec.Text.take 1
+   take = Attoparsec.Text.take
+   satisfy predicate = Attoparsec.Text.satisfyWith Text.singleton predicate
+   string = Attoparsec.Text.string
+
+instance InputCharParsing Attoparsec.Text.Parser where
+   satisfyCharInput predicate = Text.singleton <$> Attoparsec.Text.satisfy predicate
+   scanChars = Attoparsec.Text.scan
+   takeCharsWhile = Attoparsec.Text.takeWhile
+   takeCharsWhile1 = Attoparsec.Text.takeWhile1
+
+instance (FactorialMonoid s, Cancellative.LeftReductive s, LookAheadParsing (Incremental.Parser t s)) =>
+         InputParsing (Incremental.Parser t s) where
+   type ParserInput (Incremental.Parser t s) = s
+   getInput = lookAhead Incremental.acceptAll
+   anyToken = Incremental.anyToken
+   take n = Incremental.count n Incremental.anyToken
+   satisfy = Incremental.satisfy
+   string = Incremental.string
+   takeWhile = Incremental.takeWhile
+   takeWhile1 = Incremental.takeWhile1
+   concatMany = Incremental.concatMany
+
+instance (TextualMonoid s, Cancellative.LeftReductive s, LookAheadParsing (Incremental.Parser t s)) =>
+         InputCharParsing (Incremental.Parser t s) where
+   satisfyCharInput = Incremental.satisfyChar
+   takeCharsWhile = Incremental.takeCharsWhile
+   takeCharsWhile1 = Incremental.takeCharsWhile1
+
+instance FixTraversable Attoparsec.Parser
+
+instance Monoid s => FixTraversable (Incremental.Parser t s) where
+   fixSequence = Incremental.record
+
+instance InputMappableParsing (Incremental.Parser t) where
+   mapParserInput = Incremental.mapInput
+   mapMaybeParserInput = Incremental.mapMaybeInput
+
diff --git a/src/Construct/Internal.hs b/src/Construct/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Construct/Internal.hs
@@ -0,0 +1,16 @@
+module Construct.Internal where
+
+-- | The central type. The four type parameters are:
+--
+--   * @m@ is the type of the parser for the format
+--   * @n@ is the container type for the serialized form of the value, typically 'Identity' unless something
+--     'Alternative' is called for.
+--   * @s@ is the type of the serialized value, typically 'ByteString'
+--   * @a@ is the type of the value in the program
+--
+-- The @parse@ and @serialize@ fields can be used to perform the two sides of the conversion between the in-memory and
+-- serialized form of the value.
+data Format m n s a = Format {
+   parse :: m a,
+   serialize :: a -> n s
+   }
diff --git a/test/Doctest.hs b/test/Doctest.hs
new file mode 100644
--- /dev/null
+++ b/test/Doctest.hs
@@ -0,0 +1,6 @@
+import Build_doctests (flags, pkgs, module_sources)
+import Test.DocTest (doctest)
+
+main = do
+    doctest (flags ++ pkgs ++ module_sources)
+    doctest (flags ++ pkgs ++ ["-pgmL", "markdown-unlit", "test/README.lhs"])
diff --git a/test/MBR.hs b/test/MBR.hs
new file mode 100644
--- /dev/null
+++ b/test/MBR.hs
@@ -0,0 +1,91 @@
+{-# LANGUAGE FlexibleInstances, StandaloneDeriving, TemplateHaskell #-}
+module MBR where
+
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as ByteString
+import Data.Functor.Identity (Identity)
+import Data.Word
+import qualified Rank2.TH
+import Text.ParserCombinators.Incremental.LeftBiasedLocal (Parser)
+
+import Construct
+import Construct.Bits
+
+import Prelude hiding ((<$), head)
+
+data MasterBootRecord f = MasterBootRecord{
+   bootLoaderCode :: f [Word8],
+   partitions     :: f [Partition Identity]
+   }
+
+data Partition f = Partition{
+   state          :: f PartitionState,
+   beginning      :: f (Position Identity),
+   partitionType  :: f PartitionType,
+   ending         :: f (Position Identity),
+   sectorOffset   :: f Word32,
+   size           :: f Word32}
+
+data PartitionState = Inactive | Active
+                    deriving (Eq, Read, Show)
+
+data PartitionType = NoType
+                   | FAT12
+                   | XENIX_ROOT
+                   | XENIX_USR
+                   | FAT16_old
+                   | Extended_DOS
+                   | FAT16
+                   | NTFS
+                   | FAT32
+                   | FAT32_LBA
+                   | ExtendedWithLBA
+                   | LINUX_SWAP
+                   | LINUX_NATIVE
+                   deriving (Eq, Read, Show)
+
+data Position f = Position{
+   head           :: f Word8,
+   sector         :: f Bits,
+   cylinder       :: f Bits}
+
+deriving instance Show (MasterBootRecord Identity)
+deriving instance Show (Partition Identity)
+deriving instance Show (Position Identity)
+
+format :: Format (Parser ByteString) Maybe ByteString (MasterBootRecord Identity)
+format = record MasterBootRecord{
+   bootLoaderCode = count 446 byte,
+   partitions = count 4 partition}
+
+partition :: Format (Parser ByteString) Maybe ByteString (Partition Identity)
+partition = record Partition{
+        state = Inactive <$ literal (ByteString.singleton 0) <|>
+                Active <$ literal (ByteString.singleton 0x80),
+        beginning = position,
+        partitionType = NoType <$ literal (ByteString.singleton 0) <|>
+                        FAT12 <$ literal (ByteString.singleton 1) <|>
+                        XENIX_ROOT <$ literal (ByteString.singleton 2) <|>
+                        XENIX_USR <$ literal (ByteString.singleton 3) <|>
+                        FAT16_old <$ literal (ByteString.singleton 4) <|>
+                        Extended_DOS <$ literal (ByteString.singleton 5) <|>
+                        FAT16 <$ literal (ByteString.singleton 6) <|>
+                        NTFS <$ literal (ByteString.singleton 7) <|>
+                        FAT32 <$ literal (ByteString.singleton 0xB) <|>
+                        FAT32_LBA <$ literal (ByteString.singleton 0xC) <|>
+                        ExtendedWithLBA <$ literal (ByteString.singleton 0xF) <|>
+                        LINUX_SWAP <$ literal (ByteString.singleton 0x82) <|>
+                        LINUX_NATIVE <$ literal (ByteString.singleton 0x83),
+        ending = position,
+        sectorOffset = cereal,
+        size = cereal}
+
+position :: Format (Parser ByteString) Maybe ByteString (Position Identity)
+position = littleEndianBytesOf $ record Position{
+   head = littleEndianBitsOf byte,
+   sector = count 6 bit,
+   cylinder = count 10 bit}
+
+$(Rank2.TH.deriveAll ''MasterBootRecord)
+$(Rank2.TH.deriveAll ''Partition)
+$(Rank2.TH.deriveAll ''Position)
diff --git a/test/README.lhs b/test/README.lhs
new file mode 100644
--- /dev/null
+++ b/test/README.lhs
@@ -0,0 +1,113 @@
+Construct.hs
+============
+
+This is a Haskell implementation of Python's [Construct](https://construct.readthedocs.io/en/latest/intro.html)
+library. It provides a succinct and easy way to specify data formats. Before you get to the succinct part, though,
+you'll probably need a bunch of extensions and imports:
+
+~~~ {.haskell}
+{-# LANGUAGE FlexibleInstances, StandaloneDeriving, TemplateHaskell #-}
+
+module README where
+
+import Data.Functor.Identity (Identity(Identity))
+import Data.Word (Word8)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as ASCII
+import qualified Rank2.TH
+import Text.ParserCombinators.Incremental.LeftBiasedLocal (Parser, completeResults, feed, feedEof)
+import Construct
+
+import Prelude hiding ((*>), (<*))
+~~~
+
+Example
+-------
+
+With that out of the way, let's take the simple example format from the original. Here's what its specification looks
+like in Haskell:
+
+~~~ {.haskell}
+data BitMap f = BitMap{
+   width :: f Word8,
+   height :: f Word8,
+   pixels :: f [[Word8]]
+   }
+deriving instance Show (BitMap Identity)
+$(Rank2.TH.deriveAll ''BitMap)
+
+format :: Format (Parser ByteString) Maybe ByteString (BitMap Identity)
+format = literal (ASCII.pack "BMP") *> mfix (\this-> record
+  BitMap{
+        width= byte,
+        height= byte,
+        pixels= count (fromIntegral $ height this) (count (fromIntegral $ width this) byte)
+        })
+~~~
+
+There are two parts to the specification.
+
+The `data BitMap` declaration specifies the in-memory layout of a simple bitmap. Note that it's declared as a record
+with every field wrapped in a type constructor parameter. This declaration style is sometimes called [Higher-Kinded
+Data](https://reasonablypolymorphic.com/blog/higher-kinded-data/). To regain a regular record, just instantiate the
+parameter to `Identity` &mdash; a `BitMap Identity` contains exactly one value of each field.
+
+The other part of the specification is the `format` definition that specifies the bi-directional mapping between the
+in-memory and the serialized form of the bitmap. A bijection, to be precise. The two definitions are enough to automatically
+serialize the in-memory record form into the binary form:
+
+~~~ {.haskell}
+-- | >>> serialize format BitMap{width= Identity 3, height= Identity 2, pixels= Identity [[7,8,9], [11,12,13]]}
+-- Just "BMP\ETX\STX\a\b\t\v\f\r"
+~~~
+
+and to parse the serialized binary form back into the record structure:
+
+~~~ {.haskell}
+-- | >>> completeResults $ feedEof $ feed (ASCII.pack "BMP" <> ByteString.pack [3, 2, 7, 8, 9, 11, 12, 13]) $ parse format
+-- [(BitMap {width = Identity 3, height = Identity 2, pixels = Identity [[7,8,9],[11,12,13]]},"")]
+~~~
+
+Examples of more complex and realistic formats can be found in the
+[`test`](https://github.com/blamario/construct/tree/master/test) directory.
+
+Acknowledgements
+----------------
+
+I owe the inspiration for this library to Yair Chuchem and his
+[post](https://yairchu.github.io/posts/codecs-as-prisms.html) that introduced me to Construct. I must also express
+gratitude to the authors of the original library of course. And finally, to the authors of the paper [Invertible
+Syntax Descriptions:Unifying Parsing and Pretty
+Printing](https://www.informatik.uni-marburg.de/~rendel/unparse/rendel10invertible.pdf) which I remembered reading
+just in time to avoid following some bad ideas.
+
+Implementation notes
+--------------------
+
+I had to overcome two problems in the course of the implementation. The first difficulty, already touched on in the
+aforementioned blog post, is how to convert a record of formats into a format of the record. As the author of
+[rank2classes](https://hackage.haskell.org/package/rank2classes), I went for an obvious solution: parameterize the
+record as seen in the example, make it an instance of the
+[`Rank2.Traversable`](https://hackage.haskell.org/package/rank2classes-1.3.1.2/docs/Rank2.html#t:Traversable) class,
+and apply [`Rank2.traverse`](https://hackage.haskell.org/package/rank2classes-1.3.1.2/docs/Rank2.html#v:traverse) to
+it.
+
+That little trick alone would have been enough for a nearly complete package, except the Python library also enables a
+record field to refer to any of the preceding fields. This is not a problem when serializing, but when parsing it
+means that a parser must be able to refer to an already-parsed part of the value that's still being parsed.
+
+The standard solution to the problem of accessing the results (parsed values) of a computation (parsing) *while within
+the computation* is known as the
+[`MonadFix`](https://hackage.haskell.org/package/base-4.12.0.0/docs/Control-Monad-Fix.html#t:MonadFix) class, though
+perhaps not as widely as it deserves. You won't find any parsers in the list of its instances, though, and for a good
+reason - it's quite impossible for a parser to obtain a value that it hasn't parsed yet. All *we* need, luckily, is
+access an already parsed part of a partially parsed structure. A limited `MonadFix` instance can do that, provided
+that we also use a specialized form of `Rank2.traverse` capable of preserving the record structure while it's being
+parsed.
+
+As it happens, I have an old parser library named
+[incremental-parser](https://hackage.haskell.org/package/incremental-parser) on Hackage, and the name seems quite
+appropriate for what I'm doing here. I added the necessary functionality there, but another parser combinator library
+should be capable of the same feat. It just needs to implement the `mfix` and `fixSequence` combinators and it can be
+used with the present library.
diff --git a/test/TAR.hs b/test/TAR.hs
new file mode 100644
--- /dev/null
+++ b/test/TAR.hs
@@ -0,0 +1,157 @@
+{-# LANGUAGE FlexibleInstances, OverloadedStrings, StandaloneDeriving, TemplateHaskell #-}
+
+module TAR where
+
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as ASCII
+import Data.Char (isOctDigit)
+import Data.Foldable (fold)
+import Data.Functor.Identity (Identity(Identity, runIdentity))
+import Data.Monoid.Textual (TextualMonoid)
+import qualified Data.Monoid.Textual
+import Data.Text (Text)
+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
+import Data.Word
+import Numeric (readOct, showOct)
+import qualified Rank2.TH
+import Data.Attoparsec.ByteString (Parser)
+
+import Construct
+
+import Prelude hiding ((<$), (<*), (*>), take, takeWhile)
+
+data Archive f = Archive{files :: f [File Identity]}
+
+data File f = File{
+   header  :: f (FileHeader () Identity),
+   content :: f ByteString}
+
+data FileType = NormalFile
+              | HardLink
+              | SymLink
+              | CharSpecial
+              | BlockSpecial
+              | Directory
+              | FIFO
+              | ContiguousFile
+              | ExtendedGlobalHeader
+              | ExtendedFileHeader
+              | VendorExtension{vendorCode :: Word8}
+              | Reserved{reservedCode :: Word8}
+              deriving (Eq, Read, Show)
+
+data FileHeader cksum f = FileHeader{
+   fileName :: f Text,
+   fileMode :: f Word32,
+   ownerID  :: f Word32,
+   groupID  :: f Word32,
+   fileSize :: f Word64,
+   modified :: f Word64,
+   checksum :: f cksum,
+   fileType :: f FileType,
+   linked   :: f Text,
+   ustar    :: f (Maybe (UStarHeader Identity))}
+
+data UStarHeader f = UStarHeader{
+   ownerName   :: f Text,
+   groupName   :: f Text,
+   deviceMajor :: f (Maybe Word32),
+   deviceMinor :: f (Maybe Word32),
+   namePrefix  :: f Text}
+
+deriving instance Show (Archive Identity)
+deriving instance Show (File Identity)
+deriving instance Show (FileHeader () Identity)
+deriving instance Show (FileHeader Word32 Identity)
+deriving instance Show (UStarHeader Identity)
+
+archive :: Format Parser Maybe ByteString (Archive Identity)
+archive = record Archive{files= many file}
+
+file :: Format Parser Maybe ByteString (File Identity)
+file = mapValue (uncurry file) (\f-> (runIdentity $ header f, runIdentity $ content f)) $
+       deppair fileHeader (\FileHeader{fileSize= size}->
+                              ByteString.replicate (((fromIntegral size + 511) `div` 512) * 512) 0 `padded`
+                              take (fromIntegral size))
+   where file h c = File (Identity h) (Identity c)
+
+fileHeader :: Format Parser Maybe ByteString (FileHeader () Identity)
+fileHeader = mapMaybeValue validate calculate (record $ fileHeaderRecord (zeroDelimitedOctal 7 <* value char ' '))
+   where validate header
+            | header' <- header{checksum= pure ()}, fromIntegral (checksum header) == sumOf header' = Just header'
+            | otherwise = Nothing
+         calculate header = Just header{checksum= pure $ sumOf header}
+
+fileHeaderRecord :: Format Parser Maybe ByteString cksum
+                 -> FileHeader cksum (Format Parser Maybe ByteString)
+fileHeaderRecord checksumFormat = FileHeader{
+   fileName = zeroDelimitedUtf8 100,
+   fileMode = zeroDelimitedOctal 8,
+   ownerID  = zeroDelimitedOctal 8,
+   groupID  = zeroDelimitedOctal 8,
+   fileSize = zeroDelimitedOctal64,
+   modified = zeroDelimitedOctal64,
+   checksum = checksumFormat,
+   fileType = NormalFile <$ value char '0'
+              <|> HardLink <$ value char '1'
+              <|> SymLink <$ value char '2'
+              <|> CharSpecial <$ value char '3'
+              <|> BlockSpecial <$ value char '4'
+              <|> Directory <$ value char '5'
+              <|> FIFO <$ value char '6'
+              <|> ContiguousFile <$ value char '7'
+              <|> ExtendedGlobalHeader <$ value char 'g'
+              <|> ExtendedFileHeader <$ value char 'x'
+              <|> mapValue VendorExtension vendorCode byte
+              <|> mapValue Reserved reservedCode byte,
+   linked = zeroDelimitedUtf8 100,
+   ustar = optional ((value (zeroDelimitedUtf8 8) "ustar  " <|> value (zeroDelimitedUtf8 8) "ustar00") *> ustarHeader)}
+
+ustarHeader :: Format Parser Maybe ByteString (UStarHeader Identity)
+ustarHeader = record UStarHeader{
+   ownerName = zeroDelimitedUtf8 32,
+   groupName = zeroDelimitedUtf8 32,
+   deviceMajor = optionWithDefault (literal $ ByteString.replicate 8 0) (zeroDelimitedOctal 8),
+   deviceMinor = optionWithDefault (literal $ ByteString.replicate 8 0) (zeroDelimitedOctal 8),
+   namePrefix = zeroDelimitedUtf8 167 {- 155+12 to pad to 512 -}}
+
+zeroDelimitedUtf8 :: Int -> Format Parser Maybe ByteString Text
+zeroDelimitedUtf8 width = mapValue decodeUtf8 encodeUtf8 $
+                          ByteString.replicate width 0 `padded1` (takeWhile (/= ByteString.singleton 0))
+
+zeroDelimitedOctal :: Int -> Format Parser Maybe ByteString Word32
+zeroDelimitedOctal width = mapValue (fst . head . readOct . ASCII.unpack)
+                                    (padLeft '0' (width - 1) . ASCII.pack . flip showOct "") $
+                           ByteString.replicate width 0 `padded1` (takeCharsWhile1 isOctDigit)
+
+zeroDelimitedOctal64 :: Format Parser Maybe ByteString Word64
+zeroDelimitedOctal64 = mapValue (fst . head . readOct . ASCII.unpack)
+                                (padLeft '0' 11 . ASCII.pack . flip showOct "") $
+                       ByteString.replicate 12 0 `padded1` (takeCharsWhile1 isOctDigit)
+
+padLeft :: Char -> Int -> ByteString -> ByteString
+padLeft filler len bs
+   | ByteString.length bs < len = ASCII.replicate (len - ByteString.length bs) filler <> bs
+   | otherwise = bs
+
+sumOf :: FileHeader () Identity -> Word32
+sumOf header = ByteString.foldl' add 0 (fold $ serialize blankFormat header)
+   where add s b = s + fromIntegral b
+         blankFormat = record (fileHeaderRecord $ literal $ ASCII.replicate 8 ' ')
+
+instance TextualMonoid ByteString where
+   fromText = encodeUtf8
+   singleton = ASCII.singleton
+   splitCharacterPrefix = ASCII.uncons
+   dropWhile _ = ASCII.dropWhile
+   dropWhile_ _ = ASCII.dropWhile
+   takeWhile _ = ASCII.takeWhile
+   takeWhile_ _ = ASCII.takeWhile
+   span _ = ASCII.span
+   span_ _ = ASCII.span
+
+$(Rank2.TH.deriveAll ''Archive)
+$(Rank2.TH.deriveAll ''File)
+$(Rank2.TH.deriveAll ''FileHeader)
+$(Rank2.TH.deriveAll ''UStarHeader)
diff --git a/test/Test.hs b/test/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Test.hs
@@ -0,0 +1,70 @@
+{-# LANGUAGE ExistentialQuantification, OverloadedStrings #-}
+
+module Main where
+
+import qualified Data.ByteString as ByteString
+import Data.ByteString (ByteString)
+import Data.Functor.Identity (Identity)
+import Data.List (isSuffixOf)
+import Data.Maybe (mapMaybe)
+import Data.Text (Text)
+import qualified Data.Text as Text
+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
+import Numeric (showHex)
+import System.Directory (doesDirectoryExist, listDirectory)
+import System.FilePath.Posix (combine)
+import qualified Text.ParserCombinators.Incremental as Incremental
+import Text.ParserCombinators.Incremental.LeftBiasedLocal (Parser)
+import qualified Data.Attoparsec.ByteString as Atto
+import Test.Tasty (TestTree, defaultMain, testGroup)
+import Test.Tasty.HUnit (assertFailure, assertEqual, testCase)
+
+import Construct
+import qualified MBR
+import qualified TAR
+import qualified URI
+import qualified WMF
+
+data TestFormat = forall f. TestFormat (Format (Parser ByteString) Maybe ByteString (f Identity))
+                | forall f. LineFormat (Format (Parser Text) Maybe Text (f Identity))
+                | forall f. AttoFormat (Format Atto.Parser Maybe ByteString (f Identity))
+
+main = exampleTree "" "test/examples" >>= defaultMain . testGroup "examples"
+
+exampleTree :: FilePath -> FilePath -> IO [TestTree]
+exampleTree ancestry path =
+   do let fullPath = combine ancestry path
+      isDir <- doesDirectoryExist fullPath
+      if isDir
+         then (:[]) . testGroup path . concat <$> (listDirectory fullPath >>= mapM (exampleTree fullPath))
+         else do blob <- ByteString.readFile fullPath
+                 let format
+                        | ".mbr"  `isSuffixOf` path = TestFormat MBR.format
+                        | ".tar"  `isSuffixOf` path = AttoFormat TAR.archive
+                        | ".uris" `isSuffixOf` path = LineFormat URI.uriReference
+                        | ".wmf"  `isSuffixOf` path = TestFormat WMF.fileFormat
+                     textLines = Text.lines (decodeUtf8 blob)
+                     roundTrip f t
+                        | Text.null t = Just t
+                        | [(structure, remainder)] <- Incremental.completeResults (Incremental.feedEof $
+                                                                                   Incremental.feed t $ parse f),
+                          Text.null remainder = serialize f structure
+                        | otherwise = Nothing
+                     Just blob'
+                        | TestFormat f <- format,
+                          [(structure, remainder)] <- Incremental.completeResults (Incremental.feedEof $
+                                                                                   Incremental.feed blob $ parse f) =
+                             (<> remainder) <$> serialize f structure
+                        | LineFormat f <- format = Just (encodeUtf8 $ mconcat $ mapMaybe (roundTrip f) textLines)
+                        | AttoFormat f <- format,
+                          Atto.Done remainder structure <- Atto.parse (parse f) blob =
+                             (<> remainder) <$> serialize f structure
+                        | AttoFormat f <- format, Atto.Partial i <- Atto.parse (parse f) blob,
+                          Atto.Done remainder structure <- i mempty =
+                             (<> remainder) <$> serialize f structure
+                 return . (:[]) . testCase path $ assertEqual "round-trip" (hex blob) (hex blob')
+
+hex :: ByteString -> String
+hex = ByteString.foldr (pad . flip showHex "") ""
+   where pad [x] s = ['0', x] ++ s
+         pad [x, y] s = [x, y] ++ s
diff --git a/test/URI.hs b/test/URI.hs
new file mode 100644
--- /dev/null
+++ b/test/URI.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE FlexibleInstances, OverloadedStrings, ScopedTypeVariables, StandaloneDeriving,
+             TemplateHaskell, TupleSections, TypeApplications #-}
+
+module URI where
+
+import Data.Bits ((.|.), (.&.), shift)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Char8 as ASCII
+import Data.Char (isAlpha, isAscii, isDigit, isHexDigit, chr, ord)
+import Data.Foldable (fold)
+import Data.Functor.Identity (Identity, runIdentity)
+import qualified Data.List as List
+import Data.Maybe (isNothing, maybe)
+import Data.Monoid.Textual (TextualMonoid)
+import qualified Data.Monoid.Factorial as Factorial
+import qualified Data.Monoid.Textual as Textual
+import Data.String (fromString)
+import Data.Text (Text)
+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
+import Data.Word
+import Numeric (readHex, showHex)
+import Numeric.Natural (Natural)
+import qualified Rank2.TH
+import Text.ParserCombinators.Incremental.LeftBiasedLocal (Parser)
+
+import Construct
+
+import Prelude hiding ((<$), (<*), (*>), take, takeWhile)
+
+data UriReference t f = UriReference{
+   scheme    :: f (Maybe t),
+   authority :: f (Maybe (Authority t Identity)),
+   path      :: f [t],
+   query     :: f (Maybe t),
+   fragment  :: f (Maybe t)
+   }
+
+data Authority t f = Authority{
+   user :: f (Maybe t),
+   host :: f (HostName t),
+   port :: f (Maybe Word16)
+   }
+
+data HostName t = IPv4address{getIPv4address :: [Word8]}
+                | IPv6address{getIPv6address :: [Word16]}
+                | IPvFuture {version :: Word,
+                             address :: t}
+                | RegisteredName{getRegisteredName :: t}
+
+uriReference :: (Show t, TextualMonoid t) => Format (Parser t) Maybe t (UriReference t Identity)
+uriReference = record UriReference{
+   scheme = optional (mapValue (uncurry (<>)) (Factorial.splitAt 1) $
+                      pair (satisfy (any alpha . Textual.characterPrefix) $ take 1)
+                           (takeCharsWhile schemeChar)
+                      <* literal ":"),
+   authority = optional $ record Authority{
+        user = optional (takeCharsWhile userChar <* literal "@"),
+        host = hostName,
+        port = optional (mapValue fromIntegral fromIntegral $ satisfy (< 65536) $
+                         literal ":" *> mapDec (takeCharsWhile1 digit))},  -- port = *DIGIT in spec?
+   path = encodedCharSequence pathChar `sepBy` literal "/",
+   query = optional (literal "*" *> encodedCharSequence queryChar),
+   fragment = optional (literal "#" *> encodedCharSequence fragmentChar)
+   }
+
+hostName :: (Show t, TextualMonoid t) => Format (Parser t) Maybe t (HostName t)
+hostName = mapValue IPv4address getIPv4address ipV4address
+           <|> literal "["
+               *> (mapValue IPv6address getIPv6address ipV6address
+                    <|> mapValue (uncurry IPvFuture) (\ipf-> (version ipf, address ipf))
+                                 (pair (literal "v" *> mapHex (takeCharsWhile1 hexDigit))
+                                       (literal "." *> takeCharsWhile1 ipFutureChar)))
+               <* literal "]"
+           <|> mapValue RegisteredName getRegisteredName (encodedCharSequence hostChar)
+
+ipV4address :: forall t. (Show t, TextualMonoid t) => Format (Parser t) Maybe t [Word8]
+ipV4address = satisfy ((== 4) . length) (decOctet `sepBy` literal ".")
+   where decOctet = mapValue fromIntegral fromIntegral $
+                    satisfy (< 256) $ mapDec $
+                    takeCharsWhile1 digit
+
+ipV6address :: (Show t, TextualMonoid t) => Format (Parser t) Maybe t [Word16]
+ipV6address = satisfy ((== 8) . length) $
+              mapValue fill shorten ipV6addressShort
+   where fill :: [Maybe Word16] -> [Word16]
+         shorten :: [Word16] -> [Maybe Word16]
+         fill words = concatMap (maybe (replicate (8 - length words) 0) (:[])) words
+         shorten [] = []
+         shorten (0:0:words) = Nothing : map Just (dropWhile (== 0) words)
+         shorten (word:words) = Just word : shorten words
+
+ipV6addressShort :: (Show t, TextualMonoid t) => Format (Parser t) Maybe t [Maybe Word16]
+ipV6addressShort = satisfy zeroOrOneNothings $
+                   mapValue (uncurry (++)) (, []) $
+                   pair (optional h16 `sepBy` literal ":")
+                        (literal ":" *> mapValue v4tov6 v6tov4 ipV4address <|> [] <$ literal "")
+   where v4tov6 [] = []
+         v4tov6 (byte1:byte2:bytes) = Just (shift (fromIntegral byte1) 8 .|. fromIntegral byte2) : v4tov6 bytes
+         v4tov6 _ = error "odd number of ipv4 bytes"
+         v6tov4 [] = []
+         v6tov4 (Just word:words) = fromIntegral (shift word $ negate 8) : fromIntegral (word .&. 0xFF) : v6tov4 words
+         v6tov4 (Nothing:words) = 0 : 0 : v6tov4 words
+         zeroOrOneNothings words = elisionCount == 0 && length words == 8 || elisionCount == 1 && length words < 7
+            where elisionCount = length (filter isNothing words)
+         h16 = mapHex $ satisfy ((<5) . Factorial.length) $ takeCharsWhile1 hexDigit
+
+mapHex :: (Show t, TextualMonoid t, Integral n, Show n) => Format (Parser t) Maybe t t -> Format (Parser t) Maybe t n
+mapDec :: (Show t, TextualMonoid t) => Format (Parser t) Maybe t t -> Format (Parser t) Maybe t Natural
+mapDec = mapValue (read . Textual.toString (error . show)) (fromString . show)
+mapHex = mapValue (fst . head . readHex . Textual.toString (error . show)) (fromString . flip showHex "")
+
+alpha, digit, hexDigit, schemeChar, hostChar, userChar, ipFutureChar,
+   pathChar, queryChar, fragmentChar, unreserved, subDelim :: Char -> Bool
+
+alpha c      = isAlpha c && isAscii c
+digit c      = isAlpha c && isDigit c
+hexDigit c   = isAlpha c && isHexDigit c
+schemeChar c = (isAlpha c || isDigit c) && isAscii c || elem @[] c "+-."
+ipFutureChar = userChar
+hostChar c   = unreserved c || subDelim c
+userChar c   = unreserved c || subDelim c || c == ':'
+pathChar c   = unreserved c || subDelim c || c == ':' || c == '@'
+queryChar c  = pathChar c || c == '/' || c == '?'
+fragmentChar = queryChar
+subDelim c   = elem @[] c "!$&'()*+,;="
+unreserved c = isAscii c && (isAlpha c || isDigit c || elem @[] c "-._~")
+
+encodedCharSequence :: forall t. (Show t, TextualMonoid t) => (Char -> Bool) -> Format (Parser t) Maybe t t
+encodedCharSequence predicate = mapValue concatSequence splitSequence $
+                                many (takeCharsWhile predicate <+> percentEncoded)
+   where concatSequence :: [Either t Char] -> t
+         splitSequence :: t -> [Either t Char]
+         percentEncoded :: Format (Parser t) Maybe t Char
+         concatSequence = mconcat . map (either id Textual.singleton)
+         splitSequence s = case Textual.splitCharacterPrefix s
+            of Just ('#', rest)
+                  | Just (hex1, rest1) <- Textual.splitCharacterPrefix rest,
+                    Just (hex2, rest2) <- Textual.splitCharacterPrefix rest1 ->
+                       Right (hexChar [hex1, hex2]) : splitSequence rest2
+                  | otherwise -> []
+               Just (c, _)
+                  | predicate c, (prefix, rest) <- Textual.span_ False predicate s -> Left prefix : splitSequence rest
+               _ -> []
+         percentEncoded = mapValue hexChar (padLeft . (`showHex` "") . ord) $
+                          literal "#" *> count 2 (satisfy hexDigit char)
+         hexChar = chr . fst . head . readHex
+         padLeft [c] = ['0', c]
+         padLeft cs = cs
+
+$(Rank2.TH.deriveAll ''UriReference)
+$(Rank2.TH.deriveAll ''Authority)
diff --git a/test/WMF.hs b/test/WMF.hs
new file mode 100644
--- /dev/null
+++ b/test/WMF.hs
@@ -0,0 +1,256 @@
+{-# LANGUAGE FlexibleInstances, StandaloneDeriving, TemplateHaskell #-}
+
+module WMF where
+
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as ByteString
+import Data.Functor.Identity (Identity)
+import Data.Int
+import Data.Word
+import Data.Serialize
+import qualified Rank2.TH
+import Text.ParserCombinators.Incremental.LeftBiasedLocal (Parser)
+
+import Construct
+
+import Prelude hiding ((<$), (*>))
+
+data File f = File{
+   placeableHeader     :: f (Maybe (PlaceableHeader Identity)),
+   wmfType             :: f WMFType,
+   version             :: f Word16,
+   size                :: f Word32,
+   numberOfObjects     :: f Word16,
+   sizeOfLargestRecord :: f Word32,
+   numberOfParams      :: f Word16,
+   records             :: f [Record Identity]
+   }
+
+deriving instance Show (File Identity)
+
+data PlaceableHeader f = PlaceableHeader{
+   handle       :: f Word16,
+   left         :: f Int16,
+   top          :: f Int16,
+   right        :: f Int16,
+   bottom       :: f Int16,
+   unitsPerInch :: f Word16,
+   checksum     :: f Word16
+   }
+
+deriving instance Show (PlaceableHeader Identity)
+
+data Record f = Record{
+   recordSize   :: f Word32,
+   function     :: f Function,
+   params       :: f [Word16]
+   }
+
+deriving instance Show (Record Identity)
+
+data WMFType = InMemory | InFile deriving (Eq, Ord, Show)
+
+data Function = AbortDoc
+              | Aldus_Header
+              | AnimatePalette
+              | Arc
+              | BitBlt
+              | Chord
+              | CLP_Header16
+              | CLP_Header32
+              | CreateBitmap
+              | CreateBitmapIndirect
+              | CreateBrush
+              | CreateBrushIndirect
+              | CreateFontIndirect
+              | CreatePalette
+              | CreatePatternBrush
+              | CreatePenIndirect
+              | CreateRegion
+              | DeleteObject
+              | DibBitblt
+              | DibCreatePatternBrush
+              | DibStretchBlt
+              | DrawText
+              | Ellipse
+              | EndDoc
+              | EndPage
+              | EOF
+              | Escape
+              | ExcludeClipRect
+              | ExtFloodFill
+              | ExtTextOut
+              | FillRegion
+              | FloodFill
+              | FrameRegion
+              | Header
+              | IntersectClipRect
+              | InvertRegion
+              | LineTo
+              | MoveTo
+              | OffsetClipRgn
+              | OffsetViewportOrg
+              | OffsetWindowOrg
+              | PaintRegion
+              | PatBlt
+              | Pie
+              | Polygon
+              | Polyline
+              | PolyPolygon
+              | RealizePalette
+              | Rectangle
+              | ResetDC
+              | ResizePalette
+              | RestoreDC
+              | RoundRect
+              | SaveDC
+              | ScaleViewportExt
+              | ScaleWindowExt
+              | SelectClipRegion
+              | SelectObject
+              | SelectPalette
+              | SetBKColor
+              | SetBKMode
+              | SetDibToDev
+              | SelLayout
+              | SetMapMode
+              | SetMapperFlags
+              | SetPalEntries
+              | SetPixel
+              | SetPolyFillMode
+              | SetReLabs
+              | SetROP2
+              | SetStretchBltMode
+              | SetTextAlign
+              | SetTextCharExtra
+              | SetTextColor
+              | SetTextJustification
+              | SetViewportExt
+              | SetViewportOrg
+              | SetWindowExt
+              | SetWindowOrg
+              | StartDoc
+              | StartPage
+              | StretchBlt
+              | StretchDIB
+              | TextOut
+              deriving (Eq, Ord, Show)
+
+fileFormat :: Format (Parser ByteString) Maybe ByteString (File Identity)
+fileFormat = record File{
+   placeableHeader = optional headerFormat,
+   wmfType = InMemory <$ literal (ByteString.pack [0, 0])
+             <|> InFile <$ literal (ByteString.pack [1, 0]),
+   version = literal (ByteString.pack [9, 0]) *> cereal' getWord16le putWord16le,
+   size = cereal' getWord32le putWord32le,
+   numberOfObjects = cereal' getWord16le putWord16le,
+   sizeOfLargestRecord = cereal' getWord32le putWord32le,
+   numberOfParams = cereal' getWord16le putWord16le,
+   records = many recordFormat
+   }
+   
+headerFormat :: Format (Parser ByteString) Maybe ByteString (PlaceableHeader Identity)
+headerFormat = record PlaceableHeader{
+   handle = literal (ByteString.pack [0xD7, 0xCD, 0xC6, 0x9A]) *> cereal' getWord16le putWord16le,
+   left = cereal' getInt16le putInt16le,
+   top = cereal' getInt16le putInt16le,
+   right = cereal' getInt16le putInt16le,
+   bottom = cereal' getInt16le putInt16le,
+   unitsPerInch = cereal' getWord16le putWord16le,
+   checksum = literal (ByteString.pack [0, 0, 0, 0]) *> cereal' getWord16le putWord16le
+   }
+
+recordFormat :: Format (Parser ByteString) Maybe ByteString (Record Identity)
+recordFormat = mfix $ \this-> record Record{
+   recordSize = cereal' getWord32le putWord32le,
+   function = AbortDoc <$ word16le 0x0052 <|>
+              Aldus_Header <$ word16le 0x0001 <|>
+              AnimatePalette <$ word16le 0x0436 <|>
+              Arc <$ word16le 0x0817 <|>
+              BitBlt <$ word16le 0x0922 <|>
+              Chord <$ word16le 0x0830 <|>
+              CLP_Header16 <$ word16le 0x0002 <|>
+              CLP_Header32 <$ word16le 0x0003 <|>
+              CreateBitmap <$ word16le 0x06FE <|>
+              CreateBitmapIndirect <$ word16le 0x02FD <|>
+              CreateBrush <$ word16le 0x00F8 <|>
+              CreateBrushIndirect <$ word16le 0x02FC <|>
+              CreateFontIndirect <$ word16le 0x02FB <|>
+              CreatePalette <$ word16le 0x00F7 <|>
+              CreatePatternBrush <$ word16le 0x01F9 <|>
+              CreatePenIndirect <$ word16le 0x02FA <|>
+              CreateRegion <$ word16le 0x06FF <|>
+              DeleteObject <$ word16le 0x01F0 <|>
+              DibBitblt <$ word16le 0x0940 <|>
+              DibCreatePatternBrush <$ word16le 0x0142 <|>
+              DibStretchBlt <$ word16le 0x0B41 <|>
+              DrawText <$ word16le 0x062F <|>
+              Ellipse <$ word16le 0x0418 <|>
+              EndDoc <$ word16le 0x005E <|>
+              EndPage <$ word16le 0x0050 <|>
+              EOF <$ word16le 0x0000 <|>
+              Escape <$ word16le 0x0626 <|>
+              ExcludeClipRect <$ word16le 0x0415 <|>
+              ExtFloodFill <$ word16le 0x0548 <|>
+              ExtTextOut <$ word16le 0x0A32 <|>
+              FillRegion <$ word16le 0x0228 <|>
+              FloodFill <$ word16le 0x0419 <|>
+              FrameRegion <$ word16le 0x0429 <|>
+              Header <$ word16le 0x0004 <|>
+              IntersectClipRect <$ word16le 0x0416 <|>
+              InvertRegion <$ word16le 0x012A <|>
+              LineTo <$ word16le 0x0213 <|>
+              MoveTo <$ word16le 0x0214 <|>
+              OffsetClipRgn <$ word16le 0x0220 <|>
+              OffsetViewportOrg <$ word16le 0x0211 <|>
+              OffsetWindowOrg <$ word16le 0x020F <|>
+              PaintRegion <$ word16le 0x012B <|>
+              PatBlt <$ word16le 0x061D <|>
+              Pie <$ word16le 0x081A <|>
+              Polygon <$ word16le 0x0324 <|>
+              Polyline <$ word16le 0x0325 <|>
+              PolyPolygon <$ word16le 0x0538 <|>
+              RealizePalette <$ word16le 0x0035 <|>
+              Rectangle <$ word16le 0x041B <|>
+              ResetDC <$ word16le 0x014C <|>
+              ResizePalette <$ word16le 0x0139 <|>
+              RestoreDC <$ word16le 0x0127 <|>
+              RoundRect <$ word16le 0x061C <|>
+              SaveDC <$ word16le 0x001E <|>
+              ScaleViewportExt <$ word16le 0x0412 <|>
+              ScaleWindowExt <$ word16le 0x0410 <|>
+              SelectClipRegion <$ word16le 0x012C <|>
+              SelectObject <$ word16le 0x012D <|>
+              SelectPalette <$ word16le 0x0234 <|>
+              SetBKColor <$ word16le 0x0201 <|>
+              SetBKMode <$ word16le 0x0102 <|>
+              SetDibToDev <$ word16le 0x0D33 <|>
+              SelLayout <$ word16le 0x0149 <|>
+              SetMapMode <$ word16le 0x0103 <|>
+              SetMapperFlags <$ word16le 0x0231 <|>
+              SetPalEntries <$ word16le 0x0037 <|>
+              SetPixel <$ word16le 0x041F <|>
+              SetPolyFillMode <$ word16le 0x0106 <|>
+              SetReLabs <$ word16le 0x0105 <|>
+              SetROP2 <$ word16le 0x0104 <|>
+              SetStretchBltMode <$ word16le 0x0107 <|>
+              SetTextAlign <$ word16le 0x012E <|>
+              SetTextCharExtra <$ word16le 0x0108 <|>
+              SetTextColor <$ word16le 0x0209 <|>
+              SetTextJustification <$ word16le 0x020A <|>
+              SetViewportExt <$ word16le 0x020E <|>
+              SetViewportOrg <$ word16le 0x020D <|>
+              SetWindowExt <$ word16le 0x020C <|>
+              SetWindowOrg <$ word16le 0x020B <|>
+              StartDoc <$ word16le 0x014D <|>
+              StartPage <$ word16le 0x004F <|>
+              StretchBlt <$ word16le 0x0B23 <|>
+              StretchDIB <$ word16le 0x0F43 <|>
+              TextOut <$ word16le 0x0521,
+   params = count (fromIntegral (recordSize this) - 3) (cereal' getWord16le putWord16le)
+   }
+   where word16le = value (cereal' getWord16le putWord16le)
+
+$(Rank2.TH.deriveAll ''File)
+$(Rank2.TH.deriveAll ''PlaceableHeader)
+$(Rank2.TH.deriveAll ''Record)
diff --git a/test/examples/wmf1.wmf b/test/examples/wmf1.wmf
new file mode 100644
Binary files /dev/null and b/test/examples/wmf1.wmf differ
