git.iko.soy/typeable/attoparsec
on master help sign in
Merge remote-tracking branch 'bgamari/fix-fastset'
commitb0da92b4bfab1c34df65f4fbd59f7bd54bcea765 authorBen Gamari <ben@smart-cactus.org> dateThu 21 Apr 2016, 23:58 (11 years ago) parent04f1df7e6813a3
10 files changed+131−33
Data/Attoparsec/Text/FastSet.hs+91−11 attoparsec.cabal+3−4 benchmarks/TextFastSet.hs+3−1 tests/QC.hs+2−2 tests/QC/Buffer.hs+3−3 tests/QC/ByteString.hs+3−3 tests/QC/Combinator.hs+3−3 tests/QC/Simple.hs+3−3 tests/QC/Text.hs+5−3 tests/QC/Text/FastSet.hs+15
Data/Attoparsec/Text/FastSet.hs+91−11
@@ -1,38 +1,118 @@
1+{-# LANGUAGE BangPatterns #-}
2+
3+------------------------------------------------------------------------------
14-- |
2−-- Module : Data.Attoparsec.Text.FastSet
3−-- Copyright : Bryan O'Sullivan 2015
5+-- Module : Data.Attoparsec.FastSet
6+-- Copyright : Felipe Lessa 2010, Bryan O'Sullivan 2007-2015
47-- License : BSD3
58--
6−-- Maintainer : bos@serpentine.com
9+-- Maintainer : felipe.lessa@gmail.com
710-- Stability : experimental
811-- Portability : unknown
912--
10−-- Fast set membership tests for 'Char' values.
11−
13+-- Fast set membership tests for 'Char' values. We test for
14+-- membership using a hashtable implemented with Robin Hood
15+-- collision resolution. The set representation is unboxed,
16+-- and the characters and hashes interleaved, for efficiency.
17+--
18+--
19+-----------------------------------------------------------------------------
1220module Data.Attoparsec.Text.FastSet
1321 (
1422 -- * Data type
1523 FastSet
1624 -- * Construction
1725 , fromList
26+ , set
1827 -- * Lookup
1928 , member
2029 -- * Handy interface
2130 , charClass
2231 ) where
2332
24−import qualified Data.IntSet as I
25−import Data.Char (ord)
33+import Data.Bits ((.|.), (.&.), shiftR)
34+import Data.Function (on)
35+import Data.List (sort, sortBy)
36+import qualified Data.Array.Base as AB
37+import qualified Data.Array.Unboxed as A
38+import qualified Data.Text as T
39+
40+data FastSet = FastSet {
41+ table :: {-# UNPACK #-} !(A.UArray Int Int)
42+ , mask :: {-# UNPACK #-} !Int
43+ }
44+
45+data Entry = Entry {
46+ key :: {-# UNPACK #-} !Char
47+ , initialIndex :: {-# UNPACK #-} !Int
48+ , index :: {-# UNPACK #-} !Int
49+ }
2650
27−newtype FastSet = FastSet I.IntSet
51+offset :: Entry -> Int
52+offset e = index e - initialIndex e
53+
54+resolveCollisions :: [Entry] -> [Entry]
55+resolveCollisions [] = []
56+resolveCollisions [e] = [e]
57+resolveCollisions (a:b:entries) = a' : resolveCollisions (b' : entries)
58+ where (a', b')
59+ | index a < index b = (a, b)
60+ | offset a < offset b = (b { index=index a }, a { index=index a + 1 })
61+ | otherwise = (a, b { index=index a + 1 })
62+
63+pad :: Int -> [Entry] -> [Entry]
64+pad = go 0
65+ where -- ensure that we pad enough so that lookups beyond the
66+ -- last hash in the table fall within the array
67+ go !_ !m [] = replicate (max 1 m + 1) empty
68+ go k m (e:entries) = map (const empty) [k..i - 1] ++ e :
69+ go (i + 1) (m + i - k - 1) entries
70+ where i = index e
71+ empty = Entry '\0' maxBound 0
72+
73+nextPowerOf2 :: Int -> Int
74+nextPowerOf2 0 = 1
75+nextPowerOf2 x = go (x - 1) 1
76+ where go y 32 = y + 1
77+ go y k = go (y .|. (y `shiftR` k)) $ k * 2
78+
79+fastHash :: Char -> Int
80+fastHash = fromEnum
2881
2982fromList :: String -> FastSet
30−fromList = FastSet . I.fromList . map ord
83+fromList s = FastSet (AB.listArray (0, length interleaved - 1) interleaved)
84+ mask'
85+ where s' = ordNub (sort s)
86+ l = length s'
87+ mask' = nextPowerOf2 ((5 * l) `div` 4) - 1
88+ entries = pad mask' .
89+ resolveCollisions .
90+ sortBy (compare `on` initialIndex) .
91+ zipWith (\c i -> Entry c i i) s' .
92+ map ((.&. mask') . fastHash) $ s'
93+ interleaved = concatMap (\e -> [fromEnum $ key e, initialIndex e])
94+ entries
95+
96+ordNub :: Eq a => [a] -> [a]
97+ordNub [] = []
98+ordNub (y:ys) = go y ys
99+ where go x (z:zs)
100+ | x == z = go x zs
101+ | otherwise = x : go z zs
102+ go x [] = [x]
103+
104+set :: T.Text -> FastSet
105+set = fromList . T.unpack
31106
32107-- | Check the set for membership.
33108member :: Char -> FastSet -> Bool
34−member c (FastSet s) = I.member (ord c) s
35−{-# INLINE member #-}
109+member c a = go (2 * i)
110+ where i = fastHash c .&. mask a
111+ lookupAt j b = (i' <= i) && (c == c' || b)
112+ where c' = toEnum $ AB.unsafeAt (table a) j
113+ i' = AB.unsafeAt (table a) $ j + 1
114+ go j = lookupAt j . lookupAt (j + 2) . lookupAt (j + 4) .
115+ lookupAt (j + 6) . go $ j + 8
36116
37117charClass :: String -> FastSet
38118charClass = fromList . go
attoparsec.cabal+3−4
@@ -89,6 +89,7 @@ test-suite tests
8989 QC.Rechunked
9090 QC.Simple
9191 QC.Text
92+ QC.Text.FastSet
9293
9394 ghc-options:
9495 -Wall -threaded -rtsopts
@@ -100,13 +101,12 @@ test-suite tests
100101 array,
101102 base >= 4 && < 5,
102103 bytestring,
103− containers,
104104 deepseq >= 1.1,
105105 QuickCheck >= 2.7,
106106 quickcheck-unicode,
107107 scientific,
108− test-framework >= 0.8.0.2,
109− test-framework-quickcheck2 >= 0.3.0.3,
108+ tasty >= 0.11,
109+ tasty-quickcheck >= 0.8,
110110 text,
111111 transformers,
112112 vector
@@ -136,7 +136,6 @@ benchmark benchmarks
136136 base == 4.*,
137137 bytestring >= 0.10.4.0,
138138 case-insensitive,
139− containers,
140139 criterion >= 1.0,
141140 deepseq >= 1.1,
142141 directory,
benchmarks/TextFastSet.hs+3−1
@@ -62,7 +62,9 @@ resolveCollisions (a:b:entries) = a' : resolveCollisions (b' : entries)
6262
6363pad :: Int -> [Entry] -> [Entry]
6464pad = go 0
65− where go !_ !m [] = replicate (max 1 m) empty
65+ where -- ensure that we pad enough so that lookups beyond the
66+ -- last hash in the table fall within the array
67+ go !_ !m [] = replicate (max 1 m + 1) empty
6668 go k m (e:entries) = map (const empty) [k..i - 1] ++ e :
6769 go (i + 1) (m + i - k - 1) entries
6870 where i = index e
tests/QC.hs+2−2
@@ -6,11 +6,11 @@ import qualified QC.ByteString as ByteString
66import qualified QC.Combinator as Combinator
77import qualified QC.Simple as Simple
88import qualified QC.Text as Text
9−import Test.Framework (defaultMain, testGroup)
9+import Test.Tasty (defaultMain, testGroup)
1010
1111main = defaultMain tests
1212
13−tests = [
13+tests = testGroup "tests" [
1414 testGroup "bs" ByteString.tests
1515 , testGroup "buf" Buffer.tests
1616 , testGroup "combinator" Combinator.tests
tests/QC/Buffer.hs+3−3
@@ -8,8 +8,8 @@ import Control.Applicative ((<$>))
88import Data.Monoid (Monoid(mconcat))
99#endif
1010import QC.Common ()
11−import Test.Framework (Test)
12−import Test.Framework.Providers.QuickCheck2 (testProperty)
11+import Test.Tasty (TestTree)
12+import Test.Tasty.QuickCheck (testProperty)
1313import Test.QuickCheck
1414import qualified Data.Attoparsec.ByteString.Buffer as BB
1515import qualified Data.Attoparsec.Text.Buffer as BT
@@ -82,7 +82,7 @@ t_dropWord16 (BP _ts t buf) = do
8282 i <- choose (0, T.lengthWord16 t)
8383 return $ T.dropWord16 i t === BT.dropWord16 i buf
8484
85−tests :: [Test]
85+tests :: [TestTree]
8686tests = [
8787 testProperty "b_unbuffer" b_unbuffer
8888 , testProperty "t_unbuffer" t_unbuffer
tests/QC/ByteString.hs+3−3
@@ -9,8 +9,8 @@ import Data.Int (Int64)
99import Data.Word (Word8)
1010import Prelude hiding (take, takeWhile)
1111import QC.Common (ASCII(..), liftOp, parseBS, toStrictBS)
12−import Test.Framework (Test)
13−import Test.Framework.Providers.QuickCheck2 (testProperty)
12+import Test.Tasty (TestTree)
13+import Test.Tasty.QuickCheck (testProperty)
1414import Test.QuickCheck
1515import qualified Data.Attoparsec.ByteString as P
1616import qualified Data.Attoparsec.ByteString.Char8 as P8
@@ -155,7 +155,7 @@ nonmembers :: [Word8] -> [Word8] -> Property
155155nonmembers s s' = property . not . any (`S.memberWord8` set) $ filter (not . (`elem` s)) s'
156156 where set = S.fromList s
157157
158−tests :: [Test]
158+tests :: [TestTree]
159159tests = [
160160 testProperty "anyWord8" anyWord8
161161 , testProperty "endOfInput" endOfInput
tests/QC/Combinator.hs+3−3
@@ -8,8 +8,8 @@ import Control.Applicative ((<*>), (<$>), (<*), (*>))
88import Data.Maybe (fromJust, isJust)
99import Data.Word (Word8)
1010import QC.Common (Repack, parseBS, repackBS, toLazyBS)
11−import Test.Framework (Test)
12−import Test.Framework.Providers.QuickCheck2 (testProperty)
11+import Test.Tasty (TestTree)
12+import Test.Tasty.QuickCheck (testProperty)
1313import Test.QuickCheck
1414import qualified Data.Attoparsec.ByteString.Char8 as P
1515import qualified Data.Attoparsec.Combinator as C
@@ -43,7 +43,7 @@ match n (NonNegative x) (NonNegative y) rs =
4343 B8.replicate x 'x', B8.pack (show n), B8.replicate y 'y'
4444 ]
4545
46−tests :: [Test]
46+tests :: [TestTree]
4747tests = [
4848 testProperty "choice" choice
4949 , testProperty "count" count
tests/QC/Simple.hs+3−3
@@ -10,8 +10,8 @@ import Data.ByteString (ByteString)
1010import Data.List (foldl')
1111import Data.Maybe (fromMaybe)
1212import QC.Rechunked (rechunkBS)
13−import Test.Framework (Test)
14−import Test.Framework.Providers.QuickCheck2 (testProperty)
13+import Test.Tasty (TestTree)
14+import Test.Tasty.QuickCheck (testProperty)
1515import Test.QuickCheck (Property, counterexample, forAll)
1616import qualified Data.Attoparsec.ByteString.Char8 as A
1717
@@ -31,7 +31,7 @@ parse :: A.Parser r -> [ByteString] -> A.Result r
3131parse p (x:xs) = foldl' A.feed (A.parse p x) xs
3232parse p [] = A.parse p ""
3333
34−tests :: [Test]
34+tests :: [TestTree]
3535tests = [
3636 testProperty "issue75" t_issue75
3737 ]
tests/QC/Text.hs+5−3
@@ -8,8 +8,9 @@ import Control.Applicative ((<*>), (<$>))
88import Data.Int (Int64)
99import Prelude hiding (take, takeWhile)
1010import QC.Common (liftOp, parseT)
11−import Test.Framework (Test)
12−import Test.Framework.Providers.QuickCheck2 (testProperty)
11+import qualified QC.Text.FastSet as FastSet
12+import Test.Tasty (TestTree, testGroup)
13+import Test.Tasty.QuickCheck (testProperty)
1314import Test.QuickCheck
1415import qualified Data.Attoparsec.Text as P
1516import qualified Data.Attoparsec.Text.Lazy as PL
@@ -160,7 +161,7 @@ nonmembers :: String -> String -> Property
160161nonmembers s s' = property . not . any (`S.member` set) $ filter (not . (`elem` s)) s'
161162 where set = S.fromList s
162163
163−tests :: [Test]
164+tests :: [TestTree]
164165tests = [
165166 testProperty "anyChar" anyChar
166167 , testProperty "asciiCI" asciiCI
@@ -188,4 +189,5 @@ tests = [
188189 , testProperty "takeWhile1_empty" takeWhile1_empty
189190 , testProperty "members" members
190191 , testProperty "nonmembers" nonmembers
192+ , testGroup "FastSet" FastSet.tests
191193 ]
tests/QC/Text/FastSet.hsnew file+15
@@ -0,0 +1,15 @@
1+module QC.Text.FastSet where
2+
3+import Test.Tasty (TestTree)
4+import Test.Tasty.QuickCheck (testProperty)
5+import Test.QuickCheck
6+import qualified Data.Attoparsec.Text.FastSet as FastSet
7+
8+membershipCorrect :: String -> String -> Property
9+membershipCorrect members others =
10+ let fs = FastSet.fromList members
11+ correct c = (c `FastSet.member` fs) == (c `elem` members)
12+ in property $ all correct (members ++ others)
13+
14+tests :: [TestTree]
15+tests = [ testProperty "membership is correct" membershipCorrect ]