packages feed

pure-borrow-0.1.0.0: test/Control/Monad/Borrow/Pure/CopyableSpec.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LinearTypes #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module Control.Monad.Borrow.Pure.CopyableSpec (
  module Control.Monad.Borrow.Pure.CopyableSpec,
) where

import Control.Monad.Borrow.Pure.BO (Mut, Share, linearly, runBO_)
import Control.Monad.Borrow.Pure.BO.Unsafe (Alias (UnsafeAlias))
import Control.Monad.Borrow.Pure.Clone (AsCopyable (AsCopyable), Clone (clone))
import Control.Monad.Borrow.Pure.Copyable
import Control.Monad.Borrow.Pure.Lifetime (Static)
import Data.Complex (Complex ((:+)))
import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef)
import GHC.IO (unsafePerformIO)
import Prelude.Linear
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit
import Unsafe.Linear qualified as Unsafe
import Prelude qualified as NonLinear

data Tracked = Tracked !(IORef Int) !(IORef Bool) !Int

instance Copyable Tracked where
  copy =
    Unsafe.toLinear
      \(UnsafeAlias value@(Tracked copies retired _)) ->
        case unsafePerformIO do
          wasRetired <- readIORef retired
          if wasRetired
            then NonLinear.error "copy evaluated after retirement"
            else modifyIORef' copies NonLinear.succ of
          () -> value

instance Consumable Tracked where
  consume = Unsafe.toLinear \_ -> ()

instance Dupable Tracked where
  dup2 = Unsafe.toLinear \value -> (value, value)

instance Movable Tracked where
  move = Unsafe.toLinear Ur

instance Copyable1 [] where
  liftCopy = genericLiftCopy

trackedValue :: Tracked -> Int
trackedValue (Tracked _ _ value) = value

cloneTracked :: IORef Int -> IORef Bool -> Tracked
cloneTracked copies retired =
  linearly \linear ->
    case runBO_
      linear
      (clone (UnsafeAlias (AsCopyable (Tracked copies retired 10)))) of
      AsCopyable copied -> copied

test_copyStrictness :: TestTree
test_copyStrictness =
  testGroup
    "copy strictness"
    [ testCase "copyMut completes the copy before returning Ur" do
        copies <- newIORef 0
        retired <- newIORef False
        let source = Tracked copies retired 10
            !(Ur copied) =
              copyMut
                (UnsafeAlias source :: Mut Static Tracked)
        copyCount <- readIORef copies
        copyCount @?= 1
        writeIORef retired True
        trackedValue copied @?= 10
    , testCase "generic Copyable forces every copied field" do
        copies <- newIORef 0
        retired <- newIORef False
        let first = Tracked copies retired 10
            second = Tracked copies retired 20
            !(firstCopy, secondCopy) =
              copy
                ( UnsafeAlias (first, second) ::
                    Share Static (Tracked, Tracked)
                )
        copyCount <- readIORef copies
        copyCount @?= 2
        writeIORef retired True
        (trackedValue firstCopy, trackedValue secondCopy) @?= (10, 20)
    , testCase "generic Copyable traverses recursive values before returning" do
        copies <- newIORef 0
        retired <- newIORef False
        let first = Tracked copies retired 10
            second = Tracked copies retired 20
            !copied =
              copy
                ( UnsafeAlias [first, second] ::
                    Share Static [Tracked]
                )
        copyCount <- readIORef copies
        copyCount @?= 2
        writeIORef retired True
        NonLinear.map trackedValue copied @?= [10, 20]
    , testCase "Complex Copyable copies both components" do
        copies <- newIORef 0
        retired <- newIORef False
        let real = Tracked copies retired 10
            imaginary = Tracked copies retired 20
            !(realCopy :+ imaginaryCopy) =
              copy
                ( UnsafeAlias (real :+ imaginary) ::
                    Share Static (Complex Tracked)
                )
        copyCount <- readIORef copies
        copyCount @?= 2
        writeIORef retired True
        (trackedValue realCopy, trackedValue imaginaryCopy) @?= (10, 20)
    , testCase "Complex Double moves without changing its numeric value" do
        case move (10 :+ 20 :: Complex Double) of
          Ur moved -> moved @?= (10 :+ 20)
    , testCase "Copyable1 forces every copied field" do
        copies <- newIORef 0
        retired <- newIORef False
        let real = Tracked copies retired 10
            imaginary = Tracked copies retired 20
            !(realCopy :+ imaginaryCopy) =
              copy1
                ( UnsafeAlias (real :+ imaginary) ::
                    Share Static (Complex Tracked)
                )
        copyCount <- readIORef copies
        copyCount @?= 2
        writeIORef retired True
        (trackedValue realCopy, trackedValue imaginaryCopy) @?= (10, 20)
    , testCase "Copyable1 traverses recursive values before returning" do
        copies <- newIORef 0
        retired <- newIORef False
        let first = Tracked copies retired 10
            second = Tracked copies retired 20
            !copied =
              copy1
                ( UnsafeAlias [first, second] ::
                    Share Static [Tracked]
                )
        copyCount <- readIORef copies
        copyCount @?= 2
        writeIORef retired True
        NonLinear.map trackedValue copied @?= [10, 20]
    , testCase "Clone through AsCopyable completes copying inside BO" do
        copies <- newIORef 0
        retired <- newIORef False
        let !copied = cloneTracked copies retired
        copyCount <- readIORef copies
        copyCount @?= 1
        writeIORef retired True
        trackedValue copied @?= 10
    ]