{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE MagicHash #-}
{-# OPTIONS_GHC -fdefer-type-errors #-}
module Data.Map.JustifiedSpec (main, spec) where
import Prelude hiding (lookup)
import Test.Hspec
import Test.QuickCheck
import Test.ShouldNotTypecheck
import Data.Map.Justified
import qualified Data.Map as M
import Data.Maybe
import GHC.Prim
import GHC.Int
import System.Mem (performGC)
import System.IO.Unsafe (unsafePerformIO)
import Data.Map.Justified.Tutorial (adjacencies)
import Data.Type.Coercion
main :: IO ()
main = hspec spec
letters :: M.Map Char Int
letters = M.fromList (Prelude.zip ['a'..'z'] [1..])
newtype AlphaNum = AlphaNum Char deriving Show
instance Arbitrary AlphaNum where
arbitrary = elements (map AlphaNum $ ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'])
spec :: Spec
spec = do
describe "using Data.Map.Justified.Map" $ do
it "wraps the underlying Data.Map.Map without modification" $ do
withMap letters theMap == letters
it "can create singleton maps" $ property $ do
\(k :: Int, v :: Int) -> withSingleton k v (uncurry lookup) == v
\(k :: Int, v :: Int) -> withSingleton k v (theMap . snd) == M.fromList [(k,v)]
describe "validated keys" $ do
it "can be obtained iff a key is present" $ property $
\(AlphaNum c) -> isJust (M.lookup c letters) == withMap letters (isJust . member c)
it "will access the expected value" $ do
map (\k -> fromJust $ M.lookup k letters) (M.keys letters)
`shouldBe`
withMap letters (\m -> map (`lookup` m) (keys m))
it "can degrade to the original key" $ do
M.keys letters `shouldBe` withMap letters (map theKey . keys)
it "can still be used in an adjusted map" $ do
withMap letters (\m -> let (k:k':_) = keys m in
(lookup k (reinsert k 17 m),
lookup k (reinsert k' 17 m),
lookup k (adjust (+1) k m),
lookup k (adjustWithKey
(\k v -> v + fromEnum (theKey k)) k m)))
`shouldBe` (17,1,2,98)
it "can not escape `withMap`" $ shouldNotTypecheck $
withMap letters (head keys)
it "can not be used in unrelated maps" $ shouldNotTypecheck $
withMap letters (\m ->
let k = head (keys m) in withMap letters (lookup k))
it "can not be directly created" $ shouldNotTypecheck $
let k = Key 'a' in withMap letters (lookup k)
describe "when adding keys" $ do
it "can translate old keys to new map" $ property $
\(AlphaNum c, AlphaNum c') -> withMap letters $
\m -> inserting c' 100 m $
\(_, upgrade, m') ->
let k = member c m
k' = member c m'
in c /= c' ==> fmap upgrade k == k'
it "does not let a new key be used in the old map without translation" $
shouldNotTypecheck $
withMap letters $ \m -> inserting 'X' 100 m $
\(_, _, m') -> map (`lookup` m) (keys m')
describe "when removing keys" $ do
it "can translate new keys to old map" $ property $
\(AlphaNum c, AlphaNum c') -> withMap letters $
\m -> deleting c' m $
\(downgrade, m') ->
let k = member c m
k' = member c m'
in c /= c' ==> k == fmap downgrade k'
it "does not let an old key be used in the new map without translation" $
shouldNotTypecheck $
withMap letters $ \m -> deleting 'X' m $
\(_, _, m') -> map (`lookup` m') (keys m)
describe "indices" $ do
it "cannot be used as keys" $
shouldNotTypecheck $
withSingleton (17 :: Int) () $ \(k,m) -> m ! findIndex k m
it "cannot be used instead of keys" $
shouldNotTypecheck $
withSingleton (17 :: Int) () $ \(k,m) -> snd (elemAt k m)
describe "at runtime" $ do
it "does not allocate map copies when gathering evidence" $ do
withMap letters $ \m -> m `isLiterally` letters
it "does not allocate map copies when forgetting evidence" $ do
withMap letters $ \m -> theMap m `isLiterally` letters
it "does not allocate key copies when gathering key evidence" $ do
withMap letters $ \m ->
each (M.keys letters) $ \k -> (k `member` m) `isLiterallyJust` k
it "does not allocate key copies when forgetting evidence" $ do
withMap letters $ \m ->
let ks = keys m
-- This depends on the Prelude rewrite rule "map coerce/coerce" firing, I think.
in map theKey ks `isLiterally` ks
it "does not allocate keys during evidence conversion for insertion" $ do
withMap letters $ \m ->
inserting 'X' 100 m $ \(_, upgraded, _) ->
each (keys m) $ \k -> upgraded k `isLiterally` k
it "does not allocate keys during evidence conversion for deletion" $ do
withMap letters $ \m ->
deleting 'X' m $ \(downgraded, m') ->
each (keys m') $ \k -> downgraded k `isLiterally` k
it "does not allocate values when performing lookup with a verified key" $ do
withMap letters $ \m ->
each (keys m) $ \k ->
(theKey k `M.lookup` theMap m) `isLiterallyJust` (k `lookup` m)
it "does not allocate when verifying a recursive map" $ fromRight $ do
withRecMap adjacencies $ \m -> m `isLiterally` adjacencies
-- | Test if two values occupy the same location in memory.
-- This is almost certainly flaky, especially if a GC occurs
-- during evaluation of ans!
isLiterally :: a -> b -> Bool
isLiterally x y = unsafePerformIO $ do
let x' = unsafeCoerce# y
x `seq` x' `seq` performGC
return $ I64# (reallyUnsafePtrEquality# x x') == 1
{-# NOINLINE isLiterally #-}
-- | Test if the lhs is Just (exactly the rhs). This is at least
-- as flaky as 'isLiterally'.
isLiterallyJust :: Maybe a -> b -> Bool
isLiterallyJust x y = maybe False id (isLiterally <$> x <*> Just y)
each :: Traversable t => t a -> (a -> Bool) -> Bool
each = flip all
fromRight :: Either a b -> b
fromRight = either (error "expected Right, got Left") id