packages feed

ychr-0.1.0.0: test/YCHR/Runtime/StoreTest.hs

{-# LANGUAGE OverloadedStrings #-}

module YCHR.Runtime.StoreTest (tests) where

import Control.Monad.IO.Class (liftIO)
import Data.Foldable (toList)
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
import YCHR.Internal.Runtime.Monad (Chr, initSessionEnv, runChr)
import YCHR.Internal.Runtime.Store
import YCHR.Internal.Runtime.Types (SuspensionId (..), Value (..))
import YCHR.Internal.Runtime.Var (equal, newVar, unify)
import YCHR.Internal.Types (ConstraintType (..), Name (..))

tests :: TestTree
tests =
  testGroup
    "YCHR.Internal.Runtime.Store"
    [ createTests,
      storeTests,
      killTests,
      fieldTests,
      iterationTests,
      observerTests
    ]

-- | A 100-slot session is large enough for every test in this module
-- (which use ConstraintType 0/1 and occasionally check ConstraintType 99).
runStoreEnv :: Chr a -> IO a
runStoreEnv action = do
  env <-
    initSessionEnv
      (replicate 100 (Unqualified ""))
      []
      Map.empty
      Map.empty
      Map.empty
      Map.empty
      Set.empty
  runChr action env

-- | Run an action and pair the result with the observer IDs accumulated
-- by the final 'unify' (or an empty list if no 'unify' happened).
runStoreObservers :: Chr [SuspensionId] -> IO [SuspensionId]
runStoreObservers = runStoreEnv

countAlive :: [Suspension] -> Chr Int
countAlive [] = pure 0
countAlive (s : ss) = do
  a <- isSuspAlive s
  rest <- countAlive ss
  pure $ (if a then 1 else 0) + rest

createTests :: TestTree
createTests =
  testGroup
    "createConstraint"
    [ testCase "returns distinct IDs" $ do
        runStoreEnv $ do
          id1 <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          id2 <- createConstraint (ConstraintType 0) [VInt 3, VInt 4]
          liftIO $ assertBool "IDs should differ" (not (idEqual id1 id2)),
      testCase "constraint is alive before storing" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          alive <- aliveConstraint sid
          liftIO $ alive @?= True
    ]

storeTests :: TestTree
storeTests =
  testGroup
    "storeConstraint"
    [ testCase "appears in snapshot" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          storeConstraint sid
          snap <- getStoreSnapshot (ConstraintType 0)
          liftIO $ length snap @?= 1,
      testCase "multiple same type" $ do
        runStoreEnv $ do
          s1 <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          s2 <- createConstraint (ConstraintType 0) [VInt 3, VInt 4]
          storeConstraint s1
          storeConstraint s2
          snap <- getStoreSnapshot (ConstraintType 0)
          liftIO $ length snap @?= 2,
      testCase "different types" $ do
        runStoreEnv $ do
          s1 <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          s2 <- createConstraint (ConstraintType 1) [VInt 5]
          storeConstraint s1
          storeConstraint s2
          snapLeq <- getStoreSnapshot (ConstraintType 0)
          snapGcd <- getStoreSnapshot (ConstraintType 1)
          liftIO $ length snapLeq @?= 1
          liftIO $ length snapGcd @?= 1,
      testCase "empty snapshot for unknown type" $ do
        runStoreEnv $ do
          snap <- getStoreSnapshot (ConstraintType 99)
          liftIO $ length snap @?= 0
    ]

killTests :: TestTree
killTests =
  testGroup
    "killConstraint"
    [ testCase "alive becomes False" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          storeConstraint sid
          killConstraint sid
          alive <- aliveConstraint sid
          liftIO $ alive @?= False,
      testCase "still in snapshot after kill" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          storeConstraint sid
          killConstraint sid
          snap <- getStoreSnapshot (ConstraintType 0)
          liftIO $ length snap @?= 1,
      testCase "doesn't affect other constraints" $ do
        runStoreEnv $ do
          s1 <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          s2 <- createConstraint (ConstraintType 0) [VInt 3, VInt 4]
          storeConstraint s1
          storeConstraint s2
          killConstraint s1
          a1 <- aliveConstraint s1
          a2 <- aliveConstraint s2
          liftIO $ a1 @?= False
          liftIO $ a2 @?= True
    ]

fieldTests :: TestTree
fieldTests =
  testGroup
    "fields"
    [ testCase "getConstraintArg" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 0) [VInt 10, VAtom "x"]
          a0 <- getConstraintArg sid 0
          a1 <- getConstraintArg sid 1
          liftIO $ case a0 of VInt 10 -> pure (); _ -> assertBool "arg 0" False
          liftIO $ case a1 of VAtom "x" -> pure (); _ -> assertBool "arg 1" False,
      testCase "getConstraintType" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 1) [VInt 5]
          t <- getConstraintType sid
          liftIO $ t @?= ConstraintType 1,
      testCase "idEqual same" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          liftIO $ assertBool "same id" (idEqual sid sid),
      testCase "idEqual different" $ do
        runStoreEnv $ do
          s1 <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          s2 <- createConstraint (ConstraintType 0) [VInt 3, VInt 4]
          liftIO $ assertBool "different id" (not (idEqual s1 s2)),
      testCase "isConstraintType true" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          r <- isConstraintType sid (ConstraintType 0)
          liftIO $ r @?= True,
      testCase "isConstraintType false" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          r <- isConstraintType sid (ConstraintType 1)
          liftIO $ r @?= False
    ]

iterationTests :: TestTree
iterationTests =
  testGroup
    "iteration"
    [ testCase "skip dead in snapshot" $ do
        runStoreEnv $ do
          s1 <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          s2 <- createConstraint (ConstraintType 0) [VInt 3, VInt 4]
          storeConstraint s1
          storeConstraint s2
          killConstraint s1
          snap <- getStoreSnapshot (ConstraintType 0)
          alive <- countAlive (toList snap)
          liftIO $ alive @?= 1,
      testCase "new constraints invisible to captured snapshot" $ do
        runStoreEnv $ do
          s1 <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          storeConstraint s1
          snap <- getStoreSnapshot (ConstraintType 0)
          s2 <- createConstraint (ConstraintType 0) [VInt 3, VInt 4]
          storeConstraint s2
          liftIO $ length snap @?= 1
          snap2 <- getStoreSnapshot (ConstraintType 0)
          liftIO $ length snap2 @?= 2,
      testCase "kill visible during iteration via isSuspAlive" $ do
        runStoreEnv $ do
          s1 <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          s2 <- createConstraint (ConstraintType 0) [VInt 3, VInt 4]
          storeConstraint s1
          storeConstraint s2
          snap <- getStoreSnapshot (ConstraintType 0)
          killConstraint s1
          (susp1, susp2) <- liftIO $ case toList snap of
            (s1' : s2' : _) -> pure (s1', s2')
            _ -> assertFailure "expected at least 2 suspensions in store"
          a1 <- isSuspAlive susp1
          a2 <- isSuspAlive susp2
          liftIO $ a1 @?= False
          liftIO $ a2 @?= True,
      testCase "filter by argument equality" $ do
        runStoreEnv $ do
          s1 <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          s2 <- createConstraint (ConstraintType 0) [VInt 3, VInt 4]
          s3 <- createConstraint (ConstraintType 0) [VInt 1, VInt 5]
          storeConstraint s1
          storeConstraint s2
          storeConstraint s3
          snap <- getStoreSnapshot (ConstraintType 0)
          let susps = toList snap
          matches <- filterByArg 0 (VInt 1) susps
          liftIO $ length matches @?= 2,
      testCase "suspArg pure access" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 0) [VInt 10, VAtom "y"]
          storeConstraint sid
          snap <- getStoreSnapshot (ConstraintType 0)
          s <- liftIO $ case toList snap of
            (s : _) -> pure s
            [] -> assertFailure "expected at least 1 suspension in store"
          liftIO $ case suspArg s 0 of VInt 10 -> pure (); _ -> assertBool "arg 0" False
          liftIO $ case suspArg s 1 of VAtom "y" -> pure (); _ -> assertBool "arg 1" False
    ]
  where
    filterByArg :: Int -> Value -> [Suspension] -> Chr [Suspension]
    filterByArg _ _ [] = pure []
    filterByArg idx val (s : ss) = do
      alive <- isSuspAlive s
      if alive
        then do
          eq <- equal (suspArg s idx) val
          rest <- filterByArg idx val ss
          pure $ if eq then s : rest else rest
        else filterByArg idx val ss

observerTests :: TestTree
observerTests =
  testGroup
    "observer registration"
    [ testCase "unifying a constraint's var arg emits SuspensionId" $ do
        obs <- runStoreObservers $ do
          x <- newVar
          sid <- createConstraint (ConstraintType 0) [x, VInt 2]
          storeConstraint sid
          (_, o) <- unify x (VInt 1)
          pure o
        assertBool
          "should contain the suspension id"
          (SuspensionId 0 `elem` obs),
      testCase "ground args produce no observer" $ do
        runStoreEnv $ do
          sid <- createConstraint (ConstraintType 0) [VInt 1, VInt 2]
          storeConstraint sid
          pure (),
      testCase "multiple constraints on same variable" $ do
        obs <- runStoreObservers $ do
          x <- newVar
          s1 <- createConstraint (ConstraintType 0) [x, VInt 2]
          s2 <- createConstraint (ConstraintType 1) [x]
          storeConstraint s1
          storeConstraint s2
          (_, o) <- unify x (VInt 1)
          pure o
        assertBool "should contain s1" (SuspensionId 0 `elem` obs)
        assertBool "should contain s2" (SuspensionId 1 `elem` obs),
      testCase "var nested in a compound arg emits SuspensionId" $ do
        obs <- runStoreObservers $ do
          x <- newVar
          -- The var is nested two levels deep inside compound
          -- arguments, not a bare top-level argument. Reactivation
          -- must still observe it (ωr Reactivate).
          sid <-
            createConstraint
              (ConstraintType 0)
              [VTerm "pair" [VTerm "box" [x], VInt 2]]
          storeConstraint sid
          (_, o) <- unify x (VInt 1)
          pure o
        assertBool
          "should contain the suspension id"
          (SuspensionId 0 `elem` obs)
    ]