Mr.Bloom
Mr.Bloom

Reputation: 365

Language Design: Confusion as to why an assigned variable returns the wrong answer in arithmetic expressions

Edit: For clarification, all arithmetic expressions in the form of integer op integer work as they should but variable op integer results in the confusing result.

In the simple language I am writing, I am at a point in which I am trying to implement user defined variables. I am following the 'Write Yourself a Scheme in 48 Hours' a guide to structure my development process. I have successfully implement variable instantiation such that the behaviour of my expression x:=4 behaves like WYS48's (define x 4) in that they both return 4.

When I try to use x in an arithmetic expression however, I get an unusual result.

Henry > x:=4
4
Henry > <x+4>
76

At first I assumed it was adding the ASCII value of x to 4 but it isn't because the ASCII value for x is 120 and it's Hex is 78, so I know it's not that. I don't see where the mistake is however in the program. I suspect it may be in my function str2Int mainly due to the subtraction of 48 in it. Below is my code to evaluate arithmetic, expressions, and the functions that I used to evaluate my x. Subtracting 71 when the expression is x:=0 seems to work for that case an arithmetic is fine for that instantiation of x but it's not a particularly optimal fix.

eval :: Env -> HenryVal -> IOThrowsError HenryVal
eval env val@(Atom _) = return val
eval env val@(ABinOp _) = return val
eval env (Assign var val) = eval env val >>= defineVar env var
eval env (ABinary op x y) = return $ evalABinOp env x op y

evalABinOp :: Env -> HenryVal -> ABinOp -> HenryVal -> HenryVal
evalABinOp env (Atom a) Add (Integer b) = Integer ((toInteger (str2Int'(str2HenryStr (a)))) + b )

str2Int' :: HenryVal -> Integer
str2Int' n = toInteger $ (ord (show n !! 1)) - 48

str2HenryStr :: String -> HenryVal
str2HenryStr s = String $ s

I'm not sure of it's relevancy but below is the code that I used to implement variable assignment

type Env = IORef [(String, IORef HenryVal)] 
type ThrowsError = Either HenryError
type IOThrowsError = ExceptT HenryError IO

nullEnv :: IO Env
nullEnv = newIORef []

liftThrows :: ThrowsError a -> IOThrowsError a
liftThrows (Left err) = throwError err
liftThrows (Right val) = return val

runIOThrows :: IOThrowsError String -> IO String
runIOThrows action = runExceptT (trapError action) >>= return . extractValue

isBound :: Env -> String -> IO Bool
isBound envRef var = readIORef envRef >>= return . maybe False (const True) . lookup var

getVar :: Env -> String -> IOThrowsError HenryVal
getVar envRef var  =  do env <- liftIO $ readIORef envRef
                         maybe (throwError $ UnboundVar "Getting an unbound variable" var)
                               (liftIO . readIORef)
                               (lookup var env)

setVar :: Env -> String -> HenryVal -> IOThrowsError HenryVal
setVar envRef var value = do env <- liftIO $ readIORef envRef
                             maybe (throwError $ UnboundVar "Setting an unbound variable" var)
                                   (liftIO . (flip writeIORef value))
                                   (lookup var env)
                             return value

defineVar :: Env -> String -> HenryVal -> IOThrowsError HenryVal
defineVar envRef var value = do
     alreadyDefined <- liftIO $ isBound envRef var
     if alreadyDefined
        then setVar envRef var value >> return value
        else liftIO $ do
             valueRef <- newIORef value
             env <- readIORef envRef
             writeIORef envRef ((var, valueRef) : env)
             return value

bindVars :: Env -> [(String, HenryVal)] -> IO Env
bindVars envRef bindings = readIORef envRef >>= extendEnv bindings >>= newIORef
     where extendEnv bindings env = liftM (++ env) (mapM addBinding bindings)
           addBinding (var, value) = do ref <- newIORef value
                                        return (var, ref)

Upvotes: 1

Views: 147

Answers (1)

Zeta
Zeta

Reputation: 105935

Unless you have any other implementation of eval, any variable will not get evaluated as its value for binary operations (aside from assignment). Let's have a look at eval and evalABinOp:

eval :: Env -> HenryVal -> IOThrowsError HenryVal
eval env val@(Atom _)     = return val
eval env val@(ABinOp _)   = return val
eval env (Assign var val) = eval env val >>= defineVar env var  -- defineVar returns val
eval env (ABinary op x y) = return $ evalABinOp env x op y      -- evalABinOp uses env?

evalABinOp :: Env -> HenryVal -> ABinOp -> HenryVal -> HenryVal
evalABinOp env (Atom a) Add (Integer b) = Integer ((toInteger (str2Int'(str2HenryStr (a)))) + b )
                                          -- env is not used on the right hand side!

As you never use env on the right hand side, we can be 100% sure that the Atom a cannot be interpreted as a variable, but must be interpreted as a number or string (or as undefined variable). Nowhere in your code do you actually look up a's value in the current environment. Instead, you transform x into an integer:

str2Int' :: HenryVal -> Integer
str2Int' n = toInteger $ (ord (show n !! 1)) - 48

str2HenryStr :: String -> HenryVal
str2HenryStr s = String $ s

I neither have HenryVal's definition nor its Show instance at hand, so I can only assume that show (String "x") results in "\"x\"" and thus "\"x\"" !! == 'x'. ord 'x' - 48 is 120 - 48 = 72. Add 4, and you end up with 76. The arithmetic works as intended. You just don't use the assigned value at all. Instead, you don't even interpret the Atom as a variable, but as an ASCII digit of a single-digit number.

That being said, how would a solution look like? Well, something like that:

evalABinOp :: Env -> HenryVal -> ABinOp -> HenryVal -> IOThrowsError HenryVal
evalABinOp env (Integer a) Add (Integer b)   = return $ Integer $ a + b
evalABinOp env (Atom a)    op  b@(Integer _) = getVar env a >>= (\c -> evalABinOp env c op b)

Instead of using a immediately, we first lookup the value. Note that this will only work if your parser interprets a number as Integer in your HenryVal language. Note that there is no str2Int magic going on: all of that should get handled long before we use x, unless we want to allow addition of Strings.

Upvotes: 2

Related Questions