git @ Cat's Eye Technologies Robin / master src / QuickCheckTests.hs
master

Tree @master (Download .tar.gz)

QuickCheckTests.hs @masterraw · history · blame

-- Copyright (c) 2012-2024, Chris Pressey, Cat's Eye Technologies.
-- This file is distributed under a 2-clause BSD license.  See LICENSES/ dir.
-- SPDX-License-Identifier: LicenseRef-BSD-2-Clause-X-Robin

module QuickCheckTests where

import Test.QuickCheck

import Data.Int

import Language.Robin.Expr
import Language.Robin.Env (Env, mergeEnvs, fromList, find, insert, empty)
import Language.Robin.Eval (eval)
import Language.Robin.Parser (parseToplevel, parseExpr)
import Language.Robin.Intrinsics (robinIntrinsics)
import Language.Robin.Builtins (robinBuiltins)
import qualified Language.Robin.CmdLine as CmdLine


stdEval env expr = eval env expr id


instance Arbitrary Expr where
    --
    -- Note that it's important that we narrow down how many subexpressions each expression
    -- has, otherwise we generate a lot of infinitely large expressions, which don't help.
    -- This is modelled after the technique shown here: https://stackoverflow.com/a/15959889
    --
    arbitrary = sized arbExpr where
        arbExpr :: Int -> Gen Expr
        arbExpr 0 = oneof [
                            Symbol <$> arbitrary,
                            Boolean <$> arbitrary,
                            Number <$> arbitrary
                          ]
        arbExpr n = do
          (Positive m) <- arbitrary
          let n' = n `div` (m + 1)
          oneof [
                   -- Abort <$> (arbExpr n'),   NOTE many laws do not hold in the presence of abort values
                   List <$> (arbExprList n')
                   -- TODO Operator ??
                ]

        arbExprList :: Int -> Gen [Expr]
        arbExprList 0 = do
            return []
        arbExprList n = do
            (Positive m) <- arbitrary
            let n' = n `div` (m + 1)
            head <- arbExpr n'
            tail <- arbExprList n'
            return (head:tail)


-- TODO: establish some arbitrary newtypes too, like RobinEnv, and RobinList, which
--       are restricted in the set of terms they will generate.

--
-- (gt? a b) should match Haskell's `a > b` in all cases.
--
propGt :: Env -> Int32 -> Int32 -> Bool
propGt env a b =
    stdEval env expr == Boolean (a > b)
    where
        expr = List [Symbol "gt?", Number a, Number b]

--
-- (lt? a b) should match Haskell's `a < b` in all cases.
--
propLt :: Env -> Int32 -> Int32 -> Bool
propLt env a b =
    stdEval env expr == Boolean (a < b)
    where
        expr = List [Symbol "lt?", Number a, Number b]

--
-- env? should evaluate to true on any valid binding alist.
--
propEnv :: Env -> [(String, Int32)] -> Bool
propEnv env entries =
    stdEval env expr == Boolean True
    where
        expr = List [Symbol "env?", List [Symbol "literal", alist]]
        alist = fromList $ map (\(k,v) -> (k, Number v)) entries


--
-- The following should be true for any symbol s and binding alist a:
-- (lookup s (delete s a))) == ()
--
propDel :: Env -> String -> [(String, Int32)] -> Property
propDel env sym entries =
    sym /= "" ==> (stdEval env expr == List [])
    where
        litSym = List [Symbol "literal", Symbol sym]
        expr = List [Symbol "lookup", litSym, List [Symbol "delete", litSym, List [Symbol "literal", alist]]]
        alist = fromList $ map (\(k,v) -> (k, Number v)) entries

--
-- The following should be true for any symbol s and binding alist a:
-- (lookup s (extend s 1 x))) == (1)
--
propExt :: Env -> String -> [(String, Int32)] -> Property
propExt env sym entries =
    sym /= "" ==> (stdEval env expr == List [Number 1])
    where
        litSym = List [Symbol "literal", Symbol sym]
        expr = List [Symbol "lookup", litSym, List [Symbol "extend", litSym, Number 1, List [Symbol "literal", alist]]]
        alist = fromList $ map (\(k,v) -> (k, Number v)) entries


--
-- The following should be true for any expression e:
-- (equal? (literal e) (literal e))
--
propEqual :: Env -> Expr -> Bool
propEqual env e =
    stdEval env expr == Boolean True
    where
        expr = List [Symbol "equal?", List [Symbol "literal", e], List [Symbol "literal", e]]


--
-- The following should be true for any expr e and list l:
-- (tail (prepend e l)) is a list
--
propList :: Env -> Expr -> Expr -> Property
propList env e l =
    isList l ==> (isList (stdEval env expr))
    where
        expr = List [Symbol "tail", List [Symbol "prepend", List [Symbol "literal", e], List [Symbol "literal", l]]]

--
-- The following should be true for any equivalent applicable expressions e1 and e2 and any list l:
-- (apply (prepend e1 l)) == (apply (prepend e2 l))
--

propEquivApply :: Env -> Expr -> Expr -> Expr -> Property
propEquivApply env e1 e2 l =
    isList l ==> (r1 == r2)
    where
        r1 = stdEval env (append (List [e1]) l)
        r2 = stdEval env (append (List [e1]) l)

--
-- ========================================================
--


testBuiltins = do
    (env, secondaryDefs) <- CmdLine.loadEnv "pkg/stdlib.robin" (mergeEnvs robinIntrinsics robinBuiltins)
    quickCheck (propEnv env)
    quickCheck (propDel env)
    quickCheck (propExt env)
    quickCheck (propList env)
    quickCheck (propEqual env)


testNoBuiltins = do
    (noBuiltinsEnv, _) <- CmdLine.loadEnv "pkg/stdlib.robin" robinIntrinsics
    quickCheck (propGt noBuiltinsEnv)
    quickCheck (propLt noBuiltinsEnv)


testSecondaryDefEnv (List []) env = return ()
testSecondaryDefEnv (List (List [Symbol name, secondaryDef]:rest)) env = do
    let Just primaryDef = find name env
    putStrLn $ "Comparing multiple definitions of " ++ name ++ "..."
    quickCheckWith stdArgs{ maxSuccess=1000 } (propEquivApply primaryDef secondaryDef)
    testSecondaryDefEnv (List rest) env


testSecondaryDefs = do
    (env, secondaryDefs) <- CmdLine.loadEnv "pkg/stdlib.robin" (mergeEnvs robinIntrinsics robinBuiltins)
    testSecondaryDefEnv secondaryDefs env


main = do
    testBuiltins
    testNoBuiltins
    testSecondaryDefs