packages feed

replace-megaparsec 1.1.5.0 → 1.2.0.0

raw patch · 9 files changed

+321/−20 lines, 9 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Replace.Megaparsec.Internal.ByteString: sepCapByteString :: forall e s m a. (MonadParsec e s m, s ~ ByteString) => m a -> m [Either (Tokens s) a]
+ Replace.Megaparsec.Internal.Text: sepCapText :: forall e s m a. (MonadParsec e s m, s ~ Text) => m a -> m [Either (Tokens s) a]

Files

CHANGELOG.md view
@@ -1,20 +1,44 @@ # Revision history for replace-megaparsec -## 1.0.0.0 -- 2019-08-24+## 1.2.0.0 -- 2019-10-31 -* First version.+Benchmark improvements -## 1.0.1.0 -- 2019-08-28+Specializations of the `sepCap` function, guided by+[replace-benchmark](https://github.com/jamesdbrock/replace-benchmark). -* Add test suite.-* `sepCap` will treats `sep` as failing if it succeeds but consumes no input.+### New benchmarks +| Program                                           | dense     | sparse   |+| :---                                              |      ---: |     ---: |+| `Replace.Megaparsec.streamEdit` `String`          | 454.95ms  | 375.04ms |+| `Replace.Megaparsec.streamEdit` `ByteString`      | 529.99ms  | 73.76ms  |+| `Replace.Megaparsec.streamEdit` `Text`            | 547.47ms  | 139.21ms |++### Old benchmarks++| Program                                           | dense     | sparse   |+| :---                                              |      ---: |     ---: |+|  `Replace.Megaparsec.streamEdit`     `String`     | 454.95ms  | 375.04ms |+|  `Replace.Megaparsec.streamEdit`     `ByteString` | 611.98ms  | 433.26ms |+|  `Replace.Megaparsec.streamEdit`     `Text`       | 592.66ms  | 353.32ms |++## 1.1.5.0 -- 2019-10-08++* Move benchmarks to [__replace-benchmark__](https://github.com/jamesdbrock/replace-benchmark)+ ## 1.1.0.0 -- 2019-09-01  * Add benchmark suite. * In `streamEditT`, replace `fold` with `mconcat`. The benchmarks now show   linear scaling instead of quadratic. -## 1.1.5.0 -- 2019-10-08+## 1.0.1.0 -- 2019-08-28 -* Move benchmarks to [__replace-benchmark__](https://github.com/jamesdbrock/replace-benchmark)+* Add test suite.+* `sepCap` will treats `sep` as failing if it succeeds but consumes no input.++## 1.0.0.0 -- 2019-08-24++* First version.+
README.md view
@@ -38,7 +38,7 @@ See [__replace-attoparsec__](https://hackage.haskell.org/package/replace-attoparsec) for the [__attoparsec__](http://hackage.haskell.org/package/attoparsec)-version. ([__megaparsec__ is as fast as __attoparsec__.](https://github.com/mrkkrp/megaparsec#performance))+version.  ## Why would we want to do pattern matching and substitution with parsers instead of regular expressions? @@ -320,6 +320,13 @@  # Benchmarks +These benchmarks are intended to measure the wall-clock speed+of *everything except the actual pattern-matching*. Speed of the+pattern-matching is the responsibility of the+[__megaparsec__](http://hackage.haskell.org/package/megaparsec) and+[__attoparsec__](http://hackage.haskell.org/package/attoparsec)+libraries.+ The benchmark task is to find all of the one-character patterns `x` in a text stream and replace them by a function which returns the constant string `oo`. So, like the regex `s/x/oo/g`.@@ -341,7 +348,8 @@ ```  Each benchmark program reads the input from `stdin`, replaces `x` with `oo`,-and writes the result to `stdout`. The time elapsed is measured by `perf stat`.+and writes the result to `stdout`. The time elapsed is measured by `perf stat`,+and the best observed time is recorded.  See [replace-benchmark](https://github.com/jamesdbrock/replace-benchmark) for details.@@ -351,10 +359,10 @@ | Python `re.sub`¹                                  | 89.23ms   | 23.98ms  | | Perl `s///ge`²                                    | 180.65ms  | 5.60ms   | | [`Replace.Megaparsec.streamEdit`][m] `String`     | 454.95ms  | 375.04ms |-| [`Replace.Megaparsec.streamEdit`][m] `ByteString` | 611.98ms  | 433.26ms |-| [`Replace.Megaparsec.streamEdit`][m] `Text`       | 592.66ms  | 353.32ms |-| [`Replace.Attoparsec.ByteString.streamEdit`][ab]  | 537.57ms  | 407.33ms |-| [`Replace.Attoparsec.Text.streamEdit`][at]        | 549.62ms  | 280.96ms |+| [`Replace.Megaparsec.streamEdit`][m] `ByteString` | 529.99ms  | 73.76ms  |+| [`Replace.Megaparsec.streamEdit`][m] `Text`       | 547.47ms  | 139.21ms |+| [`Replace.Attoparsec.ByteString.streamEdit`][ab]  | 394.12ms  | 41.13ms  |+| [`Replace.Attoparsec.Text.streamEdit`][at]        | 515.26ms  | 46.10ms  | | [`Text.Regex.Applicative.replace`][ra] `String`   | 1083.98ms | 646.40ms | | [`Text.Regex.PCRE.Heavy.gsub`][ph] `Text`         | ⊥³        | 14.76ms  | 
replace-megaparsec.cabal view
@@ -1,5 +1,5 @@ name:                replace-megaparsec-version:             1.1.5.0+version:             1.2.0.0 cabal-version:       1.18 synopsis:            Find, replace, and edit text patterns with Megaparsec parsers homepage:            https://github.com/jamesdbrock/replace-megaparsec@@ -26,8 +26,12 @@   hs-source-dirs:      src   build-depends:       base >= 4.0 && < 5.0                      , megaparsec+                     , bytestring+                     , text   default-language:    Haskell2010   exposed-modules:     Replace.Megaparsec+                     , Replace.Megaparsec.Internal.ByteString+                     , Replace.Megaparsec.Internal.Text   ghc-options:         -O2 -Wall  test-suite test-string
src/Replace/Megaparsec.hs view
@@ -35,6 +35,8 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE CPP #-}  module Replace.Megaparsec   (@@ -57,7 +59,11 @@ import Control.Exception (throw) import Data.Typeable import Control.Monad+import qualified Data.ByteString as B+import qualified Data.Text as T import Text.Megaparsec+import Replace.Megaparsec.Internal.ByteString+import Replace.Megaparsec.Internal.Text  -- | -- == Separate and capture@@ -91,7 +97,6 @@ -- but, importantly, it returns the parsed result of the @sep@ parser instead -- of throwing it away. ---{-# INLINABLE sepCap #-} sepCap     :: forall e s m a. (MonadParsec e s m)     => m a -- ^ The pattern matching parser @sep@@@ -115,7 +120,31 @@         offset2 <- getOffset         when (offset1 >= offset2) empty         return x+{-# INLINE [1] sepCap #-}+-- https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#specialisation+-- What we're missing here is a rule that can pick up non-ParsecT instances+-- of MonadParsec for GHC < 8.8.+#if MIN_VERSION_GLASGOW_HASKELL(8,8,1,0)+{-# RULES "sepCap/ByteString" [2]+ forall e. forall.+ sepCap           @e @B.ByteString =+ sepCapByteString @e @B.ByteString #-}+{-# RULES "sepCap/Text" [2]+ forall e. forall.+ sepCap     @e @T.Text =+ sepCapText @e @T.Text #-}+#elif MIN_VERSION_GLASGOW_HASKELL(8,0,2,0)+{-# RULES "sepCap/ByteString" [2]+ forall (pa :: ParsecT e B.ByteString m a).+ sepCap           @e @B.ByteString @(ParsecT e B.ByteString m) @a pa =+ sepCapByteString @e @B.ByteString @(ParsecT e B.ByteString m) @a pa #-}+{-# RULES "sepCap/Text" [2]+ forall (pa :: ParsecT e T.Text m a).+ sepCap     @e @T.Text @(ParsecT e T.Text m) @a pa =+ sepCapText @e @T.Text @(ParsecT e T.Text m) @a pa #-}+#endif + -- | -- == Find all occurences, parse and capture pattern matches --@@ -130,13 +159,14 @@ -- @ -- findAllCap sep = 'sepCap' ('Text.Megaparsec.match' sep) -- @-{-# INLINABLE findAllCap #-} findAllCap     :: MonadParsec e s m     => m a -- ^ The pattern matching parser @sep@     -> m [Either (Tokens s) (Tokens s, a)] findAllCap sep = sepCap (match sep)+{-# INLINABLE findAllCap #-} + -- | -- == Find all occurences --@@ -151,12 +181,12 @@ -- @ -- findAll sep = (fmap.fmap) ('Data.Bifunctor.second' fst) $ 'sepCap' ('Text.Megaparsec.match' sep) -- @-{-# INLINABLE findAll #-} findAll     :: MonadParsec e s m     => m a -- ^ The pattern matching parser @sep@     -> m [Either (Tokens s) (Tokens s)] findAll sep = (fmap.fmap) (second fst) $ sepCap (match sep)+{-# INLINABLE findAll #-}   -- |@@ -208,7 +238,6 @@ -- this function should never throw an exception, because it only throws -- when the 'sepCap' parser fails, and the 'sepCap' parser -- can never fail. If this function ever throws, please report that as a bug.-{-# INLINABLE streamEdit #-} streamEdit     :: forall s a. (Stream s, Monoid s, Tokens s ~ s, Show s, Show (Token s), Typeable s)     => Parsec Void s a@@ -220,6 +249,7 @@         -- ^ The input stream of text to be edited.     -> s streamEdit sep editor = runIdentity . streamEditT sep (Identity . editor)+{-# INLINABLE streamEdit #-}  -- | -- == Stream editor transformer@@ -234,7 +264,6 @@ -- -- If you want the @editor@ function or the parser @sep@ to remember some state, -- then run this in a stateful monad.-{-# INLINABLE streamEditT #-} streamEditT     :: forall s m a. (Stream s, Monad m, Monoid s, Tokens s ~ s, Show s, Show (Token s), Typeable s)     => ParsecT Void s m a@@ -251,4 +280,5 @@         -- sepCap can never fail, but if it does, throw.         -- Don't use MonadFail because Identity is not a MonadFail.         (Right r) -> fmap mconcat $ traverse (either return editor) r+{-# INLINABLE streamEditT #-} 
+ src/Replace/Megaparsec/Internal/ByteString.hs view
@@ -0,0 +1,73 @@+-- |+-- Module    : Replace.Megaparsec.Internal.ByteString+-- Copyright : ©2019 James Brock+-- License   : BSD2+-- Maintainer: James Brock <jamesbrock@gmail.com>+--+-- This internal module is for 'Data.ByteString.ByteString' specializations.+--+-- The functions in this module are supposed to be chosen automatically+-- by rewrite rules in the "Replace.Megaparsec" module, so you should never+-- need to import this module.++{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeFamilies #-}++module Replace.Megaparsec.Internal.ByteString+  (+    -- * Parser combinator+    sepCapByteString+  )+where++import Control.Monad+import qualified Data.ByteString as B+import Text.Megaparsec++{-# INLINE [1] sepCapByteString #-}+sepCapByteString+    :: forall e s m a. (MonadParsec e s m, s ~ B.ByteString)+    => m a -- ^ The pattern matching parser @sep@+    -> m [Either (Tokens s) a]+sepCapByteString sep = getInput >>= go+  where+    -- the go function will search for the first pattern match,+    -- and then capture the pattern match along with the preceding+    -- unmatched string, and then recurse.+    -- restBegin is the rest of the buffer after the last pattern+    -- match.+    go restBegin = do+        (<|>)+            ( do+                restThis <- getInput+                -- About 'thisiter':+                -- It looks stupid and introduces a completely unnecessary+                -- Maybe, but when I refactor to eliminate 'thisiter' and+                -- the Maybe then the benchmarks get dramatically worse.+                thisiter <- (<|>)+                    ( do+                        x <- sep+                        restAfter <- getInput+                        -- Don't allow a match of a zero-width pattern+                        when (B.length restAfter >= B.length restThis) empty+                        pure $ Just (x, restAfter)+                    )+                    (anySingle >> pure Nothing)+                case thisiter of+                    (Just (x, restAfter)) | B.length restThis < B.length restBegin -> do+                        -- we've got a match with some preceding unmatched string+                        let unmatched = B.take (B.length restBegin - B.length restThis) restBegin+                        (Left unmatched:) <$> (Right x:) <$> go restAfter+                    (Just (x, restAfter)) -> do+                        -- we're got a match with no preceding unmatched string+                        (Right x:) <$> go restAfter+                    Nothing -> go restBegin -- no match, try again+            )+            ( do+                -- We're at the end of the input, so return+                -- whatever unmatched string we've got since offsetBegin+                if B.length restBegin > 0 then+                    pure [Left restBegin]+                else pure []+            )+
+ src/Replace/Megaparsec/Internal/Text.hs view
@@ -0,0 +1,74 @@+-- |+-- Module    : Replace.Megaparsec.Internal.Text+-- Copyright : ©2019 James Brock+-- License   : BSD2+-- Maintainer: James Brock <jamesbrock@gmail.com>+--+-- This internal module is for 'Data.Text.Text' specializations.+--+-- The functions in this module are supposed to be chosen automatically+-- by rewrite rules in the "Replace.Megaparsec" module, so you should never+-- need to import this module.++{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeFamilies #-}++module Replace.Megaparsec.Internal.Text+  (+    -- * Parser combinator+    sepCapText+  )+where++import Control.Monad+import qualified Data.Text as T+import Data.Text.Internal (Text(..))+import Text.Megaparsec++{-# INLINE [1] sepCapText #-}+sepCapText+    :: forall e s m a. (MonadParsec e s m, s ~ T.Text)+    => m a -- ^ The pattern matching parser @sep@+    -> m [Either (Tokens s) a]+sepCapText sep = getInput >>= go+  where+    -- the go function will search for the first pattern match,+    -- and then capture the pattern match along with the preceding+    -- unmatched string, and then recurse.+    -- restBegin is the rest of the buffer after the last pattern+    -- match.+    go restBegin@(Text tarray beginIndx beginLen) = do+        (<|>)+            ( do+                (Text _ _ thisLen) <- getInput+                -- About 'thisiter':+                -- It looks stupid and introduces a completely unnecessary+                -- Maybe, but when I refactor to eliminate 'thisiter' and+                -- the Maybe then the benchmarks get dramatically worse.+                thisiter <- (<|>)+                    ( do+                        x <- sep+                        restAfter@(Text _ _ afterLen) <- getInput+                        -- Don't allow a match of a zero-width pattern+                        when (afterLen >= thisLen) empty+                        pure $ Just (x, restAfter)+                    )+                    (anySingle >> pure Nothing)+                case thisiter of+                    (Just (x, restAfter)) | thisLen < beginLen -> do+                        -- we've got a match with some preceding unmatched string+                        let unmatched = Text tarray beginIndx (beginLen - thisLen)+                        (Left unmatched:) <$> (Right x:) <$> go restAfter+                    (Just (x, restAfter)) -> do+                        -- we're got a match with no preceding unmatched string+                        (Right x:) <$> go restAfter+                    Nothing -> go restBegin -- no match, try again+            )+            ( do+                    -- We're at the end of the input, so return+                    -- whatever unmatched string we've got since offsetBegin+                if beginLen > 0 then+                    pure [Left restBegin]+                else pure []+            )+
tests/TestByteString.hs view
@@ -1,9 +1,11 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE CPP #-}  module TestByteString ( tests ) where -import Distribution.TestSuite+import Distribution.TestSuite as TestSuite import Replace.Megaparsec import Text.Megaparsec import Text.Megaparsec.Byte@@ -39,10 +41,21 @@         (sepCap (fail "" :: Parser ()))         ("xxx")         ([Left "xxx"])+#if MIN_VERSION_GLASGOW_HASKELL(8,6,0,0)     , Test $ runParserTest "read fail"         (sepCap (return (read "a" :: Int) :: Parser Int))         ("a")         ([Left "a"])+#endif+    , Test $ streamEditTest "x to o"+        (string "x" :: Parser B.ByteString) (const "o")+        "x x x" "o o o"+    , Test $ streamEditTest "x to o inner"+        (string "x" :: Parser B.ByteString) (const "o")+        " x x x " " o o o "+    , Test $ streamEditTest "ordering"+        (string "456" :: Parser B.ByteString) (const "ABC")+        "123456789" "123ABC789"     ]   where     runParserTest nam p input expected = TestInstance@@ -55,6 +68,19 @@                             else return (Finished $ Fail                                         $ show output ++ " ≠ " ++ show expected)             , name = nam+            , tags = []+            , options = []+            , setOption = \_ _ -> Left "no options supported"+            }++    streamEditTest nam sep editor input expected = TestInstance+            { run = do+                let output = streamEdit sep editor input+                if (output == expected)+                    then return (Finished Pass)+                    else return (Finished $ TestSuite.Fail+                                $ show output ++ " ≠ " ++ show expected)+            , name = "streamEdit " ++ nam             , tags = []             , options = []             , setOption = \_ _ -> Left "no options supported"
tests/TestString.hs view
@@ -1,8 +1,11 @@ {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE CPP #-}  module TestString ( tests ) where  import Distribution.TestSuite+import Distribution.TestSuite as TestSuite import Replace.Megaparsec import Text.Megaparsec import Text.Megaparsec.Char@@ -34,10 +37,25 @@         (sepCap (fail "" :: Parser ()))         ("xxx")         ([Left "xxx"])+#if MIN_VERSION_GLASGOW_HASKELL(8,6,0,0)     , Test $ runParserTest "read fail"         (sepCap (return (read "a" :: Int) :: Parser Int))         ("a")         ([Left "a"])+#endif+    , Test $ runParserTest "findAll astral"+        (findAll ((takeWhileP Nothing (=='𝅘𝅥𝅯') :: Parser String)))+        ("𝄞𝅘𝅥𝅘𝅥𝅘𝅥𝅘𝅥𝅘𝅥𝅯𝅘𝅥𝅯𝅘𝅥𝅯𝅘𝅥𝅯𝅘𝅥𝅘𝅥𝅘𝅥𝅘𝅥")+        [Left "𝄞𝅘𝅥𝅘𝅥𝅘𝅥𝅘𝅥", Right "𝅘𝅥𝅯𝅘𝅥𝅯𝅘𝅥𝅯𝅘𝅥𝅯", Left "𝅘𝅥𝅘𝅥𝅘𝅥𝅘𝅥"]+    , Test $ streamEditTest "x to o"+        (string "x" :: Parser String) (const "o")+        "x x x" "o o o"+    , Test $ streamEditTest "x to o inner"+        (string "x" :: Parser String) (const "o")+        " x x x " " o o o "+    , Test $ streamEditTest "ordering"+        (string "456" :: Parser String) (const "ABC")+        "123456789" "123ABC789"     ]   where     runParserTest nam p input expected = TestInstance@@ -50,6 +68,19 @@                             else return (Finished $ Fail                                         $ show output ++ " ≠ " ++ show expected)             , name = nam+            , tags = []+            , options = []+            , setOption = \_ _ -> Left "no options supported"+            }++    streamEditTest nam sep editor input expected = TestInstance+            { run = do+                let output = streamEdit sep editor input+                if (output == expected)+                    then return (Finished Pass)+                    else return (Finished $ TestSuite.Fail+                                $ show output ++ " ≠ " ++ show expected)+            , name = "streamEdit " ++ nam             , tags = []             , options = []             , setOption = \_ _ -> Left "no options supported"
tests/TestText.hs view
@@ -1,9 +1,12 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE CPP #-}  module TestText ( tests ) where  import Distribution.TestSuite+import Distribution.TestSuite as TestSuite import Replace.Megaparsec import Text.Megaparsec import Text.Megaparsec.Char@@ -36,10 +39,25 @@         (sepCap (fail "" :: Parser ()))         ("xxx")         ([Left "xxx"])+#if MIN_VERSION_GLASGOW_HASKELL(8,6,0,0)     , Test $ runParserTest "read fail"         (sepCap (return (read "a" :: Int) :: Parser Int))         ("a")         ([Left "a"])+#endif+    , Test $ runParserTest "findAll astral"+        (findAll ((takeWhileP Nothing (=='𝅘𝅥𝅯') :: Parser T.Text)))+        ("𝄞𝅘𝅥𝅘𝅥𝅘𝅥𝅘𝅥𝅘𝅥𝅯𝅘𝅥𝅯𝅘𝅥𝅯𝅘𝅥𝅯𝅘𝅥𝅘𝅥𝅘𝅥𝅘𝅥" :: T.Text)+        [Left "𝄞𝅘𝅥𝅘𝅥𝅘𝅥𝅘𝅥", Right "𝅘𝅥𝅯𝅘𝅥𝅯𝅘𝅥𝅯𝅘𝅥𝅯", Left "𝅘𝅥𝅘𝅥𝅘𝅥𝅘𝅥"]+    , Test $ streamEditTest "x to o"+        (string "x" :: Parser T.Text) (const "o")+        "x x x" "o o o"+    , Test $ streamEditTest "x to o inner"+        (string "x" :: Parser T.Text) (const "o")+        " x x x " " o o o "+    , Test $ streamEditTest "ordering"+        (string "456" :: Parser T.Text) (const "ABC")+        "123456789" "123ABC789"     ]   where     runParserTest nam p input expected = TestInstance@@ -52,6 +70,19 @@                             else return (Finished $ Fail                                         $ show output ++ " ≠ " ++ show expected)             , name = nam+            , tags = []+            , options = []+            , setOption = \_ _ -> Left "no options supported"+            }++    streamEditTest nam sep editor input expected = TestInstance+            { run = do+                let output = streamEdit sep editor input+                if (output == expected)+                    then return (Finished Pass)+                    else return (Finished $ TestSuite.Fail+                                $ show output ++ " ≠ " ++ show expected)+            , name = "streamEdit " ++ nam             , tags = []             , options = []             , setOption = \_ _ -> Left "no options supported"