From 63b88617e5b2ca505367dfafe3bcc0b7575c52e7 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 14 Aug 2018 12:48:57 +0200 Subject: [PATCH 01/69] Intermediary commit --- Data/HashMap/Base.hs | 73 ++++++++++++++++++-------------------------- stack.yaml | 3 ++ 2 files changed, 33 insertions(+), 43 deletions(-) create mode 100644 stack.yaml diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 553d2b3a..9085181a 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -181,7 +181,7 @@ data HashMap k v | BitmapIndexed !Bitmap !(A.Array (HashMap k v)) | Leaf !Hash !(Leaf k v) | Full !(A.Array (HashMap k v)) - | Collision !Hash !(A.Array (Leaf k v)) + | Collision !Hash !Salt !(HashMap k v) deriving (Typeable) type role HashMap nominal representational @@ -191,7 +191,7 @@ instance (NFData k, NFData v) => NFData (HashMap k v) where rnf (BitmapIndexed _ ary) = rnf ary rnf (Leaf _ l) = rnf l rnf (Full ary) = rnf ary - rnf (Collision _ ary) = rnf ary + rnf (Collision _ _ ary) = rnf ary instance Functor (HashMap k) where fmap = map @@ -231,6 +231,7 @@ hashMapDataType :: DataType hashMapDataType = mkDataType "Data.HashMap.Base.HashMap" [fromListConstr] type Hash = Word +type Salt = Int type Bitmap = Word type Shift = Int @@ -293,8 +294,8 @@ equal1 eq = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go (Collision h1 ary1) (Collision h2 ary2) - = h1 == h2 && isPermutationBy leafEq (A.toList ary1) (A.toList ary2) + go (Collision h1 s1 hm1) (Collision h2 s2 hm2) + = h1 == h2 && s1 == s2 && go hm1 hm2 go _ _ = False leafEq (L k1 v1) (L k2 v2) = k1 == k2 && eq v1 v2 @@ -311,10 +312,10 @@ equal2 eqk eqv t1 t2 = go (toList' t1 []) (toList' t2 []) | k1 == k2 && leafEq l1 l2 = go tl1 tl2 - go (Collision k1 ary1 : tl1) (Collision k2 ary2 : tl2) - | k1 == k2 && - A.length ary1 == A.length ary2 && - isPermutationBy leafEq (A.toList ary1) (A.toList ary2) + go (Collision h1 s1 hm1 : tl1) (Collision h2 s2 hm2 : tl2) + | h1 == h2 && + s1 == s2 && + go (toList' hm1 []) (toList' hm2 []) = go tl1 tl2 go [] [] = True go _ _ = False @@ -345,13 +346,13 @@ cmp cmpk cmpv t1 t2 = go (toList' t1 []) (toList' t2 []) = compare k1 k2 `mappend` leafCompare l1 l2 `mappend` go tl1 tl2 - go (Collision k1 ary1 : tl1) (Collision k2 ary2 : tl2) - = compare k1 k2 `mappend` - compare (A.length ary1) (A.length ary2) `mappend` - unorderedCompare leafCompare (A.toList ary1) (A.toList ary2) `mappend` + go (Collision h1 s1 hm1 : tl1) (Collision h2 s2 hm2 : tl2) + = compare h1 h2 `mappend` + compare s1 s2 `mappend` + go (toList' hm1 []) (toList' hm2 []) `mappend` go tl1 tl2 - go (Leaf _ _ : _) (Collision _ _ : _) = LT - go (Collision _ _ : _) (Leaf _ _ : _) = GT + go (Leaf _ _ : _) (Collision _ _ _ : _) = LT + go (Collision _ _ _ : _) (Leaf _ _ : _) = GT go [] [] = EQ go [] _ = LT go _ [] = GT @@ -366,9 +367,9 @@ equalKeys1 eq t1 t2 = go (toList' t1 []) (toList' t2 []) go (Leaf k1 l1 : tl1) (Leaf k2 l2 : tl2) | k1 == k2 && leafEq l1 l2 = go tl1 tl2 - go (Collision k1 ary1 : tl1) (Collision k2 ary2 : tl2) - | k1 == k2 && A.length ary1 == A.length ary2 && - isPermutationBy leafEq (A.toList ary1) (A.toList ary2) + go (Collision h1 s1 hm1 : tl1) (Collision h2 s2 hm2 : tl2) + | h1 == h2 && s1 == s2 && + go (toList' hm1 []) (toList' hm2 []) = go tl1 tl2 go [] [] = True go _ _ = False @@ -385,8 +386,8 @@ equalKeys = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go (Collision h1 ary1) (Collision h2 ary2) - = h1 == h2 && isPermutationBy leafEq (A.toList ary1) (A.toList ary2) + go (Collision h1 s1 hm1) (Collision h2 s2 hm2) + = h1 == h2 && s1 == s2 && go hm1 hm2 go _ _ = False leafEq (L k1 _) (L k2 _) = k1 == k2 @@ -401,20 +402,13 @@ instance H.Hashable2 HashMap where = s `hashLeafWithSalt` l `go` tl -- For collisions we hashmix hash value -- and then array of values' hashes sorted - go s (Collision h a : tl) - = (s `H.hashWithSalt` h) `hashCollisionWithSalt` a `go` tl + go s (Collision h s' hm' : tl) + = (((s `H.hashWithSalt` h) `H.hashWithSalt` s') `go` (toList' hm' [])) `go` tl go s (_ : tl) = s `go` tl -- hashLeafWithSalt :: Int -> Leaf k v -> Int hashLeafWithSalt s (L k v) = (s `hk` k) `hv` v - -- hashCollisionWithSalt :: Int -> A.Array (Leaf k v) -> Int - hashCollisionWithSalt s - = L.foldl' H.hashWithSalt s . arrayHashesSorted s - - -- arrayHashesSorted :: Int -> A.Array (Leaf k v) -> [Int] - arrayHashesSorted s = L.sort . L.map (hashLeafWithSalt s) . A.toList - instance (Hashable k) => H.Hashable1 (HashMap k) where liftHashWithSalt = H.liftHashWithSalt2 H.hashWithSalt #endif @@ -428,33 +422,26 @@ instance (Hashable k, Hashable v) => Hashable (HashMap k v) where = s `hashLeafWithSalt` l `go` tl -- For collisions we hashmix hash value -- and then array of values' hashes sorted - go s (Collision h a : tl) - = (s `H.hashWithSalt` h) `hashCollisionWithSalt` a `go` tl + go s (Collision h s' hm' : tl) + = (((s `H.hashWithSalt` h) `H.hashWithSalt` s') `go` (toList' hm' [])) `go` tl go s (_ : tl) = s `go` tl hashLeafWithSalt :: Int -> Leaf k v -> Int hashLeafWithSalt s (L k v) = s `H.hashWithSalt` k `H.hashWithSalt` v - hashCollisionWithSalt :: Int -> A.Array (Leaf k v) -> Int - hashCollisionWithSalt s - = L.foldl' H.hashWithSalt s . arrayHashesSorted s - - arrayHashesSorted :: Int -> A.Array (Leaf k v) -> [Int] - arrayHashesSorted s = L.sort . L.map (hashLeafWithSalt s) . A.toList - - -- Helper to get 'Leaf's and 'Collision's as a list. + -- Helper to get 'Leaf's as a list. toList' :: HashMap k v -> [HashMap k v] -> [HashMap k v] toList' (BitmapIndexed _ ary) a = A.foldr toList' a ary toList' (Full ary) a = A.foldr toList' a ary toList' l@(Leaf _ _) a = l : a -toList' c@(Collision _ _) a = c : a +toList' (Collision _ _ hm) a = toList' hm a toList' Empty a = a -- Helper function to detect 'Leaf's and 'Collision's. isLeafOrCollision :: HashMap k v -> Bool -isLeafOrCollision (Leaf _ _) = True -isLeafOrCollision (Collision _ _) = True -isLeafOrCollision _ = False +isLeafOrCollision (Leaf _ _) = True +isLeafOrCollision (Collision _ _ _) = True +isLeafOrCollision _ = False ------------------------------------------------------------------------ -- * Construction @@ -483,7 +470,7 @@ size t = go t 0 go (Leaf _ _) n = n + 1 go (BitmapIndexed _ ary) n = A.foldl' (flip go) n ary go (Full ary) n = A.foldl' (flip go) n ary - go (Collision _ ary) n = n + A.length ary + go (Collision _ _ hm) n = go hm n -- | /O(log n)/ Return 'True' if the specified key is present in the -- map, 'False' otherwise. diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 00000000..d082516b --- /dev/null +++ b/stack.yaml @@ -0,0 +1,3 @@ +resolver: lts-10.10 +packages: +- . From c96f41e220ab265686afcbbf4fa26e185b2223e9 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 14 Aug 2018 14:06:23 +0200 Subject: [PATCH 02/69] Halfway through --- Data/HashMap/Base.hs | 312 +++++++++++++++++++++---------------------- 1 file changed, 154 insertions(+), 158 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 9085181a..58eb42fe 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -78,6 +78,7 @@ module Data.HashMap.Base -- Internals used by the strict version , Hash + , Salt , Bitmap , bitmapIndexedOrFull , collision @@ -161,9 +162,9 @@ import Data.Coerce (coerce) -- | A set of values. A set cannot contain duplicate values. ------------------------------------------------------------------------ --- | Convenience function. Compute a hash value for the given value. -hash :: H.Hashable a => a -> Hash -hash = fromIntegral . H.hash +-- | Convenience function. Compute a hash value for the given value with a given salt. +hashWithSalt :: H.Hashable a => Salt -> a -> Hash +hashWithSalt s v = fromIntegral $ H.hashWithSalt s v data Leaf k v = L !k v deriving (Eq) @@ -181,7 +182,7 @@ data HashMap k v | BitmapIndexed !Bitmap !(A.Array (HashMap k v)) | Leaf !Hash !(Leaf k v) | Full !(A.Array (HashMap k v)) - | Collision !Hash !Salt !(HashMap k v) + | Collision !Hash !Salt !(HashMap k v) -- The new salt (TODO remove this comment) deriving (Typeable) type role HashMap nominal representational @@ -452,7 +453,7 @@ empty = Empty -- | /O(1)/ Construct a map with a single element. singleton :: (Hashable k) => k -> v -> HashMap k v -singleton k v = Leaf (hash k) (L k v) +singleton k v = Leaf (hashWithSalt defaultSalt k) (L k v) ------------------------------------------------------------------------ -- * Basic interface @@ -493,18 +494,18 @@ lookup k m = case lookup# k m of {-# INLINE lookup #-} lookup# :: (Eq k, Hashable k) => k -> HashMap k v -> (# (# #) | v #) -lookup# k m = lookupCont (\_ -> (# (# #) | #)) (\v _i -> (# | v #)) (hash k) k m +lookup# k m = lookupCont (\_ -> (# (# #) | #)) (\v _i -> (# | v #)) (hashWithSalt defaultSalt k) defaultSalt k m {-# INLINABLE lookup# #-} #else -lookup k m = lookupCont (\_ -> Nothing) (\v _i -> Just v) (hash k) k m +lookup k m = lookupCont (\_ -> Nothing) (\v _i -> Just v) (hashWithSalt s k) defaultSalt k m {-# INLINABLE lookup #-} #endif -- | lookup' is a version of lookup that takes the hash separately. -- It is used to implement alterF. -lookup' :: Eq k => Hash -> k -> HashMap k v -> Maybe v +lookup' :: Eq k => Hash -> Salt -> k -> HashMap k v -> Maybe v #if __GLASGOW_HASKELL__ >= 802 -- GHC does not yet perform a worker-wrapper transformation on -- unboxed sums automatically. That seems likely to happen at some @@ -512,12 +513,12 @@ lookup' :: Eq k => Hash -> k -> HashMap k v -> Maybe v -- lookup' would probably prefer to be implemented in terms of its own -- lookup'#, but it's not important enough and we don't want too much -- code. -lookup' h k m = case lookupRecordCollision# h k m of +lookup' h s k m = case lookupRecordCollision# h s k m of (# (# #) | #) -> Nothing (# | (# a, _i #) #) -> Just a {-# INLINE lookup' #-} #else -lookup' h k m = lookupCont (\_ -> Nothing) (\v _i -> Just v) h k m +lookup' h s k m = lookupCont (\_ -> Nothing) (\v _i -> Just v) h s k m {-# INLINABLE lookup' #-} #endif @@ -538,9 +539,9 @@ data LookupRes a = Absent | Present a !Int -- Key not in map => Absent -- Key in map, no collision => Present v (-1) -- Key in map, collision => Present v position -lookupRecordCollision :: Eq k => Hash -> k -> HashMap k v -> LookupRes v +lookupRecordCollision :: Eq k => Hash -> Salt -> k -> HashMap k v -> LookupRes v #if __GLASGOW_HASKELL__ >= 802 -lookupRecordCollision h k m = case lookupRecordCollision# h k m of +lookupRecordCollision h s k m = case lookupRecordCollision# h s k m of (# (# #) | #) -> Absent (# | (# a, i #) #) -> Present a (I# i) -- GHC will eliminate the I# {-# INLINE lookupRecordCollision #-} @@ -550,15 +551,15 @@ lookupRecordCollision h k m = case lookupRecordCollision# h k m of -- may be changing in GHC 8.6 or so (there is some work in progress), but -- for now we use Int# explicitly here. We don't need to push the Int# -- into lookupCont because inlining takes care of that. -lookupRecordCollision# :: Eq k => Hash -> k -> HashMap k v -> (# (# #) | (# v, Int# #) #) -lookupRecordCollision# h k m = - lookupCont (\_ -> (# (# #) | #)) (\v (I# i) -> (# | (# v, i #) #)) h k m +lookupRecordCollision# :: Eq k => Hash -> Salt -> k -> HashMap k v -> (# (# #) | (# v, Int# #) #) +lookupRecordCollision# h s k m = + lookupCont (\_ -> (# (# #) | #)) (\v (I# i) -> (# | (# v, i #) #)) h s k m -- INLINABLE to specialize to the Eq instance. {-# INLINABLE lookupRecordCollision# #-} #else /* GHC < 8.2 so there are no unboxed sums */ -lookupRecordCollision h k m = lookupCont (\_ -> Absent) Present h k m +lookupRecordCollision h s k m = lookupCont (\_ -> Absent) Present h s k m {-# INLINABLE lookupRecordCollision #-} #endif @@ -580,23 +581,24 @@ lookupCont :: => ((# #) -> r) -- Absent continuation -> (v -> Int -> r) -- Present continuation -> Hash -- The hash of the key + -> Salt -- The salt that was used to obtain that hash -> k -> HashMap k v -> r -lookupCont absent present !h0 !k0 !m0 = go h0 k0 0 m0 +lookupCont absent present !h0 !s0 !k0 !m0 = go h0 s0 k0 0 m0 where - go :: Eq k => Hash -> k -> Int -> HashMap k v -> r - go !_ !_ !_ Empty = absent (# #) - go h k _ (Leaf hx (L kx x)) + go :: Eq k => Hash -> Salt -> k -> Int -> HashMap k v -> r + go !_ !_ !_ !_ Empty = absent (# #) + go h _ k _ (Leaf hx (L kx x)) | h == hx && k == kx = present x (-1) | otherwise = absent (# #) - go h k s (BitmapIndexed b v) + go h s k bs (BitmapIndexed b v) | b .&. m == 0 = absent (# #) | otherwise = - go h k (s+bitsPerSubkey) (A.index v (sparseIndex b m)) + go h s k (bs+bitsPerSubkey) (A.index v (sparseIndex b m)) where m = mask h s - go h k s (Full v) = - go h k (s+bitsPerSubkey) (A.index v (index h s)) - go h k _ (Collision hx v) - | h == hx = lookupInArrayCont absent present k v + go h s k bs (Full v) = + go h s k (bs+bitsPerSubkey) (A.index v (index h s)) + go h s k _ (Collision hx sx hmx) + | h == hx = go h sx k 0 hmx | otherwise = absent (# #) {-# INLINE lookupCont #-} @@ -621,14 +623,26 @@ lookupDefault def k t = case lookup k t of infixl 9 ! -- | Create a 'Collision' value with two 'Leaf' values. -collision :: Hash -> Leaf k v -> Leaf k v -> HashMap k v -collision h !e1 !e2 = - let v = A.run $ do mary <- A.new 2 e1 - A.write mary 1 e2 - return mary - in Collision h v +collision :: (Eq k, Hashable k) => Hash -> Salt -> Leaf k v -> Leaf k v -> HashMap k v +collision h s !(L k1 v1) !(L k2 v2) = + let s' = nextSalt s + rehash = hashWithSalt s' + h1 = rehash k1 + h2 = rehash k2 + hm = runST $ two 0 h1 k1 v1 h2 k2 v2 + in Collision h s' hm {-# INLINE collision #-} +defaultSalt :: Int +#if WORD_SIZE_IN_BITS == 64 +defaultSalt = -2578643520546668380 -- 0xdc36d1615b7400a4 +#else +defaultSalt = 0x087fc72c +#endif + +nextSalt :: Salt -> Salt +nextSalt = (+1) + -- | Create a 'BitmapIndexed' or 'Full' node. bitmapIndexedOrFull :: Bitmap -> A.Array (HashMap k v) -> HashMap k v bitmapIndexedOrFull b ary @@ -640,42 +654,43 @@ bitmapIndexedOrFull b ary -- key in this map. If this map previously contained a mapping for -- the key, the old value is replaced. insert :: (Eq k, Hashable k) => k -> v -> HashMap k v -> HashMap k v -insert k v m = insert' (hash k) k v m +insert k v m = insert' (hashWithSalt defaultSalt k) defaultSalt k v m {-# INLINABLE insert #-} -insert' :: Eq k => Hash -> k -> v -> HashMap k v -> HashMap k v -insert' h0 k0 v0 m0 = go h0 k0 v0 0 m0 +insert' :: (Eq k, Hashable k) => Hash -> Salt -> k -> v -> HashMap k v -> HashMap k v +insert' h0 s0 k0 v0 m0 = go h0 s0 k0 v0 0 m0 where - go !h !k x !_ Empty = Leaf h (L k x) - go h k x s t@(Leaf hy l@(L ky y)) + go :: (Eq k, Hashable k) => Hash -> Salt -> k -> v -> Int -> HashMap k v -> HashMap k v + go !h _ !k x !_ Empty = Leaf h (L k x) + go h s k x bs t@(Leaf hy l@(L ky y)) | hy == h = if ky == k then if x `ptrEq` y then t else Leaf h (L k x) - else collision h l (L k x) - | otherwise = runST (two s h k x hy ky y) - go h k x s t@(BitmapIndexed b ary) + else collision h s l (L k x) + | otherwise = runST (two bs h k x hy ky y) + go h s k x bs t@(BitmapIndexed b ary) | b .&. m == 0 = let !ary' = A.insert ary i $! Leaf h (L k x) in bitmapIndexedOrFull (b .|. m) ary' | otherwise = let !st = A.index ary i - !st' = go h k x (s+bitsPerSubkey) st + !st' = go h s k x (bs+bitsPerSubkey) st in if st' `ptrEq` st then t else BitmapIndexed b (A.update ary i st') - where m = mask h s + where m = mask h bs i = sparseIndex b m - go h k x s t@(Full ary) = + go h s k x bs t@(Full ary) = let !st = A.index ary i - !st' = go h k x (s+bitsPerSubkey) st + !st' = go h s k x (bs+bitsPerSubkey) st in if st' `ptrEq` st then t else Full (update16 ary i st') - where i = index h s - go h k x s t@(Collision hy v) - | h == hy = Collision h (updateOrSnocWith const k x v) - | otherwise = go h k x s $ BitmapIndexed (mask hy s) (A.singleton t) + where i = index h bs + go h s k x bs t@(Collision hx sx hmx) + | h == hx = Collision hx sx $ insert' (hashWithSalt sx k) sx k x hmx + | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insert' #-} -- Insert optimized for the case when we know the key is not in the map. @@ -685,75 +700,63 @@ insert' h0 k0 v0 m0 = go h0 k0 v0 0 m0 -- We can skip: -- - the key equality check on a Leaf -- - check for its existence in the array for a hash collision -insertNewKey :: Hash -> k -> v -> HashMap k v -> HashMap k v -insertNewKey !h0 !k0 x0 !m0 = go h0 k0 x0 0 m0 +insertNewKey :: (Eq k, Hashable k) => Hash -> Salt -> k -> v -> HashMap k v -> HashMap k v +insertNewKey !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 where - go !h !k x !_ Empty = Leaf h (L k x) - go h k x s (Leaf hy l@(L ky y)) - | hy == h = collision h l (L k x) - | otherwise = runST (two s h k x hy ky y) - go h k x s (BitmapIndexed b ary) + go !h _ !k x !_ Empty = Leaf h (L k x) + go h s k x bs (Leaf hy l@(L ky y)) + | hy == h = collision h s l (L k x) + | otherwise = runST (two bs h k x hy ky y) + go h s k x bs (BitmapIndexed b ary) | b .&. m == 0 = let !ary' = A.insert ary i $! Leaf h (L k x) in bitmapIndexedOrFull (b .|. m) ary' | otherwise = let !st = A.index ary i - !st' = go h k x (s+bitsPerSubkey) st + !st' = go h s k x (bs+bitsPerSubkey) st in BitmapIndexed b (A.update ary i st') - where m = mask h s + where m = mask h bs i = sparseIndex b m - go h k x s (Full ary) = + go h s k x bs (Full ary) = let !st = A.index ary i - !st' = go h k x (s+bitsPerSubkey) st + !st' = go h s k x (bs+bitsPerSubkey) st in Full (update16 ary i st') - where i = index h s - go h k x s t@(Collision hy v) - | h == hy = Collision h (snocNewLeaf (L k x) v) + where i = index h bs + go h s k x bs t@(Collision hx sx hmx) + | h == hx = Collision hx sx $ insertNewKey (hashWithSalt sx k) sx k x hmx | otherwise = - go h k x s $ BitmapIndexed (mask hy s) (A.singleton t) - where - snocNewLeaf :: Leaf k v -> A.Array (Leaf k v) -> A.Array (Leaf k v) - snocNewLeaf leaf ary = A.run $ do - let n = A.length ary - mary <- A.new_ (n + 1) - A.copy ary 0 mary 0 n - A.write mary n leaf - return mary + go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# NOINLINE insertNewKey #-} -- Insert optimized for the case when we know the key is in the map. -- --- It is only valid to call this when the key exists in the map and you know the --- hash collision position if there was one. This information can be obtained --- from 'lookupRecordCollision'. If there is no collision pass (-1) as collPos --- (first argument). --- -- We can skip the key equality check on a Leaf because we know the leaf must be -- for this key. -insertKeyExists :: Int -> Hash -> k -> v -> HashMap k v -> HashMap k v -insertKeyExists !collPos0 !h0 !k0 x0 !m0 = go collPos0 h0 k0 x0 0 m0 +insertKeyExists :: (Hashable k) => Hash -> Salt -> k -> v -> HashMap k v -> HashMap k v +insertKeyExists !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 where - go !_collPos !h !k x !_s (Leaf _hy _kx) + go !h _ !k x !_s (Leaf _hy _kx) = Leaf h (L k x) - go collPos h k x s (BitmapIndexed b ary) + go h s k x bs (BitmapIndexed b ary) | b .&. m == 0 = let !ary' = A.insert ary i $ Leaf h (L k x) in bitmapIndexedOrFull (b .|. m) ary' | otherwise = let !st = A.index ary i - !st' = go collPos h k x (s+bitsPerSubkey) st + !st' = go h s k x (bs+bitsPerSubkey) st in BitmapIndexed b (A.update ary i st') - where m = mask h s + where m = mask h bs i = sparseIndex b m - go collPos h k x s (Full ary) = + go h s k x bs (Full ary) = let !st = A.index ary i - !st' = go collPos h k x (s+bitsPerSubkey) st + !st' = go h s k x (bs+bitsPerSubkey) st in Full (update16 ary i st') - where i = index h s - go collPos h k x _s (Collision _hy v) - | collPos >= 0 = Collision h (setAtPosition collPos k x v) - | otherwise = Empty -- error "Internal error: go {collPos negative}" + where i = index h bs + go h s k x bs t@(Collision hx sx hmx) + | h == hx = Collision hx sx $ insertKeyExists (hashWithSalt sx k) sx k x hmx + | otherwise = + go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) go _ _ _ _ _ Empty = Empty -- error "Internal error: go Empty" {-# NOINLINE insertKeyExists #-} @@ -768,37 +771,38 @@ setAtPosition i k x ary = A.update ary i (L k x) -- | In-place update version of insert unsafeInsert :: (Eq k, Hashable k) => k -> v -> HashMap k v -> HashMap k v -unsafeInsert k0 v0 m0 = runST (go h0 k0 v0 0 m0) +unsafeInsert k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) where - h0 = hash k0 - go !h !k x !_ Empty = return $! Leaf h (L k x) - go h k x s t@(Leaf hy l@(L ky y)) + h0 = hashWithSalt s0 k0 + s0 = defaultSalt + go !h _ !k x !_ Empty = return $! Leaf h (L k x) + go h s k x bs t@(Leaf hy l@(L ky y)) | hy == h = if ky == k then if x `ptrEq` y then return t else return $! Leaf h (L k x) - else return $! collision h l (L k x) - | otherwise = two s h k x hy ky y - go h k x s t@(BitmapIndexed b ary) + else return $! collision h s l (L k x) + | otherwise = two bs h k x hy ky y + go h s k x bs t@(BitmapIndexed b ary) | b .&. m == 0 = do ary' <- A.insertM ary i $! Leaf h (L k x) return $! bitmapIndexedOrFull (b .|. m) ary' | otherwise = do st <- A.indexM ary i - st' <- go h k x (s+bitsPerSubkey) st + st' <- go h s k x (bs+bitsPerSubkey) st A.unsafeUpdateM ary i st' return t - where m = mask h s + where m = mask h bs i = sparseIndex b m - go h k x s t@(Full ary) = do + go h s k x bs t@(Full ary) = do st <- A.indexM ary i - st' <- go h k x (s+bitsPerSubkey) st + st' <- go h s k x (bs+bitsPerSubkey) st A.unsafeUpdateM ary i st' return t - where i = index h s - go h k x s t@(Collision hy v) - | h == hy = return $! Collision h (updateOrSnocWith const k x v) - | otherwise = go h k x s $ BitmapIndexed (mask hy s) (A.singleton t) + where i = index h bs + go h s k x bs t@(Collision hx sx hmx) + | h == hx = Collision hx sx <$> go (hashWithSalt sx k) sx k x 0 hmx + | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE unsafeInsert #-} -- | Create a map from two key-value pairs which hashes don't collide. @@ -843,45 +847,43 @@ insertWith f k new m = insertModifying new (\old -> (# f new old #)) k m -- thunks in the tree. insertModifying :: (Eq k, Hashable k) => v -> (v -> (# v #)) -> k -> HashMap k v -> HashMap k v -insertModifying x f k0 m0 = go h0 k0 0 m0 +insertModifying x f k0 m0 = go h0 s0 k0 0 m0 where - !h0 = hash k0 - go !h !k !_ Empty = Leaf h (L k x) - go h k s t@(Leaf hy l@(L ky y)) + !h0 = hashWithSalt s0 k0 + !s0 = defaultSalt + go !h _ !k !_ Empty = Leaf h (L k x) + go h s k bs t@(Leaf hy l@(L ky y)) | hy == h = if ky == k then case f y of (# v' #) | ptrEq y v' -> t | otherwise -> Leaf h (L k (v')) - else collision h l (L k x) - | otherwise = runST (two s h k x hy ky y) - go h k s t@(BitmapIndexed b ary) + else collision h s l (L k x) + | otherwise = runST (two bs h k x hy ky y) + go h s k bs t@(BitmapIndexed b ary) | b .&. m == 0 = let ary' = A.insert ary i $! Leaf h (L k x) in bitmapIndexedOrFull (b .|. m) ary' | otherwise = let !st = A.index ary i - !st' = go h k (s+bitsPerSubkey) st + !st' = go h s k (bs+bitsPerSubkey) st ary' = A.update ary i $! st' in if ptrEq st st' then t else BitmapIndexed b ary' - where m = mask h s + where m = mask h bs i = sparseIndex b m - go h k s t@(Full ary) = + go h s k bs t@(Full ary) = let !st = A.index ary i - !st' = go h k (s+bitsPerSubkey) st + !st' = go h s k (bs+bitsPerSubkey) st ary' = update16 ary i $! st' in if ptrEq st st' then t else Full ary' - where i = index h s - go h k s t@(Collision hy v) - | h == hy = - let !v' = insertModifyingArr x f k v - in if A.unsafeSameArray v v' - then t - else Collision h v' - | otherwise = go h k s $ BitmapIndexed (mask hy s) (A.singleton t) + where i = index h bs + go h s k bs t@(Collision hx sx hmx) + | h == hx = + go (hashWithSalt sx k) sx k 0 hmx + | otherwise = go h s k bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insertModifying #-} -- Like insertModifying for arrays; used to implement insertModifying @@ -908,56 +910,57 @@ insertModifyingArr x f k0 ary0 = go k0 ary0 0 (A.length ary0) unsafeInsertWith :: forall k v. (Eq k, Hashable k) => (v -> v -> v) -> k -> v -> HashMap k v -> HashMap k v -unsafeInsertWith f k0 v0 m0 = runST (go h0 k0 v0 0 m0) +unsafeInsertWith f k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) where - h0 = hash k0 - go :: Hash -> k -> v -> Shift -> HashMap k v -> ST s (HashMap k v) - go !h !k x !_ Empty = return $! Leaf h (L k x) - go h k x s (Leaf hy l@(L ky y)) + h0 = hashWithSalt s0 k0 + s0 = defaultSalt + go :: Hash -> Salt -> k -> v -> Shift -> HashMap k v -> ST s (HashMap k v) + go !h _ !k x !_ Empty = return $! Leaf h (L k x) + go h s k x bs (Leaf hy l@(L ky y)) | hy == h = if ky == k then return $! Leaf h (L k (f x y)) - else return $! collision h l (L k x) - | otherwise = two s h k x hy ky y - go h k x s t@(BitmapIndexed b ary) + else return $! collision h s l (L k x) + | otherwise = two bs h k x hy ky y + go h s k x bs t@(BitmapIndexed b ary) | b .&. m == 0 = do ary' <- A.insertM ary i $! Leaf h (L k x) return $! bitmapIndexedOrFull (b .|. m) ary' | otherwise = do st <- A.indexM ary i - st' <- go h k x (s+bitsPerSubkey) st + st' <- go h s k x (bs+bitsPerSubkey) st A.unsafeUpdateM ary i st' return t - where m = mask h s + where m = mask h bs i = sparseIndex b m - go h k x s t@(Full ary) = do + go h s k x bs t@(Full ary) = do st <- A.indexM ary i - st' <- go h k x (s+bitsPerSubkey) st + st' <- go h s k x (bs+bitsPerSubkey) st A.unsafeUpdateM ary i st' return t - where i = index h s - go h k x s t@(Collision hy v) - | h == hy = return $! Collision h (updateOrSnocWith f k x v) - | otherwise = go h k x s $ BitmapIndexed (mask hy s) (A.singleton t) + where i = index h bs + go h s k x bs t@(Collision hx sx hmx) + | h == hx = Collision hx sx <$> go (hashWithSalt sx k) sx k x 0 hmx + | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE unsafeInsertWith #-} -- | /O(log n)/ Remove the mapping for the specified key from this map -- if present. delete :: (Eq k, Hashable k) => k -> HashMap k v -> HashMap k v -delete k m = delete' (hash k) k m +delete k m = delete' (hashWithSalt defaultSalt k) defaultSalt k m {-# INLINABLE delete #-} -delete' :: Eq k => Hash -> k -> HashMap k v -> HashMap k v -delete' h0 k0 m0 = go h0 k0 0 m0 +delete' :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> HashMap k v +delete' h0 s0 k0 m0 = go h0 s0 k0 0 m0 where - go !_ !_ !_ Empty = Empty - go h k _ t@(Leaf hy (L ky _)) + go !_ _ !_ !_ Empty = Empty + go h _ k _ t@(Leaf hy (L ky _)) | hy == h && ky == k = Empty | otherwise = t - go h k s t@(BitmapIndexed b ary) + go h s k bs t@(BitmapIndexed b ary) | b .&. m == 0 = t | otherwise = let !st = A.index ary i - !st' = go h k (s+bitsPerSubkey) st + !st' = go h s k (bs+bitsPerSubkey) st in if st' `ptrEq` st then t else case st' of @@ -972,11 +975,11 @@ delete' h0 k0 m0 = go h0 k0 0 m0 bIndexed = BitmapIndexed (b .&. complement m) (A.delete ary i) l | isLeafOrCollision l && A.length ary == 1 -> l _ -> BitmapIndexed b (A.update ary i st') - where m = mask h s + where m = mask h bs i = sparseIndex b m - go h k s t@(Full ary) = + go h s k bs t@(Full ary) = let !st = A.index ary i - !st' = go h k (s+bitsPerSubkey) st + !st' = go h s k (bs+bitsPerSubkey) st in if st' `ptrEq` st then t else case st' of @@ -985,16 +988,9 @@ delete' h0 k0 m0 = go h0 k0 0 m0 bm = fullNodeMask .&. complement (1 `unsafeShiftL` i) in BitmapIndexed bm ary' _ -> Full (A.update ary i st') - where i = index h s - go h k _ t@(Collision hy v) - | h == hy = case indexOf k v of - Just i - | A.length v == 2 -> - if i == 0 - then Leaf h (A.index v 1) - else Leaf h (A.index v 0) - | otherwise -> Collision h (A.delete v i) - Nothing -> t + where i = index h bs + go h s k _ t@(Collision hx sx hmx) + | h == hx = go (hashWithSalt sx k) sx k 0 hmx | otherwise = t {-# INLINABLE delete' #-} From 561214d03c837785d8ea6d139e6f92249340a8ce Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 14 Aug 2018 14:43:08 +0200 Subject: [PATCH 03/69] Until unionWithKey --- Data/HashMap/Base.hs | 75 ++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 58eb42fe..5ff2da4d 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -996,20 +996,16 @@ delete' h0 s0 k0 m0 = go h0 s0 k0 0 m0 -- | Delete optimized for the case when we know the key is in the map. -- --- It is only valid to call this when the key exists in the map and you know the --- hash collision position if there was one. This information can be obtained --- from 'lookupRecordCollision'. If there is no collision pass (-1) as collPos. --- -- We can skip: -- - the key equality check on the leaf, if we reach a leaf it must be the key -deleteKeyExists :: Int -> Hash -> k -> HashMap k v -> HashMap k v -deleteKeyExists !collPos0 !h0 !k0 !m0 = go collPos0 h0 k0 0 m0 +deleteKeyExists :: Hashable k => Hash -> Salt -> k -> HashMap k v -> HashMap k v +deleteKeyExists !h0 s0 !k0 !m0 = go h0 s0 k0 0 m0 where - go :: Int -> Hash -> k -> Int -> HashMap k v -> HashMap k v - go !_collPos !_h !_k !_s (Leaf _ _) = Empty - go collPos h k s (BitmapIndexed b ary) = + go :: Hashable k => Hash -> Salt -> k -> Int -> HashMap k v -> HashMap k v + go !_h !_s !_k !_bs (Leaf _ _) = Empty + go h s k bs (BitmapIndexed b ary) = let !st = A.index ary i - !st' = go collPos h k (s+bitsPerSubkey) st + !st' = go h s k (bs+bitsPerSubkey) st in case st' of Empty | A.length ary == 1 -> Empty | A.length ary == 2 -> @@ -1022,24 +1018,21 @@ deleteKeyExists !collPos0 !h0 !k0 !m0 = go collPos0 h0 k0 0 m0 bIndexed = BitmapIndexed (b .&. complement m) (A.delete ary i) l | isLeafOrCollision l && A.length ary == 1 -> l _ -> BitmapIndexed b (A.update ary i st') - where m = mask h s + where m = mask h bs i = sparseIndex b m - go collPos h k s (Full ary) = + go h s k bs (Full ary) = let !st = A.index ary i - !st' = go collPos h k (s+bitsPerSubkey) st + !st' = go h s k (bs+bitsPerSubkey) st in case st' of Empty -> let ary' = A.delete ary i bm = fullNodeMask .&. complement (1 `unsafeShiftL` i) in BitmapIndexed bm ary' _ -> Full (A.update ary i st') - where i = index h s - go collPos h _ _ (Collision _hy v) - | A.length v == 2 - = if collPos == 0 - then Leaf h (A.index v 1) - else Leaf h (A.index v 0) - | otherwise = Collision h (A.delete v collPos) + where i = index h bs + go h s k _ t@(Collision hx sx hmx) + | h == hx = go (hashWithSalt sx k) sx k 0 hmx + | otherwise = Empty -- error "Internal error: unexpected collision" go !_ !_ !_ !_ Empty = Empty -- error "Internal error: deleteKeyExists empty" {-# NOINLINE deleteKeyExists #-} @@ -1060,36 +1053,34 @@ adjust f k m = adjust# (\v -> (# f v #)) k m adjust# :: (Eq k, Hashable k) => (v -> (# v #)) -> k -> HashMap k v -> HashMap k v adjust# f k0 m0 = go h0 k0 0 m0 where - h0 = hash k0 + h0 = hashWithSalt s0 k0 + s0 = defaultSalt go !_ !_ !_ Empty = Empty go h k _ t@(Leaf hy (L ky y)) | hy == h && ky == k = case f y of (# y' #) | ptrEq y y' -> t | otherwise -> Leaf h (L k y') | otherwise = t - go h k s t@(BitmapIndexed b ary) + go h k bs t@(BitmapIndexed b ary) | b .&. m == 0 = t | otherwise = let !st = A.index ary i - !st' = go h k (s+bitsPerSubkey) st + !st' = go h k (bs+bitsPerSubkey) st ary' = A.update ary i $! st' in if ptrEq st st' then t else BitmapIndexed b ary' - where m = mask h s + where m = mask h bs i = sparseIndex b m - go h k s t@(Full ary) = - let i = index h s + go h k bs t@(Full ary) = + let i = index h bs !st = A.index ary i - !st' = go h k (s+bitsPerSubkey) st + !st' = go h k (bs+bitsPerSubkey) st ary' = update16 ary i $! st' in if ptrEq st st' then t else Full ary' - go h k _ t@(Collision hy v) - | h == hy = let !v' = updateWith# f k v - in if A.unsafeSameArray v v' - then t - else Collision h v' + go h k _ t@(Collision hx sx hmx) + | h == hx = go (hashWithSalt sx k) k 0 hmx | otherwise = t {-# INLINABLE adjust# #-} @@ -1129,12 +1120,13 @@ alterF :: (Functor f, Eq k, Hashable k) -- @f@ and a functor that is similar to Const but not actually Const. alterF f = \ !k !m -> let - !h = hash k - mv = lookup' h k m + !h = hashWithSalt s k + s = defaultSalt + mv = lookup' h s k m in (<$> f mv) $ \fres -> case fres of - Nothing -> delete' h k m - Just v' -> insert' h k v' m + Nothing -> delete' h s k m + Just v' -> insert' h s k v' m -- We unconditionally rewrite alterF in RULES, but we expose an -- unfolding just in case it's used in some way that prevents the @@ -1235,14 +1227,14 @@ alterFEager f !k m = (<$> f mv) $ \fres -> Absent -> m -- Key did exist - Present _ collPos -> deleteKeyExists collPos h k m + Present _ _ -> deleteKeyExists h s k m ------------------------------ -- Update value Just v' -> case lookupRes of -- Key did not exist before, insert v' under a new key - Absent -> insertNewKey h k v' m + Absent -> insertNewKey h s k v' m -- Key existed before Present v collPos -> @@ -1250,10 +1242,11 @@ alterFEager f !k m = (<$> f mv) $ \fres -> -- If the value is identical, no-op then m -- If the value changed, update the value. - else insertKeyExists collPos h k v' m + else insertKeyExists h s k v' m - where !h = hash k - !lookupRes = lookupRecordCollision h k m + where !h = hashWithSalt s k + !s = defaultSalt + !lookupRes = lookupRecordCollision h s k m !mv = case lookupRes of Absent -> Nothing Present v _ -> Just v From 69a5f957fcca725dfd11a7bc473b9ffa0ef24216 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 14 Aug 2018 14:50:59 +0200 Subject: [PATCH 04/69] Until mapWithKey --- Data/HashMap/Base.hs | 81 ++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 5ff2da4d..3e3ddcde 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1276,86 +1276,87 @@ unionWith f = unionWithKey (const f) -- result. unionWithKey :: (Eq k, Hashable k) => (k -> v -> v -> v) -> HashMap k v -> HashMap k v -> HashMap k v -unionWithKey f = go 0 +unionWithKey f = go 0 s0 where + s0 = defaultSalt -- empty vs. anything - go !_ t1 Empty = t1 - go _ Empty t2 = t2 + go !_ !_ t1 Empty = t1 + go _ _ Empty t2 = t2 -- leaf vs. leaf - go s t1@(Leaf h1 l1@(L k1 v1)) t2@(Leaf h2 l2@(L k2 v2)) + go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Leaf h2 l2@(L k2 v2)) | h1 == h2 = if k1 == k2 then Leaf h1 (L k1 (f k1 v1 v2)) - else collision h1 l1 l2 - | otherwise = goDifferentHash s h1 h2 t1 t2 - go s t1@(Leaf h1 (L k1 v1)) t2@(Collision h2 ls2) - | h1 == h2 = Collision h1 (updateOrSnocWithKey f k1 v1 ls2) - | otherwise = goDifferentHash s h1 h2 t1 t2 - go s t1@(Collision h1 ls1) t2@(Leaf h2 (L k2 v2)) - | h1 == h2 = Collision h1 (updateOrSnocWithKey (flip . f) k2 v2 ls1) - | otherwise = goDifferentHash s h1 h2 t1 t2 - go s t1@(Collision h1 ls1) t2@(Collision h2 ls2) - | h1 == h2 = Collision h1 (updateOrConcatWithKey f ls1 ls2) - | otherwise = goDifferentHash s h1 h2 t1 t2 + else collision h1 s l1 l2 + | otherwise = goDifferentHash bs s h1 h2 t1 t2 + go bs s t1@(Leaf h1 (L k1 v1)) t2@(Collision h2 s2 hm2) + | h1 == h2 = Collision h1 s2 $ insert' (hashWithSalt s2 k1) s2 k1 v1 hm2 + | otherwise = goDifferentHash bs s h1 h2 t1 t2 + go bs s t1@(Collision h1 s1 hm1) t2@(Leaf h2 (L k2 v2)) + | h1 == h2 = Collision h1 s1 $ insert' (hashWithSalt s1 k2) s1 k2 v2 hm1 + | otherwise = goDifferentHash bs s h1 h2 t1 t2 + go bs s t1@(Collision h1 s1 hm1) t2@(Collision h2 _ hm2) -- Second salt must be equal + | h1 == h2 = Collision h1 s1 $ go 0 s1 hm1 hm2 + | otherwise = goDifferentHash bs s h1 h2 t1 t2 -- branch vs. branch - go s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) = + go bs s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) = let b' = b1 .|. b2 - ary' = unionArrayBy (go (s+bitsPerSubkey)) b1 b2 ary1 ary2 + ary' = unionArrayBy (go (bs+bitsPerSubkey) s) b1 b2 ary1 ary2 in bitmapIndexedOrFull b' ary' - go s (BitmapIndexed b1 ary1) (Full ary2) = - let ary' = unionArrayBy (go (s+bitsPerSubkey)) b1 fullNodeMask ary1 ary2 + go bs s (BitmapIndexed b1 ary1) (Full ary2) = + let ary' = unionArrayBy (go (bs+bitsPerSubkey) s) b1 fullNodeMask ary1 ary2 in Full ary' - go s (Full ary1) (BitmapIndexed b2 ary2) = - let ary' = unionArrayBy (go (s+bitsPerSubkey)) fullNodeMask b2 ary1 ary2 + go bs s (Full ary1) (BitmapIndexed b2 ary2) = + let ary' = unionArrayBy (go (bs+bitsPerSubkey) s) fullNodeMask b2 ary1 ary2 in Full ary' - go s (Full ary1) (Full ary2) = - let ary' = unionArrayBy (go (s+bitsPerSubkey)) fullNodeMask fullNodeMask + go bs s (Full ary1) (Full ary2) = + let ary' = unionArrayBy (go (bs+bitsPerSubkey) s) fullNodeMask fullNodeMask ary1 ary2 in Full ary' -- leaf vs. branch - go s (BitmapIndexed b1 ary1) t2 + go bs s (BitmapIndexed b1 ary1) t2 | b1 .&. m2 == 0 = let ary' = A.insert ary1 i t2 b' = b1 .|. m2 in bitmapIndexedOrFull b' ary' | otherwise = let ary' = A.updateWith' ary1 i $ \st1 -> - go (s+bitsPerSubkey) st1 t2 + go (bs+bitsPerSubkey) s st1 t2 in BitmapIndexed b1 ary' where h2 = leafHashCode t2 - m2 = mask h2 s + m2 = mask h2 bs i = sparseIndex b1 m2 - go s t1 (BitmapIndexed b2 ary2) + go bs s t1 (BitmapIndexed b2 ary2) | b2 .&. m1 == 0 = let ary' = A.insert ary2 i $! t1 b' = b2 .|. m1 in bitmapIndexedOrFull b' ary' | otherwise = let ary' = A.updateWith' ary2 i $ \st2 -> - go (s+bitsPerSubkey) t1 st2 + go (bs+bitsPerSubkey) s t1 st2 in BitmapIndexed b2 ary' where h1 = leafHashCode t1 - m1 = mask h1 s + m1 = mask h1 bs i = sparseIndex b2 m1 - go s (Full ary1) t2 = + go bs s (Full ary1) t2 = let h2 = leafHashCode t2 - i = index h2 s - ary' = update16With' ary1 i $ \st1 -> go (s+bitsPerSubkey) st1 t2 + i = index h2 bs + ary' = update16With' ary1 i $ \st1 -> go (bs+bitsPerSubkey) s st1 t2 in Full ary' - go s t1 (Full ary2) = + go bs s t1 (Full ary2) = let h1 = leafHashCode t1 - i = index h1 s - ary' = update16With' ary2 i $ \st2 -> go (s+bitsPerSubkey) t1 st2 + i = index h1 bs + ary' = update16With' ary2 i $ \st2 -> go (bs+bitsPerSubkey) s t1 st2 in Full ary' leafHashCode (Leaf h _) = h - leafHashCode (Collision h _) = h + leafHashCode (Collision h _ _) = h leafHashCode _ = error "leafHashCode" - goDifferentHash s h1 h2 t1 t2 - | m1 == m2 = BitmapIndexed m1 (A.singleton $! go (s+bitsPerSubkey) t1 t2) + goDifferentHash bs s h1 h2 t1 t2 + | m1 == m2 = BitmapIndexed m1 (A.singleton $! go (bs+bitsPerSubkey) s t1 t2) | m1 < m2 = BitmapIndexed (m1 .|. m2) (A.pair t1 t2) | otherwise = BitmapIndexed (m1 .|. m2) (A.pair t2 t1) where - m1 = mask h1 s - m2 = mask h2 s + m1 = mask h1 bs + m2 = mask h2 bs {-# INLINE unionWithKey #-} -- | Strict in the result of @f@. From aaf7dda9f276062fba66387b589a5fe8d536002a Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 14 Aug 2018 14:52:45 +0200 Subject: [PATCH 05/69] until filterMapAux --- Data/HashMap/Base.hs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 3e3ddcde..c0f95556 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1409,8 +1409,7 @@ mapWithKey f = go go (Full ary) = Full $ A.map go ary -- Why map strictly over collision arrays? Because there's no -- point suspending the O(1) work this does for each leaf. - go (Collision h ary) = Collision h $ - A.map' (\ (L k v) -> L k (f k v)) ary + go (Collision h s hm) = Collision h s $ go hm {-# INLINE mapWithKey #-} -- | /O(n)/ Transform this map by applying a function to every value. @@ -1438,8 +1437,8 @@ traverseWithKey f = go go (Leaf h (L k v)) = Leaf h . L k <$> f k v go (BitmapIndexed b ary) = BitmapIndexed b <$> A.traverse go ary go (Full ary) = Full <$> A.traverse go ary - go (Collision h ary) = - Collision h <$> A.traverse' (\ (L k v) -> L k <$> f k v) ary + go (Collision h s hm) = + Collision h s <$> go hm {-# INLINE traverseWithKey #-} ------------------------------------------------------------------------ @@ -1525,7 +1524,7 @@ foldlWithKey' f = go go z (Leaf _ (L k v)) = f z k v go z (BitmapIndexed _ ary) = A.foldl' go z ary go z (Full ary) = A.foldl' go z ary - go z (Collision _ ary) = A.foldl' (\ z' (L k v) -> f z' k v) z ary + go z (Collision _ _ hm) = go z hm {-# INLINE foldlWithKey' #-} -- | /O(n)/ Reduce this map by applying a binary operator to all @@ -1545,7 +1544,7 @@ foldrWithKey f = go go z (Leaf _ (L k v)) = f k v z go z (BitmapIndexed _ ary) = A.foldr (flip go) z ary go z (Full ary) = A.foldr (flip go) z ary - go z (Collision _ ary) = A.foldr (\ (L k v) z' -> f k v z') z ary + go z (Collision _ _ hm) = go z hm {-# INLINE foldrWithKey #-} ------------------------------------------------------------------------ From 609881947c005293a7ba09264556d6f6b4f55086 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 14 Aug 2018 14:56:13 +0200 Subject: [PATCH 06/69] Finished a module --- Data/HashMap/Base.hs | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index c0f95556..bfa6a107 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -82,7 +82,7 @@ module Data.HashMap.Base , Bitmap , bitmapIndexedOrFull , collision - , hash + , hashWithSalt , mask , index , bitsPerSubkey @@ -1594,7 +1594,7 @@ filterMapAux onLeaf onColl = go | otherwise = Empty go (BitmapIndexed b ary) = filterA ary b go (Full ary) = filterA ary fullNodeMask - go (Collision h ary) = filterC ary h + go (Collision _ _ hm) = go hm filterA ary0 b0 = let !n = A.length ary0 @@ -1624,28 +1624,6 @@ filterMapAux onLeaf onColl = go (bi `unsafeShiftL` 1) n t -> do A.write mary j t step ary mary b (i+1) (j+1) (bi `unsafeShiftL` 1) n - - filterC ary0 h = - let !n = A.length ary0 - in runST $ do - mary <- A.new_ n - step ary0 mary 0 0 n - where - step :: A.Array (Leaf k v1) -> A.MArray s (Leaf k v2) - -> Int -> Int -> Int - -> ST s (HashMap k v2) - step !ary !mary i !j n - | i >= n = case j of - 0 -> return Empty - 1 -> do l <- A.read mary 0 - return $! Leaf h l - _ | i == j -> do ary2 <- A.unsafeFreeze mary - return $! Collision h ary2 - | otherwise -> do ary2 <- A.trim mary j - return $! Collision h ary2 - | Just el <- onColl $! A.index ary i - = A.write mary j el >> step ary mary (i+1) (j+1) n - | otherwise = step ary mary (i+1) j n {-# INLINE filterMapAux #-} -- | /O(n)/ Filter this map by retaining only elements which values From 209ee6b864d91a35a4614c354d67a5b1524c5921 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 14 Aug 2018 15:19:30 +0200 Subject: [PATCH 07/69] Something that compiles --- Data/HashMap/Base.hs | 1 + Data/HashMap/Strict/Base.hs | 196 +++++++++++++++++++----------------- 2 files changed, 102 insertions(+), 95 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index bfa6a107..05fa160c 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -83,6 +83,7 @@ module Data.HashMap.Base , bitmapIndexedOrFull , collision , hashWithSalt + , defaultSalt , mask , index , bitsPerSubkey diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index 890d18ff..c5edb9b2 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -148,99 +148,102 @@ insert k !v = HM.insert k v -- > where f new old = new + old insertWith :: (Eq k, Hashable k) => (v -> v -> v) -> k -> v -> HashMap k v -> HashMap k v -insertWith f k0 v0 m0 = go h0 k0 v0 0 m0 +insertWith f k0 v0 m0 = go h0 s0 k0 v0 0 m0 where - h0 = hash k0 - go !h !k x !_ Empty = leaf h k x - go h k x s (Leaf hy l@(L ky y)) + h0 = hashWithSalt s0 k0 + s0 = defaultSalt + go !h _ !k x !_ Empty = leaf h k x + go h s k x bs (Leaf hy l@(L ky y)) | hy == h = if ky == k then leaf h k (f x y) - else x `seq` (collision h l (L k x)) - | otherwise = x `seq` runST (two s h k x hy ky y) - go h k x s (BitmapIndexed b ary) + else x `seq` (collision h s l (L k x)) + | otherwise = x `seq` runST (two bs h k x hy ky y) + go h s k x bs (BitmapIndexed b ary) | b .&. m == 0 = let ary' = A.insert ary i $! leaf h k x in bitmapIndexedOrFull (b .|. m) ary' | otherwise = let st = A.index ary i - st' = go h k x (s+bitsPerSubkey) st + st' = go h s k x (bs+bitsPerSubkey) st ary' = A.update ary i $! st' in BitmapIndexed b ary' - where m = mask h s + where m = mask h bs i = sparseIndex b m - go h k x s (Full ary) = + go h s k x bs (Full ary) = let st = A.index ary i - st' = go h k x (s+bitsPerSubkey) st + st' = go h s k x (bs+bitsPerSubkey) st ary' = update16 ary i $! st' in Full ary' - where i = index h s - go h k x s t@(Collision hy v) - | h == hy = Collision h (updateOrSnocWith f k x v) - | otherwise = go h k x s $ BitmapIndexed (mask hy s) (A.singleton t) + where i = index h bs + go h s k x bs t@(Collision hx sx hm) + | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k x 0 hm + | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insertWith #-} -- | In-place update version of insertWith unsafeInsertWith :: (Eq k, Hashable k) => (v -> v -> v) -> k -> v -> HashMap k v -> HashMap k v -unsafeInsertWith f k0 v0 m0 = runST (go h0 k0 v0 0 m0) +unsafeInsertWith f k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) where - h0 = hash k0 - go !h !k x !_ Empty = return $! leaf h k x - go h k x s (Leaf hy l@(L ky y)) + h0 = hashWithSalt s0 k0 + s0 = defaultSalt + go !h !_ !k x !_ Empty = return $! leaf h k x + go h s k x bs (Leaf hy l@(L ky y)) | hy == h = if ky == k then return $! leaf h k (f x y) else do let l' = x `seq` (L k x) - return $! collision h l l' - | otherwise = x `seq` two s h k x hy ky y - go h k x s t@(BitmapIndexed b ary) + return $! collision h s l l' + | otherwise = x `seq` two bs h k x hy ky y + go h s k x bs t@(BitmapIndexed b ary) | b .&. m == 0 = do ary' <- A.insertM ary i $! leaf h k x return $! bitmapIndexedOrFull (b .|. m) ary' | otherwise = do st <- A.indexM ary i - st' <- go h k x (s+bitsPerSubkey) st + st' <- go h s k x (bs+bitsPerSubkey) st A.unsafeUpdateM ary i st' return t - where m = mask h s + where m = mask h bs i = sparseIndex b m - go h k x s t@(Full ary) = do + go h s k x bs t@(Full ary) = do st <- A.indexM ary i - st' <- go h k x (s+bitsPerSubkey) st + st' <- go h s k x (bs+bitsPerSubkey) st A.unsafeUpdateM ary i st' return t - where i = index h s - go h k x s t@(Collision hy v) - | h == hy = return $! Collision h (updateOrSnocWith f k x v) - | otherwise = go h k x s $ BitmapIndexed (mask hy s) (A.singleton t) + where i = index h bs + go h s k x bs t@(Collision hx sx hm) + | h == hx = Collision hx sx <$> go (hashWithSalt sx k) sx k x 0 hm + | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE unsafeInsertWith #-} -- | /O(log n)/ Adjust the value tied to a given key in this map only -- if it is present. Otherwise, leave the map alone. adjust :: (Eq k, Hashable k) => (v -> v) -> k -> HashMap k v -> HashMap k v -adjust f k0 m0 = go h0 k0 0 m0 +adjust f k0 m0 = go h0 s0 k0 0 m0 where - h0 = hash k0 - go !_ !_ !_ Empty = Empty - go h k _ t@(Leaf hy (L ky y)) + h0 = hashWithSalt s0 k0 + s0 = defaultSalt + go !_ !_ !_ !_ Empty = Empty + go h _ k _ t@(Leaf hy (L ky y)) | hy == h && ky == k = leaf h k (f y) | otherwise = t - go h k s t@(BitmapIndexed b ary) + go h s k bs t@(BitmapIndexed b ary) | b .&. m == 0 = t | otherwise = let st = A.index ary i - st' = go h k (s+bitsPerSubkey) st + st' = go h s k (bs+bitsPerSubkey) st ary' = A.update ary i $! st' in BitmapIndexed b ary' - where m = mask h s + where m = mask h bs i = sparseIndex b m - go h k s (Full ary) = - let i = index h s + go h s k bs (Full ary) = + let i = index h bs st = A.index ary i - st' = go h k (s+bitsPerSubkey) st + st' = go h s k (bs+bitsPerSubkey) st ary' = update16 ary i $! st' in Full ary' - go h k _ t@(Collision hy v) - | h == hy = Collision h (updateWith f k v) + go h s k _ t@(Collision hx sx hm) + | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k 0 hm | otherwise = t {-# INLINABLE adjust #-} @@ -277,12 +280,13 @@ alterF :: (Functor f, Eq k, Hashable k) -- version; otherwise someone could tell the difference using a lazy -- @f@ and a functor that is similar to Const but not actually Const. alterF f = \ !k !m -> - let !h = hash k - mv = lookup' h k m + let !h = hashWithSalt s k + s = defaultSalt + mv = lookup' h s k m in (<$> f mv) $ \fres -> case fres of - Nothing -> delete' h k m - Just !v' -> insert' h k v' m + Nothing -> delete' h s k m + Just !v' -> insert' h s k v' m -- We rewrite this function unconditionally in RULES, but we expose -- an unfolding just in case it's used in a context where the rules @@ -361,14 +365,14 @@ alterFEager f !k !m = (<$> f mv) $ \fres -> Absent -> m -- Key did exist, no collision - Present _ collPos -> deleteKeyExists collPos h k m + Present _ _ -> deleteKeyExists h s k m ------------------------------ -- Update value Just v' -> case lookupRes of -- Key did not exist before, insert v' under a new key - Absent -> insertNewKey h k v' m + Absent -> insertNewKey h s k v' m -- Key existed before, no hash collision Present v collPos -> v' `seq` @@ -376,10 +380,11 @@ alterFEager f !k !m = (<$> f mv) $ \fres -> -- If the value is identical, no-op then m -- If the value changed, update the value. - else insertKeyExists collPos h k v' m + else insertKeyExists h s k v' m - where !h = hash k - !lookupRes = lookupRecordCollision h k m + where !h = hashWithSalt s k + !s = defaultSalt + !lookupRes = lookupRecordCollision h s k m !mv = case lookupRes of Absent -> Nothing Present v _ -> Just v @@ -400,86 +405,87 @@ unionWith f = unionWithKey (const f) -- the provided function (first argument) will be used to compute the result. unionWithKey :: (Eq k, Hashable k) => (k -> v -> v -> v) -> HashMap k v -> HashMap k v -> HashMap k v -unionWithKey f = go 0 +unionWithKey f = go 0 s0 where + s0 = defaultSalt -- empty vs. anything - go !_ t1 Empty = t1 - go _ Empty t2 = t2 + go !_ !_ t1 Empty = t1 + go _ _ Empty t2 = t2 -- leaf vs. leaf - go s t1@(Leaf h1 l1@(L k1 v1)) t2@(Leaf h2 l2@(L k2 v2)) + go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Leaf h2 l2@(L k2 v2)) | h1 == h2 = if k1 == k2 then leaf h1 k1 (f k1 v1 v2) - else collision h1 l1 l2 - | otherwise = goDifferentHash s h1 h2 t1 t2 - go s t1@(Leaf h1 (L k1 v1)) t2@(Collision h2 ls2) - | h1 == h2 = Collision h1 (updateOrSnocWithKey f k1 v1 ls2) - | otherwise = goDifferentHash s h1 h2 t1 t2 - go s t1@(Collision h1 ls1) t2@(Leaf h2 (L k2 v2)) - | h1 == h2 = Collision h1 (updateOrSnocWithKey (flip . f) k2 v2 ls1) - | otherwise = goDifferentHash s h1 h2 t1 t2 - go s t1@(Collision h1 ls1) t2@(Collision h2 ls2) - | h1 == h2 = Collision h1 (updateOrConcatWithKey f ls1 ls2) - | otherwise = goDifferentHash s h1 h2 t1 t2 + else collision h1 s l1 l2 + | otherwise = goDifferentHash bs s h1 h2 t1 t2 + go bs s t1@(Leaf h1 (L k1 v1)) t2@(Collision h2 s2 hm2) + | h1 == h2 = Collision h1 s2 $ insert' (hashWithSalt s2 k1) s2 k1 v1 hm2 + | otherwise = goDifferentHash bs s h1 h2 t1 t2 + go bs s t1@(Collision h1 s1 hm1) t2@(Leaf h2 (L k2 v2)) + | h1 == h2 = Collision h1 s1 $ insert' (hashWithSalt s1 k2) s1 k2 v2 hm1 + | otherwise = goDifferentHash bs s h1 h2 t1 t2 + go bs s t1@(Collision h1 s1 hm1) t2@(Collision h2 _ hm2) -- Second salt must be equal + | h1 == h2 = Collision h1 s1 $ go 0 s1 hm1 hm2 + | otherwise = goDifferentHash bs s h1 h2 t1 t2 -- branch vs. branch - go s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) = + go bs s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) = let b' = b1 .|. b2 - ary' = unionArrayBy (go (s+bitsPerSubkey)) b1 b2 ary1 ary2 + ary' = unionArrayBy (go (bs+bitsPerSubkey) s) b1 b2 ary1 ary2 in bitmapIndexedOrFull b' ary' - go s (BitmapIndexed b1 ary1) (Full ary2) = - let ary' = unionArrayBy (go (s+bitsPerSubkey)) b1 fullNodeMask ary1 ary2 + go bs s (BitmapIndexed b1 ary1) (Full ary2) = + let ary' = unionArrayBy (go (bs+bitsPerSubkey) s) b1 fullNodeMask ary1 ary2 in Full ary' - go s (Full ary1) (BitmapIndexed b2 ary2) = - let ary' = unionArrayBy (go (s+bitsPerSubkey)) fullNodeMask b2 ary1 ary2 + go bs s (Full ary1) (BitmapIndexed b2 ary2) = + let ary' = unionArrayBy (go (bs+bitsPerSubkey) s) fullNodeMask b2 ary1 ary2 in Full ary' - go s (Full ary1) (Full ary2) = - let ary' = unionArrayBy (go (s+bitsPerSubkey)) fullNodeMask fullNodeMask + go bs s (Full ary1) (Full ary2) = + let ary' = unionArrayBy (go (bs+bitsPerSubkey) s) fullNodeMask fullNodeMask ary1 ary2 in Full ary' -- leaf vs. branch - go s (BitmapIndexed b1 ary1) t2 + go bs s (BitmapIndexed b1 ary1) t2 | b1 .&. m2 == 0 = let ary' = A.insert ary1 i t2 b' = b1 .|. m2 in bitmapIndexedOrFull b' ary' | otherwise = let ary' = A.updateWith' ary1 i $ \st1 -> - go (s+bitsPerSubkey) st1 t2 + go (bs+bitsPerSubkey) s st1 t2 in BitmapIndexed b1 ary' where h2 = leafHashCode t2 - m2 = mask h2 s + m2 = mask h2 bs i = sparseIndex b1 m2 - go s t1 (BitmapIndexed b2 ary2) + go bs s t1 (BitmapIndexed b2 ary2) | b2 .&. m1 == 0 = let ary' = A.insert ary2 i $! t1 b' = b2 .|. m1 in bitmapIndexedOrFull b' ary' | otherwise = let ary' = A.updateWith' ary2 i $ \st2 -> - go (s+bitsPerSubkey) t1 st2 + go (bs+bitsPerSubkey) s t1 st2 in BitmapIndexed b2 ary' where h1 = leafHashCode t1 - m1 = mask h1 s + m1 = mask h1 bs i = sparseIndex b2 m1 - go s (Full ary1) t2 = + go bs s (Full ary1) t2 = let h2 = leafHashCode t2 - i = index h2 s - ary' = update16With' ary1 i $ \st1 -> go (s+bitsPerSubkey) st1 t2 + i = index h2 bs + ary' = update16With' ary1 i $ \st1 -> go (bs+bitsPerSubkey) s st1 t2 in Full ary' - go s t1 (Full ary2) = + go bs s t1 (Full ary2) = let h1 = leafHashCode t1 - i = index h1 s - ary' = update16With' ary2 i $ \st2 -> go (s+bitsPerSubkey) t1 st2 + i = index h1 bs + ary' = update16With' ary2 i $ \st2 -> go (bs+bitsPerSubkey) s t1 st2 in Full ary' leafHashCode (Leaf h _) = h - leafHashCode (Collision h _) = h + leafHashCode (Collision h _ _) = h leafHashCode _ = error "leafHashCode" - goDifferentHash s h1 h2 t1 t2 - | m1 == m2 = BitmapIndexed m1 (A.singleton $! go (s+bitsPerSubkey) t1 t2) + goDifferentHash bs s h1 h2 t1 t2 + | m1 == m2 = BitmapIndexed m1 (A.singleton $! go (bs+bitsPerSubkey) s t1 t2) | m1 < m2 = BitmapIndexed (m1 .|. m2) (A.pair t1 t2) | otherwise = BitmapIndexed (m1 .|. m2) (A.pair t2 t1) where - m1 = mask h1 s - m2 = mask h2 s + m1 = mask h1 bs + m2 = mask h2 bs {-# INLINE unionWithKey #-} ------------------------------------------------------------------------ @@ -493,8 +499,8 @@ mapWithKey f = go go (Leaf h (L k v)) = leaf h k (f k v) go (BitmapIndexed b ary) = BitmapIndexed b $ A.map' go ary go (Full ary) = Full $ A.map' go ary - go (Collision h ary) = - Collision h $ A.map' (\ (L k v) -> let !v' = f k v in L k v') ary + go (Collision h s hm) = + Collision h s $ go hm {-# INLINE mapWithKey #-} -- | /O(n)/ Transform this map by applying a function to every value. @@ -545,8 +551,8 @@ traverseWithKey f = go go (Leaf h (L k v)) = leaf h k <$> f k v go (BitmapIndexed b ary) = BitmapIndexed b <$> A.traverse' go ary go (Full ary) = Full <$> A.traverse' go ary - go (Collision h ary) = - Collision h <$> A.traverse' (\ (L k v) -> (L k $!) <$> f k v) ary + go (Collision h s hm) = + Collision h s <$> go hm {-# INLINE traverseWithKey #-} ------------------------------------------------------------------------ From f5474ddcba725d1917598e96538d3ad64cc3e6bd Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 14 Aug 2018 20:49:54 +0200 Subject: [PATCH 08/69] Fixed double collisions --- Data/HashMap/Base.hs | 24 +- hashmap-strict-properties.prof.html | 931 ++++++++++++++++++++++++++++ helper.hs | 19 + 3 files changed, 963 insertions(+), 11 deletions(-) create mode 100644 hashmap-strict-properties.prof.html create mode 100644 helper.hs diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 05fa160c..f0857028 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -625,12 +625,14 @@ infixl 9 ! -- | Create a 'Collision' value with two 'Leaf' values. collision :: (Eq k, Hashable k) => Hash -> Salt -> Leaf k v -> Leaf k v -> HashMap k v -collision h s !(L k1 v1) !(L k2 v2) = +collision h s !l1@(L k1 v1) !l2@(L k2 v2) = let s' = nextSalt s rehash = hashWithSalt s' h1 = rehash k1 h2 = rehash k2 - hm = runST $ two 0 h1 k1 v1 h2 k2 v2 + hm = if h1 == h2 + then Collision h s' $ collision h1 s' l1 l2 + else runST $ two 0 h1 k1 v1 h2 k2 v2 in Collision h s' hm {-# INLINE collision #-} @@ -690,7 +692,7 @@ insert' h0 s0 k0 v0 m0 = go h0 s0 k0 v0 0 m0 else Full (update16 ary i st') where i = index h bs go h s k x bs t@(Collision hx sx hmx) - | h == hx = Collision hx sx $ insert' (hashWithSalt sx k) sx k x hmx + | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k x 0 hmx | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insert' #-} @@ -724,7 +726,7 @@ insertNewKey !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 in Full (update16 ary i st') where i = index h bs go h s k x bs t@(Collision hx sx hmx) - | h == hx = Collision hx sx $ insertNewKey (hashWithSalt sx k) sx k x hmx + | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k x 0 hmx | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# NOINLINE insertNewKey #-} @@ -755,7 +757,7 @@ insertKeyExists !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 in Full (update16 ary i st') where i = index h bs go h s k x bs t@(Collision hx sx hmx) - | h == hx = Collision hx sx $ insertKeyExists (hashWithSalt sx k) sx k x hmx + | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k x 0 hmx | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) go _ _ _ _ _ Empty = Empty -- error "Internal error: go Empty" @@ -776,7 +778,7 @@ unsafeInsert k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) where h0 = hashWithSalt s0 k0 s0 = defaultSalt - go !h _ !k x !_ Empty = return $! Leaf h (L k x) + go !h !_ !k x !_ Empty = return $! Leaf h (L k x) go h s k x bs t@(Leaf hy l@(L ky y)) | hy == h = if ky == k then if x `ptrEq` y @@ -883,7 +885,7 @@ insertModifying x f k0 m0 = go h0 s0 k0 0 m0 where i = index h bs go h s k bs t@(Collision hx sx hmx) | h == hx = - go (hashWithSalt sx k) sx k 0 hmx + Collision hx sx $ go (hashWithSalt sx k) sx k 0 hmx | otherwise = go h s k bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insertModifying #-} @@ -991,7 +993,7 @@ delete' h0 s0 k0 m0 = go h0 s0 k0 0 m0 _ -> Full (A.update ary i st') where i = index h bs go h s k _ t@(Collision hx sx hmx) - | h == hx = go (hashWithSalt sx k) sx k 0 hmx + | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k 0 hmx | otherwise = t {-# INLINABLE delete' #-} @@ -1032,7 +1034,7 @@ deleteKeyExists !h0 s0 !k0 !m0 = go h0 s0 k0 0 m0 _ -> Full (A.update ary i st') where i = index h bs go h s k _ t@(Collision hx sx hmx) - | h == hx = go (hashWithSalt sx k) sx k 0 hmx + | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k 0 hmx | otherwise = Empty -- error "Internal error: unexpected collision" go !_ !_ !_ !_ Empty = Empty -- error "Internal error: deleteKeyExists empty" {-# NOINLINE deleteKeyExists #-} @@ -1081,7 +1083,7 @@ adjust# f k0 m0 = go h0 k0 0 m0 then t else Full ary' go h k _ t@(Collision hx sx hmx) - | h == hx = go (hashWithSalt sx k) k 0 hmx + | h == hx = Collision hx sx $ go (hashWithSalt sx k) k 0 hmx | otherwise = t {-# INLINABLE adjust# #-} @@ -1595,7 +1597,7 @@ filterMapAux onLeaf onColl = go | otherwise = Empty go (BitmapIndexed b ary) = filterA ary b go (Full ary) = filterA ary fullNodeMask - go (Collision _ _ hm) = go hm + go (Collision _ _ hm) = undefined hm -- TODO filterA ary0 b0 = let !n = A.length ary0 diff --git a/hashmap-strict-properties.prof.html b/hashmap-strict-properties.prof.html new file mode 100644 index 00000000..d7558312 --- /dev/null +++ b/hashmap-strict-properties.prof.html @@ -0,0 +1,931 @@ + + + + + hashmap-strict-properties + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ + diff --git a/helper.hs b/helper.hs new file mode 100644 index 00000000..433cb648 --- /dev/null +++ b/helper.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +module Main + ( module Main + , module Data.HashMap.Base + ) where + +import Data.HashMap.Base +import Data.Hashable +import Test.QuickCheck + +newtype Key = K + { unK :: Int + } deriving (Arbitrary, Eq, Ord, Read, Show) + +instance Hashable Key where + hashWithSalt salt k = Data.Hashable.hashWithSalt salt (unK k) `mod` 20 + +main = pure () From 3db53fc4986fbcedf5de9000b210deaddf141012 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 15 Aug 2018 08:30:03 +0200 Subject: [PATCH 09/69] fixed some bugs --- Data/HashMap/Base.hs | 44 +++++++++++++++++++++++--------------------- helper.hs | 3 +++ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index f0857028..7286e0fc 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -168,7 +168,7 @@ hashWithSalt :: H.Hashable a => Salt -> a -> Hash hashWithSalt s v = fromIntegral $ H.hashWithSalt s v data Leaf k v = L !k v - deriving (Eq) + deriving (Show, Eq) instance (NFData k, NFData v) => NFData (Leaf k v) where rnf (L k v) = rnf k `seq` rnf v @@ -184,7 +184,7 @@ data HashMap k v | Leaf !Hash !(Leaf k v) | Full !(A.Array (HashMap k v)) | Collision !Hash !Salt !(HashMap k v) -- The new salt (TODO remove this comment) - deriving (Typeable) + deriving (Show, Typeable) type role HashMap nominal representational @@ -264,9 +264,9 @@ instance (Eq k, Hashable k, Read k, Read e) => Read (HashMap k e) where readListPrec = readListPrecDefault -instance (Show k, Show v) => Show (HashMap k v) where - showsPrec d m = showParen (d > 10) $ - showString "fromList " . shows (toList m) +--instance (Show k, Show v) => Show (HashMap k v) where +-- showsPrec d m = showParen (d > 10) $ +-- showString "fromList " . shows (toList m) instance Traversable (HashMap k) where traverse f = traverseWithKey (const f) @@ -506,7 +506,7 @@ lookup k m = lookupCont (\_ -> Nothing) (\v _i -> Just v) (hashWithSalt s k) def -- | lookup' is a version of lookup that takes the hash separately. -- It is used to implement alterF. -lookup' :: Eq k => Hash -> Salt -> k -> HashMap k v -> Maybe v +lookup' :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> Maybe v #if __GLASGOW_HASKELL__ >= 802 -- GHC does not yet perform a worker-wrapper transformation on -- unboxed sums automatically. That seems likely to happen at some @@ -540,7 +540,7 @@ data LookupRes a = Absent | Present a !Int -- Key not in map => Absent -- Key in map, no collision => Present v (-1) -- Key in map, collision => Present v position -lookupRecordCollision :: Eq k => Hash -> Salt -> k -> HashMap k v -> LookupRes v +lookupRecordCollision :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> LookupRes v #if __GLASGOW_HASKELL__ >= 802 lookupRecordCollision h s k m = case lookupRecordCollision# h s k m of (# (# #) | #) -> Absent @@ -552,7 +552,7 @@ lookupRecordCollision h s k m = case lookupRecordCollision# h s k m of -- may be changing in GHC 8.6 or so (there is some work in progress), but -- for now we use Int# explicitly here. We don't need to push the Int# -- into lookupCont because inlining takes care of that. -lookupRecordCollision# :: Eq k => Hash -> Salt -> k -> HashMap k v -> (# (# #) | (# v, Int# #) #) +lookupRecordCollision# :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> (# (# #) | (# v, Int# #) #) lookupRecordCollision# h s k m = lookupCont (\_ -> (# (# #) | #)) (\v (I# i) -> (# | (# v, i #) #)) h s k m -- INLINABLE to specialize to the Eq instance. @@ -578,7 +578,7 @@ lookupCont :: #else forall r k v. #endif - Eq k + (Eq k, Hashable k) => ((# #) -> r) -- Absent continuation -> (v -> Int -> r) -- Present continuation -> Hash -- The hash of the key @@ -595,11 +595,11 @@ lookupCont absent present !h0 !s0 !k0 !m0 = go h0 s0 k0 0 m0 | b .&. m == 0 = absent (# #) | otherwise = go h s k (bs+bitsPerSubkey) (A.index v (sparseIndex b m)) - where m = mask h s + where m = mask h bs go h s k bs (Full v) = - go h s k (bs+bitsPerSubkey) (A.index v (index h s)) + go h s k (bs+bitsPerSubkey) (A.index v (index h bs)) go h s k _ (Collision hx sx hmx) - | h == hx = go h sx k 0 hmx + | h == hx = go (hashWithSalt sx k) sx k 0 hmx | otherwise = absent (# #) {-# INLINE lookupCont #-} @@ -625,15 +625,17 @@ infixl 9 ! -- | Create a 'Collision' value with two 'Leaf' values. collision :: (Eq k, Hashable k) => Hash -> Salt -> Leaf k v -> Leaf k v -> HashMap k v -collision h s !l1@(L k1 v1) !l2@(L k2 v2) = - let s' = nextSalt s - rehash = hashWithSalt s' - h1 = rehash k1 - h2 = rehash k2 - hm = if h1 == h2 - then Collision h s' $ collision h1 s' l1 l2 - else runST $ two 0 h1 k1 v1 h2 k2 v2 - in Collision h s' hm +collision h0 s0 !l1@(L k1 v1) !l2@(L k2 v2) = go h0 s0 + where + go h s = + let s' = nextSalt s + rehash = hashWithSalt s' + h1 = rehash k1 + h2 = rehash k2 + hm = if h1 == h2 + then go h1 s' + else runST $ two 0 h1 k1 v1 h2 k2 v2 + in Collision h s' hm {-# INLINE collision #-} defaultSalt :: Int diff --git a/helper.hs b/helper.hs index 433cb648..79715b0e 100644 --- a/helper.hs +++ b/helper.hs @@ -1,13 +1,16 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} module Main ( module Main , module Data.HashMap.Base + , ppShow ) where import Data.HashMap.Base import Data.Hashable import Test.QuickCheck +import Text.Show.Pretty newtype Key = K { unK :: Int From feeb8c452f9100705f2621d5f7694d1fbf67d13b Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 15 Aug 2018 08:36:17 +0200 Subject: [PATCH 10/69] At least now the test completes --- tests/HashMapProperties.hs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/HashMapProperties.hs b/tests/HashMapProperties.hs index ded3be6d..26e5f562 100644 --- a/tests/HashMapProperties.hs +++ b/tests/HashMapProperties.hs @@ -128,19 +128,17 @@ pInsert k v = M.insert k v `eq_` HM.insert k v pDelete :: Key -> [(Key, Int)] -> Bool pDelete k = M.delete k `eq_` HM.delete k -newtype AlwaysCollide = AC Int - deriving (Arbitrary, Eq, Ord, Show) - -instance Hashable AlwaysCollide where - hashWithSalt _ _ = 1 - -- White-box test that tests the case of deleting one of two keys from -- a map, where the keys' hash values collide. -pDeleteCollision :: AlwaysCollide -> AlwaysCollide -> AlwaysCollide -> Int +pDeleteCollision :: Key -> Key -> Key -> Int -> Property -pDeleteCollision k1 k2 k3 idx = (k1 /= k2) && (k2 /= k3) && (k1 /= k3) ==> - HM.member toKeep $ HM.delete toDelete $ - HM.fromList [(k1, 1 :: Int), (k2, 2), (k3, 3)] +pDeleteCollision k1 k2 k3 idx = + (k1 /= k2) && (k2 /= k3) && (k1 /= k3) && + (hashWithSalt defaultSalt k1 == hashWithSalt defaultSalt k2) && + (hashWithSalt defaultSalt k2 == hashWithSalt defaultSalt k3) && + (hashWithSalt defaultSalt k1 == hashWithSalt defaultSalt k3) ==> + HM.member toKeep $ HM.delete toDelete $ + HM.fromList [(k1, 1 :: Int), (k2, 2), (k3, 3)] where which = idx `mod` 3 toDelete @@ -153,6 +151,7 @@ pDeleteCollision k1 k2 k3 idx = (k1 /= k2) && (k2 /= k3) && (k1 /= k3) ==> | which == 1 = k3 | which == 2 = k1 | otherwise = error "Impossible" + defaultSalt = -2578643520546668380 -- TODO what do do with this test? pInsertWith :: Key -> [(Key, Int)] -> Bool pInsertWith k = M.insertWith (+) k 1 `eq_` HM.insertWith (+) k 1 From e725b1f1b24d7964343d7f29ada9d7df536ece0b Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 15 Aug 2018 08:49:52 +0200 Subject: [PATCH 11/69] Fixed the unions --- Data/HashMap/Base.hs | 8 ++++---- Data/HashMap/Strict/Base.hs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 7286e0fc..96717cd7 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1293,11 +1293,11 @@ unionWithKey f = go 0 s0 then Leaf h1 (L k1 (f k1 v1 v2)) else collision h1 s l1 l2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Leaf h1 (L k1 v1)) t2@(Collision h2 s2 hm2) - | h1 == h2 = Collision h1 s2 $ insert' (hashWithSalt s2 k1) s2 k1 v1 hm2 + go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Collision h2 s2 hm2) + | h1 == h2 = Collision h1 s2 $ go 0 s2 (Leaf (hashWithSalt s2 k1) l1) hm2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 s1 hm1) t2@(Leaf h2 (L k2 v2)) - | h1 == h2 = Collision h1 s1 $ insert' (hashWithSalt s1 k2) s1 k2 v2 hm1 + go bs s t1@(Collision h1 s1 hm1) t2@(Leaf h2 l2@(L k2 v2)) + | h1 == h2 = Collision h1 s1 $ go 0 s1 hm1 (Leaf (hashWithSalt s1 k2) l2) | otherwise = goDifferentHash bs s h1 h2 t1 t2 go bs s t1@(Collision h1 s1 hm1) t2@(Collision h2 _ hm2) -- Second salt must be equal | h1 == h2 = Collision h1 s1 $ go 0 s1 hm1 hm2 diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index c5edb9b2..3e165f6a 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -417,11 +417,11 @@ unionWithKey f = go 0 s0 then leaf h1 k1 (f k1 v1 v2) else collision h1 s l1 l2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Leaf h1 (L k1 v1)) t2@(Collision h2 s2 hm2) - | h1 == h2 = Collision h1 s2 $ insert' (hashWithSalt s2 k1) s2 k1 v1 hm2 + go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Collision h2 s2 hm2) + | h1 == h2 = Collision h1 s2 $ go 0 s2 (Leaf (hashWithSalt s2 k1) l1) hm2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 s1 hm1) t2@(Leaf h2 (L k2 v2)) - | h1 == h2 = Collision h1 s1 $ insert' (hashWithSalt s1 k2) s1 k2 v2 hm1 + go bs s t1@(Collision h1 s1 hm1) t2@(Leaf h2 l2@(L k2 v2)) + | h1 == h2 = Collision h1 s1 $ go 0 s1 hm1 (Leaf (hashWithSalt s1 k2) l2) | otherwise = goDifferentHash bs s h1 h2 t1 t2 go bs s t1@(Collision h1 s1 hm1) t2@(Collision h2 _ hm2) -- Second salt must be equal | h1 == h2 = Collision h1 s1 $ go 0 s1 hm1 hm2 From e687bba82113bf17df5dc67e28fbd6c2c40fe6ad Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 15 Aug 2018 08:58:47 +0200 Subject: [PATCH 12/69] Made strict hashmap tests pass --- Data/HashMap/Base.hs | 17 ++++++----------- Data/HashMap/Strict/Base.hs | 5 +---- tests/HashMapProperties.hs | 8 +++++++- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 96717cd7..f630f78f 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1558,12 +1558,9 @@ foldrWithKey f = go -- | /O(n)/ Transform this map by applying a function to every value -- and retaining only some of them. mapMaybeWithKey :: (k -> v1 -> Maybe v2) -> HashMap k v1 -> HashMap k v2 -mapMaybeWithKey f = filterMapAux onLeaf onColl +mapMaybeWithKey f = filterMapAux onLeaf where onLeaf (Leaf h (L k v)) | Just v' <- f k v = Just (Leaf h (L k v')) onLeaf _ = Nothing - - onColl (L k v) | Just v' <- f k v = Just (L k v') - | otherwise = Nothing {-# INLINE mapMaybeWithKey #-} -- | /O(n)/ Transform this map by applying a function to every value @@ -1575,12 +1572,9 @@ mapMaybe f = mapMaybeWithKey (const f) -- | /O(n)/ Filter this map by retaining only elements satisfying a -- predicate. filterWithKey :: forall k v. (k -> v -> Bool) -> HashMap k v -> HashMap k v -filterWithKey pred = filterMapAux onLeaf onColl +filterWithKey pred = filterMapAux onLeaf where onLeaf t@(Leaf _ (L k v)) | pred k v = Just t onLeaf _ = Nothing - - onColl el@(L k v) | pred k v = Just el - onColl _ = Nothing {-# INLINE filterWithKey #-} @@ -1588,10 +1582,9 @@ filterWithKey pred = filterMapAux onLeaf onColl -- allowing the former to former to reuse terms. filterMapAux :: forall k v1 v2 . (HashMap k v1 -> Maybe (HashMap k v2)) - -> (Leaf k v1 -> Maybe (Leaf k v2)) -> HashMap k v1 -> HashMap k v2 -filterMapAux onLeaf onColl = go +filterMapAux onLeaf = go where go Empty = Empty go t@Leaf{} @@ -1599,7 +1592,7 @@ filterMapAux onLeaf onColl = go | otherwise = Empty go (BitmapIndexed b ary) = filterA ary b go (Full ary) = filterA ary fullNodeMask - go (Collision _ _ hm) = undefined hm -- TODO + go (Collision h s hm) = filterC h s hm filterA ary0 b0 = let !n = A.length ary0 @@ -1629,6 +1622,8 @@ filterMapAux onLeaf onColl = go (bi `unsafeShiftL` 1) n t -> do A.write mary j t step ary mary b (i+1) (j+1) (bi `unsafeShiftL` 1) n + + filterC h s hm = Collision h s $ go hm -- TODO deal with the collision being deleted? {-# INLINE filterMapAux #-} -- | /O(n)/ Filter this map by retaining only elements which values diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index 3e165f6a..edfeb03d 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -515,12 +515,9 @@ map f = mapWithKey (const f) -- | /O(n)/ Transform this map by applying a function to every value -- and retaining only some of them. mapMaybeWithKey :: (k -> v1 -> Maybe v2) -> HashMap k v1 -> HashMap k v2 -mapMaybeWithKey f = filterMapAux onLeaf onColl +mapMaybeWithKey f = filterMapAux onLeaf where onLeaf (Leaf h (L k v)) | Just v' <- f k v = Just (leaf h k v') onLeaf _ = Nothing - - onColl (L k v) | Just v' <- f k v = Just (L k v') - | otherwise = Nothing {-# INLINE mapMaybeWithKey #-} -- | /O(n)/ Transform this map by applying a function to every value diff --git a/tests/HashMapProperties.hs b/tests/HashMapProperties.hs index 26e5f562..1bff4973 100644 --- a/tests/HashMapProperties.hs +++ b/tests/HashMapProperties.hs @@ -128,9 +128,15 @@ pInsert k v = M.insert k v `eq_` HM.insert k v pDelete :: Key -> [(Key, Int)] -> Bool pDelete k = M.delete k `eq_` HM.delete k +newtype MostlyCollide = AC Int + deriving (Arbitrary, Eq, Ord, Show) + +instance Hashable MostlyCollide where + hashWithSalt s (AC i) = (hashWithSalt s i) `mod` 3 + -- White-box test that tests the case of deleting one of two keys from -- a map, where the keys' hash values collide. -pDeleteCollision :: Key -> Key -> Key -> Int +pDeleteCollision :: MostlyCollide -> MostlyCollide -> MostlyCollide -> Int -> Property pDeleteCollision k1 k2 k3 idx = (k1 /= k2) && (k2 /= k3) && (k1 /= k3) && From 789dd5efa62b5dd0ed53f07df64a9230e83cd07a Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 15 Aug 2018 09:11:00 +0200 Subject: [PATCH 13/69] Now without storing the salt anywhere --- Data/HashMap/Base.hs | 125 ++++++++++++++++++------------------ Data/HashMap/Strict/Base.hs | 34 +++++----- 2 files changed, 79 insertions(+), 80 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index f630f78f..58c45521 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -84,6 +84,7 @@ module Data.HashMap.Base , collision , hashWithSalt , defaultSalt + , nextSalt , mask , index , bitsPerSubkey @@ -183,7 +184,7 @@ data HashMap k v | BitmapIndexed !Bitmap !(A.Array (HashMap k v)) | Leaf !Hash !(Leaf k v) | Full !(A.Array (HashMap k v)) - | Collision !Hash !Salt !(HashMap k v) -- The new salt (TODO remove this comment) + | Collision !Hash !(HashMap k v) deriving (Show, Typeable) type role HashMap nominal representational @@ -193,7 +194,7 @@ instance (NFData k, NFData v) => NFData (HashMap k v) where rnf (BitmapIndexed _ ary) = rnf ary rnf (Leaf _ l) = rnf l rnf (Full ary) = rnf ary - rnf (Collision _ _ ary) = rnf ary + rnf (Collision _ ary) = rnf ary instance Functor (HashMap k) where fmap = map @@ -296,8 +297,8 @@ equal1 eq = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go (Collision h1 s1 hm1) (Collision h2 s2 hm2) - = h1 == h2 && s1 == s2 && go hm1 hm2 + go (Collision h1 hm1) (Collision h2 hm2) + = h1 == h2 && go hm1 hm2 go _ _ = False leafEq (L k1 v1) (L k2 v2) = k1 == k2 && eq v1 v2 @@ -314,9 +315,8 @@ equal2 eqk eqv t1 t2 = go (toList' t1 []) (toList' t2 []) | k1 == k2 && leafEq l1 l2 = go tl1 tl2 - go (Collision h1 s1 hm1 : tl1) (Collision h2 s2 hm2 : tl2) + go (Collision h1 hm1 : tl1) (Collision h2 hm2 : tl2) | h1 == h2 && - s1 == s2 && go (toList' hm1 []) (toList' hm2 []) = go tl1 tl2 go [] [] = True @@ -348,13 +348,12 @@ cmp cmpk cmpv t1 t2 = go (toList' t1 []) (toList' t2 []) = compare k1 k2 `mappend` leafCompare l1 l2 `mappend` go tl1 tl2 - go (Collision h1 s1 hm1 : tl1) (Collision h2 s2 hm2 : tl2) + go (Collision h1 hm1 : tl1) (Collision h2 hm2 : tl2) = compare h1 h2 `mappend` - compare s1 s2 `mappend` go (toList' hm1 []) (toList' hm2 []) `mappend` go tl1 tl2 - go (Leaf _ _ : _) (Collision _ _ _ : _) = LT - go (Collision _ _ _ : _) (Leaf _ _ : _) = GT + go (Leaf _ _ : _) (Collision _ _ : _) = LT + go (Collision _ _ : _) (Leaf _ _ : _) = GT go [] [] = EQ go [] _ = LT go _ [] = GT @@ -369,8 +368,8 @@ equalKeys1 eq t1 t2 = go (toList' t1 []) (toList' t2 []) go (Leaf k1 l1 : tl1) (Leaf k2 l2 : tl2) | k1 == k2 && leafEq l1 l2 = go tl1 tl2 - go (Collision h1 s1 hm1 : tl1) (Collision h2 s2 hm2 : tl2) - | h1 == h2 && s1 == s2 && + go (Collision h1 hm1 : tl1) (Collision h2 hm2 : tl2) + | h1 == h2 && go (toList' hm1 []) (toList' hm2 []) = go tl1 tl2 go [] [] = True @@ -388,8 +387,8 @@ equalKeys = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go (Collision h1 s1 hm1) (Collision h2 s2 hm2) - = h1 == h2 && s1 == s2 && go hm1 hm2 + go (Collision h1 hm1) (Collision h2 hm2) + = h1 == h2 && go hm1 hm2 go _ _ = False leafEq (L k1 _) (L k2 _) = k1 == k2 @@ -404,8 +403,8 @@ instance H.Hashable2 HashMap where = s `hashLeafWithSalt` l `go` tl -- For collisions we hashmix hash value -- and then array of values' hashes sorted - go s (Collision h s' hm' : tl) - = (((s `H.hashWithSalt` h) `H.hashWithSalt` s') `go` (toList' hm' [])) `go` tl + go s (Collision h hm' : tl) + = ((s `H.hashWithSalt` h) `go` (toList' hm' [])) `go` tl go s (_ : tl) = s `go` tl -- hashLeafWithSalt :: Int -> Leaf k v -> Int @@ -424,8 +423,8 @@ instance (Hashable k, Hashable v) => Hashable (HashMap k v) where = s `hashLeafWithSalt` l `go` tl -- For collisions we hashmix hash value -- and then array of values' hashes sorted - go s (Collision h s' hm' : tl) - = (((s `H.hashWithSalt` h) `H.hashWithSalt` s') `go` (toList' hm' [])) `go` tl + go s (Collision h hm' : tl) + = ((s `H.hashWithSalt` h) `go` (toList' hm' [])) `go` tl go s (_ : tl) = s `go` tl hashLeafWithSalt :: Int -> Leaf k v -> Int @@ -436,13 +435,13 @@ toList' :: HashMap k v -> [HashMap k v] -> [HashMap k v] toList' (BitmapIndexed _ ary) a = A.foldr toList' a ary toList' (Full ary) a = A.foldr toList' a ary toList' l@(Leaf _ _) a = l : a -toList' (Collision _ _ hm) a = toList' hm a +toList' (Collision _ hm) a = toList' hm a toList' Empty a = a -- Helper function to detect 'Leaf's and 'Collision's. isLeafOrCollision :: HashMap k v -> Bool isLeafOrCollision (Leaf _ _) = True -isLeafOrCollision (Collision _ _ _) = True +isLeafOrCollision (Collision _ _) = True isLeafOrCollision _ = False ------------------------------------------------------------------------ @@ -472,7 +471,7 @@ size t = go t 0 go (Leaf _ _) n = n + 1 go (BitmapIndexed _ ary) n = A.foldl' (flip go) n ary go (Full ary) n = A.foldl' (flip go) n ary - go (Collision _ _ hm) n = go hm n + go (Collision _ hm) n = go hm n -- | /O(log n)/ Return 'True' if the specified key is present in the -- map, 'False' otherwise. @@ -598,8 +597,8 @@ lookupCont absent present !h0 !s0 !k0 !m0 = go h0 s0 k0 0 m0 where m = mask h bs go h s k bs (Full v) = go h s k (bs+bitsPerSubkey) (A.index v (index h bs)) - go h s k _ (Collision hx sx hmx) - | h == hx = go (hashWithSalt sx k) sx k 0 hmx + go h s k _ (Collision hx hmx) + | h == hx = go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx | otherwise = absent (# #) {-# INLINE lookupCont #-} @@ -635,7 +634,7 @@ collision h0 s0 !l1@(L k1 v1) !l2@(L k2 v2) = go h0 s0 hm = if h1 == h2 then go h1 s' else runST $ two 0 h1 k1 v1 h2 k2 v2 - in Collision h s' hm + in Collision h hm {-# INLINE collision #-} defaultSalt :: Int @@ -693,8 +692,8 @@ insert' h0 s0 k0 v0 m0 = go h0 s0 k0 v0 0 m0 then t else Full (update16 ary i st') where i = index h bs - go h s k x bs t@(Collision hx sx hmx) - | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k x 0 hmx + go h s k x bs t@(Collision hx hmx) + | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insert' #-} @@ -727,8 +726,8 @@ insertNewKey !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 !st' = go h s k x (bs+bitsPerSubkey) st in Full (update16 ary i st') where i = index h bs - go h s k x bs t@(Collision hx sx hmx) - | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k x 0 hmx + go h s k x bs t@(Collision hx hmx) + | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# NOINLINE insertNewKey #-} @@ -758,8 +757,8 @@ insertKeyExists !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 !st' = go h s k x (bs+bitsPerSubkey) st in Full (update16 ary i st') where i = index h bs - go h s k x bs t@(Collision hx sx hmx) - | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k x 0 hmx + go h s k x bs t@(Collision hx hmx) + | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) go _ _ _ _ _ Empty = Empty -- error "Internal error: go Empty" @@ -805,8 +804,8 @@ unsafeInsert k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) A.unsafeUpdateM ary i st' return t where i = index h bs - go h s k x bs t@(Collision hx sx hmx) - | h == hx = Collision hx sx <$> go (hashWithSalt sx k) sx k x 0 hmx + go h s k x bs t@(Collision hx hmx) + | h == hx = Collision hx <$> go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE unsafeInsert #-} @@ -885,9 +884,9 @@ insertModifying x f k0 m0 = go h0 s0 k0 0 m0 then t else Full ary' where i = index h bs - go h s k bs t@(Collision hx sx hmx) + go h s k bs t@(Collision hx hmx) | h == hx = - Collision hx sx $ go (hashWithSalt sx k) sx k 0 hmx + Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx | otherwise = go h s k bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insertModifying #-} @@ -943,8 +942,8 @@ unsafeInsertWith f k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) A.unsafeUpdateM ary i st' return t where i = index h bs - go h s k x bs t@(Collision hx sx hmx) - | h == hx = Collision hx sx <$> go (hashWithSalt sx k) sx k x 0 hmx + go h s k x bs t@(Collision hx hmx) + | h == hx = Collision hx <$> go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE unsafeInsertWith #-} @@ -994,8 +993,8 @@ delete' h0 s0 k0 m0 = go h0 s0 k0 0 m0 in BitmapIndexed bm ary' _ -> Full (A.update ary i st') where i = index h bs - go h s k _ t@(Collision hx sx hmx) - | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k 0 hmx + go h s k _ t@(Collision hx hmx) + | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx | otherwise = t {-# INLINABLE delete' #-} @@ -1035,8 +1034,8 @@ deleteKeyExists !h0 s0 !k0 !m0 = go h0 s0 k0 0 m0 in BitmapIndexed bm ary' _ -> Full (A.update ary i st') where i = index h bs - go h s k _ t@(Collision hx sx hmx) - | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k 0 hmx + go h s k _ t@(Collision hx hmx) + | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx | otherwise = Empty -- error "Internal error: unexpected collision" go !_ !_ !_ !_ Empty = Empty -- error "Internal error: deleteKeyExists empty" {-# NOINLINE deleteKeyExists #-} @@ -1056,36 +1055,36 @@ adjust f k m = adjust# (\v -> (# f v #)) k m -- | Much like 'adjust', but not inherently leaky. adjust# :: (Eq k, Hashable k) => (v -> (# v #)) -> k -> HashMap k v -> HashMap k v -adjust# f k0 m0 = go h0 k0 0 m0 +adjust# f k0 m0 = go h0 s0 k0 0 m0 where h0 = hashWithSalt s0 k0 s0 = defaultSalt - go !_ !_ !_ Empty = Empty - go h k _ t@(Leaf hy (L ky y)) + go !_ !_ !_ !_ Empty = Empty + go h _ k _ t@(Leaf hy (L ky y)) | hy == h && ky == k = case f y of (# y' #) | ptrEq y y' -> t | otherwise -> Leaf h (L k y') | otherwise = t - go h k bs t@(BitmapIndexed b ary) + go h s k bs t@(BitmapIndexed b ary) | b .&. m == 0 = t | otherwise = let !st = A.index ary i - !st' = go h k (bs+bitsPerSubkey) st + !st' = go h s k (bs+bitsPerSubkey) st ary' = A.update ary i $! st' in if ptrEq st st' then t else BitmapIndexed b ary' where m = mask h bs i = sparseIndex b m - go h k bs t@(Full ary) = + go h s k bs t@(Full ary) = let i = index h bs !st = A.index ary i - !st' = go h k (bs+bitsPerSubkey) st + !st' = go h s k (bs+bitsPerSubkey) st ary' = update16 ary i $! st' in if ptrEq st st' then t else Full ary' - go h k _ t@(Collision hx sx hmx) - | h == hx = Collision hx sx $ go (hashWithSalt sx k) k 0 hmx + go h s k _ t@(Collision hx hmx) + | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx | otherwise = t {-# INLINABLE adjust# #-} @@ -1293,14 +1292,14 @@ unionWithKey f = go 0 s0 then Leaf h1 (L k1 (f k1 v1 v2)) else collision h1 s l1 l2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Collision h2 s2 hm2) - | h1 == h2 = Collision h1 s2 $ go 0 s2 (Leaf (hashWithSalt s2 k1) l1) hm2 + go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Collision h2 hm2) + | h1 == h2 = Collision h1 $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 s1 hm1) t2@(Leaf h2 l2@(L k2 v2)) - | h1 == h2 = Collision h1 s1 $ go 0 s1 hm1 (Leaf (hashWithSalt s1 k2) l2) + go bs s t1@(Collision h1 hm1) t2@(Leaf h2 l2@(L k2 v2)) + | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 s1 hm1) t2@(Collision h2 _ hm2) -- Second salt must be equal - | h1 == h2 = Collision h1 s1 $ go 0 s1 hm1 hm2 + go bs s t1@(Collision h1 hm1) t2@(Collision h2 hm2) + | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 hm2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 -- branch vs. branch go bs s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) = @@ -1352,7 +1351,7 @@ unionWithKey f = go 0 s0 in Full ary' leafHashCode (Leaf h _) = h - leafHashCode (Collision h _ _) = h + leafHashCode (Collision h _) = h leafHashCode _ = error "leafHashCode" goDifferentHash bs s h1 h2 t1 t2 @@ -1414,7 +1413,7 @@ mapWithKey f = go go (Full ary) = Full $ A.map go ary -- Why map strictly over collision arrays? Because there's no -- point suspending the O(1) work this does for each leaf. - go (Collision h s hm) = Collision h s $ go hm + go (Collision h hm) = Collision h $ go hm {-# INLINE mapWithKey #-} -- | /O(n)/ Transform this map by applying a function to every value. @@ -1442,8 +1441,8 @@ traverseWithKey f = go go (Leaf h (L k v)) = Leaf h . L k <$> f k v go (BitmapIndexed b ary) = BitmapIndexed b <$> A.traverse go ary go (Full ary) = Full <$> A.traverse go ary - go (Collision h s hm) = - Collision h s <$> go hm + go (Collision h hm) = + Collision h <$> go hm {-# INLINE traverseWithKey #-} ------------------------------------------------------------------------ @@ -1529,7 +1528,7 @@ foldlWithKey' f = go go z (Leaf _ (L k v)) = f z k v go z (BitmapIndexed _ ary) = A.foldl' go z ary go z (Full ary) = A.foldl' go z ary - go z (Collision _ _ hm) = go z hm + go z (Collision _ hm) = go z hm {-# INLINE foldlWithKey' #-} -- | /O(n)/ Reduce this map by applying a binary operator to all @@ -1549,7 +1548,7 @@ foldrWithKey f = go go z (Leaf _ (L k v)) = f k v z go z (BitmapIndexed _ ary) = A.foldr (flip go) z ary go z (Full ary) = A.foldr (flip go) z ary - go z (Collision _ _ hm) = go z hm + go z (Collision _ hm) = go z hm {-# INLINE foldrWithKey #-} ------------------------------------------------------------------------ @@ -1592,7 +1591,7 @@ filterMapAux onLeaf = go | otherwise = Empty go (BitmapIndexed b ary) = filterA ary b go (Full ary) = filterA ary fullNodeMask - go (Collision h s hm) = filterC h s hm + go (Collision h hm) = filterC h hm filterA ary0 b0 = let !n = A.length ary0 @@ -1623,7 +1622,7 @@ filterMapAux onLeaf = go t -> do A.write mary j t step ary mary b (i+1) (j+1) (bi `unsafeShiftL` 1) n - filterC h s hm = Collision h s $ go hm -- TODO deal with the collision being deleted? + filterC h hm = Collision h $ go hm -- TODO deal with the collision being deleted? {-# INLINE filterMapAux #-} -- | /O(n)/ Filter this map by retaining only elements which values diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index edfeb03d..92005593 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -175,8 +175,8 @@ insertWith f k0 v0 m0 = go h0 s0 k0 v0 0 m0 ary' = update16 ary i $! st' in Full ary' where i = index h bs - go h s k x bs t@(Collision hx sx hm) - | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k x 0 hm + go h s k x bs t@(Collision hx hm) + | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hm | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insertWith #-} @@ -212,8 +212,8 @@ unsafeInsertWith f k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) A.unsafeUpdateM ary i st' return t where i = index h bs - go h s k x bs t@(Collision hx sx hm) - | h == hx = Collision hx sx <$> go (hashWithSalt sx k) sx k x 0 hm + go h s k x bs t@(Collision hx hm) + | h == hx = Collision hx <$> go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hm | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE unsafeInsertWith #-} @@ -242,8 +242,8 @@ adjust f k0 m0 = go h0 s0 k0 0 m0 st' = go h s k (bs+bitsPerSubkey) st ary' = update16 ary i $! st' in Full ary' - go h s k _ t@(Collision hx sx hm) - | h == hx = Collision hx sx $ go (hashWithSalt sx k) sx k 0 hm + go h s k _ t@(Collision hx hm) + | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hm | otherwise = t {-# INLINABLE adjust #-} @@ -417,14 +417,14 @@ unionWithKey f = go 0 s0 then leaf h1 k1 (f k1 v1 v2) else collision h1 s l1 l2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Collision h2 s2 hm2) - | h1 == h2 = Collision h1 s2 $ go 0 s2 (Leaf (hashWithSalt s2 k1) l1) hm2 + go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Collision h2 hm2) + | h1 == h2 = Collision h1 $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 s1 hm1) t2@(Leaf h2 l2@(L k2 v2)) - | h1 == h2 = Collision h1 s1 $ go 0 s1 hm1 (Leaf (hashWithSalt s1 k2) l2) + go bs s t1@(Collision h1 hm1) t2@(Leaf h2 l2@(L k2 v2)) + | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 s1 hm1) t2@(Collision h2 _ hm2) -- Second salt must be equal - | h1 == h2 = Collision h1 s1 $ go 0 s1 hm1 hm2 + go bs s t1@(Collision h1 hm1) t2@(Collision h2 hm2) -- Second salt must be equal + | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 hm2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 -- branch vs. branch go bs s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) = @@ -476,7 +476,7 @@ unionWithKey f = go 0 s0 in Full ary' leafHashCode (Leaf h _) = h - leafHashCode (Collision h _ _) = h + leafHashCode (Collision h _) = h leafHashCode _ = error "leafHashCode" goDifferentHash bs s h1 h2 t1 t2 @@ -499,8 +499,8 @@ mapWithKey f = go go (Leaf h (L k v)) = leaf h k (f k v) go (BitmapIndexed b ary) = BitmapIndexed b $ A.map' go ary go (Full ary) = Full $ A.map' go ary - go (Collision h s hm) = - Collision h s $ go hm + go (Collision h hm) = + Collision h $ go hm {-# INLINE mapWithKey #-} -- | /O(n)/ Transform this map by applying a function to every value. @@ -548,8 +548,8 @@ traverseWithKey f = go go (Leaf h (L k v)) = leaf h k <$> f k v go (BitmapIndexed b ary) = BitmapIndexed b <$> A.traverse' go ary go (Full ary) = Full <$> A.traverse' go ary - go (Collision h s hm) = - Collision h s <$> go hm + go (Collision h hm) = + Collision h <$> go hm {-# INLINE traverseWithKey #-} ------------------------------------------------------------------------ From 5c6aa0f77919deb2f94062b8fc14b20bb1f0735a Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 15 Aug 2018 09:13:39 +0200 Subject: [PATCH 14/69] Putting back the show instance --- Data/HashMap/Base.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 58c45521..f51b2bb0 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -169,7 +169,7 @@ hashWithSalt :: H.Hashable a => Salt -> a -> Hash hashWithSalt s v = fromIntegral $ H.hashWithSalt s v data Leaf k v = L !k v - deriving (Show, Eq) + deriving (Eq) instance (NFData k, NFData v) => NFData (Leaf k v) where rnf (L k v) = rnf k `seq` rnf v @@ -185,7 +185,7 @@ data HashMap k v | Leaf !Hash !(Leaf k v) | Full !(A.Array (HashMap k v)) | Collision !Hash !(HashMap k v) - deriving (Show, Typeable) + deriving (Typeable) type role HashMap nominal representational @@ -265,9 +265,9 @@ instance (Eq k, Hashable k, Read k, Read e) => Read (HashMap k e) where readListPrec = readListPrecDefault ---instance (Show k, Show v) => Show (HashMap k v) where --- showsPrec d m = showParen (d > 10) $ --- showString "fromList " . shows (toList m) +instance (Show k, Show v) => Show (HashMap k v) where + showsPrec d m = showParen (d > 10) $ + showString "fromList " . shows (toList m) instance Traversable (HashMap k) where traverse f = traverseWithKey (const f) From a1b5427ec83946f884e35927961a3a8d5cd48b6f Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 15 Aug 2018 09:18:56 +0200 Subject: [PATCH 15/69] Fixed the filter function as well, to not retain too many collisions --- Data/HashMap/Base.hs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index f51b2bb0..0a3ec190 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1622,7 +1622,12 @@ filterMapAux onLeaf = go t -> do A.write mary j t step ary mary b (i+1) (j+1) (bi `unsafeShiftL` 1) n - filterC h hm = Collision h $ go hm -- TODO deal with the collision being deleted? + filterC h hm = + let hm' = go hm + in case hm' of + Empty -> Empty + Leaf _ l -> Leaf h l + _ -> Collision h hm' {-# INLINE filterMapAux #-} -- | /O(n)/ Filter this map by retaining only elements which values From 9eb436e23c87f9a411792be0e52f24e1d95427da Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 15 Aug 2018 12:28:08 +0200 Subject: [PATCH 16/69] Fixed compilation warnings --- Data/HashMap/Base.hs | 93 ++----------------------------------- Data/HashMap/Strict/Base.hs | 51 ++------------------ 2 files changed, 8 insertions(+), 136 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 0a3ec190..f231f664 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -137,7 +137,6 @@ import qualified Data.Hashable as H import Data.Hashable (Hashable) import Data.HashMap.Unsafe (runST) import Data.HashMap.UnsafeShift (unsafeShiftL, unsafeShiftR) -import Data.HashMap.List (isPermutationBy, unorderedCompare) import Data.Typeable (Typeable) import GHC.Exts (isTrue#) @@ -624,7 +623,7 @@ infixl 9 ! -- | Create a 'Collision' value with two 'Leaf' values. collision :: (Eq k, Hashable k) => Hash -> Salt -> Leaf k v -> Leaf k v -> HashMap k v -collision h0 s0 !l1@(L k1 v1) !l2@(L k2 v2) = go h0 s0 +collision h0 s0 !(L k1 v1) !(L k2 v2) = go h0 s0 where go h s = let s' = nextSalt s @@ -765,14 +764,6 @@ insertKeyExists !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 {-# NOINLINE insertKeyExists #-} --- Replace the ith Leaf with Leaf k v. --- --- This does not check that @i@ is within bounds of the array. -setAtPosition :: Int -> k -> v -> A.Array (Leaf k v) -> A.Array (Leaf k v) -setAtPosition i k x ary = A.update ary i (L k x) -{-# INLINE setAtPosition #-} - - -- | In-place update version of insert unsafeInsert :: (Eq k, Hashable k) => k -> v -> HashMap k v -> HashMap k v unsafeInsert k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) @@ -890,26 +881,6 @@ insertModifying x f k0 m0 = go h0 s0 k0 0 m0 | otherwise = go h s k bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insertModifying #-} --- Like insertModifying for arrays; used to implement insertModifying -insertModifyingArr :: Eq k => v -> (v -> (# v #)) -> k -> A.Array (Leaf k v) - -> A.Array (Leaf k v) -insertModifyingArr x f k0 ary0 = go k0 ary0 0 (A.length ary0) - where - go !k !ary !i !n - | i >= n = A.run $ do - -- Not found, append to the end. - mary <- A.new_ (n + 1) - A.copy ary 0 mary 0 n - A.write mary n (L k x) - return mary - | otherwise = case A.index ary i of - (L kx y) | k == kx -> case f y of - (# y' #) -> if ptrEq y y' - then ary - else A.update ary i (L k y') - | otherwise -> go k ary (i+1) n -{-# INLINE insertModifyingArr #-} - -- | In-place update version of insertWith unsafeInsertWith :: forall k v. (Eq k, Hashable k) => (v -> v -> v) -> k -> v -> HashMap k v @@ -1034,7 +1005,7 @@ deleteKeyExists !h0 s0 !k0 !m0 = go h0 s0 k0 0 m0 in BitmapIndexed bm ary' _ -> Full (A.update ary i st') where i = index h bs - go h s k _ t@(Collision hx hmx) + go h s k _ (Collision hx hmx) | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx | otherwise = Empty -- error "Internal error: unexpected collision" go !_ !_ !_ !_ Empty = Empty -- error "Internal error: deleteKeyExists empty" @@ -1241,7 +1212,7 @@ alterFEager f !k m = (<$> f mv) $ \fres -> Absent -> insertNewKey h s k v' m -- Key existed before - Present v collPos -> + Present v _ -> if v `ptrEq` v' -- If the value is identical, no-op then m @@ -1292,10 +1263,10 @@ unionWithKey f = go 0 s0 then Leaf h1 (L k1 (f k1 v1 v2)) else collision h1 s l1 l2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Collision h2 hm2) + go bs s t1@(Leaf h1 l1@(L k1 _)) t2@(Collision h2 hm2) | h1 == h2 = Collision h1 $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 hm1) t2@(Leaf h2 l2@(L k2 v2)) + go bs s t1@(Collision h1 hm1) t2@(Leaf h2 l2@(L k2 _)) | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) | otherwise = goDifferentHash bs s h1 h2 t1 t2 go bs s t1@(Collision h1 hm1) t2@(Collision h2 hm2) @@ -1678,26 +1649,6 @@ fromListWith f = L.foldl' (\ m (k, v) -> unsafeInsertWith f k v m) empty ------------------------------------------------------------------------ -- Array operations --- | /O(n)/ Look up the value associated with the given key in an --- array. -lookupInArrayCont :: -#if __GLASGOW_HASKELL__ >= 802 - forall rep (r :: TYPE rep) k v. -#else - forall r k v. -#endif - Eq k => ((# #) -> r) -> (v -> Int -> r) -> k -> A.Array (Leaf k v) -> r -lookupInArrayCont absent present k0 ary0 = go k0 ary0 0 (A.length ary0) - where - go :: Eq k => k -> A.Array (Leaf k v) -> Int -> Int -> r - go !k !ary !i !n - | i >= n = absent (# #) - | otherwise = case A.index ary i of - (L kx v) - | k == kx -> present v i - | otherwise -> go k ary (i+1) n -{-# INLINE lookupInArrayCont #-} - -- | /O(n)/ Lookup the value associated with the given key in this -- array. Returns 'Nothing' if the key wasn't found. indexOf :: Eq k => k -> A.Array (Leaf k v) -> Maybe Int @@ -1711,40 +1662,6 @@ indexOf k0 ary0 = go k0 ary0 0 (A.length ary0) | otherwise -> go k ary (i+1) n {-# INLINABLE indexOf #-} -updateWith# :: Eq k => (v -> (# v #)) -> k -> A.Array (Leaf k v) -> A.Array (Leaf k v) -updateWith# f k0 ary0 = go k0 ary0 0 (A.length ary0) - where - go !k !ary !i !n - | i >= n = ary - | otherwise = case A.index ary i of - (L kx y) | k == kx -> case f y of - (# y' #) - | ptrEq y y' -> ary - | otherwise -> A.update ary i (L k y') - | otherwise -> go k ary (i+1) n -{-# INLINABLE updateWith# #-} - -updateOrSnocWith :: Eq k => (v -> v -> v) -> k -> v -> A.Array (Leaf k v) - -> A.Array (Leaf k v) -updateOrSnocWith f = updateOrSnocWithKey (const f) -{-# INLINABLE updateOrSnocWith #-} - -updateOrSnocWithKey :: Eq k => (k -> v -> v -> v) -> k -> v -> A.Array (Leaf k v) - -> A.Array (Leaf k v) -updateOrSnocWithKey f k0 v0 ary0 = go k0 v0 ary0 0 (A.length ary0) - where - go !k v !ary !i !n - | i >= n = A.run $ do - -- Not found, append to the end. - mary <- A.new_ (n + 1) - A.copy ary 0 mary 0 n - A.write mary n (L k v) - return mary - | otherwise = case A.index ary i of - (L kx y) | k == kx -> A.update ary i (L k (f k v y)) - | otherwise -> go k v ary (i+1) n -{-# INLINABLE updateOrSnocWithKey #-} - updateOrConcatWith :: Eq k => (v -> v -> v) -> A.Array (Leaf k v) -> A.Array (Leaf k v) -> A.Array (Leaf k v) updateOrConcatWith f = updateOrConcatWithKey (const f) {-# INLINABLE updateOrConcatWith #-} diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index 92005593..37dad0c0 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -375,7 +375,7 @@ alterFEager f !k !m = (<$> f mv) $ \fres -> Absent -> insertNewKey h s k v' m -- Key existed before, no hash collision - Present v collPos -> v' `seq` + Present v _ -> v' `seq` if v `ptrEq` v' -- If the value is identical, no-op then m @@ -417,10 +417,10 @@ unionWithKey f = go 0 s0 then leaf h1 k1 (f k1 v1 v2) else collision h1 s l1 l2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Collision h2 hm2) + go bs s t1@(Leaf h1 l1@(L k1 _)) t2@(Collision h2 hm2) | h1 == h2 = Collision h1 $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 hm1) t2@(Leaf h2 l2@(L k2 v2)) + go bs s t1@(Collision h1 hm1) t2@(Leaf h2 l2@(L k2 _)) | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) | otherwise = goDifferentHash bs s h1 h2 t1 t2 go bs s t1@(Collision h1 hm1) t2@(Collision h2 hm2) -- Second salt must be equal @@ -618,51 +618,6 @@ fromListWith :: (Eq k, Hashable k) => (v -> v -> v) -> [(k, v)] -> HashMap k v fromListWith f = L.foldl' (\ m (k, v) -> unsafeInsertWith f k v m) empty {-# INLINE fromListWith #-} ------------------------------------------------------------------------- --- Array operations - -updateWith :: Eq k => (v -> v) -> k -> A.Array (Leaf k v) -> A.Array (Leaf k v) -updateWith f k0 ary0 = go k0 ary0 0 (A.length ary0) - where - go !k !ary !i !n - | i >= n = ary - | otherwise = case A.index ary i of - (L kx y) | k == kx -> let !v' = f y in A.update ary i (L k v') - | otherwise -> go k ary (i+1) n -{-# INLINABLE updateWith #-} - --- | Append the given key and value to the array. If the key is --- already present, instead update the value of the key by applying --- the given function to the new and old value (in that order). The --- value is always evaluated to WHNF before being inserted into the --- array. -updateOrSnocWith :: Eq k => (v -> v -> v) -> k -> v -> A.Array (Leaf k v) - -> A.Array (Leaf k v) -updateOrSnocWith f = updateOrSnocWithKey (const f) -{-# INLINABLE updateOrSnocWith #-} - --- | Append the given key and value to the array. If the key is --- already present, instead update the value of the key by applying --- the given function to the new and old value (in that order). The --- value is always evaluated to WHNF before being inserted into the --- array. -updateOrSnocWithKey :: Eq k => (k -> v -> v -> v) -> k -> v -> A.Array (Leaf k v) - -> A.Array (Leaf k v) -updateOrSnocWithKey f k0 v0 ary0 = go k0 v0 ary0 0 (A.length ary0) - where - go !k v !ary !i !n - | i >= n = A.run $ do - -- Not found, append to the end. - mary <- A.new_ (n + 1) - A.copy ary 0 mary 0 n - let !l = v `seq` (L k v) - A.write mary n l - return mary - | otherwise = case A.index ary i of - (L kx y) | k == kx -> let !v' = f k v y in A.update ary i (L k v') - | otherwise -> go k v ary (i+1) n -{-# INLINABLE updateOrSnocWithKey #-} - ------------------------------------------------------------------------ -- Smart constructors -- From 8edc380a5bfa25377bb1fd75fbd2ae657416d105 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 15 Aug 2018 12:35:19 +0200 Subject: [PATCH 17/69] woops --- hashmap-strict-properties.prof.html | 931 ---------------------------- 1 file changed, 931 deletions(-) delete mode 100644 hashmap-strict-properties.prof.html diff --git a/hashmap-strict-properties.prof.html b/hashmap-strict-properties.prof.html deleted file mode 100644 index d7558312..00000000 --- a/hashmap-strict-properties.prof.html +++ /dev/null @@ -1,931 +0,0 @@ - - - - - hashmap-strict-properties - - - - - - - - - - - - - - - - -
-
-
-
- -
- - From 29d9f901ff1a823e78432b807abd85b7bbba2322 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 15 Aug 2018 12:38:19 +0200 Subject: [PATCH 18/69] Deleted a rogue comment --- Data/HashMap/Strict/Base.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index 37dad0c0..f15597a5 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -423,7 +423,7 @@ unionWithKey f = go 0 s0 go bs s t1@(Collision h1 hm1) t2@(Leaf h2 l2@(L k2 _)) | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 hm1) t2@(Collision h2 hm2) -- Second salt must be equal + go bs s t1@(Collision h1 hm1) t2@(Collision h2 hm2) | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 hm2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 -- branch vs. branch From 42dc26f763521c3b8329e0c9b7e7fa2fb27c0781 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 21 Aug 2018 12:28:00 +0200 Subject: [PATCH 19/69] little comment --- Data/HashMap/Base.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index f231f664..4073e16c 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -183,7 +183,7 @@ data HashMap k v | BitmapIndexed !Bitmap !(A.Array (HashMap k v)) | Leaf !Hash !(Leaf k v) | Full !(A.Array (HashMap k v)) - | Collision !Hash !(HashMap k v) + | Collision !Hash !(HashMap k v) -- Only necessary to simplify the code deriving (Typeable) type role HashMap nominal representational From b46bbb010414a7b947d8bf666826cb4009b3db7a Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 12 Oct 2018 12:21:55 +0200 Subject: [PATCH 20/69] different hash --- Data/HashMap/Base.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 4073e16c..3947d13d 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -636,11 +636,13 @@ collision h0 s0 !(L k1 v1) !(L k2 v2) = go h0 s0 in Collision h hm {-# INLINE collision #-} +-- These are taken from wikipedia: +-- https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV_hash_parameters defaultSalt :: Int #if WORD_SIZE_IN_BITS == 64 -defaultSalt = -2578643520546668380 -- 0xdc36d1615b7400a4 +defaultSalt = 14695981039346656037 -- 0xcbf29ce484222325 in hex #else -defaultSalt = 0x087fc72c +defaultSalt = 2166136261 -- 0x811c9dc5 in hex #endif nextSalt :: Salt -> Salt From b883ecb0b5b1b058082a719894b3428c7d3efcc9 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 12 Oct 2018 13:57:39 +0200 Subject: [PATCH 21/69] Something that compiles --- Data/HashMap/Base.hs | 227 +++++++++++++++++++++++------------- Data/HashMap/Strict/Base.hs | 61 +++++++--- 2 files changed, 188 insertions(+), 100 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 3947d13d..63a3bba1 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -183,17 +183,17 @@ data HashMap k v | BitmapIndexed !Bitmap !(A.Array (HashMap k v)) | Leaf !Hash !(Leaf k v) | Full !(A.Array (HashMap k v)) - | Collision !Hash !(HashMap k v) -- Only necessary to simplify the code + | Collision !Hash !(Leaf k v) !(Leaf k v) !(HashMap k v) deriving (Typeable) type role HashMap nominal representational instance (NFData k, NFData v) => NFData (HashMap k v) where - rnf Empty = () - rnf (BitmapIndexed _ ary) = rnf ary - rnf (Leaf _ l) = rnf l - rnf (Full ary) = rnf ary - rnf (Collision _ ary) = rnf ary + rnf Empty = () + rnf (BitmapIndexed _ ary) = rnf ary + rnf (Leaf _ l) = rnf l + rnf (Full ary) = rnf ary + rnf (Collision _ l1 l2 ary) = rnf l1 `seq` rnf l2 `seq` rnf ary instance Functor (HashMap k) where fmap = map @@ -296,8 +296,8 @@ equal1 eq = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go (Collision h1 hm1) (Collision h2 hm2) - = h1 == h2 && go hm1 hm2 + go (Collision h1 l11 l12 hm1) (Collision h2 l21 l22 hm2) + = h1 == h2 && l11 `leafEq` l21 && l12 `leafEq` l22 && go hm1 hm2 go _ _ = False leafEq (L k1 v1) (L k2 v2) = k1 == k2 && eq v1 v2 @@ -314,8 +314,10 @@ equal2 eqk eqv t1 t2 = go (toList' t1 []) (toList' t2 []) | k1 == k2 && leafEq l1 l2 = go tl1 tl2 - go (Collision h1 hm1 : tl1) (Collision h2 hm2 : tl2) + go (Collision h1 l11 l12 hm1 : tl1) (Collision h2 l21 l22 hm2 : tl2) | h1 == h2 && + leafEq l11 l21 && + leafEq l12 l22 && go (toList' hm1 []) (toList' hm2 []) = go tl1 tl2 go [] [] = True @@ -347,12 +349,14 @@ cmp cmpk cmpv t1 t2 = go (toList' t1 []) (toList' t2 []) = compare k1 k2 `mappend` leafCompare l1 l2 `mappend` go tl1 tl2 - go (Collision h1 hm1 : tl1) (Collision h2 hm2 : tl2) + go (Collision h1 l11 l12 hm1 : tl1) (Collision h2 l21 l22 hm2 : tl2) = compare h1 h2 `mappend` + leafCompare l11 l21 `mappend` + leafCompare l12 l22 `mappend` go (toList' hm1 []) (toList' hm2 []) `mappend` go tl1 tl2 - go (Leaf _ _ : _) (Collision _ _ : _) = LT - go (Collision _ _ : _) (Leaf _ _ : _) = GT + go (Leaf _ _ : _) (Collision _ _ _ _ : _) = LT + go (Collision _ _ _ _ : _) (Leaf _ _ : _) = GT go [] [] = EQ go [] _ = LT go _ [] = GT @@ -367,8 +371,10 @@ equalKeys1 eq t1 t2 = go (toList' t1 []) (toList' t2 []) go (Leaf k1 l1 : tl1) (Leaf k2 l2 : tl2) | k1 == k2 && leafEq l1 l2 = go tl1 tl2 - go (Collision h1 hm1 : tl1) (Collision h2 hm2 : tl2) + go (Collision h1 l11 l12 hm1 : tl1) (Collision h2 l21 l22 hm2 : tl2) | h1 == h2 && + leafEq l11 l21 && + leafEq l12 l22 && go (toList' hm1 []) (toList' hm2 []) = go tl1 tl2 go [] [] = True @@ -386,8 +392,11 @@ equalKeys = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go (Collision h1 hm1) (Collision h2 hm2) - = h1 == h2 && go hm1 hm2 + go (Collision h1 l11 l12 hm1) (Collision h2 l21 l22 hm2) + = h1 == h2 && + leafEq l11 l21 && + leafEq l12 l22 && + go hm1 hm2 go _ _ = False leafEq (L k1 _) (L k2 _) = k1 == k2 @@ -402,8 +411,9 @@ instance H.Hashable2 HashMap where = s `hashLeafWithSalt` l `go` tl -- For collisions we hashmix hash value -- and then array of values' hashes sorted - go s (Collision h hm' : tl) - = ((s `H.hashWithSalt` h) `go` (toList' hm' [])) `go` tl + go s (Collision h l1 l2 hm' : tl) + = (((s `H.hashWithSalt` h) `hashLeafWithSalt` l1 `hashLeafWithSalt` l2) + `go` (toList' hm' [])) `go` tl go s (_ : tl) = s `go` tl -- hashLeafWithSalt :: Int -> Leaf k v -> Int @@ -422,8 +432,9 @@ instance (Hashable k, Hashable v) => Hashable (HashMap k v) where = s `hashLeafWithSalt` l `go` tl -- For collisions we hashmix hash value -- and then array of values' hashes sorted - go s (Collision h hm' : tl) - = ((s `H.hashWithSalt` h) `go` (toList' hm' [])) `go` tl + go s (Collision h l1 l2 hm' : tl) + = (((s `H.hashWithSalt` h) `hashLeafWithSalt` l1 `hashLeafWithSalt` l2) + `go` (toList' hm' [])) `go` tl go s (_ : tl) = s `go` tl hashLeafWithSalt :: Int -> Leaf k v -> Int @@ -431,17 +442,17 @@ instance (Hashable k, Hashable v) => Hashable (HashMap k v) where -- Helper to get 'Leaf's as a list. toList' :: HashMap k v -> [HashMap k v] -> [HashMap k v] -toList' (BitmapIndexed _ ary) a = A.foldr toList' a ary -toList' (Full ary) a = A.foldr toList' a ary -toList' l@(Leaf _ _) a = l : a -toList' (Collision _ hm) a = toList' hm a -toList' Empty a = a +toList' (BitmapIndexed _ ary) a = A.foldr toList' a ary +toList' (Full ary) a = A.foldr toList' a ary +toList' l@(Leaf _ _) a = l : a +toList' (Collision h l1 l2 hm) a = (Leaf h l1) : (Leaf h l2) : toList' hm a +toList' Empty a = a -- Helper function to detect 'Leaf's and 'Collision's. isLeafOrCollision :: HashMap k v -> Bool -isLeafOrCollision (Leaf _ _) = True -isLeafOrCollision (Collision _ _) = True -isLeafOrCollision _ = False +isLeafOrCollision (Leaf _ _) = True +isLeafOrCollision (Collision _ _ _ _) = True +isLeafOrCollision _ = False ------------------------------------------------------------------------ -- * Construction @@ -470,7 +481,7 @@ size t = go t 0 go (Leaf _ _) n = n + 1 go (BitmapIndexed _ ary) n = A.foldl' (flip go) n ary go (Full ary) n = A.foldl' (flip go) n ary - go (Collision _ hm) n = go hm n + go (Collision _ _ _ hm) n = go hm n + 2 -- | /O(log n)/ Return 'True' if the specified key is present in the -- map, 'False' otherwise. @@ -596,8 +607,11 @@ lookupCont absent present !h0 !s0 !k0 !m0 = go h0 s0 k0 0 m0 where m = mask h bs go h s k bs (Full v) = go h s k (bs+bitsPerSubkey) (A.index v (index h bs)) - go h s k _ (Collision hx hmx) - | h == hx = go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx + go h s k _ (Collision hx (L k1 v1) (L k2 v2) hmx) + | h == hx = case () of + () | k == k1 -> present v1 (-1) + | k == k2 -> present v2 (-1) + | otherwise -> go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx | otherwise = absent (# #) {-# INLINE lookupCont #-} @@ -623,17 +637,7 @@ infixl 9 ! -- | Create a 'Collision' value with two 'Leaf' values. collision :: (Eq k, Hashable k) => Hash -> Salt -> Leaf k v -> Leaf k v -> HashMap k v -collision h0 s0 !(L k1 v1) !(L k2 v2) = go h0 s0 - where - go h s = - let s' = nextSalt s - rehash = hashWithSalt s' - h1 = rehash k1 - h2 = rehash k2 - hm = if h1 == h2 - then go h1 s' - else runST $ two 0 h1 k1 v1 h2 k2 v2 - in Collision h hm +collision h _ l1 l2 = Collision h l1 l2 Empty {-# INLINE collision #-} -- These are taken from wikipedia: @@ -693,8 +697,13 @@ insert' h0 s0 k0 v0 m0 = go h0 s0 k0 v0 0 m0 then t else Full (update16 ary i st') where i = index h bs - go h s k x bs t@(Collision hx hmx) - | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx + go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) + | h == hx = + let go' + | k == k1 = if x `ptrEq` v1 then t else Collision hx (L k x) l2 hmx + | k == k2 = if x `ptrEq` v2 then t else Collision hx l1 (L k x) hmx + | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx + in go' | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insert' #-} @@ -727,8 +736,8 @@ insertNewKey !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 !st' = go h s k x (bs+bitsPerSubkey) st in Full (update16 ary i st') where i = index h bs - go h s k x bs t@(Collision hx hmx) - | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx + go h s k x bs t@(Collision hx l1 l2 hmx) + | h == hx = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# NOINLINE insertNewKey #-} @@ -737,8 +746,8 @@ insertNewKey !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 -- Insert optimized for the case when we know the key is in the map. -- -- We can skip the key equality check on a Leaf because we know the leaf must be --- for this key. -insertKeyExists :: (Hashable k) => Hash -> Salt -> k -> v -> HashMap k v -> HashMap k v +-- for this key. We still need to do the equality check in case of collisions. +insertKeyExists :: (Eq k, Hashable k) => Hash -> Salt -> k -> v -> HashMap k v -> HashMap k v insertKeyExists !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 where go !h _ !k x !_s (Leaf _hy _kx) @@ -758,8 +767,13 @@ insertKeyExists !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 !st' = go h s k x (bs+bitsPerSubkey) st in Full (update16 ary i st') where i = index h bs - go h s k x bs t@(Collision hx hmx) - | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx + go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) + | h == hx = + let go' + | k == k1 = if x `ptrEq` v1 then t else Collision hx (L k x) l2 hmx + | k == k2 = if x `ptrEq` v2 then t else Collision hx l1 (L k x) hmx + | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx + in go' | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) go _ _ _ _ _ Empty = Empty -- error "Internal error: go Empty" @@ -797,8 +811,13 @@ unsafeInsert k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) A.unsafeUpdateM ary i st' return t where i = index h bs - go h s k x bs t@(Collision hx hmx) - | h == hx = Collision hx <$> go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx + go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) + | h == hx = + let go' + | k == k1 = return $! if x `ptrEq` v1 then t else Collision hx (L k x) l2 hmx + | k == k2 = return $! if x `ptrEq` v2 then t else Collision hx l1 (L k x) hmx + | otherwise = Collision hx l1 l2 <$> go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx + in go' | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE unsafeInsert #-} @@ -877,9 +896,19 @@ insertModifying x f k0 m0 = go h0 s0 k0 0 m0 then t else Full ary' where i = index h bs - go h s k bs t@(Collision hx hmx) + go h s k bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) | h == hx = - Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx + let go' + | k == k1 = + case f v1 of + (# v' #) | ptrEq v1 v' -> t + | otherwise -> Collision hx (L k v') l2 hmx + | k == k2 = + case f v2 of + (# v' #) | ptrEq v2 v' -> t + | otherwise -> Collision hx l1 (L k v') hmx + | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx + in go' | otherwise = go h s k bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insertModifying #-} @@ -915,8 +944,13 @@ unsafeInsertWith f k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) A.unsafeUpdateM ary i st' return t where i = index h bs - go h s k x bs t@(Collision hx hmx) - | h == hx = Collision hx <$> go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx + go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) + | h == hx = + let go' + | k == k1 = return $! Collision hx (L k (f x v1)) l2 hmx + | k == k2 = return $! Collision hx l1 (L k (f x v2)) hmx + | otherwise = Collision hx l1 l2 <$> go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx + in go' | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE unsafeInsertWith #-} @@ -966,8 +1000,13 @@ delete' h0 s0 k0 m0 = go h0 s0 k0 0 m0 in BitmapIndexed bm ary' _ -> Full (A.update ary i st') where i = index h bs - go h s k _ t@(Collision hx hmx) - | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx + go h s k _ t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) + | h == hx = + let go' + | k == k1 = undefined + | k == k2 = undefined + | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx + in go' | otherwise = t {-# INLINABLE delete' #-} @@ -975,10 +1014,12 @@ delete' h0 s0 k0 m0 = go h0 s0 k0 0 m0 -- -- We can skip: -- - the key equality check on the leaf, if we reach a leaf it must be the key -deleteKeyExists :: Hashable k => Hash -> Salt -> k -> HashMap k v -> HashMap k v +-- +-- We still need to do the equality check for collisions. +deleteKeyExists :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> HashMap k v deleteKeyExists !h0 s0 !k0 !m0 = go h0 s0 k0 0 m0 where - go :: Hashable k => Hash -> Salt -> k -> Int -> HashMap k v -> HashMap k v + go :: (Eq k, Hashable k) => Hash -> Salt -> k -> Int -> HashMap k v -> HashMap k v go !_h !_s !_k !_bs (Leaf _ _) = Empty go h s k bs (BitmapIndexed b ary) = let !st = A.index ary i @@ -1007,8 +1048,13 @@ deleteKeyExists !h0 s0 !k0 !m0 = go h0 s0 k0 0 m0 in BitmapIndexed bm ary' _ -> Full (A.update ary i st') where i = index h bs - go h s k _ (Collision hx hmx) - | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx + go h s k _ (Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) + | h == hx = + let go' + | k == k1 = undefined + | k == k2 = undefined + | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx + in go' | otherwise = Empty -- error "Internal error: unexpected collision" go !_ !_ !_ !_ Empty = Empty -- error "Internal error: deleteKeyExists empty" {-# NOINLINE deleteKeyExists #-} @@ -1056,8 +1102,13 @@ adjust# f k0 m0 = go h0 s0 k0 0 m0 in if ptrEq st st' then t else Full ary' - go h s k _ t@(Collision hx hmx) - | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx + go h s k _ t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) + | h == hx = + let go' + | k == k1 = undefined + | k == k2 = undefined + | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx + in go' | otherwise = t {-# INLINABLE adjust# #-} @@ -1265,14 +1316,26 @@ unionWithKey f = go 0 s0 then Leaf h1 (L k1 (f k1 v1 v2)) else collision h1 s l1 l2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Leaf h1 l1@(L k1 _)) t2@(Collision h2 hm2) - | h1 == h2 = Collision h1 $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 + go bs s t1@(Leaf h1 l1@(L k1 _)) t2@(Collision h2 l21@(L k21 v21) l22@(L k22 v22) hm2) + | h1 == h2 = + let go' + | True = undefined + | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 + in go' | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 hm1) t2@(Leaf h2 l2@(L k2 _)) - | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) + go bs s t1@(Collision h1 l11@(L k11 v11) l12@(L k12 v12) hm1) t2@(Leaf h2 l2@(L k2 _)) + | h1 == h2 = + let go' + | True = undefined + | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) + in go' | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 hm1) t2@(Collision h2 hm2) - | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 hm2 + go bs s t1@(Collision h1 l11@(L k11 v11) l12@(L k12 v12) hm1) t2@(Collision h2 l21@(L k21 v21) l22@(L k22 v22) hm2) + | h1 == h2 = + let go' + | True = undefined + | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) hm1 hm2 + in go' | otherwise = goDifferentHash bs s h1 h2 t1 t2 -- branch vs. branch go bs s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) = @@ -1324,7 +1387,7 @@ unionWithKey f = go 0 s0 in Full ary' leafHashCode (Leaf h _) = h - leafHashCode (Collision h _) = h + leafHashCode (Collision h _ _ _) = h leafHashCode _ = error "leafHashCode" goDifferentHash bs s h1 h2 t1 t2 @@ -1386,7 +1449,7 @@ mapWithKey f = go go (Full ary) = Full $ A.map go ary -- Why map strictly over collision arrays? Because there's no -- point suspending the O(1) work this does for each leaf. - go (Collision h hm) = Collision h $ go hm + go (Collision h (L k1 v1) (L k2 v2) hm) = Collision h (L k1 (f k2 v1)) (L k2 (f k2 v2)) $ go hm {-# INLINE mapWithKey #-} -- | /O(n)/ Transform this map by applying a function to every value. @@ -1414,8 +1477,8 @@ traverseWithKey f = go go (Leaf h (L k v)) = Leaf h . L k <$> f k v go (BitmapIndexed b ary) = BitmapIndexed b <$> A.traverse go ary go (Full ary) = Full <$> A.traverse go ary - go (Collision h hm) = - Collision h <$> go hm + go (Collision h (L k1 v1) (L k2 v2) hm) = + Collision h <$> (L k1 <$> f k1 v1) <*> (L k2 <$> f k2 v2) <*> go hm {-# INLINE traverseWithKey #-} ------------------------------------------------------------------------ @@ -1501,7 +1564,7 @@ foldlWithKey' f = go go z (Leaf _ (L k v)) = f z k v go z (BitmapIndexed _ ary) = A.foldl' go z ary go z (Full ary) = A.foldl' go z ary - go z (Collision _ hm) = go z hm + go z (Collision _ (L k1 v1) (L k2 v2) hm) = go (f (f z k1 v1) k2 v2) hm {-# INLINE foldlWithKey' #-} -- | /O(n)/ Reduce this map by applying a binary operator to all @@ -1521,7 +1584,7 @@ foldrWithKey f = go go z (Leaf _ (L k v)) = f k v z go z (BitmapIndexed _ ary) = A.foldr (flip go) z ary go z (Full ary) = A.foldr (flip go) z ary - go z (Collision _ hm) = go z hm + go z (Collision _ (L k1 v1) (L k2 v2) hm) = f k1 v1 (f k2 v2 (go z hm)) {-# INLINE foldrWithKey #-} ------------------------------------------------------------------------ @@ -1564,7 +1627,12 @@ filterMapAux onLeaf = go | otherwise = Empty go (BitmapIndexed b ary) = filterA ary b go (Full ary) = filterA ary fullNodeMask - go (Collision h hm) = filterC h hm + go (Collision h l1 l2 hm) = undefined + -- let hm' = go hm + -- in case hm' of + -- Empty -> Empty + -- Leaf _ l -> Leaf h l + -- _ -> Collision h hm' filterA ary0 b0 = let !n = A.length ary0 @@ -1594,13 +1662,6 @@ filterMapAux onLeaf = go (bi `unsafeShiftL` 1) n t -> do A.write mary j t step ary mary b (i+1) (j+1) (bi `unsafeShiftL` 1) n - - filterC h hm = - let hm' = go hm - in case hm' of - Empty -> Empty - Leaf _ l -> Leaf h l - _ -> Collision h hm' {-# INLINE filterMapAux #-} -- | /O(n)/ Filter this map by retaining only elements which values diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index f15597a5..9c8d5021 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -175,8 +175,13 @@ insertWith f k0 v0 m0 = go h0 s0 k0 v0 0 m0 ary' = update16 ary i $! st' in Full ary' where i = index h bs - go h s k x bs t@(Collision hx hm) - | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hm + go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hm) + | h == hx = + let go' + | k == k1 = Collision hx (L k (f x v1)) l2 hm + | k == k2 = Collision hx l1 (L k (f x v2)) hm + | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hm + in go' | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE insertWith #-} @@ -212,8 +217,13 @@ unsafeInsertWith f k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) A.unsafeUpdateM ary i st' return t where i = index h bs - go h s k x bs t@(Collision hx hm) - | h == hx = Collision hx <$> go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hm + go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hm) + | h == hx = + let go' + | k == k1 = return $! Collision hx (L k x) l2 hm + | k == k2 = return $! Collision hx l1 (L k x) hm + | otherwise = Collision hx l1 l2 <$> go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hm + in go' | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) {-# INLINABLE unsafeInsertWith #-} @@ -242,8 +252,13 @@ adjust f k0 m0 = go h0 s0 k0 0 m0 st' = go h s k (bs+bitsPerSubkey) st ary' = update16 ary i $! st' in Full ary' - go h s k _ t@(Collision hx hm) - | h == hx = Collision hx $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hm + go h s k _ t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hm) + | h == hx = + let go' + | k == k1 = Collision hx (L k (f v1)) l2 hm + | k == k2 = Collision hx l1 (L k (f v2)) hm + | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hm + in go' | otherwise = t {-# INLINABLE adjust #-} @@ -417,14 +432,26 @@ unionWithKey f = go 0 s0 then leaf h1 k1 (f k1 v1 v2) else collision h1 s l1 l2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Leaf h1 l1@(L k1 _)) t2@(Collision h2 hm2) - | h1 == h2 = Collision h1 $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 + go bs s t1@(Leaf h1 l1@(L k1 _)) t2@(Collision h2 l21@(L k21 v21) l22@(L k22 v22) hm2) + | h1 == h2 = + let go' + | True = undefined + | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 + in go' | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 hm1) t2@(Leaf h2 l2@(L k2 _)) - | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) + go bs s t1@(Collision h1 l11@(L k11 v11) l12@(L k12 v12) hm1) t2@(Leaf h2 l2@(L k2 _)) + | h1 == h2 = + let go' + | True = undefined + | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) + in go' | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 hm1) t2@(Collision h2 hm2) - | h1 == h2 = Collision h1 $ go 0 (nextSalt s) hm1 hm2 + go bs s t1@(Collision h1 l11@(L k11 v11) l12@(L k12 v12) hm1) t2@(Collision h2 l21@(L k21 v21) l22@(L k22 v22) hm2) + | h1 == h2 = + let go' + | True = undefined + | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) hm1 hm2 + in go' | otherwise = goDifferentHash bs s h1 h2 t1 t2 -- branch vs. branch go bs s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) = @@ -476,7 +503,7 @@ unionWithKey f = go 0 s0 in Full ary' leafHashCode (Leaf h _) = h - leafHashCode (Collision h _) = h + leafHashCode (Collision h _ _ _) = h leafHashCode _ = error "leafHashCode" goDifferentHash bs s h1 h2 t1 t2 @@ -499,8 +526,8 @@ mapWithKey f = go go (Leaf h (L k v)) = leaf h k (f k v) go (BitmapIndexed b ary) = BitmapIndexed b $ A.map' go ary go (Full ary) = Full $ A.map' go ary - go (Collision h hm) = - Collision h $ go hm + go (Collision h (L k1 v1) (L k2 v2) hm) = + Collision h (v1 `seq` (L k1 (f k1 v1))) (v2 `seq` (L k2 (f k2 v2))) (go hm) {-# INLINE mapWithKey #-} -- | /O(n)/ Transform this map by applying a function to every value. @@ -548,8 +575,8 @@ traverseWithKey f = go go (Leaf h (L k v)) = leaf h k <$> f k v go (BitmapIndexed b ary) = BitmapIndexed b <$> A.traverse' go ary go (Full ary) = Full <$> A.traverse' go ary - go (Collision h hm) = - Collision h <$> go hm + go (Collision h (L k1 v1) (L k2 v2) hm) = + Collision h <$> (L k1 <$> f k1 v1) <*> (L k2 <$> f k2 v2) <*> go hm {-# INLINE traverseWithKey #-} ------------------------------------------------------------------------ From 992ca509b204567e6fe1592ff36553958401adbd Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 12 Oct 2018 14:00:59 +0200 Subject: [PATCH 22/69] added back the always collision test, it does not loop infinitely now --- tests/HashMapProperties.hs | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/tests/HashMapProperties.hs b/tests/HashMapProperties.hs index 1bff4973..d34f0d18 100644 --- a/tests/HashMapProperties.hs +++ b/tests/HashMapProperties.hs @@ -128,17 +128,42 @@ pInsert k v = M.insert k v `eq_` HM.insert k v pDelete :: Key -> [(Key, Int)] -> Bool pDelete k = M.delete k `eq_` HM.delete k -newtype MostlyCollide = AC Int +newtype AlwaysCollide = AC Int + deriving (Arbitrary, Eq, Ord, Show) + +instance Hashable AlwaysCollide where + hashWithSalt _ _ = 1 + +pDeleteAlwaysCollision :: AlwaysCollide -> AlwaysCollide -> AlwaysCollide -> Int -> Property +pDeleteAlwaysCollision k1 k2 k3 idx = (k1 /= k2) && (k2 /= k3) && (k1 /= k3) ==> + HM.member toKeep $ HM.delete toDelete $ + HM.fromList [(k1, 1 :: Int), (k2, 2), (k3, 3)] + where + which = idx `mod` 3 + toDelete + | which == 0 = k1 + | which == 1 = k2 + | which == 2 = k3 + | otherwise = error "Impossible" + toKeep + | which == 0 = k2 + | which == 1 = k3 + | which == 2 = k1 + | otherwise = error "Impossible" + + + +newtype MostlyCollide = MC Int deriving (Arbitrary, Eq, Ord, Show) instance Hashable MostlyCollide where - hashWithSalt s (AC i) = (hashWithSalt s i) `mod` 3 + hashWithSalt s (MC i) = (hashWithSalt s i) `mod` 3 -- White-box test that tests the case of deleting one of two keys from -- a map, where the keys' hash values collide. -pDeleteCollision :: MostlyCollide -> MostlyCollide -> MostlyCollide -> Int +pDeleteMostlyCollision :: MostlyCollide -> MostlyCollide -> MostlyCollide -> Int -> Property -pDeleteCollision k1 k2 k3 idx = +pDeleteMostlyCollision k1 k2 k3 idx = (k1 /= k2) && (k2 /= k3) && (k1 /= k3) && (hashWithSalt defaultSalt k1 == hashWithSalt defaultSalt k2) && (hashWithSalt defaultSalt k2 == hashWithSalt defaultSalt k3) && @@ -370,7 +395,8 @@ tests = , testProperty "lookup" pLookup , testProperty "insert" pInsert , testProperty "delete" pDelete - , testProperty "deleteCollision" pDeleteCollision + , testProperty "deleteAlwaysCollision" pDeleteAlwaysCollision + , testProperty "deleteMostlyCollision" pDeleteMostlyCollision , testProperty "insertWith" pInsertWith , testProperty "adjust" pAdjust , testProperty "updateAdjust" pUpdateAdjust From dcec9f1a7232af4bd37e92e0855132b070dab0ef Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 12 Oct 2018 14:12:45 +0200 Subject: [PATCH 23/69] Fixed the strictness properties --- Data/HashMap/Strict/Base.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index 9c8d5021..3939c484 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -178,8 +178,8 @@ insertWith f k0 v0 m0 = go h0 s0 k0 v0 0 m0 go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hm) | h == hx = let go' - | k == k1 = Collision hx (L k (f x v1)) l2 hm - | k == k2 = Collision hx l1 (L k (f x v2)) hm + | k == k1 = Collision hx (L k $! f x v1) l2 hm + | k == k2 = Collision hx l1 (L k $! f x v2) hm | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hm in go' | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) @@ -255,8 +255,8 @@ adjust f k0 m0 = go h0 s0 k0 0 m0 go h s k _ t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hm) | h == hx = let go' - | k == k1 = Collision hx (L k (f v1)) l2 hm - | k == k2 = Collision hx l1 (L k (f v2)) hm + | k == k1 = Collision hx (L k $! f v1) l2 hm + | k == k2 = Collision hx l1 (L k $! f v2) hm | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hm in go' | otherwise = t From c348ab598a518f5fef3a94a5c8a92203befdd7a8 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 12 Oct 2018 15:17:12 +0200 Subject: [PATCH 24/69] Got deletions working, sortof --- Data/HashMap/Base.hs | 61 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 63a3bba1..c59b199c 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1003,8 +1003,14 @@ delete' h0 s0 k0 m0 = go h0 s0 k0 0 m0 go h s k _ t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) | h == hx = let go' - | k == k1 = undefined - | k == k2 = undefined + | k == k1 = case unConsHM hmx of + WasEmpty -> Leaf hx l2 + NowEmpty l3 -> Collision hx l2 l3 Empty + UnConsed l3 hmx' -> Collision hx l2 l3 hmx' + | k == k2 = case unConsHM hmx of + WasEmpty -> Leaf hx l1 + NowEmpty l3 -> Collision hx l1 l3 Empty + UnConsed l3 hmx' -> Collision hx l1 l3 hmx' | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx in go' | otherwise = t @@ -1760,6 +1766,57 @@ updateOrConcatWithKey f ary1 ary2 = A.run $ do return mary {-# INLINABLE updateOrConcatWithKey #-} +-- Helper functions for very rare cases. + +-- Remove (any) one element from an array of hashmaps +unConsA :: A.Array (HashMap k v) -> UnCons k v (A.Array (HashMap k v)) +unConsA ary = case A.length ary of + 0 -> WasEmpty + 1 -> case A.index ary 0 of + Empty -> WasEmpty -- That would be weird, but fine. + Leaf _ l -> NowEmpty l + BitmapIndexed bm ary' -> case unConsA ary' of + WasEmpty -> WasEmpty -- That would be weird, but fine. + NowEmpty l -> NowEmpty l + UnConsed l a' -> UnConsed l (A.singleton (BitmapIndexed bm a')) + Full ary' -> case unConsA ary' of + WasEmpty -> WasEmpty -- That would be weird, but fine. + NowEmpty l -> NowEmpty l + UnConsed l a' -> UnConsed l (A.singleton (Full a')) + Collision h l1 l2 hm -> UnConsed l1 $ A.singleton $ case unConsHM hm of + WasEmpty -> Leaf h l2 + NowEmpty l3 -> Collision h l2 l3 Empty + UnConsed l3 hm' -> Collision h l2 l3 hm' + _ -> goA 0 + where + goA ix = case unConsHM $ A.index ary ix of + WasEmpty -> goA $ ix + 1 -- TODO this is probably problematic when the entire array only houses Empty hashmaps + NowEmpty l -> UnConsed l $ A.delete ary ix + UnConsed l hm' -> UnConsed l $ A.update ary ix hm' + +-- Remove (any) one element from a hashmap +unConsHM :: HashMap k v -> UnCons k v (HashMap k v) +unConsHM hm = case hm of + Empty -> WasEmpty + Leaf _ l -> NowEmpty l + BitmapIndexed bm ary' -> case unConsA ary' of + WasEmpty -> WasEmpty + NowEmpty l -> NowEmpty l + UnConsed l a' -> UnConsed l (BitmapIndexed bm a') + Full ary' -> case unConsA ary' of + WasEmpty -> WasEmpty -- That would be weird, but fine. + NowEmpty l -> NowEmpty l + UnConsed l a' -> UnConsed l (Full a') -- TODO This 'Full' seems wrong + Collision h l1 l2 hm -> UnConsed l1 $ case unConsHM hm of + WasEmpty -> Leaf h l2 + NowEmpty l3 -> Collision h l2 l3 Empty + UnConsed l3 hm' -> Collision h l2 l3 hm' + +data UnCons k v f + = WasEmpty + | NowEmpty (Leaf k v) + | UnConsed (Leaf k v) f + ------------------------------------------------------------------------ -- Manually unrolled loops From 07ed2f5e40a0c4e2d785f4358924ba92d7bb824c Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 12 Oct 2018 15:21:32 +0200 Subject: [PATCH 25/69] Got adjust working, sort-of --- Data/HashMap/Base.hs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index c59b199c..bc100c1d 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1111,8 +1111,12 @@ adjust# f k0 m0 = go h0 s0 k0 0 m0 go h s k _ t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) | h == hx = let go' - | k == k1 = undefined - | k == k2 = undefined + | k == k1 = case f v1 of + (# v1' #) | ptrEq v1 v1' -> t + | otherwise -> Collision hx (L k v1') l2 hmx + | k == k2 = case f v2 of + (# v2' #) | ptrEq v2 v2' -> t + | otherwise -> Collision hx l1 (L k v2') hmx | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx in go' | otherwise = t From 3dda7e1f0a7d6af1347eb9c61e0e3800ae11fe00 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 12 Oct 2018 15:23:46 +0200 Subject: [PATCH 26/69] fixed deleteKeyExists, sort-of --- Data/HashMap/Base.hs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index bc100c1d..e14013e0 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1057,8 +1057,14 @@ deleteKeyExists !h0 s0 !k0 !m0 = go h0 s0 k0 0 m0 go h s k _ (Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) | h == hx = let go' - | k == k1 = undefined - | k == k2 = undefined + | k == k1 = case unConsHM hmx of + WasEmpty -> Leaf hx l2 + NowEmpty l3 -> Collision hx l2 l3 Empty + UnConsed l3 hmx' -> Collision hx l2 l3 hmx' + | k == k2 = case unConsHM hmx of + WasEmpty -> Leaf hx l1 + NowEmpty l3 -> Collision hx l1 l3 Empty + UnConsed l3 hmx' -> Collision hx l1 l3 hmx' | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx in go' | otherwise = Empty -- error "Internal error: unexpected collision" From d4e09fa7e11f8b9b96cf4b9204ba29d192e13718 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 15 Oct 2018 04:54:47 +0200 Subject: [PATCH 27/69] Fixed the lazy union tests --- Data/HashMap/Base.hs | 22 ++++++++++------------ tests/HashMapProperties.hs | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index e14013e0..1d58ebfb 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1332,26 +1332,24 @@ unionWithKey f = go 0 s0 then Leaf h1 (L k1 (f k1 v1 v2)) else collision h1 s l1 l2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Leaf h1 l1@(L k1 _)) t2@(Collision h2 l21@(L k21 v21) l22@(L k22 v22) hm2) + go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Collision h2 l21@(L k21 v21) l22@(L k22 v22) hm2) | h1 == h2 = let go' - | True = undefined - | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 + | k1 == k21 = Collision h1 (L k1 (f k1 v1 v21)) l22 hm2 + | k1 == k22 = Collision h1 l21 (L k1 (f k1 v1 v22)) hm2 + | otherwise = Collision h1 l21 l22 $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 in go' | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 l11@(L k11 v11) l12@(L k12 v12) hm1) t2@(Leaf h2 l2@(L k2 _)) + go bs s t1@(Collision h1 l11@(L k11 v11) l12@(L k12 v12) hm1) t2@(Leaf h2 l2@(L k2 v2)) | h1 == h2 = let go' - | True = undefined - | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) + | k2 == k11 = Collision h1 (L k2 (f k2 v11 v2)) l12 hm1 + | k2 == k12 = Collision h1 l11 (L k2 (f k2 v12 v2)) hm1 + | otherwise = Collision h1 l11 l12 $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) in go' | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 l11@(L k11 v11) l12@(L k12 v12) hm1) t2@(Collision h2 l21@(L k21 v21) l22@(L k22 v22) hm2) - | h1 == h2 = - let go' - | True = undefined - | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) hm1 hm2 - in go' + go bs s t1@(Collision h1 _ _ _) t2@(Collision h2 _ _ _) + | h1 == h2 = L.foldl' (\h (k,v) -> go 0 s h (Leaf (hashWithSalt s k) (L k v))) t1 $ toList t2 -- Nothing better we can do, unfortunately. | otherwise = goDifferentHash bs s h1 h2 t1 t2 -- branch vs. branch go bs s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) = diff --git a/tests/HashMapProperties.hs b/tests/HashMapProperties.hs index d34f0d18..b2b4c422 100644 --- a/tests/HashMapProperties.hs +++ b/tests/HashMapProperties.hs @@ -3,7 +3,7 @@ -- | Tests for the 'Data.HashMap.Lazy' module. We test functions by -- comparing them to a simpler model, an association list. -module Main (main) where +module Main where import Control.Monad ( guard ) import qualified Data.Foldable as Foldable From 65b6783e939389ea684deeb9bb29a9b5c6c9a0c4 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 15 Oct 2018 05:20:19 +0200 Subject: [PATCH 28/69] Fixed the filter functions --- Data/HashMap/Base.hs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 1d58ebfb..862f4a82 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1641,12 +1641,25 @@ filterMapAux onLeaf = go | otherwise = Empty go (BitmapIndexed b ary) = filterA ary b go (Full ary) = filterA ary fullNodeMask - go (Collision h l1 l2 hm) = undefined - -- let hm' = go hm - -- in case hm' of - -- Empty -> Empty - -- Leaf _ l -> Leaf h l - -- _ -> Collision h hm' + go (Collision h l1 l2 hm) = case (onLeaf (Leaf h l1), onLeaf (Leaf h l2)) of + (Just (Leaf _ l1'), Just (Leaf _ l2')) -> Collision h l1' l2' $ go hm + (Just (Leaf _ l1'), Nothing) -> case go' hm of + Nothing -> Leaf h l1' + Just (Leaf _ l3', hm') -> Collision h l1' l3' hm' + (Nothing, Just (Leaf _ l2')) -> case go' hm of + Nothing -> Leaf h l2' + Just (Leaf _ l3', hm') -> Collision h l2' l3' hm' + (Nothing, Nothing) -> go hm + _ -> error "Should not happen, can be fixed with refactoring I think." + where + go' hm = case unConsHM hm of + WasEmpty -> Nothing + NowEmpty l -> case onLeaf (Leaf h l) of + Nothing -> Nothing + Just l' -> Just (l', Empty) + UnConsed l hm' -> case onLeaf (Leaf h l) of + Nothing -> go' hm' + Just l' -> Just (l', go hm') filterA ary0 b0 = let !n = A.length ary0 From 9c2deeb6feab43f1cfade5e4665ec468166f3afa Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 15 Oct 2018 05:29:05 +0200 Subject: [PATCH 29/69] Simplified unconsing hashmaps --- Data/HashMap/Base.hs | 62 +++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 862f4a82..d464a00b 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1004,13 +1004,11 @@ delete' h0 s0 k0 m0 = go h0 s0 k0 0 m0 | h == hx = let go' | k == k1 = case unConsHM hmx of - WasEmpty -> Leaf hx l2 - NowEmpty l3 -> Collision hx l2 l3 Empty - UnConsed l3 hmx' -> Collision hx l2 l3 hmx' + Nothing -> Leaf hx l2 + Just (l3, hmx') -> Collision hx l2 l3 hmx' | k == k2 = case unConsHM hmx of - WasEmpty -> Leaf hx l1 - NowEmpty l3 -> Collision hx l1 l3 Empty - UnConsed l3 hmx' -> Collision hx l1 l3 hmx' + Nothing -> Leaf hx l1 + Just (l3, hmx') -> Collision hx l1 l3 hmx' | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx in go' | otherwise = t @@ -1058,13 +1056,11 @@ deleteKeyExists !h0 s0 !k0 !m0 = go h0 s0 k0 0 m0 | h == hx = let go' | k == k1 = case unConsHM hmx of - WasEmpty -> Leaf hx l2 - NowEmpty l3 -> Collision hx l2 l3 Empty - UnConsed l3 hmx' -> Collision hx l2 l3 hmx' + Nothing -> Leaf hx l2 + Just (l3, hmx') -> Collision hx l2 l3 hmx' | k == k2 = case unConsHM hmx of - WasEmpty -> Leaf hx l1 - NowEmpty l3 -> Collision hx l1 l3 Empty - UnConsed l3 hmx' -> Collision hx l1 l3 hmx' + Nothing -> Leaf hx l1 + Just (l3, hmx') -> Collision hx l1 l3 hmx' | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx in go' | otherwise = Empty -- error "Internal error: unexpected collision" @@ -1653,11 +1649,8 @@ filterMapAux onLeaf = go _ -> error "Should not happen, can be fixed with refactoring I think." where go' hm = case unConsHM hm of - WasEmpty -> Nothing - NowEmpty l -> case onLeaf (Leaf h l) of - Nothing -> Nothing - Just l' -> Just (l', Empty) - UnConsed l hm' -> case onLeaf (Leaf h l) of + Nothing -> Nothing + Just (l, hm') -> case onLeaf (Leaf h l) of Nothing -> go' hm' Just l' -> Just (l', go hm') @@ -1805,33 +1798,30 @@ unConsA ary = case A.length ary of NowEmpty l -> NowEmpty l UnConsed l a' -> UnConsed l (A.singleton (Full a')) Collision h l1 l2 hm -> UnConsed l1 $ A.singleton $ case unConsHM hm of - WasEmpty -> Leaf h l2 - NowEmpty l3 -> Collision h l2 l3 Empty - UnConsed l3 hm' -> Collision h l2 l3 hm' + Nothing -> Leaf h l2 + Just (l3, hm') -> Collision h l2 l3 hm' _ -> goA 0 where goA ix = case unConsHM $ A.index ary ix of - WasEmpty -> goA $ ix + 1 -- TODO this is probably problematic when the entire array only houses Empty hashmaps - NowEmpty l -> UnConsed l $ A.delete ary ix - UnConsed l hm' -> UnConsed l $ A.update ary ix hm' + Nothing -> goA $ ix + 1 -- TODO this is probably problematic when the entire array only houses Empty hashmaps + Just (l, hm') -> UnConsed l $ A.update ary ix hm' -- Remove (any) one element from a hashmap -unConsHM :: HashMap k v -> UnCons k v (HashMap k v) +unConsHM :: HashMap k v -> Maybe (Leaf k v, HashMap k v) unConsHM hm = case hm of - Empty -> WasEmpty - Leaf _ l -> NowEmpty l + Empty -> Nothing + Leaf _ l -> Just (l, Empty) BitmapIndexed bm ary' -> case unConsA ary' of - WasEmpty -> WasEmpty - NowEmpty l -> NowEmpty l - UnConsed l a' -> UnConsed l (BitmapIndexed bm a') + WasEmpty -> Nothing + NowEmpty l -> Just (l, Empty) + UnConsed l a' -> Just (l, BitmapIndexed bm a') Full ary' -> case unConsA ary' of - WasEmpty -> WasEmpty -- That would be weird, but fine. - NowEmpty l -> NowEmpty l - UnConsed l a' -> UnConsed l (Full a') -- TODO This 'Full' seems wrong - Collision h l1 l2 hm -> UnConsed l1 $ case unConsHM hm of - WasEmpty -> Leaf h l2 - NowEmpty l3 -> Collision h l2 l3 Empty - UnConsed l3 hm' -> Collision h l2 l3 hm' + WasEmpty -> Nothing -- That would be weird, but fine. + NowEmpty l -> Just (l, Empty) + UnConsed l a' -> Just (l, Full a') -- TODO This 'Full' seems wrong + Collision h l1 l2 hm -> Just $ (,) l1 $ case unConsHM hm of + Nothing -> Leaf h l2 + Just (l3, hm') -> Collision h l2 l3 hm' data UnCons k v f = WasEmpty From 2951ae722ae902d0e4e229692d866356670c67c2 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 15 Oct 2018 05:29:59 +0200 Subject: [PATCH 30/69] fixed strict unions --- Data/HashMap/Strict/Base.hs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index 3939c484..005c7ae5 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -432,28 +432,25 @@ unionWithKey f = go 0 s0 then leaf h1 k1 (f k1 v1 v2) else collision h1 s l1 l2 | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Leaf h1 l1@(L k1 _)) t2@(Collision h2 l21@(L k21 v21) l22@(L k22 v22) hm2) + go bs s t1@(Leaf h1 l1@(L k1 v1)) t2@(Collision h2 l21@(L k21 v21) l22@(L k22 v22) hm2) | h1 == h2 = let go' - | True = undefined - | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 + | k1 == k21 = Collision h1 (L k1 (f k1 v1 v21)) l22 hm2 + | k1 == k22 = Collision h1 l21 (L k1 (f k1 v1 v22)) hm2 + | otherwise = Collision h1 l21 l22 $ go 0 (nextSalt s) (Leaf (hashWithSalt (nextSalt s) k1) l1) hm2 in go' | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 l11@(L k11 v11) l12@(L k12 v12) hm1) t2@(Leaf h2 l2@(L k2 _)) + go bs s t1@(Collision h1 l11@(L k11 v11) l12@(L k12 v12) hm1) t2@(Leaf h2 l2@(L k2 v2)) | h1 == h2 = let go' - | True = undefined - | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) + | k2 == k11 = Collision h1 (L k2 (f k2 v11 v2)) l12 hm1 + | k2 == k12 = Collision h1 l11 (L k2 (f k2 v12 v2)) hm1 + | otherwise = Collision h1 l11 l12 $ go 0 (nextSalt s) hm1 (Leaf (hashWithSalt (nextSalt s) k2) l2) in go' | otherwise = goDifferentHash bs s h1 h2 t1 t2 - go bs s t1@(Collision h1 l11@(L k11 v11) l12@(L k12 v12) hm1) t2@(Collision h2 l21@(L k21 v21) l22@(L k22 v22) hm2) - | h1 == h2 = - let go' - | True = undefined - | otherwise = Collision h1 undefined undefined $ go 0 (nextSalt s) hm1 hm2 - in go' + go bs s t1@(Collision h1 _ _ _) t2@(Collision h2 _ _ _) + | h1 == h2 = L.foldl' (\h (k,v) -> go 0 s h (Leaf (hashWithSalt s k) (L k v))) t1 $ toList t2 -- Nothing better we can do, unfortunately. | otherwise = goDifferentHash bs s h1 h2 t1 t2 - -- branch vs. branch go bs s (BitmapIndexed b1 ary1) (BitmapIndexed b2 ary2) = let b' = b1 .|. b2 ary' = unionArrayBy (go (bs+bitsPerSubkey) s) b1 b2 ary1 ary2 From 1d71c962ae0f3284875da606b1ec9dc5631f086e Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 15 Oct 2018 06:15:16 +0200 Subject: [PATCH 31/69] little refactorings --- Data/HashMap/Base.hs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index d464a00b..e7044c8c 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1000,7 +1000,7 @@ delete' h0 s0 k0 m0 = go h0 s0 k0 0 m0 in BitmapIndexed bm ary' _ -> Full (A.update ary i st') where i = index h bs - go h s k _ t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) + go h s k _ t@(Collision hx l1@(L k1 _) l2@(L k2 _) hmx) | h == hx = let go' | k == k1 = case unConsHM hmx of @@ -1052,7 +1052,7 @@ deleteKeyExists !h0 s0 !k0 !m0 = go h0 s0 k0 0 m0 in BitmapIndexed bm ary' _ -> Full (A.update ary i st') where i = index h bs - go h s k _ (Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) + go h s k _ (Collision hx l1@(L k1 _) l2@(L k2 _) hmx) | h == hx = let go' | k == k1 = case unConsHM hmx of @@ -1642,17 +1642,19 @@ filterMapAux onLeaf = go (Just (Leaf _ l1'), Nothing) -> case go' hm of Nothing -> Leaf h l1' Just (Leaf _ l3', hm') -> Collision h l1' l3' hm' + _ -> error "Should not happen, can be fixed with refactoring I think." (Nothing, Just (Leaf _ l2')) -> case go' hm of Nothing -> Leaf h l2' Just (Leaf _ l3', hm') -> Collision h l2' l3' hm' + _ -> error "Should not happen, can be fixed with refactoring I think." (Nothing, Nothing) -> go hm _ -> error "Should not happen, can be fixed with refactoring I think." where - go' hm = case unConsHM hm of + go' hm_ = case unConsHM hm_ of Nothing -> Nothing - Just (l, hm') -> case onLeaf (Leaf h l) of - Nothing -> go' hm' - Just l' -> Just (l', go hm') + Just (l, hm_') -> case onLeaf (Leaf h l) of + Nothing -> go' hm_' + Just l' -> Just (l', go hm_') filterA ary0 b0 = let !n = A.length ary0 @@ -1783,7 +1785,7 @@ updateOrConcatWithKey f ary1 ary2 = A.run $ do -- Helper functions for very rare cases. -- Remove (any) one element from an array of hashmaps -unConsA :: A.Array (HashMap k v) -> UnCons k v (A.Array (HashMap k v)) +unConsA :: A.Array (HashMap k v) -> UnCons k v unConsA ary = case A.length ary of 0 -> WasEmpty 1 -> case A.index ary 0 of @@ -1796,7 +1798,7 @@ unConsA ary = case A.length ary of Full ary' -> case unConsA ary' of WasEmpty -> WasEmpty -- That would be weird, but fine. NowEmpty l -> NowEmpty l - UnConsed l a' -> UnConsed l (A.singleton (Full a')) + UnConsed l a' -> UnConsed l (A.singleton (Full a')) -- 'Full' seems wrong Collision h l1 l2 hm -> UnConsed l1 $ A.singleton $ case unConsHM hm of Nothing -> Leaf h l2 Just (l3, hm') -> Collision h l2 l3 hm' @@ -1806,6 +1808,11 @@ unConsA ary = case A.length ary of Nothing -> goA $ ix + 1 -- TODO this is probably problematic when the entire array only houses Empty hashmaps Just (l, hm') -> UnConsed l $ A.update ary ix hm' +data UnCons k v + = WasEmpty + | NowEmpty (Leaf k v) + | UnConsed (Leaf k v) (A.Array (HashMap k v)) + -- Remove (any) one element from a hashmap unConsHM :: HashMap k v -> Maybe (Leaf k v, HashMap k v) unConsHM hm = case hm of @@ -1819,14 +1826,9 @@ unConsHM hm = case hm of WasEmpty -> Nothing -- That would be weird, but fine. NowEmpty l -> Just (l, Empty) UnConsed l a' -> Just (l, Full a') -- TODO This 'Full' seems wrong - Collision h l1 l2 hm -> Just $ (,) l1 $ case unConsHM hm of + Collision h l1 l2 hm' -> Just $ (,) l1 $ case unConsHM hm' of Nothing -> Leaf h l2 - Just (l3, hm') -> Collision h l2 l3 hm' - -data UnCons k v f - = WasEmpty - | NowEmpty (Leaf k v) - | UnConsed (Leaf k v) f + Just (l3, hm'') -> Collision h l2 l3 hm'' ------------------------------------------------------------------------ -- Manually unrolled loops From 822f08d84127ce47bda79d601ea5c183cfa747e5 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 15 Oct 2018 06:59:28 +0200 Subject: [PATCH 32/69] Fixed equality, and messed up hashable because we just fixed what was be a false positive --- Data/HashMap/Base.hs | 48 ++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index e7044c8c..16a75fa7 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -137,6 +137,7 @@ import qualified Data.Hashable as H import Data.Hashable (Hashable) import Data.HashMap.Unsafe (runST) import Data.HashMap.UnsafeShift (unsafeShiftL, unsafeShiftR) +import Data.HashMap.List (isPermutationBy, unorderedCompare) import Data.Typeable (Typeable) import GHC.Exts (isTrue#) @@ -296,8 +297,11 @@ equal1 eq = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go (Collision h1 l11 l12 hm1) (Collision h2 l21 l22 hm2) - = h1 == h2 && l11 `leafEq` l21 && l12 `leafEq` l22 && go hm1 hm2 + go (Collision h1 (L k11 v11) (L k12 v12) hm1) (Collision h2 (L k21 v21) (L k22 v22) hm2) + = h1 == h2 + && isPermutationBy (\(k1, v1) (k2, v2) -> k1 == k2 && eq v1 v2) + ((k11, v11) : (k12, v12) : toList hm1) + ((k21, v21) : (k22, v22) : toList hm2) go _ _ = False leafEq (L k1 v1) (L k2 v2) = k1 == k2 && eq v1 v2 @@ -310,15 +314,16 @@ equal2 eqk eqv t1 t2 = go (toList' t1 []) (toList' t2 []) -- 'Collision's read from left to right should be the same (modulo the -- order of elements in 'Collision'). - go (Leaf k1 l1 : tl1) (Leaf k2 l2 : tl2) - | k1 == k2 && + go (Leaf h1 l1 : tl1) (Leaf h2 l2 : tl2) + | h1 == h2 && leafEq l1 l2 = go tl1 tl2 - go (Collision h1 l11 l12 hm1 : tl1) (Collision h2 l21 l22 hm2 : tl2) + go (Collision h1 (L k11 v11) (L k12 v12) hm1 : tl1) (Collision h2 (L k21 v21) (L k22 v22) hm2 : tl2) | h1 == h2 && - leafEq l11 l21 && - leafEq l12 l22 && - go (toList' hm1 []) (toList' hm2 []) + (length hm1 == length hm2) && + isPermutationBy (\(k1, v1) (k2, v2) -> eqk k1 k2 && eqv v1 v2) + ((k11, v11) : (k12, v12) : toList hm1) + ((k21, v21) : (k22, v22) : toList hm2) = go tl1 tl2 go [] [] = True go _ _ = False @@ -349,11 +354,12 @@ cmp cmpk cmpv t1 t2 = go (toList' t1 []) (toList' t2 []) = compare k1 k2 `mappend` leafCompare l1 l2 `mappend` go tl1 tl2 - go (Collision h1 l11 l12 hm1 : tl1) (Collision h2 l21 l22 hm2 : tl2) + go (Collision h1 (L k11 v11) (L k12 v12) hm1 : tl1) (Collision h2 (L k21 v21) (L k22 v22) hm2 : tl2) = compare h1 h2 `mappend` - leafCompare l11 l21 `mappend` - leafCompare l12 l22 `mappend` - go (toList' hm1 []) (toList' hm2 []) `mappend` + compare (length hm1) (length hm2) `mappend` + unorderedCompare (\(k1, v1) (k2, v2) -> (k1 `cmpk` k2) `mappend` (v1 `cmpv` v2)) + ((k11, v11) : (k12, v12) : toList hm1) + ((k21, v21) : (k22, v22) : toList hm2) `mappend` go tl1 tl2 go (Leaf _ _ : _) (Collision _ _ _ _ : _) = LT go (Collision _ _ _ _ : _) (Leaf _ _ : _) = GT @@ -371,11 +377,12 @@ equalKeys1 eq t1 t2 = go (toList' t1 []) (toList' t2 []) go (Leaf k1 l1 : tl1) (Leaf k2 l2 : tl2) | k1 == k2 && leafEq l1 l2 = go tl1 tl2 - go (Collision h1 l11 l12 hm1 : tl1) (Collision h2 l21 l22 hm2 : tl2) + go (Collision h1 (L k11 _) (L k12 _) hm1 : tl1) (Collision h2 (L k21 _) (L k22 _) hm2 : tl2) | h1 == h2 && - leafEq l11 l21 && - leafEq l12 l22 && - go (toList' hm1 []) (toList' hm2 []) + (length hm1 == length hm2) && + isPermutationBy eq + (k11 : k12 : keys hm1) + (k21 : k22 : keys hm2) = go tl1 tl2 go [] [] = True go _ _ = False @@ -392,11 +399,12 @@ equalKeys = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go (Collision h1 l11 l12 hm1) (Collision h2 l21 l22 hm2) + go (Collision h1 (L k11 _) (L k12 _) hm1) (Collision h2 (L k21 _) (L k22 _) hm2) = h1 == h2 && - leafEq l11 l21 && - leafEq l12 l22 && - go hm1 hm2 + (length hm1 == length hm2) && + isPermutationBy (==) + (k11 : k12 : keys hm1) + (k21 : k22 : keys hm2) go _ _ = False leafEq (L k1 _) (L k2 _) = k1 == k2 From f5bbe44abbca5e31eb4748453a3f02222c51e7a1 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 15 Oct 2018 07:22:58 +0200 Subject: [PATCH 33/69] Fixed the hashing --- Data/HashMap/Base.hs | 25 +++++++++++++++++-------- Data/HashMap/Strict/Base.hs | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 16a75fa7..fe115927 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -419,11 +419,15 @@ instance H.Hashable2 HashMap where = s `hashLeafWithSalt` l `go` tl -- For collisions we hashmix hash value -- and then array of values' hashes sorted - go s (Collision h l1 l2 hm' : tl) - = (((s `H.hashWithSalt` h) `hashLeafWithSalt` l1 `hashLeafWithSalt` l2) - `go` (toList' hm' [])) `go` tl + go s (Collision _ l1 l2 hm' : tl) + = collisionHash l1 l2 hm' s `go` tl go s (_ : tl) = s `go` tl + collisionHash (L k1 v1) (L k2 v2) hm' s + = L.foldl' H.hashWithSalt s + $ L.sort . L.map (\(k, v) -> (s `hk` k) `hv` v) + $ (k1, v1) : (k2, v2) : toList hm' + -- hashLeafWithSalt :: Int -> Leaf k v -> Int hashLeafWithSalt s (L k v) = (s `hk` k) `hv` v @@ -440,20 +444,25 @@ instance (Hashable k, Hashable v) => Hashable (HashMap k v) where = s `hashLeafWithSalt` l `go` tl -- For collisions we hashmix hash value -- and then array of values' hashes sorted - go s (Collision h l1 l2 hm' : tl) - = (((s `H.hashWithSalt` h) `hashLeafWithSalt` l1 `hashLeafWithSalt` l2) - `go` (toList' hm' [])) `go` tl + go s (Collision _ l1 l2 hm' : tl) + = collisionHash l1 l2 hm' s `go` tl go s (_ : tl) = s `go` tl + collisionHash :: Leaf k v -> Leaf k v -> HashMap k v -> Int -> Int + collisionHash (L k1 v1) (L k2 v2) hm' s + = L.foldl' H.hashWithSalt s + $ L.sort . L.map (H.hashWithSalt s) + $ (k1, v1) : (k2, v2) : toList hm' + hashLeafWithSalt :: Int -> Leaf k v -> Int hashLeafWithSalt s (L k v) = s `H.hashWithSalt` k `H.hashWithSalt` v - -- Helper to get 'Leaf's as a list. + -- Helper to get 'Leaf's as a list, leave collisions as-is. toList' :: HashMap k v -> [HashMap k v] -> [HashMap k v] toList' (BitmapIndexed _ ary) a = A.foldr toList' a ary toList' (Full ary) a = A.foldr toList' a ary toList' l@(Leaf _ _) a = l : a -toList' (Collision h l1 l2 hm) a = (Leaf h l1) : (Leaf h l2) : toList' hm a +toList' (Collision h l1 l2 hm) a = Collision h l1 l2 hm : a toList' Empty a = a -- Helper function to detect 'Leaf's and 'Collision's. diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index 005c7ae5..aa7c6fab 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -217,7 +217,7 @@ unsafeInsertWith f k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) A.unsafeUpdateM ary i st' return t where i = index h bs - go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hm) + go h s k x bs t@(Collision hx l1@(L k1 _) l2@(L k2 _) hm) | h == hx = let go' | k == k1 = return $! Collision hx (L k x) l2 hm From 0a7b81c0bb4c1695576b4795a343422bb40b9330 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 15 Oct 2018 07:42:13 +0200 Subject: [PATCH 34/69] Strictness seems to have been fixed --- Data/HashMap/Strict/Base.hs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index aa7c6fab..40331d00 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -153,10 +153,10 @@ insertWith f k0 v0 m0 = go h0 s0 k0 v0 0 m0 h0 = hashWithSalt s0 k0 s0 = defaultSalt go !h _ !k x !_ Empty = leaf h k x - go h s k x bs (Leaf hy l@(L ky y)) + go h s k x bs (Leaf hy lf@(L ky y)) | hy == h = if ky == k then leaf h k (f x y) - else x `seq` (collision h s l (L k x)) + else collision h s lf (l k x) | otherwise = x `seq` runST (two bs h k x hy ky y) go h s k x bs (BitmapIndexed b ary) | b .&. m == 0 = @@ -178,8 +178,8 @@ insertWith f k0 v0 m0 = go h0 s0 k0 v0 0 m0 go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hm) | h == hx = let go' - | k == k1 = Collision hx (L k $! f x v1) l2 hm - | k == k2 = Collision hx l1 (L k $! f x v2) hm + | k == k1 = Collision hx (l k (f x v1)) l2 hm + | k == k2 = Collision hx l1 (l k (f x v2)) hm | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hm in go' | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) @@ -193,12 +193,10 @@ unsafeInsertWith f k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) h0 = hashWithSalt s0 k0 s0 = defaultSalt go !h !_ !k x !_ Empty = return $! leaf h k x - go h s k x bs (Leaf hy l@(L ky y)) + go h s k x bs (Leaf hy lf@(L ky y)) | hy == h = if ky == k then return $! leaf h k (f x y) - else do - let l' = x `seq` (L k x) - return $! collision h s l l' + else return $! collision h s lf (l k x) | otherwise = x `seq` two bs h k x hy ky y go h s k x bs t@(BitmapIndexed b ary) | b .&. m == 0 = do @@ -217,11 +215,11 @@ unsafeInsertWith f k0 v0 m0 = runST (go h0 s0 k0 v0 0 m0) A.unsafeUpdateM ary i st' return t where i = index h bs - go h s k x bs t@(Collision hx l1@(L k1 _) l2@(L k2 _) hm) + go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hm) | h == hx = let go' - | k == k1 = return $! Collision hx (L k x) l2 hm - | k == k2 = return $! Collision hx l1 (L k x) hm + | k == k1 = return $! Collision hx (l k (f x v1)) l2 hm + | k == k2 = return $! Collision hx l1 (l k (f x v2)) hm | otherwise = Collision hx l1 l2 <$> go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hm in go' | otherwise = go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) @@ -649,5 +647,8 @@ fromListWith f = L.foldl' (\ m (k, v) -> unsafeInsertWith f k v m) empty -- inserted into the constructor. leaf :: Hash -> k -> v -> HashMap k v -leaf h k = \ !v -> Leaf h (L k v) +leaf h k = \ !v -> Leaf h (l k v) + +l :: k -> v -> Leaf k v +l k = \ !v -> L k v {-# INLINE leaf #-} From 4e6e23d39597537a823037f0220a0f3dbda8f239 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 15 Oct 2018 07:48:09 +0200 Subject: [PATCH 35/69] All tests seem to have been fixed now --- Data/HashMap/Base.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index fe115927..f11ffda9 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1815,7 +1815,7 @@ unConsA ary = case A.length ary of Full ary' -> case unConsA ary' of WasEmpty -> WasEmpty -- That would be weird, but fine. NowEmpty l -> NowEmpty l - UnConsed l a' -> UnConsed l (A.singleton (Full a')) -- 'Full' seems wrong + UnConsed l a' -> UnConsed l (A.singleton (Full a')) -- TODO 'Full' seems wrong Collision h l1 l2 hm -> UnConsed l1 $ A.singleton $ case unConsHM hm of Nothing -> Leaf h l2 Just (l3, hm') -> Collision h l2 l3 hm' @@ -1823,7 +1823,9 @@ unConsA ary = case A.length ary of where goA ix = case unConsHM $ A.index ary ix of Nothing -> goA $ ix + 1 -- TODO this is probably problematic when the entire array only houses Empty hashmaps - Just (l, hm') -> UnConsed l $ A.update ary ix hm' + Just (l, hm') -> case hm' of + Empty -> UnConsed l $ A.delete ary ix + _ -> UnConsed l $ A.update ary ix hm' data UnCons k v = WasEmpty From 6810a2972eb31ed9c26d95b87ea2775f83be7ab7 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 15 Oct 2018 08:03:16 +0200 Subject: [PATCH 36/69] Refactored lookupRes --- Data/HashMap/Base.hs | 28 ++++++++++++++-------------- Data/HashMap/Strict/Base.hs | 6 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index f11ffda9..e6372581 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -152,7 +152,7 @@ import qualified Data.Hashable.Lifted as H #endif #if __GLASGOW_HASKELL__ >= 802 -import GHC.Exts (TYPE, Int (..), Int#) +import GHC.Exts (TYPE) #endif #if MIN_VERSION_base(4,8,0) @@ -521,7 +521,7 @@ lookup k m = case lookup# k m of {-# INLINE lookup #-} lookup# :: (Eq k, Hashable k) => k -> HashMap k v -> (# (# #) | v #) -lookup# k m = lookupCont (\_ -> (# (# #) | #)) (\v _i -> (# | v #)) (hashWithSalt defaultSalt k) defaultSalt k m +lookup# k m = lookupCont (\_ -> (# (# #) | #)) (\v -> (# | v #)) (hashWithSalt defaultSalt k) defaultSalt k m {-# INLINABLE lookup# #-} #else @@ -542,7 +542,7 @@ lookup' :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> Maybe v -- code. lookup' h s k m = case lookupRecordCollision# h s k m of (# (# #) | #) -> Nothing - (# | (# a, _i #) #) -> Just a + (# | a #) -> Just a {-# INLINE lookup' #-} #else lookup' h s k m = lookupCont (\_ -> Nothing) (\v _i -> Just v) h s k m @@ -551,7 +551,7 @@ lookup' h s k m = lookupCont (\_ -> Nothing) (\v _i -> Just v) h s k m -- The result of a lookup, keeping track of if a hash collision occured. -- If a collision did not occur then it will have the Int value (-1). -data LookupRes a = Absent | Present a !Int +data LookupRes a = Absent | Present a -- Internal helper for lookup. This version takes the precomputed hash so -- that functions that make multiple calls to lookup and related functions @@ -570,7 +570,7 @@ lookupRecordCollision :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v #if __GLASGOW_HASKELL__ >= 802 lookupRecordCollision h s k m = case lookupRecordCollision# h s k m of (# (# #) | #) -> Absent - (# | (# a, i #) #) -> Present a (I# i) -- GHC will eliminate the I# + (# | a #) -> Present a {-# INLINE lookupRecordCollision #-} -- Why do we produce an Int# instead of an Int? Unfortunately, GHC is not @@ -578,9 +578,9 @@ lookupRecordCollision h s k m = case lookupRecordCollision# h s k m of -- may be changing in GHC 8.6 or so (there is some work in progress), but -- for now we use Int# explicitly here. We don't need to push the Int# -- into lookupCont because inlining takes care of that. -lookupRecordCollision# :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> (# (# #) | (# v, Int# #) #) +lookupRecordCollision# :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> (# (# #) | v #) lookupRecordCollision# h s k m = - lookupCont (\_ -> (# (# #) | #)) (\v (I# i) -> (# | (# v, i #) #)) h s k m + lookupCont (\_ -> (# (# #) | #)) (\v -> (# | v #)) h s k m -- INLINABLE to specialize to the Eq instance. {-# INLINABLE lookupRecordCollision# #-} @@ -606,7 +606,7 @@ lookupCont :: #endif (Eq k, Hashable k) => ((# #) -> r) -- Absent continuation - -> (v -> Int -> r) -- Present continuation + -> (v -> r) -- Present continuation -> Hash -- The hash of the key -> Salt -- The salt that was used to obtain that hash -> k -> HashMap k v -> r @@ -615,7 +615,7 @@ lookupCont absent present !h0 !s0 !k0 !m0 = go h0 s0 k0 0 m0 go :: Eq k => Hash -> Salt -> k -> Int -> HashMap k v -> r go !_ !_ !_ !_ Empty = absent (# #) go h _ k _ (Leaf hx (L kx x)) - | h == hx && k == kx = present x (-1) + | h == hx && k == kx = present x | otherwise = absent (# #) go h s k bs (BitmapIndexed b v) | b .&. m == 0 = absent (# #) @@ -626,8 +626,8 @@ lookupCont absent present !h0 !s0 !k0 !m0 = go h0 s0 k0 0 m0 go h s k (bs+bitsPerSubkey) (A.index v (index h bs)) go h s k _ (Collision hx (L k1 v1) (L k2 v2) hmx) | h == hx = case () of - () | k == k1 -> present v1 (-1) - | k == k2 -> present v2 (-1) + () | k == k1 -> present v1 + | k == k2 -> present v2 | otherwise -> go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx | otherwise = absent (# #) {-# INLINE lookupCont #-} @@ -1284,7 +1284,7 @@ alterFEager f !k m = (<$> f mv) $ \fres -> Absent -> m -- Key did exist - Present _ _ -> deleteKeyExists h s k m + Present _ -> deleteKeyExists h s k m ------------------------------ -- Update value @@ -1294,7 +1294,7 @@ alterFEager f !k m = (<$> f mv) $ \fres -> Absent -> insertNewKey h s k v' m -- Key existed before - Present v _ -> + Present v -> if v `ptrEq` v' -- If the value is identical, no-op then m @@ -1306,7 +1306,7 @@ alterFEager f !k m = (<$> f mv) $ \fres -> !lookupRes = lookupRecordCollision h s k m !mv = case lookupRes of Absent -> Nothing - Present v _ -> Just v + Present v -> Just v {-# INLINABLE alterFEager #-} #endif diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index 40331d00..55efc28c 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -378,7 +378,7 @@ alterFEager f !k !m = (<$> f mv) $ \fres -> Absent -> m -- Key did exist, no collision - Present _ _ -> deleteKeyExists h s k m + Present _ -> deleteKeyExists h s k m ------------------------------ -- Update value @@ -388,7 +388,7 @@ alterFEager f !k !m = (<$> f mv) $ \fres -> Absent -> insertNewKey h s k v' m -- Key existed before, no hash collision - Present v _ -> v' `seq` + Present v -> v' `seq` if v `ptrEq` v' -- If the value is identical, no-op then m @@ -400,7 +400,7 @@ alterFEager f !k !m = (<$> f mv) $ \fres -> !lookupRes = lookupRecordCollision h s k m !mv = case lookupRes of Absent -> Nothing - Present v _ -> Just v + Present v -> Just v {-# INLINABLE alterFEager #-} #endif From 5dd6dd2066aaebe5e023c7e0446009061f6c643d Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 17 Oct 2018 09:42:49 +0200 Subject: [PATCH 37/69] Dealth with some review comments --- Data/HashMap/Base.hs | 56 +++++++++++++++++-------------------- Data/HashMap/Strict/Base.hs | 2 +- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index e6372581..4a434cf9 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -100,7 +100,7 @@ module Data.HashMap.Base , filterMapAux , equalKeys , equalKeys1 - , lookupRecordCollision + , lookupWithRes , LookupRes(..) , insert' , delete' @@ -540,7 +540,7 @@ lookup' :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> Maybe v -- lookup' would probably prefer to be implemented in terms of its own -- lookup'#, but it's not important enough and we don't want too much -- code. -lookup' h s k m = case lookupRecordCollision# h s k m of +lookup' h s k m = case lookupWithRes# h s k m of (# (# #) | #) -> Nothing (# | a #) -> Just a {-# INLINE lookup' #-} @@ -564,34 +564,33 @@ data LookupRes a = Absent | Present a -- -- Outcomes: -- Key not in map => Absent --- Key in map, no collision => Present v (-1) --- Key in map, collision => Present v position -lookupRecordCollision :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> LookupRes v +-- Key in map => Present v +-- +-- TODO(syd): We could add some optimisation here where we optimise for the common case +-- where a collision is in the first two elements and not in the recursive HashMap. +-- This could mean turning 'Present a' into 'Present a ColPos' with +-- 'data ColPos = NoCollision | CollisionFirst | CollisionSecond | CollisionRest' +lookupWithRes :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> LookupRes v #if __GLASGOW_HASKELL__ >= 802 -lookupRecordCollision h s k m = case lookupRecordCollision# h s k m of +lookupWithRes h s k m = case lookupWithRes# h s k m of (# (# #) | #) -> Absent (# | a #) -> Present a -{-# INLINE lookupRecordCollision #-} - --- Why do we produce an Int# instead of an Int? Unfortunately, GHC is not --- yet any good at unboxing things *inside* products, let alone sums. That --- may be changing in GHC 8.6 or so (there is some work in progress), but --- for now we use Int# explicitly here. We don't need to push the Int# --- into lookupCont because inlining takes care of that. -lookupRecordCollision# :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> (# (# #) | v #) -lookupRecordCollision# h s k m = +{-# INLINE lookupWithRes #-} + +lookupWithRes# :: (Eq k, Hashable k) => Hash -> Salt -> k -> HashMap k v -> (# (# #) | v #) +lookupWithRes# h s k m = lookupCont (\_ -> (# (# #) | #)) (\v -> (# | v #)) h s k m -- INLINABLE to specialize to the Eq instance. -{-# INLINABLE lookupRecordCollision# #-} +{-# INLINABLE lookupWithRes# #-} #else /* GHC < 8.2 so there are no unboxed sums */ -lookupRecordCollision h s k m = lookupCont (\_ -> Absent) Present h s k m -{-# INLINABLE lookupRecordCollision #-} +lookupWithRes h s k m = lookupCont (\_ -> Absent) Present h s k m +{-# INLINABLE lookupWithRes #-} #endif --- A two-continuation version of lookupRecordCollision. This lets us --- share source code between lookup and lookupRecordCollision without +-- A two-continuation version of lookupWithRes. This lets us +-- share source code between lookup and lookupWithRes without -- risking any performance degradation. -- -- The absent continuation has type @((# #) -> r)@ instead of just @r@ @@ -687,7 +686,7 @@ insert' :: (Eq k, Hashable k) => Hash -> Salt -> k -> v -> HashMap k v -> HashMa insert' h0 s0 k0 v0 m0 = go h0 s0 k0 v0 0 m0 where go :: (Eq k, Hashable k) => Hash -> Salt -> k -> v -> Int -> HashMap k v -> HashMap k v - go !h _ !k x !_ Empty = Leaf h (L k x) + go !h !_ !k x !_ Empty = Leaf h (L k x) go h s k x bs t@(Leaf hy l@(L ky y)) | hy == h = if ky == k then if x `ptrEq` y @@ -784,15 +783,10 @@ insertKeyExists !h0 !s0 !k0 x0 !m0 = go h0 s0 k0 x0 0 m0 !st' = go h s k x (bs+bitsPerSubkey) st in Full (update16 ary i st') where i = index h bs - go h s k x bs t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) - | h == hx = - let go' - | k == k1 = if x `ptrEq` v1 then t else Collision hx (L k x) l2 hmx - | k == k2 = if x `ptrEq` v2 then t else Collision hx l1 (L k x) hmx - | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx - in go' - | otherwise = - go h s k x bs $ BitmapIndexed (mask hx bs) (A.singleton t) + go _ s k x _ t@(Collision hx l1@(L k1 v1) l2@(L k2 v2) hmx) + | k == k1 = if x `ptrEq` v1 then t else Collision hx (L k x) l2 hmx + | k == k2 = if x `ptrEq` v2 then t else Collision hx l1 (L k x) hmx + | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k x 0 hmx go _ _ _ _ _ Empty = Empty -- error "Internal error: go Empty" {-# NOINLINE insertKeyExists #-} @@ -1303,7 +1297,7 @@ alterFEager f !k m = (<$> f mv) $ \fres -> where !h = hashWithSalt s k !s = defaultSalt - !lookupRes = lookupRecordCollision h s k m + !lookupRes = lookupWithRes h s k m !mv = case lookupRes of Absent -> Nothing Present v -> Just v diff --git a/Data/HashMap/Strict/Base.hs b/Data/HashMap/Strict/Base.hs index 55efc28c..f22d7ccc 100644 --- a/Data/HashMap/Strict/Base.hs +++ b/Data/HashMap/Strict/Base.hs @@ -397,7 +397,7 @@ alterFEager f !k !m = (<$> f mv) $ \fres -> where !h = hashWithSalt s k !s = defaultSalt - !lookupRes = lookupRecordCollision h s k m + !lookupRes = lookupWithRes h s k m !mv = case lookupRes of Absent -> Nothing Present v -> Just v From 87de07591b93dee87f7ce61593abdaf7dbe039fe Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Thu, 18 Oct 2018 11:43:17 +0200 Subject: [PATCH 38/69] Fixed most comments --- Data/HashMap/Base.hs | 27 ++++++++++++--------------- Data/HashSet/Base.hs | 2 +- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 4a434cf9..40dec09a 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -277,17 +277,17 @@ instance Traversable (HashMap k) where instance Eq2 HashMap where liftEq2 = equal2 -instance Eq k => Eq1 (HashMap k) where +instance (Eq k, Hashable k) => Eq1 (HashMap k) where liftEq = equal1 #endif -instance (Eq k, Eq v) => Eq (HashMap k v) where +instance (Eq k, Hashable k, Eq v) => Eq (HashMap k v) where (==) = equal1 (==) -- We rely on there being no Empty constructors in the tree! -- This ensures that two equal HashMaps will have the same -- shape, modulo the order of entries in Collisions. -equal1 :: Eq k +equal1 :: (Eq k, Hashable k) => (v -> v' -> Bool) -> HashMap k v -> HashMap k v' -> Bool equal1 eq = go @@ -297,11 +297,9 @@ equal1 eq = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go (Collision h1 (L k11 v11) (L k12 v12) hm1) (Collision h2 (L k21 v21) (L k22 v22) hm2) - = h1 == h2 - && isPermutationBy (\(k1, v1) (k2, v2) -> k1 == k2 && eq v1 v2) - ((k11, v11) : (k12, v12) : toList hm1) - ((k21, v21) : (k22, v22) : toList hm2) + go t1@(Collision h1 _ _ _) t2@(Collision h2 _ _ _) = h1 == h2 && + size t1 == size t2 + && foldrWithKey (\k v r -> maybe False (eq v) (lookup k t2) && r) True t1 go _ _ = False leafEq (L k1 v1) (L k2 v2) = k1 == k2 && eq v1 v2 @@ -334,7 +332,7 @@ equal2 eqk eqv t1 t2 = go (toList' t1 []) (toList' t2 []) instance Ord2 HashMap where liftCompare2 = cmp -instance Ord k => Ord1 (HashMap k) where +instance (Ord k, Hashable k) => Ord1 (HashMap k) where liftCompare = cmp compare #endif @@ -343,7 +341,7 @@ instance Ord k => Ord1 (HashMap k) where -- /Note:/ Because the hash is not guaranteed to be stable across library -- versions, OSes, or architectures, neither is an actual order of elements in -- 'HashMap' or an result of `compare`.is stable. -instance (Ord k, Ord v) => Ord (HashMap k v) where +instance (Ord k, Hashable k, Ord v) => Ord (HashMap k v) where compare = cmp compare compare cmp :: (k -> k' -> Ordering) -> (v -> v' -> Ordering) @@ -467,9 +465,9 @@ toList' Empty a = a -- Helper function to detect 'Leaf's and 'Collision's. isLeafOrCollision :: HashMap k v -> Bool -isLeafOrCollision (Leaf _ _) = True -isLeafOrCollision (Collision _ _ _ _) = True -isLeafOrCollision _ = False +isLeafOrCollision (Leaf {}) = True +isLeafOrCollision (Collision {}) = True +isLeafOrCollision _ = False ------------------------------------------------------------------------ -- * Construction @@ -498,7 +496,7 @@ size t = go t 0 go (Leaf _ _) n = n + 1 go (BitmapIndexed _ ary) n = A.foldl' (flip go) n ary go (Full ary) n = A.foldl' (flip go) n ary - go (Collision _ _ _ hm) n = go hm n + 2 + go (Collision _ _ _ hm) n = go hm (n + 2) -- | /O(log n)/ Return 'True' if the specified key is present in the -- map, 'False' otherwise. @@ -1798,7 +1796,6 @@ updateOrConcatWithKey f ary1 ary2 = A.run $ do -- Remove (any) one element from an array of hashmaps unConsA :: A.Array (HashMap k v) -> UnCons k v unConsA ary = case A.length ary of - 0 -> WasEmpty 1 -> case A.index ary 0 of Empty -> WasEmpty -- That would be weird, but fine. Leaf _ l -> NowEmpty l diff --git a/Data/HashSet/Base.hs b/Data/HashSet/Base.hs index 09294203..bbc37744 100644 --- a/Data/HashSet/Base.hs +++ b/Data/HashSet/Base.hs @@ -128,7 +128,7 @@ instance Eq1 HashSet where liftEq eq (HashSet a) (HashSet b) = equalKeys1 eq a b #endif -instance (Ord a) => Ord (HashSet a) where +instance (Ord a, Hashable a) => Ord (HashSet a) where compare (HashSet a) (HashSet b) = compare a b {-# INLINE compare #-} From 4360a4bc0215f2b5f3a24e38b9f7c98932f4a97e Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Thu, 18 Oct 2018 15:24:31 +0200 Subject: [PATCH 39/69] Sped up equalKeys for collisions --- Data/HashMap/Base.hs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 40dec09a..9940949d 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -297,8 +297,9 @@ equal1 eq = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go t1@(Collision h1 _ _ _) t2@(Collision h2 _ _ _) = h1 == h2 && - size t1 == size t2 + go t1@(Collision h1 _ _ _) t2@(Collision h2 _ _ _) = + h1 == h2 + && size t1 == size t2 && foldrWithKey (\k v r -> maybe False (eq v) (lookup k t2) && r) True t1 go _ _ = False @@ -397,12 +398,10 @@ equalKeys = go = bm1 == bm2 && A.sameArray1 go ary1 ary2 go (Leaf h1 l1) (Leaf h2 l2) = h1 == h2 && leafEq l1 l2 go (Full ary1) (Full ary2) = A.sameArray1 go ary1 ary2 - go (Collision h1 (L k11 _) (L k12 _) hm1) (Collision h2 (L k21 _) (L k22 _) hm2) - = h1 == h2 && - (length hm1 == length hm2) && - isPermutationBy (==) - (k11 : k12 : keys hm1) - (k21 : k22 : keys hm2) + go t1@(Collision h1 _ _ _) t2@(Collision h2 _ _ _) + = h1 == h2 + && size t1 == size t2 + && L.foldr (\k r -> (k `elem` (keys t2)) && r) True (keys t1) go _ _ = False leafEq (L k1 _) (L k2 _) = k1 == k2 From 9403284d7cba45d6415dbdd7b7cd25f4188ca42e Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 19 Oct 2018 10:16:50 +0200 Subject: [PATCH 40/69] First draft of validity-based testing --- Data/HashMap/Array.hs | 4 + Data/HashMap/Base.hs | 62 +++++++- tests/Validity.hs | 302 +++++++++++++++++++++++++++++++++++++ unordered-containers.cabal | 49 +++++- 4 files changed, 412 insertions(+), 5 deletions(-) create mode 100644 tests/Validity.hs diff --git a/Data/HashMap/Array.hs b/Data/HashMap/Array.hs index 0149da4d..7b8fab78 100644 --- a/Data/HashMap/Array.hs +++ b/Data/HashMap/Array.hs @@ -85,6 +85,7 @@ import GHC.Exts (Array#, newArray#, readArray#, writeArray#, import qualified Prelude #endif +import Data.Validity import Data.HashMap.Unsafe (runST) import Control.Monad ((>=>)) @@ -178,6 +179,9 @@ data Array a = Array { unArray :: !(Array# a) } +instance Validity a => Validity (Array a) where + validate a = annotate (toList a) "The array elements" + instance Show a => Show (Array a) where show = show . toList diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 9940949d..bcdd47dc 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -128,6 +128,7 @@ import Data.Bits ((.&.), (.|.), complement, popCount) import Data.Data hiding (Typeable) import qualified Data.Foldable as Foldable import qualified Data.List as L +import Data.Validity import GHC.Exts ((==#), build, reallyUnsafePtrEquality#) import Prelude hiding (filter, foldr, lookup, map, null, pred) import Text.Read hiding (step) @@ -169,7 +170,13 @@ hashWithSalt :: H.Hashable a => Salt -> a -> Hash hashWithSalt s v = fromIntegral $ H.hashWithSalt s v data Leaf k v = L !k v - deriving (Eq) + deriving (Show, Eq) + +instance (Validity k, Validity v) => Validity (Leaf k v) where + validate (L k v) = mconcat + [ annotate k "key" + , annotate v "value" + ] instance (NFData k, NFData v) => NFData (Leaf k v) where rnf (L k v) = rnf k `seq` rnf v @@ -189,6 +196,59 @@ data HashMap k v type role HashMap nominal representational +-- TODO +-- * The keys do hash to the hash they should +-- * In Collision +-- * In Collision's recursive hashmap +-- * Validate the array structure too +instance (Hashable k, Validity k, Validity v) => Validity (HashMap k v) where + validate = go defaultSalt + where + go s hm = case hm of + Empty -> mempty + (Leaf h l@(L k _)) -> mconcat + [ annotate h "Hash" + , annotate l "Leaf" + , check (hashWithSalt s k == h) "The hash is correct." + ] + (BitmapIndexed bm a) -> mconcat + [ annotate bm "Bitmap" + , decorate "Array" $ decorateList (A.toList a) $ go s + , check (A.length a == popCount bm) + "Within 'BitmapIndexed' are values in the array equal to popCount bm." + ] + (Full a) -> mconcat + [ decorate "Array" $ decorateList (A.toList a) $ go s + , check (A.length a == 2 ^ bitsPerSubkey) + "Within 'Full' are 2 ^ bitsPerSubkey values in the array." + ] + (Collision h l1 l2 hm') -> mconcat + [ annotate h "Hash" + , annotate l1 "The first collision" + , annotate l2 "The second collision" + , decorate "The recursive HashMap" $ go (nextSalt s) hm' + ] + +#if !MIN_VERSION_validity(0,6,0) +-- | Decorate a validation with a location +decorate :: String -> Validation -> Validation +decorate = flip annotateValidation + +annotateValidation :: Validation -> String -> Validation +annotateValidation val s = + case val of + Validation errs -> Validation $ L.map (Location s) errs +#endif + +#if !MIN_VERSION_validity(0,8,0) +-- | Decorate a piecewise validation of a list with their location in the list +decorateList :: [a] -> (a -> Validation) -> Validation +decorateList as func = mconcat $ + flip L.map (zip [0..] as) $ \(i, a) -> + decorate (unwords ["The element at index", show (i :: Integer), "in the list"]) $ + func a +#endif + instance (NFData k, NFData v) => NFData (HashMap k v) where rnf Empty = () rnf (BitmapIndexed _ ary) = rnf ary diff --git a/tests/Validity.hs b/tests/Validity.hs new file mode 100644 index 00000000..d01d7767 --- /dev/null +++ b/tests/Validity.hs @@ -0,0 +1,302 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Main (main) where + +import GHC.Generics (Generic) + +import Data.Bits (popCount) +import Control.Monad (replicateM) +import Data.Hashable (Hashable(hashWithSalt)) +#if defined(STRICT) +import qualified Data.HashMap.Strict as HM +#else +import qualified Data.HashMap.Lazy as HM +import qualified Data.HashMap.Base as HM +#endif +import Data.HashMap.Base (HashMap(..), Leaf(..)) +import qualified Data.HashMap.Base as HM (defaultSalt, hashWithSalt, nextSalt) +import qualified Data.HashMap.Array as A + +import Data.Validity (Validity) +import Data.GenValidity (GenUnchecked(..), GenValid(..), genSplit, genSplit4) +import Test.Validity (producesValidsOnValids, producesValidsOnValids2, producesValidsOnValids3, genGeneratesValid) +import Test.QuickCheck (Arbitrary, CoArbitrary, Function, Property, Gen, sized, oneof, resize, scale) +import Test.Framework(Test, defaultMain, testGroup) +import Test.Framework.Providers.QuickCheck2 (testProperty) +import Test.QuickCheck.Function (Fun, apply, applyFun2, applyFun3) + +instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (Leaf k v) where + genUnchecked = sized $ \n -> do + (a, b) <- genSplit n + k <- resize a genUnchecked + v <- resize b genUnchecked + pure $ L k v + shrinkUnchecked (L k v) = [L k' v' | (k', v') <- shrinkUnchecked (k, v)] + +instance (GenValid k, GenValid v) => GenValid (Leaf k v) where + genValid = sized $ \n -> do + (a, b) <- genSplit n + k <- resize a genValid + v <- resize b genValid + pure $ L k v + +instance GenUnchecked a => GenUnchecked (A.Array a) where + genUnchecked = do + l <- genUnchecked + pure $ A.fromList (length l) l + shrinkUnchecked _ = [] -- TODO: whrite shrinking + +instance GenValid a => GenValid (A.Array a) where + genValid = do + l <- genValid + pure $ A.fromList (length l) l + +instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (HashMap k v) where + genUnchecked = sized $ \n -> case n of + 0 -> pure Empty + _ -> oneof + [ do + (a, b) <- genSplit n + BitmapIndexed + <$> resize a genUnchecked + <*> resize b genUnchecked + , do + (a, b) <- genSplit n + Leaf + <$> resize a genUnchecked + <*> resize b genUnchecked + , Full <$> genUnchecked + , do + (a,b,c,d) <- genSplit4 n + Collision + <$> resize a genUnchecked + <*> resize b genUnchecked + <*> resize c genUnchecked + <*> resize d genUnchecked + ] + shrinkUnchecked _ = [] -- TODO: write shrinking + +instance (Hashable k, GenValid k, GenValid v) => GenValid (HashMap k v) where + genValid = go HM.defaultSalt + where + go s = sized $ \n -> case n of + 0 -> pure Empty + _ -> oneof + [ do + (a, b) <- genSplit n + k <- resize a genValid + v <- resize b genValid + pure $ Leaf (HM.hashWithSalt s k) (L k v) + , do + bm <- genValid + let pc = popCount bm + values <- replicateM pc $ scale (`div` pc) $ go s -- TODO Redistribute size better. + pure $ BitmapIndexed bm $ A.fromList pc values + , do + let l = 2 ^ (4 :: Int) -- 4 == bitsPerSubkey + values <- replicateM l $ scale (`div` l) $ go s -- TODO Redistribute size better. + pure $ Full $ A.fromList l values + , do + (a,b,c,d) <- genSplit4 n + hm' <- resize d $ go (HM.nextSalt s) + Collision + <$> resize a genValid + <*> resize b genValid + <*> resize c genValid + <*> pure hm' + ] + +-- Key type that generates more hash collisions. +newtype Key = K + { unK :: Int } + deriving (Arbitrary, CoArbitrary, Validity, GenUnchecked, GenValid, Eq, Ord, Read, Show, Generic) + +instance Hashable Key where + hashWithSalt salt k = hashWithSalt salt (unK k) `mod` 20 + +instance Function Key + +pSingleton :: Property +pSingleton = producesValidsOnValids2 (HM.singleton :: Key -> Int -> HM.HashMap Key Int) + +pNull :: Property +pNull = producesValidsOnValids (HM.null :: HM.HashMap Key Int -> Bool) + +pSize :: Property +pSize = producesValidsOnValids (HM.size :: HM.HashMap Key Int -> Int) + +pMember :: Property +pMember = producesValidsOnValids2 (HM.member :: Key -> HM.HashMap Key Int -> Bool) + +pLookup :: Property +pLookup = producesValidsOnValids2 (HM.lookup :: Key -> HM.HashMap Key Int -> Maybe Int) + +pLookupDefault :: Property +pLookupDefault = producesValidsOnValids3 (HM.lookupDefault :: Int -> Key -> HM.HashMap Key Int -> Int) + +pInsert :: Property +pInsert = producesValidsOnValids3 (HM.insert :: Key -> Int -> HM.HashMap Key Int -> HM.HashMap Key Int) + +pInsertWith :: Property +pInsertWith = producesValidsOnValids3 (HM.insertWith (+) :: Key -> Int -> HM.HashMap Key Int -> HM.HashMap Key Int) + +pDelete :: Property +pDelete = producesValidsOnValids2 (HM.delete :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) + +pAdjust :: Fun Int Int -> Property +pAdjust f = producesValidsOnValids2 (HM.adjust (apply f) :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) + +pUpdate :: (Fun Int (Maybe Int)) -> Property +pUpdate f = producesValidsOnValids2 (HM.update (apply f) :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) + +pAlter :: Property +pAlter = producesValidsOnValids2 (HM.alter (fmap succ) :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) + +pAlterF :: Fun (Maybe Int) [Maybe Int] -> Property +pAlterF f = producesValidsOnValids2 (HM.alterF (apply f) :: Key -> HM.HashMap Key Int -> [HM.HashMap Key Int]) + +pUnion :: Property +pUnion = producesValidsOnValids2 (HM.union :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) + +pUnionWith :: Fun (Int, Int) Int -> Property +pUnionWith f = producesValidsOnValids2 (HM.unionWith (applyFun2 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) + +pUnionWithKey :: Fun (Key, Int, Int) Int -> Property +pUnionWithKey f = producesValidsOnValids2 (HM.unionWithKey (applyFun3 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) + +pUnions :: Property +pUnions = producesValidsOnValids (HM.unions :: [HashMap Key Int] -> HashMap Key Int) + +pMap :: Fun Int Int -> Property +pMap f = producesValidsOnValids (HM.map (apply f) :: HashMap Key Int -> HashMap Key Int) + +pMapWithKey :: Fun (Key, Int) Int -> Property +pMapWithKey f = producesValidsOnValids (HM.mapWithKey (applyFun2 f) :: HashMap Key Int -> HashMap Key Int) + +pTraverseWithKey :: Fun (Key, Int) (Maybe Int) -> Property +pTraverseWithKey f = producesValidsOnValids (HM.traverseWithKey (applyFun2 f) :: HashMap Key Int -> Maybe (HashMap Key Int)) + +pDifference :: Property +pDifference = producesValidsOnValids2 (HM.difference :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) + +pDifferenceWith :: Fun (Int, Int) (Maybe Int) -> Property +pDifferenceWith f = producesValidsOnValids2 (HM.differenceWith (applyFun2 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) + +pIntersection :: Property +pIntersection = producesValidsOnValids2 (HM.intersection :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) + +pIntersectionWith :: Fun (Int, Int) Int -> Property +pIntersectionWith f = producesValidsOnValids2 (HM.intersectionWith (applyFun2 f) :: HashMap Key Int -> HashMap \ + Key Int -> HashMap Key Int) + +pIntersectionWithKey :: Fun (Key, Int, Int) Int -> Property +pIntersectionWithKey f = producesValidsOnValids2 (HM.intersectionWithKey (applyFun3 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) + +pFoldl' :: Fun (Word, Int) Word -> Property +pFoldl' f = producesValidsOnValids2 (HM.foldl' (applyFun2 f) :: Word -> HashMap Key Int -> Word) + +pFoldlWithKey' :: Fun (Word, Key, Int) Word -> Property +pFoldlWithKey' f = producesValidsOnValids2 (HM.foldlWithKey' (applyFun3 f) :: Word -> HashMap Key Int -> Word) + +pFoldr :: Fun (Int, Word) Word -> Property +pFoldr f = producesValidsOnValids2 (HM.foldr (applyFun2 f) :: Word -> HashMap Key Int -> Word) + +pFoldrWithKey :: Fun (Key, Int, Word) Word -> Property +pFoldrWithKey f = producesValidsOnValids2 (HM.foldrWithKey (applyFun3 f) :: Word -> HashMap Key Int -> Word) + +pFilter :: Fun Int Bool -> Property +pFilter f = producesValidsOnValids (HM.filter (apply f) :: HashMap Key Int -> HashMap Key Int) + +pFilterWithKey :: Fun (Key, Int) Bool -> Property +pFilterWithKey f = producesValidsOnValids (HM.filterWithKey (applyFun2 f) :: HashMap Key Int -> HashMap Key Int) + +pMapMaybe :: Fun Int (Maybe Int) -> Property +pMapMaybe f = producesValidsOnValids (HM.mapMaybe (apply f) :: HashMap Key Int -> HashMap Key Int) + +pMapMaybeWithKey :: Fun (Key, Int) (Maybe Int) -> Property +pMapMaybeWithKey f = producesValidsOnValids (HM.mapMaybeWithKey (applyFun2 f) :: HashMap Key Int -> HashMap Key Int) + +pKeys :: Property +pKeys = producesValidsOnValids (HM.keys :: HashMap Key Int -> [Key]) + +pElems :: Property +pElems = producesValidsOnValids (HM.elems :: HashMap Key Int -> [Int]) + +pToList :: Property +pToList = producesValidsOnValids (HM.toList :: HashMap Key Int -> [(Key, Int)]) + +pFromList :: Property +pFromList = producesValidsOnValids (HM.fromList :: [(Key, Int)] -> HashMap Key Int) + +pFromListWith :: Fun (Int, Int) Int -> Property +pFromListWith f = producesValidsOnValids (HM.fromListWith (applyFun2 f) :: [(Key, Int)] -> HashMap Key Int) + + +------------------------------------------------------------------------ +-- * Test list + +tests :: [Test] +tests = + [ + -- Basic interface + testGroup "HashMap" + [ testProperty "genValid generates valid values for Leaf" $ + genGeneratesValid (genValid :: Gen (Leaf Key Int)) shrinkValid + , testProperty "genValid generates valid values for Array" $ + genGeneratesValid (genValid :: Gen (A.Array Key)) shrinkValid + , testProperty "genValid generates valid values for HashMap" $ + genGeneratesValid (genValid :: Gen (HashMap Key Int)) shrinkValid + ] + , testGroup "HashMap" + [ testProperty "singleton produces valid HashMaps" pSingleton + , testProperty "null produces valid Bools" pNull + , testProperty "size produces valid HashMaps" pSize + , testProperty "member produces valid HashMaps" pMember + , testProperty "lookup produces valid HashMaps" pLookup + , testProperty "lookupDefault produces valid HashMaps" pLookupDefault + , testProperty "insert produces valid HashMaps" pInsert + , testProperty "insertWith produces valid HashMaps" pInsertWith + , testProperty "delete produces valid HashMaps" pDelete + , testProperty "adjust produces valid HashMaps" pAdjust + , testProperty "update produces valid HashMaps" pUpdate + , testProperty "alter produces valid HashMaps" pAlter + , testProperty "alterF produces valid HashMaps" pAlterF + , testProperty "union produces valid HashMaps" pUnion + , testProperty "unionWith produces valid HashMaps" pUnionWith + , testProperty "unionWithKey produces valid HashMaps" pUnionWithKey + , testProperty "unions produces valid HashMaps" pUnions + , testProperty "map produces valid HashMaps" pMap + , testProperty "mapWithKey produces valid HashMaps" pMapWithKey + , testProperty "traverseWithKey produces valid HashMaps" pTraverseWithKey + , testProperty "difference produces valid HashMaps" pDifference + , testProperty "differenceWith produces valid HashMaps" pDifferenceWith + , testProperty "intersection produces valid HashMaps" pIntersection + , testProperty "intersectionWith produces valid HashMaps" pIntersectionWith + , testProperty "intersectionWithKey produces valid HashMaps" pIntersectionWithKey + , testProperty "foldl' produces valid HashMaps" pFoldl' + , testProperty "foldlWithKey' produces valid HashMaps" pFoldlWithKey' + , testProperty "foldr produces valid HashMaps" pFoldr + , testProperty "foldrWithKey produces valid HashMaps" pFoldrWithKey + , testProperty "filter produces valid HashMaps" pFilter + , testProperty "filterWithKey produces valid HashMaps" pFilterWithKey + , testProperty "mapMaybe produces valid HashMaps" pMapMaybe + , testProperty "mapMaybeWithKey produces valid HashMaps" pMapMaybeWithKey + , testProperty "keys produces valid lists" pKeys + , testProperty "elems produces valid lists" pElems + , testProperty "toList produces valid lists" pToList + , testProperty "fromList produces valid HashMaps" pFromList + , testProperty "fromListWith produces valid HashMaps" pFromListWith + ] + ] +-- TODO keysSet + +------------------------------------------------------------------------ +-- * Test harness + +main :: IO () +main = defaultMain tests diff --git a/unordered-containers.cabal b/unordered-containers.cabal index 95f84581..7c36f1b6 100644 --- a/unordered-containers.cabal +++ b/unordered-containers.cabal @@ -28,13 +28,13 @@ flag debug library exposed-modules: + Data.HashMap.Array + Data.HashMap.Base Data.HashMap.Lazy Data.HashMap.Strict + Data.HashMap.Strict.Base Data.HashSet other-modules: - Data.HashMap.Array - Data.HashMap.Base - Data.HashMap.Strict.Base Data.HashMap.List Data.HashMap.Unsafe Data.HashMap.UnsafeShift @@ -43,7 +43,8 @@ library build-depends: base >= 4.7 && < 5, deepseq >= 1.1, - hashable >= 1.0.1.1 && < 1.3 + hashable >= 1.0.1.1 && < 1.3, + validity >= 0.4 default-language: Haskell2010 @@ -64,6 +65,46 @@ library if flag(debug) cpp-options: -DASSERTS +test-suite validity-lazy + hs-source-dirs: tests + main-is: Validity.hs + type: exitcode-stdio-1.0 + + build-depends: + base, + hashable >= 1.0.1.1, + QuickCheck >= 2.4.0.1, + test-framework >= 0.3.3, + test-framework-quickcheck2 >= 0.2.9, + validity, + genvalidity, + genvalidity-hspec, + unordered-containers + + default-language: Haskell2010 + ghc-options: -Wall + cpp-options: -DASSERTS + +test-suite validity-strict + hs-source-dirs: tests + main-is: Validity.hs + type: exitcode-stdio-1.0 + + build-depends: + base, + hashable >= 1.0.1.1, + QuickCheck >= 2.4.0.1, + test-framework >= 0.3.3, + test-framework-quickcheck2 >= 0.2.9, + validity, + genvalidity, + genvalidity-hspec, + unordered-containers + + default-language: Haskell2010 + ghc-options: -Wall + cpp-options: -DASSERTS -DSTRICT + test-suite hashmap-lazy-properties hs-source-dirs: tests main-is: HashMapProperties.hs From 6d16ceb47ef5ebc505128d4b7e1fd650806de140 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 19 Oct 2018 10:21:09 +0200 Subject: [PATCH 41/69] Moved the validity dependency out of the library --- Data/HashMap/Array.hs | 4 --- Data/HashMap/Base.hs | 60 -------------------------------------- tests/Validity.hs | 60 +++++++++++++++++++++++++++++++++++++- unordered-containers.cabal | 3 +- 4 files changed, 60 insertions(+), 67 deletions(-) diff --git a/Data/HashMap/Array.hs b/Data/HashMap/Array.hs index 7b8fab78..0149da4d 100644 --- a/Data/HashMap/Array.hs +++ b/Data/HashMap/Array.hs @@ -85,7 +85,6 @@ import GHC.Exts (Array#, newArray#, readArray#, writeArray#, import qualified Prelude #endif -import Data.Validity import Data.HashMap.Unsafe (runST) import Control.Monad ((>=>)) @@ -179,9 +178,6 @@ data Array a = Array { unArray :: !(Array# a) } -instance Validity a => Validity (Array a) where - validate a = annotate (toList a) "The array elements" - instance Show a => Show (Array a) where show = show . toList diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index bcdd47dc..d4142097 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -128,7 +128,6 @@ import Data.Bits ((.&.), (.|.), complement, popCount) import Data.Data hiding (Typeable) import qualified Data.Foldable as Foldable import qualified Data.List as L -import Data.Validity import GHC.Exts ((==#), build, reallyUnsafePtrEquality#) import Prelude hiding (filter, foldr, lookup, map, null, pred) import Text.Read hiding (step) @@ -172,12 +171,6 @@ hashWithSalt s v = fromIntegral $ H.hashWithSalt s v data Leaf k v = L !k v deriving (Show, Eq) -instance (Validity k, Validity v) => Validity (Leaf k v) where - validate (L k v) = mconcat - [ annotate k "key" - , annotate v "value" - ] - instance (NFData k, NFData v) => NFData (Leaf k v) where rnf (L k v) = rnf k `seq` rnf v @@ -196,59 +189,6 @@ data HashMap k v type role HashMap nominal representational --- TODO --- * The keys do hash to the hash they should --- * In Collision --- * In Collision's recursive hashmap --- * Validate the array structure too -instance (Hashable k, Validity k, Validity v) => Validity (HashMap k v) where - validate = go defaultSalt - where - go s hm = case hm of - Empty -> mempty - (Leaf h l@(L k _)) -> mconcat - [ annotate h "Hash" - , annotate l "Leaf" - , check (hashWithSalt s k == h) "The hash is correct." - ] - (BitmapIndexed bm a) -> mconcat - [ annotate bm "Bitmap" - , decorate "Array" $ decorateList (A.toList a) $ go s - , check (A.length a == popCount bm) - "Within 'BitmapIndexed' are values in the array equal to popCount bm." - ] - (Full a) -> mconcat - [ decorate "Array" $ decorateList (A.toList a) $ go s - , check (A.length a == 2 ^ bitsPerSubkey) - "Within 'Full' are 2 ^ bitsPerSubkey values in the array." - ] - (Collision h l1 l2 hm') -> mconcat - [ annotate h "Hash" - , annotate l1 "The first collision" - , annotate l2 "The second collision" - , decorate "The recursive HashMap" $ go (nextSalt s) hm' - ] - -#if !MIN_VERSION_validity(0,6,0) --- | Decorate a validation with a location -decorate :: String -> Validation -> Validation -decorate = flip annotateValidation - -annotateValidation :: Validation -> String -> Validation -annotateValidation val s = - case val of - Validation errs -> Validation $ L.map (Location s) errs -#endif - -#if !MIN_VERSION_validity(0,8,0) --- | Decorate a piecewise validation of a list with their location in the list -decorateList :: [a] -> (a -> Validation) -> Validation -decorateList as func = mconcat $ - flip L.map (zip [0..] as) $ \(i, a) -> - decorate (unwords ["The element at index", show (i :: Integer), "in the list"]) $ - func a -#endif - instance (NFData k, NFData v) => NFData (HashMap k v) where rnf Empty = () rnf (BitmapIndexed _ ary) = rnf ary diff --git a/tests/Validity.hs b/tests/Validity.hs index d01d7767..9c1c8b85 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -9,6 +9,7 @@ module Main (main) where import GHC.Generics (Generic) +import Data.Validity import Data.Bits (popCount) import Control.Monad (replicateM) import Data.Hashable (Hashable(hashWithSalt)) @@ -19,7 +20,7 @@ import qualified Data.HashMap.Lazy as HM import qualified Data.HashMap.Base as HM #endif import Data.HashMap.Base (HashMap(..), Leaf(..)) -import qualified Data.HashMap.Base as HM (defaultSalt, hashWithSalt, nextSalt) +import qualified Data.HashMap.Base as HM (defaultSalt, hashWithSalt, nextSalt, bitsPerSubkey) import qualified Data.HashMap.Array as A import Data.Validity (Validity) @@ -30,6 +31,63 @@ import Test.Framework(Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.QuickCheck.Function (Fun, apply, applyFun2, applyFun3) +instance (Validity k, Validity v) => Validity (Leaf k v) where + validate (L k v) = mconcat + [ annotate k "key" + , annotate v "value" + ] + +instance Validity a => Validity (A.Array a) where + validate a = annotate (A.toList a) "The array elements" + +instance (Hashable k, Validity k, Validity v) => Validity (HashMap k v) where + validate = go HM.defaultSalt + where + go s hm = case hm of + Empty -> mempty + (Leaf h l@(L k _)) -> mconcat + [ annotate h "Hash" + , annotate l "Leaf" + , check (HM.hashWithSalt s k == h) "The hash is correct." + ] + (BitmapIndexed bm a) -> mconcat + [ annotate bm "Bitmap" + , decorate "Array" $ decorateList (A.toList a) $ go s + , check (A.length a == popCount bm) + "Within 'BitmapIndexed' are values in the array equal to popCount bm." + ] + (Full a) -> mconcat + [ decorate "Array" $ decorateList (A.toList a) $ go s + , check (A.length a == 2 ^ HM.bitsPerSubkey) + "Within 'Full' are 2 ^ bitsPerSubkey values in the array." + ] + (Collision h l1 l2 hm') -> mconcat + [ annotate h "Hash" + , annotate l1 "The first collision" + , annotate l2 "The second collision" + , decorate "The recursive HashMap" $ go (HM.nextSalt s) hm' + ] + +#if !MIN_VERSION_validity(0,6,0) +-- | Decorate a validation with a location +decorate :: String -> Validation -> Validation +decorate = flip annotateValidation + +annotateValidation :: Validation -> String -> Validation +annotateValidation val s = + case val of + Validation errs -> Validation $ map (Location s) errs +#endif + +#if !MIN_VERSION_validity(0,8,0) +-- | Decorate a piecewise validation of a list with their location in the list +decorateList :: [a] -> (a -> Validation) -> Validation +decorateList as func = mconcat $ + flip map (zip [0..] as) $ \(i, a) -> + decorate (unwords ["The element at index", show (i :: Integer), "in the list"]) $ + func a +#endif + instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (Leaf k v) where genUnchecked = sized $ \n -> do (a, b) <- genSplit n diff --git a/unordered-containers.cabal b/unordered-containers.cabal index 7c36f1b6..b5d8eae8 100644 --- a/unordered-containers.cabal +++ b/unordered-containers.cabal @@ -43,8 +43,7 @@ library build-depends: base >= 4.7 && < 5, deepseq >= 1.1, - hashable >= 1.0.1.1 && < 1.3, - validity >= 0.4 + hashable >= 1.0.1.1 && < 1.3 default-language: Haskell2010 From 4e9d582829ca634b3c3694b9f201f11bbde6c762 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 19 Oct 2018 12:44:50 +0200 Subject: [PATCH 42/69] More accurate validity --- Data/HashMap/Base.hs | 8 ++-- newer-stack.yaml | 3 ++ tests/Validity.hs | 108 ++++++++++++++++++++++++++++--------------- 3 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 newer-stack.yaml diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index d4142097..100ef07b 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -185,7 +185,7 @@ data HashMap k v | Leaf !Hash !(Leaf k v) | Full !(A.Array (HashMap k v)) | Collision !Hash !(Leaf k v) !(Leaf k v) !(HashMap k v) - deriving (Typeable) + deriving (Typeable, Show) type role HashMap nominal representational @@ -265,9 +265,9 @@ instance (Eq k, Hashable k, Read k, Read e) => Read (HashMap k e) where readListPrec = readListPrecDefault -instance (Show k, Show v) => Show (HashMap k v) where - showsPrec d m = showParen (d > 10) $ - showString "fromList " . shows (toList m) +-- instance (Show k, Show v) => Show (HashMap k v) where +-- showsPrec d m = showParen (d > 10) $ +-- showString "fromList " . shows (toList m) instance Traversable (HashMap k) where traverse f = traverseWithKey (const f) diff --git a/newer-stack.yaml b/newer-stack.yaml new file mode 100644 index 00000000..26f10a32 --- /dev/null +++ b/newer-stack.yaml @@ -0,0 +1,3 @@ +resolver: lts-12.12 +packages: +- . diff --git a/tests/Validity.hs b/tests/Validity.hs index 9c1c8b85..5ebd100e 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -3,6 +3,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Main (main) where @@ -11,13 +12,13 @@ import GHC.Generics (Generic) import Data.Validity import Data.Bits (popCount) -import Control.Monad (replicateM) +import Data.List (nub) +import Control.Monad (foldM) import Data.Hashable (Hashable(hashWithSalt)) #if defined(STRICT) import qualified Data.HashMap.Strict as HM #else import qualified Data.HashMap.Lazy as HM -import qualified Data.HashMap.Base as HM #endif import Data.HashMap.Base (HashMap(..), Leaf(..)) import qualified Data.HashMap.Base as HM (defaultSalt, hashWithSalt, nextSalt, bitsPerSubkey) @@ -25,8 +26,8 @@ import qualified Data.HashMap.Array as A import Data.Validity (Validity) import Data.GenValidity (GenUnchecked(..), GenValid(..), genSplit, genSplit4) -import Test.Validity (producesValidsOnValids, producesValidsOnValids2, producesValidsOnValids3, genGeneratesValid) -import Test.QuickCheck (Arbitrary, CoArbitrary, Function, Property, Gen, sized, oneof, resize, scale) +import Test.Validity (producesValidsOnValids, producesValidsOnValids2, producesValidsOnValids3, shouldBeValid) +import Test.QuickCheck (Arbitrary, CoArbitrary, Function, Property, Gen, sized, oneof, resize, scale, suchThat, forAll) import Test.Framework(Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.QuickCheck.Function (Fun, apply, applyFun2, applyFun3) @@ -40,34 +41,44 @@ instance (Validity k, Validity v) => Validity (Leaf k v) where instance Validity a => Validity (A.Array a) where validate a = annotate (A.toList a) "The array elements" -instance (Hashable k, Validity k, Validity v) => Validity (HashMap k v) where +instance (Eq k, Hashable k, Validity k, Validity v) => Validity (HashMap k v) where validate = go HM.defaultSalt where go s hm = case hm of Empty -> mempty - (Leaf h l@(L k _)) -> mconcat + (Leaf h l@(L k _)) -> decorate "Leaf" $ mconcat [ annotate h "Hash" , annotate l "Leaf" , check (HM.hashWithSalt s k == h) "The hash is correct." ] - (BitmapIndexed bm a) -> mconcat + (BitmapIndexed bm a) -> decorate "BitmapIndexed" $ mconcat [ annotate bm "Bitmap" , decorate "Array" $ decorateList (A.toList a) $ go s , check (A.length a == popCount bm) - "Within 'BitmapIndexed' are values in the array equal to popCount bm." + "There are values in the array equal to popCount bm." + , check (uniques $ concatMap HM.keys $ A.toList a) "The keys are unique." ] - (Full a) -> mconcat + (Full a) -> decorate "Full" $ mconcat [ decorate "Array" $ decorateList (A.toList a) $ go s , check (A.length a == 2 ^ HM.bitsPerSubkey) - "Within 'Full' are 2 ^ bitsPerSubkey values in the array." + "There are 2 ^ bitsPerSubkey values in the array." + , check (uniques $ concatMap HM.keys $ A.toList a) "The keys are unique." ] - (Collision h l1 l2 hm') -> mconcat + (Collision h l1@(L k1 _) l2@(L k2 _) hm') -> decorate "Collision" $ mconcat [ annotate h "Hash" , annotate l1 "The first collision" , annotate l2 "The second collision" + , check (HM.hashWithSalt s k1 == h) "The hash of the first collision is correct." + , check (HM.hashWithSalt s k2 == h) "The hash of the second collision is correct." + , check (k1 /= k2) "The keys within the collision are not equal." + , check (uniques (k1 : k2 : HM.keys hm')) "The keys are unique." , decorate "The recursive HashMap" $ go (HM.nextSalt s) hm' + -- TODO the recursive hashmap has to collide on the current hash too ] +uniques :: Eq a => [a] -> Bool +uniques l = length (nub l) == length l + #if !MIN_VERSION_validity(0,6,0) -- | Decorate a validation with a location decorate :: String -> Validation -> Validation @@ -139,35 +150,58 @@ instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (HashMap k v) where ] shrinkUnchecked _ = [] -- TODO: write shrinking -instance (Hashable k, GenValid k, GenValid v) => GenValid (HashMap k v) where - genValid = go HM.defaultSalt +instance (Eq k, Hashable k, GenValid k, GenValid v) => GenValid (HashMap k v) where + genValid = go [] HM.defaultSalt where - go s = sized $ \n -> case n of - 0 -> pure Empty + genKey :: [k] -> Gen k + genKey keys = genValid `suchThat` (`notElem` keys) + genL :: [k] -> Gen (Leaf k v) + genL keys = sized $ \n -> do + (a, b) <- genSplit n + k <- resize a $ genKey keys + v <- resize b genValid + pure $ L k v + genFixedNbOfHms :: Int -> [k] -> Int -> Gen ([k], [HashMap k v]) + genFixedNbOfHms nb keys s = do + let go' :: ([k], [HashMap k v]) -> Int -> Gen ([k], [HashMap k v]) + go' (ks, hms) _ = do + (ks', hm') <- scale (`div` nb) $ genAndRecordKeys ks s + pure (ks' ++ ks, hm': hms) + (keys', values) <- foldM go' (keys, []) [1 .. nb] + pure (keys', values) + genAndRecordKeys :: [k] -> Int -> Gen ([k], HashMap k v) + genAndRecordKeys keys s = sized $ \n -> case n of + 0 -> pure (keys, Empty) _ -> oneof [ do - (a, b) <- genSplit n - k <- resize a genValid - v <- resize b genValid - pure $ Leaf (HM.hashWithSalt s k) (L k v) + l@(L k _) <- genL keys + let hm' = Leaf (HM.hashWithSalt s k) l :: HashMap k v + pure $ (k : keys, hm') , do - bm <- genValid - let pc = popCount bm - values <- replicateM pc $ scale (`div` pc) $ go s -- TODO Redistribute size better. - pure $ BitmapIndexed bm $ A.fromList pc values + bm <- genValid :: Gen Word + let pc = popCount bm :: Int + (keys', hms) <- genFixedNbOfHms pc keys s :: Gen ([k], [HashMap k v]) + let hm' = BitmapIndexed bm $ A.fromList pc hms :: HashMap k v + pure (keys', hm') , do let l = 2 ^ (4 :: Int) -- 4 == bitsPerSubkey - values <- replicateM l $ scale (`div` l) $ go s -- TODO Redistribute size better. - pure $ Full $ A.fromList l values + (keys', hms) <- genFixedNbOfHms l keys s + let hm' = Full $ A.fromList l hms :: HashMap k v + pure (keys', hm') :: Gen ([k], HashMap k v) , do - (a,b,c,d) <- genSplit4 n - hm' <- resize d $ go (HM.nextSalt s) - Collision - <$> resize a genValid - <*> resize b genValid - <*> resize c genValid - <*> pure hm' + (a,b) <- genSplit n + (l1@(L k1 _), l2@(L k2 _)) <- resize a $ + ((,) <$> genL keys <*> genL keys) + `suchThat` + (\((L k1 _), (L k2 _)) -> HM.hashWithSalt s k1 == HM.hashWithSalt s k2 && k1 /= k2) + -- Note: this can take long for keys that don't have a lot of collisions, so best + -- to only use it with the 'Key' in this module. + (keys', hm') <- resize b $ genAndRecordKeys (k1 : k2 : keys) (HM.nextSalt s) + pure (keys', Collision (HM.hashWithSalt s k1) l1 l2 hm') :: Gen ([k], HashMap k v) ] + -- Carry the keys that have already been used, and the salt on the current level. + go :: [k] -> Int -> Gen (HashMap k v) + go keys s = snd <$> genAndRecordKeys keys s -- Key type that generates more hash collisions. newtype Key = K @@ -175,7 +209,7 @@ newtype Key = K deriving (Arbitrary, CoArbitrary, Validity, GenUnchecked, GenValid, Eq, Ord, Read, Show, Generic) instance Hashable Key where - hashWithSalt salt k = hashWithSalt salt (unK k) `mod` 20 + hashWithSalt salt k = hashWithSalt salt (unK k) `mod` 10 instance Function Key @@ -300,15 +334,17 @@ pFromListWith f = producesValidsOnValids (HM.fromListWith (applyFun2 f) :: [(Key tests :: [Test] tests = + let genValidHelper gen = forAll gen shouldBeValid + in [ -- Basic interface testGroup "HashMap" [ testProperty "genValid generates valid values for Leaf" $ - genGeneratesValid (genValid :: Gen (Leaf Key Int)) shrinkValid + genValidHelper (genValid :: Gen (Leaf Key Int)) , testProperty "genValid generates valid values for Array" $ - genGeneratesValid (genValid :: Gen (A.Array Key)) shrinkValid + genValidHelper (genValid :: Gen (A.Array Key)) , testProperty "genValid generates valid values for HashMap" $ - genGeneratesValid (genValid :: Gen (HashMap Key Int)) shrinkValid + genValidHelper (genValid :: Gen (HashMap Key Int)) ] , testGroup "HashMap" [ testProperty "singleton produces valid HashMaps" pSingleton From b190874de01d3fa4c406fcfaf385bcb499660d10 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 19 Oct 2018 12:57:56 +0200 Subject: [PATCH 43/69] more accurate validity --- tests/Validity.hs | 58 +++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/tests/Validity.hs b/tests/Validity.hs index 5ebd100e..8435aa7c 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -10,9 +10,10 @@ module Main (main) where import GHC.Generics (Generic) -import Data.Validity import Data.Bits (popCount) +import Data.Foldable (foldl') import Data.List (nub) +import Data.Validity import Control.Monad (foldM) import Data.Hashable (Hashable(hashWithSalt)) #if defined(STRICT) @@ -20,7 +21,7 @@ import qualified Data.HashMap.Strict as HM #else import qualified Data.HashMap.Lazy as HM #endif -import Data.HashMap.Base (HashMap(..), Leaf(..)) +import Data.HashMap.Base (HashMap(..), Leaf(..), Salt, Hash) import qualified Data.HashMap.Base as HM (defaultSalt, hashWithSalt, nextSalt, bitsPerSubkey) import qualified Data.HashMap.Array as A @@ -73,7 +74,8 @@ instance (Eq k, Hashable k, Validity k, Validity v) => Validity (HashMap k v) wh , check (k1 /= k2) "The keys within the collision are not equal." , check (uniques (k1 : k2 : HM.keys hm')) "The keys are unique." , decorate "The recursive HashMap" $ go (HM.nextSalt s) hm' - -- TODO the recursive hashmap has to collide on the current hash too + , check (all (\k -> HM.hashWithSalt s k == h) (HM.keys hm')) + "All recursive keys hash to the hash within the collision." ] uniques :: Eq a => [a] -> Bool @@ -150,58 +152,64 @@ instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (HashMap k v) where ] shrinkUnchecked _ = [] -- TODO: write shrinking +-- Note: This instance has to generate collisions. +-- this can take long for keys that don't have a lot of collisions, so best +-- to only use it with the 'Key' in this module. instance (Eq k, Hashable k, GenValid k, GenValid v) => GenValid (HashMap k v) where - genValid = go [] HM.defaultSalt + genValid = snd <$> genAndRecordKeys [] [] HM.defaultSalt where - genKey :: [k] -> Gen k - genKey keys = genValid `suchThat` (`notElem` keys) - genL :: [k] -> Gen (Leaf k v) - genL keys = sized $ \n -> do + genKey :: [(Salt, Hash)] -> [k] -> Gen k + genKey hs keys = + foldl' + (\g (s,h) -> modGenWith s h g) + (genValid `suchThat` (`notElem` keys)) + hs + where + modGenWith :: Salt -> Hash -> Gen k -> Gen k + modGenWith s h g = g `suchThat` (\k -> HM.hashWithSalt s k == h) + genL :: [(Salt, Hash)] -> [k] -> Gen (Leaf k v) + genL hs keys = sized $ \n -> do (a, b) <- genSplit n - k <- resize a $ genKey keys + k <- resize a $ genKey hs keys v <- resize b genValid pure $ L k v - genFixedNbOfHms :: Int -> [k] -> Int -> Gen ([k], [HashMap k v]) - genFixedNbOfHms nb keys s = do + genFixedNbOfHms :: Int -> [(Salt, Hash)] -> [k] -> Salt -> Gen ([k], [HashMap k v]) + genFixedNbOfHms nb hs keys s = do let go' :: ([k], [HashMap k v]) -> Int -> Gen ([k], [HashMap k v]) go' (ks, hms) _ = do - (ks', hm') <- scale (`div` nb) $ genAndRecordKeys ks s + (ks', hm') <- scale (`div` nb) $ genAndRecordKeys hs ks s pure (ks' ++ ks, hm': hms) (keys', values) <- foldM go' (keys, []) [1 .. nb] pure (keys', values) - genAndRecordKeys :: [k] -> Int -> Gen ([k], HashMap k v) - genAndRecordKeys keys s = sized $ \n -> case n of + genAndRecordKeys :: [(Salt, Hash)] -> [k] -> Salt -> Gen ([k], HashMap k v) + genAndRecordKeys hs keys s = sized $ \n -> case n of 0 -> pure (keys, Empty) _ -> oneof [ do - l@(L k _) <- genL keys + l@(L k _) <- genL hs keys let hm' = Leaf (HM.hashWithSalt s k) l :: HashMap k v pure $ (k : keys, hm') , do bm <- genValid :: Gen Word let pc = popCount bm :: Int - (keys', hms) <- genFixedNbOfHms pc keys s :: Gen ([k], [HashMap k v]) + (keys', hms) <- genFixedNbOfHms pc hs keys s :: Gen ([k], [HashMap k v]) let hm' = BitmapIndexed bm $ A.fromList pc hms :: HashMap k v pure (keys', hm') , do let l = 2 ^ (4 :: Int) -- 4 == bitsPerSubkey - (keys', hms) <- genFixedNbOfHms l keys s + (keys', hms) <- genFixedNbOfHms l hs keys s let hm' = Full $ A.fromList l hms :: HashMap k v pure (keys', hm') :: Gen ([k], HashMap k v) , do (a,b) <- genSplit n (l1@(L k1 _), l2@(L k2 _)) <- resize a $ - ((,) <$> genL keys <*> genL keys) + ((,) <$> genL hs keys <*> genL hs keys) `suchThat` (\((L k1 _), (L k2 _)) -> HM.hashWithSalt s k1 == HM.hashWithSalt s k2 && k1 /= k2) - -- Note: this can take long for keys that don't have a lot of collisions, so best - -- to only use it with the 'Key' in this module. - (keys', hm') <- resize b $ genAndRecordKeys (k1 : k2 : keys) (HM.nextSalt s) - pure (keys', Collision (HM.hashWithSalt s k1) l1 l2 hm') :: Gen ([k], HashMap k v) + let h = HM.hashWithSalt s k1 + (keys', hm') <- resize b $ genAndRecordKeys ((s, h) : hs) (k1 : k2 : keys) (HM.nextSalt s) + pure (keys', Collision h l1 l2 hm') :: Gen ([k], HashMap k v) ] - -- Carry the keys that have already been used, and the salt on the current level. - go :: [k] -> Int -> Gen (HashMap k v) - go keys s = snd <$> genAndRecordKeys keys s -- Key type that generates more hash collisions. newtype Key = K From 04b5ec56965ffcac1ebad1057f2f486b20eee727 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 19 Oct 2018 13:13:04 +0200 Subject: [PATCH 44/69] Gave up on trying to write a principled genValid --- Data/HashMap/Base.hs | 1 + tests/Validity.hs | 78 ++++++++------------------------------------ 2 files changed, 15 insertions(+), 64 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 100ef07b..3efacfc2 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -179,6 +179,7 @@ instance (NFData k, NFData v) => NFData (Leaf k v) where -- | A map from keys to values. A map cannot contain duplicate keys; -- each key can map to at most one value. +-- TODO document all invariants data HashMap k v = Empty | BitmapIndexed !Bitmap !(A.Array (HashMap k v)) diff --git a/tests/Validity.hs b/tests/Validity.hs index 8435aa7c..63c7eca8 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -11,24 +11,22 @@ module Main (main) where import GHC.Generics (Generic) import Data.Bits (popCount) -import Data.Foldable (foldl') import Data.List (nub) import Data.Validity -import Control.Monad (foldM) import Data.Hashable (Hashable(hashWithSalt)) #if defined(STRICT) import qualified Data.HashMap.Strict as HM #else import qualified Data.HashMap.Lazy as HM #endif -import Data.HashMap.Base (HashMap(..), Leaf(..), Salt, Hash) +import Data.HashMap.Base (HashMap(..), Leaf(..)) import qualified Data.HashMap.Base as HM (defaultSalt, hashWithSalt, nextSalt, bitsPerSubkey) import qualified Data.HashMap.Array as A import Data.Validity (Validity) import Data.GenValidity (GenUnchecked(..), GenValid(..), genSplit, genSplit4) import Test.Validity (producesValidsOnValids, producesValidsOnValids2, producesValidsOnValids3, shouldBeValid) -import Test.QuickCheck (Arbitrary, CoArbitrary, Function, Property, Gen, sized, oneof, resize, scale, suchThat, forAll) +import Test.QuickCheck (Arbitrary, CoArbitrary, Function, Property, Gen, sized, oneof, resize, forAll) import Test.Framework(Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.QuickCheck.Function (Fun, apply, applyFun2, applyFun3) @@ -54,13 +52,19 @@ instance (Eq k, Hashable k, Validity k, Validity v) => Validity (HashMap k v) wh ] (BitmapIndexed bm a) -> decorate "BitmapIndexed" $ mconcat [ annotate bm "Bitmap" - , decorate "Array" $ decorateList (A.toList a) $ go s + , decorate "Array" $ decorateList (A.toList a) $ \hm_ -> mconcat + [ go s hm_ + , check (not $ HM.null hm_) "The sub HashMap is not empty" + ] , check (A.length a == popCount bm) "There are values in the array equal to popCount bm." , check (uniques $ concatMap HM.keys $ A.toList a) "The keys are unique." ] (Full a) -> decorate "Full" $ mconcat - [ decorate "Array" $ decorateList (A.toList a) $ go s + [ decorate "Array" $ decorateList (A.toList a) $ \hm_ -> mconcat + [ go s hm_ + , check (not $ HM.null hm_) "The sub HashMap is not empty" + ] , check (A.length a == 2 ^ HM.bitsPerSubkey) "There are 2 ^ bitsPerSubkey values in the array." , check (uniques $ concatMap HM.keys $ A.toList a) "The keys are unique." @@ -152,64 +156,10 @@ instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (HashMap k v) where ] shrinkUnchecked _ = [] -- TODO: write shrinking --- Note: This instance has to generate collisions. --- this can take long for keys that don't have a lot of collisions, so best --- to only use it with the 'Key' in this module. +-- It turns out it's almost impossible to write this instance without using internals. +-- This is good-enough for now. instance (Eq k, Hashable k, GenValid k, GenValid v) => GenValid (HashMap k v) where - genValid = snd <$> genAndRecordKeys [] [] HM.defaultSalt - where - genKey :: [(Salt, Hash)] -> [k] -> Gen k - genKey hs keys = - foldl' - (\g (s,h) -> modGenWith s h g) - (genValid `suchThat` (`notElem` keys)) - hs - where - modGenWith :: Salt -> Hash -> Gen k -> Gen k - modGenWith s h g = g `suchThat` (\k -> HM.hashWithSalt s k == h) - genL :: [(Salt, Hash)] -> [k] -> Gen (Leaf k v) - genL hs keys = sized $ \n -> do - (a, b) <- genSplit n - k <- resize a $ genKey hs keys - v <- resize b genValid - pure $ L k v - genFixedNbOfHms :: Int -> [(Salt, Hash)] -> [k] -> Salt -> Gen ([k], [HashMap k v]) - genFixedNbOfHms nb hs keys s = do - let go' :: ([k], [HashMap k v]) -> Int -> Gen ([k], [HashMap k v]) - go' (ks, hms) _ = do - (ks', hm') <- scale (`div` nb) $ genAndRecordKeys hs ks s - pure (ks' ++ ks, hm': hms) - (keys', values) <- foldM go' (keys, []) [1 .. nb] - pure (keys', values) - genAndRecordKeys :: [(Salt, Hash)] -> [k] -> Salt -> Gen ([k], HashMap k v) - genAndRecordKeys hs keys s = sized $ \n -> case n of - 0 -> pure (keys, Empty) - _ -> oneof - [ do - l@(L k _) <- genL hs keys - let hm' = Leaf (HM.hashWithSalt s k) l :: HashMap k v - pure $ (k : keys, hm') - , do - bm <- genValid :: Gen Word - let pc = popCount bm :: Int - (keys', hms) <- genFixedNbOfHms pc hs keys s :: Gen ([k], [HashMap k v]) - let hm' = BitmapIndexed bm $ A.fromList pc hms :: HashMap k v - pure (keys', hm') - , do - let l = 2 ^ (4 :: Int) -- 4 == bitsPerSubkey - (keys', hms) <- genFixedNbOfHms l hs keys s - let hm' = Full $ A.fromList l hms :: HashMap k v - pure (keys', hm') :: Gen ([k], HashMap k v) - , do - (a,b) <- genSplit n - (l1@(L k1 _), l2@(L k2 _)) <- resize a $ - ((,) <$> genL hs keys <*> genL hs keys) - `suchThat` - (\((L k1 _), (L k2 _)) -> HM.hashWithSalt s k1 == HM.hashWithSalt s k2 && k1 /= k2) - let h = HM.hashWithSalt s k1 - (keys', hm') <- resize b $ genAndRecordKeys ((s, h) : hs) (k1 : k2 : keys) (HM.nextSalt s) - pure (keys', Collision h l1 l2 hm') :: Gen ([k], HashMap k v) - ] + genValid = HM.fromList <$> genValid -- Key type that generates more hash collisions. newtype Key = K @@ -217,7 +167,7 @@ newtype Key = K deriving (Arbitrary, CoArbitrary, Validity, GenUnchecked, GenValid, Eq, Ord, Read, Show, Generic) instance Hashable Key where - hashWithSalt salt k = hashWithSalt salt (unK k) `mod` 10 + hashWithSalt salt k = hashWithSalt salt (unK k) `mod` 4 instance Function Key From d90dc838c7c3a18d90fc4abc58c5f6a994d4967b Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 19 Oct 2018 13:31:31 +0200 Subject: [PATCH 45/69] Improved shrinking --- tests/Validity.hs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/Validity.hs b/tests/Validity.hs index 63c7eca8..435cf809 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -124,7 +124,7 @@ instance GenUnchecked a => GenUnchecked (A.Array a) where genUnchecked = do l <- genUnchecked pure $ A.fromList (length l) l - shrinkUnchecked _ = [] -- TODO: whrite shrinking + shrinkUnchecked _ = [] -- TODO: write shrinking instance GenValid a => GenValid (A.Array a) where genValid = do @@ -154,12 +154,22 @@ instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (HashMap k v) where <*> resize c genUnchecked <*> resize d genUnchecked ] - shrinkUnchecked _ = [] -- TODO: write shrinking + shrinkUnchecked hm = case hm of + Empty -> [] + Leaf h l -> [Leaf h' l' | (h', l') <- shrinkUnchecked (h, l)] + BitmapIndexed bm a -> [BitmapIndexed bm' a' | (bm', a') <- shrinkUnchecked (bm, a)] + Full a -> Full <$> shrinkUnchecked a + Collision h l1 l2 hm_ -> + [ Collision h' l1' l2' hm_' + | (h', l1', l2', hm_') <- shrinkUnchecked (h, l1, l2, hm_) + ] -- It turns out it's almost impossible to write this instance without using internals. -- This is good-enough for now. instance (Eq k, Hashable k, GenValid k, GenValid v) => GenValid (HashMap k v) where genValid = HM.fromList <$> genValid + shrinkValid hm = HM.fromList <$> shrinkValid (HM.toList hm) + -- TODO improve the shrinking -- Key type that generates more hash collisions. newtype Key = K From 64c1a0ac14c4444b5c31eed20feaed7ccbc89082 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 19 Oct 2018 13:41:57 +0200 Subject: [PATCH 46/69] sick and tired of hashes for a moment --- Data/HashMap/Base.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 3efacfc2..85cbaa1b 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1656,7 +1656,7 @@ filterMapAux onLeaf = go Nothing -> Leaf h l2' Just (Leaf _ l3', hm') -> Collision h l2' l3' hm' _ -> error "Should not happen, can be fixed with refactoring I think." - (Nothing, Nothing) -> go hm + (Nothing, Nothing) -> rehash h $ go hm _ -> error "Should not happen, can be fixed with refactoring I think." where go' hm_ = case unConsHM hm_ of @@ -1664,6 +1664,7 @@ filterMapAux onLeaf = go Just (l, hm_') -> case onLeaf (Leaf h l) of Nothing -> go' hm_' Just l' -> Just (l', go hm_') + rehash = undefined filterA ary0 b0 = let !n = A.length ary0 From b076445d317eed997aea55c7275ae30e0affc0c6 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 26 Oct 2018 13:09:07 +0300 Subject: [PATCH 47/69] Failing test case --- Data/HashMap/Base.hs | 15 +- tests/Validity.hs | 554 ++++++++++++++++++++++++++++--------------- 2 files changed, 370 insertions(+), 199 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 85cbaa1b..3f515dce 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -109,6 +109,9 @@ module Data.HashMap.Base , insertKeyExists , deleteKeyExists , insertModifying + , unConsA + , UnCons(..) + , unConsHM , ptrEq , adjust# ) where @@ -1565,7 +1568,7 @@ intersectionWithKey f a b = foldlWithKey' go empty a -- | /O(n)/ Reduce this map by applying a binary operator to all -- elements, using the given starting value (typically the -- left-identity of the operator). Each application of the operator --- is evaluated before using the result in the next application. +-- is evaluated before using the result in the next application. -- This function is strict in the starting value. foldl' :: (a -> v -> a) -> a -> HashMap k v -> a foldl' f = foldlWithKey' (\ z _ v -> f z v) @@ -1574,7 +1577,7 @@ foldl' f = foldlWithKey' (\ z _ v -> f z v) -- | /O(n)/ Reduce this map by applying a binary operator to all -- elements, using the given starting value (typically the -- left-identity of the operator). Each application of the operator --- is evaluated before using the result in the next application. +-- is evaluated before using the result in the next application. -- This function is strict in the starting value. foldlWithKey' :: (a -> k -> v -> a) -> a -> HashMap k v -> a foldlWithKey' f = go @@ -1664,7 +1667,12 @@ filterMapAux onLeaf = go Just (l, hm_') -> case onLeaf (Leaf h l) of Nothing -> go' hm_' Just l' -> Just (l', go hm_') - rehash = undefined + rehash h hm = case hm of + Empty -> Empty + Leaf _ l -> Leaf h l + BitmapIndexed b ary -> undefined -- filterA ary b + Full ary -> undefined -- filterA ary fullNodeMask + Collision h' l1 l2 hm' -> Collision h l1 l2 $ rehash h' hm' filterA ary0 b0 = let !n = A.length ary0 @@ -1823,6 +1831,7 @@ data UnCons k v = WasEmpty | NowEmpty (Leaf k v) | UnConsed (Leaf k v) (A.Array (HashMap k v)) + deriving (Show) -- Remove (any) one element from a hashmap unConsHM :: HashMap k v -> Maybe (Leaf k v, HashMap k v) diff --git a/tests/Validity.hs b/tests/Validity.hs index 435cf809..cff9d4e0 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -6,85 +6,147 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -module Main (main) where +module Main + ( main + ) where import GHC.Generics (Generic) import Data.Bits (popCount) +import Data.Hashable (Hashable(hashWithSalt)) import Data.List (nub) import Data.Validity -import Data.Hashable (Hashable(hashWithSalt)) #if defined(STRICT) import qualified Data.HashMap.Strict as HM #else import qualified Data.HashMap.Lazy as HM #endif -import Data.HashMap.Base (HashMap(..), Leaf(..)) -import qualified Data.HashMap.Base as HM (defaultSalt, hashWithSalt, nextSalt, bitsPerSubkey) import qualified Data.HashMap.Array as A +import Data.HashMap.Base (HashMap(..), Leaf(..), UnCons(..)) +import qualified Data.HashMap.Base as HM + (bitsPerSubkey, defaultSalt, hashWithSalt, index, mask, nextSalt, + sparseIndex, unConsA, unConsHM) +import Data.GenValidity + (GenUnchecked(..), GenValid(..), genSplit, genSplit4) import Data.Validity (Validity) -import Data.GenValidity (GenUnchecked(..), GenValid(..), genSplit, genSplit4) -import Test.Validity (producesValidsOnValids, producesValidsOnValids2, producesValidsOnValids3, shouldBeValid) -import Test.QuickCheck (Arbitrary, CoArbitrary, Function, Property, Gen, sized, oneof, resize, forAll) -import Test.Framework(Test, defaultMain, testGroup) +import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) +import Test.QuickCheck + (Arbitrary, CoArbitrary, Function, Gen, Property, forAll, oneof, + resize, sized) import Test.QuickCheck.Function (Fun, apply, applyFun2, applyFun3) +import Test.Validity + (producesValidsOnValids, producesValidsOnValids2, + producesValidsOnValids3, shouldBeValid) instance (Validity k, Validity v) => Validity (Leaf k v) where - validate (L k v) = mconcat - [ annotate k "key" - , annotate v "value" - ] + validate (L k v) = mconcat [annotate k "key", annotate v "value"] instance Validity a => Validity (A.Array a) where validate a = annotate (A.toList a) "The array elements" -instance (Eq k, Hashable k, Validity k, Validity v) => Validity (HashMap k v) where - validate = go HM.defaultSalt +instance (Eq k, Hashable k, Validity k, Validity v) => Validity (UnCons k v) where + validate WasEmpty = mempty + validate (NowEmpty l) = decorate "NowEmpty" $ validate l + validate (UnConsed l a) = + decorate "UnConsed" $ + mconcat [decorate "Leaf" $ validate l, decorate "Array" $ validate a] + +instance (Eq k, Hashable k, Validity k, Validity v) => + Validity (HashMap k v) where + validate = go HM.defaultSalt 0 where - go s hm = case hm of - Empty -> mempty - (Leaf h l@(L k _)) -> decorate "Leaf" $ mconcat - [ annotate h "Hash" - , annotate l "Leaf" - , check (HM.hashWithSalt s k == h) "The hash is correct." - ] - (BitmapIndexed bm a) -> decorate "BitmapIndexed" $ mconcat - [ annotate bm "Bitmap" - , decorate "Array" $ decorateList (A.toList a) $ \hm_ -> mconcat - [ go s hm_ - , check (not $ HM.null hm_) "The sub HashMap is not empty" - ] - , check (A.length a == popCount bm) - "There are values in the array equal to popCount bm." - , check (uniques $ concatMap HM.keys $ A.toList a) "The keys are unique." - ] - (Full a) -> decorate "Full" $ mconcat - [ decorate "Array" $ decorateList (A.toList a) $ \hm_ -> mconcat - [ go s hm_ - , check (not $ HM.null hm_) "The sub HashMap is not empty" - ] - , check (A.length a == 2 ^ HM.bitsPerSubkey) - "There are 2 ^ bitsPerSubkey values in the array." - , check (uniques $ concatMap HM.keys $ A.toList a) "The keys are unique." - ] - (Collision h l1@(L k1 _) l2@(L k2 _) hm') -> decorate "Collision" $ mconcat - [ annotate h "Hash" - , annotate l1 "The first collision" - , annotate l2 "The second collision" - , check (HM.hashWithSalt s k1 == h) "The hash of the first collision is correct." - , check (HM.hashWithSalt s k2 == h) "The hash of the second collision is correct." - , check (k1 /= k2) "The keys within the collision are not equal." - , check (uniques (k1 : k2 : HM.keys hm')) "The keys are unique." - , decorate "The recursive HashMap" $ go (HM.nextSalt s) hm' - , check (all (\k -> HM.hashWithSalt s k == h) (HM.keys hm')) - "All recursive keys hash to the hash within the collision." - ] + go s bs hm = + case hm of + Empty -> mempty + (Leaf h l@(L k _)) -> + decorate "Leaf" $ + mconcat + [ annotate h "Hash" + , annotate l "Leaf" + , check + (HM.hashWithSalt s k == h) + "The hash is correct." + ] + (BitmapIndexed bm a) -> + decorate "BitmapIndexed" $ + mconcat + [ annotate bm "Bitmap" + , decorate "Array" $ + decorateList (zip [0 ..] $ A.toList a) $ \(ix, hm_) -> + mconcat + [ go s (bs + HM.bitsPerSubkey) hm_ + , check + (not $ HM.null hm_) + "The sub HashMap is not empty" + , decorateList (HM.keys hm_) $ \k -> + flip + check + "The hash, when masked with the current bitmask, of the key, produces the index in the array where the hashmap is found" $ + let h = HM.hashWithSalt s k + in HM.sparseIndex bm (HM.mask h bs) == + ix + ] + , check + (A.length a == popCount bm) + "There are values in the array equal to popCount bm." + , check + (uniques $ concatMap HM.keys $ A.toList a) + "The keys are unique." + ] + (Full a) -> + decorate "Full" $ + mconcat + [ decorate "Array" $ + decorateList (zip [0 ..] $ A.toList a) $ \(ix, hm_) -> + mconcat + [ go s (bs + HM.bitsPerSubkey) hm_ + , check + (not $ HM.null hm_) + "The sub HashMap is not empty" + , decorateList (HM.keys hm_) $ \k -> + flip + check + "The hash, when masked with the current bitmask, of the key, produces the index in the array where the hashmap is found" $ + let h = HM.hashWithSalt s k + in HM.index h bs == ix + ] + , check + (A.length a == 2 ^ HM.bitsPerSubkey) + "There are 2 ^ bitsPerSubkey values in the array." + , check + (uniques $ concatMap HM.keys $ A.toList a) + "The keys are unique." + ] + (Collision h l1@(L k1 _) l2@(L k2 _) hm') -> + decorate "Collision" $ + mconcat + [ annotate h "Hash" + , annotate l1 "The first collision" + , annotate l2 "The second collision" + , check + (HM.hashWithSalt s k1 == h) + "The hash of the first collision is correct." + , check + (HM.hashWithSalt s k2 == h) + "The hash of the second collision is correct." + , check + (k1 /= k2) + "The keys within the collision are not equal." + , check + (uniques (k1 : k2 : HM.keys hm')) + "The keys are unique." + , decorate "The recursive HashMap" $ + go (HM.nextSalt s) 0 hm' + , check + (all (\k -> HM.hashWithSalt s k == h) + (HM.keys hm')) + "All recursive keys hash to the hash within the collision, using the salt at this level." + ] uniques :: Eq a => [a] -> Bool uniques l = length (nub l) == length l - #if !MIN_VERSION_validity(0,6,0) -- | Decorate a validation with a location decorate :: String -> Validation -> Validation @@ -93,96 +155,111 @@ decorate = flip annotateValidation annotateValidation :: Validation -> String -> Validation annotateValidation val s = case val of - Validation errs -> Validation $ map (Location s) errs + Validation errs -> Validation $ map (Location s) errs #endif #if !MIN_VERSION_validity(0,8,0) -- | Decorate a piecewise validation of a list with their location in the list decorateList :: [a] -> (a -> Validation) -> Validation -decorateList as func = mconcat $ - flip map (zip [0..] as) $ \(i, a) -> - decorate (unwords ["The element at index", show (i :: Integer), "in the list"]) $ +decorateList as func = + mconcat $ + flip map (zip [0 ..] as) $ \(i, a) -> + decorate + (unwords + ["The element at index", show (i :: Integer), "in the list"]) $ func a #endif - instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (Leaf k v) where - genUnchecked = sized $ \n -> do - (a, b) <- genSplit n - k <- resize a genUnchecked - v <- resize b genUnchecked - pure $ L k v + genUnchecked = + sized $ \n -> do + (a, b) <- genSplit n + k <- resize a genUnchecked + v <- resize b genUnchecked + pure $ L k v shrinkUnchecked (L k v) = [L k' v' | (k', v') <- shrinkUnchecked (k, v)] instance (GenValid k, GenValid v) => GenValid (Leaf k v) where - genValid = sized $ \n -> do - (a, b) <- genSplit n - k <- resize a genValid - v <- resize b genValid - pure $ L k v + genValid = + sized $ \n -> do + (a, b) <- genSplit n + k <- resize a genValid + v <- resize b genValid + pure $ L k v instance GenUnchecked a => GenUnchecked (A.Array a) where genUnchecked = do - l <- genUnchecked - pure $ A.fromList (length l) l + l <- genUnchecked + pure $ A.fromList (length l) l shrinkUnchecked _ = [] -- TODO: write shrinking instance GenValid a => GenValid (A.Array a) where genValid = do - l <- genValid - pure $ A.fromList (length l) l + l <- genValid + pure $ A.fromList (length l) l instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (HashMap k v) where - genUnchecked = sized $ \n -> case n of - 0 -> pure Empty - _ -> oneof - [ do - (a, b) <- genSplit n - BitmapIndexed - <$> resize a genUnchecked - <*> resize b genUnchecked - , do - (a, b) <- genSplit n - Leaf - <$> resize a genUnchecked - <*> resize b genUnchecked - , Full <$> genUnchecked - , do - (a,b,c,d) <- genSplit4 n - Collision - <$> resize a genUnchecked - <*> resize b genUnchecked - <*> resize c genUnchecked - <*> resize d genUnchecked - ] - shrinkUnchecked hm = case hm of - Empty -> [] - Leaf h l -> [Leaf h' l' | (h', l') <- shrinkUnchecked (h, l)] - BitmapIndexed bm a -> [BitmapIndexed bm' a' | (bm', a') <- shrinkUnchecked (bm, a)] - Full a -> Full <$> shrinkUnchecked a - Collision h l1 l2 hm_ -> - [ Collision h' l1' l2' hm_' - | (h', l1', l2', hm_') <- shrinkUnchecked (h, l1, l2, hm_) - ] + genUnchecked = + sized $ \n -> + case n of + 0 -> pure Empty + _ -> + oneof + [ do (a, b) <- genSplit n + BitmapIndexed <$> resize a genUnchecked <*> + resize b genUnchecked + , do (a, b) <- genSplit n + Leaf <$> resize a genUnchecked <*> + resize b genUnchecked + , Full <$> genUnchecked + , do (a, b, c, d) <- genSplit4 n + Collision <$> resize a genUnchecked <*> + resize b genUnchecked <*> + resize c genUnchecked <*> + resize d genUnchecked + ] + shrinkUnchecked hm = + case hm of + Empty -> [] + Leaf h l -> [Leaf h' l' | (h', l') <- shrinkUnchecked (h, l)] + BitmapIndexed bm a -> + [BitmapIndexed bm' a' | (bm', a') <- shrinkUnchecked (bm, a)] + Full a -> Full <$> shrinkUnchecked a + Collision h l1 l2 hm_ -> + [ Collision h' l1' l2' hm_' + | (h', l1', l2', hm_') <- shrinkUnchecked (h, l1, l2, hm_) + ] -- It turns out it's almost impossible to write this instance without using internals. -- This is good-enough for now. -instance (Eq k, Hashable k, GenValid k, GenValid v) => GenValid (HashMap k v) where +instance (Eq k, Hashable k, GenValid k, GenValid v) => + GenValid (HashMap k v) where genValid = HM.fromList <$> genValid shrinkValid hm = HM.fromList <$> shrinkValid (HM.toList hm) -- TODO improve the shrinking -- Key type that generates more hash collisions. newtype Key = K - { unK :: Int } - deriving (Arbitrary, CoArbitrary, Validity, GenUnchecked, GenValid, Eq, Ord, Read, Show, Generic) + { unK :: Int + } deriving ( Arbitrary + , CoArbitrary + , Validity + , GenUnchecked + , GenValid + , Eq + , Ord + , Read + , Show + , Generic + ) instance Hashable Key where - hashWithSalt salt k = hashWithSalt salt (unK k) `mod` 4 + hashWithSalt salt k = hashWithSalt salt (unK k) `mod` 20 instance Function Key pSingleton :: Property -pSingleton = producesValidsOnValids2 (HM.singleton :: Key -> Int -> HM.HashMap Key Int) +pSingleton = + producesValidsOnValids2 (HM.singleton :: Key -> Int -> HM.HashMap Key Int) pNull :: Property pNull = producesValidsOnValids (HM.null :: HM.HashMap Key Int -> Bool) @@ -191,95 +268,152 @@ pSize :: Property pSize = producesValidsOnValids (HM.size :: HM.HashMap Key Int -> Int) pMember :: Property -pMember = producesValidsOnValids2 (HM.member :: Key -> HM.HashMap Key Int -> Bool) +pMember = + producesValidsOnValids2 (HM.member :: Key -> HM.HashMap Key Int -> Bool) pLookup :: Property -pLookup = producesValidsOnValids2 (HM.lookup :: Key -> HM.HashMap Key Int -> Maybe Int) +pLookup = + producesValidsOnValids2 + (HM.lookup :: Key -> HM.HashMap Key Int -> Maybe Int) pLookupDefault :: Property -pLookupDefault = producesValidsOnValids3 (HM.lookupDefault :: Int -> Key -> HM.HashMap Key Int -> Int) +pLookupDefault = + producesValidsOnValids3 + (HM.lookupDefault :: Int -> Key -> HM.HashMap Key Int -> Int) pInsert :: Property -pInsert = producesValidsOnValids3 (HM.insert :: Key -> Int -> HM.HashMap Key Int -> HM.HashMap Key Int) +pInsert = + producesValidsOnValids3 + (HM.insert :: Key -> Int -> HM.HashMap Key Int -> HM.HashMap Key Int) pInsertWith :: Property -pInsertWith = producesValidsOnValids3 (HM.insertWith (+) :: Key -> Int -> HM.HashMap Key Int -> HM.HashMap Key Int) +pInsertWith = + producesValidsOnValids3 + (HM.insertWith (+) :: Key -> Int -> HM.HashMap Key Int -> HM.HashMap Key Int) pDelete :: Property -pDelete = producesValidsOnValids2 (HM.delete :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) +pDelete = + producesValidsOnValids2 + (HM.delete :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) pAdjust :: Fun Int Int -> Property -pAdjust f = producesValidsOnValids2 (HM.adjust (apply f) :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) +pAdjust f = + producesValidsOnValids2 + (HM.adjust (apply f) :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) pUpdate :: (Fun Int (Maybe Int)) -> Property -pUpdate f = producesValidsOnValids2 (HM.update (apply f) :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) +pUpdate f = + producesValidsOnValids2 + (HM.update (apply f) :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) pAlter :: Property -pAlter = producesValidsOnValids2 (HM.alter (fmap succ) :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) +pAlter = + producesValidsOnValids2 + (HM.alter (fmap succ) :: Key -> HM.HashMap Key Int -> HM.HashMap Key Int) pAlterF :: Fun (Maybe Int) [Maybe Int] -> Property -pAlterF f = producesValidsOnValids2 (HM.alterF (apply f) :: Key -> HM.HashMap Key Int -> [HM.HashMap Key Int]) +pAlterF f = + producesValidsOnValids2 + (HM.alterF (apply f) :: Key -> HM.HashMap Key Int -> [HM.HashMap Key Int]) pUnion :: Property -pUnion = producesValidsOnValids2 (HM.union :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) +pUnion = + producesValidsOnValids2 + (HM.union :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) pUnionWith :: Fun (Int, Int) Int -> Property -pUnionWith f = producesValidsOnValids2 (HM.unionWith (applyFun2 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) +pUnionWith f = + producesValidsOnValids2 + (HM.unionWith (applyFun2 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) pUnionWithKey :: Fun (Key, Int, Int) Int -> Property -pUnionWithKey f = producesValidsOnValids2 (HM.unionWithKey (applyFun3 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) +pUnionWithKey f = + producesValidsOnValids2 + (HM.unionWithKey (applyFun3 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) pUnions :: Property -pUnions = producesValidsOnValids (HM.unions :: [HashMap Key Int] -> HashMap Key Int) +pUnions = + producesValidsOnValids (HM.unions :: [HashMap Key Int] -> HashMap Key Int) pMap :: Fun Int Int -> Property -pMap f = producesValidsOnValids (HM.map (apply f) :: HashMap Key Int -> HashMap Key Int) +pMap f = + producesValidsOnValids + (HM.map (apply f) :: HashMap Key Int -> HashMap Key Int) pMapWithKey :: Fun (Key, Int) Int -> Property -pMapWithKey f = producesValidsOnValids (HM.mapWithKey (applyFun2 f) :: HashMap Key Int -> HashMap Key Int) +pMapWithKey f = + producesValidsOnValids + (HM.mapWithKey (applyFun2 f) :: HashMap Key Int -> HashMap Key Int) pTraverseWithKey :: Fun (Key, Int) (Maybe Int) -> Property -pTraverseWithKey f = producesValidsOnValids (HM.traverseWithKey (applyFun2 f) :: HashMap Key Int -> Maybe (HashMap Key Int)) +pTraverseWithKey f = + producesValidsOnValids + (HM.traverseWithKey (applyFun2 f) :: HashMap Key Int -> Maybe (HashMap Key Int)) pDifference :: Property -pDifference = producesValidsOnValids2 (HM.difference :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) +pDifference = + producesValidsOnValids2 + (HM.difference :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) pDifferenceWith :: Fun (Int, Int) (Maybe Int) -> Property -pDifferenceWith f = producesValidsOnValids2 (HM.differenceWith (applyFun2 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) +pDifferenceWith f = + producesValidsOnValids2 + (HM.differenceWith (applyFun2 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) pIntersection :: Property -pIntersection = producesValidsOnValids2 (HM.intersection :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) +pIntersection = + producesValidsOnValids2 + (HM.intersection :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) pIntersectionWith :: Fun (Int, Int) Int -> Property -pIntersectionWith f = producesValidsOnValids2 (HM.intersectionWith (applyFun2 f) :: HashMap Key Int -> HashMap \ - Key Int -> HashMap Key Int) +pIntersectionWith f = + producesValidsOnValids2 + (HM.intersectionWith (applyFun2 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) pIntersectionWithKey :: Fun (Key, Int, Int) Int -> Property -pIntersectionWithKey f = producesValidsOnValids2 (HM.intersectionWithKey (applyFun3 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) +pIntersectionWithKey f = + producesValidsOnValids2 + (HM.intersectionWithKey (applyFun3 f) :: HashMap Key Int -> HashMap Key Int -> HashMap Key Int) pFoldl' :: Fun (Word, Int) Word -> Property -pFoldl' f = producesValidsOnValids2 (HM.foldl' (applyFun2 f) :: Word -> HashMap Key Int -> Word) +pFoldl' f = + producesValidsOnValids2 + (HM.foldl' (applyFun2 f) :: Word -> HashMap Key Int -> Word) pFoldlWithKey' :: Fun (Word, Key, Int) Word -> Property -pFoldlWithKey' f = producesValidsOnValids2 (HM.foldlWithKey' (applyFun3 f) :: Word -> HashMap Key Int -> Word) +pFoldlWithKey' f = + producesValidsOnValids2 + (HM.foldlWithKey' (applyFun3 f) :: Word -> HashMap Key Int -> Word) pFoldr :: Fun (Int, Word) Word -> Property -pFoldr f = producesValidsOnValids2 (HM.foldr (applyFun2 f) :: Word -> HashMap Key Int -> Word) +pFoldr f = + producesValidsOnValids2 + (HM.foldr (applyFun2 f) :: Word -> HashMap Key Int -> Word) pFoldrWithKey :: Fun (Key, Int, Word) Word -> Property -pFoldrWithKey f = producesValidsOnValids2 (HM.foldrWithKey (applyFun3 f) :: Word -> HashMap Key Int -> Word) +pFoldrWithKey f = + producesValidsOnValids2 + (HM.foldrWithKey (applyFun3 f) :: Word -> HashMap Key Int -> Word) pFilter :: Fun Int Bool -> Property -pFilter f = producesValidsOnValids (HM.filter (apply f) :: HashMap Key Int -> HashMap Key Int) +pFilter f = + producesValidsOnValids + (HM.filter (apply f) :: HashMap Key Int -> HashMap Key Int) pFilterWithKey :: Fun (Key, Int) Bool -> Property -pFilterWithKey f = producesValidsOnValids (HM.filterWithKey (applyFun2 f) :: HashMap Key Int -> HashMap Key Int) +pFilterWithKey f = + producesValidsOnValids + (HM.filterWithKey (applyFun2 f) :: HashMap Key Int -> HashMap Key Int) pMapMaybe :: Fun Int (Maybe Int) -> Property -pMapMaybe f = producesValidsOnValids (HM.mapMaybe (apply f) :: HashMap Key Int -> HashMap Key Int) +pMapMaybe f = + producesValidsOnValids + (HM.mapMaybe (apply f) :: HashMap Key Int -> HashMap Key Int) pMapMaybeWithKey :: Fun (Key, Int) (Maybe Int) -> Property -pMapMaybeWithKey f = producesValidsOnValids (HM.mapMaybeWithKey (applyFun2 f) :: HashMap Key Int -> HashMap Key Int) +pMapMaybeWithKey f = + producesValidsOnValids + (HM.mapMaybeWithKey (applyFun2 f) :: HashMap Key Int -> HashMap Key Int) pKeys :: Property pKeys = producesValidsOnValids (HM.keys :: HashMap Key Int -> [Key]) @@ -291,74 +425,102 @@ pToList :: Property pToList = producesValidsOnValids (HM.toList :: HashMap Key Int -> [(Key, Int)]) pFromList :: Property -pFromList = producesValidsOnValids (HM.fromList :: [(Key, Int)] -> HashMap Key Int) +pFromList = + producesValidsOnValids (HM.fromList :: [(Key, Int)] -> HashMap Key Int) pFromListWith :: Fun (Int, Int) Int -> Property -pFromListWith f = producesValidsOnValids (HM.fromListWith (applyFun2 f) :: [(Key, Int)] -> HashMap Key Int) +pFromListWith f = + producesValidsOnValids + (HM.fromListWith (applyFun2 f) :: [(Key, Int)] -> HashMap Key Int) + +pUnConsA :: Property +pUnConsA = + producesValidsOnValids + (HM.unConsA :: A.Array (HashMap Key Int) -> UnCons Key Int) +pUnConsHM :: Property +pUnConsHM = + producesValidsOnValids + (HM.unConsHM :: HashMap Key Int -> Maybe (Leaf Key Int, HashMap Key Int)) ------------------------------------------------------------------------ -- * Test list - tests :: [Test] tests = - let genValidHelper gen = forAll gen shouldBeValid - in - [ + let genValidHelper gen = forAll gen shouldBeValid -- Basic interface - testGroup "HashMap" - [ testProperty "genValid generates valid values for Leaf" $ - genValidHelper (genValid :: Gen (Leaf Key Int)) - , testProperty "genValid generates valid values for Array" $ - genValidHelper (genValid :: Gen (A.Array Key)) - , testProperty "genValid generates valid values for HashMap" $ - genValidHelper (genValid :: Gen (HashMap Key Int)) - ] - , testGroup "HashMap" - [ testProperty "singleton produces valid HashMaps" pSingleton - , testProperty "null produces valid Bools" pNull - , testProperty "size produces valid HashMaps" pSize - , testProperty "member produces valid HashMaps" pMember - , testProperty "lookup produces valid HashMaps" pLookup - , testProperty "lookupDefault produces valid HashMaps" pLookupDefault - , testProperty "insert produces valid HashMaps" pInsert - , testProperty "insertWith produces valid HashMaps" pInsertWith - , testProperty "delete produces valid HashMaps" pDelete - , testProperty "adjust produces valid HashMaps" pAdjust - , testProperty "update produces valid HashMaps" pUpdate - , testProperty "alter produces valid HashMaps" pAlter - , testProperty "alterF produces valid HashMaps" pAlterF - , testProperty "union produces valid HashMaps" pUnion - , testProperty "unionWith produces valid HashMaps" pUnionWith - , testProperty "unionWithKey produces valid HashMaps" pUnionWithKey - , testProperty "unions produces valid HashMaps" pUnions - , testProperty "map produces valid HashMaps" pMap - , testProperty "mapWithKey produces valid HashMaps" pMapWithKey - , testProperty "traverseWithKey produces valid HashMaps" pTraverseWithKey - , testProperty "difference produces valid HashMaps" pDifference - , testProperty "differenceWith produces valid HashMaps" pDifferenceWith - , testProperty "intersection produces valid HashMaps" pIntersection - , testProperty "intersectionWith produces valid HashMaps" pIntersectionWith - , testProperty "intersectionWithKey produces valid HashMaps" pIntersectionWithKey - , testProperty "foldl' produces valid HashMaps" pFoldl' - , testProperty "foldlWithKey' produces valid HashMaps" pFoldlWithKey' - , testProperty "foldr produces valid HashMaps" pFoldr - , testProperty "foldrWithKey produces valid HashMaps" pFoldrWithKey - , testProperty "filter produces valid HashMaps" pFilter - , testProperty "filterWithKey produces valid HashMaps" pFilterWithKey - , testProperty "mapMaybe produces valid HashMaps" pMapMaybe - , testProperty "mapMaybeWithKey produces valid HashMaps" pMapMaybeWithKey - , testProperty "keys produces valid lists" pKeys - , testProperty "elems produces valid lists" pElems - , testProperty "toList produces valid lists" pToList - , testProperty "fromList produces valid HashMaps" pFromList - , testProperty "fromListWith produces valid HashMaps" pFromListWith - ] - ] --- TODO keysSet + in [ testGroup + "HashMap" + [ testProperty "genValid generates valid values for Leaf" $ + genValidHelper (genValid :: Gen (Leaf Key Int)) + , testProperty "genValid generates valid values for Array" $ + genValidHelper (genValid :: Gen (A.Array Key)) + , testProperty "genValid generates valid values for HashMap" $ + genValidHelper (genValid :: Gen (HashMap Key Int)) + ] + , testGroup + "HashMap" + [ testProperty "singleton produces valid HashMaps" pSingleton + , testProperty "null produces valid Bools" pNull + , testProperty "size produces valid HashMaps" pSize + , testProperty "member produces valid HashMaps" pMember + , testProperty "lookup produces valid HashMaps" pLookup + , testProperty + "lookupDefault produces valid HashMaps" + pLookupDefault + , testProperty "insert produces valid HashMaps" pInsert + , testProperty "insertWith produces valid HashMaps" pInsertWith + , testProperty "delete produces valid HashMaps" pDelete + , testProperty "adjust produces valid HashMaps" pAdjust + , testProperty "update produces valid HashMaps" pUpdate + , testProperty "alter produces valid HashMaps" pAlter + , testProperty "alterF produces valid HashMaps" pAlterF + , testProperty "union produces valid HashMaps" pUnion + , testProperty "unionWith produces valid HashMaps" pUnionWith + , testProperty "unionWithKey produces valid HashMaps" pUnionWithKey + , testProperty "unions produces valid HashMaps" pUnions + , testProperty "map produces valid HashMaps" pMap + , testProperty "mapWithKey produces valid HashMaps" pMapWithKey + , testProperty + "traverseWithKey produces valid HashMaps" + pTraverseWithKey + , testProperty "difference produces valid HashMaps" pDifference + , testProperty + "differenceWith produces valid HashMaps" + pDifferenceWith + , testProperty "intersection produces valid HashMaps" pIntersection + , testProperty + "intersectionWith produces valid HashMaps" + pIntersectionWith + , testProperty + "intersectionWithKey produces valid HashMaps" + pIntersectionWithKey + , testProperty "foldl' produces valid HashMaps" pFoldl' + , testProperty + "foldlWithKey' produces valid HashMaps" + pFoldlWithKey' + , testProperty "foldr produces valid HashMaps" pFoldr + , testProperty "foldrWithKey produces valid HashMaps" pFoldrWithKey + , testProperty "filter produces valid HashMaps" pFilter + , testProperty + "filterWithKey produces valid HashMaps" + pFilterWithKey + , testProperty "mapMaybe produces valid HashMaps" pMapMaybe + , testProperty + "mapMaybeWithKey produces valid HashMaps" + pMapMaybeWithKey + , testProperty "keys produces valid lists" pKeys + , testProperty "elems produces valid lists" pElems + , testProperty "toList produces valid lists" pToList + , testProperty "fromList produces valid HashMaps" pFromList + , testProperty "fromListWith produces valid HashMaps" pFromListWith + , testProperty "unConsA produces valid results" pUnConsA + , testProperty "unConsHM produces valid HashMaps" pUnConsHM + ] + ] +-- TODO keysSet ------------------------------------------------------------------------ -- * Test harness - main :: IO () main = defaultMain tests From b5169e1a05f7682d0ff06a98f92a3c362b5e206d Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 26 Oct 2018 13:18:58 +0300 Subject: [PATCH 48/69] Used some invariants to simplify code --- Data/HashMap/Base.hs | 15 ++++++--------- tests/Validity.hs | 16 ++++++++-------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 3f515dce..0520331c 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1806,30 +1806,29 @@ updateOrConcatWithKey f ary1 ary2 = A.run $ do unConsA :: A.Array (HashMap k v) -> UnCons k v unConsA ary = case A.length ary of 1 -> case A.index ary 0 of - Empty -> WasEmpty -- That would be weird, but fine. + Empty -> error "Invariant violated: Empty hashmap in array." Leaf _ l -> NowEmpty l BitmapIndexed bm ary' -> case unConsA ary' of - WasEmpty -> WasEmpty -- That would be weird, but fine. NowEmpty l -> NowEmpty l UnConsed l a' -> UnConsed l (A.singleton (BitmapIndexed bm a')) Full ary' -> case unConsA ary' of - WasEmpty -> WasEmpty -- That would be weird, but fine. NowEmpty l -> NowEmpty l - UnConsed l a' -> UnConsed l (A.singleton (Full a')) -- TODO 'Full' seems wrong + UnConsed l a' -> + let bm = undefined + in UnConsed l (A.singleton (BitmapIndexed bm a')) -- TODO 'Full' seems wrong Collision h l1 l2 hm -> UnConsed l1 $ A.singleton $ case unConsHM hm of Nothing -> Leaf h l2 Just (l3, hm') -> Collision h l2 l3 hm' _ -> goA 0 where goA ix = case unConsHM $ A.index ary ix of - Nothing -> goA $ ix + 1 -- TODO this is probably problematic when the entire array only houses Empty hashmaps + Nothing -> goA $ ix + 1 -- this is fine because the array only houses nonempty hashmaps Just (l, hm') -> case hm' of Empty -> UnConsed l $ A.delete ary ix _ -> UnConsed l $ A.update ary ix hm' data UnCons k v - = WasEmpty - | NowEmpty (Leaf k v) + = NowEmpty (Leaf k v) | UnConsed (Leaf k v) (A.Array (HashMap k v)) deriving (Show) @@ -1839,11 +1838,9 @@ unConsHM hm = case hm of Empty -> Nothing Leaf _ l -> Just (l, Empty) BitmapIndexed bm ary' -> case unConsA ary' of - WasEmpty -> Nothing NowEmpty l -> Just (l, Empty) UnConsed l a' -> Just (l, BitmapIndexed bm a') Full ary' -> case unConsA ary' of - WasEmpty -> Nothing -- That would be weird, but fine. NowEmpty l -> Just (l, Empty) UnConsed l a' -> Just (l, Full a') -- TODO This 'Full' seems wrong Collision h l1 l2 hm' -> Just $ (,) l1 $ case unConsHM hm' of diff --git a/tests/Validity.hs b/tests/Validity.hs index cff9d4e0..a663bae9 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -46,8 +46,8 @@ instance (Validity k, Validity v) => Validity (Leaf k v) where instance Validity a => Validity (A.Array a) where validate a = annotate (A.toList a) "The array elements" -instance (Eq k, Hashable k, Validity k, Validity v) => Validity (UnCons k v) where - validate WasEmpty = mempty +instance (Eq k, Hashable k, Validity k, Validity v) => + Validity (UnCons k v) where validate (NowEmpty l) = decorate "NowEmpty" $ validate l validate (UnConsed l a) = decorate "UnConsed" $ @@ -433,16 +433,16 @@ pFromListWith f = producesValidsOnValids (HM.fromListWith (applyFun2 f) :: [(Key, Int)] -> HashMap Key Int) -pUnConsA :: Property -pUnConsA = - producesValidsOnValids - (HM.unConsA :: A.Array (HashMap Key Int) -> UnCons Key Int) - pUnConsHM :: Property pUnConsHM = producesValidsOnValids (HM.unConsHM :: HashMap Key Int -> Maybe (Leaf Key Int, HashMap Key Int)) +pUnConsA :: Property +pUnConsA = + producesValidsOnValids + (HM.unConsA :: A.Array (HashMap Key Int) -> UnCons Key Int) + ------------------------------------------------------------------------ -- * Test list tests :: [Test] @@ -514,8 +514,8 @@ tests = , testProperty "toList produces valid lists" pToList , testProperty "fromList produces valid HashMaps" pFromList , testProperty "fromListWith produces valid HashMaps" pFromListWith - , testProperty "unConsA produces valid results" pUnConsA , testProperty "unConsHM produces valid HashMaps" pUnConsHM + , testProperty "unConsA produces valid results" pUnConsA ] ] From dc908551ee4f69c248e9d4e51a2257043a6f244d Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Fri, 26 Oct 2018 16:56:59 +0300 Subject: [PATCH 49/69] More specific unconsing --- Data/HashMap/Base.hs | 27 ++++++++++++++++----------- tests/Validity.hs | 8 ++++++-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 0520331c..2fafa5f6 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -1810,26 +1810,29 @@ unConsA ary = case A.length ary of Leaf _ l -> NowEmpty l BitmapIndexed bm ary' -> case unConsA ary' of NowEmpty l -> NowEmpty l - UnConsed l a' -> UnConsed l (A.singleton (BitmapIndexed bm a')) + UnConsed _ l a' -> UnConsed 0 l (A.singleton (BitmapIndexed bm a')) Full ary' -> case unConsA ary' of NowEmpty l -> NowEmpty l - UnConsed l a' -> - let bm = undefined - in UnConsed l (A.singleton (BitmapIndexed bm a')) -- TODO 'Full' seems wrong - Collision h l1 l2 hm -> UnConsed l1 $ A.singleton $ case unConsHM hm of + UnConsed i l a' -> + let bm = fullNodeMask .&. complement (1 `unsafeShiftL` i) + in UnConsed 0 l (A.singleton (BitmapIndexed bm a')) + Collision h l1 l2 hm -> UnConsed 0 l1 $ A.singleton $ case unConsHM hm of Nothing -> Leaf h l2 Just (l3, hm') -> Collision h l2 l3 hm' _ -> goA 0 where goA ix = case unConsHM $ A.index ary ix of - Nothing -> goA $ ix + 1 -- this is fine because the array only houses nonempty hashmaps + Nothing -> goA $ ix + 1 -- This is fine because the array only houses nonempty hashmaps. Just (l, hm') -> case hm' of - Empty -> UnConsed l $ A.delete ary ix - _ -> UnConsed l $ A.update ary ix hm' + Empty -> UnConsed ix l $ A.delete ary ix -- This is fine because the array cannot become empty. + _ -> UnConsed ix l $ A.update ary ix hm' data UnCons k v = NowEmpty (Leaf k v) - | UnConsed (Leaf k v) (A.Array (HashMap k v)) + | UnConsed + Int -- The indexed of the array that was un-consed from + (Leaf k v) -- The leaf that was un-consed + (A.Array (HashMap k v)) -- The leftover array deriving (Show) -- Remove (any) one element from a hashmap @@ -1839,10 +1842,12 @@ unConsHM hm = case hm of Leaf _ l -> Just (l, Empty) BitmapIndexed bm ary' -> case unConsA ary' of NowEmpty l -> Just (l, Empty) - UnConsed l a' -> Just (l, BitmapIndexed bm a') + UnConsed _ l a' -> Just (l, BitmapIndexed bm a') -- TODO ignore the index? Full ary' -> case unConsA ary' of NowEmpty l -> Just (l, Empty) - UnConsed l a' -> Just (l, Full a') -- TODO This 'Full' seems wrong + UnConsed i l a' -> + let bm = fullNodeMask .&. complement (1 `unsafeShiftL` i) + in Just (l, BitmapIndexed bm a') Collision h l1 l2 hm' -> Just $ (,) l1 $ case unConsHM hm' of Nothing -> Leaf h l2 Just (l3, hm'') -> Collision h l2 l3 hm'' diff --git a/tests/Validity.hs b/tests/Validity.hs index a663bae9..29353170 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -49,9 +49,13 @@ instance Validity a => Validity (A.Array a) where instance (Eq k, Hashable k, Validity k, Validity v) => Validity (UnCons k v) where validate (NowEmpty l) = decorate "NowEmpty" $ validate l - validate (UnConsed l a) = + validate (UnConsed ix l a) = decorate "UnConsed" $ - mconcat [decorate "Leaf" $ validate l, decorate "Array" $ validate a] + mconcat + [ decorate "Int" $ validate ix + , decorate "Leaf" $ validate l + , decorate "Array" $ validate a + ] instance (Eq k, Hashable k, Validity k, Validity v) => Validity (HashMap k v) where From 136374a43112f515c46a383b0cb1a803423d9c47 Mon Sep 17 00:00:00 2001 From: David Feuer Date: Sat, 27 Oct 2018 17:30:22 -0400 Subject: [PATCH 50/69] Fix unconsing and filtering --- Data/HashMap/Base.hs | 154 ++++++++++++++++++++----------------------- tests/Validity.hs | 33 +++++----- 2 files changed, 87 insertions(+), 100 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 2fafa5f6..f9d2924d 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -109,9 +109,10 @@ module Data.HashMap.Base , insertKeyExists , deleteKeyExists , insertModifying - , unConsA - , UnCons(..) - , unConsHM + , unconsA + , UnconsA(..) + , UnconsHM(..) + , unconsHM , ptrEq , adjust# ) where @@ -1015,12 +1016,12 @@ delete' h0 s0 k0 m0 = go h0 s0 k0 0 m0 go h s k _ t@(Collision hx l1@(L k1 _) l2@(L k2 _) hmx) | h == hx = let go' - | k == k1 = case unConsHM hmx of - Nothing -> Leaf hx l2 - Just (l3, hmx') -> Collision hx l2 l3 hmx' - | k == k2 = case unConsHM hmx of - Nothing -> Leaf hx l1 - Just (l3, hmx') -> Collision hx l1 l3 hmx' + | k == k1 = case unconsHM hmx of + UnconsEmptyHM -> Leaf hx l2 + UnconsedHM l3 hmx' -> Collision hx l2 l3 hmx' + | k == k2 = case unconsHM hmx of + UnconsEmptyHM -> Leaf hx l1 + UnconsedHM l3 hmx' -> Collision hx l1 l3 hmx' | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx in go' | otherwise = t @@ -1067,12 +1068,12 @@ deleteKeyExists !h0 s0 !k0 !m0 = go h0 s0 k0 0 m0 go h s k _ (Collision hx l1@(L k1 _) l2@(L k2 _) hmx) | h == hx = let go' - | k == k1 = case unConsHM hmx of - Nothing -> Leaf hx l2 - Just (l3, hmx') -> Collision hx l2 l3 hmx' - | k == k2 = case unConsHM hmx of - Nothing -> Leaf hx l1 - Just (l3, hmx') -> Collision hx l1 l3 hmx' + | k == k1 = case unconsHM hmx of + UnconsEmptyHM -> Leaf hx l2 + UnconsedHM l3 hmx' -> Collision hx l2 l3 hmx' + | k == k2 = case unconsHM hmx of + UnconsEmptyHM -> Leaf hx l1 + UnconsedHM l3 hmx' -> Collision hx l1 l3 hmx' | otherwise = Collision hx l1 l2 $ go (hashWithSalt (nextSalt s) k) (nextSalt s) k 0 hmx in go' | otherwise = Empty -- error "Internal error: unexpected collision" @@ -1651,28 +1652,18 @@ filterMapAux onLeaf = go go (Full ary) = filterA ary fullNodeMask go (Collision h l1 l2 hm) = case (onLeaf (Leaf h l1), onLeaf (Leaf h l2)) of (Just (Leaf _ l1'), Just (Leaf _ l2')) -> Collision h l1' l2' $ go hm - (Just (Leaf _ l1'), Nothing) -> case go' hm of - Nothing -> Leaf h l1' - Just (Leaf _ l3', hm') -> Collision h l1' l3' hm' - _ -> error "Should not happen, can be fixed with refactoring I think." - (Nothing, Just (Leaf _ l2')) -> case go' hm of - Nothing -> Leaf h l2' - Just (Leaf _ l3', hm') -> Collision h l2' l3' hm' - _ -> error "Should not happen, can be fixed with refactoring I think." - (Nothing, Nothing) -> rehash h $ go hm + (Just (Leaf _ l1'), Nothing) -> go1 l1' + (Nothing, Just (Leaf _ l2')) -> go1 l2' + (Nothing, Nothing) -> case unconsHM (go hm) of + UnconsEmptyHM -> Empty + UnconsedHM l1' hm' -> case unconsHM hm' of + UnconsEmptyHM -> Leaf h l1' + UnconsedHM l2' hm'' -> Collision h l1' l2' hm'' _ -> error "Should not happen, can be fixed with refactoring I think." where - go' hm_ = case unConsHM hm_ of - Nothing -> Nothing - Just (l, hm_') -> case onLeaf (Leaf h l) of - Nothing -> go' hm_' - Just l' -> Just (l', go hm_') - rehash h hm = case hm of - Empty -> Empty - Leaf _ l -> Leaf h l - BitmapIndexed b ary -> undefined -- filterA ary b - Full ary -> undefined -- filterA ary fullNodeMask - Collision h' l1 l2 hm' -> Collision h l1 l2 $ rehash h' hm' + go1 l1' = case unconsHM (go hm) of + UnconsEmptyHM -> Leaf h l1' + UnconsedHM l2' hm' -> Collision h l1' l2' hm' filterA ary0 b0 = let !n = A.length ary0 @@ -1802,55 +1793,54 @@ updateOrConcatWithKey f ary1 ary2 = A.run $ do -- Helper functions for very rare cases. --- Remove (any) one element from an array of hashmaps -unConsA :: A.Array (HashMap k v) -> UnCons k v -unConsA ary = case A.length ary of - 1 -> case A.index ary 0 of - Empty -> error "Invariant violated: Empty hashmap in array." - Leaf _ l -> NowEmpty l - BitmapIndexed bm ary' -> case unConsA ary' of - NowEmpty l -> NowEmpty l - UnConsed _ l a' -> UnConsed 0 l (A.singleton (BitmapIndexed bm a')) - Full ary' -> case unConsA ary' of - NowEmpty l -> NowEmpty l - UnConsed i l a' -> - let bm = fullNodeMask .&. complement (1 `unsafeShiftL` i) - in UnConsed 0 l (A.singleton (BitmapIndexed bm a')) - Collision h l1 l2 hm -> UnConsed 0 l1 $ A.singleton $ case unConsHM hm of - Nothing -> Leaf h l2 - Just (l3, hm') -> Collision h l2 l3 hm' - _ -> goA 0 - where - goA ix = case unConsHM $ A.index ary ix of - Nothing -> goA $ ix + 1 -- This is fine because the array only houses nonempty hashmaps. - Just (l, hm') -> case hm' of - Empty -> UnConsed ix l $ A.delete ary ix -- This is fine because the array cannot become empty. - _ -> UnConsed ix l $ A.update ary ix hm' - -data UnCons k v - = NowEmpty (Leaf k v) - | UnConsed - Int -- The indexed of the array that was un-consed from - (Leaf k v) -- The leaf that was un-consed - (A.Array (HashMap k v)) -- The leftover array - deriving (Show) +-- Remove (any) one element from a non-empty array +-- of non-empty hashmaps. +unconsA :: A.Array (HashMap k v) -> UnconsA k v +unconsA ary = + case unconsHM (A.index ary 0) of + UnconsEmptyHM -> + error "Data.HashMap internal error: empty hashmap in array." + UnconsedHM lf Empty + | A.length ary == 1 -> NowEmptyA lf + | otherwise -> RemovedFirstA lf (A.delete ary 0) + UnconsedHM lf hm' -> UnconsedFromFirstA lf (A.update ary 0 hm') + +data UnconsA k v + = NowEmptyA !(Leaf k v) + | UnconsedFromFirstA + !(Leaf k v) -- The leaf that was un-consed + !(A.Array (HashMap k v)) -- The leftover array + | RemovedFirstA + !(Leaf k v) -- The leaf that was un-consed + !(A.Array (HashMap k v)) -- The leftover array + deriving Show + +data UnconsHM k v + = UnconsEmptyHM + | UnconsedHM !(Leaf k v) !(HashMap k v) + deriving Show -- Remove (any) one element from a hashmap -unConsHM :: HashMap k v -> Maybe (Leaf k v, HashMap k v) -unConsHM hm = case hm of - Empty -> Nothing - Leaf _ l -> Just (l, Empty) - BitmapIndexed bm ary' -> case unConsA ary' of - NowEmpty l -> Just (l, Empty) - UnConsed _ l a' -> Just (l, BitmapIndexed bm a') -- TODO ignore the index? - Full ary' -> case unConsA ary' of - NowEmpty l -> Just (l, Empty) - UnConsed i l a' -> - let bm = fullNodeMask .&. complement (1 `unsafeShiftL` i) - in Just (l, BitmapIndexed bm a') - Collision h l1 l2 hm' -> Just $ (,) l1 $ case unConsHM hm' of - Nothing -> Leaf h l2 - Just (l3, hm'') -> Collision h l2 l3 hm'' +unconsHM :: HashMap k v -> UnconsHM k v +unconsHM hm = case hm of + Empty -> UnconsEmptyHM + Leaf _ l -> UnconsedHM l Empty + BitmapIndexed bm ary' -> case unconsA ary' of + NowEmptyA l -> UnconsedHM l Empty + UnconsedFromFirstA l a' -> UnconsedHM l (BitmapIndexed bm a') + RemovedFirstA l a' -> shortened l bm a' + Full ary' -> case unconsA ary' of + NowEmptyA l -> UnconsedHM l Empty + UnconsedFromFirstA l a' -> UnconsedHM l (Full a') + RemovedFirstA l a' -> shortened l fullNodeMask a' + Collision h l1 l2 hm' -> UnconsedHM l1 $ case unconsHM hm' of + UnconsEmptyHM -> Leaf h l2 + UnconsedHM l3 hm'' -> Collision h l2 l3 hm'' + where + -- The array was shortened, so we need to remove the first + -- element from the bitmap. + shortened l bm ar = + UnconsedHM l $ BitmapIndexed (bm .&. (bm - 1)) ar ------------------------------------------------------------------------ -- Manually unrolled loops diff --git a/tests/Validity.hs b/tests/Validity.hs index 29353170..fc4eeee5 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -22,10 +22,10 @@ import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Lazy as HM #endif import qualified Data.HashMap.Array as A -import Data.HashMap.Base (HashMap(..), Leaf(..), UnCons(..)) +import Data.HashMap.Base (HashMap(..), Leaf(..), UnconsHM(..)) import qualified Data.HashMap.Base as HM (bitsPerSubkey, defaultSalt, hashWithSalt, index, mask, nextSalt, - sparseIndex, unConsA, unConsHM) + sparseIndex, unconsHM) import Data.GenValidity (GenUnchecked(..), GenValid(..), genSplit, genSplit4) @@ -47,14 +47,13 @@ instance Validity a => Validity (A.Array a) where validate a = annotate (A.toList a) "The array elements" instance (Eq k, Hashable k, Validity k, Validity v) => - Validity (UnCons k v) where - validate (NowEmpty l) = decorate "NowEmpty" $ validate l - validate (UnConsed ix l a) = - decorate "UnConsed" $ + Validity (UnconsHM k v) where + validate UnconsEmptyHM = decorate "UnconsEmptyHM" valid + validate (UnconsedHM l hm) = + decorate "UnconsedHM" $ mconcat - [ decorate "Int" $ validate ix - , decorate "Leaf" $ validate l - , decorate "Array" $ validate a + [ decorate "Leaf" $ validate l + , decorate "Map" $ validate hm ] instance (Eq k, Hashable k, Validity k, Validity v) => @@ -437,15 +436,14 @@ pFromListWith f = producesValidsOnValids (HM.fromListWith (applyFun2 f) :: [(Key, Int)] -> HashMap Key Int) -pUnConsHM :: Property -pUnConsHM = +pUnconsHM :: Property +pUnconsHM = producesValidsOnValids - (HM.unConsHM :: HashMap Key Int -> Maybe (Leaf Key Int, HashMap Key Int)) + (HM.unconsHM :: HashMap Key Int -> UnconsHM Key Int) -pUnConsA :: Property -pUnConsA = - producesValidsOnValids - (HM.unConsA :: A.Array (HashMap Key Int) -> UnCons Key Int) +-- There isn't currently a pUnconsA because I (David Feuer) don't know +-- the right way to tell genvalidity-hspec that we only want cases +-- where the array is non-empty and all of its elements are non-empty. ------------------------------------------------------------------------ -- * Test list @@ -518,8 +516,7 @@ tests = , testProperty "toList produces valid lists" pToList , testProperty "fromList produces valid HashMaps" pFromList , testProperty "fromListWith produces valid HashMaps" pFromListWith - , testProperty "unConsHM produces valid HashMaps" pUnConsHM - , testProperty "unConsA produces valid results" pUnConsA + , testProperty "unconsHM produces valid HashMaps" pUnconsHM ] ] From eadac29809f38f671bcf82d04ad04cd7fe43d2da Mon Sep 17 00:00:00 2001 From: David Feuer Date: Sat, 27 Oct 2018 19:56:01 -0400 Subject: [PATCH 51/69] Improve the Ord and Ord 1 implementations Also add more tests to deal with the fact that we now have more equality and comparison implementations. --- Data/HashMap/Base.hs | 48 +++++++++++++++++++++++++++++++++----- tests/HashMapProperties.hs | 20 ++++++++++++++++ tests/HashSetProperties.hs | 32 ++++++++++++++++++++----- 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 2fafa5f6..d9f12946 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -163,6 +163,7 @@ import Data.Functor.Identity (Identity (..)) #endif import Control.Applicative (Const (..)) import Data.Coerce (coerce) +import Data.Function (on) -- | A set of values. A set cannot contain duplicate values. ------------------------------------------------------------------------ @@ -335,10 +336,10 @@ equal2 eqk eqv t1 t2 = go (toList' t1 []) (toList' t2 []) #if MIN_VERSION_base(4,9,0) instance Ord2 HashMap where - liftCompare2 = cmp + liftCompare2 = cmp2 instance (Ord k, Hashable k) => Ord1 (HashMap k) where - liftCompare = cmp compare + liftCompare = cmp1 #endif -- | The order is total. @@ -347,11 +348,46 @@ instance (Ord k, Hashable k) => Ord1 (HashMap k) where -- versions, OSes, or architectures, neither is an actual order of elements in -- 'HashMap' or an result of `compare`.is stable. instance (Ord k, Hashable k, Ord v) => Ord (HashMap k v) where - compare = cmp compare compare + compare = cmp1 compare -cmp :: (k -> k' -> Ordering) -> (v -> v' -> Ordering) +cmp1 :: (Ord k, Hashable k) => (v -> v' -> Ordering) + -> HashMap k v -> HashMap k v' -> Ordering +cmp1 cmpv t1 t2 = go (toList' t1 []) (toList' t2 []) + where + go (Leaf k1 l1 : tl1) (Leaf k2 l2 : tl2) + = compare k1 k2 `mappend` + leafCompare l1 l2 `mappend` + go tl1 tl2 + go (Collision h1 (L k11 v11) (L k12 v12) hm1 : tl1) (Collision h2 (L k21 v21) (L k22 v22) hm2 : tl2) + = compare h1 h2 `mappend` + compare (length hm1) (length hm2) `mappend` + liftCompareList pairCompare + -- We don't use sortOn because fst is cheap. + (L.sortBy (compare `on` fst) $ + (k11, v11) : (k12, v12) : toList hm1) + (L.sortBy (compare `on` fst) $ + (k21, v21) : (k22, v22) : toList hm2) `mappend` + go tl1 tl2 + go (Leaf _ _ : _) (Collision _ _ _ _ : _) = LT + go (Collision _ _ _ _ : _) (Leaf _ _ : _) = GT + go [] [] = EQ + go [] _ = LT + go _ [] = GT + go _ _ = error "cmp2: Should never happen, toList' includes non Leaf / Collision" + + leafCompare (L k v) (L k' v') = compare k k' `mappend` cmpv v v' + pairCompare (k1, v1) (k2, v2) = (k1 `compare` k2) `mappend` (v1 `cmpv` v2) + +-- Our own copy of liftCompare for lists. +liftCompareList :: (a -> b -> Ordering) -> [a] -> [b] -> Ordering +liftCompareList _ [] [] = EQ +liftCompareList _ [] _ = LT +liftCompareList _ _ [] = GT +liftCompareList cmp (x : xs) (y : ys) = cmp x y `mappend` liftCompareList cmp xs ys + +cmp2 :: (k -> k' -> Ordering) -> (v -> v' -> Ordering) -> HashMap k v -> HashMap k' v' -> Ordering -cmp cmpk cmpv t1 t2 = go (toList' t1 []) (toList' t2 []) +cmp2 cmpk cmpv t1 t2 = go (toList' t1 []) (toList' t2 []) where go (Leaf k1 l1 : tl1) (Leaf k2 l2 : tl2) = compare k1 k2 `mappend` @@ -369,7 +405,7 @@ cmp cmpk cmpv t1 t2 = go (toList' t1 []) (toList' t2 []) go [] [] = EQ go [] _ = LT go _ [] = GT - go _ _ = error "cmp: Should never happend, toList' includes non Leaf / Collision" + go _ _ = error "cmp2: Should never happen, toList' includes non Leaf / Collision" leafCompare (L k v) (L k' v') = cmpk k k' `mappend` cmpv v v' diff --git a/tests/HashMapProperties.hs b/tests/HashMapProperties.hs index b2b4c422..8f1833ae 100644 --- a/tests/HashMapProperties.hs +++ b/tests/HashMapProperties.hs @@ -27,6 +27,9 @@ import Data.Functor.Identity (Identity (..)) import Control.Applicative (Const (..)) import Test.QuickCheck.Function (Fun, apply) import Test.QuickCheck.Poly (A, B) +#if MIN_VERSION_base(4,9,0) +import Data.Functor.Classes (eq2, compare2) +#endif -- Key type that generates more hash collisions. newtype Key = K { unK :: Int } @@ -44,6 +47,15 @@ instance Hashable Key where pEq :: [(Key, Int)] -> [(Key, Int)] -> Bool pEq xs = (M.fromList xs ==) `eq` (HM.fromList xs ==) +-- Caution: this is a rather weak test. Fortunately, the "Permutation" +-- properties in the HashSet test suite should catch most of what this +-- doesn't. +pEq2 :: [(Key, Int)] -> [(Key, Int)] -> Property +pEq2 xs ys = (x == y) === (x `eq2` y) + where + x = HM.fromList xs + y = HM.fromList ys + pNeq :: [(Key, Int)] -> [(Key, Int)] -> Bool pNeq xs = (M.fromList xs /=) `eq` (HM.fromList xs /=) @@ -76,6 +88,12 @@ pOrd3 xs ys = case (compare x y, compare y x) of x = HM.fromList xs y = HM.fromList ys +pOrd4 :: [(Key, Int)] -> [(Key, Int)] -> Property +pOrd4 xs ys = compare2 x y === compare x y + where + x = HM.fromList xs + y = HM.fromList ys + pOrdEq :: [(Key, Int)] -> [(Key, Int)] -> Bool pOrdEq xs ys = case (compare x y, x == y) of (EQ, True) -> True @@ -379,9 +397,11 @@ tests = testGroup "instances" [ testProperty "==" pEq , testProperty "/=" pNeq + , testProperty "eq2 = (==)" pEq2 , testProperty "compare reflexive" pOrd1 , testProperty "compare transitive" pOrd2 , testProperty "compare antisymmetric" pOrd3 + , testProperty "compare2 = compare" pOrd4 , testProperty "Ord => Eq" pOrdEq , testProperty "Read/Show" pReadShow , testProperty "Functor" pFunctor diff --git a/tests/HashSetProperties.hs b/tests/HashSetProperties.hs index 130c1dc4..fd65bf9e 100644 --- a/tests/HashSetProperties.hs +++ b/tests/HashSetProperties.hs @@ -14,6 +14,7 @@ import Data.Ord (comparing) import Test.QuickCheck (Arbitrary, Property, (==>), (===)) import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) +import Data.Functor.Classes (eq1, compare1) -- Key type that generates more hash collisions. newtype Key = K { unK :: Int } @@ -80,13 +81,29 @@ pFoldable :: [Int] -> Bool pFoldable = (L.sort . Foldable.foldr (:) []) `eq` (L.sort . Foldable.foldr (:) []) -pPermutationEq :: [Key] -> [Int] -> Bool -pPermutationEq xs is = S.fromList xs == S.fromList ys +pPermutationEq :: [(Key, Int)] -> Bool +pPermutationEq xs = S.fromList x == S.fromList y where - ys = shuffle is xs - shuffle idxs = L.map snd - . L.sortBy (comparing fst) - . L.zip (idxs ++ [L.maximum (0:is) + 1 ..]) + x = L.map fst xs + y = L.map fst . L.sortBy (comparing snd) $ xs + +pPermutationEq1 :: [(Key, Int)] -> Bool +pPermutationEq1 xs = S.fromList x `eq1` S.fromList y + where + x = L.map fst xs + y = L.map fst . L.sortBy (comparing snd) $ xs + +pPermutationOrd :: [(Key, Int)] -> Bool +pPermutationOrd xs = (S.fromList x `compare` S.fromList y) == EQ + where + x = L.map fst xs + y = L.map fst . L.sortBy (comparing snd) $ xs + +pPermutationOrd1 :: [(Key, Int)] -> Bool +pPermutationOrd1 xs = (S.fromList x `compare1` S.fromList y) == EQ + where + x = L.map fst xs + y = L.map fst . L.sortBy (comparing snd) $ xs pHashable :: [Key] -> [Int] -> Int -> Property pHashable xs is salt = @@ -174,6 +191,9 @@ tests = testGroup "instances" [ testProperty "==" pEq , testProperty "Permutation ==" pPermutationEq + , testProperty "Permutation eq1" pPermutationEq1 + , testProperty "Permutation compare" pPermutationOrd + , testProperty "Permutation compare1" pPermutationOrd1 , testProperty "/=" pNeq , testProperty "compare reflexive" pOrd1 , testProperty "compare transitive" pOrd2 From 6ec3979eda9555bc3ac7af55343727401d647268 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Sat, 27 Oct 2018 12:11:11 +0300 Subject: [PATCH 52/69] equal1 and comp1 --- Data/HashMap/Base.hs | 4 ++++ tests/Validity.hs | 29 +++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index cb3d784c..daf18c47 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -23,6 +23,10 @@ module Data.HashMap.Base -- * Basic interface , null , size + , equal1 + , equal2 + , cmp1 + , cmp2 , member , lookup , lookupDefault diff --git a/tests/Validity.hs b/tests/Validity.hs index fc4eeee5..566c0220 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -24,8 +24,9 @@ import qualified Data.HashMap.Lazy as HM import qualified Data.HashMap.Array as A import Data.HashMap.Base (HashMap(..), Leaf(..), UnconsHM(..)) import qualified Data.HashMap.Base as HM - (bitsPerSubkey, defaultSalt, hashWithSalt, index, mask, nextSalt, - sparseIndex, unconsHM) + (bitsPerSubkey, cmp1, cmp2, defaultSalt, equal1, equal2, + hashWithSalt, index, mask, nextSalt, sparseIndex, unConsA, + unConsHM) import Data.GenValidity (GenUnchecked(..), GenValid(..), genSplit, genSplit4) @@ -270,6 +271,26 @@ pNull = producesValidsOnValids (HM.null :: HM.HashMap Key Int -> Bool) pSize :: Property pSize = producesValidsOnValids (HM.size :: HM.HashMap Key Int -> Int) +pEqual1 :: Fun (Int, Int) Bool -> Property +pEqual1 f = + producesValidsOnValids2 + (HM.equal1 (applyFun2 f) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Bool) + +pEqual2 :: Fun (Key, Key) Bool -> Fun (Int, Int) Bool -> Property +pEqual2 f g = + producesValidsOnValids2 + (HM.equal2 (applyFun2 f) (applyFun2 g) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Bool) + +pCmp1 :: Fun (Int, Int) Ordering -> Property +pCmp1 f = + producesValidsOnValids2 + (HM.cmp1 (applyFun2 f) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Ordering) + +pCmp2 :: Fun (Key, Key) Ordering -> Fun (Int, Int) Ordering -> Property +pCmp2 f g = + producesValidsOnValids2 + (HM.cmp2 (applyFun2 f) (applyFun2 g) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Ordering) + pMember :: Property pMember = producesValidsOnValids2 (HM.member :: Key -> HM.HashMap Key Int -> Bool) @@ -465,6 +486,10 @@ tests = [ testProperty "singleton produces valid HashMaps" pSingleton , testProperty "null produces valid Bools" pNull , testProperty "size produces valid HashMaps" pSize + , testProperty "equal1 produce valid Bools" pEqual1 + , testProperty "equal2 produce valid Bools" pEqual2 + , testProperty "cmp1 produce valid Orderings" pCmp1 + , testProperty "cmp2 produce valid Orderings" pCmp2 , testProperty "member produces valid HashMaps" pMember , testProperty "lookup produces valid HashMaps" pLookup , testProperty From 5ac7859d9ef401ab5902bd22d339c19cfabcca98 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 29 Oct 2018 11:47:59 +0100 Subject: [PATCH 53/69] done for now? --- tests/Validity.hs | 221 ++++++++++++++++++++++++++-------------------- 1 file changed, 125 insertions(+), 96 deletions(-) diff --git a/tests/Validity.hs b/tests/Validity.hs index 566c0220..5b176031 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -24,22 +24,44 @@ import qualified Data.HashMap.Lazy as HM import qualified Data.HashMap.Array as A import Data.HashMap.Base (HashMap(..), Leaf(..), UnconsHM(..)) import qualified Data.HashMap.Base as HM - (bitsPerSubkey, cmp1, cmp2, defaultSalt, equal1, equal2, - hashWithSalt, index, mask, nextSalt, sparseIndex, unConsA, - unConsHM) - -import Data.GenValidity - (GenUnchecked(..), GenValid(..), genSplit, genSplit4) + ( bitsPerSubkey + , cmp1 + , cmp2 + , defaultSalt + , equal1 + , equal2 + , hashWithSalt + , index + , mask + , nextSalt + , sparseIndex + , unconsHM + ) +import qualified Data.HashSet as HS +import Data.HashSet (HashSet) + +import Data.GenValidity (GenUnchecked(..), GenValid(..), genSplit, genSplit4) import Data.Validity (Validity) import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.QuickCheck - (Arbitrary, CoArbitrary, Function, Gen, Property, forAll, oneof, - resize, sized) + ( Arbitrary + , CoArbitrary + , Function + , Gen + , Property + , forAll + , oneof + , resize + , sized + ) import Test.QuickCheck.Function (Fun, apply, applyFun2, applyFun3) import Test.Validity - (producesValidsOnValids, producesValidsOnValids2, - producesValidsOnValids3, shouldBeValid) + ( producesValidsOnValids + , producesValidsOnValids2 + , producesValidsOnValids3 + , shouldBeValid + ) instance (Validity k, Validity v) => Validity (Leaf k v) where validate (L k v) = mconcat [annotate k "key", annotate v "value"] @@ -52,10 +74,7 @@ instance (Eq k, Hashable k, Validity k, Validity v) => validate UnconsEmptyHM = decorate "UnconsEmptyHM" valid validate (UnconsedHM l hm) = decorate "UnconsedHM" $ - mconcat - [ decorate "Leaf" $ validate l - , decorate "Map" $ validate hm - ] + mconcat [decorate "Leaf" $ validate l, decorate "Map" $ validate hm] instance (Eq k, Hashable k, Validity k, Validity v) => Validity (HashMap k v) where @@ -89,8 +108,8 @@ instance (Eq k, Hashable k, Validity k, Validity v) => check "The hash, when masked with the current bitmask, of the key, produces the index in the array where the hashmap is found" $ let h = HM.hashWithSalt s k - in HM.sparseIndex bm (HM.mask h bs) == - ix + in HM.sparseIndex bm (HM.mask h bs) == + ix ] , check (A.length a == popCount bm) @@ -114,7 +133,7 @@ instance (Eq k, Hashable k, Validity k, Validity v) => check "The hash, when masked with the current bitmask, of the key, produces the index in the array where the hashmap is found" $ let h = HM.hashWithSalt s k - in HM.index h bs == ix + in HM.index h bs == ix ] , check (A.length a == 2 ^ HM.bitsPerSubkey) @@ -149,6 +168,9 @@ instance (Eq k, Hashable k, Validity k, Validity v) => "All recursive keys hash to the hash within the collision, using the salt at this level." ] +instance (Eq v, Hashable v, Validity v) => Validity (HashSet v) where + validate hs = decorate "asMap" $ validate $ HS.toMap hs + uniques :: Eq a => [a] -> Bool uniques l = length (nub l) == length l #if !MIN_VERSION_validity(0,6,0) @@ -457,96 +479,103 @@ pFromListWith f = producesValidsOnValids (HM.fromListWith (applyFun2 f) :: [(Key, Int)] -> HashMap Key Int) +pKeysSet :: Property +pKeysSet = producesValidsOnValids (HM.keysSet :: HashMap Key Int -> HashSet Key) + pUnconsHM :: Property pUnconsHM = - producesValidsOnValids - (HM.unconsHM :: HashMap Key Int -> UnconsHM Key Int) + producesValidsOnValids (HM.unconsHM :: HashMap Key Int -> UnconsHM Key Int) -- There isn't currently a pUnconsA because I (David Feuer) don't know -- the right way to tell genvalidity-hspec that we only want cases -- where the array is non-empty and all of its elements are non-empty. - ------------------------------------------------------------------------ -- * Test list tests :: [Test] tests = let genValidHelper gen = forAll gen shouldBeValid -- Basic interface - in [ testGroup - "HashMap" - [ testProperty "genValid generates valid values for Leaf" $ - genValidHelper (genValid :: Gen (Leaf Key Int)) - , testProperty "genValid generates valid values for Array" $ - genValidHelper (genValid :: Gen (A.Array Key)) - , testProperty "genValid generates valid values for HashMap" $ - genValidHelper (genValid :: Gen (HashMap Key Int)) - ] - , testGroup - "HashMap" - [ testProperty "singleton produces valid HashMaps" pSingleton - , testProperty "null produces valid Bools" pNull - , testProperty "size produces valid HashMaps" pSize - , testProperty "equal1 produce valid Bools" pEqual1 - , testProperty "equal2 produce valid Bools" pEqual2 - , testProperty "cmp1 produce valid Orderings" pCmp1 - , testProperty "cmp2 produce valid Orderings" pCmp2 - , testProperty "member produces valid HashMaps" pMember - , testProperty "lookup produces valid HashMaps" pLookup - , testProperty - "lookupDefault produces valid HashMaps" - pLookupDefault - , testProperty "insert produces valid HashMaps" pInsert - , testProperty "insertWith produces valid HashMaps" pInsertWith - , testProperty "delete produces valid HashMaps" pDelete - , testProperty "adjust produces valid HashMaps" pAdjust - , testProperty "update produces valid HashMaps" pUpdate - , testProperty "alter produces valid HashMaps" pAlter - , testProperty "alterF produces valid HashMaps" pAlterF - , testProperty "union produces valid HashMaps" pUnion - , testProperty "unionWith produces valid HashMaps" pUnionWith - , testProperty "unionWithKey produces valid HashMaps" pUnionWithKey - , testProperty "unions produces valid HashMaps" pUnions - , testProperty "map produces valid HashMaps" pMap - , testProperty "mapWithKey produces valid HashMaps" pMapWithKey - , testProperty - "traverseWithKey produces valid HashMaps" - pTraverseWithKey - , testProperty "difference produces valid HashMaps" pDifference - , testProperty - "differenceWith produces valid HashMaps" - pDifferenceWith - , testProperty "intersection produces valid HashMaps" pIntersection - , testProperty - "intersectionWith produces valid HashMaps" - pIntersectionWith - , testProperty - "intersectionWithKey produces valid HashMaps" - pIntersectionWithKey - , testProperty "foldl' produces valid HashMaps" pFoldl' - , testProperty - "foldlWithKey' produces valid HashMaps" - pFoldlWithKey' - , testProperty "foldr produces valid HashMaps" pFoldr - , testProperty "foldrWithKey produces valid HashMaps" pFoldrWithKey - , testProperty "filter produces valid HashMaps" pFilter - , testProperty - "filterWithKey produces valid HashMaps" - pFilterWithKey - , testProperty "mapMaybe produces valid HashMaps" pMapMaybe - , testProperty - "mapMaybeWithKey produces valid HashMaps" - pMapMaybeWithKey - , testProperty "keys produces valid lists" pKeys - , testProperty "elems produces valid lists" pElems - , testProperty "toList produces valid lists" pToList - , testProperty "fromList produces valid HashMaps" pFromList - , testProperty "fromListWith produces valid HashMaps" pFromListWith - , testProperty "unconsHM produces valid HashMaps" pUnconsHM - ] - ] - --- TODO keysSet ------------------------------------------------------------------------- --- * Test harness + in [ testGroup + "HashMap" + [ testProperty "genValid generates valid values for Leaf" $ + genValidHelper (genValid :: Gen (Leaf Key Int)) + , testProperty "genValid generates valid values for Array" $ + genValidHelper (genValid :: Gen (A.Array Key)) + , testProperty "genValid generates valid values for HashMap" $ + genValidHelper (genValid :: Gen (HashMap Key Int)) + ] + , testGroup + "HashMap" + [ testProperty "singleton produces valid HashMaps" pSingleton + , testProperty "null produces valid Bools" pNull + , testProperty "size produces valid HashMaps" pSize + , testProperty "equal1 produce valid Bools" pEqual1 + , testProperty "equal2 produce valid Bools" pEqual2 + , testProperty "cmp1 produce valid Orderings" pCmp1 + , testProperty "cmp2 produce valid Orderings" pCmp2 + , testProperty "member produces valid HashMaps" pMember + , testProperty "lookup produces valid HashMaps" pLookup + , testProperty + "lookupDefault produces valid HashMaps" + pLookupDefault + , testProperty "insert produces valid HashMaps" pInsert + , testProperty "insertWith produces valid HashMaps" pInsertWith + , testProperty "delete produces valid HashMaps" pDelete + , testProperty "adjust produces valid HashMaps" pAdjust + , testProperty "update produces valid HashMaps" pUpdate + , testProperty "alter produces valid HashMaps" pAlter + , testProperty "alterF produces valid HashMaps" pAlterF + , testProperty "union produces valid HashMaps" pUnion + , testProperty "unionWith produces valid HashMaps" pUnionWith + , testProperty + "unionWithKey produces valid HashMaps" + pUnionWithKey + , testProperty "unions produces valid HashMaps" pUnions + , testProperty "map produces valid HashMaps" pMap + , testProperty "mapWithKey produces valid HashMaps" pMapWithKey + , testProperty + "traverseWithKey produces valid HashMaps" + pTraverseWithKey + , testProperty "difference produces valid HashMaps" pDifference + , testProperty + "differenceWith produces valid HashMaps" + pDifferenceWith + , testProperty + "intersection produces valid HashMaps" + pIntersection + , testProperty + "intersectionWith produces valid HashMaps" + pIntersectionWith + , testProperty + "intersectionWithKey produces valid HashMaps" + pIntersectionWithKey + , testProperty "foldl' produces valid HashMaps" pFoldl' + , testProperty + "foldlWithKey' produces valid HashMaps" + pFoldlWithKey' + , testProperty "foldr produces valid HashMaps" pFoldr + , testProperty + "foldrWithKey produces valid HashMaps" + pFoldrWithKey + , testProperty "filter produces valid HashMaps" pFilter + , testProperty + "filterWithKey produces valid HashMaps" + pFilterWithKey + , testProperty "mapMaybe produces valid HashMaps" pMapMaybe + , testProperty + "mapMaybeWithKey produces valid HashMaps" + pMapMaybeWithKey + , testProperty "keys produces valid lists" pKeys + , testProperty "elems produces valid lists" pElems + , testProperty "toList produces valid lists" pToList + , testProperty "fromList produces valid HashMaps" pFromList + , testProperty + "fromListWith produces valid HashMaps" + pFromListWith + , testProperty "unconsHM produces valid HashMaps" pUnconsHM + , testProperty "keysSet produces valid HashSets" pKeysSet + ] + ] + main :: IO () main = defaultMain tests From cbf034b2ca0e25ed3d928d8a9e66a3760d40704d Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 29 Oct 2018 12:04:47 +0100 Subject: [PATCH 54/69] Tried to fix the ghc-8.0.2 part of the CI --- Data/HashMap/Base.hs | 9 ++++----- newer-stack.yaml | 3 --- other.yaml | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/Validity.hs | 4 ++-- 4 files changed, 48 insertions(+), 10 deletions(-) delete mode 100644 newer-stack.yaml create mode 100644 other.yaml diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index daf18c47..81426d3e 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -188,7 +188,6 @@ instance (NFData k, NFData v) => NFData (Leaf k v) where -- | A map from keys to values. A map cannot contain duplicate keys; -- each key can map to at most one value. --- TODO document all invariants data HashMap k v = Empty | BitmapIndexed !Bitmap !(A.Array (HashMap k v)) @@ -568,7 +567,7 @@ lookup# k m = lookupCont (\_ -> (# (# #) | #)) (\v -> (# | v #)) (hashWithSalt d #else -lookup k m = lookupCont (\_ -> Nothing) (\v _i -> Just v) (hashWithSalt s k) defaultSalt k m +lookup k m = lookupCont (\_ -> Nothing) (\v -> Just v) (hashWithSalt defaultSalt k) defaultSalt k m {-# INLINABLE lookup #-} #endif @@ -587,7 +586,7 @@ lookup' h s k m = case lookupWithRes# h s k m of (# | a #) -> Just a {-# INLINE lookup' #-} #else -lookup' h s k m = lookupCont (\_ -> Nothing) (\v _i -> Just v) h s k m +lookup' h s k m = lookupCont (\_ -> Nothing) (\v -> Just v) h s k m {-# INLINABLE lookup' #-} #endif @@ -1847,10 +1846,10 @@ unconsA ary = data UnconsA k v = NowEmptyA !(Leaf k v) - | UnconsedFromFirstA + | UnconsedFromFirstA -- Got an element out of the array, but leave the array. !(Leaf k v) -- The leaf that was un-consed !(A.Array (HashMap k v)) -- The leftover array - | RemovedFirstA + | RemovedFirstA -- Got an element out of the array, and removed the array because it became empty. !(Leaf k v) -- The leaf that was un-consed !(A.Array (HashMap k v)) -- The leftover array deriving Show diff --git a/newer-stack.yaml b/newer-stack.yaml deleted file mode 100644 index 26f10a32..00000000 --- a/newer-stack.yaml +++ /dev/null @@ -1,3 +0,0 @@ -resolver: lts-12.12 -packages: -- . diff --git a/other.yaml b/other.yaml new file mode 100644 index 00000000..b1d9d75f --- /dev/null +++ b/other.yaml @@ -0,0 +1,42 @@ +resolver: ghc-8.0.2 +packages: +- . +extra-deps: +- hashable-1.2.7.0@sha256:03b6836ca9cd3ad0e5a2f3cce989b001dd0e05f306a873db3196037adb30e0a4 +- text-1.2.3.1@sha256:fc9719142e5cdd6f254b3f4831b133d3fad697ae9b54fe26424f0e023c8cc87d +- ChasingBottoms-1.3.1.5@sha256:22fcd1eb29164e5b298904ad3d5b81c082bb9ffb15ebbd134bbfa743ee70ff97 +- HUnit-1.6.0.0@sha256:8014b27c884becd8041214a1ab263fa92244ca62700a911aff604a1047869298 +- QuickCheck-2.12.6.1@sha256:95b4e3b01033bf7d2ad21d0085266dba378477ac88e3d11ff238fc9db2575e6d +- containers-0.6.0.1@sha256:50024f330653c702a339f7743522b3c2a4fff9c6bf5a7071affe270da2ee649a +- genvalidity-0.6.1.0@sha256:bac6bae7671c2c8198eec4f27d92310de0d6b88aca4c4f56dae3ab2a987f54c1 +- genvalidity-hspec-0.6.2.0@sha256:d74da398f241b2877613c471f3b4e02801430f0647f0fb2817f2779a636341ad +- test-framework-0.8.2.0@sha256:8ce5620cddac0deb0b29d46ceaaf6810b4ac5b2b8a620687de336fe1ec0dc6a9 +- test-framework-hunit-0.3.0.2@sha256:7fd007e9cb082cd64a2213a6d36acf057f7d6df6b5343a088e81b2b3a9a23545 +- test-framework-quickcheck2-0.3.0.5@sha256:7e306ac4802fcdc294298335a13335db3ee75face72f1cf6f3bff6590c7d9b5c +- validity-0.8.0.0@sha256:3712b48461dc58f0e1aecb7ae016c6702299a875282bb7b51e75eca2a65e8b69 +- ansi-terminal-0.8.1@sha256:4cebbd2034dcd9029369c0a49c72666be8dacd11a2827d33bcfa539f2933614a +- ansi-wl-pprint-0.6.8.2@sha256:8cb4b04401948c08feafa49e7ee0982b8492d599163107b6934093f1a912f54e +- call-stack-0.1.0@sha256:3453a0c5ed3a7a7de0cc0703907e05bd251c766cce8a38efe41b7188d228e3fa +- erf-2.0.0.0@sha256:11c5d3747d17b589009cbe9656ca792e4b5b2560d4aa44f6f1e80044794b1a8b +- extensible-exceptions-0.1.1.4@sha256:eb5fe684a7ffe8d1ed2ed6cdaec7dfb29efc780811ea7158a64edc2abc516f47 +- genvalidity-property-0.2.1.1@sha256:3ee8a594ce8b10b4624c7c8fe5aeabf573728a6ca36a92e227fdb6a9d22e862b +- hostname-1.0@sha256:8203b6ecd14ca1ef12f73a471b0a6a4be3ad4568d8b84f2bc4bc9e0abb8c4153 +- hspec-2.5.8@sha256:7fae2ebfe990f726e12980980b8c1af32755048a68e51e814095c89342634441 +- hspec-core-2.5.8@sha256:94e5259328977b867d73eba1bd667e34cfbc2c68b46009cbdc54589b483a87e3 +- mtl-2.2.2@sha256:1050fb71acd9f5d67da7d992583f5bd0eb14407b9dc7acc122af1b738b706ca3 +- old-locale-1.0.0.7@sha256:fa998be2c7e00cd26a6e9075bea790caaf3932caa3e9497ad69bc20380dd6911 +- random-1.1@sha256:7b67624fd76ddf97c206de0801dc7e888097e9d572974be9b9ea6551d76965df +- regex-posix-0.95.2@sha256:4fba55cdae7fb6c11ee2f5e823e56cc5fc30d7041edeef4c81f7f26b9235d624 +- syb-0.7@sha256:9c559edeef6b9eb1362d7e16fbb15e786be871050cf085a46fcead282bb86e89 +- tf-random-0.5@sha256:14012837d0f0e18fdbbe3d56e67da8622ee5e20b180abce952dd50bd9f36b326 +- xml-1.3.14@sha256:e52f778760b7def64ced0f39ad2022d839baca1e3d730cdc33ef89f66145b26d +- clock-0.7.2@sha256:c5c2514aead260101c7528d10bf0f41d9d1d7b919136e2dff77a57ec16740bd2 +- colour-2.3.4@sha256:90257dac3d9149b2b384184638b610bf95d7d180e1e4d3a329e038f18c6b7859 +- hspec-discover-2.5.8@sha256:573e0b4be3511d302bdfd532dff5bb8a1f85aca8359c2949fbfe6dd07bd1ecb7 +- hspec-expectations-0.8.2@sha256:e2db24881baadc2d9d23b03cb629e80dcbda89a6b04ace9adb5f4d02ef8b31aa +- primitive-0.6.4.0@sha256:5b6a2c3cc70a35aabd4565fcb9bb1dd78fe2814a36e62428a9a1aae8c32441a1 +- quickcheck-io-0.2.0@sha256:7bf0b68fb90873825eb2e5e958c1b76126dcf984debb998e81673e6d837e0b2d +- regex-base-0.93.2@sha256:eb820674e31c6b8c87a45d09115ba518c7cd0844cffc5437291289592681070e +- setenv-0.1.1.3@sha256:c5916ac0d2a828473cd171261328a290afe0abd799db1ac8c310682fe778c45b +- stm-2.5.0.0@sha256:1fb8bd117550d560d1b33a6404e27fdb090e70921e0f6434722cdbbce20d8256 + diff --git a/tests/Validity.hs b/tests/Validity.hs index 5b176031..9b309202 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -71,7 +71,7 @@ instance Validity a => Validity (A.Array a) where instance (Eq k, Hashable k, Validity k, Validity v) => Validity (UnconsHM k v) where - validate UnconsEmptyHM = decorate "UnconsEmptyHM" valid + validate UnconsEmptyHM = decorate "UnconsEmptyHM" mempty validate (UnconsedHM l hm) = decorate "UnconsedHM" $ mconcat [decorate "Leaf" $ validate l, decorate "Map" $ validate hm] @@ -216,7 +216,7 @@ instance GenUnchecked a => GenUnchecked (A.Array a) where genUnchecked = do l <- genUnchecked pure $ A.fromList (length l) l - shrinkUnchecked _ = [] -- TODO: write shrinking + shrinkUnchecked = fmap (\l -> A.fromList (length l) l) . shrinkUnchecked . A.toList instance GenValid a => GenValid (A.Array a) where genValid = do From b804e5c625ca2748cd426ec3e10d07e80f44cb61 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 29 Oct 2018 12:10:46 +0100 Subject: [PATCH 55/69] Woops --- other.yaml | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 other.yaml diff --git a/other.yaml b/other.yaml deleted file mode 100644 index b1d9d75f..00000000 --- a/other.yaml +++ /dev/null @@ -1,42 +0,0 @@ -resolver: ghc-8.0.2 -packages: -- . -extra-deps: -- hashable-1.2.7.0@sha256:03b6836ca9cd3ad0e5a2f3cce989b001dd0e05f306a873db3196037adb30e0a4 -- text-1.2.3.1@sha256:fc9719142e5cdd6f254b3f4831b133d3fad697ae9b54fe26424f0e023c8cc87d -- ChasingBottoms-1.3.1.5@sha256:22fcd1eb29164e5b298904ad3d5b81c082bb9ffb15ebbd134bbfa743ee70ff97 -- HUnit-1.6.0.0@sha256:8014b27c884becd8041214a1ab263fa92244ca62700a911aff604a1047869298 -- QuickCheck-2.12.6.1@sha256:95b4e3b01033bf7d2ad21d0085266dba378477ac88e3d11ff238fc9db2575e6d -- containers-0.6.0.1@sha256:50024f330653c702a339f7743522b3c2a4fff9c6bf5a7071affe270da2ee649a -- genvalidity-0.6.1.0@sha256:bac6bae7671c2c8198eec4f27d92310de0d6b88aca4c4f56dae3ab2a987f54c1 -- genvalidity-hspec-0.6.2.0@sha256:d74da398f241b2877613c471f3b4e02801430f0647f0fb2817f2779a636341ad -- test-framework-0.8.2.0@sha256:8ce5620cddac0deb0b29d46ceaaf6810b4ac5b2b8a620687de336fe1ec0dc6a9 -- test-framework-hunit-0.3.0.2@sha256:7fd007e9cb082cd64a2213a6d36acf057f7d6df6b5343a088e81b2b3a9a23545 -- test-framework-quickcheck2-0.3.0.5@sha256:7e306ac4802fcdc294298335a13335db3ee75face72f1cf6f3bff6590c7d9b5c -- validity-0.8.0.0@sha256:3712b48461dc58f0e1aecb7ae016c6702299a875282bb7b51e75eca2a65e8b69 -- ansi-terminal-0.8.1@sha256:4cebbd2034dcd9029369c0a49c72666be8dacd11a2827d33bcfa539f2933614a -- ansi-wl-pprint-0.6.8.2@sha256:8cb4b04401948c08feafa49e7ee0982b8492d599163107b6934093f1a912f54e -- call-stack-0.1.0@sha256:3453a0c5ed3a7a7de0cc0703907e05bd251c766cce8a38efe41b7188d228e3fa -- erf-2.0.0.0@sha256:11c5d3747d17b589009cbe9656ca792e4b5b2560d4aa44f6f1e80044794b1a8b -- extensible-exceptions-0.1.1.4@sha256:eb5fe684a7ffe8d1ed2ed6cdaec7dfb29efc780811ea7158a64edc2abc516f47 -- genvalidity-property-0.2.1.1@sha256:3ee8a594ce8b10b4624c7c8fe5aeabf573728a6ca36a92e227fdb6a9d22e862b -- hostname-1.0@sha256:8203b6ecd14ca1ef12f73a471b0a6a4be3ad4568d8b84f2bc4bc9e0abb8c4153 -- hspec-2.5.8@sha256:7fae2ebfe990f726e12980980b8c1af32755048a68e51e814095c89342634441 -- hspec-core-2.5.8@sha256:94e5259328977b867d73eba1bd667e34cfbc2c68b46009cbdc54589b483a87e3 -- mtl-2.2.2@sha256:1050fb71acd9f5d67da7d992583f5bd0eb14407b9dc7acc122af1b738b706ca3 -- old-locale-1.0.0.7@sha256:fa998be2c7e00cd26a6e9075bea790caaf3932caa3e9497ad69bc20380dd6911 -- random-1.1@sha256:7b67624fd76ddf97c206de0801dc7e888097e9d572974be9b9ea6551d76965df -- regex-posix-0.95.2@sha256:4fba55cdae7fb6c11ee2f5e823e56cc5fc30d7041edeef4c81f7f26b9235d624 -- syb-0.7@sha256:9c559edeef6b9eb1362d7e16fbb15e786be871050cf085a46fcead282bb86e89 -- tf-random-0.5@sha256:14012837d0f0e18fdbbe3d56e67da8622ee5e20b180abce952dd50bd9f36b326 -- xml-1.3.14@sha256:e52f778760b7def64ced0f39ad2022d839baca1e3d730cdc33ef89f66145b26d -- clock-0.7.2@sha256:c5c2514aead260101c7528d10bf0f41d9d1d7b919136e2dff77a57ec16740bd2 -- colour-2.3.4@sha256:90257dac3d9149b2b384184638b610bf95d7d180e1e4d3a329e038f18c6b7859 -- hspec-discover-2.5.8@sha256:573e0b4be3511d302bdfd532dff5bb8a1f85aca8359c2949fbfe6dd07bd1ecb7 -- hspec-expectations-0.8.2@sha256:e2db24881baadc2d9d23b03cb629e80dcbda89a6b04ace9adb5f4d02ef8b31aa -- primitive-0.6.4.0@sha256:5b6a2c3cc70a35aabd4565fcb9bb1dd78fe2814a36e62428a9a1aae8c32441a1 -- quickcheck-io-0.2.0@sha256:7bf0b68fb90873825eb2e5e958c1b76126dcf984debb998e81673e6d837e0b2d -- regex-base-0.93.2@sha256:eb820674e31c6b8c87a45d09115ba518c7cd0844cffc5437291289592681070e -- setenv-0.1.1.3@sha256:c5916ac0d2a828473cd171261328a290afe0abd799db1ac8c310682fe778c45b -- stm-2.5.0.0@sha256:1fb8bd117550d560d1b33a6404e27fdb090e70921e0f6434722cdbbce20d8256 - From 35bd6794945d6cb93a6b18680dd7aaa3ba2e9cdd Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 29 Oct 2018 15:21:09 +0100 Subject: [PATCH 56/69] Added a check to the validity instance which exposes another problem --- tests/Validity.hs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/Validity.hs b/tests/Validity.hs index 9b309202..06f9f70e 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -13,6 +13,7 @@ module Main import GHC.Generics (Generic) import Data.Bits (popCount) +import Data.Functor import Data.Hashable (Hashable(hashWithSalt)) import Data.List (nub) import Data.Validity @@ -69,7 +70,7 @@ instance (Validity k, Validity v) => Validity (Leaf k v) where instance Validity a => Validity (A.Array a) where validate a = annotate (A.toList a) "The array elements" -instance (Eq k, Hashable k, Validity k, Validity v) => +instance (Eq k, Hashable k, Validity k, Eq v, Validity v) => Validity (UnconsHM k v) where validate UnconsEmptyHM = decorate "UnconsEmptyHM" mempty validate (UnconsedHM l hm) = @@ -78,10 +79,16 @@ instance (Eq k, Hashable k, Validity k, Validity v) => instance (Eq k, Hashable k, Validity k, Validity v) => Validity (HashMap k v) where - validate = go HM.defaultSalt 0 + validate hm = + mconcat + [ go HM.defaultSalt 0 hm + , check + (HM.fromList (HM.toList (hm $> ())) == (hm $> ())) + "fromList and toList are inverses for this hashmap." + ] where - go s bs hm = - case hm of + go s bs hm_ = + case hm_ of Empty -> mempty (Leaf h l@(L k _)) -> decorate "Leaf" $ @@ -216,7 +223,8 @@ instance GenUnchecked a => GenUnchecked (A.Array a) where genUnchecked = do l <- genUnchecked pure $ A.fromList (length l) l - shrinkUnchecked = fmap (\l -> A.fromList (length l) l) . shrinkUnchecked . A.toList + shrinkUnchecked = + fmap (\l -> A.fromList (length l) l) . shrinkUnchecked . A.toList instance GenValid a => GenValid (A.Array a) where genValid = do @@ -257,7 +265,7 @@ instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (HashMap k v) where -- It turns out it's almost impossible to write this instance without using internals. -- This is good-enough for now. -instance (Eq k, Hashable k, GenValid k, GenValid v) => +instance (Eq k, Hashable k, GenValid k, Eq v, GenValid v) => GenValid (HashMap k v) where genValid = HM.fromList <$> genValid shrinkValid hm = HM.fromList <$> shrinkValid (HM.toList hm) From 886856b1915b72cabf053047131294c7e52d6076 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Mon, 29 Oct 2018 15:24:04 +0100 Subject: [PATCH 57/69] got rid of compiler warnings --- tests/Validity.hs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Validity.hs b/tests/Validity.hs index 06f9f70e..fa4bbaed 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -104,13 +104,13 @@ instance (Eq k, Hashable k, Validity k, Validity v) => mconcat [ annotate bm "Bitmap" , decorate "Array" $ - decorateList (zip [0 ..] $ A.toList a) $ \(ix, hm_) -> + decorateList (zip [0 ..] $ A.toList a) $ \(ix, hm__) -> mconcat - [ go s (bs + HM.bitsPerSubkey) hm_ + [ go s (bs + HM.bitsPerSubkey) hm__ , check - (not $ HM.null hm_) + (not $ HM.null hm__) "The sub HashMap is not empty" - , decorateList (HM.keys hm_) $ \k -> + , decorateList (HM.keys hm__) $ \k -> flip check "The hash, when masked with the current bitmask, of the key, produces the index in the array where the hashmap is found" $ @@ -129,13 +129,13 @@ instance (Eq k, Hashable k, Validity k, Validity v) => decorate "Full" $ mconcat [ decorate "Array" $ - decorateList (zip [0 ..] $ A.toList a) $ \(ix, hm_) -> + decorateList (zip [0 ..] $ A.toList a) $ \(ix, hm__) -> mconcat - [ go s (bs + HM.bitsPerSubkey) hm_ + [ go s (bs + HM.bitsPerSubkey) hm__ , check - (not $ HM.null hm_) + (not $ HM.null hm__) "The sub HashMap is not empty" - , decorateList (HM.keys hm_) $ \k -> + , decorateList (HM.keys hm__) $ \k -> flip check "The hash, when masked with the current bitmask, of the key, produces the index in the array where the hashmap is found" $ From cf55ac474b1e171436e2984d13e72c1eb4f3f665 Mon Sep 17 00:00:00 2001 From: David Feuer Date: Mon, 29 Oct 2018 12:45:39 -0400 Subject: [PATCH 58/69] Collapse nodes properly in unconsHM When a `BitmapIndexed` has an array with exactly one element, that element must not be a leaf or a collision. I missed that invariant in the definition of `unconsHM`. Fix that, and extend the validity check to verify the invariant. --- Data/HashMap/Base.hs | 48 ++++++--- tests/Validity.hs | 228 +++++++++++++++++++++---------------------- 2 files changed, 147 insertions(+), 129 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 81426d3e..45972643 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -80,7 +80,7 @@ module Data.HashMap.Base , fromList , fromListWith - -- Internals used by the strict version + -- Internals used by the strict version or tests , Hash , Salt , Bitmap @@ -119,6 +119,7 @@ module Data.HashMap.Base , unconsHM , ptrEq , adjust# + , isLeafOrCollision ) where #if __GLASGOW_HASKELL__ < 710 @@ -1846,10 +1847,12 @@ unconsA ary = data UnconsA k v = NowEmptyA !(Leaf k v) - | UnconsedFromFirstA -- Got an element out of the array, but leave the array. + -- Got an entry from the array without changing its length + | UnconsedFromFirstA !(Leaf k v) -- The leaf that was un-consed !(A.Array (HashMap k v)) -- The leftover array - | RemovedFirstA -- Got an element out of the array, and removed the array because it became empty. + -- Got an entry from the array, shortening it by one element. + | RemovedFirstA !(Leaf k v) -- The leaf that was un-consed !(A.Array (HashMap k v)) -- The leftover array deriving Show @@ -1866,20 +1869,41 @@ unconsHM hm = case hm of Leaf _ l -> UnconsedHM l Empty BitmapIndexed bm ary' -> case unconsA ary' of NowEmptyA l -> UnconsedHM l Empty - UnconsedFromFirstA l a' -> UnconsedHM l (BitmapIndexed bm a') - RemovedFirstA l a' -> shortened l bm a' + UnconsedFromFirstA l a' -> UnconsedHM l (collapse_maybe bm a') + RemovedFirstA l a' -> UnconsedHM l (collapse_maybe (shorten_bm bm) a') Full ary' -> case unconsA ary' of - NowEmptyA l -> UnconsedHM l Empty - UnconsedFromFirstA l a' -> UnconsedHM l (Full a') - RemovedFirstA l a' -> shortened l fullNodeMask a' + NowEmptyA l -> + UnconsedHM l Empty + UnconsedFromFirstA l a' -> + UnconsedHM l (Full a') + RemovedFirstA l a' -> + UnconsedHM l $ BitmapIndexed (shorten_bm fullNodeMask) a' Collision h l1 l2 hm' -> UnconsedHM l1 $ case unconsHM hm' of UnconsEmptyHM -> Leaf h l2 UnconsedHM l3 hm'' -> Collision h l2 l3 hm'' where - -- The array was shortened, so we need to remove the first - -- element from the bitmap. - shortened l bm ar = - UnconsedHM l $ BitmapIndexed (bm .&. (bm - 1)) ar + -- When the array is shortened, we need to delete the + -- first element (the lowest set bit) from the bitmap. + shorten_bm bm = bm .&. (bm - 1) + + -- We must never create a BitmapIndexed with exactly + -- one element whose sole element is a leaf or collision. + -- In such cases, we instead collapse a level, removing + -- the BitmapIndexed. This "smart constructor" takes care + -- of potential collapse. + -- + -- Note: we *could* arrange to catch this condition earlier, + -- as delete' does, preventing the allocation of an unnecessary + -- one-element array. But to reach this code we must be dealing with a + -- collision bucket that has at least four elements. That puts us + -- well off the fast path already. + collapse_maybe bm ar + | popCount bm == 1 -- The array has exactly one element. + , let the_elem = A.index ar 0 + , isLeafOrCollision the_elem + = the_elem + | otherwise + = BitmapIndexed bm ar ------------------------------------------------------------------------ -- Manually unrolled loops diff --git a/tests/Validity.hs b/tests/Validity.hs index fa4bbaed..6abfc4fd 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -24,20 +24,7 @@ import qualified Data.HashMap.Lazy as HM #endif import qualified Data.HashMap.Array as A import Data.HashMap.Base (HashMap(..), Leaf(..), UnconsHM(..)) -import qualified Data.HashMap.Base as HM - ( bitsPerSubkey - , cmp1 - , cmp2 - , defaultSalt - , equal1 - , equal2 - , hashWithSalt - , index - , mask - , nextSalt - , sparseIndex - , unconsHM - ) +import qualified Data.HashMap.Base as HMB import qualified Data.HashSet as HS import Data.HashSet (HashSet) @@ -70,7 +57,7 @@ instance (Validity k, Validity v) => Validity (Leaf k v) where instance Validity a => Validity (A.Array a) where validate a = annotate (A.toList a) "The array elements" -instance (Eq k, Hashable k, Validity k, Eq v, Validity v) => +instance (Eq k, Hashable k, Validity k, Validity v) => Validity (UnconsHM k v) where validate UnconsEmptyHM = decorate "UnconsEmptyHM" mempty validate (UnconsedHM l hm) = @@ -81,7 +68,7 @@ instance (Eq k, Hashable k, Validity k, Validity v) => Validity (HashMap k v) where validate hm = mconcat - [ go HM.defaultSalt 0 hm + [ go HMB.defaultSalt 0 hm , check (HM.fromList (HM.toList (hm $> ())) == (hm $> ())) "fromList and toList are inverses for this hashmap." @@ -96,7 +83,7 @@ instance (Eq k, Hashable k, Validity k, Validity v) => [ annotate h "Hash" , annotate l "Leaf" , check - (HM.hashWithSalt s k == h) + (HMB.hashWithSalt s k == h) "The hash is correct." ] (BitmapIndexed bm a) -> @@ -106,7 +93,7 @@ instance (Eq k, Hashable k, Validity k, Validity v) => , decorate "Array" $ decorateList (zip [0 ..] $ A.toList a) $ \(ix, hm__) -> mconcat - [ go s (bs + HM.bitsPerSubkey) hm__ + [ go s (bs + HMB.bitsPerSubkey) hm__ , check (not $ HM.null hm__) "The sub HashMap is not empty" @@ -114,8 +101,8 @@ instance (Eq k, Hashable k, Validity k, Validity v) => flip check "The hash, when masked with the current bitmask, of the key, produces the index in the array where the hashmap is found" $ - let h = HM.hashWithSalt s k - in HM.sparseIndex bm (HM.mask h bs) == + let h = HMB.hashWithSalt s k + in HMB.sparseIndex bm (HMB.mask h bs) == ix ] , check @@ -124,6 +111,13 @@ instance (Eq k, Hashable k, Validity k, Validity v) => , check (uniques $ concatMap HM.keys $ A.toList a) "The keys are unique." + , check + (A.length a >= 1) + "The array has at least one element." + , check + (A.length a > 1 || not (HMB.isLeafOrCollision (A.index a 0))) $ + "If the array has only one element, then that element is" ++ + "not a leaf or collision." ] (Full a) -> decorate "Full" $ @@ -131,7 +125,7 @@ instance (Eq k, Hashable k, Validity k, Validity v) => [ decorate "Array" $ decorateList (zip [0 ..] $ A.toList a) $ \(ix, hm__) -> mconcat - [ go s (bs + HM.bitsPerSubkey) hm__ + [ go s (bs + HMB.bitsPerSubkey) hm__ , check (not $ HM.null hm__) "The sub HashMap is not empty" @@ -139,11 +133,11 @@ instance (Eq k, Hashable k, Validity k, Validity v) => flip check "The hash, when masked with the current bitmask, of the key, produces the index in the array where the hashmap is found" $ - let h = HM.hashWithSalt s k - in HM.index h bs == ix + let h = HMB.hashWithSalt s k + in HMB.index h bs == ix ] , check - (A.length a == 2 ^ HM.bitsPerSubkey) + (A.length a == 2 ^ HMB.bitsPerSubkey) "There are 2 ^ bitsPerSubkey values in the array." , check (uniques $ concatMap HM.keys $ A.toList a) @@ -156,10 +150,10 @@ instance (Eq k, Hashable k, Validity k, Validity v) => , annotate l1 "The first collision" , annotate l2 "The second collision" , check - (HM.hashWithSalt s k1 == h) + (HMB.hashWithSalt s k1 == h) "The hash of the first collision is correct." , check - (HM.hashWithSalt s k2 == h) + (HMB.hashWithSalt s k2 == h) "The hash of the second collision is correct." , check (k1 /= k2) @@ -168,14 +162,14 @@ instance (Eq k, Hashable k, Validity k, Validity v) => (uniques (k1 : k2 : HM.keys hm')) "The keys are unique." , decorate "The recursive HashMap" $ - go (HM.nextSalt s) 0 hm' + go (HMB.nextSalt s) 0 hm' , check - (all (\k -> HM.hashWithSalt s k == h) + (all (\k -> HMB.hashWithSalt s k == h) (HM.keys hm')) "All recursive keys hash to the hash within the collision, using the salt at this level." ] -instance (Eq v, Hashable v, Validity v) => Validity (HashSet v) where +instance (Eq k, Hashable k, Validity k) => Validity (HashSet k) where validate hs = decorate "asMap" $ validate $ HS.toMap hs uniques :: Eq a => [a] -> Bool @@ -265,11 +259,11 @@ instance (GenUnchecked k, GenUnchecked v) => GenUnchecked (HashMap k v) where -- It turns out it's almost impossible to write this instance without using internals. -- This is good-enough for now. -instance (Eq k, Hashable k, GenValid k, Eq v, GenValid v) => +instance (Eq k, Hashable k, GenValid k, GenValid v) => GenValid (HashMap k v) where genValid = HM.fromList <$> genValid shrinkValid hm = HM.fromList <$> shrinkValid (HM.toList hm) - -- TODO improve the shrinking + -- TODO improve the shrinking, maybe. -- Key type that generates more hash collisions. newtype Key = K @@ -304,22 +298,22 @@ pSize = producesValidsOnValids (HM.size :: HM.HashMap Key Int -> Int) pEqual1 :: Fun (Int, Int) Bool -> Property pEqual1 f = producesValidsOnValids2 - (HM.equal1 (applyFun2 f) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Bool) + (HMB.equal1 (applyFun2 f) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Bool) pEqual2 :: Fun (Key, Key) Bool -> Fun (Int, Int) Bool -> Property pEqual2 f g = producesValidsOnValids2 - (HM.equal2 (applyFun2 f) (applyFun2 g) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Bool) + (HMB.equal2 (applyFun2 f) (applyFun2 g) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Bool) pCmp1 :: Fun (Int, Int) Ordering -> Property pCmp1 f = producesValidsOnValids2 - (HM.cmp1 (applyFun2 f) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Ordering) + (HMB.cmp1 (applyFun2 f) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Ordering) pCmp2 :: Fun (Key, Key) Ordering -> Fun (Int, Int) Ordering -> Property pCmp2 f g = producesValidsOnValids2 - (HM.cmp2 (applyFun2 f) (applyFun2 g) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Ordering) + (HMB.cmp2 (applyFun2 f) (applyFun2 g) :: HM.HashMap Key Int -> HM.HashMap Key Int -> Ordering) pMember :: Property pMember = @@ -492,7 +486,7 @@ pKeysSet = producesValidsOnValids (HM.keysSet :: HashMap Key Int -> HashSet Key) pUnconsHM :: Property pUnconsHM = - producesValidsOnValids (HM.unconsHM :: HashMap Key Int -> UnconsHM Key Int) + producesValidsOnValids (HMB.unconsHM :: HashMap Key Int -> UnconsHM Key Int) -- There isn't currently a pUnconsA because I (David Feuer) don't know -- the right way to tell genvalidity-hspec that we only want cases @@ -503,87 +497,87 @@ tests :: [Test] tests = let genValidHelper gen = forAll gen shouldBeValid -- Basic interface - in [ testGroup - "HashMap" - [ testProperty "genValid generates valid values for Leaf" $ - genValidHelper (genValid :: Gen (Leaf Key Int)) - , testProperty "genValid generates valid values for Array" $ - genValidHelper (genValid :: Gen (A.Array Key)) - , testProperty "genValid generates valid values for HashMap" $ - genValidHelper (genValid :: Gen (HashMap Key Int)) - ] - , testGroup - "HashMap" - [ testProperty "singleton produces valid HashMaps" pSingleton - , testProperty "null produces valid Bools" pNull - , testProperty "size produces valid HashMaps" pSize - , testProperty "equal1 produce valid Bools" pEqual1 - , testProperty "equal2 produce valid Bools" pEqual2 - , testProperty "cmp1 produce valid Orderings" pCmp1 - , testProperty "cmp2 produce valid Orderings" pCmp2 - , testProperty "member produces valid HashMaps" pMember - , testProperty "lookup produces valid HashMaps" pLookup - , testProperty - "lookupDefault produces valid HashMaps" - pLookupDefault - , testProperty "insert produces valid HashMaps" pInsert - , testProperty "insertWith produces valid HashMaps" pInsertWith - , testProperty "delete produces valid HashMaps" pDelete - , testProperty "adjust produces valid HashMaps" pAdjust - , testProperty "update produces valid HashMaps" pUpdate - , testProperty "alter produces valid HashMaps" pAlter - , testProperty "alterF produces valid HashMaps" pAlterF - , testProperty "union produces valid HashMaps" pUnion - , testProperty "unionWith produces valid HashMaps" pUnionWith - , testProperty - "unionWithKey produces valid HashMaps" - pUnionWithKey - , testProperty "unions produces valid HashMaps" pUnions - , testProperty "map produces valid HashMaps" pMap - , testProperty "mapWithKey produces valid HashMaps" pMapWithKey - , testProperty - "traverseWithKey produces valid HashMaps" - pTraverseWithKey - , testProperty "difference produces valid HashMaps" pDifference - , testProperty - "differenceWith produces valid HashMaps" - pDifferenceWith - , testProperty - "intersection produces valid HashMaps" - pIntersection - , testProperty - "intersectionWith produces valid HashMaps" - pIntersectionWith - , testProperty - "intersectionWithKey produces valid HashMaps" - pIntersectionWithKey - , testProperty "foldl' produces valid HashMaps" pFoldl' - , testProperty - "foldlWithKey' produces valid HashMaps" - pFoldlWithKey' - , testProperty "foldr produces valid HashMaps" pFoldr - , testProperty - "foldrWithKey produces valid HashMaps" - pFoldrWithKey - , testProperty "filter produces valid HashMaps" pFilter - , testProperty - "filterWithKey produces valid HashMaps" - pFilterWithKey - , testProperty "mapMaybe produces valid HashMaps" pMapMaybe - , testProperty - "mapMaybeWithKey produces valid HashMaps" - pMapMaybeWithKey - , testProperty "keys produces valid lists" pKeys - , testProperty "elems produces valid lists" pElems - , testProperty "toList produces valid lists" pToList - , testProperty "fromList produces valid HashMaps" pFromList - , testProperty - "fromListWith produces valid HashMaps" - pFromListWith - , testProperty "unconsHM produces valid HashMaps" pUnconsHM - , testProperty "keysSet produces valid HashSets" pKeysSet - ] - ] + in [ testGroup + "HashMap" + [ testProperty "genValid generates valid values for Leaf" $ + genValidHelper (genValid :: Gen (Leaf Key Int)) + , testProperty "genValid generates valid values for Array" $ + genValidHelper (genValid :: Gen (A.Array Key)) + , testProperty "genValid generates valid values for HashMap" $ + genValidHelper (genValid :: Gen (HashMap Key Int)) + ] + , testGroup + "HashMap" + [ testProperty "singleton produces valid HashMaps" pSingleton + , testProperty "null produces valid Bools" pNull + , testProperty "size produces valid HashMaps" pSize + , testProperty "equal1 produce valid Bools" pEqual1 + , testProperty "equal2 produce valid Bools" pEqual2 + , testProperty "cmp1 produce valid Orderings" pCmp1 + , testProperty "cmp2 produce valid Orderings" pCmp2 + , testProperty "member produces valid HashMaps" pMember + , testProperty "lookup produces valid HashMaps" pLookup + , testProperty + "lookupDefault produces valid HashMaps" + pLookupDefault + , testProperty "insert produces valid HashMaps" pInsert + , testProperty "insertWith produces valid HashMaps" pInsertWith + , testProperty "delete produces valid HashMaps" pDelete + , testProperty "adjust produces valid HashMaps" pAdjust + , testProperty "update produces valid HashMaps" pUpdate + , testProperty "alter produces valid HashMaps" pAlter + , testProperty "alterF produces valid HashMaps" pAlterF + , testProperty "union produces valid HashMaps" pUnion + , testProperty "unionWith produces valid HashMaps" pUnionWith + , testProperty + "unionWithKey produces valid HashMaps" + pUnionWithKey + , testProperty "unions produces valid HashMaps" pUnions + , testProperty "map produces valid HashMaps" pMap + , testProperty "mapWithKey produces valid HashMaps" pMapWithKey + , testProperty + "traverseWithKey produces valid HashMaps" + pTraverseWithKey + , testProperty "difference produces valid HashMaps" pDifference + , testProperty + "differenceWith produces valid HashMaps" + pDifferenceWith + , testProperty + "intersection produces valid HashMaps" + pIntersection + , testProperty + "intersectionWith produces valid HashMaps" + pIntersectionWith + , testProperty + "intersectionWithKey produces valid HashMaps" + pIntersectionWithKey + , testProperty "foldl' produces valid HashMaps" pFoldl' + , testProperty + "foldlWithKey' produces valid HashMaps" + pFoldlWithKey' + , testProperty "foldr produces valid HashMaps" pFoldr + , testProperty + "foldrWithKey produces valid HashMaps" + pFoldrWithKey + , testProperty "filter produces valid HashMaps" pFilter + , testProperty + "filterWithKey produces valid HashMaps" + pFilterWithKey + , testProperty "mapMaybe produces valid HashMaps" pMapMaybe + , testProperty + "mapMaybeWithKey produces valid HashMaps" + pMapMaybeWithKey + , testProperty "keys produces valid lists" pKeys + , testProperty "elems produces valid lists" pElems + , testProperty "toList produces valid lists" pToList + , testProperty "fromList produces valid HashMaps" pFromList + , testProperty + "fromListWith produces valid HashMaps" + pFromListWith + , testProperty "unconsHM produces valid HashMaps" pUnconsHM + , testProperty "keysSet produces valid HashSets" pKeysSet + ] + ] main :: IO () main = defaultMain tests From 364a3cb58aa4ccf284bf1f291fe726e373831576 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 30 Oct 2018 07:29:20 +0100 Subject: [PATCH 59/69] tried to fix travis --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 040ea2fc..8440c99a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,16 +30,16 @@ matrix: include: - compiler: "ghc-7.8.4" # env: TEST=--disable-tests BENCH=--disable-benchmarks - addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.0,ghc-7.8.4], sources: [hvr-ghc]}} + addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-7.8.4], sources: [hvr-ghc]}} - compiler: "ghc-7.10.3" # env: TEST=--disable-tests BENCH=--disable-benchmarks - addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.0,ghc-7.10.3], sources: [hvr-ghc]}} + addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-7.10.3], sources: [hvr-ghc]}} - compiler: "ghc-8.0.2" # env: TEST=--disable-tests BENCH=--disable-benchmarks - addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.0,ghc-8.0.2], sources: [hvr-ghc]}} + addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.0.2], sources: [hvr-ghc]}} - compiler: "ghc-8.2.2" # env: TEST=--disable-tests BENCH=--disable-benchmarks - addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.0,ghc-8.2.2], sources: [hvr-ghc]}} + addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.2.2], sources: [hvr-ghc]}} - compiler: "ghc-8.4.1" env: GHCHEAD=true addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.4.1], sources: [hvr-ghc]}} From 853f978925ed5af65f09ad1fc40bfe9358e78d37 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 30 Oct 2018 07:37:04 +0100 Subject: [PATCH 60/69] again --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8440c99a..9b834c9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -87,7 +87,7 @@ install: (cd "."; autoreconf -i); fi - rm -f cabal.project.freeze - - cabal new-build -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dep -j2 all + - cabal new-build -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dep -j2 all --max-backjumps=-1 - cabal new-build -w ${HC} --disable-tests --disable-benchmarks --project-file="cabal.project" --dep -j2 all - rm -rf "."/.ghc.environment.* "."/dist - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX) From bddfabfe7e94fb60d31aaeb002c0ade0532ea196 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 30 Oct 2018 07:37:55 +0100 Subject: [PATCH 61/69] also reorder goals --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9b834c9a..b8048c6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -87,7 +87,7 @@ install: (cd "."; autoreconf -i); fi - rm -f cabal.project.freeze - - cabal new-build -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dep -j2 all --max-backjumps=-1 + - cabal new-build -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dep -j2 all --reorder-goals --max-backjumps=-1 - cabal new-build -w ${HC} --disable-tests --disable-benchmarks --project-file="cabal.project" --dep -j2 all - rm -rf "."/.ghc.environment.* "."/dist - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX) From 3c81c624d71595dfb9e29ea3907fc17e086addb3 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 30 Oct 2018 07:45:53 +0100 Subject: [PATCH 62/69] Lower bounds to make cabal fall in line --- unordered-containers.cabal | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/unordered-containers.cabal b/unordered-containers.cabal index b5d8eae8..dffcf81e 100644 --- a/unordered-containers.cabal +++ b/unordered-containers.cabal @@ -75,9 +75,9 @@ test-suite validity-lazy QuickCheck >= 2.4.0.1, test-framework >= 0.3.3, test-framework-quickcheck2 >= 0.2.9, - validity, - genvalidity, - genvalidity-hspec, + validity >= 0.4.0.4, + genvalidity >= 0.4.0.4, + genvalidity-hspec >= 0.5.0.0, unordered-containers default-language: Haskell2010 @@ -95,9 +95,9 @@ test-suite validity-strict QuickCheck >= 2.4.0.1, test-framework >= 0.3.3, test-framework-quickcheck2 >= 0.2.9, - validity, - genvalidity, - genvalidity-hspec, + validity >= 0.4.0.4, + genvalidity >= 0.4.0.4, + genvalidity-hspec >= 0.5.0.0, unordered-containers default-language: Haskell2010 From 13a40c0e8ddc550ebb5fd8c54828cc93dcf8449d Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 30 Oct 2018 07:56:30 +0100 Subject: [PATCH 63/69] Trying again --- tests/Validity.hs | 180 ++++++++++++++++++------------------- unordered-containers.cabal | 4 +- 2 files changed, 92 insertions(+), 92 deletions(-) diff --git a/tests/Validity.hs b/tests/Validity.hs index 6abfc4fd..f8815a85 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -38,18 +38,17 @@ import Test.QuickCheck , Function , Gen , Property - , forAll , oneof , resize , sized ) import Test.QuickCheck.Function (Fun, apply, applyFun2, applyFun3) -import Test.Validity +import Test.Validity.Functions ( producesValidsOnValids , producesValidsOnValids2 , producesValidsOnValids3 - , shouldBeValid ) +import Test.Validity.GenValidity.Property (genGeneratesValid) instance (Validity k, Validity v) => Validity (Leaf k v) where validate (L k v) = mconcat [annotate k "key", annotate v "value"] @@ -101,7 +100,7 @@ instance (Eq k, Hashable k, Validity k, Validity v) => flip check "The hash, when masked with the current bitmask, of the key, produces the index in the array where the hashmap is found" $ - let h = HMB.hashWithSalt s k + let h = HMB.hashWithSalt s k in HMB.sparseIndex bm (HMB.mask h bs) == ix ] @@ -115,9 +114,10 @@ instance (Eq k, Hashable k, Validity k, Validity v) => (A.length a >= 1) "The array has at least one element." , check - (A.length a > 1 || not (HMB.isLeafOrCollision (A.index a 0))) $ - "If the array has only one element, then that element is" ++ - "not a leaf or collision." + (A.length a > 1 || + not (HMB.isLeafOrCollision (A.index a 0))) $ + "If the array has only one element, then that element is" ++ + "not a leaf or collision." ] (Full a) -> decorate "Full" $ @@ -133,7 +133,7 @@ instance (Eq k, Hashable k, Validity k, Validity v) => flip check "The hash, when masked with the current bitmask, of the key, produces the index in the array where the hashmap is found" $ - let h = HMB.hashWithSalt s k + let h = HMB.hashWithSalt s k in HMB.index h bs == ix ] , check @@ -495,89 +495,89 @@ pUnconsHM = -- * Test list tests :: [Test] tests = - let genValidHelper gen = forAll gen shouldBeValid + let genValidHelper gen = genGeneratesValid gen (const []) -- Basic interface - in [ testGroup - "HashMap" - [ testProperty "genValid generates valid values for Leaf" $ - genValidHelper (genValid :: Gen (Leaf Key Int)) - , testProperty "genValid generates valid values for Array" $ - genValidHelper (genValid :: Gen (A.Array Key)) - , testProperty "genValid generates valid values for HashMap" $ - genValidHelper (genValid :: Gen (HashMap Key Int)) - ] - , testGroup - "HashMap" - [ testProperty "singleton produces valid HashMaps" pSingleton - , testProperty "null produces valid Bools" pNull - , testProperty "size produces valid HashMaps" pSize - , testProperty "equal1 produce valid Bools" pEqual1 - , testProperty "equal2 produce valid Bools" pEqual2 - , testProperty "cmp1 produce valid Orderings" pCmp1 - , testProperty "cmp2 produce valid Orderings" pCmp2 - , testProperty "member produces valid HashMaps" pMember - , testProperty "lookup produces valid HashMaps" pLookup - , testProperty - "lookupDefault produces valid HashMaps" - pLookupDefault - , testProperty "insert produces valid HashMaps" pInsert - , testProperty "insertWith produces valid HashMaps" pInsertWith - , testProperty "delete produces valid HashMaps" pDelete - , testProperty "adjust produces valid HashMaps" pAdjust - , testProperty "update produces valid HashMaps" pUpdate - , testProperty "alter produces valid HashMaps" pAlter - , testProperty "alterF produces valid HashMaps" pAlterF - , testProperty "union produces valid HashMaps" pUnion - , testProperty "unionWith produces valid HashMaps" pUnionWith - , testProperty - "unionWithKey produces valid HashMaps" - pUnionWithKey - , testProperty "unions produces valid HashMaps" pUnions - , testProperty "map produces valid HashMaps" pMap - , testProperty "mapWithKey produces valid HashMaps" pMapWithKey - , testProperty - "traverseWithKey produces valid HashMaps" - pTraverseWithKey - , testProperty "difference produces valid HashMaps" pDifference - , testProperty - "differenceWith produces valid HashMaps" - pDifferenceWith - , testProperty - "intersection produces valid HashMaps" - pIntersection - , testProperty - "intersectionWith produces valid HashMaps" - pIntersectionWith - , testProperty - "intersectionWithKey produces valid HashMaps" - pIntersectionWithKey - , testProperty "foldl' produces valid HashMaps" pFoldl' - , testProperty - "foldlWithKey' produces valid HashMaps" - pFoldlWithKey' - , testProperty "foldr produces valid HashMaps" pFoldr - , testProperty - "foldrWithKey produces valid HashMaps" - pFoldrWithKey - , testProperty "filter produces valid HashMaps" pFilter - , testProperty - "filterWithKey produces valid HashMaps" - pFilterWithKey - , testProperty "mapMaybe produces valid HashMaps" pMapMaybe - , testProperty - "mapMaybeWithKey produces valid HashMaps" - pMapMaybeWithKey - , testProperty "keys produces valid lists" pKeys - , testProperty "elems produces valid lists" pElems - , testProperty "toList produces valid lists" pToList - , testProperty "fromList produces valid HashMaps" pFromList - , testProperty - "fromListWith produces valid HashMaps" - pFromListWith - , testProperty "unconsHM produces valid HashMaps" pUnconsHM - , testProperty "keysSet produces valid HashSets" pKeysSet - ] - ] + in [ testGroup + "HashMap" + [ testProperty "genValid generates valid values for Leaf" $ + genValidHelper (genValid :: Gen (Leaf Key Int)) + , testProperty "genValid generates valid values for Array" $ + genValidHelper (genValid :: Gen (A.Array Key)) + , testProperty "genValid generates valid values for HashMap" $ + genValidHelper (genValid :: Gen (HashMap Key Int)) + ] + , testGroup + "HashMap" + [ testProperty "singleton produces valid HashMaps" pSingleton + , testProperty "null produces valid Bools" pNull + , testProperty "size produces valid HashMaps" pSize + , testProperty "equal1 produce valid Bools" pEqual1 + , testProperty "equal2 produce valid Bools" pEqual2 + , testProperty "cmp1 produce valid Orderings" pCmp1 + , testProperty "cmp2 produce valid Orderings" pCmp2 + , testProperty "member produces valid HashMaps" pMember + , testProperty "lookup produces valid HashMaps" pLookup + , testProperty + "lookupDefault produces valid HashMaps" + pLookupDefault + , testProperty "insert produces valid HashMaps" pInsert + , testProperty "insertWith produces valid HashMaps" pInsertWith + , testProperty "delete produces valid HashMaps" pDelete + , testProperty "adjust produces valid HashMaps" pAdjust + , testProperty "update produces valid HashMaps" pUpdate + , testProperty "alter produces valid HashMaps" pAlter + , testProperty "alterF produces valid HashMaps" pAlterF + , testProperty "union produces valid HashMaps" pUnion + , testProperty "unionWith produces valid HashMaps" pUnionWith + , testProperty + "unionWithKey produces valid HashMaps" + pUnionWithKey + , testProperty "unions produces valid HashMaps" pUnions + , testProperty "map produces valid HashMaps" pMap + , testProperty "mapWithKey produces valid HashMaps" pMapWithKey + , testProperty + "traverseWithKey produces valid HashMaps" + pTraverseWithKey + , testProperty "difference produces valid HashMaps" pDifference + , testProperty + "differenceWith produces valid HashMaps" + pDifferenceWith + , testProperty + "intersection produces valid HashMaps" + pIntersection + , testProperty + "intersectionWith produces valid HashMaps" + pIntersectionWith + , testProperty + "intersectionWithKey produces valid HashMaps" + pIntersectionWithKey + , testProperty "foldl' produces valid HashMaps" pFoldl' + , testProperty + "foldlWithKey' produces valid HashMaps" + pFoldlWithKey' + , testProperty "foldr produces valid HashMaps" pFoldr + , testProperty + "foldrWithKey produces valid HashMaps" + pFoldrWithKey + , testProperty "filter produces valid HashMaps" pFilter + , testProperty + "filterWithKey produces valid HashMaps" + pFilterWithKey + , testProperty "mapMaybe produces valid HashMaps" pMapMaybe + , testProperty + "mapMaybeWithKey produces valid HashMaps" + pMapMaybeWithKey + , testProperty "keys produces valid lists" pKeys + , testProperty "elems produces valid lists" pElems + , testProperty "toList produces valid lists" pToList + , testProperty "fromList produces valid HashMaps" pFromList + , testProperty + "fromListWith produces valid HashMaps" + pFromListWith + , testProperty "unconsHM produces valid HashMaps" pUnconsHM + , testProperty "keysSet produces valid HashSets" pKeysSet + ] + ] main :: IO () main = defaultMain tests diff --git a/unordered-containers.cabal b/unordered-containers.cabal index dffcf81e..c0b9138b 100644 --- a/unordered-containers.cabal +++ b/unordered-containers.cabal @@ -77,7 +77,7 @@ test-suite validity-lazy test-framework-quickcheck2 >= 0.2.9, validity >= 0.4.0.4, genvalidity >= 0.4.0.4, - genvalidity-hspec >= 0.5.0.0, + genvalidity-property >= 0.1.0.0, unordered-containers default-language: Haskell2010 @@ -97,7 +97,7 @@ test-suite validity-strict test-framework-quickcheck2 >= 0.2.9, validity >= 0.4.0.4, genvalidity >= 0.4.0.4, - genvalidity-hspec >= 0.5.0.0, + genvalidity-property >= 0.1.0.0, unordered-containers default-language: Haskell2010 From 07ec38d74e9a783b508d00dd7a56a44c4efc4d58 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 30 Oct 2018 08:28:59 +0100 Subject: [PATCH 64/69] Compatibility with old versions of quickcheck --- Data/HashMap/Base.hs | 10 ++++----- tests/Validity.hs | 48 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 45972643..55e5767f 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -123,7 +123,7 @@ module Data.HashMap.Base ) where #if __GLASGOW_HASKELL__ < 710 -import Control.Applicative ((<$>), Applicative(pure)) +import Control.Applicative ((<$>), (<*>), Applicative(pure)) import Data.Monoid (Monoid(mempty, mappend)) import Data.Traversable (Traversable(..)) import Data.Word (Word) @@ -329,7 +329,7 @@ equal2 eqk eqv t1 t2 = go (toList' t1 []) (toList' t2 []) = go tl1 tl2 go (Collision h1 (L k11 v11) (L k12 v12) hm1 : tl1) (Collision h2 (L k21 v21) (L k22 v22) hm2 : tl2) | h1 == h2 && - (length hm1 == length hm2) && + (size hm1 == size hm2) && isPermutationBy (\(k1, v1) (k2, v2) -> eqk k1 k2 && eqv v1 v2) ((k11, v11) : (k12, v12) : toList hm1) ((k21, v21) : (k22, v22) : toList hm2) @@ -365,7 +365,7 @@ cmp1 cmpv t1 t2 = go (toList' t1 []) (toList' t2 []) go tl1 tl2 go (Collision h1 (L k11 v11) (L k12 v12) hm1 : tl1) (Collision h2 (L k21 v21) (L k22 v22) hm2 : tl2) = compare h1 h2 `mappend` - compare (length hm1) (length hm2) `mappend` + compare (size hm1) (size hm2) `mappend` liftCompareList pairCompare -- We don't use sortOn because fst is cheap. (L.sortBy (compare `on` fst) $ @@ -400,7 +400,7 @@ cmp2 cmpk cmpv t1 t2 = go (toList' t1 []) (toList' t2 []) go tl1 tl2 go (Collision h1 (L k11 v11) (L k12 v12) hm1 : tl1) (Collision h2 (L k21 v21) (L k22 v22) hm2 : tl2) = compare h1 h2 `mappend` - compare (length hm1) (length hm2) `mappend` + compare (size hm1) (size hm2) `mappend` unorderedCompare (\(k1, v1) (k2, v2) -> (k1 `cmpk` k2) `mappend` (v1 `cmpv` v2)) ((k11, v11) : (k12, v12) : toList hm1) ((k21, v21) : (k22, v22) : toList hm2) `mappend` @@ -423,7 +423,7 @@ equalKeys1 eq t1 t2 = go (toList' t1 []) (toList' t2 []) = go tl1 tl2 go (Collision h1 (L k11 _) (L k12 _) hm1 : tl1) (Collision h2 (L k21 _) (L k22 _) hm2 : tl2) | h1 == h2 && - (length hm1 == length hm2) && + (size hm1 == size hm2) && isPermutationBy eq (k11 : k12 : keys hm1) (k21 : k22 : keys hm2) diff --git a/tests/Validity.hs b/tests/Validity.hs index f8815a85..dc1ab2c0 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-} +{-# LANGUAGE TypeOperators #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} @@ -11,7 +12,12 @@ module Main ) where import GHC.Generics (Generic) - +#if __GLASGOW_HASKELL__ < 710 +import Control.Applicative (Applicative(pure), (<$>), (<*>)) +import Data.Monoid (Monoid(mappend, mempty)) +import Data.Traversable (Traversable(..)) +import Data.Word (Word) +#endif import Data.Bits (popCount) import Data.Functor import Data.Hashable (Hashable(hashWithSalt)) @@ -35,21 +41,48 @@ import Test.Framework.Providers.QuickCheck2 (testProperty) import Test.QuickCheck ( Arbitrary , CoArbitrary - , Function , Gen , Property , oneof , resize , sized ) -import Test.QuickCheck.Function (Fun, apply, applyFun2, applyFun3) +import Test.QuickCheck.Function + ( (:->) + , Fun(..) + , Function(..) + , apply + , functionMap + ) import Test.Validity.Functions ( producesValidsOnValids , producesValidsOnValids2 , producesValidsOnValids3 ) import Test.Validity.GenValidity.Property (genGeneratesValid) - +#if !MIN_VERSION_QuickCheck(2,10,0) +-- | Extracts the value of a binary function. +-- +-- 'Fn2' is the pattern equivalent of this function. +-- +-- > prop_zipWith :: Fun (Int, Bool) Char -> [Int] -> [Bool] -> Bool +-- > prop_zipWith f xs ys = zipWith (applyFun2 f) xs ys == [ applyFun2 f x y | (x, y) <- zip xs ys] +-- +applyFun2 :: Fun (a, b) c -> (a -> b -> c) +applyFun2 (Fun _ f) a b = f (a, b) + +-- | Extracts the value of a ternary function. 'Fn3' is the +-- pattern equivalent of this function. +applyFun3 :: Fun (a, b, c) d -> (a -> b -> c -> d) +applyFun3 (Fun _ f) a b c = f (a, b, c) + +-- | Provides a 'Function' instance for types with 'Integral'. +functionIntegral :: Integral a => (a -> b) -> (a :-> b) +functionIntegral = functionMap fromIntegral fromInteger + +instance Function Word where + function = functionIntegral +#endif instance (Validity k, Validity v) => Validity (Leaf k v) where validate (L k v) = mconcat [annotate k "key", annotate v "value"] @@ -277,13 +310,18 @@ newtype Key = K , Ord , Read , Show + , Integral + , Real + , Num + , Enum , Generic ) instance Hashable Key where hashWithSalt salt k = hashWithSalt salt (unK k) `mod` 20 -instance Function Key +instance Function Key where + function = functionIntegral pSingleton :: Property pSingleton = From 918cfda8573899b1f234f35ebeee95f33df2d001 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 30 Oct 2018 08:38:56 +0100 Subject: [PATCH 65/69] and again --- tests/Validity.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Validity.hs b/tests/Validity.hs index dc1ab2c0..03be8099 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -82,6 +82,8 @@ functionIntegral = functionMap fromIntegral fromInteger instance Function Word where function = functionIntegral +#else +import Test.QuickCheck.Function (applyFun2, applyFun3, functionIntegral) #endif instance (Validity k, Validity v) => Validity (Leaf k v) where validate (L k v) = mconcat [annotate k "key", annotate v "value"] From eb46c82db66ced9f47358cb8d5405d114b7d438b Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 30 Oct 2018 09:05:36 +0100 Subject: [PATCH 66/69] Not sure if this will help, but we'll see --- tests/HashSetProperties.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/HashSetProperties.hs b/tests/HashSetProperties.hs index fd65bf9e..7ff3b82c 100644 --- a/tests/HashSetProperties.hs +++ b/tests/HashSetProperties.hs @@ -14,7 +14,9 @@ import Data.Ord (comparing) import Test.QuickCheck (Arbitrary, Property, (==>), (===)) import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) -import Data.Functor.Classes (eq1, compare1) +#if MIN_VERSION_base(4,9,0) +import Data.Functor.Classes (eq1, compare1, eq2, compare2) +#endif -- Key type that generates more hash collisions. newtype Key = K { unK :: Int } From b5fb685a882b38b475cfc72b390ed68ad8e2e102 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Tue, 30 Oct 2018 09:26:45 +0100 Subject: [PATCH 67/69] CPP for some tests --- tests/HashMapProperties.hs | 8 ++++++++ tests/HashSetProperties.hs | 10 +++++++++- tests/Validity.hs | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/HashMapProperties.hs b/tests/HashMapProperties.hs index 8f1833ae..ec96b841 100644 --- a/tests/HashMapProperties.hs +++ b/tests/HashMapProperties.hs @@ -47,6 +47,7 @@ instance Hashable Key where pEq :: [(Key, Int)] -> [(Key, Int)] -> Bool pEq xs = (M.fromList xs ==) `eq` (HM.fromList xs ==) +#if MIN_VERSION_base(4,9,0) -- Caution: this is a rather weak test. Fortunately, the "Permutation" -- properties in the HashSet test suite should catch most of what this -- doesn't. @@ -55,6 +56,7 @@ pEq2 xs ys = (x == y) === (x `eq2` y) where x = HM.fromList xs y = HM.fromList ys +#endif pNeq :: [(Key, Int)] -> [(Key, Int)] -> Bool pNeq xs = (M.fromList xs /=) `eq` (HM.fromList xs /=) @@ -88,11 +90,13 @@ pOrd3 xs ys = case (compare x y, compare y x) of x = HM.fromList xs y = HM.fromList ys +#if MIN_VERSION_base(4,9,0) pOrd4 :: [(Key, Int)] -> [(Key, Int)] -> Property pOrd4 xs ys = compare2 x y === compare x y where x = HM.fromList xs y = HM.fromList ys +#endif pOrdEq :: [(Key, Int)] -> [(Key, Int)] -> Bool pOrdEq xs ys = case (compare x y, x == y) of @@ -397,11 +401,15 @@ tests = testGroup "instances" [ testProperty "==" pEq , testProperty "/=" pNeq +#if MIN_VERSION_base(4,9,0) , testProperty "eq2 = (==)" pEq2 +#endif , testProperty "compare reflexive" pOrd1 , testProperty "compare transitive" pOrd2 , testProperty "compare antisymmetric" pOrd3 +#if MIN_VERSION_base(4,9,0) , testProperty "compare2 = compare" pOrd4 +#endif , testProperty "Ord => Eq" pOrdEq , testProperty "Read/Show" pReadShow , testProperty "Functor" pFunctor diff --git a/tests/HashSetProperties.hs b/tests/HashSetProperties.hs index 7ff3b82c..abdf8d5e 100644 --- a/tests/HashSetProperties.hs +++ b/tests/HashSetProperties.hs @@ -15,7 +15,7 @@ import Test.QuickCheck (Arbitrary, Property, (==>), (===)) import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) #if MIN_VERSION_base(4,9,0) -import Data.Functor.Classes (eq1, compare1, eq2, compare2) +import Data.Functor.Classes (eq1, compare1) #endif -- Key type that generates more hash collisions. @@ -89,11 +89,13 @@ pPermutationEq xs = S.fromList x == S.fromList y x = L.map fst xs y = L.map fst . L.sortBy (comparing snd) $ xs +#if MIN_VERSION_base(4,9,0) pPermutationEq1 :: [(Key, Int)] -> Bool pPermutationEq1 xs = S.fromList x `eq1` S.fromList y where x = L.map fst xs y = L.map fst . L.sortBy (comparing snd) $ xs +#endif pPermutationOrd :: [(Key, Int)] -> Bool pPermutationOrd xs = (S.fromList x `compare` S.fromList y) == EQ @@ -101,11 +103,13 @@ pPermutationOrd xs = (S.fromList x `compare` S.fromList y) == EQ x = L.map fst xs y = L.map fst . L.sortBy (comparing snd) $ xs +#if MIN_VERSION_base(4,9,0) pPermutationOrd1 :: [(Key, Int)] -> Bool pPermutationOrd1 xs = (S.fromList x `compare1` S.fromList y) == EQ where x = L.map fst xs y = L.map fst . L.sortBy (comparing snd) $ xs +#endif pHashable :: [Key] -> [Int] -> Int -> Property pHashable xs is salt = @@ -193,9 +197,13 @@ tests = testGroup "instances" [ testProperty "==" pEq , testProperty "Permutation ==" pPermutationEq +#if MIN_VERSION_base(4,9,0) , testProperty "Permutation eq1" pPermutationEq1 +#endif , testProperty "Permutation compare" pPermutationOrd +#if MIN_VERSION_base(4,9,0) , testProperty "Permutation compare1" pPermutationOrd1 +#endif , testProperty "/=" pNeq , testProperty "compare reflexive" pOrd1 , testProperty "compare transitive" pOrd2 diff --git a/tests/Validity.hs b/tests/Validity.hs index 03be8099..dc3870f7 100644 --- a/tests/Validity.hs +++ b/tests/Validity.hs @@ -335,6 +335,8 @@ pNull = producesValidsOnValids (HM.null :: HM.HashMap Key Int -> Bool) pSize :: Property pSize = producesValidsOnValids (HM.size :: HM.HashMap Key Int -> Int) +-- These four tests will ensure that `equal1`, etc do not segfault and succesfully +-- evaluate to a `Bool` (or `Ordering`, respectively) pEqual1 :: Fun (Int, Int) Bool -> Property pEqual1 f = producesValidsOnValids2 From 6838fff2ec28c9551140fed5056cdd9c0a304b6a Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 31 Oct 2018 09:10:37 +0100 Subject: [PATCH 68/69] put back the show instance --- Data/HashMap/Base.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Data/HashMap/Base.hs b/Data/HashMap/Base.hs index 55e5767f..3753f05a 100644 --- a/Data/HashMap/Base.hs +++ b/Data/HashMap/Base.hs @@ -195,7 +195,7 @@ data HashMap k v | Leaf !Hash !(Leaf k v) | Full !(A.Array (HashMap k v)) | Collision !Hash !(Leaf k v) !(Leaf k v) !(HashMap k v) - deriving (Typeable, Show) + deriving (Typeable) type role HashMap nominal representational @@ -275,9 +275,9 @@ instance (Eq k, Hashable k, Read k, Read e) => Read (HashMap k e) where readListPrec = readListPrecDefault --- instance (Show k, Show v) => Show (HashMap k v) where --- showsPrec d m = showParen (d > 10) $ --- showString "fromList " . shows (toList m) +instance (Show k, Show v) => Show (HashMap k v) where + showsPrec d m = showParen (d > 10) $ + showString "fromList " . shows (toList m) instance Traversable (HashMap k) where traverse f = traverseWithKey (const f) From 58af4519ace34c5f7d3c1359907ff75e27b9cdb8 Mon Sep 17 00:00:00 2001 From: Tom Sydney Kerckhove Date: Wed, 31 Oct 2018 09:37:01 +0100 Subject: [PATCH 69/69] some docs --- docs/developer-guide.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 75f688c1..3d04a148 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -21,7 +21,7 @@ motivate its existance. ## A note on hash functions -While the [hashable](http://hackage.haskell.org/package/containers) package is a +While the [hashable](http://hackage.haskell.org/package/hashable) package is a separate package, it was co-designed with this package. Its main role is to support this package and not to provide good general purpose hash functions (e.g. to use when fingerprinting a text file). @@ -84,15 +84,13 @@ data HashMap k v | BitmapIndexed !Bitmap !(A.Array (HashMap k v)) | Leaf !Hash !(Leaf k v) | Full !(A.Array (HashMap k v)) - | Collision !Hash !(A.Array (Leaf k v)) + | Collision !Hash !(Leaf k v) !(Leaf k v) !(HashMap k v) ``` Here's a quick overview in order of simplicty: * `Empty` -- The empty map. * `Leaf` -- A key-value pair. - * `Collision` -- An array of key-value pairs where the keys have identical hash - values. Element order doesn't matter. * `Full` -- An array of *2^B* children. Given a key you can find the child it is part of by taking *B* bits of the hash value for the key and indexing into the key. Which bits to use depends on the tree level. @@ -101,6 +99,8 @@ Here's a quick overview in order of simplicty: used to convert from the index taken from the hash value, just like above, to the actual index in the array. This node gets upgraded to a `Full` node when it contains *2^B* elements. + * `Collision` -- Twe key-value pairs that collide on the given hash, and a + recursive hashmap where keys are hashed with a different salt. The number of bits of the hash value to use at each level of the tree, *B*, is a compiled time constant (i.e. 4). In general a larger *B* improves lookup @@ -111,6 +111,19 @@ updating the spine of the tree). indexing, faster copying on modification (given that its size is statically know), and lower memory use. +`Collision` is a special case. The previous version used `Collision !Hash +!(A.Array (HashMap k v))` instead of `Collision !Hash !(Leaf k v) !(Leaf k v) +!(HashMap k v)`. As a result, hash-collisions would cause `O(number of +collisions)` insertion time. Because the currently used hash function is not +collision-resistant, it is cheap to make enough collisions to have this cause a +denial of service exploit. However, we can use the fact that `hashable` supports +salted hashing by default to alleviate at least some of this problem. In the +current version, insertion time is not `O(number of collisions)` but `O(number +of consequtive levels of collision)`. While this is not better in case all the +collisions also collide on all subsequent salts, it is likely that a collision +using one salt will not collide again on the next salt, so this approach is +faster in the common case of collisions. + ## Why things are fast Performance is largely dominated by memory layout and allocation. The code has