Compare commits

..

47 Commits

Author SHA1 Message Date
78ee47c2e6 🤐 Zip 2024-10-21 18:47:06 +02:00
52610b1ed8 🤐 Zip 2024-10-21 18:45:16 +02:00
d57412b454 📝 new test 2024-10-21 18:35:54 +02:00
ea542df037 🔪 Killing workers 2024-10-21 17:41:01 +02:00
f38281b346 💥 Crashing 2024-10-21 16:49:16 +02:00
638786f8c2 🤖 task 3 and 4 2024-10-21 16:33:05 +02:00
b1335209b6 ✏️ 2024-10-21 11:47:34 +02:00
63bdbe688f 🤓 Can run jobs 2024-10-21 11:35:42 +02:00
849ce2858f :clown-face: fixed a test 2024-10-21 10:17:40 +02:00
5a9e4d675b 2024-10-18 10:14:00 +02:00
7f0191098e 📬 nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare nightmare 2024-10-16 16:54:24 +02:00
46154359eb 📬 help 2024-10-16 16:15:38 +02:00
775013b825 A6 2024-10-16 13:08:00 +02:00
b0b087648c 2024-10-11 16:30:03 +02:00
ff512028f3 2024-10-11 16:22:47 +02:00
8ab279d488 🤡 onlyCheckedErrors 2024-10-11 15:46:28 +02:00
533f16ba81 🤡 parsePrinted 2024-10-11 15:17:55 +02:00
e994dbda38 🤡 2024-10-11 14:41:07 +02:00
38b4e22c1e A5 2024-10-10 11:14:29 +02:00
8d35d03be6 🤡 PDF 2024-10-06 18:19:50 +02:00
4470d4ea3c 🤐 Zip 2024-10-06 18:00:33 +02:00
f6cb79a62f :hat: Conforming to other code 2024-10-06 17:32:05 +02:00
13dd49ee75 2024-10-06 16:45:13 +02:00
067f70622f :) 2024-10-06 15:30:00 +02:00
95ad5d0b02 Add TryCatchOp support for runEvalIO 2024-10-06 15:09:20 +02:00
94ba5579c8 :( 2024-10-06 14:39:27 +02:00
b2d7c75b01 📚 3.2 done bitchessssssssssss 2024-10-04 17:23:29 +02:00
d5b072851e 2024-10-04 17:22:54 +02:00
35a7b6cfec 🔥 2024-10-04 15:59:25 +02:00
c0b4dfb0d4 🤡 database file 2024-10-04 15:58:21 +02:00
4ddb42582a :clown: KV-store 2024-10-04 14:45:28 +02:00
46aa789d64 try-catch 2024-10-04 13:29:34 +02:00
fb7f5c936a 🚀 Catch'ing 2024-10-03 18:34:32 +02:00
98d41e6a6d 🎣 Add TryCatchOp and add our eval. catch implementation and runeval missing 2024-10-03 16:32:44 +02:00
fcd0ed780e ✍️ added corrected a4 assignment 2024-10-03 16:28:35 +02:00
4eea3b47f7 2024-09-28 15:37:01 +02:00
18c54613aa 2024-09-28 15:16:53 +02:00
0dccdc0a95 2024-09-28 15:13:45 +02:00
31a4cdaca8 2024-09-28 14:43:34 +02:00
a9a530444e 😎 Task 4 and 5 2024-09-28 14:09:12 +02:00
a32fcdf9da 🖨️ Half a printing function part 2 (does not work) 2024-09-26 17:01:06 +02:00
b399cbec6e 🖨️ Half a printing function (does not work) 2024-09-26 16:55:57 +02:00
dd66ba3d7b 🚂 Equality and power 2024-09-26 16:12:59 +02:00
007b33f87d 🎃 Function application 2024-09-26 15:57:23 +02:00
ff778af26f 💕 Adding assignment 3 2024-09-24 14:22:53 +02:00
db1f22e983 🎉 updated zip file to actually be correct 2024-09-22 22:29:22 +02:00
27bc99d398 🎉 Assignment 2 submission 2024-09-22 22:23:59 +02:00
44 changed files with 2741 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*/dist-newstyle
*/db.txt

BIN
a2/PrehnGadeDekens_a2.pdf Normal file

Binary file not shown.

BIN
a2/PrehnGadeDekens_a2.zip Normal file

Binary file not shown.

1
a3/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist-newstyle

BIN
a3/DekensGadePrehnA3.zip Normal file

Binary file not shown.

39
a3/a3.cabal Normal file
View File

@ -0,0 +1,39 @@
cabal-version: 3.0
name: a3
version: 1.0.0.0
build-type: Simple
common common
default-language: Haskell2010
ghc-options: -Wall
library
import: common
hs-source-dirs: src
build-depends:
base
, tasty
, tasty-hunit
, megaparsec
exposed-modules:
APL.AST
APL.Eval
APL.Parser
APL.Parser_Tests
executable apl
import: common
main-is: apl.hs
build-depends:
base
, a3
test-suite a3-test
import: common
type: exitcode-stdio-1.0
main-is: runtests.hs
build-depends:
base
, tasty
, a3

BIN
a3/a3.pdf Normal file

Binary file not shown.

35
a3/apl.hs Normal file
View File

@ -0,0 +1,35 @@
module Main (main) where
import APL.Eval (Val (..), eval, runEval)
import APL.Parser (parseAPL)
import System.Environment
( getArgs,
getProgName,
)
import System.Exit (ExitCode (..), exitWith)
import System.IO (hPutStrLn, stderr, stdout)
stringVal :: Val -> String
stringVal (ValBool b) = show b
stringVal (ValInt x) = show x
stringVal ValFun {} = "#<fun>"
main :: IO ()
main = do
args <- getArgs
case args of
[fname] -> do
s <- readFile fname
case parseAPL fname s of
Left err -> hPutStrLn stderr err
Right e -> case runEval (eval e) of
Left err -> hPutStrLn stderr err
Right v -> hPutStrLn stdout $ stringVal v
_ -> do
prog <- getProgName
failure $ "Usage: " ++ prog ++ " FILE"
pure ()
where
failure s = do
hPutStrLn stderr s
exitWith $ ExitFailure 1

5
a3/runtests.hs Normal file
View File

@ -0,0 +1,5 @@
import qualified APL.Parser_Tests
import Test.Tasty (defaultMain)
main :: IO ()
main = defaultMain APL.Parser_Tests.tests

27
a3/src/APL/AST.hs Normal file
View File

@ -0,0 +1,27 @@
module APL.AST
( VName,
Exp (..),
)
where
type VName = String
data Exp
= CstInt Integer
| CstBool Bool
| Add Exp Exp
| Sub Exp Exp
| Mul Exp Exp
| Div Exp Exp
| Pow Exp Exp
| Eql Exp Exp
| If Exp Exp Exp
| Var VName
| Let VName Exp Exp
| Lambda VName Exp
| Apply Exp Exp
| TryCatch Exp Exp
| Print String Exp
| KvPut Exp Exp
| KvGet Exp
deriving (Eq, Show)

132
a3/src/APL/Eval.hs Normal file
View File

@ -0,0 +1,132 @@
module APL.Eval
( Val (..),
Env,
eval,
runEval,
Error,
)
where
import APL.AST (Exp (..), VName)
import Control.Monad (ap, liftM)
data Val
= ValInt Integer
| ValBool Bool
| ValFun Env VName Exp
deriving (Eq, Show)
type Env = [(VName, Val)]
envEmpty :: Env
envEmpty = []
envExtend :: VName -> Val -> Env -> Env
envExtend v val env = (v, val) : env
envLookup :: VName -> Env -> Maybe Val
envLookup v env = lookup v env
type Error = String
newtype EvalM a = EvalM (Env -> Either Error a)
instance Functor EvalM where
fmap = liftM
instance Applicative EvalM where
pure x = EvalM $ \_env -> Right x
(<*>) = ap
instance Monad EvalM where
EvalM x >>= f = EvalM $ \env ->
case x env of
Left err -> Left err
Right x' ->
let EvalM y = f x'
in y env
askEnv :: EvalM Env
askEnv = EvalM $ \env -> Right env
localEnv :: (Env -> Env) -> EvalM a -> EvalM a
localEnv f (EvalM m) = EvalM $ \env -> m (f env)
failure :: String -> EvalM a
failure s = EvalM $ \_env -> Left s
catch :: EvalM a -> EvalM a -> EvalM a
catch (EvalM m1) (EvalM m2) = EvalM $ \env ->
case m1 env of
Left _ -> m2 env
Right x -> Right x
runEval :: EvalM a -> Either Error a
runEval (EvalM m) = m envEmpty
evalIntBinOp :: (Integer -> Integer -> EvalM Integer) -> Exp -> Exp -> EvalM Val
evalIntBinOp f e1 e2 = do
v1 <- eval e1
v2 <- eval e2
case (v1, v2) of
(ValInt x, ValInt y) -> ValInt <$> f x y
(_, _) -> failure "Non-integer operand"
evalIntBinOp' :: (Integer -> Integer -> Integer) -> Exp -> Exp -> EvalM Val
evalIntBinOp' f e1 e2 =
evalIntBinOp f' e1 e2
where
f' x y = pure $ f x y
eval :: Exp -> EvalM Val
eval (CstInt x) = pure $ ValInt x
eval (CstBool b) = pure $ ValBool b
eval (Var v) = do
env <- askEnv
case envLookup v env of
Just x -> pure x
Nothing -> failure $ "Unknown variable: " ++ v
eval (Add e1 e2) = evalIntBinOp' (+) e1 e2
eval (Sub e1 e2) = evalIntBinOp' (-) e1 e2
eval (Mul e1 e2) = evalIntBinOp' (*) e1 e2
eval (Div e1 e2) = evalIntBinOp checkedDiv e1 e2
where
checkedDiv _ 0 = failure "Division by zero"
checkedDiv x y = pure $ x `div` y
eval (Pow e1 e2) = evalIntBinOp checkedPow e1 e2
where
checkedPow x y =
if y < 0
then failure "Negative exponent"
else pure $ x ^ y
eval (Eql e1 e2) = do
v1 <- eval e1
v2 <- eval e2
case (v1, v2) of
(ValInt x, ValInt y) -> pure $ ValBool $ x == y
(ValBool x, ValBool y) -> pure $ ValBool $ x == y
(_, _) -> failure "Invalid operands to equality"
eval (If cond e1 e2) = do
cond' <- eval cond
case cond' of
ValBool True -> eval e1
ValBool False -> eval e2
_ -> failure "Non-boolean conditional."
eval (Let var e1 e2) = do
v1 <- eval e1
localEnv (envExtend var v1) $ eval e2
eval (Lambda var body) = do
env <- askEnv
pure $ ValFun env var body
eval (Apply e1 e2) = do
v1 <- eval e1
v2 <- eval e2
case (v1, v2) of
(ValFun f_env var body, arg) ->
localEnv (const $ envExtend var arg f_env) $ eval body
(_, _) ->
failure "Cannot apply non-function"
eval (TryCatch e1 e2) =
eval e1 `catch` eval e2
eval e =
error $ "Evaluation of this expression not implemented:\n" ++ show e

185
a3/src/APL/Parser.hs Normal file
View File

@ -0,0 +1,185 @@
module APL.Parser (parseAPL) where
import APL.AST (Exp (..), VName)
import Control.Monad (void)
import Data.Char (isAlpha, isAlphaNum, isDigit)
import Data.Void (Void)
import Text.Megaparsec
( Parsec,
choice,
chunk,
eof,
errorBundlePretty,
many,
notFollowedBy,
parse,
satisfy,
some,
try,
)
import Text.Megaparsec.Char (space)
type Parser = Parsec Void String
lexeme :: Parser a -> Parser a
lexeme p = p <* space
keywords :: [String]
keywords =
[ "if",
"then",
"else",
"true",
"false",
"let",
"in",
"try",
"catch",
"print",
"put",
"get"
]
lVName :: Parser VName
lVName = lexeme $ try $ do
c <- satisfy isAlpha
cs <- many $ satisfy isAlphaNum
let v = c : cs
if v `elem` keywords
then fail "Unexpected keyword"
else pure v
lStringLit :: Parser String
lStringLit =
lexeme $ many (satisfy (/= '"'))
lInteger :: Parser Integer
lInteger =
lexeme $ read <$> some (satisfy isDigit) <* notFollowedBy (satisfy isAlphaNum)
lString :: String -> Parser ()
lString s = lexeme $ void $ chunk s
lKeyword :: String -> Parser ()
lKeyword s = lexeme $ void $ try $ chunk s <* notFollowedBy (satisfy isAlphaNum)
lBool :: Parser Bool
lBool =
lexeme . try . choice $
[ const True <$> lKeyword "true",
const False <$> lKeyword "false"
]
pStringLit :: Parser String
pStringLit =
lString "\"" *> lStringLit <* lString "\""
pAtom :: Parser Exp
pAtom =
choice
[ CstInt <$> lInteger,
CstBool <$> lBool,
Var <$> lVName,
lString "(" *> pExp <* lString ")"
]
pFExp :: Parser Exp
pFExp = pAtom >>= chain
where
chain x =
choice
[ do
y <- pAtom
chain $ Apply x y,
pure x
]
pLExp :: Parser Exp
pLExp =
choice
[ If
<$> (lKeyword "if" *> pExp)
<*> (lKeyword "then" *> pExp)
<*> (lKeyword "else" *> pExp),
Lambda
<$> (lString "\\" *> lVName)
<*> (lString "->" *> pExp),
TryCatch
<$> (lKeyword "try" *> pExp)
<*> (lKeyword "catch" *> pExp),
Let
<$> (lKeyword "let" *> lVName)
<*> (lString "=" *> pExp)
<*> (lKeyword "in" *> pExp),
pFExp
]
pExp3 :: Parser Exp
pExp3 =
choice
[
Print
<$> (lKeyword "print" *> pStringLit)
<*> pAtom,
KvGet <$> (lKeyword "get" *> pAtom),
KvPut <$> (lKeyword "put" *> pAtom)
<*> pAtom,
pLExp
]
pExp2 :: Parser Exp
pExp2 = pExp3 >>= chain
where
chain x =
choice
[ do
lString "**"
y <- pExp2
chain $ Pow x y,
pure x
]
pExp1 :: Parser Exp
pExp1 = pExp2 >>= chain
where
chain x =
choice
[
do
lString "*"
y <- pLExp
chain $ Mul x y,
do
lString "/"
y <- pLExp
chain $ Div x y,
pure x
]
pExp0 :: Parser Exp
pExp0 = pExp1 >>= chain
where
chain x =
choice
[ do
lString "+"
y <- pExp1
chain $ Add x y,
do
lString "-"
y <- pExp1
chain $ Sub x y,
do
lString "=="
y <- pExp1
chain $ Eql x y,
pure x
]
pExp :: Parser Exp
pExp = pExp0
parseAPL :: FilePath -> String -> Either String Exp
parseAPL fname s = case parse (space *> pExp <* eof) fname s of
Left err -> Left $ errorBundlePretty err
Right x -> Right x

116
a3/src/APL/Parser_Tests.hs Normal file
View File

@ -0,0 +1,116 @@
module APL.Parser_Tests (tests) where
import APL.AST (Exp (..))
import APL.Parser (parseAPL)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
parserTest :: String -> Exp -> TestTree
parserTest s e =
testCase s $
case parseAPL "input" s of
Left err -> assertFailure err
Right e' -> e' @?= e
parserTestFail :: String -> TestTree
parserTestFail s =
testCase s $
case parseAPL "input" s of
Left _ -> pure ()
Right e ->
assertFailure $
"Expected parse error but received this AST:\n" ++ show e
tests :: TestTree
tests =
testGroup
"Parsing"
[ testGroup
"Constants"
[ parserTest "123" $ CstInt 123,
parserTest " 123" $ CstInt 123,
parserTest "123 " $ CstInt 123,
parserTestFail "123f",
parserTest "true" $ CstBool True,
parserTest "false" $ CstBool False
],
testGroup
"Basic operators"
[ parserTest "x+y" $ Add (Var "x") (Var "y"),
parserTest "x-y" $ Sub (Var "x") (Var "y"),
parserTest "x*y" $ Mul (Var "x") (Var "y"),
parserTest "x/y" $ Div (Var "x") (Var "y")
],
testGroup
"Operator priority"
[ parserTest "x+y+z" $ Add (Add (Var "x") (Var "y")) (Var "z"),
parserTest "x+y-z" $ Sub (Add (Var "x") (Var "y")) (Var "z"),
parserTest "x+y*z" $ Add (Var "x") (Mul (Var "y") (Var "z")),
parserTest "x*y*z" $ Mul (Mul (Var "x") (Var "y")) (Var "z"),
parserTest "x/y/z" $ Div (Div (Var "x") (Var "y")) (Var "z")
],
testGroup
"Conditional expressions"
[ parserTest "if x then y else z" $ If (Var "x") (Var "y") (Var "z"),
parserTest "if x then y else if x then y else z" $
If (Var "x") (Var "y") $
If (Var "x") (Var "y") (Var "z"),
parserTest "if x then (if x then y else z) else z" $
If (Var "x") (If (Var "x") (Var "y") (Var "z")) (Var "z"),
parserTest "1 + if x then y else z" $
Add (CstInt 1) (If (Var "x") (Var "y") (Var "z"))
],
testGroup
"Lexing edge cases"
[ parserTest "2 " $ CstInt 2,
parserTest " 2" $ CstInt 2
],
testGroup
"FExp"
[ parserTest "x y" $ Apply (Var "x") (Var "y"),
parserTest "x y z" $ Apply (Apply (Var "x") (Var "y")) (Var "z"),
parserTest "x y z+5" $ Add (Apply (Apply (Var "x") (Var "y")) (Var "z")) (CstInt 5),
parserTest "x (y z)" $ Apply (Var "x") (Apply (Var "y") (Var "z"))
],
testGroup
"Eql"
[ parserTest "x == y" $ Eql (Var "x") (Var "y"),
parserTest "x == y == z" $ Eql (Eql (Var "x") (Var "y")) (Var "z"),
parserTest "x + y == z * x" $ Eql (Add (Var "x") (Var "y")) (Mul (Var "z") (Var "x"))
],
testGroup
"Pow"
[ parserTest "x ** y" $ Pow (Var "x") (Var "y"),
parserTest "x ** y ** z" $ Pow (Var "x") (Pow (Var "y") (Var "z")),
parserTest "x + y ** z * x" $ Add (Var "x") (Mul (Pow (Var "y") (Var "z")) (Var "x"))
],
testGroup
"Print"
[ parserTest "print \"test\" x" $ Print "test" (Var "x"),
parserTest "print \"7\" x" $ Print "7" (Var "x"),
parserTest "print \"cool_print\" x" $ Print "cool_print" (Var "x"),
parserTest "print \"\" x" $ Print "" (Var "x")
],
testGroup
"Get"
[ parserTest "get x" $ KvGet (Var "x"),
parserTest "get 1" $ KvGet (CstInt 1)
],
testGroup
"Put"
[ parserTest "put x y" $ KvPut (Var "x") (Var "y")
],
testGroup
"Lambda"
[ parserTest "\\x->y" $ Lambda "x" (Var "y"),
parserTest "\\x->y+z+5" $ Lambda "x" (Add (Add (Var "y") (Var "z")) (CstInt 5))
],
testGroup
"TryCatch"
[ parserTest "try x catch y" $ TryCatch (Var "x") (Var "y")
],
testGroup
"Let"
[ parserTest "let x=5 in y" $ Let "x" (CstInt 5) (Var "y")
]
]

BIN
a4/DekensGadePrehn-a4.pdf Normal file

Binary file not shown.

BIN
a4/DekensGadePrehn-a4.zip Normal file

Binary file not shown.

35
a4/a4.cabal Normal file
View File

@ -0,0 +1,35 @@
cabal-version: 3.0
name: a4
version: 1.0.0.0
build-type: Simple
common common
default-language: Haskell2010
ghc-options: -Wall
library
import: common
hs-source-dirs: src
build-depends:
base
, tasty
, tasty-hunit
, process
, directory
exposed-modules:
APL.AST
APL.Eval
APL.InterpPure
APL.InterpIO
APL.Interp_Tests
APL.Monad
APL.Util
test-suite a4-test
import: common
type: exitcode-stdio-1.0
main-is: runtests.hs
build-depends:
base
, tasty
, a4

BIN
a4/a4.pdf Normal file

Binary file not shown.

5
a4/runtests.hs Normal file
View File

@ -0,0 +1,5 @@
import qualified APL.Interp_Tests
import Test.Tasty (defaultMain)
main :: IO ()
main = defaultMain APL.Interp_Tests.tests

27
a4/src/APL/AST.hs Normal file
View File

@ -0,0 +1,27 @@
module APL.AST
( VName,
Exp (..),
)
where
type VName = String
data Exp
= CstInt Integer
| CstBool Bool
| Add Exp Exp
| Sub Exp Exp
| Mul Exp Exp
| Div Exp Exp
| Pow Exp Exp
| Eql Exp Exp
| If Exp Exp Exp
| Var VName
| Let VName Exp Exp
| Lambda VName Exp
| Apply Exp Exp
| TryCatch Exp Exp
| Print String Exp
| KvPut Exp Exp
| KvGet Exp
deriving (Eq, Show)

93
a4/src/APL/Eval.hs Normal file
View File

@ -0,0 +1,93 @@
module APL.Eval
( eval,
)
where
import APL.AST (Exp (..))
import APL.Monad
evalIntBinOp :: (Integer -> Integer -> EvalM Integer) -> Exp -> Exp -> EvalM Val
evalIntBinOp f e1 e2 = do
v1 <- eval e1
v2 <- eval e2
case (v1, v2) of
(ValInt x, ValInt y) -> ValInt <$> f x y
(_, _) -> failure "Non-integer operand"
evalIntBinOp' :: (Integer -> Integer -> Integer) -> Exp -> Exp -> EvalM Val
evalIntBinOp' f e1 e2 =
evalIntBinOp f' e1 e2
where
f' x y = pure $ f x y
-- Replaced their eval with ours as instructed NOTE
eval :: Exp -> EvalM Val
eval (CstInt x) = pure $ ValInt x
eval (CstBool b) = pure $ ValBool b
eval (Var v) = do
env <- askEnv
case envLookup v env of
Just x -> pure x
Nothing -> failure $ "Unknown variable: " ++ v
eval (Add e1 e2) = evalIntBinOp' (+) e1 e2
eval (Sub e1 e2) = evalIntBinOp' (-) e1 e2
eval (Mul e1 e2) = evalIntBinOp' (*) e1 e2
eval (Div e1 e2) = evalIntBinOp checkedDiv e1 e2
where
checkedDiv _ 0 = failure "Division by zero"
checkedDiv x y = pure $ x `div` y
eval (Pow e1 e2) = evalIntBinOp checkedPow e1 e2
where
checkedPow x y =
if y < 0
then failure "Negative exponent"
else pure $ x ^ y
eval (Eql e1 e2) = do
v1 <- eval e1
v2 <- eval e2
case (v1, v2) of
(ValInt x, ValInt y) -> pure $ ValBool $ x == y
(ValBool x, ValBool y) -> pure $ ValBool $ x == y
(_, _) -> failure "Invalid operands to equality"
eval (If cond e1 e2) = do
cond' <- eval cond
case cond' of
ValBool True -> eval e1
ValBool False -> eval e2
_ -> failure "Non-boolean conditional."
eval (Let var e1 e2) = do
v1 <- eval e1
localEnv (envExtend var v1) $ eval e2
eval (Lambda var body) = do
env <- askEnv
pure $ ValFun env var body
eval (Apply e1 e2) = do
v1 <- eval e1
v2 <- eval e2
case (v1, v2) of
(ValFun f_env var body, arg) ->
localEnv (const $ envExtend var arg f_env) $ eval body
(_, _) ->
failure "Cannot apply non-function"
eval (TryCatch e1 e2) =
eval e1 `catch` eval e2
eval (Print s e1) = do
v1 <- eval e1
case v1 of
(ValInt i) -> do
evalPrint (s++": "++(show i))
pure $ v1
(ValBool b) -> do
evalPrint (s++": "++(show b))
pure $ v1
(ValFun _ _ _) -> do
evalPrint (s++": #<fun>")
pure $ v1
eval (KvPut e1 e2) = do
v1 <- eval e1
v2 <- eval e2
evalKvPut v1 v2
pure $ v2
eval (KvGet e) = do
v <- eval e
evalKvGet v

112
a4/src/APL/InterpIO.hs Normal file
View File

@ -0,0 +1,112 @@
module APL.InterpIO (runEvalIO) where
import APL.Monad
import APL.Util
import System.Directory (removeFile)
import System.IO (hFlush, readFile', stdout)
-- Converts a string into a value. Only 'ValInt's and 'ValBool' are supported.
readVal :: String -> Maybe Val
readVal = unserialize
-- 'prompt s' prints 's' to the console and then reads a line from stdin.
prompt :: String -> IO String
prompt s = do
putStr s
hFlush stdout
getLine
-- 'writeDB dbFile s' writes the 'State' 's' to the file 'db'.
writeDB :: FilePath -> State -> IO ()
writeDB db s =
writeFile db $ serialize s
-- 'readDB db' reads the database stored in 'db'.
readDB :: FilePath -> IO (Either Error State)
readDB db = do
ms <- readFile' db
case unserialize ms of
Just s -> pure $ pure s
Nothing -> pure $ Left "Invalid DB."
-- 'copyDB db1 db2' copies 'db1' to 'db2'.
copyDB :: FilePath -> FilePath -> IO ()
copyDB db db' = do
s <- readFile' db
writeFile db' s
-- Removes all key-value pairs from the database file.
clearDB :: IO ()
clearDB = writeFile dbFile ""
-- The name of the database file.
dbFile :: FilePath
dbFile = "db.txt"
-- Creates a fresh temporary database, passes it to a function returning an
-- IO-computation, executes the computation, deletes the temporary database, and
-- finally returns the result of the computation. The temporary database file is
-- guaranteed fresh and won't have a name conflict with any other files.
withTempDB :: (FilePath -> IO a) -> IO a
withTempDB m = do
tempDB <- newTempDB -- Create a new temp database file.
res <- m tempDB -- Run the computation with the new file.
removeFile tempDB -- Delete the temp database file.
pure res -- Return the result of the computation.
runEvalIO :: EvalM a -> IO (Either Error a)
runEvalIO evalm = do
clearDB
runEvalIO' envEmpty dbFile evalm
where
runEvalIO' :: Env -> FilePath -> EvalM a -> IO (Either Error a)
runEvalIO' _ _ (Pure x) = pure $ pure x
runEvalIO' r db (Free (ReadOp k)) = runEvalIO' r db $ k r
runEvalIO' r db (Free (StateGetOp k)) = do
result <- readDB db
case result of
Right s -> runEvalIO' r db $ k s
Left e -> pure $ Left e
runEvalIO' r db (Free (StatePutOp s m)) = do
writeDB db s
runEvalIO' r db m
runEvalIO' r db (Free (PrintOp p m)) = do
putStrLn p
runEvalIO' r db m
runEvalIO' r db (Free (KvGetOp key k)) = do
result <- readDB db
case result of
Right s -> case (lookup key s) of
Just val -> runEvalIO' r db $ k val
Nothing -> do
input <- prompt $ "Invalid key: "++(show key)++". Enter a replacement: "
let val = readVal input
case val of
Just v -> runEvalIO' r db $ (Free (KvGetOp v k))
Nothing -> pure $ Left $ "Invalid key: "++input
Left e -> pure $ Left e
runEvalIO' r db (Free (KvPutOp key val m)) = do
result <- readDB db
case result of
Right dbState -> do
let dbState' = (key,val):dbState
writeDB db dbState'
runEvalIO' r db m
Left e -> pure $ Left e
runEvalIO' r db (Free (TryCatchOp m l)) = do
result <- runEvalIO' r db m
case result of
Right x -> pure $ Right x
Left _ -> runEvalIO' r db l
runEvalIO' r db (Free (TransactionOp l m)) = do
withTempDB transactionDB
runEvalIO' r db m
where
transactionDB :: (FilePath -> IO ())
transactionDB db' = do
copyDB db db'
resl <- runEvalIO' r db (do l)
case resl of
Right _ -> pure ()
Left _ -> copyDB db' db
runEvalIO' _ _ (Free (ErrorOp e)) = pure $ Left e

35
a4/src/APL/InterpPure.hs Normal file
View File

@ -0,0 +1,35 @@
module APL.InterpPure (runEval) where
import APL.Monad
runEval :: EvalM a -> ([String], Either Error a)
runEval = runEval' envEmpty stateInitial
where
runEval' :: Env -> State -> EvalM a -> ([String], Either Error a)
runEval' _ _ (Pure x) = ([], pure x)
runEval' r s (Free (ReadOp k)) = runEval' r s $ k r
runEval' r s (Free (StateGetOp k)) = runEval' r s $ k s
runEval' r _ (Free (StatePutOp s' m)) = runEval' r s' m
runEval' r s (Free (PrintOp p m)) =
let (ps, res) = runEval' r s m
in (p : ps, res)
runEval' r s (Free (TryCatchOp m l)) =
case (runEval' r s m) of
(_, Left _) -> runEval' r s l
a -> a
runEval' r s (Free (KvGetOp key k)) =
case (lookup key s) of
Just val -> runEval' r s $ k val
Nothing -> ([], Left ("Cannot find key: "++(show key)))
runEval' r s (Free (KvPutOp key val m)) =
runEval' r ((key,val):s) m
runEval' r s (Free (TransactionOp l m)) =
let (p, resl) = runEval' r s (do l >> getState)
in case resl of
Right s' ->
let (ps, resm) = runEval' r s' m
in (p ++ ps, resm)
Left _ ->
let (ps, resm) = runEval' r s m
in (p ++ ps, resm)
runEval' _ _ (Free (ErrorOp e)) = ([], Left e)

273
a4/src/APL/Interp_Tests.hs Normal file
View File

@ -0,0 +1,273 @@
module APL.Interp_Tests (tests) where
import APL.AST (Exp (..))
import APL.Eval (eval)
import APL.InterpIO (runEvalIO)
import APL.InterpPure (runEval)
import APL.Monad
import APL.Util (captureIO)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
eval' :: Exp -> ([String], Either Error Val)
eval' = runEval . eval
evalIO' :: Exp -> IO (Either Error Val)
evalIO' = runEvalIO . eval
tests :: TestTree
tests = testGroup "Free monad interpreters" [pureTests, ioTests]
pureTests :: TestTree
pureTests =
testGroup
"Pure interpreter"
[ testCase "localEnv" $
runEval
( localEnv (const [("x", ValInt 1)]) $
askEnv
)
@?= ([], Right [("x", ValInt 1)]),
--
testCase "Let" $
eval' (Let "x" (Add (CstInt 2) (CstInt 3)) (Var "x"))
@?= ([], Right (ValInt 5)),
--
testCase "Let (shadowing)" $
eval'
( Let
"x"
(Add (CstInt 2) (CstInt 3))
(Let "x" (CstBool True) (Var "x"))
)
@?= ([], Right (ValBool True)),
--
testCase "State" $
runEval
( do
putState [(ValInt 0, ValInt 1)]
modifyState $ map (\(key, _) -> (key, ValInt 5))
getState
)
@?= ([], Right [(ValInt 0, ValInt 5)]),
--
testCase "Print" $
runEval (evalPrint "test")
@?= (["test"], Right ()),
--
testCase "Error" $
runEval
( do
_ <- failure "Oh no!"
evalPrint "test"
)
@?= ([], Left "Oh no!"),
--
testCase "Div0" $
eval' (Div (CstInt 7) (CstInt 0))
@?= ([], Left "Division by zero"),
--
testCase "TryCatch try1" $
eval' (TryCatch (CstInt 1) (CstInt 2))
@?= ([], Right (ValInt 1)),
--
testCase "TryCatch try2" $
eval' (TryCatch (CstInt 1) (Div (CstInt 1) (CstInt 0)))
@?= ([], Right (ValInt 1)),
--
testCase "TryCatch catch1" $
eval' (TryCatch (Div (CstInt 1) (CstInt 0)) (CstInt 1))
@?= ([], Right (ValInt 1)),
--
testCase "TryCatch catch2" $
eval' (TryCatch (Div (CstInt 1) (CstInt 0)) (Div (CstInt 1) (CstInt 0)))
@?= ([], Left "Division by zero"),
--
testCase "KvPutOp" $
eval' (KvPut (CstInt 1) (CstInt 2))
@?= ([], Right (ValInt 2)),
--
testCase "KvGetOp" $
eval' (Let "_" (KvPut (CstInt 1) (CstInt 2)) (KvGet (CstInt 1)))
@?= ([], Right (ValInt 2)),
--
testCase "KvGetOp shadowing" $
eval' (Let "_" (KvPut (CstInt 1) (CstInt 2)) (Let "_" (KvPut (CstInt 1) (CstInt 3)) (KvGet (CstInt 1))))
@?= ([], Right (ValInt 3)),
--
testCase "KvGetOp fail" $
eval' (KvGet (CstInt 1))
@?= ([], Left "Cannot find key: ValInt 1"),
--
testCase "TransactionOp 1" $
let goodPut = (evalKvPut (ValInt 0) (ValInt 1)) in
runEval (transaction goodPut)
@?= ([], Right ()),
--
testCase "TransactionOp 2" $
let goodPut = evalKvPut (ValInt 0) (ValInt 1) in
let get0 = eval (KvGet (CstInt 0)) in
runEval (transaction goodPut >> get0)
@?= ([], Right (ValInt 1)),
--
testCase "TransactionOp 3" $
let oPut = (evalKvPut (ValInt 1) (ValInt 2)) in
let okayPut = ((evalKvGet (ValInt 1) >> evalKvPut (ValInt 0) (ValInt 1))) in
let get0 = eval (KvGet (CstInt 0)) in
runEval (oPut >> (transaction okayPut) >> get0)
@?= ([], Right (ValInt 1)),
--
testCase "TransactionOp Fail" $
let badPut = (evalKvPut (ValInt 0) (ValInt 1)) >> (failure "die") in
let get0 = eval (KvGet (CstInt 0)) in
runEval (transaction badPut >> get0)
@?= ([], Left "Cannot find key: ValInt 0"),
--
testCase "TransactionOp Propagation" $
let badPut = (evalKvPut (ValInt 0) (ValInt 1)) >> (failure "die") in
runEval (transaction badPut)
@?= ([], Right ()),
--
testCase "TransactionOp Printing" $
runEval (transaction (evalPrint "weee" >> failure "oh shit"))
@?= (["weee"], Right ()),
--
testCase "TransactionOp Nested" $
let badPut = (evalKvPut (ValInt 0) (ValInt 1)) >> (failure "die") in
let goodPut = evalKvPut (ValInt 0) (ValInt 1) in
let get0 = eval (KvGet (CstInt 0)) in
runEval (transaction (goodPut >> transaction badPut) >> get0)
@?= ([], Right (ValInt 1))
]
ioTests :: TestTree
ioTests =
testGroup
"IO interpreter"
[ testCase "print" $ do
let s1 = "Lalalalala"
s2 = "Weeeeeeeee"
(out, res) <-
captureIO [] $
runEvalIO $ do
evalPrint s1
evalPrint s2
(out, res) @?= ([s1, s2], Right ()),
---
testCase "print 2" $ do
(out, res) <-
captureIO [] $
evalIO' $
Print "This is also 1" $
Print "This is 1" $
CstInt 1
(out, res) @?= (["This is 1: 1", "This is also 1: 1"], Right $ ValInt 1),
---
testCase "State" $ do
r <- runEvalIO $ do
putState [(ValInt 0, ValInt 1)]
modifyState $ map (\(key, _) -> (key, ValInt 5))
getState
r @?= Right [(ValInt 0, ValInt 5)],
---
testCase "TryCatch try1" $ do
r <- evalIO' (TryCatch (CstInt 1) (CstInt 2))
r @?= Right (ValInt 1),
---
testCase "TryCatch try2" $ do
r <- evalIO' (TryCatch (CstInt 1) (Div (CstInt 1) (CstInt 0)))
r @?= Right (ValInt 1),
---
testCase "TryCatch catch1" $ do
r <- evalIO' (TryCatch (Div (CstInt 1) (CstInt 0)) (CstInt 1))
r @?= Right (ValInt 1),
---
testCase "TryCatch catch2" $ do
r <- evalIO' (TryCatch (Div (CstInt 1) (CstInt 0)) (Div (CstInt 1) (CstInt 0)))
r @?= Left "Division by zero",
---
testCase "KvPutOp" $ do
r <- evalIO' (KvPut (CstInt 1) (CstInt 2))
r @?= Right (ValInt 2),
--
testCase "KvGetOp" $ do
r <- evalIO' (Let "_" (KvPut (CstInt 1) (CstInt 2)) (KvGet (CstInt 1)))
r @?= Right (ValInt 2),
--
testCase "KvGetOp Bool" $ do
r <- evalIO' (Let "_" (KvPut (CstBool True) (CstBool False)) (KvGet (CstBool True)))
r @?= Right (ValBool False),
--
testCase "KvGetOp shadowing" $ do
r <- evalIO' (Let "_" (KvPut (CstInt 1) (CstInt 2)) (Let "_" (KvPut (CstInt 1) (CstInt 3)) (KvGet (CstInt 1))))
r @?= Right (ValInt 3),
--
testCase "KvGetOp fail" $ do
(_, r) <-
captureIO [":)"] $
evalIO' (KvGet (CstInt 1))
r @?= Left "Invalid key: :)",
--
testCase "KvGetOp invalid int" $ do
(_, r) <-
captureIO ["ValInt 1"] $
evalIO' (Let "_" (KvPut (CstInt 1) (CstInt 2)) (KvGet (CstInt 3)))
r @?= Right (ValInt 2),
--
testCase "KvGetOp invalid bool" $ do
(_, r) <-
captureIO ["ValBool True"] $
evalIO' (Let "_" (KvPut (CstBool True) (CstInt 2)) (KvGet (CstInt 3)))
r @?= Right (ValInt 2),
--
testCase "KvGetOp invalid multiple" $ do
(_, r) <-
captureIO ["ValInt 1","ValInt 2","ValInt 3"] $
evalIO' (Let "_" (KvPut (CstInt 3) (CstInt 2)) (KvGet (CstInt 4)))
r @?= Right (ValInt 2),
--
testCase "TransactionOp 1" $
let goodPut = (evalKvPut (ValInt 0) (ValInt 1)) in do
r <- runEvalIO (transaction goodPut)
r @?= Right (),
--
testCase "TransactionOp 2" $
let goodPut = evalKvPut (ValInt 0) (ValInt 1) in
let get0 = eval (KvGet (CstInt 0)) in do
r <- runEvalIO (transaction goodPut >> get0)
r @?= Right (ValInt 1),
--
testCase "TransactionOp 3" $
let oPut = (evalKvPut (ValInt 1) (ValInt 2)) in
let okayPut = ((evalKvGet (ValInt 1) >> evalKvPut (ValInt 0) (ValInt 1))) in
let get0 = eval (KvGet (CstInt 0)) in do
r <- runEvalIO (oPut >> (transaction okayPut) >> get0)
r @?= Right (ValInt 1),
--
testCase "TransactionOp Fail" $
let badPut = (evalKvPut (ValInt 0) (ValInt 1)) >> (failure "die") in
let get0 = eval (KvGet (CstInt 0)) in do
(_, r) <-
captureIO [":)"] $
runEvalIO (transaction badPut >> get0)
r @?= Left "Invalid key: :)",
-- --
testCase "TransactionOp Propagation" $
let badPut = (evalKvPut (ValInt 0) (ValInt 1)) >> (failure "die") in do
r <- runEvalIO (transaction badPut)
r @?= Right (),
-- --
testCase "TransactionOp Printing" $ do
(p, r) <-
captureIO [] $
runEvalIO (transaction (evalPrint "weee" >> failure "oh shit"))
(p, r) @?= (["weee"], Right ()),
-- --
testCase "TransactionOp Nested" $
let badPut = (evalKvPut (ValInt 0) (ValInt 1)) >> (failure "die") in
let goodPut = evalKvPut (ValInt 0) (ValInt 1) in
let get0 = eval (KvGet (CstInt 0)) in do
r <- runEvalIO (transaction (goodPut >> transaction badPut) >> get0)
r @?= Right (ValInt 1)
]

137
a4/src/APL/Monad.hs Normal file
View File

@ -0,0 +1,137 @@
module APL.Monad
( envEmpty,
envExtend,
envLookup,
stateInitial,
askEnv,
modifyEffects,
localEnv,
getState,
putState,
modifyState,
evalPrint,
catch,
failure,
evalKvGet,
evalKvPut,
transaction,
EvalM,
Val (..),
EvalOp (..),
Free (..),
Error,
Env,
State,
)
where
import APL.AST (Exp (..), VName)
import Control.Monad (ap)
data Val
= ValInt Integer
| ValBool Bool
| ValFun Env VName Exp
deriving (Eq, Show)
type Error = String
type Env = [(VName, Val)]
envEmpty :: Env
envEmpty = []
envExtend :: VName -> Val -> Env -> Env
envExtend v val env = (v, val) : env
envLookup :: VName -> Env -> Maybe Val
envLookup v env = lookup v env
type State = [(Val, Val)]
stateInitial :: State
stateInitial = []
data Free e a
= Pure a
| Free (e (Free e a))
instance (Functor e) => Functor (Free e) where
fmap f (Pure x) = Pure $ f x
fmap f (Free g) = Free $ fmap (fmap f) g
instance (Functor e) => Applicative (Free e) where
pure = Pure
(<*>) = ap
instance (Functor e) => Monad (Free e) where
Pure x >>= f = f x
Free g >>= f = Free $ h <$> g
where
h x = x >>= f
data EvalOp a
= ReadOp (Env -> a)
| StateGetOp (State -> a)
| StatePutOp State a
| PrintOp String a
| ErrorOp Error
| TryCatchOp a a
| KvGetOp Val (Val -> a)
| KvPutOp Val Val a
| TransactionOp (EvalM ()) a
instance Functor EvalOp where
fmap f (ReadOp k) = ReadOp $ f . k
fmap f (StateGetOp k) = StateGetOp $ f . k
fmap f (StatePutOp s m) = StatePutOp s $ f m
fmap f (PrintOp p m) = PrintOp p $ f m
fmap _ (ErrorOp e) = ErrorOp e
fmap f (TryCatchOp m1 m2) = TryCatchOp (f m1) (f m2)
fmap f (KvGetOp v k) = KvGetOp v (f . k)
fmap f (KvPutOp v1 v2 m) = KvPutOp v1 v2 (f m)
fmap f (TransactionOp l m) = TransactionOp l (f m)
type EvalM a = Free EvalOp a
askEnv :: EvalM Env
askEnv = Free $ ReadOp $ \env -> pure env
modifyEffects :: (Functor e, Functor h) => (e (Free e a) -> h (Free e a)) -> Free e a -> Free h a
modifyEffects _ (Pure x) = Pure x
modifyEffects g (Free e) = Free $ modifyEffects g <$> g e
localEnv :: (Env -> Env) -> EvalM a -> EvalM a
localEnv f = modifyEffects g
where
g (ReadOp k) = ReadOp $ k . f
g op = op
getState :: EvalM State
getState = Free $ StateGetOp $ \s -> pure s
putState :: State -> EvalM ()
putState s = Free $ StatePutOp s $ pure ()
modifyState :: (State -> State) -> EvalM ()
modifyState f = do
s <- getState
putState $ f s
evalPrint :: String -> EvalM ()
evalPrint p = Free $ PrintOp p $ pure ()
failure :: String -> EvalM a
failure = Free . ErrorOp
catch :: EvalM a -> EvalM a -> EvalM a
catch m1 m2 = Free $ TryCatchOp m1 m2
evalKvGet :: Val -> EvalM Val
evalKvGet v = Free $ KvGetOp v $ \w -> pure w
evalKvPut :: Val -> Val -> EvalM ()
evalKvPut v1 v2 = Free $ KvPutOp v1 v2 $ pure ()
transaction :: EvalM () -> EvalM ()
transaction v = Free $ TransactionOp v $ pure ()

99
a4/src/APL/Util.hs Normal file
View File

@ -0,0 +1,99 @@
{-# LANGUAGE FlexibleInstances #-}
module APL.Util
( Serialize (..),
newTempDB,
captureIO,
)
where
import APL.Monad
import Control.Concurrent (threadDelay)
import Control.Exception (bracket)
import Control.Monad (guard)
import Data.Char (isDigit)
import Data.List (stripPrefix)
import Data.Maybe (mapMaybe)
import GHC.IO.Handle (hDuplicate, hDuplicateTo)
import System.Directory (listDirectory)
import System.IO
import System.Process (createPipe)
import Text.Read (readMaybe)
class Serialize a where
serialize :: a -> String
unserialize :: String -> Maybe a
instance Serialize Val where
serialize (ValInt x) = "ValInt " <> show x
serialize (ValBool b) = "ValBool " <> show b
serialize ValFun {} = error "ValFun serialization is not supported."
unserialize s =
case words s of
["ValInt", rest]
| all isDigit rest -> ValInt <$> readMaybe rest
["ValBool", rest] -> ValBool <$> readMaybe rest
_ -> Nothing
instance (Serialize a, Serialize b) => Serialize (a, b) where
serialize (a, b) =
serialize a ++ "," ++ serialize b
unserialize s =
case span (/= ',') s of
(s_a, _ : s_b) -> (,) <$> unserialize s_a <*> unserialize s_b
_ -> Nothing
instance Serialize [(Val, Val)] where
serialize kv =
unlines $ map serialize kv
unserialize =
mapM unserialize . lines
newTempDB :: IO FilePath
newTempDB = do
files <- listDirectory "."
let n = maximum (0 : mapMaybe match files) + 1
tempFile = "temp" ++ show n ++ ".txt"
writeFile tempFile ""
pure $ tempFile
where
match :: FilePath -> Maybe Int
match s = do
s' <- stripPrefix "temp" s
let (n_s, rest) = (takeWhile isDigit s', dropWhile isDigit s')
guard $ rest == ".txt"
readMaybe n_s
captureIO :: [String] -> IO a -> IO ([String], a)
captureIO inputs m = do
hFlush stdout
threadDelay 50000 -- Needed to make sure things are actually flushed
stdin' <- hDuplicate stdin
stdout' <- hDuplicate stdout
(inR, inW) <- createPipe
(outR, outW) <- createPipe
hSetBuffering inW NoBuffering
hSetBuffering outW NoBuffering
bracket
( do
inR `hDuplicateTo` stdin
outW `hDuplicateTo` stdout
)
( \_ -> do
stdin' `hDuplicateTo` stdin
stdout' `hDuplicateTo` stdout
mapM_ hClose [stdin', stdout', inR, inW, outW]
)
( \_ -> do
mapM_ (hPutStrLn inW) inputs
hFlush inW
res <- m
output <- hGetContents outR -- hGetContents closes outR
pure (lines output, res)
)

BIN
a5/DekensGadePrehn-a5.pdf Normal file

Binary file not shown.

BIN
a5/DekensGadePrehn-a5.zip Normal file

Binary file not shown.

41
a5/a5.cabal Normal file
View File

@ -0,0 +1,41 @@
cabal-version: 3.0
name: a5
version: 1.0.0.0
build-type: Simple
common common
default-language: Haskell2010
ghc-options: -Wall -Wno-orphans
library
import: common
hs-source-dirs: src
build-depends:
base
, megaparsec
, QuickCheck
exposed-modules:
APL.AST
APL.Parser
APL.Error
APL.Eval
APL.Check
APL.Tests
executable apl
import: common
main-is: apl.hs
build-depends:
base
, a5
test-suite a5-test
import: common
type: exitcode-stdio-1.0
main-is: runtests.hs
build-depends:
base
, tasty
, tasty-quickcheck
, a5

BIN
a5/a5.pdf Normal file

Binary file not shown.

35
a5/apl.hs Normal file
View File

@ -0,0 +1,35 @@
module Main (main) where
import APL.Eval (Val (..), eval, runEval)
import APL.Parser (parseAPL)
import System.Environment
( getArgs,
getProgName,
)
import System.Exit (ExitCode (..), exitWith)
import System.IO (hPutStrLn, stderr, stdout)
stringVal :: Val -> String
stringVal (ValBool b) = show b
stringVal (ValInt x) = show x
stringVal ValFun {} = "#<fun>"
main :: IO ()
main = do
args <- getArgs
case args of
[fname] -> do
s <- readFile fname
case parseAPL fname s of
Left err -> hPutStrLn stderr err
Right e -> case runEval (eval e) of
Left err -> hPutStrLn stderr $ show err
Right v -> hPutStrLn stdout $ stringVal v
_ -> do
prog <- getProgName
failure $ "Usage: " ++ prog ++ " FILE"
pure ()
where
failure e = do
hPutStrLn stderr $ show e
exitWith $ ExitFailure 1

6
a5/runtests.hs Normal file
View File

@ -0,0 +1,6 @@
import qualified APL.Tests
import Test.Tasty (defaultMain)
import Test.Tasty.QuickCheck (testProperties)
main :: IO ()
main = defaultMain (testProperties "APL properties" APL.Tests.properties)

82
a5/src/APL/AST.hs Normal file
View File

@ -0,0 +1,82 @@
module APL.AST
( VName
, Exp (..)
, printExp
, subExp
)
where
type VName = String
data Exp
= CstInt Integer
| CstBool Bool
| Add Exp Exp
| Sub Exp Exp
| Mul Exp Exp
| Div Exp Exp
| Pow Exp Exp
| Eql Exp Exp
| If Exp Exp Exp
| Var VName
| Let VName Exp Exp
| Lambda VName Exp
| Apply Exp Exp
| TryCatch Exp Exp
deriving (Eq, Show)
parens :: String -> String
parens x = "(" ++ x ++ ")"
printBinOp :: String -> Exp -> Exp -> String
printBinOp op x y = parens $ printExp x ++ " " ++ op ++ " " ++ printExp y
printExp :: Exp -> String
printExp (CstInt x) = show x
printExp (CstBool b) = if b then "true" else "false"
printExp (Add x y) = printBinOp "+" x y
printExp (Sub x y) = printBinOp "-" x y
printExp (Mul x y) = printBinOp "*" x y
printExp (Div x y) = printBinOp "/" x y
printExp (Pow x y) = printBinOp "**" x y
printExp (Eql x y) = printBinOp "==" x y
printExp (If x y z) =
parens $
"if "
++ printExp x
++ " then "
++ printExp y
++ " else "
++ printExp z
printExp (Var v) = v
printExp (Let v e1 e2) =
parens $
"let "
++ v
++ " = "
++ printExp e1
++ " in "
++ printExp e2
printExp (Lambda v body) =
parens $ "\\" ++ v ++ " -> " ++ printExp body
printExp (Apply x y) =
parens $ printExp x ++ " " ++ printExp y
printExp (TryCatch x y) =
parens $ "try " ++ printExp x ++ " catch " ++ printExp y
subExp :: Exp -> [Exp]
subExp e = e : case e of
CstInt _ -> []
CstBool _ -> []
Add e1 e2 -> subExp e1 ++ subExp e2
Sub e1 e2 -> subExp e1 ++ subExp e2
Mul e1 e2 -> subExp e1 ++ subExp e2
Div e1 e2 -> subExp e1 ++ subExp e2
Pow e1 e2 -> subExp e1 ++ subExp e2
Eql e1 e2 -> subExp e1 ++ subExp e2
If e0 e1 e2 -> subExp e0 ++ subExp e1 ++ subExp e2
Var _ -> []
Let _ e1 e2 -> subExp e1 ++ subExp e2
Lambda _ body -> subExp body
Apply e1 e2 -> subExp e1 ++ subExp e2
TryCatch e1 e2 -> subExp e1 ++ subExp e2

92
a5/src/APL/Check.hs Normal file
View File

@ -0,0 +1,92 @@
module APL.Check (checkExp, Error) where
import APL.AST (Exp (..), VName)
import APL.Error (Error (..))
import Control.Monad (ap, liftM, unless)
import Data.List (union)
type Vars = [VName]
newtype CheckM a = CheckM {runCheckM :: Vars -> (a, [Error])}
instance Functor CheckM where
fmap = liftM
instance Applicative CheckM where
(<*>) = ap
pure x = CheckM $ \_ -> (x, [])
instance Monad CheckM where
CheckM x >>= f = CheckM $ \vars ->
let (y, errs1) = x vars
(z, errs2) = runCheckM (f y) vars
in (z, union errs1 errs2)
askVars :: CheckM Vars
askVars = CheckM $ \vars -> (vars, [])
localVars :: (Vars -> Vars) -> CheckM a -> CheckM a
localVars f m = CheckM $ \vars ->
runCheckM m (f vars)
failure :: Error -> CheckM ()
failure err = CheckM $ \_ -> ((), [err])
maskErrors :: CheckM a -> CheckM a
maskErrors m = CheckM $ \vars ->
let (x, _) = runCheckM m vars in (x, [])
check :: Exp -> CheckM ()
check (CstInt _) = pure ()
check (CstBool _) = pure ()
check (Var v) = do
vars <- askVars
unless (v `elem` vars) $
failure $
UnknownVariable v
check (Add x y) = do
failure NonInteger
check x
check y
check (Sub x y) = do
failure NonInteger
check x
check y
check (Mul x y) = do
failure NonInteger
check x
check y
check (Div x y) = do
failure NonInteger
failure DivisionByZero
check x
check y
check (Pow x y) = do
failure NonInteger
failure NegativeExponent
check x
check y
check (Eql x y) = do
failure InvalidEqual
check x
check y
check (If x y z) = do
failure NonBoolean
check x
check y
check z
check (Let v e1 e2) = do
check e1
localVars (v :) $ check e2
check (Lambda v e) = do
localVars (v :) $ check e
check (Apply x y) = do
failure NonFunction
check x
check y
check (TryCatch x y) = do
maskErrors $ check x
check y
checkExp :: Exp -> [Error]
checkExp e = snd $ runCheckM (check e) []

35
a5/src/APL/Error.hs Normal file
View File

@ -0,0 +1,35 @@
module APL.Error
( Error(..)
, isVariableError
, isDomainError
, isTypeError
)
where
import APL.AST (VName)
data Error
= NonInteger
| UnknownVariable VName
| DivisionByZero
| NegativeExponent
| InvalidEqual
| NonBoolean
| NonFunction
deriving (Show, Eq)
isVariableError :: Error -> Bool
isVariableError (UnknownVariable _) = True
isVariableError _ = False
isDomainError :: Error -> Bool
isDomainError DivisionByZero = True
isDomainError NegativeExponent = True
isDomainError _ = False
isTypeError :: Error -> Bool
isTypeError NonInteger = True
isTypeError InvalidEqual = True
isTypeError NonBoolean = True
isTypeError NonFunction = True
isTypeError _ = False

128
a5/src/APL/Eval.hs Normal file
View File

@ -0,0 +1,128 @@
module APL.Eval
( Val (..),
Env,
eval,
runEval,
)
where
import APL.AST (Exp (..), VName)
import APL.Error (Error (..))
import Control.Monad (ap, liftM)
data Val
= ValInt Integer
| ValBool Bool
| ValFun Env VName Exp
deriving (Eq, Show)
type Env = [(VName, Val)]
envEmpty :: Env
envEmpty = []
envExtend :: VName -> Val -> Env -> Env
envExtend v val env = (v, val) : env
envLookup :: VName -> Env -> Maybe Val
envLookup v env = lookup v env
newtype EvalM a = EvalM (Env -> Either Error a)
instance Functor EvalM where
fmap = liftM
instance Applicative EvalM where
pure x = EvalM $ \_env -> Right x
(<*>) = ap
instance Monad EvalM where
EvalM x >>= f = EvalM $ \env ->
case x env of
Left err -> Left err
Right x' ->
let EvalM y = f x'
in y env
askEnv :: EvalM Env
askEnv = EvalM $ \env -> Right env
localEnv :: (Env -> Env) -> EvalM a -> EvalM a
localEnv f (EvalM m) = EvalM $ \env -> m (f env)
failure :: Error -> EvalM a
failure s = EvalM $ \_env -> Left s
catch :: EvalM a -> EvalM a -> EvalM a
catch (EvalM m1) (EvalM m2) = EvalM $ \env ->
case m1 env of
Left _ -> m2 env
Right x -> Right x
runEval :: EvalM a -> Either Error a
runEval (EvalM m) = m envEmpty
evalIntBinOp :: (Integer -> Integer -> EvalM Integer) -> Exp -> Exp -> EvalM Val
evalIntBinOp f e1 e2 = do
v1 <- eval e1
v2 <- eval e2
case (v1, v2) of
(ValInt x, ValInt y) -> ValInt <$> f x y
(_, _) -> failure NonInteger
evalIntBinOp' :: (Integer -> Integer -> Integer) -> Exp -> Exp -> EvalM Val
evalIntBinOp' f e1 e2 =
evalIntBinOp f' e1 e2
where
f' x y = pure $ f x y
eval :: Exp -> EvalM Val
eval (CstInt x) = pure $ ValInt x
eval (CstBool b) = pure $ ValBool b
eval (Var v) = do
env <- askEnv
case envLookup v env of
Just x -> pure x
Nothing -> failure $ UnknownVariable v
eval (Add e1 e2) = evalIntBinOp' (+) e1 e2
eval (Sub e1 e2) = evalIntBinOp' (-) e1 e2
eval (Mul e1 e2) = evalIntBinOp' (*) e1 e2
eval (Div e1 e2) = evalIntBinOp checkedDiv e1 e2
where
checkedDiv _ 0 = failure DivisionByZero
checkedDiv x y = pure $ x `div` y
eval (Pow e1 e2) = evalIntBinOp checkedPow e1 e2
where
checkedPow x y =
if y < 0
then failure NegativeExponent
else pure $ x ^ y
eval (Eql e1 e2) = do
v1 <- eval e1
v2 <- eval e2
case (v1, v2) of
(ValInt x, ValInt y) -> pure $ ValBool $ x == y
(ValBool x, ValBool y) -> pure $ ValBool $ x == y
(_, _) -> failure InvalidEqual
eval (If cond e1 e2) = do
cond' <- eval cond
case cond' of
ValBool True -> eval e1
ValBool False -> eval e2
_ -> failure NonBoolean
eval (Let var e1 e2) = do
v1 <- eval e1
localEnv (envExtend var v1) $ eval e2
eval (Lambda var body) = do
env <- askEnv
pure $ ValFun env var body
eval (Apply e1 e2) = do
v1 <- eval e1
v2 <- eval e2
case (v1, v2) of
(ValFun f_env var body, arg) ->
localEnv (const $ envExtend var arg f_env) $ eval body
(_, _) ->
failure NonFunction
eval (TryCatch e1 e2) =
eval e1 `catch` eval e2

168
a5/src/APL/Parser.hs Normal file
View File

@ -0,0 +1,168 @@
module APL.Parser (parseAPL, keywords) where
import APL.AST (Exp (..), VName)
import Control.Monad (void)
import Data.Char (isAlpha, isAlphaNum, isDigit)
import Data.Void (Void)
import Text.Megaparsec
( Parsec,
choice,
chunk,
eof,
errorBundlePretty,
many,
notFollowedBy,
parse,
satisfy,
some,
try,
)
import Text.Megaparsec.Char (space)
type Parser = Parsec Void String
lexeme :: Parser a -> Parser a
lexeme p = p <* space
keywords :: [String]
keywords =
[ "if",
"then",
"else",
"true",
"false",
"let",
"in",
"try",
"catch"
]
lVName :: Parser VName
lVName = lexeme $ try $ do
c <- satisfy isAlpha
cs <- many $ satisfy isAlphaNum
let v = c : cs
if v `elem` keywords
then fail "Unexpected keyword"
else pure v
lInteger :: Parser Integer
lInteger =
lexeme $ read <$> some (satisfy isDigit) <* notFollowedBy (satisfy isAlphaNum)
lString :: String -> Parser ()
lString s = lexeme $ void $ chunk s
lKeyword :: String -> Parser ()
lKeyword s = lexeme $ void $ try $ chunk s <* notFollowedBy (satisfy isAlphaNum)
lBool :: Parser Bool
lBool =
lexeme . try . choice $
[ const True <$> lKeyword "true",
const False <$> lKeyword "false"
]
pAtom :: Parser Exp
pAtom =
choice
[ CstInt <$> lInteger,
CstBool <$> lBool,
Var <$> lVName,
lString "(" *> pExp <* lString ")"
]
pFExp :: Parser Exp
pFExp = chain =<< pAtom
where
chain x =
choice
[ do
y <- pAtom
chain $ Apply x y,
pure x
]
pLExp :: Parser Exp
pLExp =
choice
[ If
<$> (lKeyword "if" *> pExp)
<*> (lKeyword "then" *> pExp)
<*> (lKeyword "else" *> pExp),
Lambda
<$> (lString "\\" *> lVName)
<*> (lString "->" *> pExp),
TryCatch
<$> (lKeyword "try" *> pExp)
<*> (lKeyword "catch" *> pExp),
Let
<$> (lKeyword "let" *> lVName)
<*> (lString "=" *> pExp)
<*> (lKeyword "in" *> pExp),
pFExp
]
pExp4 :: Parser Exp
pExp4 = pLExp >>= chain
where
chain x =
choice
[ do
lString "**"
y <- pLExp
Pow x <$> chain y,
pure x
]
pExp3 :: Parser Exp
pExp3 = pExp4 >>= chain
where
chain x =
choice
[ do
lString "*"
y <- pExp4
chain $ Mul x y,
do
lString "/"
y <- pExp4
chain $ Div x y,
pure x
]
pExp2 :: Parser Exp
pExp2 = pExp3 >>= chain
where
chain x =
choice
[ do
lString "+"
y <- pExp3
chain $ Add x y,
do
lString "-"
y <- pExp3
chain $ Sub x y,
pure x
]
pExp1 :: Parser Exp
pExp1 = pExp2 >>= chain
where
chain x =
choice
[ do
lString "=="
y <- pExp2
chain $ Eql x y,
pure x
]
pExp :: Parser Exp
pExp = pExp1
parseAPL :: FilePath -> String -> Either String Exp
parseAPL fname s = case parse (space *> pExp <* eof) fname s of
Left err -> Left $ errorBundlePretty err
Right x -> Right x

127
a5/src/APL/Tests.hs Normal file
View File

@ -0,0 +1,127 @@
module APL.Tests
( properties,
genVar
)
where
import APL.AST (Exp (..), subExp, VName, printExp)
import APL.Eval (eval, runEval)
import APL.Parser (parseAPL, keywords)
import APL.Error (isVariableError, isDomainError, isTypeError)
import APL.Check (checkExp)
import Test.QuickCheck
( Property
, Gen
, Arbitrary (arbitrary, shrink)
, property
, cover
, checkCoverage
, oneof
, sized
, frequency
, elements
, listOf
, suchThat
, resize
, withMaxSuccess
)
genString :: Gen String
genString = resize 4 $ listOf $ elements ['a'..'z']
varTest :: String -> Bool
varTest s = (not (s `elem` keywords)) && ((length s) > 1)
genVar :: Gen String
genVar = suchThat (genString) (varTest)
genInt :: Gen Integer
genInt = suchThat arbitrary (\i -> i >= 0)
instance Arbitrary Exp where
arbitrary = sized (genExp [])
shrink (Add e1 e2) =
e1 : e2 : [Add e1' e2 | e1' <- shrink e1] ++ [Add e1 e2' | e2' <- shrink e2]
shrink (Sub e1 e2) =
e1 : e2 : [Sub e1' e2 | e1' <- shrink e1] ++ [Sub e1 e2' | e2' <- shrink e2]
shrink (Mul e1 e2) =
e1 : e2 : [Mul e1' e2 | e1' <- shrink e1] ++ [Mul e1 e2' | e2' <- shrink e2]
shrink (Div e1 e2) =
e1 : e2 : [Div e1' e2 | e1' <- shrink e1] ++ [Div e1 e2' | e2' <- shrink e2]
shrink (Pow e1 e2) =
e1 : e2 : [Pow e1' e2 | e1' <- shrink e1] ++ [Pow e1 e2' | e2' <- shrink e2]
shrink (Eql e1 e2) =
e1 : e2 : [Eql e1' e2 | e1' <- shrink e1] ++ [Eql e1 e2' | e2' <- shrink e2]
shrink (If cond e1 e2) =
e1 : e2 : [If cond' e1 e2 | cond' <- shrink cond] ++ [If cond e1' e2 | e1' <- shrink e1] ++ [If cond e1 e2' | e2' <- shrink e2]
shrink (Let x e1 e2) =
e1 : [Let x e1' e2 | e1' <- shrink e1] ++ [Let x e1 e2' | e2' <- shrink e2]
shrink (Lambda x e) =
[Lambda x e' | e' <- shrink e]
shrink (Apply e1 e2) =
e1 : e2 : [Apply e1' e2 | e1' <- shrink e1] ++ [Apply e1 e2' | e2' <- shrink e2]
shrink (TryCatch e1 e2) =
e1 : e2 : [TryCatch e1' e2 | e1' <- shrink e1] ++ [TryCatch e1 e2' | e2' <- shrink e2]
shrink _ = []
genExp :: [VName] -> Int -> Gen Exp
genExp _ 0 = oneof [CstInt <$> genInt, CstBool <$> arbitrary]
genExp vars size =
frequency
[ (1, CstInt <$> genInt)
, (1, CstBool <$> arbitrary)
, (1, Add <$> genExp vars halfSize <*> genExp vars halfSize)
, (1, Sub <$> genExp vars halfSize <*> genExp vars halfSize)
, (1, Mul <$> genExp vars halfSize <*> genExp vars halfSize)
, (1, Div <$> genExp vars halfSize <*> genExp vars halfSize)
, (1, Pow <$> genExp vars halfSize <*> genExp vars halfSize)
, (1, Eql <$> genExp vars halfSize <*> genExp vars halfSize)
, (1, If <$> genExp vars thirdSize <*> genExp vars thirdSize <*> genExp vars thirdSize)
, (1, Var <$> genVar)
, (if (length vars) > 0 then 50 else 0, Var <$> elements vars)
, (25, do
var <- genVar
e1 <- genExp vars halfSize
e2 <- genExp (var:vars) halfSize
pure $ Let var e1 e2
)
, (25, do
var <- genVar
body <- genExp vars (size - 1)
pure $ Lambda var body
)
, (1, Apply <$> genExp vars halfSize <*> genExp vars halfSize)
, (1, TryCatch <$> genExp vars halfSize <*> genExp vars halfSize)
]
where
halfSize = size `div` 2
thirdSize = size `div` 3
expCoverage :: Exp -> Property
expCoverage e = checkCoverage
. cover 20 (any isDomainError (checkExp e)) "domain error"
. cover 20 (not $ any isDomainError (checkExp e)) "no domain error"
. cover 20 (any isTypeError (checkExp e)) "type error"
. cover 20 (not $ any isTypeError (checkExp e)) "no type error"
. cover 5 (any isVariableError (checkExp e)) "variable error"
. cover 70 (not $ any isVariableError (checkExp e)) "no variable error"
. cover 50 (or [2 <= n && n <= 4 | Var v <- subExp e, let n = length v]) "non-trivial variable"
$ ()
parsePrinted :: Exp -> Bool
parsePrinted e = case (parseAPL "input" (printExp e)) of
Left _ -> False
Right e' -> e == e'
onlyCheckedErrors :: Exp -> Bool
onlyCheckedErrors e = case runEval (eval e) of
Right _ -> True
Left err -> err `elem` (checkExp e)
properties :: [(String, Property)]
properties =
[ ("expCoverage", property expCoverage)
, ("parsePrinted", property (withMaxSuccess 10000 parsePrinted))
, ("onlyCheckedErrors", property (withMaxSuccess 1000000 onlyCheckedErrors))
]

BIN
a6/DekensGadePrehn-a6.zip Normal file

Binary file not shown.

30
a6/a6.cabal Normal file
View File

@ -0,0 +1,30 @@
cabal-version: 3.0
name: a6
version: 1.0.0.0
build-type: Simple
common common
default-language: Haskell2010
ghc-options: -Wall
library
import: common
hs-source-dirs: src
build-depends:
base
, tasty
, tasty-hunit
, clock
exposed-modules:
SPC
SPC_Tests
GenServer
test-suite a6-tests
import: common
type: exitcode-stdio-1.0
main-is: runtests.hs
build-depends:
base
, tasty
, a6

BIN
a6/a6.pdf Normal file

Binary file not shown.

5
a6/runtests.hs Normal file
View File

@ -0,0 +1,5 @@
import qualified SPC_Tests
import Test.Tasty (defaultMain)
main :: IO ()
main = defaultMain SPC_Tests.tests

45
a6/src/GenServer.hs Normal file
View File

@ -0,0 +1,45 @@
module GenServer
( Chan,
Server(..),
receive,
send,
sendTo,
spawn,
ReplyChan,
requestReply,
reply,
)
where
import Control.Concurrent (Chan)
import qualified Control.Concurrent as CC
data Server msg = Server CC.ThreadId (Chan msg)
data ReplyChan a = ReplyChan (Chan a)
send :: Chan a -> a -> IO ()
send chan msg =
CC.writeChan chan msg
sendTo :: Server a -> a -> IO ()
sendTo (Server _tid input) msg =
send input msg
receive :: Chan a -> IO a
receive = CC.readChan
spawn :: (Chan a -> IO ()) -> IO (Server a)
spawn server = do
input <- CC.newChan
tid <- CC.forkIO $ server input
pure $ Server tid input
requestReply :: Server a -> (ReplyChan b -> a) -> IO b
requestReply serv con = do
reply_chan <- CC.newChan
sendTo serv $ con $ ReplyChan reply_chan
receive reply_chan
reply :: ReplyChan a -> a -> IO ()
reply (ReplyChan chan) x = send chan x

409
a6/src/SPC.hs Normal file
View File

@ -0,0 +1,409 @@
{-# LANGUAGE InstanceSigs #-}
module SPC
( -- * SPC startup
SPC,
startSPC,
-- * Job functions
Job (..),
JobId,
JobStatus (..),
JobDoneReason (..),
jobAdd,
jobStatus,
jobWait,
jobCancel,
-- * Worker functions
WorkerName,
workerAdd,
workerStop,
-- debugState,
-- SPCState (..)
)
where
import Control.Concurrent
( forkIO,
killThread,
threadDelay,
ThreadId
)
import Control.Exception (SomeException, catch)
import Control.Monad (ap, forever, liftM, void, filterM, when)
import GenServer
import System.Clock.Seconds (Clock (Monotonic), Seconds, getTime)
-- First some general utility functions.
-- | Retrieve Unix time using a monotonic clock. You cannot use this
-- to measure the actual world time, but you can use it to measure
-- elapsed time.
getSeconds :: IO Seconds
getSeconds = getTime Monotonic
-- | Remove mapping from association list.
removeAssoc :: (Eq k) => k -> [(k, v)] -> [(k, v)]
removeAssoc needle ((k, v) : kvs) =
if k == needle
then kvs
else (k, v) : removeAssoc needle kvs
removeAssoc _ [] = []
-- Then the definition of the glorious SPC.
-- | A job that is to be enqueued in the glorious SPC.
data Job = Job
{ -- | The IO action that comprises the actual action of the job.
jobAction :: IO (),
-- | The maximum allowed runtime of the job, counting from when
-- the job begins executing (not when it is enqueued).
jobMaxSeconds :: Int
}
-- | A unique identifier of a job that has been enqueued.
newtype JobId = JobId Int
deriving (Eq, Ord, Show)
-- | How a job finished.
data JobDoneReason
= -- | Normal termination.
Done
| -- | The job was killed because it ran for too long.
DoneTimeout
| -- | The job was explicitly cancelled, or the worker
-- it was running on was stopped.
DoneCancelled
| -- | The job crashed due to an exception.
DoneCrashed
deriving (Eq, Ord, Show)
-- | The status of a job.
data JobStatus
= -- | The job is done and this is why.
JobDone JobDoneReason
| -- | The job is still running.
JobRunning
| -- | The job is enqueued, but is waiting for an idle worker.
JobPending
| -- | A job with this ID is not known to this SPC instance.
JobUnknown
deriving (Eq, Ord, Show)
-- | A worker decides its own human-readable name. This is useful for
-- debugging.
type WorkerName = String
-- | Messages sent to workers. These are sent both by SPC and by
-- processes spawned by the workes.
data WorkerMsg
= -- | New job time
MsgStartJob (IO ()) JobId (ReplyChan ThreadId)
| -- | Remove worker
MsgKill
-- Messages sent to SPC.
data SPCMsg
= -- | Add the job, and reply with the job ID.
MsgJobAdd Job (ReplyChan JobId)
| -- | Cancel the given job.
MsgJobCancel JobId
| -- | Immediately reply the status of the job.
MsgJobStatus JobId (ReplyChan JobStatus)
| -- | Reply when the job is done.
MsgJobWait JobId (ReplyChan JobDoneReason)
| -- | Some time has passed.
MsgTick
| -- | Ask if worker exists
MsgWorkerExists WorkerName (ReplyChan Bool)
| -- | Add a new worker
MsgAddWorker WorkerName Worker
| -- | Worker finished job
MsgJobDone JobId
| -- | Crashed
MsgJobCrashed JobId
| -- | Remove worker (workplace accident)
MsgRemoveWorker WorkerName
-- | A handle to the SPC instance.
data SPC = SPC (Server SPCMsg)
-- | A handle to a worker.
data Worker = Worker (Server WorkerMsg)
-- | The central state. Must be protected from the bourgeoisie.
data SPCState = SPCState
{ spcJobsPending :: [(JobId, Job)],
spcJobsRunning :: [(JobId, (WorkerName, Seconds, ThreadId))],
spcJobsDone :: [(JobId, JobDoneReason)],
spcJobCounter :: JobId,
spcWorkers :: [(WorkerName, Worker)],
spcWaiting :: [(JobId, (ReplyChan JobDoneReason))]
}
-- | The monad in which the main SPC thread runs. This is a state
-- monad with support for IO.
newtype SPCM a = SPCM (SPCState -> IO (a, SPCState))
instance Functor SPCM where
fmap :: (a -> b) -> SPCM a -> SPCM b
fmap = liftM
instance Applicative SPCM where
pure x = SPCM $ \state -> pure (x, state)
(<*>) = ap
instance Monad SPCM where
SPCM m >>= f = SPCM $ \state -> do
(x, state') <- m state
let SPCM f' = f x
f' state'
-- | Retrieve the state.
get :: SPCM SPCState
get = SPCM $ \state -> pure (state, state)
-- | Overwrite the state.
put :: SPCState -> SPCM ()
put state = SPCM $ \_ -> pure ((), state)
-- | Lift an 'IO' action into 'SPCM'.
io :: IO a -> SPCM a
io m = SPCM $ \state -> do
x <- m
pure (x, state)
-- | Run the SPCM monad.
runSPCM :: SPCState -> SPCM a -> IO a
runSPCM state (SPCM f) = fst <$> f state
workerIsIdle :: (WorkerName, Worker) -> SPCM Bool
workerIsIdle (name, _) = do
state <- get
pure (all (\(_, (w,_,_)) -> w /= name) (spcJobsRunning state))
checkJobTimeout :: (JobId, (WorkerName, Seconds, ThreadId)) -> SPCM ()
checkJobTimeout (jobid, (_, deadline, t)) = do
now <- io $ getSeconds
when (now >= deadline) $ do
io $ killThread t
jobDone jobid DoneTimeout
checkTimeouts :: SPCM ()
checkTimeouts = do
state <- get
mapM_ checkJobTimeout (spcJobsRunning state)
getIdleWorkers :: SPCM [(WorkerName, Worker)]
getIdleWorkers = do
state <- get
filterM (workerIsIdle) (spcWorkers state)
schedule :: SPCM ()
schedule = do
state <- get
case spcJobsPending state of
((jobid, job) : jobs) -> do
workers <- getIdleWorkers
case workers of
(workerName,worker):_ -> do
w <- (\(Worker w) -> pure w) worker
threadId <- io $ requestReply w (MsgStartJob (jobAction job) jobid)
now <- io $ getSeconds
let deadline = now + fromIntegral (jobMaxSeconds job)
put $
state
{ spcJobsRunning = (jobid, (workerName, deadline, threadId)) : spcJobsRunning state,
spcJobsPending = jobs
}
_ -> pure ()
_ -> pure ()
handleMsg :: Chan SPCMsg -> SPCM ()
handleMsg c = do
checkTimeouts
schedule
msg <- io $ receive c
case msg of
MsgJobAdd job rsvp -> do
state <- get
let JobId jobid = spcJobCounter state
put $
state
{ spcJobsPending =
(spcJobCounter state, job) : spcJobsPending state,
spcJobCounter = JobId $ succ jobid
}
io $ reply rsvp $ JobId jobid
MsgJobStatus jobid rsvp -> do
state <- get
io $ reply rsvp $ case ( lookup jobid $ spcJobsPending state,
lookup jobid $ spcJobsRunning state,
lookup jobid $ spcJobsDone state
) of
(Just _, _, _) -> JobPending
(_, Just _, _) -> JobRunning
(_, _, Just r) -> JobDone r
_ -> JobUnknown
MsgWorkerExists name rsvp -> do
state <- get
io $ reply rsvp $ case (lookup name $ spcWorkers state) of
Just _ -> True
_ -> False
MsgAddWorker name worker -> do
state <- get
put $
state
{ spcWorkers =
(name, worker) : spcWorkers state
}
MsgJobDone jobid -> do
state <- get
case (lookup jobid $ spcJobsRunning state) of
Just (_, _, _) -> do
jobDone jobid Done
Nothing -> pure ()
MsgJobWait jobid rsvp -> do
state <- get
case lookup jobid $ spcJobsDone state of
Just reason -> do
io $ reply rsvp $ reason
Nothing ->
put $ state {spcWaiting = (jobid, rsvp) : spcWaiting state}
MsgJobCancel jobid -> do
state <- get
case (lookup jobid $ spcJobsRunning state, lookup jobid $ spcJobsPending state) of
(Just (_,_,t), _) -> do
io $ killThread t
jobDone jobid DoneCancelled
(_, Just _) -> do
put $
state
{ spcJobsPending = removeAssoc jobid $ spcJobsPending state,
spcJobsDone = (jobid, DoneCancelled) : spcJobsDone state
}
_ -> pure ()
MsgJobCrashed jobid -> do
state <- get
case (lookup jobid $ spcJobsRunning state) of
Just (_, _, _) -> do
jobDone jobid DoneCrashed
Nothing -> pure ()
MsgRemoveWorker workerName -> do
state <- get
case (lookup workerName $ spcWorkers state) of
Just (Worker (Server threadId _)) -> do
jobs <- pure $ map (\(jobid, (name,_,t)) -> (name,(jobid,t))) $ spcJobsRunning state
case (lookup workerName jobs) of
Just (jobid,t) -> do
io $ killThread t
jobDone jobid DoneCancelled
Nothing -> pure ()
state2 <- get
put $ state2 {spcWorkers = removeAssoc workerName $ spcWorkers state2}
io $ killThread threadId
_ -> pure ()
_ -> pure ()
startSPC :: IO SPC
startSPC = do
let initial_state =
SPCState
{ spcJobCounter = JobId 0,
spcJobsPending = [],
spcJobsRunning = [],
spcJobsDone = [],
spcWorkers = [],
spcWaiting = []
}
c <- spawn $ \c -> runSPCM initial_state $ forever $ handleMsg c
void $ spawn $ timer c
pure $ SPC c
where
timer c _ = forever $ do
threadDelay 1000000 -- 1 second
sendTo c MsgTick
jobDone :: JobId -> JobDoneReason -> SPCM ()
jobDone jobid reason = do
state <- get
case lookup jobid $ spcJobsDone state of
Just _ ->
-- We already know this job is done.
pure ()
Nothing -> do
case (lookup jobid (spcWaiting state)) of
Just rsvp -> io $ reply rsvp $ reason
_ -> pure ()
put $
state
{ spcJobsRunning =
removeAssoc jobid $ spcJobsRunning state,
spcJobsDone =
(jobid, reason) : spcJobsDone state
}
-- | Add a job for scheduling.
jobAdd :: SPC -> Job -> IO JobId
jobAdd (SPC c) job =
requestReply c $ MsgJobAdd job
-- | Asynchronously query the job status.
jobStatus :: SPC -> JobId -> IO JobStatus
jobStatus (SPC c) jobid =
requestReply c $ MsgJobStatus jobid
-- | Synchronously block until job is done and return the reason.
jobWait :: SPC -> JobId -> IO JobDoneReason
jobWait (SPC c) jobid =
requestReply c $ MsgJobWait jobid
-- | Asynchronously cancel a job.
jobCancel :: SPC -> JobId -> IO ()
jobCancel (SPC c) jobid =
sendTo c $ MsgJobCancel jobid
-- debugState :: SPC -> IO SPCState
-- debugState (SPC c) =
-- requestReply c $ MsgDebug
-- | Add a new worker with this name. Fails with 'Left' if a worker
-- with that name already exists.
workerAdd :: SPC -> WorkerName -> IO (Either String Worker)
workerAdd (SPC c) name = do
exists <- requestReply c $ MsgWorkerExists name
if exists
then pure $ Left "Worker with given name already exist"
else do
worker <- workerSpawn name c
sendTo c $ MsgAddWorker name worker
pure $ Right worker
workerSpawn :: WorkerName -> (Server SPCMsg) -> IO Worker
workerSpawn name c = do
w <- spawn $ workerLoop name c
pure $ Worker w
workerLoop :: WorkerName -> (Server SPCMsg) -> Chan WorkerMsg -> IO ()
workerLoop name c m = forever $ do
msg <- receive m
case msg of
-- stuff happening here
MsgStartJob action jobid rsvp -> do
t <- forkIO $ do
let doJob = do
action
sendTo c $ MsgJobDone jobid
onException :: SomeException -> IO ()
onException _ =
sendTo c $ MsgJobCrashed jobid
doJob `catch` onException
reply rsvp t
MsgKill -> sendTo c $ MsgRemoveWorker name
-- | Shut down a running worker. No effect if the worker is already
-- terminated.
workerStop :: Worker -> IO ()
workerStop (Worker w) = sendTo w MsgKill

180
a6/src/SPC_Tests.hs Normal file
View File

@ -0,0 +1,180 @@
module SPC_Tests (tests) where
import Control.Concurrent (threadDelay)
import Data.IORef
import SPC
import Test.Tasty (TestTree, localOption, mkTimeout, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import Data.Either (isRight)
tests :: TestTree
tests =
localOption (mkTimeout 3000000) $
testGroup
"SPC (core)"
[
testCase "workerAdd" $ do
spc <- startSPC
w <- workerAdd spc "R2-D2"
isRight w @?= True,
testCase "workerAdd (2)" $ do
spc <- startSPC
w1 <- workerAdd spc "MSE-6"
isRight w1 @?= True
w2 <- workerAdd spc "GNK"
isRight w2 @?= True,
testCase "workerAdd (fail)" $ do
spc <- startSPC
w1 <- workerAdd spc "BD-1"
isRight w1 @?= True
w2 <- workerAdd spc "BD-1"
isRight w2 @?= False,
testCase "Running a job" $ do
ref <- newIORef False
spc <- startSPC
w <- workerAdd spc "R5-D4"
isRight w @?= True
j <- jobAdd spc $ Job (writeIORef ref True) 1
r <- jobWait spc j
r @?= Done
x <- readIORef ref
x @?= True,
testCase "Adding job before worker" $ do
ref <- newIORef False
spc <- startSPC
j <- jobAdd spc $ Job (writeIORef ref True) 1
w <- workerAdd spc "R5-D4"
isRight w @?= True
r <- jobWait spc j
r @?= Done
x <- readIORef ref
x @?= True,
testCase "Running two jobs" $ do
ref <- newIORef (0::Int)
spc <- startSPC
w <- workerAdd spc "K-2SO"
isRight w @?= True
j1 <- jobAdd spc $ Job (writeIORef ref 1) 1
r1 <- jobWait spc j1
r1 @?= Done
x1 <- readIORef ref
x1 @?= 1
j2 <- jobAdd spc $ Job (writeIORef ref 2) 1
r2 <- jobWait spc j2
r2 @?= Done
x2 <- readIORef ref
x2 @?= 2,
testCase "Canceling job (pending)" $ do
spc <- startSPC
j <- jobAdd spc $ Job (pure ()) 1
jobCancel spc j
r <- jobStatus spc j
r @?= JobDone DoneCancelled,
testCase "Canceling job (running)" $ do
spc <- startSPC
w <- workerAdd spc "IG-88"
isRight w @?= True
j <- jobAdd spc $ Job (threadDelay 2000000) 2
jobCancel spc j
r <- jobStatus spc j
r @?= JobDone DoneCancelled,
testCase "Canceling job (running) (new job)" $ do
ref <- newIORef False
spc <- startSPC
w <- workerAdd spc "C-3PO"
isRight w @?= True
j1 <- jobAdd spc $ Job (threadDelay 2000000) 2
jobCancel spc j1
r1 <- jobStatus spc j1
r1 @?= JobDone DoneCancelled
-- job has been cancelled. Starting new job
j2 <- jobAdd spc $ Job (writeIORef ref True) 1
r2 <- jobWait spc j2
r2 @?= Done
x <- readIORef ref
x @?= True,
testCase "Timeout" $ do
spc <- startSPC
w <- workerAdd spc "L3-37"
isRight w @?= True
j <- jobAdd spc $ Job (threadDelay 2000000) 1
r <- jobWait spc j
r @?= DoneTimeout,
testCase "Timeout (2 jobs)" $ do
ref <- newIORef False
spc <- startSPC
w <- workerAdd spc "General Kalani"
isRight w @?= True
j1 <- jobAdd spc $ Job (threadDelay 2000000) 1
j2 <- jobAdd spc $ Job (writeIORef ref True) 1
r1 <- jobWait spc j1
r1 @?= DoneTimeout
r2 <- jobWait spc j2
r2 @?= Done
x <- readIORef ref
x @?= True,
testCase "Crash" $ do
ref <- newIORef False
spc <- startSPC
w <- workerAdd spc "C1-10P"
isRight w @?= True
j1 <- jobAdd spc $ Job (error "boom") 1
r1 <- jobWait spc j1
r1 @?= DoneCrashed
-- Ensure new jobs can still work.
j2 <- jobAdd spc $ Job (writeIORef ref True) 1
r2 <- jobWait spc j2
r2 @?= Done
x <- readIORef ref
x @?= True,
testCase "Remove worker" $ do
spc <- startSPC
w1 <- workerAdd spc "D-O"
isRight w1 @?= True
case w1 of
(Right worker) -> do
w2 <- workerAdd spc "D-O" -- Can't make another worker with same name yet
isRight w2 @?= False
j <- jobAdd spc $ Job (threadDelay 1000000) 1
workerStop worker
threadDelay 100
r <- jobStatus spc j
r @?= JobDone DoneCancelled
w3 <- workerAdd spc "D-O" -- But we can make one now
isRight w3 @?= True
_ -> False @?= True
]