pure-borrow-0.1.0.0: test/Data/Vector/Mutable/Linear/BorrowSpec.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE LinearTypes #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
module Data.Vector.Mutable.Linear.BorrowSpec (
module Data.Vector.Mutable.Linear.BorrowSpec,
) where
import Control.Exception qualified as Exception
import Control.Functor.Linear qualified as Control
import Control.Monad.Borrow.Pure.BO
import Control.Monad.Borrow.Pure.BO.Unsafe (Alias (UnsafeAlias))
import Control.Monad.Borrow.Pure.Copyable
import Control.Syntax.DataFlow qualified as DataFlow
import Data.Bifunctor.Linear qualified as Bi
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Data.List qualified as List
import Data.Vector qualified as V
import Data.Vector.Mutable.Linear.Borrow qualified as VL
import GHC.IO (unsafePerformIO)
import Prelude.Linear
import Test.Falsify.Generator qualified as G
import Test.Falsify.Predicate qualified as P
import Test.Falsify.Property qualified as F
import Test.Falsify.Range qualified as G
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Falsify (testProperty)
import Test.Tasty.HUnit
import Unsafe.Linear qualified as Unsafe
import Prelude qualified as NonLinear
data MoveTracked = MoveTracked !(IORef Int) !Int !Bool
instance Consumable MoveTracked where
consume = Unsafe.toLinear \_ -> ()
instance Dupable MoveTracked where
dup2 = Unsafe.toLinear \value -> (value, value)
instance Movable MoveTracked where
move =
Unsafe.toLinear \(MoveTracked moves value _) ->
case unsafePerformIO (modifyIORef' moves NonLinear.succ) of
() -> Ur (MoveTracked moves value True)
data CopyTracked = CopyTracked !(IORef Int) !(IORef Int) !Int
instance Copyable CopyTracked where
copy =
Unsafe.toLinear \(UnsafeAlias value@(CopyTracked copies retired _)) ->
case unsafePerformIO do
retirementCount <- readIORef retired
if retirementCount == 0
then modifyIORef' copies NonLinear.succ
else NonLinear.error "copy invoked after source retirement" of
() -> value
instance Consumable CopyTracked where
consume =
Unsafe.toLinear \(CopyTracked _ retired _) ->
unsafePerformIO (modifyIORef' retired NonLinear.succ)
instance Dupable CopyTracked where
dup2 = Unsafe.toLinear \value -> (value, value)
instance Movable CopyTracked where
move =
Unsafe.toLinear \value@(CopyTracked _ retired _) ->
case unsafePerformIO (modifyIORef' retired NonLinear.succ) of
() -> Ur value
materializeMoveTracked :: IORef Int -> [(Int, Bool)]
materializeMoveTracked moves =
linearly \linear ->
case VL.toVector
( VL.fromVector
(V.fromList [MoveTracked moves 10 False, MoveTracked moves 20 False])
linear
) of
Ur vector ->
NonLinear.map
(\(MoveTracked _ value wasMoved) -> (value, wasMoved))
(V.toList vector)
discardMaterializedMoveTracked :: IORef Int -> ()
discardMaterializedMoveTracked moves =
linearly \linear ->
case VL.toVector
( VL.fromVector
(V.fromList [MoveTracked moves 10 False, MoveTracked moves 20 False])
linear
) of
Ur _ -> ()
test_materialization :: TestTree
test_materialization =
testGroup
"materialization"
[ testCase "invokes move for every owned element" do
moves <- newIORef 0
materializeMoveTracked moves @?= [(10, True), (20, True)]
moveCount <- readIORef moves
moveCount @?= 2
, testCase "completes moves even when the result is discarded" do
moves <- newIORef 0
_ <- Exception.evaluate (discardMaterializedMoveTracked moves)
moveCount <- readIORef moves
moveCount @?= 2
]
copyAtMutValue :: Int -> [Int] -> (Int, [Int])
copyAtMutValue i xs = linearly \lin -> DataFlow.do
(lin, lin') <- dup lin
vec <- VL.fromList xs lin
runBO lin' Control.do
(mvec, lend) <- borrowM vec
(Ur x, mvec) <- VL.copyAtMut i mvec
let !() = consume mvec
pureAfter (x, unur $ VL.toList (reclaim lend))
retireCopiedResult ::
(Ur CopyTracked, Mut α (VL.Vector CopyTracked)) %1 ->
VL.Vector CopyTracked %1 ->
Int
retireCopiedResult =
Unsafe.toLinear2 \(copiedResult, borrowed) owner ->
consume borrowed `lseq`
case VL.toVector owner of
Ur frozen ->
case V.length frozen of
!_ ->
case copiedResult of
Ur (CopyTracked _ _ value) -> value
copyAtMutAfterRetirement :: IORef Int -> IORef Int -> Int
copyAtMutAfterRetirement copies retired =
linearly \linear -> DataFlow.do
(ownerLinear, runLinear) <- dup linear
runBO runLinear Control.do
(vector, lend) <-
borrowM
(VL.fromList [CopyTracked copies retired 10] ownerLinear)
copiedResult <- VL.copyAtMut 0 vector
pureAfter (retireCopiedResult copiedResult (reclaim lend))
assertCopyAtMutBoundsError :: Int -> Assertion
assertCopyAtMutBoundsError i = do
result <- Exception.try @Exception.ErrorCall $ Exception.evaluate $ copyAtMutValue i [10, 20, 30]
case result of
Left exception ->
assertBool
("unexpected error: " <> Exception.displayException exception)
(("get: index " <> show i <> " out of bound: 3") `List.isPrefixOf` Exception.displayException exception)
Right value -> assertFailure ("expected bounds error, got " <> show value)
test_copyAtMut :: TestTree
test_copyAtMut =
testGroup
"copyAtMut"
[ testCase "copies the selected element and preserves the vector" do
copyAtMutValue 1 [10, 20, 30] @?= (20, [10, 20, 30])
, testCase "completes copying before mutable recovery" do
copies <- newIORef 0
retired <- newIORef 0
copyAtMutAfterRetirement copies retired @?= 10
copyCount <- readIORef copies
copyCount @?= 1
retirementCount <- readIORef retired
retirementCount @?= 1
, testCase "rejects a negative index" do
assertCopyAtMutBoundsError (-1)
, testCase "rejects an index at the upper bound" do
assertCopyAtMutBoundsError 3
]
qsortVec :: (Ord a, Copyable a, Movable a) => V.Vector a -> V.Vector a
qsortVec v = unur $ linearly \lin -> DataFlow.do
(l1, l2) <- dup lin
runBO l1 Control.do
(v, lend) <- borrowM (VL.fromVector v l2)
VL.qsort 8 v
pureAfter $ VL.toVector (reclaim lend)
divideList :: [Int] -> (Int, [Int])
divideList [] = (0, [])
divideList xs =
let v0 = (V.fromList xs)
pivot = v0 V.! (V.length v0 `quot` 2)
in Bi.second unur $ linearly \lin -> DataFlow.do
(l1, l2) <- dup lin
runBO l1 Control.do
(v, lend) <- borrowM (VL.fromList xs l2)
VL.size v & \(Ur len, v) -> Control.do
(lo, hi) <- VL.divide pivot v 0 len
VL.size lo & \(Ur n, lo) -> DataFlow.do
consume lo
consume hi
pureAfter (n, VL.toList $ reclaim lend)
test_divideList :: TestTree
test_divideList =
testGroup
"divideList"
[ testCase "empty" do
divideList [] @?= (0, [])
, testProperty "singleton" do
x <- F.gen $ G.int $ G.between (-100, 100)
F.assert $
P.expect (0, [x])
P..$ ("answer", divideList [x])
, testProperty "non-empty" do
xs <- F.gen $ G.list (G.between (1, 100)) $ G.int $ G.between (0, 100)
let v = V.fromList xs
pivot = v V.! (V.length v `quot` 2)
(off, vs) = divideList xs
(lo, hi) = V.splitAt off $ V.fromList vs
F.collect "length" [ceiling @_ @Int (fromIntegral @_ @Double (V.length v) / 10) * 10]
F.collect "min" [NonLinear.minimum v `quot` 10 * 10]
F.collect "max" [NonLinear.maximum v `quot` 10 * 10]
F.info $ "pivot: " <> show pivot
F.assert $
P.satisfies ("lo <= " <> show pivot, V.all (NonLinear.<= pivot))
P..$ ("lo", lo)
F.assert $
P.satisfies ("hi >= " <> show pivot, V.all (NonLinear.>= pivot))
P..$ ("hi", hi)
]
test_qsort :: TestTree
test_qsort =
testGroup
"qsort"
[ testCase "empty" do
qsortVec (V.empty @Int) @?= V.empty
, testProperty "coincides with Data.List.sort on Ints" do
xs <- F.gen $ G.list (G.between (1, 100)) $ G.int $ G.between (-100, 100)
let v = V.fromList xs
sorted = qsortVec v
F.collect "length" [ceiling @_ @Int (fromIntegral @_ @Double (V.length v) / 10) * 10]
F.collect "min" [NonLinear.minimum v `quot` 10 * 10]
F.collect "max" [NonLinear.maximum v `quot` 10 * 10]
F.collect "sorted" [V.and $ V.zipWith (NonLinear.<=) v (V.tail v)]
F.info $ "input: " <> show xs
F.assert $
P.expect (V.fromList $ List.sort xs)
P..$ ("output", sorted)
]
example1 :: (Int, [Int])
example1 = linearly \lin -> DataFlow.do
(lin, lin') <- dup lin
vec <- VL.fromList [0, 1, 2] lin
runBO lin' Control.do
(mvec, lend) <- borrowM vec
mvec <- VL.modify 0 (+ 3) mvec
mvec <- VL.modify 2 (+ 5) mvec
mvec <- VL.modify 0 (* 4) mvec
let !(Ur svec) = share mvec
Ur n <- VL.copyAt 0 svec
pureAfter $ (n, unur $ VL.toList (reclaim lend))
test_example1 :: TestTree
test_example1 =
testCase "example1" do
example1 @?= (12, [12, 1, 7])
example2 :: (Int, [Int])
example2 = linearly \lin -> DataFlow.do
(lin, lin') <- dup lin
vec <- VL.fromList [0, 1, 2] lin
runBO lin' Control.do
(mvec, lend) <- borrowM vec
let !(mvec1, mvec2) = VL.splitAt 1 mvec
(mvec, ()) <-
parBO
( Control.do
mvec1 <- VL.modify 0 (+ 3) mvec1
VL.modify 0 (* 4) mvec1
)
(consume Control.<$> VL.modify 1 (+ 5) mvec2)
let !(Ur svec) = share mvec
Ur n <- VL.copyAt 0 svec
pureAfter $ (n, unur $ VL.toList (reclaim lend))
test_example2 :: TestTree
test_example2 =
testCase "example2" do
example2 @?= (12, [12, 1, 7])
example3 :: (Int, [Int])
example3 = linearly \lin -> DataFlow.do
(lin, lin') <- dup lin
vec <- VL.fromList [0, 1, 2] lin
runBO lin' Control.do
(mvec, lend) <- borrowM vec
mvec <- reborrowing_ mvec \mvec -> Control.do
let !(mvec1, mvec2) = VL.splitAt 1 mvec
consume
Control.<$> parBO
( Control.do
mvec1 <- VL.modify 0 (+ 3) mvec1
VL.modify 0 (* 4) mvec1
)
(VL.modify 1 (+ 5) mvec2)
let !(Ur svec) = share mvec
Ur n <- VL.copyAt 0 svec
pureAfter $ (n, unur $ VL.toList (reclaim lend))
test_example3 :: TestTree
test_example3 =
testCase "example3" do
example3 @?= (12, [12, 1, 7])
discardingScopes :: (Int, [Int])
discardingScopes = linearly \lin -> DataFlow.do
(lin, lin') <- dup lin
vec <- VL.fromList [10, 20, 30] lin
runBO lin' Control.do
(mvec, lend) <- borrowM vec
mvec <- reborrowing_ mvec \mvec -> Control.do
mvec <- sharing_ mvec \shared ->
consume Control.<$> parBO (VL.copyAt 0 shared) (VL.copyAt 1 shared)
mvec <- VL.modify 0 (+ 1) mvec
Control.pure $ consume mvec
mvec <- VL.modify 2 (+ 2) mvec
let !(Ur svec) = share mvec
Ur n <- VL.copyAt 0 svec
pureAfter $ (n, unur $ VL.toList (reclaim lend))
test_discardingScopes :: TestTree
test_discardingScopes =
testCase "result-discarding scopes restore the outer mutable borrow" do
discardingScopes @?= (11, [11, 20, 32])