├── .gitignore ├── .idea ├── IdeaProject.iml ├── codeStyleSettings.xml ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── highlighting.xml ├── libraries │ ├── SBT__org_scala_lang_scala_actors_2_10_0_test.xml │ ├── SBT__org_scala_lang_scala_library_2_10_1.xml │ ├── SBT__org_scala_lang_scala_reflect_2_10_0_test.xml │ ├── SBT__org_scalatest_scalatest_2_10_1_9_1_test.xml │ ├── SBT__scala_2_10_1.xml │ └── SBT__scala_2_9_2.xml ├── modules.xml ├── projectCodeStyle.xml ├── scala_compiler.xml ├── scopes │ └── scope_settings.xml ├── uiDesigner.xml └── vcs.xml ├── .idea_modules ├── Scheme Scala-build.iml └── Scheme Scala.iml ├── README.md ├── build.sbt ├── project └── plugins.sbt └── src ├── main └── scala │ └── mtscheme │ ├── BuiltIn.scala │ ├── Env.scala │ ├── HandRolledParser.scala │ ├── Interpreter.scala │ ├── Parser.scala │ ├── package.scala │ └── repl.scala └── test └── scala └── mtscheme ├── EnvTest.scala ├── InterpreterTest.scala └── ParserTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | */target* 3 | target* 4 | .idea/workspace.xml 5 | .idea/tasks.xml 6 | .idea/misc.xml 7 | *~ -------------------------------------------------------------------------------- /.idea/IdeaProject.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/highlighting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/libraries/SBT__org_scala_lang_scala_actors_2_10_0_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/SBT__org_scala_lang_scala_library_2_10_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/SBT__org_scala_lang_scala_reflect_2_10_0_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/SBT__org_scalatest_scalatest_2_10_1_9_1_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/SBT__scala_2_10_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/SBT__scala_2_9_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/projectCodeStyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/scala_compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea_modules/Scheme Scala-build.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /.idea_modules/Scheme Scala.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An interpreter of (a subset of) the Scheme programming language 2 | 3 | Written in Scala 4 | 5 | Copyright Martin Trojer 6 | 7 | This is a toy, created as code example to the blogposts listed below. 8 | It was never the intention create a complete Scheme implementation. 9 | If you're interested in Scheme on the JVM, I suggest Kawa 10 | http://www.gnu.org/software/kawa/ 11 | 12 | http://martintrojer.github.io/scala/2013/06/06/scheme-in-scala/ 13 | 14 | ## Usage 15 | 16 | ``` 17 | $ sbt run 18 | [info] Running mtscheme.repl 19 | mtscheme v0.1 20 | null 21 | > (+ 1 2 3) 22 | 6 23 | > (define (map f l) (if (not (null? l)) (cons (f (car l)) (map f (cdr l))))) 24 | null 25 | > (map (lambda (x) (* x x)) (list 1 2 3)) 26 | (1, 4, 9, ) 27 | > 28 | > (define (foreach f l) (if (not (null? l)) (begin (f (car l)) (foreach f (cdr l))))) 29 | null 30 | > (foreach display (list 1 "foo" 2 "bar")) 31 | 1 32 | foo 33 | 2 34 | bar 35 | null 36 | > 37 | ``` 38 | 39 | ## Tests 40 | 41 | ```$ sbt test``` 42 | 43 | ## License 44 | 45 | GPLv3 46 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "Scheme Scala" 2 | 3 | version := "0.1" 4 | 5 | scalaVersion := "2.10.1" 6 | 7 | libraryDependencies += "org.scalatest" % "scalatest_2.10" % "1.9.1" % "test" 8 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "Sonatype snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/" 2 | 3 | addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.4.0") 4 | -------------------------------------------------------------------------------- /src/main/scala/mtscheme/BuiltIn.scala: -------------------------------------------------------------------------------- 1 | package mtscheme 2 | 3 | import mtscheme.Interpreter._ 4 | 5 | object BuiltIn { 6 | 7 | def aritFun(op: ((BigDecimal, BigDecimal) => BigDecimal)) 8 | (env: Env, comb: List[ExprT]) = { 9 | def error = throw new IllegalArgumentException("arithmetic error") 10 | comb.map(e => eval(env, e)._2) match { 11 | case Value(Num(first)) :: t => 12 | val res = t.foldLeft(first)((acc, e) => 13 | e match { case Value(Num(v)) => op(acc,v); case _ => error }) 14 | (env,Value(Num(res))) 15 | case _ => error 16 | } 17 | } 18 | 19 | def combFun(op: ((BigDecimal, BigDecimal) => Boolean)) 20 | (env: Env, comb: List[ExprT]) = { 21 | def error = throw new IllegalArgumentException("comparison error") 22 | comb.map(e => eval(env, e)._2) match { 23 | case Value(Num(first)) :: t => 24 | val res = t.foldLeft((true, first))((acc, e) => e match { 25 | case Value(Num(v)) => (acc._1 && op(acc._2, v), v) 26 | case _ => error }) 27 | (env,Value(Bool(res._1))) 28 | case _ => error 29 | } 30 | } 31 | 32 | // build a list of LValues from a Comb of Symbols 33 | def buildList(comb: List[ExprT]): List[ValueT] = comb match { 34 | case List() => List() 35 | case Symbol(n) :: t => Name(n) :: buildList(t) 36 | case Value(v) :: t => v :: buildList(t) 37 | case _ => throw new IllegalArgumentException("define args") 38 | } 39 | 40 | def listToString(ls: List[ExprT]) = { 41 | def ltos(ls: List[ExprT]): String = ls match { 42 | case List() => "" 43 | case Value(Num(v)) :: t => v.toString + ", " + ltos(t) 44 | case Value(Bool(v)) :: t => v.toString + ", " + ltos(t) 45 | case Value(Name(v)) :: t => v.toString + ", " + ltos(t) 46 | case EList(l) :: t => "(" + ltos(l) + "), " + ltos(t) 47 | case _ :: t => ltos(t) 48 | } 49 | "(" + ltos(ls) + ")" 50 | } 51 | 52 | // ------------------------------------------- 53 | 54 | def _not(env: Env, comb: List[ExprT]) = comb match { 55 | case expr :: Nil => eval(env, expr) match { 56 | case (_, Value(Bool(v))) => (env, Value(Bool(!v))) 57 | case _ => throw new IllegalArgumentException("not") 58 | } 59 | case _ => throw new IllegalArgumentException("not") 60 | } 61 | 62 | def _if(env: Env, comb: List[ExprT]) = { 63 | def error = throw new IllegalArgumentException("if") 64 | val (condExpr, posExpr, negExpr) = comb match { 65 | case condExpr :: posExpr :: negExpr :: Nil => 66 | (condExpr, posExpr, Some(negExpr)) 67 | case condExpr :: posExpr :: Nil => 68 | (condExpr, posExpr, None) 69 | case _ => error 70 | } 71 | (eval(env, condExpr))._2 match { 72 | case Value(Bool(c)) => 73 | if (c) 74 | eval(env, posExpr) 75 | else negExpr match { 76 | case Some(e) => eval(env, e) 77 | case None => (env, NullExpr()) 78 | } 79 | case _ => error 80 | } 81 | } 82 | 83 | def _cond(env: Env, comb: List[ExprT]) = { 84 | def error = throw new IllegalArgumentException("cond") 85 | 86 | def doExpr(comb: List[ExprT]) = comb match { 87 | case Symbol("else") :: posExpr :: Nil => 88 | Some(eval(env, posExpr)._2) 89 | case condExpr :: posExpr :: Nil => eval(env, condExpr)._2 match { 90 | case Value(Bool(true)) => Some(eval(env, posExpr)._2) 91 | case Value(Bool(false)) => None 92 | case _ => error 93 | } 94 | case _ => error 95 | } 96 | 97 | def runExprs(comb: List[ExprT]): ExprT = comb match { 98 | case Comb(c) :: rest => doExpr(c) match { 99 | case Some(e) => e 100 | case None => runExprs(rest) 101 | } 102 | case _ => NullExpr() 103 | } 104 | 105 | (env, runExprs(comb)) 106 | } 107 | 108 | def _define(env: Env, comb: List[ExprT]) = { 109 | def error = throw new IllegalArgumentException("define") 110 | def getStr(expr: ExprT) = expr match { 111 | case Symbol(n) => n 112 | case Value(Name(n)) => n 113 | case _ => error 114 | } 115 | comb match { 116 | // variable definition (lambda 'values' fall into this category) 117 | case Symbol(n) :: expr :: Nil => { 118 | val (nenv, res) = eval(env, expr) 119 | (nenv.addEntry(n -> res), NullExpr()) 120 | } 121 | // function definition 122 | case Comb(ns) :: body => { 123 | val fname = getStr(ns.head) 124 | val args = buildList(ns.tail) 125 | (env.addEntry(fname -> Func(args, body)), NullExpr()) 126 | } 127 | case _ => error 128 | } 129 | } 130 | 131 | def _cons(env: Env, comb: List[ExprT]) = comb match { 132 | case e1 :: e2 :: Nil => 133 | (eval(env, e1)._2, eval(env, e2)._2) match { 134 | case (expr1, NullExpr()) => 135 | (env, EList(List(expr1))) 136 | case (expr1, EList(l2)) => 137 | (env, EList(expr1 :: l2)) 138 | case (expr1, expr2) => 139 | (env, EList(List(expr1, expr2))) 140 | } 141 | case _ => throw new IllegalArgumentException("cons") 142 | } 143 | 144 | def _list(env: Env, comb: List[ExprT]) = { 145 | def doExpr(comb: List[ExprT]): List[ExprT] = comb match { 146 | case List() => Nil 147 | case h :: t => eval(env, h)._2 :: doExpr(t) 148 | } 149 | (env, EList(doExpr(comb))) 150 | } 151 | 152 | def _append(env: Env, comb: List[ExprT]) = { 153 | def doExpr(comb: List[ExprT]): List[ExprT] = comb match { 154 | case List() => Nil 155 | case h :: t => eval(env, h)._2 match { 156 | case EList(l) => l ::: doExpr(t) 157 | case expr => expr :: doExpr(t) 158 | } 159 | } 160 | (env, EList(doExpr(comb))) 161 | } 162 | 163 | def _car(env: Env, comb: List[ExprT]) = comb match { 164 | case h :: Nil => eval(env, h)._2 match { 165 | case EList(l) => (env, l.head) 166 | case _ => throw new IllegalArgumentException("car") 167 | } 168 | case _ => throw new IllegalArgumentException("car") 169 | } 170 | 171 | def _cdr(env: Env, comb: List[ExprT]) = comb match { 172 | case h :: Nil => eval(env, h)._2 match { 173 | case EList(List()) => (env, EList(List())) 174 | case EList(l) => (env, EList(l.tail)) 175 | case _ => throw new IllegalArgumentException("car") 176 | } 177 | case _ => throw new IllegalArgumentException("car") 178 | } 179 | 180 | def _null(env: Env, comb: List[ExprT]) = comb match { 181 | case h :: Nil => eval(env, h)._2 match { 182 | case EList(List()) => (env, Value(Bool(true))) 183 | case _ => (env, Value(Bool(false))) 184 | } 185 | case _ => throw new IllegalArgumentException("null?") 186 | } 187 | 188 | def _let(env: Env, comb: List[ExprT]) = { 189 | def error = throw new IllegalArgumentException("let") 190 | def doBind(acc: Env, binds: List[ExprT]): Env = binds match { 191 | case List() => 192 | acc 193 | case Comb(c) :: t => c match { 194 | case Symbol(v) :: expr :: Nil => 195 | doBind(acc.addEntry(v -> eval(env, expr)._2), t) 196 | case _ => error 197 | } 198 | case _ => error 199 | } 200 | comb match { 201 | case Comb(binds) :: body :: Nil => 202 | val newEnv = doBind(env.expand(), binds) 203 | eval(newEnv, body) 204 | case _ => error 205 | } 206 | } 207 | 208 | def _begin(env: Env, comb: List[ExprT]) = evalAll(env, comb) 209 | 210 | def _lambda(env: Env, comb: List[ExprT]) = comb match { 211 | case Comb(args) :: body => (env, Func(buildList(args), body)) 212 | case _ => throw new IllegalArgumentException("lambda") 213 | } 214 | 215 | def _display(env: Env, comb: List[ExprT]) = { 216 | comb match { 217 | case expr :: Nil => (eval(env, expr)._2) match { 218 | case Value(Num(v)) => println(v) 219 | case Value(Name(v)) => println(v) 220 | case Value(Bool(v)) => println(v) 221 | case EList(l) => println(listToString(l)) 222 | case _ => throw new IllegalArgumentException("display") 223 | } 224 | case _ => throw new IllegalArgumentException("display") 225 | } 226 | (env, NullExpr()) 227 | } 228 | 229 | def _newline(env: Env, comb: List[ExprT]) = { 230 | comb match { 231 | case List() => println() 232 | case _ => throw new IllegalArgumentException("newline") 233 | } 234 | (env, NullExpr()) 235 | } 236 | 237 | // ------------------------------------------- 238 | 239 | val globalEnv = Env(EnvT(EnvMapT( 240 | ("+" -> Proc(aritFun(_+_) _)), 241 | ("-" -> Proc(aritFun(_-_) _)), 242 | ("*" -> Proc(aritFun(_*_) _)), 243 | ("/" -> Proc(aritFun(_/_) _)), 244 | 245 | ("=" -> Proc(combFun(_==_) _)), 246 | (">" -> Proc(combFun(_>_) _)), 247 | ("<" -> Proc(combFun(_<_) _)), 248 | (">=" -> Proc(combFun(_>=_) _)), 249 | ("<=" -> Proc(combFun(_<=_) _)), 250 | 251 | ("not" -> Proc(_not)), 252 | ("if" -> Proc(_if)), 253 | ("cond" -> Proc(_cond)), 254 | ("define" -> Proc(_define)), 255 | ("cons" -> Proc(_cons)), 256 | ("list" -> Proc(_list)), 257 | ("append" -> Proc(_append)), 258 | ("car" -> Proc(_car)), 259 | ("cdr" -> Proc(_cdr)), 260 | ("null?" -> Proc(_null)), 261 | ("let" -> Proc(_let)), 262 | ("begin" -> Proc(_begin)), 263 | ("lambda" -> Proc(_lambda)), 264 | ("display" -> Proc(_display)), 265 | ("newline" -> Proc(_newline)), 266 | 267 | ("true" -> Value(Bool(true))), 268 | ("false" -> Value(Bool(false))) 269 | 270 | ))) 271 | } 272 | -------------------------------------------------------------------------------- /src/main/scala/mtscheme/Env.scala: -------------------------------------------------------------------------------- 1 | package mtscheme 2 | 3 | case class Env(val env: EnvT) { 4 | 5 | def addEntry(entry: (String, ExprT)): Env = env match { 6 | case h::t => Env((h + entry) :: t) 7 | case List() => throw new IllegalArgumentException 8 | } 9 | 10 | def expand(): Env = Env(EnvMapT() :: env) 11 | 12 | def lookUp(s: String): Option[ExprT] = 13 | env find (_ contains s) map (_(s)) 14 | 15 | override def equals(that: Any) = that match { 16 | case Env(thatEnv) => env == thatEnv 17 | case _ => false 18 | } 19 | } 20 | 21 | object Env { 22 | def apply() = new Env(EnvT()) 23 | } -------------------------------------------------------------------------------- /src/main/scala/mtscheme/HandRolledParser.scala: -------------------------------------------------------------------------------- 1 | package mtscheme 2 | 3 | // ---------------------------------------- 4 | // Case classes 5 | 6 | sealed trait Token 7 | case class TOpen() extends Token 8 | case class TClose() extends Token 9 | case class TNumber(v: String) extends Token 10 | case class TString(v: String) extends Token 11 | case class TSymbol(v: String) extends Token 12 | 13 | // ---------------------------------------- 14 | 15 | object HandRolledParser { 16 | 17 | def tokenize(source: List[Char]) = { 18 | 19 | def string(acc: String, cs: List[Char]): (String, List[Char]) = cs match { 20 | case '\\' :: '"' :: t => string(acc + "\"", t) // escaped quote becomes quote 21 | case '"' :: t => (acc, t) // closing quote terminates 22 | case c :: t => string(acc + c, t) // otherwise accumulate chars 23 | case _ => throw new IllegalArgumentException("malformed string") 24 | } 25 | 26 | def token(acc: String, cs: List[Char]): (String, List[Char]) = cs match { 27 | case t @ ')' :: _ => (acc, t) // closing paren terminates 28 | case w :: t if w.isWhitespace => (acc, t) // whitespace terminates 29 | case List() => (acc, List()) // end of list terminates 30 | case c :: t => token(acc + c, t) // otherwise accumulate chars 31 | } 32 | 33 | def doTok(acc: List[Token], cs: List[Char]): List[Token] = cs match { 34 | case w :: t if w.isWhitespace => doTok(acc, t) // skip whitespace 35 | case '(' :: t => doTok(TOpen() :: acc, t) 36 | case ')' :: t => doTok(TClose() :: acc, t) 37 | case '"' :: t => { // start of string 38 | val (s, rst) = string("", t) 39 | doTok(TString(s) :: acc, rst) 40 | } 41 | case '-' :: d :: t if d.isDigit => { // start of neg number 42 | val (n, rst) = token("-" + d, t) 43 | doTok(TNumber(n) :: acc, rst) 44 | } 45 | case '+' :: d :: t if d.isDigit => { // start of positive number 46 | val (n, rst) = token(d.toString, t) 47 | doTok(TNumber(n) :: acc, rst) 48 | } 49 | // TODO; how to remove this duplication? (extractors?) 50 | case d :: t if d.isDigit => { 51 | val (n, rst) = token(d.toString, t) 52 | doTok(TNumber(n) :: acc, rst) 53 | } 54 | case s :: t => { // otherwise start of symbol 55 | val (ts, rst) = token(s.toString, t) 56 | doTok(TSymbol(ts) :: acc, rst) 57 | } 58 | case List() => acc.reverse // terminate 59 | } 60 | 61 | doTok(List(), source) 62 | } 63 | 64 | def parse(source: String) = { 65 | def mapTok(tok: Token) = tok match { 66 | case TNumber(n) => Value(Num(BigDecimal(n))) 67 | case TString(s) => Value(Name(s)) 68 | case TSymbol(s) => Symbol(s) 69 | case _ => throw new IllegalArgumentException("syntax error") 70 | } 71 | def doParse(acc: List[ExprT], toks: List[Token]): (List[ExprT], List[Token]) = toks match { 72 | case TOpen() :: t => { 73 | val (e, rst) = doParse(List(), t) 74 | doParse(Comb(e) :: acc, rst) 75 | } 76 | case TClose() :: t => (acc.reverse, t) 77 | case h :: t => doParse(mapTok(h) :: acc, t) 78 | case List() => (acc.reverse, List()) 79 | } 80 | 81 | val (res, _) = doParse(List(), (tokenize(source.toList))) 82 | res 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/scala/mtscheme/Interpreter.scala: -------------------------------------------------------------------------------- 1 | package mtscheme 2 | 3 | // ---------------------------------------- 4 | // Case classes 5 | 6 | sealed trait ValueT 7 | case class Num(v: BigDecimal) extends ValueT 8 | case class Bool(v: Boolean) extends ValueT 9 | case class Name(v: String) extends ValueT 10 | 11 | sealed trait ExprT 12 | case class NullExpr() extends ExprT 13 | case class Comb(v: List[ExprT]) extends ExprT 14 | case class EList(v: List[ExprT]) extends ExprT 15 | case class Func(args: List[ValueT], body: List[ExprT]) extends ExprT 16 | case class Proc(f: ((Env, List[ExprT]) => (Env, ExprT))) extends ExprT 17 | case class Symbol(v: String) extends ExprT 18 | case class Value(v: ValueT) extends ExprT 19 | 20 | // ---------------------------------------- 21 | 22 | object Interpreter { 23 | 24 | def eval(env: Env, expr: ExprT): (Env, ExprT) = expr match { 25 | case NullExpr() => throw new IllegalStateException("invalid interpreter state") 26 | case Comb(List()) => throw new IllegalStateException("invalid combination") 27 | case Comb(h :: t) => 28 | eval(env, h) match { 29 | case (_, Proc(f)) => apply(f, t, env) 30 | case (nEnv, Func(args, body)) => { 31 | if (args.length != t.length) throw new IllegalArgumentException("invalid number of arguments") 32 | val newEnv = (args zip t).foldLeft(nEnv.expand())((acc, av) => bindArg(acc, av._1, av._2)) 33 | evalAll(newEnv, body) 34 | } 35 | case (nEnv, expr) => (nEnv, expr) 36 | } 37 | case Proc(f) => (env, Proc(f)) 38 | case Func(args, body) => throw new IllegalArgumentException("invalid function call") 39 | case v @ Value(_) => (env, v) 40 | case l @ List(_) => (env, l) 41 | case Symbol(s) => 42 | env.lookUp(s) match { 43 | case Some(e) => (env, e) 44 | case None => throw new IllegalArgumentException("unbound symbol '" + s +"'") 45 | } 46 | } 47 | 48 | private def apply(f: ((Env, List[ExprT]) => (Env, ExprT)), 49 | args: List[ExprT], env: Env) = 50 | f(env, args) 51 | 52 | // bind argument in a new environment 53 | private def bindArg(env: Env, arg: ValueT, expr: ExprT) = arg match { 54 | case Name(n) => env.addEntry(n -> eval(env, expr)._2) 55 | case _ => throw new IllegalArgumentException 56 | } 57 | 58 | // Eval a combination (a list of expressions), return the value of the last one 59 | def evalAll(env: Env, comb: List[ExprT]): (Env, ExprT) = comb match { 60 | case List() => (env, NullExpr()) 61 | case h :: t => { 62 | val (nEnv, res) = eval(env, h) 63 | t.length match { 64 | case 0 => (env, res) 65 | case 1 => eval(nEnv, t.head) 66 | case _ => evalAll(nEnv, t) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/scala/mtscheme/Parser.scala: -------------------------------------------------------------------------------- 1 | package mtscheme 2 | 3 | import scala.util.parsing.combinator._ 4 | 5 | object Parser extends JavaTokenParsers { 6 | 7 | val value: Parser[ValueT] = stringLiteral ^^ (x => Name(x.tail.init)) | 8 | floatingPointNumber ^^ (x => Num(BigDecimal(x))) 9 | 10 | val expression: Parser[ExprT] = value ^^ (x => Value(x)) | 11 | """[^()\s]+""".r ^^ (x => Symbol(x)) | 12 | combination 13 | 14 | val combination: Parser[Comb] = "(" ~> rep(expression) <~ ")" ^^ (x => Comb(x)) 15 | 16 | val program: Parser[List[ExprT]] = rep(expression) 17 | 18 | // --- 19 | 20 | def parse(source: String) = parseAll(program, source).get 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/mtscheme/package.scala: -------------------------------------------------------------------------------- 1 | package object mtscheme { 2 | 3 | // Is this kosher? 4 | 5 | type EnvT = List[EnvMapT] 6 | type EnvMapT = Map[String,ExprT] 7 | 8 | def EnvT() = List(EnvMapT()) 9 | def EnvT(xs: EnvMapT*) = List(xs:_*) 10 | def EnvMapT(xs: (String, ExprT)*) = Map(xs:_*) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/mtscheme/repl.scala: -------------------------------------------------------------------------------- 1 | package mtscheme 2 | 3 | import mtscheme.BuiltIn._ 4 | import mtscheme.Interpreter.eval 5 | import mtscheme.Parser.parse 6 | 7 | object repl extends App { 8 | 9 | def commandLoop(env: Env, res: ExprT): Unit = { 10 | res match { 11 | case Value(Num(v)) => println(v) 12 | case Value(Name(v)) => println(v) 13 | case Value(Bool(v)) => println(v) 14 | case EList(l) => println(listToString(l)) 15 | case _ => println("null") 16 | } 17 | try { 18 | print("> ") 19 | val line = readLine() 20 | (commandLoop _).tupled(eval(env, parse(line).head)) 21 | } catch { 22 | case e: Exception => commandLoop(env, Value(Name("Error; " + e.getMessage))) 23 | } 24 | } 25 | 26 | val greet = "(display \"mtscheme v0.1 \")" 27 | val (env, res) = eval(globalEnv, parse(greet).head) 28 | commandLoop(env, res) 29 | } 30 | -------------------------------------------------------------------------------- /src/test/scala/mtscheme/EnvTest.scala: -------------------------------------------------------------------------------- 1 | package mtscheme 2 | 3 | import org.scalatest.FunSuite 4 | 5 | class EnvTest extends FunSuite { 6 | 7 | val entry = ("foo" -> Value(Num(1))) 8 | val testEnv = Env().addEntry(entry) 9 | 10 | test("addEntry") { 11 | val resEnv = Env(EnvT(EnvMapT(entry))) 12 | expectResult(resEnv) { Env().addEntry(entry) } 13 | } 14 | 15 | test("simple lookup") { 16 | expectResult(Some(Value(Num(1)))) { testEnv lookUp "foo" } 17 | } 18 | 19 | test("nested lookup") { 20 | val env = testEnv.expand() 21 | expectResult(Some(Value(Num(1)))) { env lookUp "foo" } 22 | } 23 | 24 | test("shadowed nested lookup") { 25 | val env = testEnv.expand().addEntry("foo"->Value(Num(2))) 26 | expectResult(Some(Value(Num(2)))) { env lookUp "foo" } 27 | } 28 | 29 | test("failing lookup") { 30 | expectResult(None) { testEnv lookUp "bar" } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/scala/mtscheme/InterpreterTest.scala: -------------------------------------------------------------------------------- 1 | package mtscheme 2 | 3 | import org.scalatest.FunSuite 4 | import mtscheme.Interpreter._ 5 | import mtscheme.Parser._ 6 | import mtscheme.BuiltIn._ 7 | 8 | class InterpreterTest extends FunSuite { 9 | 10 | // TODO; extractors to remove duplication? 11 | 12 | def getNumResult(env: Env, expr: ExprT) = eval(env, expr)._2 match { 13 | case Value(Num(n)) => n 14 | case _ => throw new IllegalArgumentException("num expression failure") 15 | } 16 | 17 | def getBoolResult(env: Env, expr: ExprT) = eval(env, expr)._2 match { 18 | case Value(Bool(n)) => n 19 | case _ => throw new IllegalArgumentException("bool expression failure") 20 | } 21 | 22 | def getEListResult(env: Env, expr: ExprT) = eval(env, expr)._2 match { 23 | case EList(l) => l 24 | case _ => throw new IllegalArgumentException("list expression failure") 25 | } 26 | 27 | def getEnv(env: Env, exprS: String) = eval(env, parse(exprS).head)._1 28 | 29 | def testNumber(env: Env, exprS: String, correct: BigDecimal) = { 30 | expectResult(correct) { getNumResult(env, parse(exprS).head) } 31 | } 32 | 33 | def testBool(env: Env, exprS: String, correct: Boolean) = { 34 | expectResult(correct) { getBoolResult(env, parse(exprS).head) } 35 | } 36 | 37 | def testNil(env: Env, exprS: String) = { 38 | expectResult(NullExpr()) { eval(env, parse(exprS).head)._2 } 39 | } 40 | 41 | def testEList(env: Env, exprS: String, correct: List[ExprT]) = { 42 | expectResult(correct) { getEListResult(env, parse(exprS).head) } 43 | } 44 | 45 | def testNumberG = (testNumber _).curried(globalEnv) 46 | def testBoolG = (testBool _).curried(globalEnv) 47 | def testEListG = (testEList _).curried(globalEnv) 48 | def testNilG = (testNil _).curried(globalEnv) 49 | 50 | // ------------------------------------------ 51 | 52 | test("add") { 53 | testNumberG("(+ 1 2)") (1+2) 54 | testNumberG("(+ 1 (+ 2 3))") (1+2+3) 55 | testNumberG("(+ 1)") (1) 56 | testNumberG("(+ 1 1 1 0.14)") (1+1+1+0.14) 57 | } 58 | 59 | test("sub") { 60 | testNumberG("(- 1 2)") (1-2) 61 | testNumberG("(- 1 (- 2 3))") (1-(2-3)) 62 | // testNumberG("(- 1)") (-1) 63 | testNumberG("(- 1 1 1)") (1-1-1) 64 | } 65 | 66 | test("mul") { 67 | testNumberG("(* 2 3.14)") (2.0*3.14) 68 | testNumberG("(+ 1 (* 2 3))") (1+2*3) 69 | testNumberG("(* 1)") (1) 70 | testNumberG("(* 2 1 2 2)") (2*1*2*2) 71 | } 72 | 73 | test("div") { 74 | testNumberG("(/ 9 3)") (9/3) 75 | testNumberG("(+ 1 (/ 2 3))") (1.0+BigDecimal(2.0)/3.0) 76 | testNumberG("(/ 1)") (1) 77 | // testNumberG("(/ 2)") (1.0/2.0) // TODO; special case not handled correctly 78 | testNumberG("(/ 1 2 3)") (1.0/BigDecimal(2.0)/3.0) 79 | } 80 | 81 | test("eq") { 82 | testBoolG("(= 2 2)") (2==2) 83 | testBoolG("(= 2 (+ 1 1))") (2==(1+1)) 84 | testBoolG("(= 1)") (true) 85 | testBoolG("(= 1 1 2)") (false) 86 | testBoolG("(= 1 1 (+ 1 1) 1)")(false) 87 | } 88 | 89 | test("gt") { 90 | testBoolG("(> 2 2)") (2>2) 91 | testBoolG("(> 1 2)") (1>2) 92 | testBoolG("(> 2 1)") (2>1) 93 | testBoolG("(> (+ 1 1 1) 2)") ((1+1+1)>2) 94 | testBoolG("(> 1)") (true) 95 | testBoolG("(> 1 1 (+ 1 1) 1)")(false) 96 | } 97 | 98 | test("lt") { 99 | testBoolG("(< 2 2)") (2<2) 100 | testBoolG("(< 1 2)") (1<2) 101 | testBoolG("(< 2 1)") (2<1) 102 | testBoolG("(< (+ 1 1 1) 2)") ((1+1+1)<2) 103 | testBoolG("(< 1)") (true) 104 | testBoolG("(< 1 1 (+ 1 1) 1)")(false) 105 | } 106 | 107 | test("ge") { 108 | testBoolG("(>= 2 2)") (2>=2) 109 | testBoolG("(>= 1 2)") (1>=2) 110 | testBoolG("(>= 2 1)") (2>=1) 111 | testBoolG("(>= (+ 1 1 1) 2)") ((1+1+1)>=2) 112 | testBoolG("(>= 1)") (true) 113 | testBoolG("(>= 1 1 (+ 1 1) 1)")(false) 114 | } 115 | 116 | test("le") { 117 | testBoolG("(<= 2 2)") (2<=2) 118 | testBoolG("(<= 1 2)") (1<=2) 119 | testBoolG("(<= 2 1)") (2<=1) 120 | testBoolG("(<= (+ 1 1 1) 2)") ((1+1+1)<=2) 121 | testBoolG("(<= 1)") (true) 122 | testBoolG("(<= 1 1 (+ 1 1) 1)")(false) 123 | } 124 | 125 | test("not") { 126 | testBoolG("(not (= 1 1))") (false) 127 | testBoolG("(not (not (= 1 1)))")(true) 128 | } 129 | 130 | test("if") { 131 | testNumberG("(if (< 2 1) 10 11)") (11) 132 | testNumberG("(if (< (+ 1 1 1) 1) 11 (* 2 5))") (10) 133 | testNumberG("(if true 1)") (1) 134 | testNilG("(if false 1)") 135 | } 136 | 137 | test("cond") { 138 | testNumberG("(cond (true 1) ((= 1 2) 2))") (1) 139 | testNumberG("(cond ((= 1 2) 1) (true 2))") (2) 140 | testNumberG("(cond (false 1) (false 2) (else 3))") (3) 141 | testNilG("(cond (false 1) (false 2))") 142 | } 143 | 144 | test("define") { 145 | expectResult(Some(Value(Num(4)))) { getEnv(globalEnv, "(define lisa 4)").lookUp("lisa") } 146 | expectResult(Some(Value(Num(3)))) { getEnv(globalEnv, "(define nisse (+ 1 1 1))").lookUp("nisse") } 147 | } 148 | 149 | test("cons") { 150 | testEListG("(cons 1 2)") (List (Value(Num(1)), Value(Num(2)))) 151 | testEListG("(cons 1 (cons 2 3))") (List (Value(Num(1)), Value(Num(2)), Value(Num(3)))) 152 | testEListG("(cons 1 (cons 2 (cons 3 4)))") (List (Value(Num(1)), Value(Num(2)), Value(Num(3)), 153 | Value(Num(4)))) 154 | testEListG("(cons (cons 1 2) 3)") (List (EList(List(Value(Num(1)), Value(Num(2)))), 155 | Value(Num(3)))) 156 | testEListG("(cons \"kalle\" 2)") (List (Value(Name("kalle")), Value(Num(2)))) 157 | } 158 | 159 | test("list") { 160 | testEListG("(list 1 2)") (List(Value(Num(1)), Value(Num(2)))) 161 | testEListG("(list 5 (list 1 1) 2)") (List(Value(Num(5)), EList(List(Value(Num(1)), Value(Num(1)))), Value(Num(2)))) 162 | testEListG("(list 1 \"kalle\")") (List(Value(Num(1)), Value(Name("kalle")))) 163 | testEListG("(list)") (List()) 164 | } 165 | 166 | test("append") { 167 | testEListG("(append (list 1 2))") (List(Value(Num(1)), Value(Num(2)))) 168 | testEListG("(append (list 1 2) (list 3 4))") (List(Value(Num(1)), Value(Num(2)), Value(Num(3)), Value(Num(4)))) 169 | testEListG("(append 1 (list 2 (list 3)))") (List(Value(Num(1)), Value(Num(2)), EList(List(Value(Num(3)))))) 170 | } 171 | 172 | test("car") { 173 | testNumberG("(car (list 1 2))") (List(1,2).head) 174 | testEListG("(car (list (list 1) 2))") (List(Value(Num(1)))) 175 | } 176 | 177 | test("cdr") { 178 | testEListG("(cdr (list 1))") (List()) 179 | testEListG("(cdr (list 1 2))") (List(Value(Num(2)))) 180 | testEListG("(cdr (list 1 (list 2 3)))") (List(EList(List(Value(Num(2)), Value(Num(3)))))) 181 | testEListG("(cdr (list (list 1)))") (List()) 182 | } 183 | 184 | test("null?") { 185 | testBoolG("(null? (list 1))") (List(1).isEmpty) 186 | testBoolG("(null? (cdr (list 1)))") (List(1).tail.isEmpty) 187 | testBoolG("(null? (cdr (cdr (list 1))))") (true) 188 | testBoolG("(null? (list))") (List().isEmpty) 189 | } 190 | 191 | test("let") { 192 | testNumberG("(let ((a 1)) a)") (1) 193 | testNumberG("(let ((a 1)(b (+ 1 1))) (+ a b))") (3) 194 | } 195 | 196 | test("begin") { 197 | testNumberG("(begin 1 2)") (2) 198 | testNumberG("(begin (define x 2) x)") (2) 199 | } 200 | 201 | test("function") { 202 | val e1 = getEnv(globalEnv, "(define (add a b) (+ a b))") 203 | testNumber(e1, "(add 1 2)", 3) 204 | 205 | val e2 = getEnv(globalEnv, "(define (fact x) (if (= x 0) 1 (* x (fact (- x 1)))))") 206 | testNumber(e2, "(fact (+ 5 5))", 3628800) 207 | 208 | val e3 = getEnv(globalEnv, "(define (add a b) (begin (define (worker x y) (+ x y)) (worker a b)))") 209 | testNumber(e3, "(add 1 3)", 4) 210 | } 211 | 212 | test("lambda") { 213 | val e1 = getEnv(globalEnv, "(define (adder val) (lambda (x) (+ x val)))") 214 | val e2 = getEnv(e1, "(define add4 (adder 4))") 215 | testNumber(e2, "(add4 4)", 8) 216 | 217 | val e3 = getEnv(globalEnv, "(define (map f l) (if (not (null? l)) (cons (f (car l)) (map f (cdr l)))))") 218 | testEList(e3, "(map (lambda (x) (* x x)) (list 1 2 3))", 219 | List(Value(Num(1)), Value(Num(4)), Value(Num(9)))) 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /src/test/scala/mtscheme/ParserTest.scala: -------------------------------------------------------------------------------- 1 | package mtscheme 2 | 3 | import org.scalatest.FunSuite 4 | 5 | import mtscheme.Parser.parse 6 | import mtscheme.HandRolledParser._ 7 | 8 | class ParserTest extends FunSuite { 9 | 10 | // ---- 11 | 12 | test("Tokens Open/Close") { 13 | expectResult(List(TOpen(), TClose())) { 14 | tokenize("()".toList) 15 | } 16 | } 17 | 18 | test("Tokens Symbol") { 19 | expectResult(List(TSymbol("kalle"))) { 20 | tokenize("kalle".toList) 21 | } 22 | } 23 | 24 | test("Tokens String") { 25 | expectResult(List(TString("kalle"))) { 26 | tokenize("\"kalle\"".toList) 27 | } 28 | } 29 | 30 | test("Tokens Numbers") { 31 | expectResult(List(TNumber("42"), TNumber("42"), TNumber("-42"))) { 32 | tokenize("42 +42 -42".toList) 33 | } 34 | } 35 | 36 | test("Tokens Factional Number") { 37 | expectResult(List(TNumber("3.14"))) { tokenize("3.14".toList)} 38 | } 39 | 40 | // ---- 41 | 42 | test("Parse OpenClose") { 43 | val res = List(Comb(List())) 44 | expectResult(res) { HandRolledParser.parse("()") } 45 | expectResult(res) { Parser.parse("()") } 46 | } 47 | 48 | test("Parse Multiple") { 49 | val res = List(Value(Num(1.0)), Value(Num(2.0))) 50 | expectResult(res) { HandRolledParser.parse("1 2") } 51 | expectResult(res) { Parser.parse("1 2") } 52 | } 53 | 54 | test("Parse Expr") { 55 | val res = List(Comb(List(Symbol("+"), Value(Num(1.0)), Value(Num(2.0))))) 56 | expectResult(res) { HandRolledParser.parse("(+ 1 2") } 57 | expectResult(res) { Parser.parse("(+ 1 2)") } 58 | } 59 | } 60 | --------------------------------------------------------------------------------