undefined
.
Eventually, the complete
version will be made available.
In class exercise: TransExercise
> {-# LANGUAGE FlexibleContexts #-}
> {-# OPTIONS -Wincomplete-patterns #-}
> module TransExercise where
This exercise involves using monad transformers to extend your interpreter for the simple imperative While
programming language.
You'll need a few additional modules for this exercise, which you will need to make sure that you also have available.
For simplicity, we define the syntax of this extended language in a separate file.
The test case try.imp demonstrates the syntax for exceptions in the extended language.
You'll also need Parser and ParserCombinator from your past homework assignment.
This exercise will give you practice with the MonadState
and MonadError
type classes and the StateT
and ExceptT
monad transformers that were introduced in the Transformers lecture. These definitions come from the mtl library.
> import Control.Monad.State (MonadState(..), StateT, State, runState, runStateT)
> import Control.Monad.Except (MonadError(..), ExceptT, runExceptT)
Expression Evaluator
- First, make sure that you understand how the expression evaluator works. Then, look at the inferred type of
evalE
in ghci.
> evalE (Var x) = do
> m <- get
> case (Map.lookup x m) of
> Just v -> return v
> Nothing -> return (IntVal 0)
> evalE (Val v) = return v
> evalE (Op e1 o e2) = evalOp o <$> evalE e1 <*> evalE e2
> evalOp :: Bop -> Value -> Value -> Value
> evalOp Plus (IntVal i1) (IntVal i2) = IntVal (i1 + i2)
> evalOp Minus (IntVal i1) (IntVal i2) = IntVal (i1 - i2)
> evalOp Times (IntVal i1) (IntVal i2) = IntVal (i1 * i2)
> evalOp Divide (IntVal _ ) (IntVal 0) = IntVal 0
> evalOp Divide (IntVal i1) (IntVal i2) = IntVal (i1 `div` i2)
> evalOp Gt (IntVal i1) (IntVal i2) = BoolVal (i1 > i2)
> evalOp Ge (IntVal i1) (IntVal i2) = BoolVal (i1 >= i2)
> evalOp Lt (IntVal i1) (IntVal i2) = BoolVal (i1 < i2)
> evalOp Le (IntVal i1) (IntVal i2) = BoolVal (i1 <= i2)
> evalOp _ _ _ = IntVal 0
- Next, modify
evalOp
andevalE
so that it usesthrowError
(from theMonadError
class) for runtime errors- in the case of divide by zero, use
IntVal 1
as the error code - in the case of invalid args to the operator, use
IntVal 2
- use code
IntVal 0
for undefined variables.
- in the case of divide by zero, use
Running and Testing the expression evaluator
To test the expression evaluator we have to pick a specific monad to use; one that satisfies both MonadState
and MonadError
constraints.
We can construct this monad easily by layering the exception monad on top of the usual State
monad.
Now we can run expressions that may throw errors!
> executeE :: Expression -> Store -> (Either Value Value, Store)
> executeE e st = runState (runExceptT comp) st where
> comp :: M Value
> comp = evalE e
We can display the errors nicely for experimentation in ghci with this function. (The display
function is defined at the end of the file).
For example, try these out:
ghci> runE (Op (Val (IntVal 1)) Divide (Val (IntVal 0)))
Uncaught exception: Divide by zero
ghci> runE (Op (Val (IntVal 1)) Divide (Val (IntVal 1)))
Result: IntVal 1
We can also write tests that expect a particular execution to raise a particular error.
> raisesE :: Expression -> Value -> Test
> s `raisesE` v = case (executeE s Map.empty) of
> (Left v',_) -> v ~?= v'
> _ -> TestCase $ assertFailure "Error in raises"
Make sure that your implementation above passes these tests.
> test_undefined :: Test
> test_undefined = "undefined variable" ~:
> ((Var "Y") `raisesE` IntVal 0)
> test_divByZero :: Test
> test_divByZero = "divide by zero" ~:
> ((Op (Val (IntVal 1)) Divide (Val (IntVal 0))) `raisesE` IntVal 1)
> test_badPlus :: Test
> test_badPlus = "bad arg to plus" ~:
> (Op (Val (IntVal 1)) Plus (Val (BoolVal True))) `raisesE` IntVal 2
> test_expErrors :: Test
> test_expErrors = "undefined variable & division by zero" ~:
> TestList [ test_undefined, test_divByZero, test_badPlus ]
Statement Evaluator
- Now modify the statement evaluator so that it throws errors Use code (IntVal 3) for integer conditions in
While
and (IntVal 4) for integer conditions inIf
statements. (Ignore the cases forTry
andThrow
for now.)
> evalS :: (MonadError Value m, MonadState Store m) => Statement -> m ()
> evalS w@(While e (Block ss)) = do
> v <- evalE e
> case v of
> BoolVal True -> evalB (Block (ss ++ [w]))
> BoolVal False -> return ()
> IntVal _ -> return ()
> evalS (Assign x e) = do
> v <- evalE e
> m <- get
> put (Map.insert x v m)
> evalS (If e b1 b2) = do
> v <- evalE e
> case v of
> BoolVal True -> evalB b1
> BoolVal False -> evalB b2
> IntVal _ -> return ()
> evalS (Try _ _ _) = error "evalS: unimplemented"
> evalS (Throw _) = error "evalS: unimplemented"
- Statement Execution, finish this
Try out your execute
with this operation:
> run :: Block -> IO ()
> run block = do let (r, s) = execute block Map.empty
> putStrLn (display r)
> putStr "Output Store: "
> putStrLn (show s)
For example:
ghci> run $ Block [While (Val (IntVal 0)) (Block [])]
Uncaught exception: Invalid condition in while statement
Output Store: fromList []
Test your functions with this helper
> raises :: Block -> Value -> Test
> s `raises` v = case (execute s Map.empty) of
> (Left v',_) -> v ~?= v'
> _ -> TestCase $ assertFailure "Error in raises"
and these tests:
> test_badWhile :: Test
> test_badWhile = Block [While (Val (IntVal 0)) (Block [])] `raises` IntVal 3
> test_badIf :: Test
> test_badIf = Block [If (Val (IntVal 0)) (Block []) (Block [])] `raises` IntVal 4
- Add user-level exceptions.
There are two new statement forms in this language. Extend the evaluator above so that it can handle them.
Throw e
should evaluate the expressione
and an exception carrying the value ofe
Try s x h
should execute the statements
and if, in the course of execution, an exception is thrown, then the exception value should be assigned to the variablex
after which the handler statementh
is executed.
Note: the catchError
function in Control.Monad.Except will be necessary for Try
statements.
For example, this code
> test1 = do
> mb <- parse "try.imp"
> case mb of
> Right b -> run b
> Left _ -> putStrLn "parse error"
Should print
Result: Right ()
Output Store: fromList [("a",IntVal 100),("e",IntVal 1),("x",IntVal 0),("y",IntVal 1),("z",IntVal 101)]
Displaying the results
> display :: Show a => (Either Value a) -> String
> display (Left v) = "Uncaught exception: " ++ displayExn v
> display (Right v) = "Result: " ++ show v
> displayExn :: Value -> String
> displayExn (IntVal 0) = "Undefined variable"
> displayExn (IntVal 1) = "Divide by zero"
> displayExn (IntVal 2) = "Invalid arguments to operator"
> displayExn (IntVal 3) = "Invalid condition in while statement"
> displayExn (IntVal 4) = "Invalid condition in if statement"
> displayExn v = "Error code: " ++ show v