pure-borrow-0.1.0.0: test/Data/HashMap/RobinHood/Mutable/LinearSpec.hs
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
-- | Specs of the owned Robin Hood hash table.
module Data.HashMap.RobinHood.Mutable.LinearSpec (
module Data.HashMap.RobinHood.Mutable.LinearSpec,
) where
import Control.Functor.Linear qualified as Lin
import Control.Monad (forM)
import Control.Monad.Borrow.Pure (linearly)
import Data.Bifunctor.Linear qualified as BiL
import Data.Foldable (forM_)
import Data.HashMap.RobinHood.Mutable.Linear qualified as LHM
import Data.HashMap.RobinHood.Mutable.LinearSpec.Cases
import Data.HashMap.Strict qualified as HMS
import Data.HashSet qualified as HashSet
import Data.Hashable (Hashable (..))
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as Map
import Data.Maybe (isJust, isNothing)
import Data.Unrestricted.Linear (UrT (..), runUrT)
import Data.Unrestricted.Linear qualified as Ur
import GHC.Generics (Generic)
import Prelude.Linear (Ur (..), unur, (&))
import Prelude.Linear qualified as PL
import Test.Falsify.Generator qualified as F
import Test.Falsify.Predicate ((.$))
import Test.Falsify.Predicate qualified as P
import Test.Falsify.Range qualified as F
import Test.Tasty
import Test.Tasty.Falsify (Property, testProperty)
import Test.Tasty.Falsify qualified as F
import Test.Tasty.HUnit
test_case1 :: TestTree
test_case1 = testCase "HashMap case 1" do
let Ur Case1Result {..} = withNewEmptyHashMap case1
initOneResident @?= Nothing
newOneResident @?= Just 1
initTwoResident @?= Nothing
deletedOneResident @?= Just 1
finalResult @?= [("Two", 2)]
test_case2 :: TestTree
test_case2 = testCase "HashMap case 2" do
let Ur finalResult = withNewEmptyHashMap case2
let resl = Map.fromList finalResult
expected = Map.fromList [(show i, i) | i <- [1 .. 15] <> [129 .. 256]]
Map.size resl @?= Map.size expected
resl @?= expected
test_case3 :: TestTree
test_case3 = testCase "HashMap case 3" do
let Ur Case3Result {..} = withNewEmptyHashMap case3
iniOneReside @?= iniOneResideExpected
oneBeforeBulkInsert @?= oneBeforeBulkInsertExpected
oneAfterBulkInsert @?= oneAfterBulkInsertExpected
sixteenAfterBulkInsert @?= sixteenAfterBulkInsertExpected
sixteenAfterBulkDelete @?= sixteenAfterBulkDeleteExpected
poppedSixteen @?= poppedSixteenExpected
finalSixteen @?= finalSixteenExpected
Map.size finalResult @?= Map.size expectedResult
finalResult @?= expectedResult
test_case4 :: TestTree
test_case4 = testCase "HashMap case 4" do
let Ur (ccVal, lst) = withNewEmptyHashMap \hm ->
LHM.insert "aa" (-10 :: Int) hm & \(Ur _, hm) ->
LHM.insert "cc" (-10) hm & \(Ur _, hm) ->
LHM.lookup "cc" hm & \(Ur valCc, hm) ->
LHM.toList hm & \(Ur lst) ->
Ur (valCc, lst)
sortOn fst lst @?= sortOn fst [("aa", -10), ("cc", -10)]
ccVal @?= Just (-10)
test_case5 :: TestTree
test_case5 = testCase "HashMap case 5" do
let input = [("abcba", 1 :: Int), ("bacba", 2), ("aaba", 3), ("baa", 4)]
let Ur (abcbaVal, lst) = withNewEmptyHashMap \hm ->
LHM.insertMany input hm & \hm ->
LHM.lookup "abcba" hm & \(Ur valCc, hm) ->
LHM.toList hm & \(Ur lst) ->
Ur (valCc, lst)
sortOn fst lst @?= sortOn fst input
abcbaVal @?= lookup "abcba" input
test_preparedInsertion :: TestTree
test_preparedInsertion = testCase "prepared insertion preserves lookup and growth" do
let expected = Map.fromList [(show i, i) | i <- [1 .. 256 :: Int]]
Ur (found, actual) = withNewEmptyHashMap (go 1)
found @?= Just 128
Map.fromList actual @?= expected
where
go :: Int -> LHM.HashMap String Int %1 -> Ur (Maybe Int, [(String, Int)])
go i hm
| i <= 256 = case LHM.lookupForInsert (show i) hm of
(Ur (Left old), hm) -> go (i + 1) (LHM.alter (const (Just old)) (show i) hm)
(Ur (Right plan), hm) -> go (i + 1) (LHM.unsafeInsertPrepared plan i hm)
| otherwise = case LHM.lookupForInsert "128" hm of
(Ur (Left value), hm) -> case LHM.toList hm of
Ur entries -> Ur (Just value, entries)
(Ur (Right plan), hm) -> case LHM.toList (LHM.unsafeInsertPrepared plan 0 hm) of
Ur entries -> Ur (Nothing, entries)
-- | A key whose every value lands in the same bucket.
newtype Colliding = Colliding Int
deriving (Show, Eq, Ord)
instance Hashable Colliding where
hashWithSalt _ _ = 0
test_preparedInsertionCollisions :: TestTree
test_preparedInsertionCollisions = testCase "prepared insertion preserves Robin Hood collisions" do
let expected = Map.fromList [(Colliding i, i) | i <- [1 .. 64]]
Ur actual = linearly \lin -> go 1 (LHM.new 1 lin)
Map.fromList actual @?= expected
where
go :: Int -> LHM.HashMap Colliding Int %1 -> Ur [(Colliding, Int)]
go i hm
| i <= 64 = case LHM.lookupForInsert (Colliding i) hm of
(Ur (Left old), hm) -> go (i + 1) (LHM.alter (const (Just old)) (Colliding i) hm)
(Ur (Right plan), hm) -> go (i + 1) (LHM.unsafeInsertPrepared plan i hm)
| otherwise = LHM.toList hm
data Instruction
= Inserts (NonEmpty (String, Int))
| Insert String Int
| Delete String
deriving (Show, Eq, Ord, Generic)
instructionG :: F.Gen Instruction
instructionG =
F.oneof $
NE.fromList
[ Insert
<$> readableStringG
<*> valG
, Inserts . NE.fromList
<$> F.list (F.between (1, 128)) ((,) <$> readableStringG <*> valG)
]
readableStringG :: F.Gen String
readableStringG = F.list (F.between (1, 5)) (F.elem $ 'a' :| "bc")
valG :: F.Gen Int
valG = F.int (F.between (-10, 10))
type Semantics = HMS.HashMap String Int
test_randomInstructions :: TestTree
test_randomInstructions = testProperty "random instructions" do
program <- F.gen $ F.list (F.between (1, 256)) instructionG
testInstructions program
-- | Run a random program against @unordered-containers@ as the oracle.
testInstructions :: [Instruction] -> Property ()
testInstructions instrs = do
unur PL.$ linearly \lin ->
go HMS.empty (LHM.new 16 lin) (pure ()) instrs
where
go ::
Semantics ->
LHM.HashMap String Int %1 ->
Property () ->
[Instruction] ->
Ur (Property ())
go sem !hm !act [] = case LHM.toList hm of
Ur lst ->
Ur $
act
*> F.assert (P.expect sem .$ ("Final dictionary", HMS.fromList lst))
go sem !hm !act (instr : rest) = case instr of
Insert k v ->
let expectOld = HMS.lookup k sem
sem' = HMS.insert k v sem
in LHM.insert k v hm & \(Ur oldVal, hm) ->
LHM.lookup k hm & \(Ur newVal, hm) ->
let checks = do
F.collect "colliding insertion" [isJust expectOld]
F.assert $
P.expect expectOld
.$ ("before insert " <> show (k, v), oldVal)
F.assert $
P.expect (Just v)
.$ ("after insert " <> show (k, v), newVal)
in go sem' hm (act *> checks) rest
Inserts kvs ->
let sem' = foldl' (flip $ uncurry HMS.insert) sem kvs
overlaps = map (\(k, _) -> HMS.member k sem) $ NE.toList kvs
targKeys = HashSet.toList $ HashSet.fromList $ map fst $ NE.toList kvs
in LHM.insertMany (NE.toList kvs) hm & \hm ->
Lin.runState
( runUrT PL.$ forM targKeys \k -> UrT $ Lin.state \hm ->
BiL.first (Ur.lift (k,)) (LHM.lookup k hm)
)
hm
& \(Ur lookups, hm) ->
let checks = do
F.label "bulk insertion size" [showSize $ NE.length kvs]
F.collect "colliding insertion" overlaps
forM_ lookups \(k, newVals) -> do
F.assert $
P.expect (HMS.lookup k sem')
.$ ("after bulk insert for " <> show k, newVals)
in go sem' hm (act *> checks) rest
Delete k ->
let expectOld = HMS.lookup k sem
sem' = HMS.delete k sem
in LHM.delete k hm & \(Ur oldVal, hm) ->
LHM.lookup k hm & \(Ur newVal, hm) ->
let checks = do
F.collect "vacuous deletion" [isNothing expectOld]
F.assert $
P.expect expectOld
.$ ("value before deletion " <> show k, oldVal)
F.assert $
P.expect Nothing
.$ ("value after deletion " <> show k, newVal)
in go sem' hm (act *> checks) rest
showSize :: Int -> String
showSize 0 = "0"
showSize i =
let lb = floor @_ @Int (fromIntegral @_ @Double i / 10) * 10
ub = lb + 10
in "[" <> show lb <> ", " <> show ub <> ")"