{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} -- | -- SPDX-License-Identifier: BSD-3-Clause -- -- Swarm unit tests module TestEval where import Control.Lens ((^.), _3) import Data.Char (ord) import Data.Map qualified as M import Data.Text (Text) import Data.Text qualified as T import Swarm.Game.State import Swarm.Language.Value import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck import TestUtil import Witch (from) testEval :: GameState -> TestTree testEval g = testGroup "Language - evaluation" [ testGroup "arithmetic" [ testProperty "addition" (\a b -> binOp a "+" b `evaluatesToP` VInt (a + b)) , testProperty "subtraction" (\a b -> binOp a "-" b `evaluatesToP` VInt (a - b)) , testProperty "multiplication" (\a b -> binOp a "*" b `evaluatesToP` VInt (a * b)) , testProperty "division" (\a (NonZero b) -> binOp a "/" b `evaluatesToP` VInt (a `div` b)) , testProperty "exponentiation" (\a (NonNegative b) -> binOp a "^" b `evaluatesToP` VInt (a ^ (b :: Integer))) ] , testGroup "int comparison" [ testProperty "==" (\a b -> binOp a "==" b `evaluatesToP` VBool ((a :: Integer) == b)) , testProperty "<" (\a b -> binOp a "<" b `evaluatesToP` VBool ((a :: Integer) < b)) , testProperty "<=" (\a b -> binOp a "<=" b `evaluatesToP` VBool ((a :: Integer) <= b)) , testProperty ">" (\a b -> binOp a ">" b `evaluatesToP` VBool ((a :: Integer) > b)) , testProperty ">=" (\a b -> binOp a ">=" b `evaluatesToP` VBool ((a :: Integer) >= b)) , testProperty "!=" (\a b -> binOp a "!=" b `evaluatesToP` VBool ((a :: Integer) /= b)) ] , testGroup "pair comparison" [ testProperty "==" (\a b -> binOp a "==" b `evaluatesToP` VBool ((a :: (Integer, Integer)) == b)) , testProperty "<" (\a b -> binOp a "<" b `evaluatesToP` VBool ((a :: (Integer, Integer)) < b)) , testProperty "<=" (\a b -> binOp a "<=" b `evaluatesToP` VBool ((a :: (Integer, Integer)) <= b)) , testProperty ">" (\a b -> binOp a ">" b `evaluatesToP` VBool ((a :: (Integer, Integer)) > b)) , testProperty ">=" (\a b -> binOp a ">=" b `evaluatesToP` VBool ((a :: (Integer, Integer)) >= b)) , testProperty "!=" (\a b -> binOp a "!=" b `evaluatesToP` VBool ((a :: (Integer, Integer)) /= b)) ] , testGroup "boolean operators" [ testCase "and" ("true && false" `evaluatesTo` VBool False) , testCase "or" ("true || false" `evaluatesTo` VBool True) ] , testGroup "sum types #224" [ testCase "inl" ("inl 3" `evaluatesTo` VInj False (VInt 3)) , testCase "inr" ("inr \"hi\"" `evaluatesTo` VInj True (VText "hi")) , testCase "inl a < inl b" ("inl 3 < inl 4" `evaluatesTo` VBool True) , testCase "inl b < inl a" ("inl 4 < inl 3" `evaluatesTo` VBool False) , testCase "inl < inr" ("inl 3 < inr true" `evaluatesTo` VBool True) , testCase "inl 4 < inr 3" ("inl 4 < inr 3" `evaluatesTo` VBool True) , testCase "inr < inl" ("inr 3 < inl true" `evaluatesTo` VBool False) , testCase "inr 3 < inl 4" ("inr 3 < inl 4" `evaluatesTo` VBool False) , testCase "inr a < inr b" ("inr 3 < inr 4" `evaluatesTo` VBool True) , testCase "inr b < inr a" ("inr 4 < inr 3" `evaluatesTo` VBool False) , testCase "case inl" ("case (inl 2) (\\x. x + 1) (\\y. y * 17)" `evaluatesTo` VInt 3) , testCase "case inr" ("case (inr 2) (\\x. x + 1) (\\y. y * 17)" `evaluatesTo` VInt 34) , testCase "nested 1" ("(\\x : int + bool + text. case x (\\q. 1) (\\s. case s (\\y. 2) (\\z. 3))) (inl 3)" `evaluatesTo` VInt 1) , testCase "nested 2" ("(\\x : int + bool + text. case x (\\q. 1) (\\s. case s (\\y. 2) (\\z. 3))) (inr (inl false))" `evaluatesTo` VInt 2) , testCase "nested 2" ("(\\x : int + bool + text. case x (\\q. 1) (\\s. case s (\\y. 2) (\\z. 3))) (inr (inr \"hi\"))" `evaluatesTo` VInt 3) ] , testGroup "operator evaluation" [ testCase "application operator #239" ("fst $ snd $ (1,2,3)" `evaluatesTo` VInt 2) ] , testGroup "recursive bindings" [ testCase "factorial" ("let fac = \\n. if (n==0) {1} {n * fac (n-1)} in fac 15" `evaluatesTo` VInt 1307674368000) , testCase "loop detected" ("let x = x in x" `throwsError` ("loop detected" `T.isInfixOf`)) ] , testGroup "delay" [ testCase "force / delay" ("force {10}" `evaluatesTo` VInt 10) , testCase "force x2 / delay x2" ("force (force { {10} })" `evaluatesTo` VInt 10) , testCase "if is lazy" ("if true {1} {1/0}" `evaluatesTo` VInt 1) , testCase "function with if is not lazy" ( "let f = \\x. \\y. if true {x} {y} in f 1 (1/0)" `throwsError` ("by zero" `T.isInfixOf`) ) , testCase "memoization baseline" ( "def fac = \\n. if (n==0) {1} {n * fac (n-1)} end; def f10 = fac 10 end; let x = f10 in noop" `evaluatesToInAtMost` (VUnit, 535) ) , testCase "memoization" ( "def fac = \\n. if (n==0) {1} {n * fac (n-1)} end; def f10 = fac 10 end; let x = f10 in let y = f10 in noop" `evaluatesToInAtMost` (VUnit, 540) ) ] , testGroup "conditions" [ testCase "if true" ("if true {1} {2}" `evaluatesTo` VInt 1) , testCase "if false" ("if false {1} {2}" `evaluatesTo` VInt 2) , testCase "if (complex condition)" ("if (let x = 3 + 7 in not (x < 2^5)) {1} {2}" `evaluatesTo` VInt 2) ] , testGroup "exceptions" [ testCase "fail" ("fail \"foo\"" `throwsError` ("foo" `T.isInfixOf`)) , testCase "try / no exception 1" ("try {return 1} {return 2}" `evaluatesTo` VInt 1) , testCase "try / no exception 2" ("try {return 1} {let x = x in x}" `evaluatesTo` VInt 1) , testCase "try / fail" ("try {fail \"foo\"} {return 3}" `evaluatesTo` VInt 3) , testCase "try / fail / fail" ("try {fail \"foo\"} {fail \"bar\"}" `throwsError` ("bar" `T.isInfixOf`)) , testCase "try / div by 0" ("try {return (1/0)} {return 3}" `evaluatesTo` VInt 3) ] , testGroup "text" [ testCase "format int" ("format 1" `evaluatesTo` VText "1") , testCase "format sum" ("format (inl 1)" `evaluatesTo` VText "inl 1") , testCase "format function" ("format (\\x. x + 1)" `evaluatesTo` VText "\\x. x + 1") , testCase "concat" ("\"x = \" ++ format (2+3) ++ \"!\"" `evaluatesTo` VText "x = 5!") , testProperty "number of characters" ( \s -> ("chars " <> tquote s) `evaluatesToP` VInt (fromIntegral $ length s) ) , testProperty "split undo concatenation" ( \s1 s2 -> -- \s1.\s2. (s1,s2) == split (chars s1) (s1 ++ s2) let (t1, t2) = (tquote s1, tquote s2) in T.concat ["(", t1, ",", t2, ") == split (chars ", t1, ") (", t1, " ++ ", t2, ")"] `evaluatesToP` VBool True ) , testProperty "charAt" $ \i (NonEmpty s) -> let i' = i `mod` length s in T.concat ["charAt ", from @String (show i'), " ", tquote s] `evaluatesToP` VInt (fromIntegral (ord (s !! i'))) , testCase "toChar 97" ("toChar 97 == \"a\"" `evaluatesTo` VBool True) , testProperty "chars/toChar" ( \(NonNegative (i :: Integer)) -> T.concat ["chars (toChar ", from @String (show i), ")"] `evaluatesToP` VInt 1 ) , testProperty "charAt/toChar" ( \(NonNegative i) -> T.concat ["charAt 0 (toChar ", from @String (show i), ")"] `evaluatesToP` VInt i ) ] , testGroup "records - #1093" [ testCase "empty record" ("[]" `evaluatesTo` VRcd M.empty) , testCase "singleton record" ("[y = 3 + 4]" `evaluatesTo` VRcd (M.singleton "y" (VInt 7))) , testCase "record equality up to reordering" ("[x = 2, y = 3] == [y = 3, x = 2]" `evaluatesTo` VBool True) , testCase "record projection" ("[x = 2, y = 3].x" `evaluatesTo` VInt 2) , testCase "nested record projection" ("let r = [x=2, y=3] in let z = [q = r, n=\"hi\"] in z.q.y" `evaluatesTo` VInt 3) , testCase "record punning" ( "let x = 2 in let y = 3 in [x,y,z=\"hi\"]" `evaluatesTo` VRcd (M.fromList [("x", VInt 2), ("y", VInt 3), ("z", VText "hi")]) ) , testCase "record comparison" ("[y=1, x=2] < [x=3,y=0]" `evaluatesTo` VBool True) , testCase "record comparison" ("[y=1, x=3] < [x=3,y=0]" `evaluatesTo` VBool False) , testCase "record function" ("let f : [x:int, y:text] -> int = \\r.r.x + 1 in f [x=3,y=\"hi\"]" `evaluatesTo` VInt 4) , testCase "format record" ("format [y = 2, x = 1+2]" `evaluatesTo` VText "[x = 3, y = 2]") , testCase "record fields don't scope over other fields" ( "let x = 1 in [x = x + 1, y = x]" `evaluatesTo` VRcd (M.fromList [("x", VInt 2), ("y", VInt 1)]) ) ] ] where tquote :: String -> Text tquote = T.pack . show throwsError :: Text -> (Text -> Bool) -> Assertion throwsError tm p = do result <- evaluate tm case result of Right _ -> assertFailure "Unexpected success" Left err -> p err @? "Expected predicate did not hold on error message " ++ from @Text @String err evaluatesTo :: Text -> Value -> Assertion evaluatesTo tm val = do result <- evaluate tm assertEqual "" (Right val) (fst <$> result) evaluatesToP :: Text -> Value -> Property evaluatesToP tm val = ioProperty $ do result <- evaluate tm return $ Right val === (fst <$> result) evaluatesToInAtMost :: Text -> (Value, Int) -> Assertion evaluatesToInAtMost tm (val, maxSteps) = do result <- evaluate tm case result of Left err -> assertFailure ("Evaluation failed: " ++ from @Text @String err) Right (v, steps) -> do assertEqual "" val v assertBool ("Took more than " ++ show maxSteps ++ " steps!") (steps <= maxSteps) evaluate :: Text -> IO (Either Text (Value, Int)) evaluate = fmap (^. _3) . eval g binOp :: Show a => a -> String -> a -> Text binOp a op b = from @String (p (show a) ++ op ++ p (show b)) where p x = "(" ++ x ++ ")"