pure-borrow-0.1.0.0: test/Data/HashMap/RobinHood/Mutable/Linear/BorrowSpec.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
-- | Specs of the borrow-aware Robin Hood hash table.
module Data.HashMap.RobinHood.Mutable.Linear.BorrowSpec (
module Data.HashMap.RobinHood.Mutable.Linear.BorrowSpec,
) where
import Control.Functor.Linear qualified as Control
import Control.Monad.Borrow.Pure
import Control.Syntax.DataFlow qualified as DataFlow
import Data.HashMap.RobinHood.Mutable.Linear.Borrow qualified as HM
import Data.HashMap.Strict qualified as HMS
import Data.List qualified as NonLinear
import Data.List.NonEmpty (NonEmpty (..))
import Prelude.Linear
import Test.Falsify.Generator qualified as G
import Test.Falsify.Predicate ((.$))
import Test.Falsify.Predicate qualified as P
import Test.Falsify.Range qualified as G
import Test.Tasty (TestTree)
import Test.Tasty.Falsify (testProperty)
import Test.Tasty.Falsify qualified as F
import Test.Tasty.HUnit
import Prelude qualified as NonLinear
type Table = HM.HashMap String Int
{- | Run an action against a freshly borrowed table.
The action owns the mutable borrow and must consume it; the table itself is
reclaimed and released once the lifetime ends.
-}
withTable ::
forall r.
Int ->
(forall α. Mut α Table %1 -> BO α (Ur r)) ->
Ur r
withTable capacity action =
linearly \linear -> DataFlow.do
(ownerLinear, runLinear) <- dup linear
runBO runLinear Control.do
(table, lend) <- borrowM (HM.empty capacity ownerLinear)
Ur result <- action table
pureAfter (consume (reclaim lend) `lseq` Ur result)
-- | Materialize an owned table's entries, then release it.
drainTable :: Table %1 -> Ur [(String, Int)]
drainTable table =
linearly \linear ->
runBO linear Control.do
(borrowed, lend) <- borrowM table
(Ur entries, borrowed) <- HM.toList borrowed
consume borrowed `lseq` pureAfter (consume (reclaim lend) `lseq` Ur entries)
insertAll :: [(String, Int)] -> Mut α Table %1 -> BO α (Mut α Table)
insertAll [] table = Control.pure table
insertAll ((k, v) : rest) table = Control.do
(Ur _, table) <- HM.insert k v table
insertAll rest table
sorted :: [(String, Int)] -> [(String, Int)]
sorted = NonLinear.sortOn NonLinear.fst
test_insertLookupDelete :: TestTree
test_insertLookupDelete = testCase "insert, lookup and delete through a mutable borrow" do
let Ur (displaced, one, missing, present, absent, count, entries) =
withTable 16 \table -> Control.do
(Ur displacedOne, table) <- HM.insert "one" 1 table
(Ur _, table) <- HM.insert "two" 2 table
(Ur displacedTwo, table) <- HM.insert "two" 22 table
(Ur one, table) <- HM.lookup "one" table
(Ur missing, table) <- HM.lookup "three" table
(Ur present, table) <- HM.member "two" table
(Ur deleted, table) <- HM.delete "one" table
(Ur absent, table) <- HM.member "one" table
(Ur count, table) <- HM.size table
(Ur entries, table) <- HM.toList table
consume table `lseq`
Control.pure
(Ur ((displacedOne, displacedTwo, deleted), one, missing, present, absent, count, entries))
displaced @?= (Nothing, Just 2, Just 1)
one @?= Just 1
missing @?= Nothing
present @?= True
absent @?= False
count @?= 1
entries @?= [("two", 22)]
test_growthIsVisibleThroughTheBorrow :: TestTree
test_growthIsVisibleThroughTheBorrow =
testCase "a growth taken while borrowed is written back to the reference" do
-- The underlying owned table replaces its backing array when it grows, so
-- this is the regression test for the 'Ref' indirection: were the grown
-- table not written back, every entry inserted after the first growth
-- would be lost.
let pairs = [(NonLinear.show i, i) | i <- [1 .. 512 :: Int]]
Ur (count, entries) = withTable 4 \table -> Control.do
table <- insertAll pairs table
(Ur count, table) <- HM.size table
(Ur entries, table) <- HM.toList table
consume table `lseq` Control.pure (Ur (count, entries))
count @?= NonLinear.length pairs
sorted entries @?= sorted pairs
test_preparedInsertion :: TestTree
test_preparedInsertion = testCase "suspended lookups resume as insertions" do
let expected = [(NonLinear.show i, i) | i <- [1 .. 256 :: Int]]
Ur (found, entries) = withTable 4 \table -> Control.do
table <- go 1 table
(Ur found, table) <- HM.lookup "128" table
(Ur entries, table) <- HM.toList table
consume table `lseq` Control.pure (Ur (found, entries))
found @?= Just 128
sorted entries @?= sorted expected
where
go :: Int -> Mut α Table %1 -> BO α (Mut α Table)
go i table
| i NonLinear.> 256 = Control.pure table
| otherwise = Control.do
(Ur plan, table) <- HM.lookupForInsert (NonLinear.show i) table
table <- case plan of
Left _ -> Control.pure table
Right plan -> HM.unsafeInsertPrepared plan i table
go (i + 1) table
test_alter :: TestTree
test_alter = testCase "alter inserts, updates and deletes" do
let Ur (inserted, updated, deleted) = withTable 16 \table -> Control.do
table <- HM.alter (\_ -> Just 1) "key" table
(Ur inserted, table) <- HM.lookup "key" table
table <- HM.alter (NonLinear.fmap (NonLinear.+ 41)) "key" table
(Ur updated, table) <- HM.lookup "key" table
table <- HM.alter (\_ -> Nothing) "key" table
(Ur deleted, table) <- HM.lookup "key" table
consume table `lseq` Control.pure (Ur (inserted, updated, deleted))
inserted @?= Just 1
updated @?= Just 42
deleted @?= Nothing
test_alterF :: TestTree
test_alterF = testCase "alterF may inspect the table it is altering" do
let Ur (observed, final) = withTable 16 \table -> Control.do
table <- HM.alter (\_ -> Just 7) "key" table
table <-
HM.alterF
(\seen -> Control.pure (Ur (NonLinear.fmap (NonLinear.* 2) seen)))
"key"
table
(Ur final, table) <- HM.lookup "key" table
(Ur observed, table) <- HM.size table
consume table `lseq` Control.pure (Ur (observed, final))
observed @?= 1
final @?= Just 14
test_takeLeavesAnEmptyTable :: TestTree
test_takeLeavesAnEmptyTable = testCase "take empties the borrowed table" do
let pairs = [("a", 1), ("b", 2), ("c", 3 :: Int)]
Ur (taken, remaining) = withTable 16 \table -> Control.do
table <- insertAll pairs table
(old, table) <- HM.take table
(Ur remaining, table) <- HM.toList table
drainTable old & \(Ur taken) ->
consume table `lseq` Control.pure (Ur (taken, remaining))
sorted taken @?= sorted pairs
remaining @?= []
test_swap :: TestTree
test_swap = testCase "swap exchanges the borrowed table with an owned one" do
let Ur (displaced, remaining) = withTable 16 \table -> Control.do
table <- insertAll [("a", 1), ("b", 2)] table
replacement <- asksLinearlyM \lin -> Control.pure (HM.fromList [("z", 26)] lin)
(old, table) <- HM.swap replacement table
(Ur remaining, table) <- HM.toList table
drainTable old & \(Ur displaced) ->
consume table `lseq` Control.pure (Ur (displaced, remaining))
sorted displaced @?= [("a", 1), ("b", 2)]
remaining @?= [("z", 26)]
test_extend :: TestTree
test_extend = testCase "extend merges an owned table into a borrowed one" do
let Ur entries = withTable 16 \table -> Control.do
table <- insertAll [("a", 1), ("b", 2)] table
donor <- asksLinearlyM \lin -> Control.pure (HM.fromList [("b", 20), ("c", 3)] lin)
table <- HM.extend donor table
(Ur entries, table) <- HM.toList table
consume table `lseq` Control.pure (Ur entries)
sorted entries @?= [("a", 1), ("b", 20), ("c", 3)]
test_union :: TestTree
test_union = testCase "union of two owned tables" do
let Ur entries = linearly \linear -> DataFlow.do
(linLeft, linRight) <- dup linear
drainTable
( HM.union
(HM.fromList [("a", 1), ("b", 2)] linLeft)
(HM.fromList [("b", 20), ("c", 3)] linRight)
)
sorted entries @?= [("a", 1), ("b", 20), ("c", 3)]
-- | Mutations through a borrow agree with @unordered-containers@.
test_randomMutations :: TestTree
test_randomMutations = testProperty "random mutations agree with the oracle" do
program <-
F.gen $
G.list (G.between (1, 256)) do
key <- G.list (G.between (1, 4)) (G.elem ('a' :| "bcd"))
value <- G.int (G.between (-10, 10))
deleting <- G.bool NonLinear.False
NonLinear.pure (if deleting then Left key else Right (key, value))
let expected = NonLinear.foldl' step HMS.empty program
Ur entries = withTable 8 \table -> Control.do
table <- apply program table
(Ur entries, table) <- HM.toList table
consume table `lseq` Control.pure (Ur entries)
F.assert $ P.expect expected .$ ("final table", HMS.fromList entries)
where
step oracle = \case
Left key -> HMS.delete key oracle
Right (key, value) -> HMS.insert key value oracle
apply ::
[Either String (String, Int)] ->
Mut α Table %1 ->
BO α (Mut α Table)
apply [] table = Control.pure table
apply (instruction : rest) table = Control.do
table <- case instruction of
Left key -> Control.fmap (\(Ur _, table) -> table) (HM.delete key table)
Right (key, value) -> Control.fmap (\(Ur _, table) -> table) (HM.insert key value table)
apply rest table