diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2018 Ben Hamlin
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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/regex-generator.cabal b/regex-generator.cabal
new file mode 100644
--- /dev/null
+++ b/regex-generator.cabal
@@ -0,0 +1,50 @@
+-- Initial regex-generator.cabal generated by cabal init.  For further 
+-- documentation, see http://haskell.org/cabal/users-guide/
+
+name:                regex-generator
+version:             0.1.0.0
+synopsis:            Generate a random string from a PCRE
+-- description:         
+license:             MIT
+license-file:        LICENSE
+author:              Ben Hamlin
+maintainer:          ben.hamlin@formal.tech
+-- copyright:           
+category:            Text
+build-type:          Simple
+-- extra-source-files:  
+cabal-version:       >=1.10
+
+library
+  exposed-modules:     Text.Regex.Regen
+                       Text.Regex.Regen.CClass
+                       Text.Regex.Regen.Gen
+                       Text.Regex.Regen.Parse
+                       Text.Regex.Regen.ParserST
+                       Text.Regex.Regen.Pattern
+                       Text.Regex.Regen.PatternException
+                       Text.Regex.Regen.Util
+  -- other-modules:       
+  other-extensions:    RecordWildCards
+  build-depends:       base       >=4.0  && <4.9,
+                       attoparsec >=0.13 && <0.14,
+                       bytestring >=0.10 && <0.11,
+                       containers >=0.5  && <0.7,
+                       exceptions >=0.8  && <0.9,
+                       random     >=1.0  && <1.2
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+test-suite generate
+  ghc-options:         -Wall
+  type:                exitcode-stdio-1.0
+  main-is:             test/Gen.hs
+  default-language:    Haskell2010
+  build-depends:       base               >=4.0  && <4.9,
+                       bytestring         >=0.10 && <0.11,
+                       hspec              >=2.2  && <2.5,
+                       HUnit              >=1.2  && <1.7,
+                       --hspec-expectations >=0.6  && <0.9,
+                       random             >=1.0  && <1.2,
+                       regex-pcre         >=0.82 && <0.95,
+                       regex-generator
diff --git a/src/Text/Regex/Regen.hs b/src/Text/Regex/Regen.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Regex/Regen.hs
@@ -0,0 +1,11 @@
+module Text.Regex.Regen
+    ( module X
+    ) where
+
+import Text.Regex.Regen.Gen as X (generate, generateOpts, generatePattern)
+import Text.Regex.Regen.Gen as X (generatePatternIO)
+import Text.Regex.Regen.Parse as X (parsePatternOpts, parsePattern)
+import Text.Regex.Regen.Pattern as X (Options(..), Strategy(..), defaultOptions)
+import Text.Regex.Regen.Pattern as X (Part(..), Range(..), Anchor(..))
+import Text.Regex.Regen.Pattern as X (Pattern(..), LineEnds(..), Group(..))
+import Text.Regex.Regen.PatternException as X
diff --git a/src/Text/Regex/Regen/CClass.hs b/src/Text/Regex/Regen/CClass.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Regex/Regen/CClass.hs
@@ -0,0 +1,106 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Text.Regex.Regen.CClass where
+
+import Prelude hiding (takeWhile)
+
+import Control.Applicative (Alternative(..))
+import Data.Attoparsec.ByteString.Char8 (isDigit, isSpace, isHorizontalSpace)
+import Data.Char (ord)
+import Data.Functor (($>))
+import Data.List ((\\))
+
+import Text.Regex.Regen.ParserST
+import Text.Regex.Regen.Pattern
+import Text.Regex.Regen.PatternException
+import Text.Regex.Regen.Util
+
+bracketClass :: ParserST Part
+bracketClass = bracketed $ negative <|> positive
+    where
+    positive = fmap (CClass True)  $             go []
+    negative = fmap (CClass False) $ char '^' *> go []
+    cpart    = posixRange <|> backslashRange <|> dashRange <|> singleton
+    go acc   = do
+        acc' <- (acc ++) <$> cpart
+        mb   <- peekChar
+        case mb of
+            Just ']' -> return acc'
+            _        -> go acc'
+
+backslashClass :: ParserST Part
+backslashClass = do
+    o <- pOptions <$> getPattern
+    dot o <|> bsR <|> bsN o <|> range
+    where
+    dot o = string "."   $> CClass False (munless (oDotAll o) (oLineEndChars o))
+    bsN o = string "\\N" $> CClass False (oLineEndChars o)
+    bsR   = string "\\R" $> CClass True ['\r','\n','\f','\v']
+    range = CClass True <$> backslashRange
+
+posixRange :: ParserST [Char]
+posixRange = toPC =<< string "[:" *> pcname <* string ":]"
+    where
+    toPC ('^':s)  = negate <$> toPC s
+    toPC "alnum"  = return $ ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9']
+    toPC "alpha"  = return $ ['a'..'z'] ++ ['A'..'Z']
+    toPC "ascii"  = return $ ['\x00'..'\x7f']
+    toPC "blank"  = return $ [' ','\t']
+    toPC "cntrl"  = return $ '\x7f' : ['\x00'..'\x1f']
+    toPC "digit"  = return $ ['0'..'9']
+    toPC "graph"  = return $ ['\x21'..'\x7e']
+    toPC "lower"  = return $ ['a'..'z']
+    toPC "print"  = return $ ['\x20'..'\x7e']
+    toPC "punct"  = return $ "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
+    toPC "space"  = return $ " \t\r\n\v\f" 
+    toPC "word"   = return $ '_' : ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9']
+    toPC "xdigit" = return $ ['a'..'f'] ++ ['A'..'Z'] ++ ['0'..'9']
+    toPC name     = eNoSuchClass name
+    negate cs     = ['\x00'..'\xff'] \\ cs
+    pcname        = many $ satisfy (/=':') <|> char ':' `notFollowedBy` ']'
+
+backslashRange :: ParserST [Char]
+backslashRange = let bytes = ['\x00'..'\xff'] in choice
+    [ string "\\d" $> [ c | c <- bytes, isDigit c ]
+    , string "\\D" $> [ c | c <- bytes, not (isDigit c) ]
+    , string "\\h" $> [ c | c <- bytes, isHSpace c ]
+    , string "\\H" $> [ c | c <- bytes, not (isHSpace c) ]
+    , string "\\s" $> [ c | c <- bytes, isSpace c ]
+    , string "\\S" $> [ c | c <- bytes, not (isSpace c) ]
+    , string "\\v" $> "\n\v\f\r"
+    , string "\\V" $> bytes \\ "\n\v\f\r"
+    , string "\\w" $> '_' : ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9']
+    , string "\\W" $> bytes \\ ('_' : ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'])
+    , string "\\N" >> eNotSupported "'\\N' in character class"
+    , string "\\L" >> eNotSupported "'\\L'"
+    , string "\\l" >> eNotSupported "'\\l'"
+    , string "\\U" >> eNotSupported "'\\U'"
+    , string "\\u" >> eNotSupported "'\\u'"
+    , string "\\Q" >> eNotSupported "'\\Q' in character class"
+    , string "\\E" >> eNotSupported "'\\E' in character class"
+    ,                   string "\\p{" *> some letter <* char '}' >> eNoProps
+    ,                   string "\\p"  *> letter                  >> eNoProps
+    , fmap (bytes \\) $ string "\\P{" *> some letter <* char '}' >> eNoProps
+    , fmap (bytes \\) $ string "\\P"  *> letter                  >> eNoProps
+    ]
+    where
+    eNoProps  = eNotSupported "properties"
+    isHSpace  = isHorizontalSpace . fromIntegral . ord
+
+dashRange :: ParserST [Char]
+dashRange = mkRange =<< (,) <$> singleton <* char '-' <*> singleton
+    where
+    mkRange p = case p of
+        ([a],[b]) | a <= b -> pure [a..b]
+        ([_],[_])          -> eClassOutOfOrder
+        _                  -> eParserError "unreachable"
+
+singleton :: ParserST [Char]
+singleton = (:[]) <$> choice
+    [ escaped hexByte
+    , escaped octalByte
+    , escaped cescape
+    , escaped literal
+    , literal
+    ]
+    where
+    literal = satisfy (/='\\')
diff --git a/src/Text/Regex/Regen/Gen.hs b/src/Text/Regex/Regen/Gen.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Regex/Regen/Gen.hs
@@ -0,0 +1,308 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Text.Regex.Regen.Gen where
+
+import Control.Applicative (Alternative(..))
+import Control.Exception (SomeException, mapException)
+import Control.Monad (MonadPlus(..), guard, (<=<), when, void)
+import Control.Monad.Catch (MonadThrow(..), MonadCatch(..))
+import Control.Monad.Catch (try, fromException, toException)
+import Data.List ((\\), foldl')
+import Data.Map (Map)
+import Data.Maybe (fromMaybe, listToMaybe, maybe, catMaybes)
+import Data.Monoid ((<>))
+import System.Random (Random, StdGen, random, randomR, newStdGen)
+import qualified Data.ByteString.Char8 as S
+import qualified Data.Map as Map
+
+import Text.Regex.Regen.Parse
+import Text.Regex.Regen.Pattern
+import Text.Regex.Regen.PatternException
+import Text.Regex.Regen.Util
+
+generate :: S.ByteString -> IO S.ByteString
+generate = generateOpts defaultOptions
+
+generateOpts :: Options -> S.ByteString -> IO S.ByteString
+generateOpts o bs = parsePatternOpts o bs >>= generatePatternIO
+
+generatePatternIO :: Pattern -> IO S.ByteString
+generatePatternIO p = do
+    (e,_) <- generatePattern p <$> newStdGen
+    either throwM pure e
+
+generatePattern :: Pattern -> StdGen -> (Either SomeException S.ByteString, StdGen)
+generatePattern p g = let (s,e) = runGen genTop (makeGenState p g) in (e, gsGen s)
+    where
+    state  = makeGenState p g
+    genTop = getGroup 0 >>= genGroup
+
+data GenState = GenState
+    { gsGen        :: !StdGen
+    , gsOptions    :: !Options
+    , gsGroups     :: !(Map Int Group)
+    , gsGroupNames :: !(Map S.ByteString Int)
+    , gsResolved   :: !(Map Int S.ByteString)
+    } deriving Show
+
+makeGenState :: Pattern -> StdGen -> GenState
+makeGenState Pattern {..} g = GenState g pOptions pGroups pGroupNames Map.empty
+
+newGenState :: Pattern -> IO GenState
+newGenState p = makeGenState p <$> newStdGen
+
+newtype Gen a = Gen { runGen :: GenState -> (GenState, Either SomeException a) }
+instance Functor Gen where
+    fmap f m = Gen $ \s -> let (s',e) = runGen m s in (s', fmap f e)
+instance Applicative Gen where
+    pure a = Gen $ \s -> (s, Right a)
+    m <*> n = Gen $ \s ->
+        let (s',f)  = runGen m s
+            (s'',a) = runGen n s'
+         in (s'', f <*> a)
+instance Alternative Gen where
+    empty = Gen $ \s -> (s, mapException fromPatEx eUnsatisfiable)
+        where
+        fromPatEx :: PatternException -> SomeException
+        fromPatEx = toException
+    m <|> n = Gen $ \s -> case runGen m s of
+        r@(_, Right _) -> r
+        (_, Left _)    -> runGen n s
+instance Monad Gen where
+    m >>= f = Gen $ \s -> case runGen m s of
+        (s', Left e)  -> (s', Left e)
+        (s', Right a) -> runGen (f a) s'
+    fail = throwM . EGenError
+instance Monoid m => Monoid (Gen m) where
+    mempty = pure mempty
+    mappend g1 g2 = g1 >>= \m1 -> g2 >>= \m2 -> pure $ m1 <> m2
+instance MonadThrow Gen where
+    throwM e = Gen $ \s -> (s, Left $ toException e)
+instance MonadCatch Gen where
+    catch m f = Gen $ \s -> case runGen m s of
+        (s', Right a) -> (s', Right a)
+        (s,  Left e)  -> case fromException e of
+            Nothing -> (s, Left e)
+            Just e' -> runGen (f e') s
+
+evalGen :: Gen a -> GenState -> Either SomeException a
+evalGen = snd .: runGen
+
+getGenState :: Gen GenState
+getGenState = Gen $ \s -> (s, Right s)
+
+putGenState :: GenState -> Gen ()
+putGenState s = Gen $ const (s, Right ())
+
+modifyGenState' :: (GenState -> GenState) -> Gen ()
+modifyGenState' f = Gen $ \s -> (f s, Right ())
+
+modifyGenState :: (GenState -> (GenState, a)) -> Gen a
+modifyGenState f = Gen $ \s -> let (s',a) = f s in (s', Right a)
+
+getOption :: (Options -> a) -> Gen a
+getOption f = f . gsOptions <$> getGenState
+
+getNamedGroup' :: S.ByteString -> Gen (Maybe Group)
+getNamedGroup' bs = lookupNamedGroup <$> getGenState
+    where
+    lookupNamedGroup :: GenState -> Maybe Group
+    lookupNamedGroup GenState {..} =
+        flip Map.lookup gsGroups =<< Map.lookup bs gsGroupNames
+
+getNamedGroup :: S.ByteString -> Gen Group
+getNamedGroup bs = getNamedGroup' bs >>=
+    maybe (fail $ "nonexistent group name: " ++ show bs) pure
+
+getGroup' :: Int -> Gen (Maybe Group)
+getGroup' ix = Map.lookup ix . gsGroups <$> getGenState
+
+getGroup :: Int -> Gen Group
+getGroup ix = getGroup' ix >>=
+    maybe (fail $ "nonexistent group index: " ++ show ix) pure
+
+getResolved' :: Int -> Gen (Maybe S.ByteString)
+getResolved' ix = Map.lookup ix . gsResolved <$> getGenState
+
+getResolved :: Int -> Gen S.ByteString
+getResolved ix = getResolved' ix >>=
+    maybe (fail $ "group index not yet resolved: " ++ show ix) pure
+
+resolveRef :: Int -> S.ByteString -> Gen S.ByteString
+resolveRef ix bs = modifyGenState f
+    where
+    f s = (s { gsResolved = Map.insert ix bs (gsResolved s) }, bs)
+
+getStdGen :: Gen StdGen
+getStdGen = gsGen <$> getGenState
+
+putStdGen :: StdGen -> Gen ()
+putStdGen g = modifyGenState' (\s -> s { gsGen = g })
+
+randomGen :: Random a => Gen a
+randomGen = do
+    g <- getStdGen
+    let (a,g') = random g
+    putStdGen g'
+    pure a
+
+randomRGen :: Random a => (a,a) -> Gen a
+randomRGen r = do
+    g <- getStdGen
+    let (a,g') = randomR r g
+    putStdGen g'
+    pure a
+
+randomElemGen :: [a] -> Gen a
+randomElemGen l = do
+    g <- getStdGen
+    guard $ not (null l)
+    let (n,g') = randomR (0, length l - 1) g
+    putStdGen g'
+    pure $ n `th` l
+    where
+    n `th` l = head $ drop n l
+
+genGroup :: Group -> Gen S.ByteString
+genGroup Group {..} = genPart gParts >>= resolveRef gIndex
+
+genPart :: Part -> Gen S.ByteString
+genPart Empty                = pure S.empty
+genPart (Byte c)             = pure $ S.singleton c
+genPart (Sequence    ps)     = genSequence ps
+genPart (CClass      b cs)   = genCClass b cs
+genPart (Quantified  p r t)  = genQuantified p r t
+genPart (Alternative ps)     = genAlternative ps
+genPart (Reference   ix)     = genReference ix
+genPart (Call        _)      = eNotSupported "pattern calls"
+genPart (Anchored    p as q) = genAnchored p as q
+
+genSequence :: [Part] -> Gen S.ByteString
+genSequence ps = mconcat $ genPart <$> ps
+
+genCClass :: Bool -> [Char] -> Gen S.ByteString
+genCClass b pos = S.singleton <$> randomElemGen cs
+    where
+    cs = if b then pos else ['\x00'..'\xff'] \\ pos
+
+genQuantified :: Part -> Range -> Strategy -> Gen S.ByteString
+genQuantified p (Range n m) _ = do
+    -- TODO: Make default spread configurable? Maybe normally distributed?
+    r <- randomRGen (n, fromMaybe (n + 10) m)
+    genSequence $ replicate r p
+
+genAlternative :: [Part] -> Gen S.ByteString
+genAlternative = genPart <=< randomElemGen
+
+genReference :: Int -> Gen S.ByteString
+genReference ix = getResolved ix <|> do
+    g <- getGroup ix
+    genGroup g
+
+genAnchored :: Part -> [Anchor] -> Part -> Gen S.ByteString
+genAnchored p1 as p2 = do
+    as' <- mapM (tryMaybe . resolve) as
+    alt <- nonEmptyAltM $ catMaybes as'
+    genPart alt
+    where
+    resolve StartOfMatch     = eGenError "start-of-match anchors unsupported"
+    resolve WordBoundary     =                  endWith isW p1 <> pure          p2
+                                            <|> pure        p1 <> startWith isW p2
+    resolve WordInternal     =                  endWith isw p1 <> startWith isw p2
+    resolve Start            =                  nullify     p1 <> pure          p2
+    resolve End              =                  pure        p1 <> nullify       p2
+    resolve BeforeNewline    = getLE >>= \le -> pure        p1 <> startWith le  p2
+    resolve AfterNewline     = getLE >>= \le -> endWith le  p1 <> pure          p2
+    resolve EndBeforeNewline = getLE >>= \le -> pure        p1 <> comprise  le  p2
+    getLE = getOption oLineEndChars >>= \cs -> pure (`elem` cs)
+    isw c = c=='_' || ('A'<=c && c<='Z') || ('a'<=c && c<='z') || ('0'<=c && c<='9')
+    isW   = not . isw
+
+nullify :: Part -> Gen Part
+nullify part = case part of
+    Empty -> mempty
+    Byte _ -> eUnsatisfiable
+    Sequence ps -> if null ps then mempty else eUnsatisfiable
+    CClass _ _ -> eUnsatisfiable
+    Quantified _ r _ -> if rMin r == 0 then mempty else eUnsatisfiable
+    Alternative ps -> foldr (<|>) empty $ nullify <$> ps
+    Reference ix -> do
+        m <- getResolved' ix
+        case m of
+            Just bs -> if S.null bs then pure () else eUnsatisfiable
+            Nothing -> do
+                g <- getGroup ix
+                p <- nullify $ gParts g
+                void . genGroup $ Group ix p
+        pure $ Reference ix
+    Call _ -> eNotSupported "pattern calls"
+    Anchored p1 as p2 -> Anchored <$> nullify p1 <*> pure as <*> nullify p2
+
+comprise :: (Char -> Bool) -> Part -> Gen Part
+comprise p part = case part of
+    Empty -> mempty
+    Byte c -> if p c then pure part else eUnsatisfiable
+    Sequence ps -> Sequence <$> mapM (comprise p) ps
+    CClass b cs -> nonEmptyCClassM p $ if b then cs else ['\x00'..'\xff'] \\ cs
+    Quantified q r t -> Quantified <$> comprise p q <*> pure r <*> pure t
+    Alternative ps -> nonEmptyAlt . catMaybes <$> mapM (tryMaybe . comprise p) ps
+    Reference ix -> do
+        m <- getResolved' ix
+        case m of
+            Just bs -> if S.all p bs then pure () else eUnsatisfiable
+            Nothing -> do
+                g <- getGroup ix
+                p <- comprise p $ gParts g
+                void . genGroup $ Group ix p
+        pure $ Reference ix
+    Call _ -> eNotSupported "pattern calls"
+    Anchored p1 as p2 -> Anchored <$> comprise p p1 <*> pure as <*> comprise p p2
+
+endWith :: (Char -> Bool) -> Part -> Gen Part
+endWith p part = case part of
+    Empty -> eUnsatisfiable
+    Byte c -> if p c then pure part else eUnsatisfiable
+    Sequence ps -> case reverse ps of
+        []         -> eUnsatisfiable
+        part : ps' -> nonEmptySeq . reverse .: (:) <$> endWith p part <*> pure ps'
+    CClass b cs -> nonEmptyCClassM p $ if b then cs else ['\x00'..'\xff'] \\ cs
+    Quantified q r t -> endWith p q >>= \q' -> nonEmptyQuantM q' r t
+    Alternative ps -> nonEmptyAlt . catMaybes <$> mapM (tryMaybe . endWith p) ps
+    Reference ix -> do
+        m <- getResolved' ix
+        case m of
+            Just bs -> if S.null bs || p (S.last bs)
+                then pure ()
+                else eUnsatisfiable
+            Nothing -> do
+                g <- getGroup ix
+                p <- endWith p $ gParts g
+                void . genGroup $ Group ix p
+        pure $ Reference ix
+    Call ix -> eNotSupported "pattern calls"
+    Anchored p1 as p2 -> Anchored p1 as <$> endWith p p2
+
+startWith :: (Char -> Bool) -> Part -> Gen Part
+startWith p part = case part of
+    Empty -> eUnsatisfiable
+    Byte c -> if p c then pure part else eUnsatisfiable
+    Sequence ps -> case ps of
+        []         -> eUnsatisfiable
+        part : ps' -> nonEmptySeq .: (:) <$> startWith p part <*> pure ps'
+    CClass b cs -> nonEmptyCClassM p $ if b then cs else ['\x00'..'\xff'] \\ cs
+    Quantified q r t -> startWith p q >>= \q' -> nonEmptyQuantM q' r t
+    Alternative ps -> nonEmptyAlt . catMaybes <$> mapM (tryMaybe . startWith p) ps
+    Reference ix -> do
+        m <- getResolved' ix
+        case m of
+            Just bs -> if S.null bs || p (S.head bs)
+                then pure ()
+                else eUnsatisfiable
+            Nothing -> do
+                g <- getGroup ix
+                p <- startWith p $ gParts g
+                void . genGroup $ Group ix p
+        pure $ Reference ix
+    Call ix -> eNotSupported "pattern calls"
+    Anchored p1 as p2 -> Anchored <$> startWith p p1 <*> pure as <*> pure p2
diff --git a/src/Text/Regex/Regen/Parse.hs b/src/Text/Regex/Regen/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Regex/Regen/Parse.hs
@@ -0,0 +1,343 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Text.Regex.Regen.Parse where
+
+import Prelude hiding (takeWhile)
+
+import Control.Applicative (Alternative(..), optional)
+import Control.Monad (when, unless, guard)
+import Control.Monad.Catch (MonadThrow)
+import Data.Char (toLower, toUpper, isAlpha)
+import Data.Functor (($>))
+import Data.List ((\\), nub)
+import Data.Map (Map)
+import Data.Set (Set)
+import qualified Control.Exception as X
+import qualified Data.Attoparsec.ByteString.Char8 as P
+import qualified Data.Attoparsec.Combinator as P
+import qualified Data.ByteString.Char8 as S
+import qualified Data.Char as Char
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+
+import Text.Regex.Regen.ParserST
+import Text.Regex.Regen.Pattern
+import Text.Regex.Regen.PatternException
+import Text.Regex.Regen.Util
+import Text.Regex.Regen.CClass (bracketClass, backslashClass, posixRange)
+
+groupName :: Char -> ParserST S.ByteString
+groupName term = check =<< takeTill (==term)
+    where
+    isNameC = (=='_') ||| Char.isAlphaNum
+    check n
+        | Char.isDigit (S.head n) = eBadGroupName "must not start with a digit"
+        | not (S.all isNameC n)   = eBadGroupName "invalid character"
+        | S.length n > 32         = eBadGroupName "longer than 32 characters"
+        | otherwise               = pure n
+
+group :: ParserST Part
+group = paren $ nonCap <|> atomic <|> reset <|> capture
+    where
+    nonCap  = string "?:" *> parts
+    atomic  = string "?>" *> parts
+    reset   = string "?|" *> eNotSupported "reset groups"
+    gName   = optional (char 'P') *> angled (groupName '>') <|> squoted (groupName '\'')
+    capture = do
+        num  <- nextGroupNum
+        name <- Just <$> (char '?' *> gName) <|> pure Nothing
+        part <- parts
+        case name of
+            Just bs -> addNamedGroup bs num part
+            Nothing -> addGroup num part
+        pure $ Reference num
+
+byte :: ParserST Part
+byte = do
+    Options {..} <- pOptions <$> getPattern
+    let isMeta  = isMeta' oFreeSpacing
+        meta    = satisfy isMeta
+        literal = satisfy $ not . isMeta
+        escLit  = satisfy $ not . (=='\\') &&& not . isSpecial
+    c <- choice
+        [ escaped cescape
+        , escaped hexByte
+        , escaped octalByte
+        , escaped meta
+        , escaped escLit
+        , literal
+        ]
+    pure $ doCases oIgnoreCase c
+    where
+    isMeta' b c = c `S.elem` ("\\^$.[|()?*+" `S.append` if b then " " else "")
+    isSpecial c = c `S.elem` "ABCDEGHKLNPQRSUVWXZabcdefghklnoprstuvwxz1234567"
+    doCases True c | isAlpha c = CClass True [toLower c, toUpper c]
+    doCases _    c             = Byte c
+
+forbidden :: ParserST Part
+forbidden = choice
+    [ string "\\C" *> eNotSupported "'\\C'"
+    , string "\\X" *> eNotSupported "'\\X'"
+    , string "\\L" *> eNotSupported "'\\L'"
+    , string "\\l" *> eNotSupported "'\\l'"
+    , string "\\U" *> eNotSupported "'\\U'"
+    , string "\\u" *> eNotSupported "'\\u'"
+    , string "\\Q" *> eNotSupported "'\\Q'"
+    , string "\\E" *> eNotSupported "'\\E'"
+    , posixRange   *> eNotSupported "posix range outside of class"
+    , range        *> eParserError "nothing to quantify"
+    ]
+    where
+    eBadRef c = eNoSuchIndex $ Left (Char.ord c - Char.ord '0')
+
+cclass :: ParserST Part
+cclass = doCases =<< (bracketClass <|> backslashClass)
+    where
+    doCases (CClass b cs) = do
+        ignoreCase <- oIgnoreCase . pOptions <$> getPattern
+        pure . CClass b . nub $ if ignoreCase
+            then foldr (\c -> ([toLower c, toUpper c] ++)) [] cs
+            else cs
+
+range :: ParserST Range
+range = plus <|> star <|> question <|> braced range'
+    where
+    plus     = Range 1 Nothing  <$ char '+'
+    star     = Range 0 Nothing  <$ char '*'
+    question = Range 0 (Just 1) <$ char '?'
+    range'   = check =<< (bounded <|> lower <|> fixed)
+    bounded  = Range <$> decimal <* char ',' <*> fmap Just decimal
+    lower    = Range <$> decimal <* char ',' <*> pure Nothing
+    fixed    = decimal >>= \n -> pure $ Range n (Just n)
+    check r@(Range n m)
+        | maybe False (n >) m = eQuantOutOfOrder
+        | otherwise           = pure r
+
+quantified :: ParserST Part -> ParserST Part
+quantified p = Quantified <$> p <*> range <*> strategy
+    where
+    strategy   = lazy <|> possessive <|> def
+    lazy       = Lazy       <$ char '?' *> eNotSupported "lazy quantifiers"
+    possessive = Possessive <$ char '+' *> eNotSupported "possessive quantifiers"
+    def        = oDefaultStrategy . pOptions <$> getPattern
+
+reference :: ParserST Part
+reference = do
+    m     <- pGroupCount <$> getPattern
+    ix    <- numbered m <|> named
+    inUse <- either groupNumInUse groupNameInUse ix
+    when (not inUse) $ eNoSuchIndex ix
+    Reference <$> case ix of
+        Left  n  -> pure n
+        Right bs -> lookupGroupIndex bs >>= maybe (eNoSuchName bs) pure
+    where
+    named = Right <$> choice
+        [ string "\\k" *> angled  (groupName '>')
+        , string "\\k" *> squoted (groupName '\'')
+        , string "\\k" *> braced  (groupName '}')
+        , string "\\g" *> braced  (groupName '}')
+        , paren $ string "?P=" *> (groupName ')')
+        ]
+    numbered m = Left <$> choice
+        [ string "\\"  *> decimal >>= \n -> guard (0 < n && n <= max 7 m) >> pure n
+        , string "\\"  *> braced decimal
+        , string "\\g" *> decimal
+        , string "\\g" *> braced decimal
+        , string "\\g" *> braced (char '-' *> decimal >>= \n -> pure $ m-n + 1)
+        ]
+    eNoSuchName = eNoSuchIndex . Right
+
+call :: ParserST Part
+call = do
+    m  <- pGroupCount <$> getPattern
+    ix <- numbered m <|> named
+    pure $ Call ix
+    where
+    named = Right <$> choice
+        [ paren $ string "?&"  *> groupName ')'
+        , paren $ string "?P>" *> groupName ')'
+        , escaped $ char 'g' *> angled  (groupName '>')
+        , escaped $ char 'g' *> squoted (groupName '\'')
+        ]
+    numbered m = Left <$> choice
+        [ paren $ string "?R" *> pure 0
+        , paren $ char '?' *> decimal
+        , paren $ char '?' *> relative m
+        , escaped $ char 'g' *> angled decimal
+        , escaped $ char 'g' *> angled (relative m)
+        , escaped $ char 'g' *> squoted decimal
+        , escaped $ char 'g' *> squoted (relative m)
+        ]
+    relative m = do
+        op <- (-) <$ char '-' <|> (+) <$ char '+'
+        n  <- decimal
+        pure $ m `op` n + 1
+
+anchors :: ParserST [Anchor]
+anchors = do
+    Options {..} <- pOptions <$> getPattern
+    choice
+        [ string "^"   $> if oMultiline then mlCaret  else caret
+        , string "$"   $> if oMultiline then mlDollar else dollar
+        , string "\\A" $> [Start]
+        , string "\\Z" $> [End, EndBeforeNewline]
+        , string "\\z" $> [End]
+        , string "\\b" $> [WordBoundary, Start, End]
+        , string "\\B" $> [WordInternal]
+        , string "\\G" >>= \_ -> eNotSupported "start-of-match anchor"
+        , string "\\K" >>= \_ -> eNotSupported "start-of-match reset"
+        ]
+    where
+    caret    = [Start]
+    mlCaret  = [Start, AfterNewline]
+    dollar   = [End, EndBeforeNewline]
+    mlDollar = [End, EndBeforeNewline, BeforeNewline]
+
+conditional :: ParserST Part
+conditional = string "(?" *> paren (choice
+    [ decimal                      >> eNoConditionals
+    , char '+'    *> decimal       >> eNoConditionals
+    , char '-'    *> decimal       >> eNoConditionals
+    , char 'R'    *> decimal       >> eNoConditionals
+    , string "R&" *> groupName ')' >> eNoConditionals
+    , char 'R'                     >> eNoConditionals
+    , angled (groupName '>')       >> eNoConditionals
+    , squoted (groupName '\'')     >> eNoConditionals
+    , groupName ')'                >> eNoConditionals
+    , string "DEFINE"              >> eNoConditionals
+    , lookAround                   >> eNoConditionals
+    , takeTill (==')')             >> eMalformed
+    ])
+    where
+    eMalformed      = eParserError "malformed name or number after '(?('"
+    eNoConditionals = eNotSupported "conditionals"
+
+empty' :: ParserST Part
+empty' = pure Empty
+
+lookAround :: ParserST ()
+lookAround = lookAround' *> eNotSupported "lookarounds" <?> "lookaround"
+    where
+    lookAround' = aheadPos <|> aheadNeg <|> behindPos <|> behindNeg
+    aheadPos    = string "(?="  *> content <* char ')'
+    aheadNeg    = string "(?!"  *> content <* char ')'
+    behindPos   = string "(?<=" *> content <* char ')'
+    behindNeg   = string "(?<!" *> content <* char ')'
+    content     = many $ satisfy (/=')')
+
+verb :: ParserST ()
+verb = paren $ char '*' *> choice
+    [ string "ACCEPT"          >>  eNoBacktrackVerbs
+    , string "FAIL"            >>  eNoBacktrackVerbs
+    , string "F"               >>  eNoBacktrackVerbs
+    , string "MARK:"   *> name >>  eNoBacktrackVerbs
+    , string ":"       *> name >>  eNoBacktrackVerbs
+    , string "COMMIT:" *> name >>  eNoBacktrackVerbs
+    , string "COMMIT"          >>  eNoBacktrackVerbs
+    , string "PRUNE:"  *> name >>  eNoBacktrackVerbs
+    , string "PRUNE"           >>  eNoBacktrackVerbs
+    , string "SKIP:"   *> name >>  eNoBacktrackVerbs
+    , string "SKIP"            >>  eNoBacktrackVerbs
+    , takeTill (==')')         >>= eNoSuchVerb . show
+    ]
+    where
+    eNoBacktrackVerbs = eNotSupported "backtracking verbs"
+    name              = takeTill (/=')')
+
+globalOption :: ParserST ()
+globalOption = paren $ char '*' >> choice
+    [ () <$ string "LIMIT_MATCH="     <* decimal
+    , () <$ string "LIMIT_RECURSION=" <* decimal
+    , () <$ string "NO_AUTO_POSSESS"
+    , () <$ string "NO_START_OPT"
+    , () <$ string "BSR_ANYCRLF"
+    , string "BSR_UNICODE" >>  noUnicode
+    , string "UTF8"        >>  noUnicode
+    , string "UTF16"       >>  noUnicode
+    , string "UTF32"       >>  noUnicode
+    , string "UTF"         >>  noUnicode
+    , string "UCP"         >>  noUnicode
+    , string "CRLF"        >>  noCRLF
+    , string "ANYCRLF"     >>  setLineEnds AnyCRLF
+    , string "ANY"         >>  setLineEnds Any
+    , string "CR"          >>  setLineEnds CR
+    , string "LF"          >>  setLineEnds LF
+    , takeTill (==')')     >>= eBadOpt
+    ]
+    where
+    eBadOpt s = eParserError $ "bad global option: " ++ show s
+    noUnicode = eNotSupported "unicode"
+    noCRLF    = eNotSupported "crlf line ends"
+
+localOption :: ParserST ()
+localOption = paren $ do
+    char '?'
+    options
+    checkOptions =<< fmap pOptions getPattern
+    where
+    options       = posOpts <* char '-' <* negOpts <|> posOpts
+    posOpts       = () <$ many (option True)
+    negOpts       = () <$ many (option False)
+    setUngreedy b = setStrategy $ if b then Lazy else Greedy
+    option      b = choice
+        [ setIgnoreCase  b <* char 'i'
+        , setMultiline   b <* char 'm'
+        , setDupNames    b <* char 'J'
+        , setDotAll      b <* char 's'
+        , setUngreedy    b <* char 'U'
+        , setFreeSpacing b <* char 'x'
+        ]
+
+comment :: ParserST ()
+comment = do
+    ignoreSpace <- oFreeSpacing . pOptions <$> getPattern
+    () <$ (spaceIf ignoreSpace <|> comment')
+    where
+    spaceIf b =         guard b     *> fmap S.singleton (satisfy Char.isSpace)
+    comment'  = paren $ string "?#" *> takeTill (==')')
+
+callout :: ParserST ()
+callout = paren $
+    string "?C" *> takeTill (==')') *> eNotSupported "callouts"
+
+parts :: ParserST Part
+parts = parts' <* many comment
+    where
+    parts'     = alt anchored <|> anchored
+    anchored   = sequence `anch` anchored <|> sequence
+    sequence   = nonEmptySeq <$> many grouping
+    grouping   = quantified atom' <|> atom'
+    atom'      = silent *> atom' <|> atom
+    atom       = reference <|> conditional <|> call <|> group <|> forbidden <|> cclass <|> byte
+    silent     = localOption <|> verb <|> lookAround <|> callout <|> comment
+    alt p      = Alternative .: (:) <$> p <*> some (char '|' *> p)
+    p `anch` q = choice
+        [ Anchored <$> p      <*> anchors <*> q
+        , Anchored <$> empty' <*> anchors <*> q
+        , Anchored <$> p      <*> anchors <*> empty'
+        ]
+
+parsePatternOpts :: Options -> S.ByteString -> IO Pattern
+parsePatternOpts o bs = case parseSTOpts o parts' bs of
+    Left  e            -> X.throwIO $ EParserError e
+    Right (pattern,ps) -> do
+        checkOptions o
+        let groups   = pGroups pattern
+            topLevel = Group 0 ps
+            groups'  = Map.insert 0 topLevel groups
+            pattern' = pattern {pGroups = groups'}
+        pure pattern'
+    where
+    parts' = many globalOption *> parts <* endOfInput
+
+parsePattern :: S.ByteString -> IO Pattern
+parsePattern = parsePatternOpts defaultOptions
+
+checkOptions :: MonadThrow m => Options -> m ()
+checkOptions Options {..}
+    | oRecursion                 = eNotSupported "recursion"
+    | oDefaultStrategy /= Greedy = eNotSupported "lazy quantifiers"
+    | oDupNames                  = eNotSupported "duplicate group names"
+    | oLineEnds == Any           = eNotSupported "unicode"
+    | oLineEnds == CRLF          = eNotSupported "crlf line ends"
+    | otherwise                  = pure ()
diff --git a/src/Text/Regex/Regen/ParserST.hs b/src/Text/Regex/Regen/ParserST.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Regex/Regen/ParserST.hs
@@ -0,0 +1,238 @@
+{-# LANGUAGE RecordWildCards #-}
+module Text.Regex.Regen.ParserST where
+
+import Control.Applicative (Alternative(..))
+import Control.Monad (guard, when)
+import Control.Monad.Catch (MonadThrow(..))
+import Data.Attoparsec.Types ()
+import Data.Char (chr, ord)
+import Data.Functor (($>))
+import Numeric (showHex, showOct)
+import qualified Control.Exception as X
+import qualified Data.Attoparsec.ByteString.Char8 as P
+import qualified Data.Attoparsec.Combinator as P
+import qualified Data.ByteString.Char8 as S
+import qualified Data.Map as Map
+
+import Text.Regex.Regen.Pattern
+import Text.Regex.Regen.PatternException
+
+newtype ParserST a = ParserST
+    { runParserST :: Pattern -> P.Parser (Pattern, a) }
+instance Functor ParserST where
+    fmap f p = ParserST $ \s -> do
+        (s', a) <- runParserST p s
+        pure (s', f a)
+instance Applicative ParserST where
+    pure  a = ParserST $ \s -> pure (s, a)
+    f <*> m = ParserST $ \s -> do
+        (s',  g) <- runParserST f s
+        (s'', a) <- runParserST m s'
+        pure (s'', g a)
+instance Alternative ParserST where
+    empty   = ParserST $ \s -> mempty
+    p <|> q = ParserST $ \s -> runParserST p s <|> runParserST q s
+instance Monoid (ParserST a) where
+    mempty  = lift mempty
+    mappend = (<|>)
+instance Monad ParserST where
+    m >>= f = ParserST $ \s -> do
+        (s',  a) <- runParserST m s
+        (s'', b) <- runParserST (f a) s'
+        pure (s'', b)
+instance MonadThrow ParserST where
+    throwM e = ParserST $ \_ -> X.throw e
+
+getPattern :: ParserST Pattern
+getPattern = ParserST $ \s -> pure (s,s)
+
+putPattern :: Pattern -> ParserST ()
+putPattern s = ParserST $ \_ -> pure (s,())
+
+modifyPattern :: (Pattern -> (Pattern, a)) -> ParserST a
+modifyPattern f = ParserST $ \s -> pure (f s)
+
+modifyPattern' :: (Pattern -> Pattern) -> ParserST ()
+modifyPattern' f = modifyPattern $ \s -> (f s, ())
+
+modifyOptions :: (Options -> (Options, a)) -> ParserST a
+modifyOptions f = modifyPattern $ \s -> let (o,a) = f (pOptions s)
+    in (s { pOptions = o }, a)
+
+modifyOptions' :: (Options -> Options) -> ParserST ()
+modifyOptions' f = modifyOptions $ \o -> (f o, ())
+
+setIgnoreCase, setMultiline, setDupNames, setDotAll, setFreeSpacing :: Bool -> ParserST ()
+setIgnoreCase  b = modifyOptions' $ \o -> o { oIgnoreCase  = b }
+setMultiline   b = modifyOptions' $ \o -> o { oMultiline   = b }
+setDupNames    b = modifyOptions' $ \o -> o { oDupNames    = b }
+setDotAll      b = modifyOptions' $ \o -> o { oDotAll      = b }
+setFreeSpacing b = modifyOptions' $ \o -> o { oFreeSpacing = b }
+
+setStrategy :: Strategy -> ParserST ()
+setStrategy s = modifyOptions' $ \o -> o { oDefaultStrategy = s }
+
+setLineEnds :: LineEnds -> ParserST ()
+setLineEnds n = modifyOptions' $ \o -> o { oLineEnds = n }
+
+nextGroupNum :: ParserST Int
+nextGroupNum = modifyPattern $
+    \p -> let n = pGroupCount p + 1 in (p { pGroupCount = n }, n)
+
+addGroup :: Int -> Part -> ParserST ()
+addGroup n part = modifyPattern' $
+    \p -> p { pGroups = Map.insert n (Group n part) (pGroups p) }
+
+groupNumInUse :: Int -> ParserST Bool
+groupNumInUse n = do
+    Pattern {..} <- getPattern
+    pure $ 0 < n && n <= pGroupCount
+
+groupNameInUse :: S.ByteString -> ParserST Bool
+groupNameInUse bs = do
+    Pattern {..} <- getPattern
+    pure $ bs `elem` Map.keys pGroupNames
+
+lookupGroupIndex :: S.ByteString -> ParserST (Maybe Int)
+lookupGroupIndex bs = do
+    Pattern {..} <- getPattern
+    pure $ Map.lookup bs pGroupNames
+
+addNamedGroup :: S.ByteString -> Int -> Part -> ParserST ()
+addNamedGroup bs n part = do
+    inUse <- groupNameInUse bs
+    when inUse $ eDupGroupName bs
+    addGroup n part
+    modifyPattern' $
+        \p -> p { pGroupNames = Map.insert bs n (pGroupNames p) }
+
+lift :: P.Parser a -> ParserST a
+lift p = ParserST $ \s -> (,) s <$> p
+
+char :: Char -> ParserST Char
+char = lift . P.char
+
+string :: S.ByteString -> ParserST S.ByteString
+string = lift . P.string
+
+satisfy :: (Char -> Bool) -> ParserST Char
+satisfy = lift . P.satisfy
+
+endOfInput :: ParserST ()
+endOfInput = lift P.endOfInput
+
+cInRange :: Char -> Char -> ParserST Char
+cInRange l u = satisfy $ \c -> l <= c && c <= u
+
+decimal :: Integral a => ParserST a
+decimal = lift P.decimal
+
+lookAhead :: ParserST a -> ParserST a
+lookAhead p = ParserST $ \s -> P.lookAhead (runParserST p s)
+
+choice :: [ParserST a] -> ParserST a
+choice ps = ParserST $ \s -> P.choice (ps' s)
+    where
+    ps' s = (\p -> runParserST p s) <$> ps
+
+infix 0 <?> 
+(<?>) :: ParserST a -> String -> ParserST a
+p <?> t = ParserST $ \s -> runParserST p s P.<?> t
+
+peekChar :: ParserST (Maybe Char)
+peekChar = lift P.peekChar
+
+notFollowedBy :: ParserST a -> Char -> ParserST a
+p `notFollowedBy` c = do
+    x <- p
+    m <- peekChar
+    guard $ m /= Just c
+    return x
+
+peek :: ParserST a -> ParserST (Maybe a)
+peek p = (Just <$> lookAhead p) <|> return Nothing
+
+till :: ParserST a -> S.ByteString -> ParserST [a]
+till p s = reverse <$> go []
+    where
+    go acc = do
+        mb <- peek $ string s
+        case mb of
+            Just t | t == s -> string s $> acc
+            _               -> p >>= \a -> go $ a:acc
+
+takeWhile :: (Char -> Bool) -> ParserST S.ByteString
+takeWhile p = lift $ P.takeWhile p
+
+takeTill :: (Char -> Bool) -> ParserST S.ByteString
+takeTill p = lift $ P.takeTill p
+
+paren, bracketed, braced, angled, squoted, escaped :: ParserST a -> ParserST a
+paren     p = char '('  *> p <* char ')'
+braced    p = char '{'  *> p <* char '}'
+bracketed p = char '['  *> p <* char ']'
+angled    p = char '<'  *> p <* char '>'
+squoted   p = char '\'' *> p <* char '\''
+escaped   p = char '\\' *> p
+
+digit, letter :: ParserST Char
+digit  = lift P.digit
+letter = lift P.letter_ascii
+
+hexByte :: ParserST Char
+hexByte = inRange $ char 'x' *> (byte' <|> braced byte' <|> pure '\0')
+    where
+    byte'      = byte2 <|> byte1
+    byte2      = decode <$> hexDigit <*> hexDigit
+    byte1      = decode <$> pure 0   <*> hexDigit
+    decode a b = chr $ (a * 0x10) + b
+    hexDigit   = hexDigit09 <|> hexDigitaz <|> hexDigitAZ
+    hexDigit09 = (\c -> ord c - ord '0' + 0x0) <$> cInRange '0' '9'
+    hexDigitaz = (\c -> ord c - ord 'a' + 0xa) <$> cInRange 'a' 'z'
+    hexDigitAZ = (\c -> ord c - ord 'A' + 0xA) <$> cInRange 'A' 'Z'
+
+octalByte :: ParserST Char
+octalByte = inRange $ byte' <|> char 'o' *> (braced byte' <|> eMissingBrace)
+    where
+    octDigit      = (\c -> ord c - ord '0') <$> cInRange '0' '7'
+    byte'         = byte3 <|> byte2 <|> byte1
+    byte3         = decode <$> octDigit <*> octDigit <*> octDigit
+    byte2         = decode <$> pure 0   <*> octDigit <*> octDigit
+    byte1         = decode <$> pure 0   <*> pure 0   <*> octDigit
+    decode a b c  = chr $ (a * 64) + (b * 8) + c
+    eMissingBrace = eParserError "missing '{' after '\\o'"
+
+inRange :: ParserST Char -> ParserST Char
+inRange p = p >>= \c ->
+    if '\x00' <= c && c <= '\x7f'
+        then pure c
+        else eParserError
+            $ showString "octal and hex escapes must be in range "
+            $ showRange '\x00' '\x7f' ""
+    where
+    showHex'  c   = showString "0x" . zeroPad 2 (showHex (ord c) "")
+    showOct'  c   = zeroPad 3 $ showOct (ord c) ""
+    zeroPad   n s = showString (replicate (n - length s) '0') . showString s
+    showHexR  c d = showHex' c . showString "-" . showHex' d
+    showOctR  c d = showOct' c . showString "-" . showOct' d
+    showRange c d = showHexR c d . showString " (" . showOctR c d . showString ")"
+
+cescape :: ParserST Char
+cescape = choice
+    [ '\t'   <$ char 't'
+    , '\x1b' <$ char 'e'
+    , '\n'   <$ char 'n'
+    , '\f'   <$ char 'f'
+    , '\r'   <$ char 'r'
+    , '\a'   <$ char 'a'
+    , '\v'   <$ char 'v'
+    , '\\'   <$ char '\\'
+    , char 'c' *> eNotSupported "'\\cx' escapes"
+    ]
+
+parseST :: ParserST a -> S.ByteString -> Either String (Pattern, a)
+parseST = parseSTOpts defaultOptions
+
+parseSTOpts :: Options -> ParserST a -> S.ByteString
+            -> Either String (Pattern, a)
+parseSTOpts o p = P.parseOnly $ runParserST p (emptyPattern o)
diff --git a/src/Text/Regex/Regen/Pattern.hs b/src/Text/Regex/Regen/Pattern.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Regex/Regen/Pattern.hs
@@ -0,0 +1,136 @@
+{-# LANGUAGE RecordWildCards #-}
+module Text.Regex.Regen.Pattern where
+
+import Control.Monad.Catch (MonadThrow)
+import Data.Map (Map)
+import Data.Monoid ((<>))
+import qualified Data.ByteString.Char8 as S
+import qualified Data.Map as Map
+
+import Text.Regex.Regen.PatternException
+
+data Pattern = Pattern
+    { pGroups     :: !(Map Int Group)
+    , pGroupNames :: !(Map S.ByteString Int)
+    , pGroupCount :: !Int
+    , pOptions    :: !Options
+    } deriving (Show, Eq)
+
+emptyPattern :: Options -> Pattern
+emptyPattern = Pattern Map.empty Map.empty 0
+
+lookupGroupByNum :: Int -> Pattern -> Maybe Group
+lookupGroupByNum n Pattern {..} = Map.lookup n pGroups
+
+lookupGroupByName :: S.ByteString -> Pattern -> Maybe Group
+lookupGroupByName bs Pattern {..} = do
+    n <- Map.lookup bs pGroupNames
+    Map.lookup n pGroups
+
+data Part
+    = Empty
+    | Byte Char
+    | Sequence [Part]
+    | CClass Bool [Char]
+    | Quantified Part Range Strategy
+    | Alternative [Part]
+    | Reference Int
+    | Call (Either Int S.ByteString)
+    | Anchored Part [Anchor] Part
+    deriving (Show, Eq)
+instance Monoid Part where
+    mempty = Empty
+    Empty        `mappend` p            = p
+    p            `mappend` Empty        = p
+    Sequence ps1 `mappend` Sequence ps2 = nonEmptySeq $ ps1 <> ps2
+    Sequence ps  `mappend` p            = nonEmptySeq $ reverse (p : reverse ps)
+    p            `mappend` Sequence ps  = nonEmptySeq $ p : ps
+    p1           `mappend` p2           = Sequence [p1,p2]
+
+data Anchor
+    = WordBoundary
+    | WordInternal
+    | Start
+    | End
+    | EndBeforeNewline
+    | BeforeNewline
+    | AfterNewline
+    | StartOfMatch
+    deriving (Show, Eq)
+
+data Group = Group
+    { gIndex :: !Int
+    , gParts :: !Part
+    } deriving (Show, Eq)
+
+data Range = Range
+    { rMin :: !Int
+    , rMax :: !(Maybe Int)
+    } deriving (Show, Eq)
+
+oLineEndChars :: Options -> [Char]
+oLineEndChars o = case oLineEnds o of
+    CR      -> ['\r']
+    LF      -> ['\n']
+    CRLF    -> []
+    AnyCRLF -> ['\r','\n']
+    Any     -> ['\r','\n']
+
+nonEmptyAltM :: MonadThrow m => [Part] -> m Part
+nonEmptyAltM [] = eUnsatisfiable
+nonEmptyAltM ps = pure $ Alternative ps
+
+nonEmptyCClassM :: MonadThrow m => (Char -> Bool) -> [Char] -> m Part
+nonEmptyCClassM p cs = case filter p cs of
+    []  -> eUnsatisfiable
+    cs' -> pure $ CClass True cs'
+
+nonEmptyQuantM :: MonadThrow m => Part -> Range -> Strategy -> m Part
+nonEmptyQuantM p (Range 0 (Just 0)) s = eUnsatisfiable
+nonEmptyQuantM p (Range 0 m)        s = pure $ Quantified p (Range 1 m) s
+nonEmptyQuantM p r                  s = pure $ Quantified p r           s
+
+nonEmptyAlt :: [Part] -> Part
+nonEmptyAlt [] = Empty
+nonEmptyAlt ps = Alternative ps
+
+nonEmptySeq :: [Part] -> Part
+nonEmptySeq [] = Empty
+nonEmptySeq ps = Sequence ps
+
+data Options = Options
+    { oDefaultStrategy :: !Strategy
+    , oMultiline       :: !Bool
+    , oIgnoreCase      :: !Bool
+    , oLineEnds        :: !LineEnds
+    , oRecursion       :: !Bool
+    , oDotAll          :: !Bool
+    , oFreeSpacing     :: !Bool
+    , oDupNames        :: !Bool
+    } deriving (Show, Eq)
+
+defaultOptions :: Options
+defaultOptions = Options
+    { oDefaultStrategy = Greedy
+    , oMultiline       = False
+    , oIgnoreCase      = False
+    , oLineEnds        = AnyCRLF
+    , oRecursion       = False
+    , oDotAll          = False
+    , oFreeSpacing     = False
+    , oDupNames        = False
+    }
+
+data LineEnds
+    = CR      -- \r
+    | LF      -- \n
+    | CRLF    -- \r\n
+    | AnyCRLF -- (\r|\n|\r\n)
+    | Any     -- Any unicode newline sequence
+    deriving (Show, Eq)
+
+data Strategy
+    = Greedy
+    | Lazy
+    | Possessive
+    deriving (Show, Eq)
diff --git a/src/Text/Regex/Regen/PatternException.hs b/src/Text/Regex/Regen/PatternException.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Regex/Regen/PatternException.hs
@@ -0,0 +1,98 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+module Text.Regex.Regen.PatternException where
+
+import Control.Monad.Catch (MonadThrow(..))
+import Data.Typeable (Typeable(..))
+import qualified Control.Exception as X
+import qualified Data.ByteString.Char8 as S
+
+data PatternException
+    = EParserError  String
+    | EGenError     String
+    | ENotSupported String
+    | ENoSuchClass  String
+    | ENoSuchVerb   String
+    | EBadGroupName S.ByteString
+    | EDupGroupName S.ByteString
+    | ENoSuchIndex  (Either Int S.ByteString)
+    | EClassOutOfOrder
+    | EQuantOutOfOrder
+    deriving (Eq, Typeable)
+instance Show PatternException where
+    show (EParserError  s) = "parser error: " ++ s
+    show (EGenError     s) = "generator error: " ++ s
+    show (ENotSupported s) = s ++ " not supported"
+    show (ENoSuchClass  s) = "unknown POSIX class name: " ++ show s
+    show (ENoSuchVerb   s) = "verb not recognized or malformed: " ++ s
+    show (EBadGroupName s) = "bad sub-pattern name: " ++ show s
+    show (EDupGroupName s) = "duplicate sub-pattern name: " ++ show s
+    show (ENoSuchIndex  e) = "non-existent sub-pattern: " ++ either show show e
+    show  EClassOutOfOrder = "out of order range in character class"
+    show  EQuantOutOfOrder = "numbers out of order in quantifier"
+instance X.Exception PatternException
+
+eParserError, eGenError, eNotSupported, eNoSuchClass, eNoSuchVerb :: MonadThrow m => String -> m a
+eParserError  = throwM . EParserError
+eGenError     = throwM . EGenError
+eNotSupported = throwM . ENotSupported
+eNoSuchClass  = throwM . ENoSuchClass
+eNoSuchVerb   = throwM . ENoSuchVerb
+
+eBadGroupName, eDupGroupName :: MonadThrow m => S.ByteString -> m a
+eBadGroupName = throwM . EBadGroupName
+eDupGroupName = throwM . EDupGroupName
+
+eNoSuchIndex :: MonadThrow m => Either Int S.ByteString -> m a
+eNoSuchIndex = throwM . ENoSuchIndex
+
+eClassOutOfOrder, eQuantOutOfOrder, eUnsatisfiable :: MonadThrow m => m a
+eClassOutOfOrder = throwM EClassOutOfOrder
+eQuantOutOfOrder = throwM EQuantOutOfOrder
+eUnsatisfiable   = eGenError "pattern unsatisfiable"
+
+isPatternException :: X.Exception e => e -> Bool
+isPatternException e = case X.fromException (X.toException e) of
+    Just (pe::PatternException) -> True
+    Nothing                     -> False
+
+isEParserError :: PatternException -> Bool
+isEParserError (EParserError _) = True
+isEParserError _                = False
+
+isEGenError :: PatternException -> Bool
+isEGenError (EGenError _) = True
+isEGenError _             = False
+
+isENotSupported :: PatternException -> Bool
+isENotSupported (ENotSupported _) = True
+isENotSupported _                 = False
+
+isENoSuchClass :: PatternException -> Bool
+isENoSuchClass (ENoSuchClass _) = True
+isENoSuchClass _                = False
+
+isEBadGroupName :: PatternException -> Bool
+isEBadGroupName (EBadGroupName _) = True
+isEBadGroupName _                 = False
+
+isEDupGroupName :: PatternException -> Bool
+isEDupGroupName (EDupGroupName _) = True
+isEDupGroupName _                 = False
+
+isENoSuchVerb :: PatternException -> Bool
+isENoSuchVerb (ENoSuchVerb _) = True
+isENoSuchVerb _               = False
+
+isENoSuchIndex :: PatternException -> Bool
+isENoSuchIndex (ENoSuchIndex _) = True
+isENoSuchIndex _                = False
+
+isEClassOutOfOrder :: PatternException -> Bool
+isEClassOutOfOrder EClassOutOfOrder = True
+isEClassOutOfOrder _                = False
+
+isEQuantOutOfOrder :: PatternException -> Bool
+isEQuantOutOfOrder EQuantOutOfOrder = True
+isEQuantOutOfOrder _                = False
+
diff --git a/src/Text/Regex/Regen/Util.hs b/src/Text/Regex/Regen/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Regex/Regen/Util.hs
@@ -0,0 +1,31 @@
+module Text.Regex.Regen.Util where
+
+import Control.Monad.Catch (Exception, MonadCatch, SomeException, try)
+import Data.Either (either)
+
+mwhen :: Monoid m => Bool -> m -> m
+mwhen b m = if b then m else mempty
+
+munless :: Monoid m => Bool -> m -> m
+munless b m = if b then mempty else m
+
+infixr 8 .:
+(.:) :: (c -> d) -> (a -> b -> c) -> a -> b -> d
+(.:) = (.).(.)
+
+infixr 3 &&&
+(&&&) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
+p &&& q = \a -> p a && q a
+
+infixr 2 |||
+(|||) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
+p ||| q = \a -> p a || q a
+
+hush :: Either e a -> Maybe a
+hush = either (const Nothing) Just
+
+tryMaybe :: MonadCatch m => m a -> m (Maybe a)
+tryMaybe m = hush <$> try' m
+    where
+    try' :: MonadCatch m => m a -> m (Either SomeException a)
+    try' = try
diff --git a/test/Gen.hs b/test/Gen.hs
new file mode 100644
--- /dev/null
+++ b/test/Gen.hs
@@ -0,0 +1,345 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Control.Exception (evaluate)
+import System.Random
+import Test.Hspec
+import Test.HUnit.Lang
+import Control.Monad (when)
+import Text.Regex.PCRE
+import qualified Data.ByteString.Char8 as S
+
+import Text.Regex.Regen
+
+shouldGenOpts :: StdGen -> Options -> S.ByteString -> Expectation
+shouldGenOpts gen o reg = do
+        p <- parsePatternOpts o reg
+        case let (e,_) = generatePattern p gen in e of
+            Left e   -> assertFailure (eThrown e)
+            Right bs -> do
+                bs' <- evaluate $ bs =~ reg
+                when (bs /= bs') $ assertFailure (eUnexp bs bs')
+    where
+    eThrown e     = "`generate` threw exception on " ++ show reg ++ ": " ++ show e
+    eUnexp bs bs' = show reg ++ " generated " ++ show bs ++ " but matched " ++ show bs'
+
+main :: IO ()
+main = do
+    seed <- randomIO
+    let gen       = mkStdGen seed
+        shouldGen = shouldGenOpts gen defaultOptions
+    hspec $ test seed shouldGen
+
+test :: Int -> (S.ByteString -> Expectation) -> Spec
+test seed f = describe ("generate (seed = " ++ show seed ++ ")") $ mapM_ ($f)
+    [ literals
+    , classes
+    , subpatterns
+    , quantifiers
+    , anchors
+    , globalOpts
+    , localOpts
+    , verbs
+    , callouts
+    , lookarounds
+    , calls
+    ]
+
+literals :: (S.ByteString -> Expectation) -> Spec
+literals shouldGen = describe "handles literals and escapes" $ do
+    it "accepts a literal string"   $ shouldGen "foo"
+    it "accepts an escaped literal" $ shouldGen "\\F\\H\\8\\9"
+    it "accepts hex escapes"        $ shouldGen "\\x0\\x61\\x{62}\\x7f"
+    it "accepts octal escapes"      $ shouldGen "\\0\\o{141}\\177"
+    it "accepts whitespace escape"  $ shouldGen "\\R"
+    it "accepts C escapes"          $ shouldGen "\\a\\n\\f\\t"
+    it "accepts escaped specials"   $ shouldGen "\\[\\(\\|\\$\\^\\.\\*"
+    it "rejects out-of-range hex escapes" $
+        generate "\\x80" `shouldThrow` isEParserError
+    it "rejects out-of-range octal escapes" $
+        generate "\\200" `shouldThrow` isEParserError
+    it "rejects '\\C'" $
+        generate "\\C" `shouldThrow` isENotSupported
+    it "rejects '\\X'" $
+        generate "\\X" `shouldThrow` isENotSupported
+    it "rejects '\\L'" $
+        generate "\\L" `shouldThrow` isENotSupported
+    it "rejects '\\l'" $
+        generate "\\l" `shouldThrow` isENotSupported
+    it "rejects '\\U'" $
+        generate "\\U" `shouldThrow` isENotSupported
+    it "rejects '\\u'" $
+        generate "\\u" `shouldThrow` isENotSupported
+    it "rejects '\\Q'" $
+        generate "\\Q" `shouldThrow` isENotSupported
+    it "rejects '\\E'" $
+        generate "\\E" `shouldThrow` isENotSupported
+    it "rejects '\\o'" $
+        generate "\\o" `shouldThrow` isEParserError
+    it "rejects '\\cx'" $
+        generate "\\c" `shouldThrow` isENotSupported
+
+classes :: (S.ByteString -> Expectation) -> Spec
+classes shouldGen = describe "handles character classes" $ do
+    it "accepts '.'"                    $ shouldGen "."
+    it "accepts character class"        $ shouldGen "[asdf]"
+    it "accepts negated class"          $ shouldGen "[^asdf]"
+    it "accepts class with '-'"         $ shouldGen "[-]"
+    it "accepts negated class with '-'" $ shouldGen "[^-]"
+    it "accepts class with ']'"         $ shouldGen "[]]"
+    it "accepts negated class with ']'" $ shouldGen "[^]]"
+    it "accepts class range"            $ shouldGen "[a-c]"
+    it "accepts multiple-range class"   $ shouldGen "[a-cd-f]"
+    it "accepts negated class range"    $ shouldGen "[^A-z]"
+    it "accepts canned range in class"  $ shouldGen "[\\d]"
+    it "accepts negated canned range"   $ shouldGen "[^\\d]"
+    it "accepts posix class"            $ shouldGen "[[:alpha:]]"
+    it "accepts negated posix class"    $ shouldGen "[^[:alpha:]]"
+    it "accepts class with everything"  $ shouldGen "[]\\wxa-b[:digit:]-]"
+    it "accepts partial posix class"    $ shouldGen "[[:f]"
+    it "accepts class with repeats"     $ shouldGen "[ooo]"
+    it "accepts class with escapes"     $ shouldGen "[\\x61\\141\\0\\n\\A]"
+    it "accepts canned classes"         $ shouldGen "\\d\\D\\w\\W"
+    it "rejects malformed class ranges" $
+        generate "[z-a]" `shouldThrow` isEClassOutOfOrder
+    it "rejects good property ranges" $
+        generate "\\p{Ll}" `shouldThrow` isENotSupported
+    it "rejects bad property ranges" $
+        generate "\\p{bad}" `shouldThrow` isENotSupported
+    it "rejects unicode '\\X'" $
+        generate "\\X" `shouldThrow` isENotSupported
+    it "rejects unicode '\\C'" $
+        generate "\\C" `shouldThrow` isENotSupported
+    it "rejects posix range out of class" $
+        generate "[:alpha:]" `shouldThrow` isENotSupported
+    it "rejects bad posix classes" $
+        generate "[[:foo:]]" `shouldThrow` isENoSuchClass
+
+subpatterns :: (S.ByteString -> Expectation) -> Spec
+subpatterns shouldGen = describe "handles subpatterns" $ do
+    it "accepts numbered groups"               $ shouldGen "(x)\\1"
+    it "accepts each numbered ref syntax"      $ shouldGen "(x)\\1\\g1\\g{1}"
+    it "accepts nested numbered groups"        $ shouldGen "((x)y)\\1\\2"
+    it "accepts named groups"                  $ shouldGen "(?P<foo>x)(?P=foo)"
+    it "accepts numbered refs to named groups" $ shouldGen "(?P<foo>x)\\1"
+    it "accepts non-capturing groups"          $ shouldGen "(?:foo)(x)\\1"
+    it "accepts atomic groups"                 $ shouldGen "(?>foo)(x)\\1"
+    it "accepts relative group refs" $
+        shouldGen "(x)(y)(z)\\g{-2}\\g{-1}\\g{-3}"
+    it "accepts nested named groups" $
+        shouldGen "(?P<xy>(?P<x>x)y)(?P=xy)(?P=x)"
+    it "accepts each named group syntax" $
+        shouldGen "(?<x>x)(?'y'y)(?P<z>z)(?P=x)(?P=z)(?P=y)"
+    it "accepts each named ref syntax" $
+        shouldGen "(?<x>x)\\k'x'\\k<x>\\k{x}\\g{x}(?P=x)"
+    it "rejects duplicate ref names" $
+        generate "(?<x>x)(?<x>y)" `shouldThrow` isEDupGroupName
+    it "rejects refs to non-existent group numbers" $
+        generate "\\g1" `shouldThrow` isENoSuchIndex
+    it "rejects refs to non-existent group names" $
+        generate "(?P=foo)" `shouldThrow` isENoSuchIndex
+    it "rejects bad group names" $
+        generate "(?P<9foo>)" `shouldThrow` isEBadGroupName
+    it "rejects bad group names in references" $
+        generate "(?P=9foo)" `shouldThrow` isEBadGroupName
+    it "rejects reset groups" $
+        generate "(?|foo)" `shouldThrow` isENotSupported
+
+quantifiers :: (S.ByteString -> Expectation) -> Spec
+quantifiers shouldGen = describe "handles quantifiers" $ do
+    it "accepts fixed quantifiers"         $ shouldGen "a{5}"
+    it "accepts bounded quantifiers"       $ shouldGen "a{1,3}"
+    it "accepts upper-bounded quantifiers" $ shouldGen "a{,3}"
+    it "accepts lower-bounded quantifiers" $ shouldGen "a{3,}"
+    it "accepts star quantifer"            $ shouldGen "a*"
+    it "accepts plus quantifer"            $ shouldGen "a+"
+    it "accepts question-mark quantifier"  $ shouldGen "a?"
+    it "accepts quantified char classes"   $ shouldGen "[asdf]{1,3}"
+    it "accepts quantified C escapes"      $ shouldGen "\\a{1,3}"
+    it "accepts quantified canned classes" $ shouldGen "\\d{1,3}"
+    it "accepts quantified hex escapes"    $ shouldGen "\\x61{1,3}"
+    it "accepts quantified octal escapes"  $ shouldGen "\\141{1,3}"
+    it "accepts quantified dot"            $ shouldGen ".{1,3}"
+    it "accepts quantified groups"         $ shouldGen "(asdf){1,3}"
+    it "rejects lazy quantifiers" $
+        generate "a{1,3}?a" `shouldThrow` isENotSupported
+    it "rejects possessive quantifiers" $
+        generate "a{1,3}+a" `shouldThrow` isENotSupported
+    it "rejects out-of-order ranges in quantifiers" $
+        generate "a{3,1}" `shouldThrow` isEQuantOutOfOrder
+    it "rejects naked quantifiers" $
+        generate "{1,3}" `shouldThrow` isEParserError
+    it "rejects quantifier after quantifier" $
+        generate "a{1,3}{1,3}" `shouldThrow` isEParserError
+    it "rejects quantifier after anchor" $
+        generate "^{1,3}" `shouldThrow` isEParserError
+
+anchors :: (S.ByteString -> Expectation) -> Spec
+anchors shouldGen = describe "handles anchors" $ do
+    describe "carat" $ do
+        it "works at start of pattern" $ shouldGen "^foo"
+        it "works in alternation"      $ shouldGen "aa|^zz"
+        it "fails within pattern" $
+            generate "a^" `shouldThrow` isEGenError
+    describe "dollar" $ do
+        it "works at end of pattern" $ shouldGen "foo$"
+        it "works in alternation"    $ shouldGen "aa$|zz"
+        it "works before newlines"   $ shouldGen "foo$\\n\\n"
+        it "fails within pattern" $
+            generate "$a" `shouldThrow` isEGenError
+    describe "\\A" $ do
+        it "works at start of pattern" $ shouldGen "\\Afoo"
+        it "works in alteration"       $ shouldGen "aa|\\Azz"
+        it "fails within pattern" $
+            generate "a\\A" `shouldThrow` isEGenError
+    describe "\\Z" $ do
+        it "works at end of pattern" $ shouldGen "foo\\Z"
+        it "works in alternation"    $ shouldGen "aa\\Z|zz"
+        it "fails within pattern" $
+            generate "\\Za" `shouldThrow` isEGenError
+    describe "\\z" $ do
+        it "works at end of pattern" $ shouldGen "foo\\z"
+        it "works in alternation"    $ shouldGen "aa\\z|zz"
+        it "fails within pattern" $
+            generate "\\za" `shouldThrow` isEGenError
+    describe "\\b" $ do
+        it "works before space"        $ shouldGen "foo\\b bar"
+        it "works after space"         $ shouldGen "foo \\bbar"
+        it "works at start of pattern" $ shouldGen "\\baa"
+        it "works at end of pattern"   $ shouldGen "zz\\b"
+        it "fails within word" $
+            generate "foo\\bbar" `shouldThrow` isEGenError
+    describe "\\B" $ do
+        it "works within word"         $ shouldGen "foo\\Bbar"
+        it "fails before space" $
+            generate "foo\\B bar" `shouldThrow` isEGenError
+        it "fails after space" $
+            generate "foo \\Bbar" `shouldThrow` isEGenError
+        it "fails at start of pattern" $
+            generate "\\Baa" `shouldThrow` isEGenError
+        it "fails at end of pattern" $
+            generate "zz\\B" `shouldThrow` isEGenError
+    describe "\\G" $ do
+        it "is unsupported" $
+            generate "foo\\G." `shouldThrow` isENotSupported
+
+globalOpts :: (S.ByteString -> Expectation) -> Spec
+globalOpts shouldGen = describe "global options" $ do
+    it "ignores 'LIMIT_MATCH='"     $ shouldGen "(*LIMIT_MATCH=10)foo"
+    it "ignores 'LIMIT_RECURSION='" $ shouldGen "(*LIMIT_RECURSION=10)foo"
+    it "ignores 'NO_AUTO_POSSESS'"  $ shouldGen "(*NO_AUTO_POSSESS)foo"
+    it "ignores 'NO_START_OPT'"     $ shouldGen "(*NO_START_OPT)foo"
+    it "ignores 'BSR_ANYCRLF'"      $ shouldGen "(*BSR_ANYCRLF)foo"
+    it "allows 'LF'"                $ shouldGen "(*LF)foo"
+    it "allows 'CR'"                $ shouldGen "(*CR)foo"
+    it "allows 'ANY'"               $ shouldGen "(*ANY)foo"
+    it "allows 'ANYCRLF'"           $ shouldGen "(*ANYCRLF)foo"
+    it "rejects 'CRLF'" $
+        generate "(*CRLF)foo" `shouldThrow` isENotSupported
+    it "rejects 'BSR_UNICODE'" $
+        generate "(*BSR_UNICODE)foo" `shouldThrow` isENotSupported
+    it "rejects 'UTF8'" $
+        generate "(*UTF8)foo" `shouldThrow` isENotSupported
+    it "rejects 'UTF16'" $
+        generate "(*UTF16)foo" `shouldThrow` isENotSupported
+    it "rejects 'UTF32'" $
+        generate "(*UTF32)foo" `shouldThrow` isENotSupported
+    it "rejects 'UTF'" $
+        generate "(*UTF)foo" `shouldThrow` isENotSupported
+    it "rejects 'UCP'" $
+        generate "(*UCP)foo" `shouldThrow` isENotSupported
+    it "rejects bad global options" $
+        generate "(*BLAH)foo" `shouldThrow` isEParserError
+
+localOpts :: (S.ByteString -> Expectation) -> Spec
+localOpts shouldGen = describe "local options" $ do
+    it "allows setting ignore-case"     $ shouldGen "(?i)asdf[asdf]{5}"
+    it "allows unsetting ignore-case"   $ shouldGen "(?i)asdf(?-i)asdf[asdf]{5}"
+    it "allows unsetting multiline"     $ shouldGen "(?-m)foo"
+    it "allows unsetting dup-groups"    $ shouldGen "(?-J)foo"
+    it "allows setting dotall"          $ shouldGen "(?s).{100}"
+    it "allows unsetting dotall"        $ shouldGen "(?s).{100}(?-s).{100}"
+    it "allows unsetting ungreedy"      $ shouldGen "(?-U)foo"
+    it "allows setting free-spacing"    $ shouldGen "(?x)a s\\td\\nf(?#x)g"
+    it "allows unsetting free-spacing"  $ shouldGen "(?x)a s(?-x)a s\\td\\nf(?#x)g"
+    it "allows setting multi options"   $ shouldGen "(?ix)a s\\td\\nf(?#x)g"
+    it "allows unsetting multi options" $ shouldGen "(?ix)(?-ix)a s\\td\\nf(?#x)g"
+    it "allows setting and unsetting"   $ shouldGen "(?i)(?x-i)a s\\td\\nf(?#x)g"
+    it "rejects setting dup-groups" $
+        generate "(?J)foo" `shouldThrow` isENotSupported
+    it "rejects setting ungreedy" $
+        generate "(?U)foo" `shouldThrow` isENotSupported
+
+verbs :: (S.ByteString -> Expectation) -> Spec
+verbs _ = describe "backtracking verbs" $ do
+    it "rejects 'ACCEPT' verb" $
+        generate "foo(*ACCEPT)bar" `shouldThrow` isENotSupported
+    it "rejects 'FAIL' verb" $
+        generate "foo(*FAIL)bar" `shouldThrow` isENotSupported
+    it "rejects 'F' verb" $
+        generate "foo(*F)bar" `shouldThrow` isENotSupported
+    it "rejects 'COMMIT' verb" $
+        generate "foo(*COMMIT)bar" `shouldThrow` isENotSupported
+    it "rejects 'PRUNE' verb" $
+        generate "foo(*PRUNE)bar" `shouldThrow` isENotSupported
+    it "rejects 'SKIP' verb" $
+        generate "foo(*SKIP)bar" `shouldThrow` isENotSupported
+    it "rejects 'MARK:' verb" $
+        generate "foo(*MARK:blah)bar" `shouldThrow` isENotSupported
+    it "rejects ':' verb" $
+        generate "foo(*:blah)bar" `shouldThrow` isENotSupported
+    it "rejects 'COMMIT:' verb" $
+        generate "foo(*COMMIT:blah)bar" `shouldThrow` isENotSupported
+    it "rejects 'PRUNE:' verb" $
+        generate "foo(*PRUNE:blah)bar" `shouldThrow` isENotSupported
+    it "rejects 'SKIP:' verb" $
+        generate "foo(*SKIP:blah)bar" `shouldThrow` isENotSupported
+
+callouts :: (S.ByteString -> Expectation) -> Spec
+callouts _ = describe "callouts" $ do
+    it "rejects callouts" $
+        generate "(?C)" `shouldThrow` isENotSupported
+    it "rejects callouts with arguments" $
+        generate "(?C42)" `shouldThrow` isENotSupported
+
+lookarounds :: (S.ByteString -> Expectation) -> Spec
+lookarounds _ = describe "lookarounds" $ do
+    it "rejects positive lookahead" $
+        generate "(?=blah)" `shouldThrow` isENotSupported
+    it "rejects negative lookahead" $
+        generate "(?!blah)" `shouldThrow` isENotSupported
+    it "rejects positive lookbehind" $
+        generate "(?<=blah)" `shouldThrow` isENotSupported
+    it "rejects negative lookbehind" $
+        generate "(?<!blah)" `shouldThrow` isENotSupported
+
+calls :: (S.ByteString -> Expectation) -> Spec
+calls _ = describe "pattern calls" $ do
+    it "rejects global recursive calls" $
+        generate "(?R)" `shouldThrow` isENotSupported
+    it "rejects absolutely numbered calls with (?n)" $
+        generate "(...)(?1)" `shouldThrow` isENotSupported
+    it "rejects relatively numbered calls with (?-n)" $
+        generate "(...)(?-1)" `shouldThrow` isENotSupported
+    it "rejects relatively numbered calls with (?+n)" $
+        generate "(x)(?+1)(...)" `shouldThrow` isENotSupported
+    it "rejects absolutely numbered calls with \\g'n'" $
+        generate "(...)\\g'1'" `shouldThrow` isENotSupported
+    it "rejects relatively numbered calls with \\g'-n'" $
+        generate "(...)\\g'-1'" `shouldThrow` isENotSupported
+    it "rejects relatively numbered calls with \\g'+n'" $
+        generate "(x)\\g'+1'(...)" `shouldThrow` isENotSupported
+    it "rejects absolutely numbered calls with \\g<n>" $
+        generate "(...)\\g<1>" `shouldThrow` isENotSupported
+    it "rejects relatively numbered calls with \\g<-n>" $
+        generate "(...)\\g<-1>" `shouldThrow` isENotSupported
+    it "rejects relatively numbered calls with \\g<+n>" $
+        generate "(x)\\g<+1>(...)" `shouldThrow` isENotSupported
+    it "rejects named calls with (?&)" $
+        generate "(?<x>...)(?&x)" `shouldThrow` isENotSupported
+    it "rejects named calls with (?P>)" $
+        generate "(?<x>...)(?P>x)" `shouldThrow` isENotSupported
+    it "rejects named calls with \\g''" $
+        generate "(?<x>...)\\g'x'" `shouldThrow` isENotSupported
+    it "rejects named calls with \\g<>" $
+        generate "(?<x>...)\\g<x>" `shouldThrow` isENotSupported
