Lecture 6 — 2020-02-04
WhileNZ
This lecture is written in literate Haskell; you can download the raw source.
We started defining a simple imperative language, which we called WhileNZ. Here’s its syntax and semantics:
n in Nat
v in Var (some set of variables)
e in Expr ::= n | e1 plus e2 | e1 times e2 | neg e | x
c in Command ::= SKIP | c1 ; c2 | x := e | WHILENZ e DO c END
sigma in Store, the set of total functions assigning ints to variables
i.e. Var -> Z
(sigma[x |-> z])(y) = / z if x = y
\ sigma(y) otherwise
eval : Store -> Expr -> Z
eval(sigma, n) = n
eval(sigma, e1 plus e2) = eval(sigma, e1) + eval(sigma, e2)
eval(sigma, e1 times e2) = eval(sigma, e1) * eval(sigma, e2)
eval(sigma, neg e) = -eval(sigma, e)
eval(sigma, x) = sigma(x)
interp : Store -> Command -> Store
interp(sigma, SKIP) = sigma
interp(sigma, c1 ; c2) = interp(interp(sigma, c1), c2)
interp(sigma, x := e) = sigma[x|->eval(sigma, e)]
interp(sigma, WHILENZ e DO c END) =
if eval(sigma, e) = 0
then sigma
else interp(interp(sigma, c), WHILENZ e DO c)
We went over an example or two in class, running some loops and observing that sometimes interp
wasn’t well defined, i.e., sometimes it ran forever, which isn’t the sort of thing that math is supposed to do.
Try running interp
on the following programs… what do they do? Do they always terminate? If so, what values will you get out?
Y := 5;
X := 1;
WHILENZ Y DO
X := Y * X;
Y := Y - 1
END
What will X
be? What about Y
?
A := X;
WHILENZ A DO
B := B + 1
END
What values will A
, B
and X
have?
X := 1;
WHILENZ X DO
X := X + 1
END
A simple interpreter
Here’s how to write an interpreter for our simple language consisting of arithmetic expressions, assignments, and loops.
The first order of business is thinking about variables: how should we represent them? Haskell’s map type seems like a good bet.
import qualified Data.Map as Map
import Data.Map (Map, (!))
type VarName = String
type Store = Map VarName Int
update :: VarName -> Int -> Store -> Store
update v n st = Map.insert v n st
Arithmetic expressions
We return to our old friend, ArithExp
, adding a notion of variable.
data ArithExp =
Num Int
| Var VarName
| Plus ArithExp ArithExp
| Times ArithExp ArithExp
| Neg ArithExp
deriving (Show, Eq)
evalA :: Store -> ArithExp -> Int
evalA st (Var x) = st ! x -- Map.findWithDefault ?
evalA _ (Num n) = n
evalA st (Plus e1 e2) = evalA st e1 + evalA st e2
evalA st (Times e1 e2) = evalA st e1 * evalA st e2
evalA st (Neg e) = - (evalA st e)
The interpreter is pretty similar to our old one, but note that we take an extra argument, st :: Store
. The store is where we look for variables. If a variable isn’t in the store, then Map.!
will complain with an error.
Commands in WhileNZ
Having converted the mathematical grammar:
s ::= skip | x := a | s1;s2 | while a s
To a Haskell datatype:
data WhileNZ =
Skip
| Assign VarName ArithExp
| Seq WhileNZ WhileNZ
| WhileNZ ArithExp WhileNZ
deriving Show
Let’s set about writing our interpreter. While evalA :: Store -> ArithExp -> Int
, here eval
takes a Store
and a statement and returns a new store.
eval :: Store -> WhileNZ -> Store
eval st Skip = st
eval st (Assign x e) = update x (evalA st e) st
eval st (Seq c1 c2) = eval (eval st c1) c2
eval st eLoop@(WhileNZ e c) =
if evalA st e == 0
then st
else eval (eval st c) eLoop
We can try writing a program or two…
p1, p2 :: WhileNZ
p1 = Seq (Assign "x" (Plus (Var "x") (Num 1)))
(Assign "y" (Plus (Var "y") (Num (-1))))
p2 = Seq (Assign "y" (Num 5))
(Seq (Assign "x" (Num 0))
(WhileNZ (Var "y") p1))
…and then we can run them in a sample store: