moonlight-pale-0.1.0.0: test/laws/LatticeSpec.hs
{-# LANGUAGE GHC2024 #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wmissing-local-signatures #-}
module LatticeSpec
( tests,
)
where
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Moonlight.Pale.Test.Laws.Lattice
( FiniteLattice,
FiniteLatticeError (..),
LatticeBounds (..),
compileFiniteLattice,
finiteLatticeJoin,
finiteLatticeLaws,
finiteLatticeMeet,
)
import Moonlight.Pale.Test.Laws.Suite (lawGroup, renderLawSuite)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertEqual, assertFailure, testCase)
import Test.Tasty.QuickCheck
( Gen,
Property,
chooseInt,
conjoin,
counterexample,
forAll,
testProperty,
vectorOf,
(===),
)
data Diamond
= DiamondBottom
| DiamondLeft
| DiamondRight
| DiamondTop
deriving stock (Eq, Ord, Show)
data OpenValue
= OpenBottom
| OpenTop
| EscapedValue
deriving stock (Eq, Ord, Show)
data TableValue
= TableValue !Int
| MissingTableEntry !Int !Int
deriving stock (Eq, Ord, Show)
data ClosedOperationTables = ClosedOperationTables
{ closedTableUniverse :: !(NonEmpty TableValue),
closedJoinTable :: !(Map (TableValue, TableValue) TableValue),
closedMeetTable :: !(Map (TableValue, TableValue) TableValue)
}
deriving stock (Show)
tests :: TestTree
tests =
testGroup
"Moonlight.Pale.Test.Laws.Lattice"
[ renderFiniteLattice "Bool bounded lattice" boolLattice,
renderFiniteLattice "diamond bounded lattice" diamondLattice,
testCase "bounds outside the universe are typed construction errors" $
assertLatticeErrors
"top is absent"
(TopOutsideUniverse True :| [])
invalidBoundedLattice,
testCase "duplicate universe values retain both positions" $
assertLatticeErrors
"the second False duplicates the first"
(DuplicateUniverseElement False 0 2 :| [])
duplicateUniverseLattice,
testCase "join closure failures name operands and escaped results" $
case closureFailureLattice of
Left errors ->
assertEqual
"every ordered pair was evaluated once and rejected"
( fmap
(\(leftValue, rightValue) ->
JoinOutsideUniverse leftValue rightValue EscapedValue
)
openPairs
)
(toList errors)
Right _ -> assertFailure "expected escaped join results to reject compilation",
testCase "dense lookup returns the original operation results" $
case diamondLattice of
Left errors -> assertFailure ("expected a compiled diamond: " <> show errors)
Right lattice -> do
assertEqual
"join table"
(Right DiamondTop)
(finiteLatticeJoin lattice DiamondLeft DiamondRight)
assertEqual
"meet table"
(Right DiamondBottom)
(finiteLatticeMeet lattice DiamondLeft DiamondRight),
testProperty
"compiled dense tables agree with a simple list oracle"
finiteLatticeDifferentialProperty
]
boolLattice :: Either (NonEmpty (FiniteLatticeError Bool)) (FiniteLattice Bool)
boolLattice =
compileFiniteLattice
"Bool"
(False :| [True])
(||)
(&&)
(Just (LatticeBounds False True))
diamondLattice :: Either (NonEmpty (FiniteLatticeError Diamond)) (FiniteLattice Diamond)
diamondLattice =
compileFiniteLattice
"diamond"
diamondUniverse
diamondJoin
diamondMeet
(Just (LatticeBounds DiamondBottom DiamondTop))
invalidBoundedLattice :: Either (NonEmpty (FiniteLatticeError Bool)) (FiniteLattice Bool)
invalidBoundedLattice =
compileFiniteLattice
"invalid Bool"
(False :| [])
(||)
(&&)
(Just (LatticeBounds False True))
duplicateUniverseLattice :: Either (NonEmpty (FiniteLatticeError Bool)) (FiniteLattice Bool)
duplicateUniverseLattice =
compileFiniteLattice
"duplicate Bool"
(False :| [True, False])
(||)
(&&)
Nothing
closureFailureLattice :: Either (NonEmpty (FiniteLatticeError OpenValue)) (FiniteLattice OpenValue)
closureFailureLattice =
compileFiniteLattice
"open operation"
openUniverse
(\_ _ -> EscapedValue)
openMeet
Nothing
openUniverse :: NonEmpty OpenValue
openUniverse = OpenBottom :| [OpenTop]
openPairs :: [(OpenValue, OpenValue)]
openPairs =
(,) <$> toList openUniverse <*> toList openUniverse
openMeet :: OpenValue -> OpenValue -> OpenValue
openMeet leftValue rightValue
| leftValue == OpenBottom = OpenBottom
| rightValue == OpenBottom = OpenBottom
| otherwise = OpenTop
diamondUniverse :: NonEmpty Diamond
diamondUniverse = DiamondBottom :| [DiamondLeft, DiamondRight, DiamondTop]
diamondJoin :: Diamond -> Diamond -> Diamond
diamondJoin leftValue rightValue
| diamondLeq leftValue rightValue = rightValue
| diamondLeq rightValue leftValue = leftValue
| otherwise = DiamondTop
diamondMeet :: Diamond -> Diamond -> Diamond
diamondMeet leftValue rightValue
| diamondLeq leftValue rightValue = leftValue
| diamondLeq rightValue leftValue = rightValue
| otherwise = DiamondBottom
diamondLeq :: Diamond -> Diamond -> Bool
diamondLeq leftValue rightValue =
leftValue == rightValue || leftValue == DiamondBottom || rightValue == DiamondTop
renderFiniteLattice ::
Show a =>
String ->
Either (NonEmpty (FiniteLatticeError a)) (FiniteLattice a) ->
TestTree
renderFiniteLattice label latticeResult =
case latticeResult of
Left errors ->
testCase (label <> " compiles") $
assertFailure ("expected valid finite lattice: " <> show errors)
Right lattice ->
renderLawSuite (lawGroup label (finiteLatticeLaws lattice))
assertLatticeErrors ::
(Eq a, Show a) =>
String ->
NonEmpty (FiniteLatticeError a) ->
Either (NonEmpty (FiniteLatticeError a)) (FiniteLattice a) ->
IO ()
assertLatticeErrors label expectedErrors latticeResult =
case latticeResult of
Left actualErrors -> assertEqual label expectedErrors actualErrors
Right _ -> assertFailure (label <> ": expected finite lattice compilation to fail")
finiteLatticeDifferentialProperty :: Property
finiteLatticeDifferentialProperty =
forAll closedOperationTablesGenerator $ \tables ->
case
compileFiniteLattice
"generated"
(closedTableUniverse tables)
(operationFromTable (closedJoinTable tables))
(operationFromTable (closedMeetTable tables))
Nothing
of
Left errors ->
counterexample ("closed operation table was rejected: " <> show errors) False
Right lattice ->
conjoin $
fmap
(\(leftValue, rightValue) ->
conjoin
[ finiteLatticeJoin lattice leftValue rightValue
=== Right (operationFromTable (closedJoinTable tables) leftValue rightValue),
finiteLatticeMeet lattice leftValue rightValue
=== Right (operationFromTable (closedMeetTable tables) leftValue rightValue)
]
)
(simplePairs (toList (closedTableUniverse tables)))
closedOperationTablesGenerator :: Gen ClosedOperationTables
closedOperationTablesGenerator = do
cardinality <- chooseInt (1, 4)
let universe = TableValue 0 :| fmap TableValue [1 .. cardinality - 1]
pairs = simplePairs (toList universe)
joinResults <- vectorOf (cardinality * cardinality) (TableValue <$> chooseInt (0, cardinality - 1))
meetResults <- vectorOf (cardinality * cardinality) (TableValue <$> chooseInt (0, cardinality - 1))
pure
ClosedOperationTables
{ closedTableUniverse = universe,
closedJoinTable = Map.fromList (zip pairs joinResults),
closedMeetTable = Map.fromList (zip pairs meetResults)
}
simplePairs :: [a] -> [(a, a)]
simplePairs values =
(,) <$> values <*> values
operationFromTable ::
Map (TableValue, TableValue) TableValue ->
TableValue ->
TableValue ->
TableValue
operationFromTable table leftValue rightValue =
case (leftValue, rightValue) of
(TableValue leftIndex, TableValue rightIndex) ->
Map.findWithDefault
(MissingTableEntry leftIndex rightIndex)
(leftValue, rightValue)
table
(MissingTableEntry leftIndex rightIndex, _) ->
MissingTableEntry leftIndex rightIndex
(_, MissingTableEntry leftIndex rightIndex) ->
MissingTableEntry leftIndex rightIndex
toList :: NonEmpty a -> [a]
toList (firstValue :| remainingValues) =
firstValue : remainingValues