> {-@ LIQUID "--no-termination" @-}
>
> module Interpreter (interpret) where
>
> import Prelude hiding (lookup)
> import qualified Data.Set as S
HINT: To do this problem, first go through this case study also included in your tarball as case-study-eval.lhs
.
Next, you will revisit your interpreter for the WHILE language to ensure that execution never fails due to a use-before-definition error.
Programs in the language are simply values of the type
> data Statement =
> Assign Variable Expression -- x = e
> | IfZ Expression Statement Statement -- if (e) {s1} else {s2}
> | WhileZ Expression Statement -- while (e) {s}
> | Sequence Statement Statement -- s1; s2
> | Skip -- no-op
to simplify matters, we assume the branch statement IfZ e s1 s2
evaluates e
and then executes s1
if the value of e
equals 0
and otherwise evaluates s2
.
Thus, the expressions are variables, constants or binary operators applied to sub-expressions
> data Expression =
> Var Variable -- x
> | Val Value -- v
> | Op Bop Expression Expression
and binary operators are simply two-ary functions
> data Bop =
> Plus -- (+) :: Int -> Int -> Int
> | Minus -- (-) :: Int -> Int -> Int
and variables and values are just:
> type Variable = String
> type Value = Int
We will represent the store i.e. the machine’s memory, as a list of Variable
- Value
pairs:
> type Store = [(Variable, Value)]
>
> update :: Store -> Variable -> Value -> Store
> update st x v = (x, v) : st
>
> lookup :: Variable -> Store -> Value
> lookup x ((y, v) : st)
> | x == y = v
> | otherwise = lookup x st
> lookup x [] = impossible "variable not found"
We can now write a function that evaluates Statement
in a Store
to yield a new updated Store
:
> evalS :: Store -> Statement -> Store
>
> evalS st Skip = st
>
> evalS st (Assign x e ) = update st x v
> where
> v = evalE st e
>
> evalS st (IfZ e s1 s2) = if v == 0
> then evalS st s1
> else evalS st s2
> where
> v = evalE st e
> evalS st w@(WhileZ e s) = if v == 0
> then evalS st (Sequence s w)
> else st
> where
> v = evalE st e
>
> evalS st (Sequence s1 s2) = evalS (evalS st s1) s2
The above uses a helper that evaluates an Expression
in a Store
to get a Value
:
> evalE :: Store -> Expression -> Value
> evalE st (Var x) = lookup x st
> evalE _ (Val v) = v
> evalE st (Op o e1 e2) = evalOp o (evalE st e1) (evalE st e2)
>
> evalOp :: Bop -> Value -> Value -> Value
> evalOp Plus i j = i + j
> evalOp Minus i j = i - j
Our goal is to write an evaluator that never fails due to an undefined variable. This means, we must ensure that the evaluator is never called with malformed programs in which some variable is used-before-being-defined.
In particular, this corresponds to establishing that the call to impossible never happens at run time, by verifying that the below typechecks:
> {-@ impossible :: {v:String | false} -> a @-}
> impossible msg = error msg
Obviously it is possible to throw an exception if you run evalS
with a bad statement. Your task is to complete the implementation of isSafe
and add suitable refinement type specifications, such that you can prove that the following interpret
function is safe:
> interpret :: Statement -> Maybe Store
> interpret s
> | isSafe s = Just (evalS [] s) -- `s` does not use any vars before definition
> | otherwise = Nothing -- `s` may use some var before definition
>
> {-@ inline isSafe @-}
> isSafe :: Statement -> Bool
> isSafe s = True -- TODO: replace with correct implementation
To implement isSafe
you probably need to write a function that computes the Set
of variables that are `read-before-definition" in a
Statement(and
Expression`):
> {-@ measure readS @-}
> readS :: Statement -> S.Set Variable
> readS (Assign x e) = S.empty -- TODO: replace with proper definition
> readS (IfZ e s1 s2) = S.empty -- TODO: replace with proper definition
> readS (WhileZ e s) = S.empty -- TODO: replace with proper definition
> readS (Sequence s1 s2) = S.empty -- TODO: replace with proper definition
> readS Skip = S.empty -- TODO: replace with proper definition
>
> {-@ measure readE @-}
> readE :: Expression -> S.Set Variable
> readE (Var x) = S.empty -- TODO: replace with proper definition
> readE (Val v) = S.empty -- TODO: replace with proper definition
> readE (Op o e1 e2) = S.empty -- TODO: replace with proper definition
Here are some example programs:
>
> safeStatement1 =
> (Assign "X" (Val 5))
> `Sequence`
> (Assign "Y" (Var "X"))
>
> safeStatement2 =
> (IfZ (Val 0)
> (Assign "Z" (Val 1))
> (Assign "Z" (Val 2)))
> `Sequence`
> (Assign "Y" (Var "Z"))
>
> unsafeStatement1 =
> (Assign "X" (Val 5))
> `Sequence`
> (Assign "Y" (Var "Z"))
>
> unsafeStatement2 =
> (WhileZ (Val 0) (Assign "Z" (Val 1)))
> `Sequence`
> (Assign "Y" (Var "Z"))
When you are done
liquid Interpreter.lhs
should return SAFE
.
You should get the following behavior in ghci
.
*Interpreter> interpret safeStatement1
Just [("Y",5),("X",5)]
*Interpreter> interpret safeStatement2
Just [("Y",1),("Z",1)]
*Interpreter> interpret unsafeStatement1
Nothing
*Interpreter> interpret unsafeStatement2
Nothing
Note that the second step is important, you could implement isSafe _ = False
which just rejects every program, but that is not a very useful interpreter.