├── .gitignore ├── README.md ├── main.hs ├── run.sh └── spec.hs /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | *.hi 3 | *.o 4 | main 5 | .liquid 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Background 2 | This project aims to write a Scheme using Haskell. It follows the [Write Yourself a Scheme in 48 Hours](https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours) Tutorial. 3 | 4 | ### Related Twitch Streams 5 | |Part 1 |Part 1.5 | Part 2 | 6 | |---------|---------|---------| 7 | |[](https://www.twitch.tv/videos/698538222) | [](https://www.twitch.tv/videos/703470743) | [](https://www.twitch.tv/videos/704147445) | 8 | 9 | ## What is a Scheme? 10 | A Scheme is a statically scoped and properly tail-recursive dialect of the Lisp programming language. 11 | You can read more about it [here](https://groups.csail.mit.edu/mac/projects/scheme/) or on [Wikipedia](https://en.wikipedia.org/wiki/Scheme_(programming_language)). 12 | 13 | # Getting Started 14 | To run the project you should install `ghc` or `ghci`, the interactive haskell environment. 15 | You can download it from the haskell website: [https://www.haskell.org/downloads/](https://www.haskell.org/downloads/). 16 | 17 | After installation you can either run the file `run.sh` or edit and recompile the `main.hs` file through `ghci`. 18 | 19 | To run examples you can follow the book [Structure and Interpretation of Computer Programs](https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book.html). 20 | This project should currently be up to 1.1.7 in Chapter 1. 21 | 22 | Tests are defined in the `spec.hs` file. Feel free to add more! 23 | 24 | # Next Steps 25 | |Name |Status | 26 | |-------------------|---------| 27 | |Add float support | 👷 TODO | 28 | |Add "cond" support | 👷 TODO | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | 3 | module Main where 4 | import Text.ParserCombinators.Parsec hiding (spaces) 5 | import System.Environment 6 | import Control.Monad 7 | import Numeric 8 | import System.IO 9 | import Control.Monad.IO.Class 10 | import Data.IORef 11 | 12 | -- ****** Environment ****** 13 | 14 | type Env = IORef [(String, IORef LispVal)] 15 | 16 | instance Show (IORef a) where 17 | show _ = "" 18 | 19 | nullEnv :: IO Env 20 | nullEnv = newIORef [] 21 | 22 | isBound :: Env -> String -> IO Bool 23 | isBound envRef var = readIORef envRef >>= return . maybe False 24 | (const True) . lookup var 25 | 26 | getVar :: Env -> String -> IO LispVal 27 | getVar envRef var = do 28 | env <- readIORef envRef 29 | -- maybe :: b -> (a -> b) -> Maybe a -> b 30 | maybe undefined 31 | readIORef 32 | (lookup var env) 33 | 34 | {- 35 | setVar :: Env -> String -> LispVal 36 | setVar envRef var value = do 37 | env <- liftIO $ readIORef envRef 38 | (liftIO . (flip writeIORef value)) (lookup var env) 39 | return value 40 | 41 | defineVar :: Env -> String -> LispVal 42 | defineVar envRef var value = do 43 | alreadyDefined <- liftIO $ isBound envRef var 44 | if alreadyDefined 45 | then setVar envRef var value >> return value 46 | else liftIO $ do 47 | valueRef <- newIORef value 48 | env <- readIORef envRef 49 | writeIORef envRef ((var, valueRef) : env) 50 | return value 51 | -} 52 | 53 | defineVar :: Env -> String -> LispVal -> IO LispVal 54 | defineVar envRef var value = do 55 | valueRef <- newIORef value 56 | env <- readIORef envRef 57 | writeIORef envRef ((var, valueRef) : env) 58 | return value 59 | 60 | 61 | -- ****** Data Types ****** 62 | 63 | 64 | data LispVal = Atom String 65 | | List [LispVal] 66 | | DottedList [LispVal] LispVal 67 | | Number Integer 68 | | Float Float 69 | | String String 70 | | Bool Bool 71 | | PrimitiveFunc ([LispVal] -> LispVal) 72 | | Func { params :: [String], vararg :: (Maybe String), 73 | body :: [LispVal], closure :: Env } 74 | deriving Show 75 | 76 | instance Show ([LispVal] -> LispVal) where 77 | show _ = "" 78 | 79 | -- ****** Parser ****** 80 | 81 | parseString :: Parser LispVal 82 | parseString = do 83 | char '"' 84 | x <- many (noneOf "\"") 85 | char '"' 86 | return $ String x 87 | 88 | 89 | parseAtom :: Parser LispVal 90 | parseAtom = do 91 | first <- letter <|> symbol 92 | rest <- many (letter <|> digit <|> symbol) 93 | let atom = first:rest 94 | return $ case atom of 95 | "#t" -> Bool True 96 | "#f" -> Bool False 97 | _ -> Atom atom 98 | 99 | 100 | parseNumber :: Parser LispVal 101 | parseNumber = do 102 | x <- many1 digit 103 | return $ Number $ read x 104 | -- parseNumber = liftM (Number . read) $ many1 digit 105 | 106 | parseFloat :: Parser LispVal 107 | parseFloat = do 108 | x <- many1 digit 109 | char '.' 110 | y <- many1 digit 111 | let atom = (x ++ "." ++ y) 112 | return $ Float $ read atom 113 | 114 | parseExpr :: Parser LispVal 115 | parseExpr = parseAtom 116 | <|> parseString 117 | <|> try parseFloat 118 | <|> parseNumber 119 | <|> parseQuoted 120 | <|> do char '(' 121 | x <- try parseList <|> parseDottedList 122 | char ')' 123 | return x 124 | 125 | parseList :: Parser LispVal 126 | parseList = liftM List $ sepBy parseExpr spaces 127 | 128 | parseDottedList :: Parser LispVal 129 | parseDottedList = do 130 | head <- endBy parseExpr spaces 131 | tail <- char '.' >> spaces >> parseExpr 132 | return $ DottedList head tail 133 | 134 | parseQuoted :: Parser LispVal 135 | parseQuoted = do 136 | char '\'' 137 | x <- parseExpr 138 | return $ List [Atom "quote", x] 139 | 140 | spaces :: Parser () 141 | spaces = skipMany1 space 142 | 143 | symbol :: Parser Char 144 | symbol = oneOf "!#$%&|*+-/:<=>?@^_~" 145 | 146 | readExpr :: String -> LispVal 147 | readExpr input = case parse parseExpr "lisp" input of 148 | Left err -> String $ "No match: " ++ show err 149 | Right val -> val 150 | 151 | -- ****** Eval ****** 152 | 153 | unpackNum :: LispVal -> Integer 154 | unpackNum (Number n) = n 155 | unpackNum (Bool True) = 1 156 | unpackNum (Bool False) = 0 157 | -- no weak typing 158 | 159 | unpackStr :: LispVal -> String 160 | unpackStr (String s) = s 161 | 162 | unpackAtom :: LispVal -> String 163 | unpackAtom (Atom s) = s 164 | 165 | unpackBool :: LispVal -> Bool 166 | unpackBool (Bool b) = b 167 | 168 | numericBinop :: (Integer -> Integer -> Integer) -> [LispVal] -> LispVal 169 | numericBinop op params = Number $ foldl1 op $ map unpackNum params 170 | 171 | compareBinop [Atom x, Atom y] = (Bool (x==y)) 172 | compareBinop _ = (Bool False) 173 | 174 | boolBinop :: (LispVal -> a) -> (a -> a -> Bool) -> [LispVal] -> LispVal 175 | boolBinop unpacker op [x, y] = Bool $ (unpacker x) `op` (unpacker y) 176 | 177 | numBoolBinop = boolBinop unpackNum 178 | strBoolBinop = boolBinop unpackStr 179 | boolBoolBinop = boolBinop unpackBool 180 | 181 | primitives :: [(String, [LispVal] -> LispVal)] 182 | primitives = [("+", numericBinop (+)), 183 | ("-", numericBinop (-)), 184 | ("*", numericBinop (*)), 185 | ("/", numericBinop div), 186 | ("mod", numericBinop mod), 187 | ("quotient", numericBinop quot), 188 | ("remainder", numericBinop rem), 189 | ("=", numBoolBinop (==)), 190 | ("<", numBoolBinop (<)), 191 | (">", numBoolBinop (>)), 192 | ("/=", numBoolBinop (/=)), 193 | (">=", numBoolBinop (>=)), 194 | ("<=", numBoolBinop (<=)), 195 | ("&&", boolBoolBinop (&&)), 196 | ("||", boolBoolBinop (||)), 197 | ("string=?", strBoolBinop (==)), 198 | ("string?", strBoolBinop (>)), 200 | ("string<=?", strBoolBinop (<=)), 201 | ("string>=?", strBoolBinop (>=)), 202 | ("eq?", compareBinop)] 203 | 204 | 205 | primitiveBindings :: IO Env 206 | primitiveBindings = do 207 | env <- nullEnv 208 | mapM (makePrimitiveFunc env) primitives 209 | return env 210 | where makePrimitiveFunc env (var, func) = defineVar env var (PrimitiveFunc func) 211 | 212 | apply :: LispVal -> [LispVal] -> IO LispVal 213 | apply (PrimitiveFunc func) args = return $ func args 214 | apply (Func params vararg body closure) args = do 215 | mapM (bindVar closure) (zip params args) 216 | -- TODO: why is body a list? I don't think this is always right 217 | eval closure (body !! 0) 218 | where bindVar env (p, arg) = defineVar env p arg 219 | 220 | 221 | -- return $ (Bool False) 222 | 223 | {- 224 | apply (Atom func) args = return $ maybe 225 | (Bool False) 226 | ($ args) 227 | (lookup func primitives) 228 | -} 229 | 230 | makeFunc varargs env params body = return $ Func (map unpackAtom params) varargs body env 231 | makeNormalFunc = makeFunc Nothing 232 | 233 | eval :: Env -> LispVal -> IO LispVal 234 | eval _ val@(Float _) = return val 235 | eval _ val@(String _) = return val 236 | eval _ val@(Number _) = return val 237 | eval _ val@(Bool _) = return val 238 | eval _ (List [Atom "quote", val]) = return val 239 | eval env (Atom id) = getVar env id 240 | eval env (List [Atom "define", Atom var, form]) = 241 | eval env form >>= defineVar env var 242 | eval env (List (Atom "define" : List (Atom var : params) : body)) = 243 | makeNormalFunc env params body >>= defineVar env var 244 | eval env (List [Atom "if", pred, conseq, alt]) = do 245 | result <- eval env pred 246 | case result of 247 | Bool False -> eval env alt 248 | otherwise -> eval env conseq 249 | eval env (List (Atom "cond" : pairs)) = evalCond pairs 250 | where evalCond (List [Atom "else", value] : []) = eval env value 251 | evalCond (List [condition, value] : rest) = do 252 | conditionResult <- eval env condition 253 | case conditionResult of 254 | Bool False -> evalCond rest 255 | _ -> eval env value 256 | evalCond [] = pure $ Atom "" 257 | eval env (List (Atom func : args)) = do 258 | x <- mapM (eval env) args 259 | f <- (getVar env func) 260 | apply f x 261 | 262 | --eval env (List (Atom func : args)) = do 263 | -- x <- mapM (eval env) args 264 | -- apply (Atom func) x 265 | --eval env (List (Atom func : args)) = return $ apply func $ mapM (eval env) args 266 | 267 | -- ****** REPL ****** 268 | 269 | readPrompt :: String -> IO String 270 | readPrompt prompt = putStr prompt >> hFlush stdout >> getLine 271 | 272 | evalString :: Env -> String -> IO String 273 | evalString env expr = liftM show $ eval env $ readExpr expr 274 | 275 | evalAndPrint :: Env -> String -> IO () 276 | evalAndPrint env expr = evalString env expr >>= putStrLn 277 | 278 | until_ pred prompt action = do 279 | result <- prompt 280 | if pred result 281 | then return () 282 | else action result >> until_ pred prompt action 283 | 284 | main :: IO () 285 | main = do 286 | env <- primitiveBindings 287 | until_ (== "quit") (readPrompt "Lisp>>> ") (evalAndPrint env) 288 | 289 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | ghc main.hs 3 | rlwrap ./main 4 | -------------------------------------------------------------------------------- /spec.hs: -------------------------------------------------------------------------------- 1 | module Spec where 2 | 3 | import Main hiding (main) 4 | import Test.Hspec 5 | import Data.Foldable 6 | 7 | test exprs expected = it (last exprs) $ do 8 | env <- primitiveBindings 9 | for_ (init exprs) $ evalString env 10 | result <- evalString env (last exprs) 11 | shouldBe result expected 12 | 13 | main :: IO () 14 | main = hspec $ do 15 | describe "tests" $ do 16 | test ["(+ 137 349)"] "Number 486" 17 | test ["(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))"] "Number 57" 18 | test ["(define size 2)", "size"] "Number 2" 19 | test ["(define (square x) (* x x))", "(square 4)"] "Number 16" 20 | test ["(define (square x) (* x x))", "(square (square 3))"] "Number 81" 21 | test ["(define (square x) (* x x))", 22 | "(define (sum-of-squares x y) (+ (square x) (square y)))", 23 | "(sum-of-squares 3 4)"] "Number 25" 24 | test ["(if #t 7 3)"] "Number 7" 25 | test ["(if #f 7 3)"] "Number 3" 26 | test ["(define (abs x) (if (< x 0) (- x) x))", 27 | "(+ (abs 5) (abs (- 5)))"] "Number 10" 28 | test ["(define (abs x) (cond ((> x 0) x) ((= x 0) 0) ((< x 0) (- x))))", 29 | "(abs (- 1))"] "Number 1" 30 | test ["(define (abs x) (cond ((< x 0) (- x)) (else x)))", 31 | "(abs (- 10))"] "Number 10" 32 | test ["(cond (1 1) (else 2))"] "Number 1" -- currently produces "Number 2" 33 | -- analogous test for `if` 34 | test ["(if 1 1 2)"] "Number 1" 35 | 36 | -- parsing has some problems with spaces 37 | test ["( * 2 2 )"] "Number 4" -- currently a parse error 38 | 39 | -- the stuff below might technically be more like 1.1.8 stuff, 40 | -- so not strictly within chapter 1.1.7 41 | 42 | -- this is why "body" is a list 43 | test ["(define (forthpower n) (define m (* n n)) (* m m))", 44 | "(forthpower 3)"] "Number 81" -- currently produces "Number 9" 45 | 46 | -- parameter definitions should not leak 47 | test ["(define (square x) (* x x))", 48 | "(define x 42)", 49 | "(square 5)", 50 | "x"] "Number 42" -- currently produces "Number 5" 51 | test ["(define (square x) (* x x))", 52 | "(define (fithpower x) (* (square (square x)) x))", 53 | "(fithpower 4)"] "Number 1024" -- currently produces "Number 4096" 54 | 55 | -- and in general, local definitions should only be visible inside of the function 56 | test ["(define m 42)", 57 | "(define (forthpower n) (define m (* n n)) (* m m))", 58 | "(forthpower 3)", 59 | "m"] "Number 42" -- currently produces "Number 9" 60 | --------------------------------------------------------------------------------