├── .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 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/compiler.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 |
--------------------------------------------------------------------------------
/.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 |
5 |
6 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/scala_compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/scopes/scope_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------