Add an implementation written in Vala.
authorSimon Tatham <anakin@pobox.com>
Fri, 10 May 2019 08:34:20 +0000 (09:34 +0100)
committerSimon Tatham <anakin@pobox.com>
Fri, 10 May 2019 08:41:11 +0000 (09:41 +0100)
I needed to learn Vala in order to work on an existing program written
in it, and since Ben Harris had been enthusing about mal to me
recently, I thought this would be a good way to learn the language.

In other words, this is my first Vala project, so I can't promise it's
perfectly idiomatic by the standards of experienced Vala programmers!

Also, the style isn't all that consistent. For example, sometimes I
implement a function for every value type by adding a method to the
Mal.Val base class, and sometimes instead I do it by having a function
(e.g. pr_str) dynamically test all the possible subtypes in a chain of
else-ifs.

Those decisions were partly because of the source file structure
required by this project (pr_str is supposed to live in its own file);
partly because for my own learning goals I wanted to try both
strategies and find out how easy they both were; and mostly just
because of indecision and bad planning.

23 files changed:
Makefile
README.md
vala/.gitignore [new file with mode: 0644]
vala/Dockerfile [new file with mode: 0644]
vala/Makefile [new file with mode: 0644]
vala/README.md [new file with mode: 0644]
vala/core.vala [new file with mode: 0644]
vala/env.vala [new file with mode: 0644]
vala/printer.vala [new file with mode: 0644]
vala/reader.vala [new file with mode: 0644]
vala/run [new file with mode: 0755]
vala/step0_repl.vala [new file with mode: 0644]
vala/step1_read_print.vala [new file with mode: 0644]
vala/step2_eval.vala [new file with mode: 0644]
vala/step3_env.vala [new file with mode: 0644]
vala/step4_if_fn_do.vala [new file with mode: 0644]
vala/step5_tco.vala [new file with mode: 0644]
vala/step6_file.vala [new file with mode: 0644]
vala/step7_quote.vala [new file with mode: 0644]
vala/step8_macros.vala [new file with mode: 0644]
vala/step9_try.vala [new file with mode: 0644]
vala/stepA_mal.vala [new file with mode: 0644]
vala/types.vala [new file with mode: 0644]

index 5fc65dd..7d6975f 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -89,7 +89,7 @@ IMPLS = ada ada.2 awk bash basic c chuck clojure coffee common-lisp cpp crystal
        guile haskell haxe hy io java js julia kotlin livescript logo lua make mal \
        matlab miniMAL nasm nim objc objpascal ocaml perl perl6 php picolisp plpgsql \
        plsql powershell ps python r racket rexx rpython ruby rust scala scheme skew \
-       swift swift3 swift4 tcl ts vb vhdl vimscript wasm yorick
+       swift swift3 swift4 tcl ts vala vb vhdl vimscript wasm yorick
 
 EXTENSION = .mal
 
@@ -246,6 +246,7 @@ swift3_STEP_TO_PROG =  swift3/$($(1))
 swift4_STEP_TO_PROG =  swift4/$($(1))
 tcl_STEP_TO_PROG =     tcl/$($(1)).tcl
 ts_STEP_TO_PROG =      ts/$($(1)).js
+vala_STEP_TO_PROG =    vala/$($(1))
 vb_STEP_TO_PROG =      vb/$($(1)).exe
 vhdl_STEP_TO_PROG =    vhdl/$($(1))
 vimscript_STEP_TO_PROG = vimscript/$($(1)).vim
index 5fbe9fd..eaffc52 100644 (file)
--- a/README.md
+++ b/README.md
@@ -81,6 +81,7 @@
 | [Swift 4](#swift-4) | [陆遥](https://github.com/LispLY)  |
 | [Tcl](#tcl-86) | [Dov Murik](https://github.com/dubek) |
 | [TypeScript](#typescript) | [Masahiro Wakame](https://github.com/vvakame) |
+| [Vala](#vala) | [Simon Tatham](https://github.com/sgtatham) |
 | [VHDL](#vhdl) | [Dov Murik](https://github.com/dubek) |
 | [Vimscript](#vimscript) | [Dov Murik](https://github.com/dubek) |
 | [Visual Basic.NET](#visual-basicnet) | [Joel Martin](https://github.com/kanaka)  |
@@ -997,6 +998,18 @@ make
 node ./stepX_YYY.js
 ```
 
+### Vala
+
+The Vala implementation of mal has been tested with the Vala 0.40.8
+compiler. You will need to install `valac` and `libreadline-dev` or
+equivalent.
+
+```
+cd vala
+make
+./stepX_YYY
+```
+
 ### VHDL
 
 The VHDL implementation of mal has been tested with GHDL 0.29.
diff --git a/vala/.gitignore b/vala/.gitignore
new file mode 100644 (file)
index 0000000..71c67bd
--- /dev/null
@@ -0,0 +1,3 @@
+*.c
+*.h
+*.o
diff --git a/vala/Dockerfile b/vala/Dockerfile
new file mode 100644 (file)
index 0000000..61fae7a
--- /dev/null
@@ -0,0 +1,6 @@
+FROM ubuntu:18.04
+
+RUN apt-get -y update
+RUN apt-get -y install git python make valac libreadline-dev
+
+WORKDIR /mal
diff --git a/vala/Makefile b/vala/Makefile
new file mode 100644 (file)
index 0000000..3353c86
--- /dev/null
@@ -0,0 +1,30 @@
+PROGRAMS = step0_repl step1_read_print step2_eval step3_env step4_if_fn_do \
+           step5_tco step6_file step7_quote step8_macros step9_try stepA_mal
+AUX1 = types.vala reader.vala printer.vala
+AUX3 = $(AUX1) env.vala
+AUX4 = $(AUX3) core.vala
+
+all: $(PROGRAMS)
+
+$(PROGRAMS): %: %.vala
+       valac -o $@ $^ $(DEFINES) --pkg readline -X -lreadline
+
+step1_read_print step2_eval: override DEFINES += -D NO_ENV
+
+step0_repl:
+step1_read_print: $(AUX1)
+step2_eval: $(AUX1)
+step3_env: $(AUX3)
+step4_if_fn_do: $(AUX4)
+step5_tco: $(AUX4)
+step6_file: $(AUX4)
+step7_quote: $(AUX4)
+step8_macros: $(AUX4)
+step9_try: $(AUX4)
+stepA_mal: $(AUX4)
+
+clean: clean-c
+       rm -f $(PROGRAMS)
+
+clean-c:
+       rm -f *.c *.h
diff --git a/vala/README.md b/vala/README.md
new file mode 100644 (file)
index 0000000..5cec39d
--- /dev/null
@@ -0,0 +1,50 @@
+# Vala implementation
+
+Notes on building:
+
+* With the Debian or Ubuntu packages `valac` and `libreadline-dev`
+  installed, and GNU make, you should be able to build using the
+  provided Makefile.
+
+* The build will not be warning-clean, because the shared modules like
+  `types.vala` and `core.vala` are shared between all the `stepN` main
+  programs, and not all the steps use all the functions in the shared
+  modules, and the Vala compiler has no way to turn off the warning
+  about unused pieces of source code.
+
+* The Vala compiler works by translating the program to C and then
+  compiling that. The C compilation stage can sometimes encounter an
+  error, in which case the compiler will leave `.c` source files in
+  the working directory. If that happens, you can run `make clean-c`
+  to get rid of them.
+
+Design notes on the implementation:
+
+* Vala has a reference counting system built in to the language.
+  Garbage collection of mal objects is delegated to that system. So
+  you can almost certainly contrive an un-GC-able cycle of objects by
+  using atoms, and I haven't done anything about that.
+
+* Vala has exceptions (which it calls 'error domains'), but they don't
+  let you store an arbitrary data type: every exception subclass you
+  make stores the same data, namely a string. So mal exceptions are
+  implemented by storing a mal value in a static variable, and then
+  throwing a particular Vala error whose semantics are 'check that
+  variable when you catch me'.
+
+* Vala's bare function pointers are hard to use, especially if you
+  want one to survive the scope it was created in. So all the core
+  functions are implemented as classes with a `call` method, which
+  leads to a lot of boilerplate.
+
+* To make `types.vala` work in step 2, when the `Env` type doesn't
+  exist yet, I had to use `#if` to condition out the parts of the code
+  that depend on that type.
+
+* Mutability of objects at the Vala level is a bit informal. A lot of
+  core functions construct a list by making an empty `Mal.List` and
+  then mutating the `GLib.List` contained in it. But once they've
+  finished and returned the `Mal.List` to their caller, that list is
+  never mutated again, which means it's safe for the copying operation
+  in `with-meta` to make a second `Mal.List` sharing the reference to
+  the same `GLib.List`.
diff --git a/vala/core.vala b/vala/core.vala
new file mode 100644 (file)
index 0000000..5487483
--- /dev/null
@@ -0,0 +1,1181 @@
+abstract class Mal.BuiltinFunctionDyadicArithmetic : Mal.BuiltinFunction {
+    public abstract int64 result(int64 a, int64 b);
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS("%s: expected two numbers", name());
+        Mal.Num a = args.vs.data as Mal.Num;
+        Mal.Num b = args.vs.next.data as Mal.Num;
+        if (a == null || b == null)
+            throw new Mal.Error.BAD_PARAMS("%s: expected two numbers", name());
+        return new Mal.Num(result(a.v, b.v));
+    }
+}
+
+class Mal.BuiltinFunctionAdd : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionAdd();
+    }
+    public override string name() { return "+"; }
+    public override int64 result(int64 a, int64 b) { return a+b; }
+}
+
+class Mal.BuiltinFunctionSub : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionSub();
+    }
+    public override string name() { return "-"; }
+    public override int64 result(int64 a, int64 b) { return a-b; }
+}
+
+class Mal.BuiltinFunctionMul : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionMul();
+    }
+    public override string name() { return "*"; }
+    public override int64 result(int64 a, int64 b) { return a*b; }
+}
+
+class Mal.BuiltinFunctionDiv : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionDiv();
+    }
+    public override string name() { return "/"; }
+    public override int64 result(int64 a, int64 b) { return a/b; }
+}
+
+class Mal.BuiltinFunctionPrStr : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionPrStr();
+    }
+    public override string name() { return "pr-str"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        string result = "";
+        string sep = "";
+        foreach (var value in args.vs) {
+            result += sep + pr_str(value, true);
+            sep = " ";
+        }
+        return new Mal.String(result);
+    }
+}
+
+class Mal.BuiltinFunctionStr : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionStr();
+    }
+    public override string name() { return "str"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        string result = "";
+        foreach (var value in args.vs) {
+            result += pr_str(value, false);
+        }
+        return new Mal.String(result);
+    }
+}
+
+class Mal.BuiltinFunctionPrn : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionPrn();
+    }
+    public override string name() { return "prn"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        string sep = "";
+        foreach (var value in args.vs) {
+            stdout.printf("%s%s", sep, pr_str(value, true));
+            sep = " ";
+        }
+        stdout.printf("\n");
+        return new Mal.Nil();
+    }
+}
+
+class Mal.BuiltinFunctionPrintln : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionPrintln();
+    }
+    public override string name() { return "println"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        string sep = "";
+        foreach (var value in args.vs) {
+            stdout.printf("%s%s", sep, pr_str(value, false));
+            sep = " ";
+        }
+        stdout.printf("\n");
+        return new Mal.Nil();
+    }
+}
+
+class Mal.BuiltinFunctionReadString : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionReadString();
+    }
+    public override string name() { return "read-string"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1 || !(args.vs.data is Mal.String))
+            throw new Mal.Error.BAD_PARAMS("%s: expected one string", name());
+        return Reader.read_str((args.vs.data as Mal.String).v);
+    }
+}
+
+class Mal.BuiltinFunctionSlurp : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionSlurp();
+    }
+    public override string name() { return "slurp"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1 || !(args.vs.data is Mal.String))
+            throw new Mal.Error.BAD_PARAMS("%s: expected one string", name());
+        string filename = (args.vs.data as Mal.String).v;
+        string contents;
+        try {
+            FileUtils.get_contents(filename, out contents);
+        } catch (FileError e) {
+            throw new Mal.Error.BAD_PARAMS("%s: unable to read '%s': %s",
+                                           name(), filename, e.message);
+        }
+        return new Mal.String(contents);
+    }
+}
+
+class Mal.BuiltinFunctionList : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionList();
+    }
+    public override string name() { return "list"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        return args;
+    }
+}
+
+class Mal.BuiltinFunctionListP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionListP();
+    }
+    public override string name() { return "list?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.List);
+    }
+}
+
+class Mal.BuiltinFunctionSequentialP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionSequentialP();
+    }
+    public override string name() { return "sequential?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.List ||
+                            args.vs.data is Mal.Vector);
+    }
+}
+
+class Mal.BuiltinFunctionNilP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionNilP();
+    }
+    public override string name() { return "nil?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.Nil);
+    }
+}
+
+class Mal.BuiltinFunctionTrueP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionTrueP();
+    }
+    public override string name() { return "true?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.Bool &&
+                            (args.vs.data as Mal.Bool).v);
+    }
+}
+
+class Mal.BuiltinFunctionFalseP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionFalseP();
+    }
+    public override string name() { return "false?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.Bool &&
+                            !(args.vs.data as Mal.Bool).v);
+    }
+}
+
+class Mal.BuiltinFunctionNumberP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionNumberP();
+    }
+    public override string name() { return "number?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.Num);
+    }
+}
+
+class Mal.BuiltinFunctionStringP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionStringP();
+    }
+    public override string name() { return "string?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.String);
+    }
+}
+
+class Mal.BuiltinFunctionSymbolP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionSymbolP();
+    }
+    public override string name() { return "symbol?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.Sym);
+    }
+}
+
+class Mal.BuiltinFunctionKeywordP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionKeywordP();
+    }
+    public override string name() { return "keyword?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.Keyword);
+    }
+}
+
+class Mal.BuiltinFunctionVector : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionVector();
+    }
+    public override string name() { return "vector"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        return new Mal.Vector.from_list(args.vs);
+    }
+}
+
+class Mal.BuiltinFunctionVectorP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionVectorP();
+    }
+    public override string name() { return "vector?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.Vector);
+    }
+}
+
+class Mal.BuiltinFunctionHashMap : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionHashMap();
+    }
+    public override string name() { return "hash-map"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        var map = new Mal.Hashmap();
+        for (var iter = args.iter(); iter.nonempty(); iter.step()) {
+            var key = iter.deref();
+            var value = iter.step().deref();
+            if (value == null)
+                throw new Mal.Error.BAD_PARAMS(
+                    "%s: expected an even number of arguments", name());
+            map.insert(key, value);
+        }
+        return map;
+    }
+}
+
+class Mal.BuiltinFunctionMapP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionMapP();
+    }
+    public override string name() { return "map?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.Hashmap);
+    }
+}
+
+class Mal.BuiltinFunctionEmptyP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionEmptyP();
+    }
+    public override string name() { return "empty?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        var list = args.vs.data as Mal.Listlike;
+        if (list == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a list-like argument", name());
+        return new Mal.Bool(list.iter().deref() == null);
+    }
+}
+
+class Mal.BuiltinFunctionFnP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionFnP();
+    }
+    public override string name() { return "fn?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        if (args.vs.data is Mal.BuiltinFunction)
+            return new Mal.Bool(true);
+        var fn = args.vs.data as Mal.Function;
+        return new Mal.Bool(fn != null && !fn.is_macro);
+    }
+}
+
+class Mal.BuiltinFunctionMacroP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionMacroP();
+    }
+    public override string name() { return "macro?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        var fn = args.vs.data as Mal.Function;
+        return new Mal.Bool(fn != null && fn.is_macro);
+    }
+}
+
+class Mal.BuiltinFunctionCount : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionCount();
+    }
+    public override string name() { return "count"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        if (args.vs.data is Mal.Nil)
+            return new Mal.Num(0);     // nil is treated like ()
+        if (args.vs.data is Mal.List)
+            return new Mal.Num((args.vs.data as Mal.List).vs.length());
+        if (args.vs.data is Mal.Vector)
+            return new Mal.Num((args.vs.data as Mal.Vector).vs.length);
+        throw new Mal.Error.BAD_PARAMS(
+            "%s: expected a list argument", name());
+    }
+}
+
+class Mal.BuiltinFunctionEQ : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionEQ();
+    }
+    public override string name() { return "="; }
+    private static bool eq(Mal.Val a, Mal.Val b) {
+        if (a is Mal.Nil && b is Mal.Nil)
+            return true;
+        if (a is Mal.Bool && b is Mal.Bool)
+            return (a as Mal.Bool).v == (b as Mal.Bool).v;
+        if (a is Mal.Sym && b is Mal.Sym)
+            return (a as Mal.Sym).v == (b as Mal.Sym).v;
+        if (a is Mal.Keyword && b is Mal.Keyword)
+            return (a as Mal.Keyword).v == (b as Mal.Keyword).v;
+        if (a is Mal.Num && b is Mal.Num)
+            return (a as Mal.Num).v == (b as Mal.Num).v;
+        if (a is Mal.String && b is Mal.String)
+            return (a as Mal.String).v == (b as Mal.String).v;
+        if (a is Mal.Listlike && b is Mal.Listlike) {
+            if (a is Mal.Nil || b is Mal.Nil)
+                return false;
+            var aiter = (a as Mal.Listlike).iter();
+            var biter = (b as Mal.Listlike).iter();
+            while (aiter.nonempty() || biter.nonempty()) {
+                if (aiter.empty() || biter.empty())
+                    return false;
+                if (!eq(aiter.deref(), biter.deref()))
+                    return false;
+                aiter.step();
+                biter.step();
+            }
+            return true;
+        }
+        if (a is Mal.Vector && b is Mal.Vector) {
+            var av = (a as Mal.Vector).vs;
+            var bv = (b as Mal.Vector).vs;
+            if (av.length != bv.length)
+                return false;
+            for (var i = 0; i < av.length; i++)
+                if (!eq(av[i], bv[i]))
+                    return false;
+            return true;
+        }
+        if (a is Mal.Hashmap && b is Mal.Hashmap) {
+            var ah = (a as Mal.Hashmap).vs;
+            var bh = (b as Mal.Hashmap).vs;
+            if (ah.length != bh.length)
+                return false;
+            foreach (var k in ah.get_keys()) {
+                var av = ah[k];
+                var bv = bh[k];
+                if (bv == null || !eq(av, bv))
+                    return false;
+            }
+            return true;
+        }
+        if (a is Mal.BuiltinFunction && b is Mal.BuiltinFunction) {
+            return ((a as Mal.BuiltinFunction).name() ==
+                    (b as Mal.BuiltinFunction).name());
+        }
+        if (a is Mal.Function && b is Mal.Function) {
+            var af = a as Mal.Function;
+            var bf = b as Mal.Function;
+            return (eq(af.parameters, bf.parameters) &&
+                    eq(af.body, bf.body));
+        }
+        return false;
+    }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected two arguments", name());
+        return new Mal.Bool(eq(args.vs.data, args.vs.next.data));
+    }
+}
+
+abstract class Mal.BuiltinFunctionNumberCmp : Mal.BuiltinFunction {
+    public abstract bool result(int64 a, int64 b);
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS("%s: expected two numbers", name());
+        Mal.Num a = args.vs.data as Mal.Num;
+        Mal.Num b = args.vs.next.data as Mal.Num;
+        if (a == null || b == null)
+            throw new Mal.Error.BAD_PARAMS("%s: expected two numbers", name());
+        return new Mal.Bool(result(a.v, b.v));
+    }
+}
+
+class Mal.BuiltinFunctionLT : Mal.BuiltinFunctionNumberCmp {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionLT();
+    }
+    public override string name() { return "<"; }
+    public override bool result(int64 a, int64 b) { return a<b; }
+}
+
+class Mal.BuiltinFunctionLE : Mal.BuiltinFunctionNumberCmp {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionLE();
+    }
+    public override string name() { return "<="; }
+    public override bool result(int64 a, int64 b) { return a<=b; }
+}
+
+class Mal.BuiltinFunctionGT : Mal.BuiltinFunctionNumberCmp {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionGT();
+    }
+    public override string name() { return ">"; }
+    public override bool result(int64 a, int64 b) { return a>b; }
+}
+
+class Mal.BuiltinFunctionGE : Mal.BuiltinFunctionNumberCmp {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionGE();
+    }
+    public override string name() { return ">="; }
+    public override bool result(int64 a, int64 b) { return a>=b; }
+}
+
+class Mal.BuiltinFunctionAtom : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionAtom();
+    }
+    public override string name() { return "atom"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Atom(args.vs.data);
+    }
+}
+
+class Mal.BuiltinFunctionAtomP : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionAtomP();
+    }
+    public override string name() { return "atom?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return new Mal.Bool(args.vs.data is Mal.Atom);
+    }
+}
+
+class Mal.BuiltinFunctionDeref : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionDeref();
+    }
+    public override string name() { return "deref"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        var atom = args.vs.data as Mal.Atom;
+        if (atom == null)
+            throw new Mal.Error.BAD_PARAMS("%s: expected an atom", name());
+        return atom.v;
+    }
+}
+
+class Mal.BuiltinFunctionReset : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionReset();
+    }
+    public override string name() { return "reset!"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected two arguments", name());
+        var atom = args.vs.data as Mal.Atom;
+        if (atom == null)
+            throw new Mal.Error.BAD_PARAMS("%s: expected an atom", name());
+        atom.v = args.vs.next.data;
+        return atom.v;
+    }
+}
+
+Mal.Val call_function(Mal.Val function, GLib.List<Mal.Val> args, string caller)
+throws Mal.Error {
+    var fnargs = new Mal.List(args);
+    if (function is Mal.BuiltinFunction) {
+        return (function as Mal.BuiltinFunction).call(fnargs);
+    } else if (function is Mal.Function) {
+        var fn = function as Mal.Function;
+        var env = new Mal.Env.funcall(fn.env, fn.parameters, fnargs);
+        return Mal.Main.EVAL(fn.body, env);
+    } else {
+        throw new Mal.Error.CANNOT_APPLY("%s: expected a function", caller);
+    }
+}
+
+class Mal.BuiltinFunctionSwap : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionSwap();
+    }
+    public override string name() { return "swap!"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() < 2)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected at least two arguments", name());
+        var atom = args.vs.data as Mal.Atom;
+        var function = args.vs.next.data;
+        var fnargs = args.vs.next.next.copy();
+        fnargs.prepend(atom.v);
+        atom.v = call_function(function, fnargs, name());
+        return atom.v;
+    }
+}
+
+class Mal.BuiltinFunctionCons : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionCons();
+    }
+    public override string name() { return "cons"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected two arguments", name());
+        var first = args.vs.data;
+        var rest = args.vs.next.data as Mal.Listlike;
+        if (rest == null) {
+            if (args.vs.next.data is Mal.Nil)
+                rest = new Mal.List.empty();
+            else
+                throw new Mal.Error.BAD_PARAMS("%s: expected a list", name());
+        }
+        var newlist = new Mal.List.empty();
+        newlist.vs.append(first);
+        for (var iter = rest.iter(); iter.nonempty(); iter.step())
+            newlist.vs.append(iter.deref());
+        return newlist;
+    }
+}
+
+class Mal.BuiltinFunctionConcat : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionConcat();
+    }
+    public override string name() { return "concat"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        var newlist = new GLib.List<Mal.Val>();
+        foreach (var listval in args.vs) {
+            if (listval is Mal.Nil)
+                continue;
+            var list = listval as Mal.Listlike;
+            if (list == null)
+                throw new Mal.Error.BAD_PARAMS("%s: expected a list", name());
+            for (var iter = list.iter(); iter.nonempty(); iter.step())
+                newlist.append(iter.deref());
+        }
+        return new Mal.List(newlist);
+    }
+}
+
+class Mal.BuiltinFunctionNth : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionNth();
+    }
+    public override string name() { return "nth"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected two arguments", name());
+        var list = args.vs.data as Mal.Listlike;
+        var index = args.vs.next.data as Mal.Num;
+        if (list == null || index == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a list and a number", name());
+        if (index.v < 0)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: negative list index", name());
+        Mal.Val? result = null;
+        if (list is Mal.Vector) {
+            var vec = list as Mal.Vector;
+            if (index.v < vec.vs.length)
+                result = vec.vs[index.v];
+        } else {
+            var iter = list.iter();
+            var i = index.v;
+            while (!iter.empty()) {
+                if (i == 0) {
+                    result = iter.deref();
+                    break;
+                }
+                iter.step();
+                i--;
+            }
+        }
+        if (result == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: list index out of range", name());
+        return result;
+    }
+}
+
+class Mal.BuiltinFunctionFirst : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionFirst();
+    }
+    public override string name() { return "first"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected two arguments", name());
+        var list = args.vs.data as Mal.Listlike;
+        if (list == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a list number", name());
+        Mal.Val? result = list.iter().deref();
+        if (result == null)
+            result = new Mal.Nil();
+        return result;
+    }
+}
+
+class Mal.BuiltinFunctionRest : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionRest();
+    }
+    public override string name() { return "rest"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected two arguments", name());
+        var list = args.vs.data as Mal.Listlike;
+        if (list == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a list", name());
+        var result = new Mal.List.empty();
+        for (var iter = list.iter().step(); iter.nonempty(); iter.step())
+            result.vs.append(iter.deref());
+        return result;
+    }
+}
+
+class Mal.BuiltinFunctionThrow : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionThrow();
+    }
+    private static Mal.Val? curr_exception;
+    static construct {
+        curr_exception = null;
+    }
+    public static void clear() {
+        curr_exception = null;
+    }
+    public static Mal.Val thrown_value(Mal.Error err) {
+        if (err is Mal.Error.EXCEPTION_THROWN) {
+            assert(curr_exception != null);
+            Mal.Val toret = curr_exception;
+            curr_exception = null;
+            return toret;
+        } else {
+            return new Mal.String(err.message);
+        }
+    }
+
+    public override string name() { return "throw"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        assert(curr_exception == null);
+        curr_exception = args.vs.data;
+        throw new Mal.Error.EXCEPTION_THROWN("core function throw called");
+    }
+}
+
+class Mal.BuiltinFunctionApply : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionApply();
+    }
+    public override string name() { return "apply"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() < 2)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected at least two arguments", name());
+        var function = args.vs.data;
+        unowned GLib.List<Mal.Val> lastlink = args.vs.last();
+        var list = lastlink.data as Mal.Listlike;
+        if (list == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected final argument to be a list", name());
+        var fnargs = list.as_glib_list();
+        for (unowned GLib.List<Mal.Val> link = lastlink.prev;
+             link != args.vs; link = link.prev)
+            fnargs.prepend(link.data);
+        return call_function(function, fnargs, name());
+    }
+}
+
+class Mal.BuiltinFunctionMap : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionMap();
+    }
+    public override string name() { return "map"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected two arguments", name());
+        var function = args.vs.data;
+        var list = args.vs.next.data as Mal.Listlike;
+        if (list == null)
+            throw new Mal.Error.BAD_PARAMS("%s: expected a list", name());
+        var result = new Mal.List.empty();
+        for (var iter = list.iter(); iter.nonempty(); iter.step()) {
+            var fnargs = new GLib.List<Mal.Val>();
+            fnargs.append(iter.deref());
+            result.vs.append(call_function(function, fnargs, name()));
+        }
+        return result;
+    }
+}
+
+class Mal.BuiltinFunctionSymbol : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionSymbol();
+    }
+    public override string name() { return "symbol"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1 || !(args.vs.data is Mal.String))
+            throw new Mal.Error.BAD_PARAMS("%s: expected one string", name());
+        return new Mal.Sym((args.vs.data as Mal.String).v);
+    }
+}
+
+class Mal.BuiltinFunctionKeyword : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionKeyword();
+    }
+    public override string name() { return "keyword"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1 || !(args.vs.data is Mal.String))
+            throw new Mal.Error.BAD_PARAMS("%s: expected one string", name());
+        return new Mal.Keyword((args.vs.data as Mal.String).v);
+    }
+}
+
+class Mal.BuiltinFunctionAssoc : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionAssoc();
+    }
+    public override string name() { return "assoc"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        var iter = args.iter();
+        var oldmap = iter.deref() as Mal.Hashmap;
+        if (iter.deref() is Mal.Nil)
+            oldmap = new Mal.Hashmap();
+        if (oldmap == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a hash-map to modify", name());
+
+        var map = new Mal.Hashmap();
+        foreach (var key in oldmap.vs.get_keys())
+            map.insert(key, oldmap.vs[key]);
+
+        for (iter.step(); iter.nonempty(); iter.step()) {
+            var key = iter.deref();
+            var value = iter.step().deref();
+            if (value == null)
+                throw new Mal.Error.BAD_PARAMS(
+                    "%s: expected an even number of arguments", name());
+            map.insert(key, value);
+        }
+        return map;
+    }
+}
+
+class Mal.BuiltinFunctionDissoc : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionDissoc();
+    }
+    public override string name() { return "dissoc"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        var iter = args.iter();
+        var oldmap = iter.deref() as Mal.Hashmap;
+        if (iter.deref() is Mal.Nil)
+            oldmap = new Mal.Hashmap();
+        if (oldmap == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a hash-map to modify", name());
+
+        var map = new Mal.Hashmap();
+        foreach (var key in oldmap.vs.get_keys())
+            map.insert(key, oldmap.vs[key]);
+
+        for (iter.step(); iter.nonempty(); iter.step()) {
+            var key = iter.deref();
+            map.remove(key);
+        }
+        return map;
+    }
+}
+
+// Can't call it BuiltinFunctionGet, or else valac defines
+// BUILTIN_FUNCTION_GET_CLASS at the C level for this class, but that
+// was already defined as the 'get class' macro for BuiltinFunction
+// itself!
+class Mal.BuiltinFunctionGetFn : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionGetFn();
+    }
+    public override string name() { return "get"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected two arguments", name());
+        if (args.vs.data is Mal.Nil)
+            return new Mal.Nil();
+        var map = args.vs.data as Mal.Hashmap;
+        if (map == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a hash-map to query", name());
+        var key = args.vs.next.data as Mal.Hashable;
+        if (key == null)
+            throw new Mal.Error.HASH_KEY_TYPE_ERROR(
+                "%s: bad type as hash key", name());
+        var value = map.vs[key];
+        return value != null ? value : new Mal.Nil();
+    }
+}
+
+class Mal.BuiltinFunctionContains : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionContains();
+    }
+    public override string name() { return "contains?"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected two arguments", name());
+        if (args.vs.data is Mal.Nil)
+            return new Mal.Bool(false);
+        var map = args.vs.data as Mal.Hashmap;
+        if (map == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a hash-map to query", name());
+        var key = args.vs.next.data as Mal.Hashable;
+        if (key == null)
+            throw new Mal.Error.HASH_KEY_TYPE_ERROR(
+                "%s: bad type as hash key", name());
+        var value = map.vs[key];
+        return new Mal.Bool(value != null);
+    }
+}
+
+class Mal.BuiltinFunctionKeys : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionKeys();
+    }
+    public override string name() { return "keys"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected one argument", name());
+        var keys = new Mal.List.empty();
+        if (args.vs.data is Mal.Nil)
+            return keys;
+        var map = args.vs.data as Mal.Hashmap;
+        if (map == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a hash-map to query", name());
+        foreach (var key in map.vs.get_keys())
+            keys.vs.append(key);
+        return keys;
+    }
+}
+
+class Mal.BuiltinFunctionVals : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionVals();
+    }
+    public override string name() { return "vals"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected one argument", name());
+        var vals = new Mal.List.empty();
+        if (args.vs.data is Mal.Nil)
+            return vals;
+        var map = args.vs.data as Mal.Hashmap;
+        if (map == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a hash-map to query", name());
+        foreach (var key in map.vs.get_keys())
+            vals.vs.append(map.vs[key]);
+        return vals;
+    }
+}
+
+class Mal.BuiltinFunctionReadline : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionReadline();
+    }
+    public override string name() { return "readline"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected one argument", name());
+        string prompt = "";
+        if (args.vs.data is Mal.String)
+            prompt = (args.vs.data as Mal.String).v;
+        else if (!(args.vs.data is Mal.Nil))
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a string prompt", name());
+        string? line = Readline.readline(prompt);
+        if (line == null)
+            return new Mal.Nil();
+        return new Mal.String(line);
+    }
+}
+
+class Mal.BuiltinFunctionMeta : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionMeta();
+    }
+    public override string name() { return "meta"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected one argument", name());
+        var vwm = args.vs.data as Mal.ValWithMetadata;
+        if (vwm == null)
+            return new Mal.Nil();
+        return vwm.metadata;
+    }
+}
+
+class Mal.BuiltinFunctionWithMeta : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionWithMeta();
+    }
+    public override string name() { return "with-meta"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected one argument", name());
+        var vwm = args.vs.data as Mal.ValWithMetadata;
+        if (vwm == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: bad type for with-meta", name());
+        var copied = vwm.copy();
+        copied.metadata = args.vs.next.data;
+        return copied;
+    }
+}
+
+class Mal.BuiltinFunctionTimeMs : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionTimeMs();
+    }
+    public override string name() { return "time-ms"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 0)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected no arguments", name());
+        var time = GLib.TimeVal();
+        time.get_current_time();
+        return new Mal.Num(time.tv_sec * 1000 + time.tv_usec / 1000);
+    }
+}
+
+class Mal.BuiltinFunctionConj : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionConj();
+    }
+    public override string name() { return "conj"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        var iter = args.iter();
+        var collection = iter.deref() as Mal.Listlike;
+        if (collection == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected a collection to modify", name());
+
+        if (collection is Mal.Vector) {
+            var oldvec = collection as Mal.Vector;
+            var n = args.vs.length() - 1;
+            var newvec = new Mal.Vector.with_size(oldvec.vs.length + n);
+            int i;
+            for (i = 0; i < oldvec.vs.length; i++)
+                newvec.vs[i] = oldvec.vs[i];
+            for (iter.step(); iter.nonempty(); iter.step(), i++)
+                newvec.vs[i] = iter.deref();
+            return newvec;
+        } else {
+            var newlist = new Mal.List.empty();
+            for (var citer = collection.iter(); citer.nonempty(); citer.step())
+                newlist.vs.append(citer.deref());
+            for (iter.step(); iter.nonempty(); iter.step())
+                newlist.vs.prepend(iter.deref());
+            return newlist;
+        }
+    }
+}
+
+class Mal.BuiltinFunctionSeq : Mal.BuiltinFunction {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionSeq();
+    }
+    public override string name() { return "seq"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS(
+                "%s: expected one argument", name());
+        Mal.List toret;
+        if (args.vs.data is Mal.List) {
+            toret = args.vs.data as Mal.List;
+        } else {
+            toret = new Mal.List.empty();
+            if (args.vs.data is Mal.String) {
+                var str = (args.vs.data as Mal.String).v;
+                if (str.length != 0) {
+                    unowned string tail = str;
+                    while (tail != "") {
+                        unowned string new_tail = tail.next_char();
+                        var ch = str.substring(str.length - tail.length,
+                                               tail.length - new_tail.length);
+                        toret.vs.append(new Mal.String(ch));
+                        tail = new_tail;
+                    }
+                }
+            } else if (args.vs.data is Mal.Listlike) {
+                var collection = args.vs.data as Mal.Listlike;
+                for (var iter = collection.iter(); iter.nonempty(); iter.step())
+                    toret.vs.append(iter.deref());
+            } else {
+                throw new Mal.Error.BAD_PARAMS("%s: bad input type", name());
+            }
+        }
+        if (toret.vs.length() == 0)
+            return new Mal.Nil();
+        return toret;
+    }
+}
+
+class Mal.Core {
+    public static GLib.HashTable<string, Mal.Val> ns;
+
+    private static void add_builtin(Mal.BuiltinFunction f) {
+        ns[f.name()] = f;
+    }
+
+    public static void make_ns() {
+        ns = new GLib.HashTable<string, Val>(str_hash, str_equal);
+        add_builtin(new BuiltinFunctionAdd());
+        add_builtin(new BuiltinFunctionSub());
+        add_builtin(new BuiltinFunctionMul());
+        add_builtin(new BuiltinFunctionDiv());
+        add_builtin(new BuiltinFunctionPrStr());
+        add_builtin(new BuiltinFunctionStr());
+        add_builtin(new BuiltinFunctionPrn());
+        add_builtin(new BuiltinFunctionPrintln());
+        add_builtin(new BuiltinFunctionReadString());
+        add_builtin(new BuiltinFunctionSlurp());
+        add_builtin(new BuiltinFunctionList());
+        add_builtin(new BuiltinFunctionListP());
+        add_builtin(new BuiltinFunctionNilP());
+        add_builtin(new BuiltinFunctionTrueP());
+        add_builtin(new BuiltinFunctionFalseP());
+        add_builtin(new BuiltinFunctionNumberP());
+        add_builtin(new BuiltinFunctionStringP());
+        add_builtin(new BuiltinFunctionSymbol());
+        add_builtin(new BuiltinFunctionSymbolP());
+        add_builtin(new BuiltinFunctionKeyword());
+        add_builtin(new BuiltinFunctionKeywordP());
+        add_builtin(new BuiltinFunctionVector());
+        add_builtin(new BuiltinFunctionVectorP());
+        add_builtin(new BuiltinFunctionSequentialP());
+        add_builtin(new BuiltinFunctionHashMap());
+        add_builtin(new BuiltinFunctionMapP());
+        add_builtin(new BuiltinFunctionEmptyP());
+        add_builtin(new BuiltinFunctionFnP());
+        add_builtin(new BuiltinFunctionMacroP());
+        add_builtin(new BuiltinFunctionCount());
+        add_builtin(new BuiltinFunctionEQ());
+        add_builtin(new BuiltinFunctionLT());
+        add_builtin(new BuiltinFunctionLE());
+        add_builtin(new BuiltinFunctionGT());
+        add_builtin(new BuiltinFunctionGE());
+        add_builtin(new BuiltinFunctionAtom());
+        add_builtin(new BuiltinFunctionAtomP());
+        add_builtin(new BuiltinFunctionDeref());
+        add_builtin(new BuiltinFunctionReset());
+        add_builtin(new BuiltinFunctionSwap());
+        add_builtin(new BuiltinFunctionCons());
+        add_builtin(new BuiltinFunctionConcat());
+        add_builtin(new BuiltinFunctionNth());
+        add_builtin(new BuiltinFunctionFirst());
+        add_builtin(new BuiltinFunctionRest());
+        add_builtin(new BuiltinFunctionThrow());
+        add_builtin(new BuiltinFunctionApply());
+        add_builtin(new BuiltinFunctionMap());
+        add_builtin(new BuiltinFunctionAssoc());
+        add_builtin(new BuiltinFunctionDissoc());
+        add_builtin(new BuiltinFunctionGetFn());
+        add_builtin(new BuiltinFunctionContains());
+        add_builtin(new BuiltinFunctionKeys());
+        add_builtin(new BuiltinFunctionVals());
+        add_builtin(new BuiltinFunctionReadline());
+        add_builtin(new BuiltinFunctionMeta());
+        add_builtin(new BuiltinFunctionWithMeta());
+        add_builtin(new BuiltinFunctionTimeMs());
+        add_builtin(new BuiltinFunctionConj());
+        add_builtin(new BuiltinFunctionSeq());
+    }
+}
diff --git a/vala/env.vala b/vala/env.vala
new file mode 100644 (file)
index 0000000..7616257
--- /dev/null
@@ -0,0 +1,68 @@
+class Mal.Env : GLib.Object {
+    public GLib.HashTable<Mal.Sym, Mal.Val> data;
+    Mal.Env? outer;
+
+    construct {
+        data = new GLib.HashTable<Mal.Sym, Mal.Val>(
+            Mal.Hashable.hash, Mal.Hashable.equal);
+    }
+
+    public Env.within(Mal.Env outer_) {
+        outer = outer_;
+    }
+
+    public Env() {
+        outer = null;
+    }
+
+    public Env.funcall(Mal.Env outer_, Mal.Listlike binds, Mal.List exprs)
+    throws Mal.Error {
+        outer = outer_;
+        var binditer = binds.iter();
+        unowned GLib.List<Mal.Val> exprlist = exprs.vs;
+
+        while (binditer.nonempty()) {
+            var paramsym = binditer.deref() as Mal.Sym;
+            if (paramsym.v == "&") {
+                binditer.step();
+                var rest = binditer.deref();
+                binditer.step();
+                if (rest == null || binditer.nonempty())
+                    throw new Mal.Error.BAD_PARAMS(
+                        "expected exactly one parameter name after &");
+                set(rest as Mal.Sym, new Mal.List(exprlist.copy()));
+                return;
+            } else {
+                if (exprlist == null)
+                    throw new Mal.Error.BAD_PARAMS(
+                        "too few arguments for function");
+                set(paramsym, exprlist.data);
+                binditer.step();
+                exprlist = exprlist.next;
+            }
+        }
+        if (exprlist != null)
+            throw new Mal.Error.BAD_PARAMS("too many arguments for function");
+    }
+
+    // Use the 'new' keyword to silence warnings about 'set' and 'get'
+    // already having meanings that we're overwriting
+    public new void set(Mal.Sym key, Mal.Val f) {
+        data[key] = f;
+    }
+
+    public Mal.Env? find(Mal.Sym key) {
+        if (key in data)
+            return this;
+        if (outer == null)
+            return null;
+        return outer.find(key);
+    }
+
+    public new Mal.Val get(Mal.Sym key) throws Mal.Error {
+        var found = find(key);
+        if (found == null)
+            throw new Error.ENV_LOOKUP_FAILED("'%s' not found", key.v);
+        return found.data[key];
+    }
+}
diff --git a/vala/printer.vala b/vala/printer.vala
new file mode 100644 (file)
index 0000000..22abb63
--- /dev/null
@@ -0,0 +1,66 @@
+namespace Mal {
+    string pr_str(Mal.Val val, bool print_readably = true) {
+        if (val is Mal.Nil)
+            return "nil";
+        if (val is Mal.Bool)
+            return (val as Mal.Bool).v ? "true" : "false";
+        if (val is Mal.Sym)
+            return (val as Mal.Sym).v;
+        if (val is Mal.Keyword)
+            return ":" + (val as Mal.Keyword).v;
+        if (val is Mal.Num)
+            return ("%"+int64.FORMAT_MODIFIER+"d")
+                .printf((val as Mal.Num).v);
+        if (val is Mal.String) {
+            string s = (val as Mal.String).v;
+            if (print_readably)
+                s = "\"%s\"".printf(s.replace("\\", "\\\\")
+                                    .replace("\n", "\\n").
+                                    replace("\"", "\\\""));
+            return s;
+        }
+        if (val is Mal.List) {
+            string toret = "(";
+            string sep = "";
+            foreach (var elt in (val as Mal.List).vs) {
+                toret += sep + pr_str(elt, print_readably);
+                sep = " ";
+            }
+            toret += ")";
+            return toret;
+        }
+        if (val is Mal.Vector) {
+            string toret = "[";
+            string sep = "";
+            foreach (var elt in (val as Mal.Vector).vs) {
+                toret += sep + pr_str(elt, print_readably);
+                sep = " ";
+            }
+            toret += "]";
+            return toret;
+        }
+        if (val is Mal.Hashmap) {
+            string toret = "{";
+            string sep = "";
+            var map = (val as Mal.Hashmap).vs;
+            foreach (var key in map.get_keys()) {
+                toret += (sep + pr_str(key, print_readably) + " " +
+                          pr_str(map[key], print_readably));
+                sep = " ";
+            }
+            toret += "}";
+            return toret;
+        }
+        if (val is Mal.BuiltinFunction) {
+            return "#<builtin:%s>".printf((val as Mal.BuiltinFunction).name());
+        }
+        if (val is Mal.Function) {
+            return "#<function>";
+        }
+        if (val is Mal.Atom) {
+            return "(atom %s)".printf(
+                pr_str((val as Mal.Atom).v, print_readably));
+        }
+        return "??";
+    }
+}
diff --git a/vala/reader.vala b/vala/reader.vala
new file mode 100644 (file)
index 0000000..caf400b
--- /dev/null
@@ -0,0 +1,180 @@
+class Mal.Reader : GLib.Object {
+    static Regex tok_re;
+    static Regex tok_num;
+
+    int origlen;
+    string data;
+    int pos;
+
+    string next_token;
+
+    static construct {
+        tok_re = /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;[^\n]*|[^\s\[\]{}('"`,;)]*)/; // comment to unconfuse emacs vala-mode "]);
+        tok_num = /^-?[0-9]/;
+    }
+
+    private string poserr(string fmt, ...) {
+        return "char %d: %s".printf(origlen - data.length,
+                                    fmt.vprintf(va_list()));
+    }
+
+    private void advance() throws Error {
+        do {
+            MatchInfo info;
+            if (!tok_re.match(data, 0, out info))
+                throw new Error.BAD_TOKEN(poserr("bad token"));
+
+            next_token = info.fetch(1);
+            int tokenend;
+            info.fetch_pos(1, null, out tokenend);
+            data = data[tokenend:data.length];
+        } while (next_token.has_prefix(";"));
+    }
+
+    public Reader(string str) throws Error {
+        data = str;
+        origlen = data.length;
+        pos = 0;
+        advance();
+    }
+
+    public string peek() throws Error {
+        return next_token;
+    }
+
+    public string next() throws Error {
+        advance();
+        return peek();
+    }
+
+    public static Mal.Val? read_str(string str) throws Error {
+        var rdr = new Reader(str);
+        if (rdr.peek() == "")
+            return null;
+        var toret = rdr.read_form();
+        if (rdr.peek() != "")
+            throw new Mal.Error.PARSE_ERROR(
+                rdr.poserr("trailing junk after expression"));
+        return toret;
+    }
+
+    public Mal.Val read_form() throws Error {
+        string token = peek();
+        if (token == "(") {
+            next(); // eat (
+            return new Mal.List(read_list(")"));
+        } else {
+            return read_atom();
+        }
+    }
+
+    public GLib.List<Mal.Val> read_list(string endtok) throws Error {
+        var list = new GLib.List<Mal.Val>();
+        string token;
+        while (true) {
+            token = peek();
+            if (token == "")
+                throw new Mal.Error.PARSE_ERROR(poserr("unbalanced parens"));
+            if (token == endtok) {
+                next(); // eat end token
+                return list;
+            }
+
+            list.append(read_form());
+        }
+    }
+
+    public Mal.Hashmap read_hashmap() throws Error {
+        var map = new Mal.Hashmap();
+        string token;
+        while (true) {
+            Mal.Val vals[2];
+            for (int i = 0; i < 2; i++) {
+                token = peek();
+                if (token == "")
+                    throw new Mal.Error.PARSE_ERROR(
+                        poserr("unbalanced braces"));
+                if (token == "}") {
+                    if (i != 0)
+                        throw new Mal.Error.PARSE_ERROR(
+                            poserr("odd number of elements in hashmap"));
+
+                    next(); // eat end token
+                    return map;
+                }
+
+                vals[i] = read_form();
+            }
+            map.insert(vals[0], vals[1]);
+        }
+    }
+
+    public Mal.Val read_atom() throws Error {
+        string token = peek();
+        next();
+        if (tok_num.match(token))
+            return new Mal.Num(int64.parse(token));
+        if (token.has_prefix(":"))
+            return new Mal.Keyword(token[1:token.length]);
+        if (token.has_prefix("\"")) {
+            if (!token.has_suffix("\""))
+                throw new Mal.Error.BAD_TOKEN(
+                    poserr("end of input in mid-string"));
+
+            token = token[1:token.length-1];
+
+            int end = 0;
+            int pos = 0;
+            string strval = "";
+
+            while ((pos = token.index_of ("\\", end)) != -1) {
+                strval += token[end:pos];
+                switch (token[pos:pos+2]) {
+                case "\\\\":
+                    strval += "\\"; break;
+                case "\\\"":
+                    strval += "\""; break;
+                case "\\n":
+                    strval += "\n"; break;
+                }
+                end = pos+2;
+            }
+            strval += token[end:token.length];
+            return new Mal.String(strval);
+        }
+        switch (token) {
+        case "nil":
+            return new Mal.Nil();
+        case "true":
+            return new Mal.Bool(true);
+        case "false":
+            return new Mal.Bool(false);
+        case "[":
+            return new Mal.Vector.from_list(read_list("]"));
+        case "{":
+            return read_hashmap();
+        case "'":
+        case "`":
+        case "~":
+        case "~@":
+        case "@":
+            var list = new GLib.List<Mal.Val>();
+            list.append(new Mal.Sym(
+                            token == "'" ? "quote" :
+                            token == "`" ? "quasiquote" :
+                            token == "~" ? "unquote" :
+                            token == "~@" ? "splice-unquote" : "deref"));
+            list.append(read_form());
+            return new Mal.List(list);
+        case "^":
+            var list = new GLib.List<Mal.Val>();
+            list.append(new Mal.Sym("with-meta"));
+            var metadata = read_form();
+            list.append(read_form());
+            list.append(metadata);
+            return new Mal.List(list);
+        default:
+            return new Mal.Sym(token);
+        }
+    }
+}
diff --git a/vala/run b/vala/run
new file mode 100755 (executable)
index 0000000..8ba68a5
--- /dev/null
+++ b/vala/run
@@ -0,0 +1,2 @@
+#!/bin/bash
+exec $(dirname $0)/${STEP:-stepA_mal} "${@}"
diff --git a/vala/step0_repl.vala b/vala/step0_repl.vala
new file mode 100644 (file)
index 0000000..817a92e
--- /dev/null
@@ -0,0 +1,36 @@
+class Mal.Main : GLib.Object {
+    public static string? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+        } else {
+            stdout.printf("\n");
+        }
+        return line;
+    }
+
+    public static string EVAL(string expr) {
+        return expr;
+    }
+
+    public static void PRINT(string value) {
+        stdout.printf("%s\n", value);
+    }
+
+    public static bool rep() {
+        string? line = READ();
+        if (line == null)
+            return false;
+        if (line.length > 0) {
+            string value = EVAL(line);
+            PRINT(value);
+        }
+        return true;
+    }
+
+    public static int main(string[] args) {
+        while (rep());
+        return 0;
+    }
+}
diff --git a/vala/step1_read_print.vala b/vala/step1_read_print.vala
new file mode 100644 (file)
index 0000000..f756ff3
--- /dev/null
@@ -0,0 +1,48 @@
+class Mal.Main : GLib.Object {
+    static bool eof;
+
+    static construct {
+        eof = false;
+    }
+
+    public static Mal.Val? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+
+            try {
+                return Reader.read_str(line);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return null;
+            }
+        } else {
+            stdout.printf("\n");
+            eof = true;
+            return null;
+        }
+    }
+
+    public static Mal.Val EVAL(Mal.Val expr) {
+        return expr;
+    }
+
+    public static void PRINT(Mal.Val value) {
+        stdout.printf("%s\n", pr_str(value));
+    }
+
+    public static void rep() {
+        Mal.Val? val = READ();
+        if (val != null) {
+            val = EVAL(val);
+            PRINT(val);
+        }
+    }
+
+    public static int main(string[] args) {
+        while (!eof)
+            rep();
+        return 0;
+    }
+}
diff --git a/vala/step2_eval.vala b/vala/step2_eval.vala
new file mode 100644 (file)
index 0000000..10c4512
--- /dev/null
@@ -0,0 +1,162 @@
+abstract class Mal.BuiltinFunctionDyadicArithmetic : Mal.BuiltinFunction {
+    public abstract int64 result(int64 a, int64 b);
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS("%s: expected two numbers", name());
+        unowned Mal.Num a = args.vs.nth_data(0) as Mal.Num;
+        unowned Mal.Num b = args.vs.nth_data(1) as Mal.Num;
+        if (a == null || b == null)
+            throw new Mal.Error.BAD_PARAMS("%s: expected two numbers", name());
+        return new Mal.Num(result(a.v, b.v));
+    }
+}
+
+class Mal.BuiltinFunctionAdd : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionAdd();
+    }
+    public override string name() { return "+"; }
+    public override int64 result(int64 a, int64 b) { return a+b; }
+}
+
+class Mal.BuiltinFunctionSub : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionSub();
+    }
+    public override string name() { return "-"; }
+    public override int64 result(int64 a, int64 b) { return a-b; }
+}
+
+class Mal.BuiltinFunctionMul : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionMul();
+    }
+    public override string name() { return "*"; }
+    public override int64 result(int64 a, int64 b) { return a*b; }
+}
+
+class Mal.BuiltinFunctionDiv : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionDiv();
+    }
+    public override string name() { return "/"; }
+    public override int64 result(int64 a, int64 b) { return a/b; }
+}
+
+class Mal.Env : GLib.Object {
+    public GLib.HashTable<Mal.Sym, Mal.Val> data;
+    construct {
+        data = new GLib.HashTable<Mal.Sym, Mal.Val>(
+            Mal.Hashable.hash, Mal.Hashable.equal);
+    }
+    // Use the 'new' keyword to silence warnings about 'set' and 'get'
+    // already having meanings that we're overwriting
+    public new void set(Mal.Sym key, Mal.Val f) {
+        data[key] = f;
+    }
+    public new Mal.Val get(Mal.Sym key) throws Mal.Error {
+        var toret = data[key];
+        if (toret == null)
+            throw new Error.ENV_LOOKUP_FAILED("no such variable '%s'", key.v);
+        return toret;
+    }
+}
+
+class Mal.Main : GLib.Object {
+    static bool eof;
+
+    static construct {
+        eof = false;
+    }
+
+    public static Mal.Val? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+
+            try {
+                return Reader.read_str(line);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return null;
+            }
+        } else {
+            stdout.printf("\n");
+            eof = true;
+            return null;
+        }
+    }
+
+    public static Mal.Val eval_ast(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.Sym)
+            return env.get(ast as Mal.Sym);
+        if (ast is Mal.List) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.List).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.List(results);
+        }
+        if (ast is Mal.Vector) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.Vector).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.Vector.from_list(results);
+        }
+        if (ast is Mal.Hashmap) {
+            var result = new Mal.Hashmap();
+            var map = (ast as Mal.Hashmap).vs;
+            foreach (var key in map.get_keys())
+                result.insert(key, EVAL(map[key], env));
+            return result;
+        }
+        return ast;
+    }
+
+    public static Mal.Val EVAL(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.List) {
+            unowned GLib.List<Mal.Val> list = (ast as Mal.List).vs;
+            if (list.first() == null)
+                return ast;
+            var newlist = eval_ast(ast, env) as Mal.List;
+            unowned GLib.List<Mal.Val> firstlink = newlist.vs.first();
+            var fn = firstlink.data as Mal.BuiltinFunction;
+            newlist.vs.remove_link(firstlink);
+            return fn.call(newlist);
+        } else {
+            return eval_ast(ast, env);
+        }
+    }
+
+    public static void PRINT(Mal.Val value) {
+        stdout.printf("%s\n", pr_str(value));
+    }
+
+    public static void rep(Mal.Env env) throws Mal.Error {
+        Mal.Val? val = READ();
+        if (val != null) {
+            val = EVAL(val, env);
+            PRINT(val);
+        }
+    }
+
+    public static int main(string[] args) {
+        var env = new Mal.Env();
+
+        env.set(new Mal.Sym("+"), new BuiltinFunctionAdd());
+        env.set(new Mal.Sym("-"), new BuiltinFunctionSub());
+        env.set(new Mal.Sym("*"), new BuiltinFunctionMul());
+        env.set(new Mal.Sym("/"), new BuiltinFunctionDiv());
+
+        while (!eof) {
+            try {
+                rep(env);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+            }
+        }
+        return 0;
+    }
+}
diff --git a/vala/step3_env.vala b/vala/step3_env.vala
new file mode 100644 (file)
index 0000000..719d749
--- /dev/null
@@ -0,0 +1,206 @@
+abstract class Mal.BuiltinFunctionDyadicArithmetic : Mal.BuiltinFunction {
+    public abstract int64 result(int64 a, int64 b);
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 2)
+            throw new Mal.Error.BAD_PARAMS("%s: expected two numbers", name());
+        unowned Mal.Num a = args.vs.nth_data(0) as Mal.Num;
+        unowned Mal.Num b = args.vs.nth_data(1) as Mal.Num;
+        if (a == null || b == null)
+            throw new Mal.Error.BAD_PARAMS("%s: expected two numbers", name());
+        return new Mal.Num(result(a.v, b.v));
+    }
+}
+
+class Mal.BuiltinFunctionAdd : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionAdd();
+    }
+    public override string name() { return "+"; }
+    public override int64 result(int64 a, int64 b) { return a+b; }
+}
+
+class Mal.BuiltinFunctionSub : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionSub();
+    }
+    public override string name() { return "-"; }
+    public override int64 result(int64 a, int64 b) { return a-b; }
+}
+
+class Mal.BuiltinFunctionMul : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionMul();
+    }
+    public override string name() { return "*"; }
+    public override int64 result(int64 a, int64 b) { return a*b; }
+}
+
+class Mal.BuiltinFunctionDiv : Mal.BuiltinFunctionDyadicArithmetic {
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionDiv();
+    }
+    public override string name() { return "/"; }
+    public override int64 result(int64 a, int64 b) { return a/b; }
+}
+
+class Mal.Main : GLib.Object {
+    static bool eof;
+
+    static construct {
+        eof = false;
+    }
+
+    public static Mal.Val? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+
+            try {
+                return Reader.read_str(line);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return null;
+            }
+        } else {
+            stdout.printf("\n");
+            eof = true;
+            return null;
+        }
+    }
+
+    public static Mal.Val eval_ast(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.Sym)
+            return env.get(ast as Mal.Sym);
+        if (ast is Mal.List) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.List).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.List(results);
+        }
+        if (ast is Mal.Vector) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.Vector).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.Vector.from_list(results);
+        }
+        if (ast is Mal.Hashmap) {
+            var result = new Mal.Hashmap();
+            var map = (ast as Mal.Hashmap).vs;
+            foreach (var key in map.get_keys())
+                result.insert(key, EVAL(map[key], env));
+            return result;
+        }
+        return ast;
+    }
+
+    private static Mal.Val define_eval(Mal.Val key, Mal.Val value,
+                                       Mal.Env eval_env, Mal.Env def_env)
+    throws Mal.Error {
+        var symkey = key as Mal.Sym;
+        if (symkey == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "let*: expected a symbol to define");
+        var val = EVAL(value, eval_env);
+        def_env.set(symkey, val);
+        return val;
+    }
+
+    public static Mal.Val EVAL(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.List) {
+            unowned GLib.List<Mal.Val> list = (ast as Mal.List).vs;
+            if (list.first() == null)
+                return ast;
+
+            var first = list.first().data;
+            if (first is Mal.Sym) {
+                var sym = first as Mal.Sym;
+                switch (sym.v) {
+                case "def!":
+                    if (list.length() != 3)
+                        throw new Mal.Error.BAD_PARAMS(
+                            "def!: expected two values");
+                    return define_eval(list.next.data, list.next.next.data,
+                                       env, env);
+                case "let*":
+                    if (list.length() != 3)
+                        throw new Mal.Error.BAD_PARAMS(
+                            "let*: expected two values");
+                    var defns = list.nth(1).data;
+                    var newenv = new Mal.Env.within(env);
+
+                    if (defns is Mal.List) {
+                        for (unowned GLib.List<Mal.Val> iter =
+                                 (defns as Mal.List).vs;
+                             iter != null; iter = iter.next.next) {
+                            if (iter.next == null)
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "let*: expected an even-length list" +
+                                    " of definitions");
+                            define_eval(iter.data, iter.next.data,
+                                        newenv, newenv);
+                        }
+                    } else if (defns is Mal.Vector) {
+                        var vec = (defns as Mal.Vector).vs;
+                        if (vec.length % 2 != 0)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected an even-length vector" +
+                                " of definitions");
+                        for (var i = 0; i < vec.length; i += 2)
+                            define_eval(vec[i], vec[i+1], newenv, newenv);
+                    } else {
+                        throw new Mal.Error.BAD_PARAMS(
+                            "let*: expected a list or vector of definitions");
+                    }
+                    return EVAL(list.nth(2).data, newenv);
+                }
+            }
+
+            var newlist = eval_ast(ast, env) as Mal.List;
+            unowned GLib.List<Mal.Val> firstlink = newlist.vs.first();
+            Mal.Val firstdata = firstlink.data;
+            newlist.vs.remove_link(firstlink);
+
+            if (firstdata is Mal.BuiltinFunction) {
+                return (firstdata as Mal.BuiltinFunction).call(newlist);
+            } else {
+                throw new Mal.Error.CANNOT_APPLY(
+                    "bad value at start of list");
+            }
+        } else {
+            return eval_ast(ast, env);
+        }
+    }
+
+    public static void PRINT(Mal.Val value) {
+        stdout.printf("%s\n", pr_str(value));
+    }
+
+    public static void rep(Mal.Env env) throws Mal.Error {
+        Mal.Val? val = READ();
+        if (val != null) {
+            val = EVAL(val, env);
+            PRINT(val);
+        }
+    }
+
+    public static int main(string[] args) {
+        var env = new Mal.Env();
+
+        env.set(new Mal.Sym("+"), new BuiltinFunctionAdd());
+        env.set(new Mal.Sym("-"), new BuiltinFunctionSub());
+        env.set(new Mal.Sym("*"), new BuiltinFunctionMul());
+        env.set(new Mal.Sym("/"), new BuiltinFunctionDiv());
+
+        while (!eof) {
+            try {
+                rep(env);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+            }
+        }
+        return 0;
+    }
+}
diff --git a/vala/step4_if_fn_do.vala b/vala/step4_if_fn_do.vala
new file mode 100644 (file)
index 0000000..33ee1b3
--- /dev/null
@@ -0,0 +1,209 @@
+class Mal.Main: GLib.Object {
+    static bool eof;
+
+    static construct {
+        eof = false;
+    }
+
+    public static Mal.Val? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+
+            try {
+                return Reader.read_str(line);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return null;
+            }
+        } else {
+            stdout.printf("\n");
+            eof = true;
+            return null;
+        }
+    }
+
+    public static Mal.Val eval_ast(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.Sym)
+            return env.get(ast as Mal.Sym);
+        if (ast is Mal.List) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.List).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.List(results);
+        }
+        if (ast is Mal.Vector) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.Vector).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.Vector.from_list(results);
+        }
+        if (ast is Mal.Hashmap) {
+            var result = new Mal.Hashmap();
+            var map = (ast as Mal.Hashmap).vs;
+            foreach (var key in map.get_keys())
+                result.insert(key, EVAL(map[key], env));
+            return result;
+        }
+        return ast;
+    }
+
+    private static Mal.Val define_eval(Mal.Val key, Mal.Val value,
+                                       Mal.Env eval_env, Mal.Env def_env)
+    throws Mal.Error {
+        var symkey = key as Mal.Sym;
+        if (symkey == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "let*: expected a symbol to define");
+        var val = EVAL(value, eval_env);
+        def_env.set(symkey, val);
+        return val;
+    }
+
+    public static Mal.Val EVAL(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.List) {
+            unowned GLib.List<Mal.Val> list = (ast as Mal.List).vs;
+            if (list.first() == null)
+                return ast;
+
+            var first = list.first().data;
+            if (first is Mal.Sym) {
+                var sym = first as Mal.Sym;
+                switch (sym.v) {
+                case "def!":
+                    if (list.length() != 3)
+                        throw new Mal.Error.BAD_PARAMS(
+                            "def!: expected two values");
+                    return define_eval(list.next.data, list.next.next.data,
+                                       env, env);
+                case "let*":
+                    if (list.length() != 3)
+                        throw new Mal.Error.BAD_PARAMS(
+                            "let*: expected two values");
+                    var defns = list.nth(1).data;
+                    var newenv = new Mal.Env.within(env);
+
+                    if (defns is Mal.List) {
+                        for (unowned GLib.List<Mal.Val> iter =
+                                 (defns as Mal.List).vs;
+                             iter != null; iter = iter.next.next) {
+                            if (iter.next == null)
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "let*: expected an even-length list" +
+                                    " of definitions");
+                            define_eval(iter.data, iter.next.data,
+                                        newenv, newenv);
+                        }
+                    } else if (defns is Mal.Vector) {
+                        var vec = (defns as Mal.Vector).vs;
+                        if (vec.length % 2 != 0)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected an even-length vector" +
+                                " of definitions");
+                        for (var i = 0; i < vec.length; i += 2)
+                            define_eval(vec[i], vec[i+1], newenv, newenv);
+                    } else {
+                        throw new Mal.Error.BAD_PARAMS(
+                            "let*: expected a list or vector of definitions");
+                    }
+                    return EVAL(list.nth(2).data, newenv);
+                case "do":
+                    Mal.Val result = null;
+                    for (list = list.next; list != null; list = list.next)
+                        result = EVAL(list.data, env);
+                    if (result == null)
+                        throw new Mal.Error.BAD_PARAMS(
+                            "do: expected at least one argument");
+                    return result;
+                case "if":
+                    if (list.length() != 3 && list.length() != 4)
+                        throw new Mal.Error.BAD_PARAMS(
+                            "if: expected two or three arguments");
+                    list = list.next;
+                    var cond = EVAL(list.data, env);
+                    list = list.next;
+                    if (!cond.truth_value()) {
+                        // Skip to the else clause, which defaults to nil.
+                        list = list.next;
+                        if (list == null)
+                            return new Mal.Nil();
+                    }
+                    return EVAL(list.data, env);
+                case "fn*":
+                    if (list.length() != 3)
+                        throw new Mal.Error.BAD_PARAMS(
+                            "fn*: expected two arguments");
+                    var binds = list.next.data as Mal.Listlike;
+                    var body = list.next.next.data;
+                    if (binds == null)
+                        throw new Mal.Error.BAD_PARAMS(
+                            "fn*: expected a list of parameter names");
+                    for (var iter = binds.iter(); iter.nonempty(); iter.step())
+                        if (!(iter.deref() is Mal.Sym))
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected parameter name to be "+
+                                "symbol");
+                    return new Mal.Function(binds, body, env);
+                }
+            }
+
+            var newlist = eval_ast(ast, env) as Mal.List;
+            unowned GLib.List<Mal.Val> firstlink = newlist.vs.first();
+            Mal.Val firstdata = firstlink.data;
+            newlist.vs.remove_link(firstlink);
+
+            if (firstdata is Mal.BuiltinFunction) {
+                return (firstdata as Mal.BuiltinFunction).call(newlist);
+            } else if (firstdata is Mal.Function) {
+                var fn = firstdata as Mal.Function;
+                var newenv = new Mal.Env.funcall(
+                    fn.env, fn.parameters, newlist);
+                return EVAL(fn.body, newenv);
+            } else {
+                throw new Mal.Error.CANNOT_APPLY(
+                    "bad value at start of list");
+            }
+        } else {
+            return eval_ast(ast, env);
+        }
+    }
+
+    public static void PRINT(Mal.Val value) {
+        stdout.printf("%s\n", pr_str(value));
+    }
+
+    public static void rep(Mal.Env env) throws Mal.Error {
+        Mal.Val? val = READ();
+        if (val != null) {
+            val = EVAL(val, env);
+            PRINT(val);
+        }
+    }
+
+    public static int main(string[] args) {
+        var env = new Mal.Env();
+
+        Mal.Core.make_ns();
+        foreach (var key in Mal.Core.ns.get_keys())
+            env.set(new Mal.Sym(key), Mal.Core.ns[key]);
+
+        try {
+            EVAL(Mal.Reader.read_str("(def! not (fn* (a) (if a false true)))"),
+                 env);
+        } catch (Mal.Error err) {
+            assert(false); // shouldn't happen
+        }
+
+        while (!eof) {
+            try {
+                rep(env);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+            }
+        }
+        return 0;
+    }
+}
diff --git a/vala/step5_tco.vala b/vala/step5_tco.vala
new file mode 100644 (file)
index 0000000..1f0651c
--- /dev/null
@@ -0,0 +1,220 @@
+class Mal.Main : GLib.Object {
+    static bool eof;
+
+    static construct {
+        eof = false;
+    }
+
+    public static Mal.Val? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+
+            try {
+                return Reader.read_str(line);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return null;
+            }
+        } else {
+            stdout.printf("\n");
+            eof = true;
+            return null;
+        }
+    }
+
+    public static Mal.Val eval_ast(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.Sym)
+            return env.get(ast as Mal.Sym);
+        if (ast is Mal.List) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.List).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.List(results);
+        }
+        if (ast is Mal.Vector) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.Vector).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.Vector.from_list(results);
+        }
+        if (ast is Mal.Hashmap) {
+            var result = new Mal.Hashmap();
+            var map = (ast as Mal.Hashmap).vs;
+            foreach (var key in map.get_keys())
+                result.insert(key, EVAL(map[key], env));
+            return result;
+        }
+        return ast;
+    }
+
+    private static Mal.Val define_eval(Mal.Val key, Mal.Val value,
+                                       Mal.Env eval_env, Mal.Env def_env)
+    throws Mal.Error {
+        var symkey = key as Mal.Sym;
+        if (symkey == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "let*: expected a symbol to define");
+        var val = EVAL(value, eval_env);
+        def_env.set(symkey, val);
+        return val;
+    }
+
+    public static Mal.Val EVAL(Mal.Val ast_, Mal.Env env_)
+    throws Mal.Error {
+        // Copy the implicitly 'unowned' function arguments into
+        // ordinary owned variables which increment the objects'
+        // reference counts. This is so that when we overwrite these
+        // variables within the loop (for TCO) the objects we assign
+        // into them don't immediately get garbage-collected.
+        Mal.Val ast = ast_;
+        Mal.Env env = env_;
+        while (true) {
+            if (ast is Mal.List) {
+                unowned GLib.List<Mal.Val> list = (ast as Mal.List).vs;
+                if (list.first() == null)
+                    return ast;
+
+                var first = list.first().data;
+                if (first is Mal.Sym) {
+                    var sym = first as Mal.Sym;
+                    switch (sym.v) {
+                    case "def!":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "def!: expected two values");
+                        return define_eval(list.next.data, list.next.next.data,
+                                           env, env);
+                    case "let*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected two values");
+                        var defns = list.nth(1).data;
+                        env = new Mal.Env.within(env);
+
+                        if (defns is Mal.List) {
+                            for (unowned GLib.List<Mal.Val> iter =
+                                     (defns as Mal.List).vs;
+                                 iter != null; iter = iter.next.next) {
+                                if (iter.next == null)
+                                    throw new Mal.Error.BAD_PARAMS(
+                                        "let*: expected an even-length list" +
+                                        " of definitions");
+                                define_eval(iter.data, iter.next.data,
+                                            env, env);
+                            }
+                        } else if (defns is Mal.Vector) {
+                            var vec = (defns as Mal.Vector).vs;
+                            if (vec.length % 2 != 0)
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "let*: expected an even-length vector" +
+                                    " of definitions");
+                            for (var i = 0; i < vec.length; i += 2)
+                                define_eval(vec[i], vec[i+1], env, env);
+                        } else {
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected a list or vector of definitions");
+                        }
+                        ast = list.nth(2).data;
+                        continue;      // tail-call optimisation
+                    case "do":
+                        Mal.Val result = null;
+                        for (list = list.next; list != null; list = list.next)
+                            result = EVAL(list.data, env);
+                        if (result == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "do: expected at least one argument");
+                        return result;
+                    case "if":
+                        if (list.length() != 3 && list.length() != 4)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "if: expected two or three arguments");
+                        list = list.next;
+                        var cond = EVAL(list.data, env);
+                        list = list.next;
+                        if (!cond.truth_value()) {
+                            // Skip to the else clause, which defaults to nil.
+                            list = list.next;
+                            if (list == null)
+                                return new Mal.Nil();
+                        }
+                        ast = list.data;
+                        continue;      // tail-call optimisation
+                    case "fn*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected two arguments");
+                        var binds = list.next.data as Mal.Listlike;
+                        var body = list.next.next.data;
+                        if (binds == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected a list of parameter names");
+                        for (var iter = binds.iter(); iter.nonempty(); iter.step())
+                            if (!(iter.deref() is Mal.Sym))
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "fn*: expected parameter name to be "+
+                                    "symbol");
+                        return new Mal.Function(binds, body, env);
+                    }
+                }
+
+                var newlist = eval_ast(ast, env) as Mal.List;
+                unowned GLib.List<Mal.Val> firstlink = newlist.vs.first();
+                Mal.Val firstdata = firstlink.data;
+                newlist.vs.remove_link(firstlink);
+
+                if (firstdata is Mal.BuiltinFunction) {
+                    return (firstdata as Mal.BuiltinFunction).call(newlist);
+                } else if (firstdata is Mal.Function) {
+                    var fn = firstdata as Mal.Function;
+                    env = new Mal.Env.funcall(fn.env, fn.parameters, newlist);
+                    ast = fn.body;
+                    continue;      // tail-call optimisation
+                } else {
+                    throw new Mal.Error.CANNOT_APPLY(
+                        "bad value at start of list");
+                }
+            } else {
+                return eval_ast(ast, env);
+            }
+        }
+    }
+
+    public static void PRINT(Mal.Val value) {
+        stdout.printf("%s\n", pr_str(value));
+    }
+
+    public static void rep(Mal.Env env) throws Mal.Error {
+        Mal.Val? val = READ();
+        if (val != null) {
+            val = EVAL(val, env);
+            PRINT(val);
+        }
+    }
+
+    public static int main(string[] args) {
+        var env = new Mal.Env();
+
+        Mal.Core.make_ns();
+        foreach (var key in Mal.Core.ns.get_keys())
+            env.set(new Mal.Sym(key), Mal.Core.ns[key]);
+
+        try {
+            EVAL(Mal.Reader.read_str("(def! not (fn* (a) (if a false true)))"),
+                 env);
+        } catch (Mal.Error err) {
+            assert(false); // shouldn't happen
+        }
+
+        while (!eof) {
+            try {
+                rep(env);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+            }
+        }
+        return 0;
+    }
+}
diff --git a/vala/step6_file.vala b/vala/step6_file.vala
new file mode 100644 (file)
index 0000000..c68d34e
--- /dev/null
@@ -0,0 +1,258 @@
+class Mal.BuiltinFunctionEval : Mal.BuiltinFunction {
+    public Mal.Env env;
+    public BuiltinFunctionEval(Mal.Env env_) { env = env_; }
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionEval(env);
+    }
+    public override string name() { return "eval"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return Mal.Main.EVAL(args.vs.data, env);
+    }
+}
+
+class Mal.Main : GLib.Object {
+    static bool eof;
+
+    static construct {
+        eof = false;
+    }
+
+    public static Mal.Val? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+
+            try {
+                return Reader.read_str(line);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return null;
+            }
+        } else {
+            stdout.printf("\n");
+            eof = true;
+            return null;
+        }
+    }
+
+    public static Mal.Val eval_ast(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.Sym)
+            return env.get(ast as Mal.Sym);
+        if (ast is Mal.List) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.List).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.List(results);
+        }
+        if (ast is Mal.Vector) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.Vector).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.Vector.from_list(results);
+        }
+        if (ast is Mal.Hashmap) {
+            var result = new Mal.Hashmap();
+            var map = (ast as Mal.Hashmap).vs;
+            foreach (var key in map.get_keys())
+                result.insert(key, EVAL(map[key], env));
+            return result;
+        }
+        return ast;
+    }
+
+    private static Mal.Val define_eval(Mal.Val key, Mal.Val value,
+                                       Mal.Env eval_env, Mal.Env def_env)
+    throws Mal.Error {
+        var symkey = key as Mal.Sym;
+        if (symkey == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "let*: expected a symbol to define");
+        var val = EVAL(value, eval_env);
+        def_env.set(symkey, val);
+        return val;
+    }
+
+    public static Mal.Val EVAL(Mal.Val ast_, Mal.Env env_)
+    throws Mal.Error {
+        // Copy the implicitly 'unowned' function arguments into
+        // ordinary owned variables which increment the objects'
+        // reference counts. This is so that when we overwrite these
+        // variables within the loop (for TCO) the objects we assign
+        // into them don't immediately get garbage-collected.
+        Mal.Val ast = ast_;
+        Mal.Env env = env_;
+        while (true) {
+            if (ast is Mal.List) {
+                unowned GLib.List<Mal.Val> list = (ast as Mal.List).vs;
+                if (list.first() == null)
+                    return ast;
+
+                var first = list.first().data;
+                if (first is Mal.Sym) {
+                    var sym = first as Mal.Sym;
+                    switch (sym.v) {
+                    case "def!":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "def!: expected two values");
+                        return define_eval(list.next.data, list.next.next.data,
+                                           env, env);
+                    case "let*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected two values");
+                        var defns = list.nth(1).data;
+                        env = new Mal.Env.within(env);
+
+                        if (defns is Mal.List) {
+                            for (unowned GLib.List<Mal.Val> iter =
+                                     (defns as Mal.List).vs;
+                                 iter != null; iter = iter.next.next) {
+                                if (iter.next == null)
+                                    throw new Mal.Error.BAD_PARAMS(
+                                        "let*: expected an even-length list" +
+                                        " of definitions");
+                                define_eval(iter.data, iter.next.data,
+                                            env, env);
+                            }
+                        } else if (defns is Mal.Vector) {
+                            var vec = (defns as Mal.Vector).vs;
+                            if (vec.length % 2 != 0)
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "let*: expected an even-length vector" +
+                                    " of definitions");
+                            for (var i = 0; i < vec.length; i += 2)
+                                define_eval(vec[i], vec[i+1], env, env);
+                        } else {
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected a list or vector of definitions");
+                        }
+                        ast = list.nth(2).data;
+                        continue;      // tail-call optimisation
+                    case "do":
+                        Mal.Val result = null;
+                        for (list = list.next; list != null; list = list.next)
+                            result = EVAL(list.data, env);
+                        if (result == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "do: expected at least one argument");
+                        return result;
+                    case "if":
+                        if (list.length() != 3 && list.length() != 4)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "if: expected two or three arguments");
+                        list = list.next;
+                        var cond = EVAL(list.data, env);
+                        list = list.next;
+                        if (!cond.truth_value()) {
+                            // Skip to the else clause, which defaults to nil.
+                            list = list.next;
+                            if (list == null)
+                                return new Mal.Nil();
+                        }
+                        ast = list.data;
+                        continue;      // tail-call optimisation
+                    case "fn*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected two arguments");
+                        var binds = list.next.data as Mal.Listlike;
+                        var body = list.next.next.data;
+                        if (binds == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected a list of parameter names");
+                        for (var iter = binds.iter(); iter.nonempty(); iter.step())
+                            if (!(iter.deref() is Mal.Sym))
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "fn*: expected parameter name to be "+
+                                    "symbol");
+                        return new Mal.Function(binds, body, env);
+                    }
+                }
+
+                var newlist = eval_ast(ast, env) as Mal.List;
+                unowned GLib.List<Mal.Val> firstlink = newlist.vs.first();
+                Mal.Val firstdata = firstlink.data;
+                newlist.vs.remove_link(firstlink);
+
+                if (firstdata is Mal.BuiltinFunction) {
+                    return (firstdata as Mal.BuiltinFunction).call(newlist);
+                } else if (firstdata is Mal.Function) {
+                    var fn = firstdata as Mal.Function;
+                    env = new Mal.Env.funcall(fn.env, fn.parameters, newlist);
+                    ast = fn.body;
+                    continue;      // tail-call optimisation
+                } else {
+                    throw new Mal.Error.CANNOT_APPLY(
+                        "bad value at start of list");
+                }
+            } else {
+                return eval_ast(ast, env);
+            }
+        }
+    }
+
+    public static void PRINT(Mal.Val value) {
+        stdout.printf("%s\n", pr_str(value));
+    }
+
+    public static void rep(Mal.Env env) throws Mal.Error {
+        Mal.Val? val = READ();
+        if (val != null) {
+            val = EVAL(val, env);
+            PRINT(val);
+        }
+    }
+
+    public static void setup(string line, Mal.Env env) {
+        try {
+            EVAL(Reader.read_str(line), env);
+        } catch (Mal.Error err) {
+            assert(false); // shouldn't happen
+        }
+    }
+
+    public static int main(string[] args) {
+        var env = new Mal.Env();
+
+        Mal.Core.make_ns();
+        foreach (var key in Mal.Core.ns.get_keys())
+            env.set(new Mal.Sym(key), Mal.Core.ns[key]);
+        env.set(new Mal.Sym("eval"), new Mal.BuiltinFunctionEval(env));
+
+        setup("(def! not (fn* (a) (if a false true)))", env);
+        setup("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", env);
+
+        var ARGV = new GLib.List<Mal.Val>();
+        if (args.length > 1) {
+            for (int i = args.length - 1; i >= 2; i--)
+                ARGV.prepend(new Mal.String(args[i]));
+        }
+        env.set(new Mal.Sym("*ARGV*"), new Mal.List(ARGV));
+
+        if (args.length > 1) {
+            var contents = new GLib.List<Mal.Val>();
+            contents.prepend(new Mal.String(args[1]));
+            contents.prepend(new Mal.Sym("load-file"));
+            try {
+                EVAL(new Mal.List(contents), env);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return 1;
+            }
+        } else {
+            while (!eof) {
+                try {
+                    rep(env);
+                } catch (Mal.Error err) {
+                    GLib.stderr.printf("%s\n", err.message);
+                }
+            }
+        }
+        return 0;
+    }
+}
diff --git a/vala/step7_quote.vala b/vala/step7_quote.vala
new file mode 100644 (file)
index 0000000..3bb3ff6
--- /dev/null
@@ -0,0 +1,318 @@
+class Mal.BuiltinFunctionEval : Mal.BuiltinFunction {
+    public Mal.Env env;
+    public BuiltinFunctionEval(Mal.Env env_) { env = env_; }
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionEval(env);
+    }
+    public override string name() { return "eval"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return Mal.Main.EVAL(args.vs.data, env);
+    }
+}
+
+class Mal.Main : GLib.Object {
+    static bool eof;
+
+    static construct {
+        eof = false;
+    }
+
+    public static Mal.Val? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+
+            try {
+                return Reader.read_str(line);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return null;
+            }
+        } else {
+            stdout.printf("\n");
+            eof = true;
+            return null;
+        }
+    }
+
+    public static Mal.Val eval_ast(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.Sym)
+            return env.get(ast as Mal.Sym);
+        if (ast is Mal.List) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.List).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.List(results);
+        }
+        if (ast is Mal.Vector) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.Vector).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.Vector.from_list(results);
+        }
+        if (ast is Mal.Hashmap) {
+            var result = new Mal.Hashmap();
+            var map = (ast as Mal.Hashmap).vs;
+            foreach (var key in map.get_keys())
+                result.insert(key, EVAL(map[key], env));
+            return result;
+        }
+        return ast;
+    }
+
+    private static Mal.Val define_eval(Mal.Val key, Mal.Val value,
+                                       Mal.Env eval_env, Mal.Env def_env)
+    throws Mal.Error {
+        var symkey = key as Mal.Sym;
+        if (symkey == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "let*: expected a symbol to define");
+        var val = EVAL(value, eval_env);
+        def_env.set(symkey, val);
+        return val;
+    }
+
+    public static Mal.Val quasiquote(Mal.Val ast)
+    throws Mal.Error {
+        if (!is_pair(ast)) {
+            var list = new GLib.List<Mal.Val>();
+            list.append(new Mal.Sym("quote"));
+            list.append(ast);
+            return new Mal.List(list);
+        }
+
+        var iter = (ast as Mal.Listlike).iter();
+        var first = iter.deref();
+        if (first is Mal.Sym && (first as Mal.Sym).v == "unquote") {
+            if (iter.step().empty())
+                throw new Mal.Error.BAD_PARAMS(
+                    "unquote: expected two values");
+            return iter.deref();
+        }
+
+        if (is_pair(first)) {
+            var fiter = (first as Mal.Listlike).iter();
+            var ffirst = fiter.deref();
+            if (ffirst is Mal.Sym &&
+                (ffirst as Mal.Sym).v == "splice-unquote") {
+                var list = new GLib.List<Mal.Val>();
+                list.append(new Mal.Sym("concat"));
+                if (fiter.step().empty())
+                    throw new Mal.Error.BAD_PARAMS(
+                        "unquote: expected two values");
+                list.append(fiter.deref());
+                var sublist = new GLib.List<Mal.Val>();
+                while (!iter.step().empty())
+                    sublist.append(iter.deref());
+                list.append(quasiquote(new Mal.List(sublist)));
+                return new Mal.List(list);
+            }
+        }
+
+        var list = new GLib.List<Mal.Val>();
+        list.append(new Mal.Sym("cons"));
+        list.append(quasiquote(first));
+        var sublist = new GLib.List<Mal.Val>();
+        while (!iter.step().empty())
+            sublist.append(iter.deref());
+        list.append(quasiquote(new Mal.List(sublist)));
+        return new Mal.List(list);
+    }
+
+    public static Mal.Val EVAL(Mal.Val ast_, Mal.Env env_)
+    throws Mal.Error {
+        // Copy the implicitly 'unowned' function arguments into
+        // ordinary owned variables which increment the objects'
+        // reference counts. This is so that when we overwrite these
+        // variables within the loop (for TCO) the objects we assign
+        // into them don't immediately get garbage-collected.
+        Mal.Val ast = ast_;
+        Mal.Env env = env_;
+        while (true) {
+            if (ast is Mal.List) {
+                unowned GLib.List<Mal.Val> list = (ast as Mal.List).vs;
+                if (list.first() == null)
+                    return ast;
+
+                var first = list.first().data;
+                if (first is Mal.Sym) {
+                    var sym = first as Mal.Sym;
+                    switch (sym.v) {
+                    case "def!":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "def!: expected two values");
+                        return define_eval(list.next.data, list.next.next.data,
+                                           env, env);
+                    case "let*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected two values");
+                        var defns = list.nth(1).data;
+                        env = new Mal.Env.within(env);
+
+                        if (defns is Mal.List) {
+                            for (unowned GLib.List<Mal.Val> iter =
+                                     (defns as Mal.List).vs;
+                                 iter != null; iter = iter.next.next) {
+                                if (iter.next == null)
+                                    throw new Mal.Error.BAD_PARAMS(
+                                        "let*: expected an even-length list" +
+                                        " of definitions");
+                                define_eval(iter.data, iter.next.data,
+                                            env, env);
+                            }
+                        } else if (defns is Mal.Vector) {
+                            var vec = (defns as Mal.Vector).vs;
+                            if (vec.length % 2 != 0)
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "let*: expected an even-length vector" +
+                                    " of definitions");
+                            for (var i = 0; i < vec.length; i += 2)
+                                define_eval(vec[i], vec[i+1], env, env);
+                        } else {
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected a list or vector of "+
+                                "definitions");
+                        }
+                        ast = list.nth(2).data;
+                        continue;      // tail-call optimisation
+                    case "do":
+                        Mal.Val result = null;
+                        for (list = list.next; list != null; list = list.next)
+                            result = EVAL(list.data, env);
+                        if (result == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "do: expected at least one argument");
+                        return result;
+                    case "if":
+                        if (list.length() != 3 && list.length() != 4)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "if: expected two or three arguments");
+                        list = list.next;
+                        var cond = EVAL(list.data, env);
+                        list = list.next;
+                        if (!cond.truth_value()) {
+                            // Skip to the else clause, which defaults to nil.
+                            list = list.next;
+                            if (list == null)
+                                return new Mal.Nil();
+                        }
+                        ast = list.data;
+                        continue;      // tail-call optimisation
+                    case "fn*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected two arguments");
+                        var binds = list.next.data as Mal.Listlike;
+                        var body = list.next.next.data;
+                        if (binds == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected a list of parameter names");
+                        for (var iter = binds.iter(); iter.nonempty();
+                             iter.step())
+                            if (!(iter.deref() is Mal.Sym))
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "fn*: expected parameter name to be "+
+                                    "symbol");
+                        return new Mal.Function(binds, body, env);
+                    case "quote":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "quote: expected one argument");
+                        return list.next.data;
+                    case "quasiquote":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "quasiquote: expected one argument");
+                        ast = quasiquote(list.next.data);
+                        continue;      // tail-call optimisation
+                    }
+                }
+
+                var newlist = eval_ast(ast, env) as Mal.List;
+                unowned GLib.List<Mal.Val> firstlink = newlist.vs.first();
+                Mal.Val firstdata = firstlink.data;
+                newlist.vs.remove_link(firstlink);
+
+                if (firstdata is Mal.BuiltinFunction) {
+                    return (firstdata as Mal.BuiltinFunction).call(newlist);
+                } else if (firstdata is Mal.Function) {
+                    var fn = firstdata as Mal.Function;
+                    env = new Mal.Env.funcall(fn.env, fn.parameters, newlist);
+                    ast = fn.body;
+                    continue;      // tail-call optimisation
+                } else {
+                    throw new Mal.Error.CANNOT_APPLY(
+                        "bad value at start of list");
+                }
+            } else {
+                return eval_ast(ast, env);
+            }
+        }
+    }
+
+    public static void PRINT(Mal.Val value) {
+        stdout.printf("%s\n", pr_str(value));
+    }
+
+    public static void rep(Mal.Env env) throws Mal.Error {
+        Mal.Val? val = READ();
+        if (val != null) {
+            val = EVAL(val, env);
+            PRINT(val);
+        }
+    }
+
+    public static void setup(string line, Mal.Env env) {
+        try {
+            EVAL(Reader.read_str(line), env);
+        } catch (Mal.Error err) {
+            assert(false); // shouldn't happen
+        }
+    }
+
+    public static int main(string[] args) {
+        var env = new Mal.Env();
+
+        Mal.Core.make_ns();
+        foreach (var key in Mal.Core.ns.get_keys())
+            env.set(new Mal.Sym(key), Mal.Core.ns[key]);
+        env.set(new Mal.Sym("eval"), new Mal.BuiltinFunctionEval(env));
+
+        setup("(def! not (fn* (a) (if a false true)))", env);
+        setup("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", env);
+
+        var ARGV = new GLib.List<Mal.Val>();
+        if (args.length > 1) {
+            for (int i = args.length - 1; i >= 2; i--)
+                ARGV.prepend(new Mal.String(args[i]));
+        }
+        env.set(new Mal.Sym("*ARGV*"), new Mal.List(ARGV));
+
+        if (args.length > 1) {
+            var contents = new GLib.List<Mal.Val>();
+            contents.prepend(new Mal.String(args[1]));
+            contents.prepend(new Mal.Sym("load-file"));
+            try {
+                EVAL(new Mal.List(contents), env);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return 1;
+            }
+        } else {
+            while (!eof) {
+                try {
+                    rep(env);
+                } catch (Mal.Error err) {
+                    GLib.stderr.printf("%s\n", err.message);
+                }
+            }
+        }
+        return 0;
+    }
+}
diff --git a/vala/step8_macros.vala b/vala/step8_macros.vala
new file mode 100644 (file)
index 0000000..99c16e0
--- /dev/null
@@ -0,0 +1,356 @@
+class Mal.BuiltinFunctionEval : Mal.BuiltinFunction {
+    public Mal.Env env;
+    public BuiltinFunctionEval(Mal.Env env_) { env = env_; }
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionEval(env);
+    }
+    public override string name() { return "eval"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return Mal.Main.EVAL(args.vs.data, env);
+    }
+}
+
+class Mal.Main : GLib.Object {
+    static bool eof;
+
+    static construct {
+        eof = false;
+    }
+
+    public static Mal.Val? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+
+            try {
+                return Reader.read_str(line);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return null;
+            }
+        } else {
+            stdout.printf("\n");
+            eof = true;
+            return null;
+        }
+    }
+
+    public static Mal.Val eval_ast(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.Sym)
+            return env.get(ast as Mal.Sym);
+        if (ast is Mal.List) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.List).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.List(results);
+        }
+        if (ast is Mal.Vector) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.Vector).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.Vector.from_list(results);
+        }
+        if (ast is Mal.Hashmap) {
+            var result = new Mal.Hashmap();
+            var map = (ast as Mal.Hashmap).vs;
+            foreach (var key in map.get_keys())
+                result.insert(key, EVAL(map[key], env));
+            return result;
+        }
+        return ast;
+    }
+
+    private static Mal.Val define_eval(Mal.Val key, Mal.Val value,
+                                       Mal.Env eval_env, Mal.Env def_env,
+                                       bool is_macro = false)
+    throws Mal.Error {
+        var symkey = key as Mal.Sym;
+        if (symkey == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "let*: expected a symbol to define");
+        var val = EVAL(value, eval_env);
+        if (val is Mal.Function)
+            (val as Mal.Function).is_macro = is_macro;
+        def_env.set(symkey, val);
+        return val;
+    }
+
+    public static Mal.Val quasiquote(Mal.Val ast)
+    throws Mal.Error {
+        if (!is_pair(ast)) {
+            var list = new GLib.List<Mal.Val>();
+            list.append(new Mal.Sym("quote"));
+            list.append(ast);
+            return new Mal.List(list);
+        }
+
+        var iter = (ast as Mal.Listlike).iter();
+        var first = iter.deref();
+        if (first is Mal.Sym && (first as Mal.Sym).v == "unquote") {
+            if (iter.step().empty())
+                throw new Mal.Error.BAD_PARAMS(
+                    "unquote: expected two values");
+            return iter.deref();
+        }
+
+        if (is_pair(first)) {
+            var fiter = (first as Mal.Listlike).iter();
+            var ffirst = fiter.deref();
+            if (ffirst is Mal.Sym &&
+                (ffirst as Mal.Sym).v == "splice-unquote") {
+                var list = new GLib.List<Mal.Val>();
+                list.append(new Mal.Sym("concat"));
+                if (fiter.step().empty())
+                    throw new Mal.Error.BAD_PARAMS(
+                        "unquote: expected two values");
+                list.append(fiter.deref());
+                var sublist = new GLib.List<Mal.Val>();
+                while (!iter.step().empty())
+                    sublist.append(iter.deref());
+                list.append(quasiquote(new Mal.List(sublist)));
+                return new Mal.List(list);
+            }
+        }
+
+        var list = new GLib.List<Mal.Val>();
+        list.append(new Mal.Sym("cons"));
+        list.append(quasiquote(first));
+        var sublist = new GLib.List<Mal.Val>();
+        while (!iter.step().empty())
+            sublist.append(iter.deref());
+        list.append(quasiquote(new Mal.List(sublist)));
+        return new Mal.List(list);
+    }
+
+    public static bool is_macro_call(Mal.Val v, Mal.Env env) {
+        var list = v as Mal.List;
+        if (list == null || list.vs == null || !(list.vs.data is Mal.Sym))
+            return false;
+        try {
+            var fn = env.get(list.vs.data as Mal.Sym) as Mal.Function;
+            return (fn != null && fn.is_macro);
+        } catch (Mal.Error err) {
+            return false;
+        }
+    }
+
+    public static Mal.Val macroexpand(Mal.Val ast_, Mal.Env env)
+    throws Mal.Error {
+        Mal.Val ast = ast_;
+        while (is_macro_call(ast, env)) {
+            var call = ast as Mal.List;
+            var macro = (env.get(call.vs.data as Mal.Sym) as Mal.Function);
+            var macroargs = new Mal.List(call.vs.copy());
+            macroargs.vs.remove_link(macroargs.vs.first());
+            var fnenv = new Mal.Env.funcall(
+                macro.env, macro.parameters, macroargs);
+            ast = Mal.Main.EVAL(macro.body, fnenv);
+        }
+        return ast;
+    }
+
+    public static Mal.Val EVAL(Mal.Val ast_, Mal.Env env_)
+    throws Mal.Error {
+        // Copy the implicitly 'unowned' function arguments into
+        // ordinary owned variables which increment the objects'
+        // reference counts. This is so that when we overwrite these
+        // variables within the loop (for TCO) the objects we assign
+        // into them don't immediately get garbage-collected.
+        Mal.Val ast = ast_;
+        Mal.Env env = env_;
+        while (true) {
+            ast = macroexpand(ast, env);
+            if (ast is Mal.List) {
+                unowned GLib.List<Mal.Val> list = (ast as Mal.List).vs;
+                if (list.first() == null)
+                    return ast;
+
+                var first = list.first().data;
+                if (first is Mal.Sym) {
+                    var sym = first as Mal.Sym;
+                    switch (sym.v) {
+                    case "def!":
+                    case "defmacro!":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "def!: expected two values");
+                        return define_eval(list.next.data, list.next.next.data,
+                                           env, env, sym.v == "defmacro!");
+                    case "let*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected two values");
+                        var defns = list.nth(1).data;
+                        env = new Mal.Env.within(env);
+
+                        if (defns is Mal.List) {
+                            for (unowned GLib.List<Mal.Val> iter =
+                                     (defns as Mal.List).vs;
+                                 iter != null; iter = iter.next.next) {
+                                if (iter.next == null)
+                                    throw new Mal.Error.BAD_PARAMS(
+                                        "let*: expected an even-length list" +
+                                        " of definitions");
+                                define_eval(iter.data, iter.next.data,
+                                            env, env);
+                            }
+                        } else if (defns is Mal.Vector) {
+                            var vec = (defns as Mal.Vector).vs;
+                            if (vec.length % 2 != 0)
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "let*: expected an even-length vector" +
+                                    " of definitions");
+                            for (var i = 0; i < vec.length; i += 2)
+                                define_eval(vec[i], vec[i+1], env, env);
+                        } else {
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected a list or vector of definitions");
+                        }
+                        ast = list.nth(2).data;
+                        continue;      // tail-call optimisation
+                    case "do":
+                        Mal.Val result = null;
+                        for (list = list.next; list != null; list = list.next)
+                            result = EVAL(list.data, env);
+                        if (result == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "do: expected at least one argument");
+                        return result;
+                    case "if":
+                        if (list.length() != 3 && list.length() != 4)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "if: expected two or three arguments");
+                        list = list.next;
+                        var cond = EVAL(list.data, env);
+                        list = list.next;
+                        if (!cond.truth_value()) {
+                            // Skip to the else clause, which defaults to nil.
+                            list = list.next;
+                            if (list == null)
+                                return new Mal.Nil();
+                        }
+                        ast = list.data;
+                        continue;      // tail-call optimisation
+                    case "fn*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected two arguments");
+                        var binds = list.next.data as Mal.Listlike;
+                        var body = list.next.next.data;
+                        if (binds == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected a list of parameter names");
+                        for (var iter = binds.iter(); iter.nonempty();
+                             iter.step())
+                            if (!(iter.deref() is Mal.Sym))
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "fn*: expected parameter name to be "+
+                                    "symbol");
+                        return new Mal.Function(binds, body, env);
+                    case "quote":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "quote: expected one argument");
+                        return list.next.data;
+                    case "quasiquote":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "quasiquote: expected one argument");
+                        ast = quasiquote(list.next.data);
+                        continue;      // tail-call optimisation
+                    case "macroexpand":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "macroexpand: expected one argument");
+                        return macroexpand(list.next.data, env);
+                    }
+                }
+
+                var newlist = eval_ast(ast, env) as Mal.List;
+                unowned GLib.List<Mal.Val> firstlink = newlist.vs.first();
+                Mal.Val firstdata = firstlink.data;
+                newlist.vs.remove_link(firstlink);
+
+                if (firstdata is Mal.BuiltinFunction) {
+                    return (firstdata as Mal.BuiltinFunction).call(newlist);
+                } else if (firstdata is Mal.Function) {
+                    var fn = firstdata as Mal.Function;
+                    env = new Mal.Env.funcall(fn.env, fn.parameters, newlist);
+                    ast = fn.body;
+                    continue;      // tail-call optimisation
+                } else {
+                    throw new Mal.Error.CANNOT_APPLY(
+                        "bad value at start of list");
+                }
+            } else {
+                return eval_ast(ast, env);
+            }
+        }
+    }
+
+    public static void PRINT(Mal.Val value) {
+        stdout.printf("%s\n", pr_str(value));
+    }
+
+    public static void rep(Mal.Env env) throws Mal.Error {
+        Mal.Val? val = READ();
+        if (val != null) {
+            val = EVAL(val, env);
+            PRINT(val);
+        }
+    }
+
+    public static void setup(string line, Mal.Env env) {
+        try {
+            EVAL(Reader.read_str(line), env);
+        } catch (Mal.Error err) {
+            assert(false); // shouldn't happen
+        }
+    }
+
+    public static int main(string[] args) {
+        var env = new Mal.Env();
+
+        Mal.Core.make_ns();
+        foreach (var key in Mal.Core.ns.get_keys())
+            env.set(new Mal.Sym(key), Mal.Core.ns[key]);
+        env.set(new Mal.Sym("eval"), new Mal.BuiltinFunctionEval(env));
+
+        setup("(def! not (fn* (a) (if a false true)))", env);
+        setup("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", env);
+        setup("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))", env);
+        setup("(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))", env);
+
+        var ARGV = new GLib.List<Mal.Val>();
+        if (args.length > 1) {
+            for (int i = args.length - 1; i >= 2; i--)
+                ARGV.prepend(new Mal.String(args[i]));
+        }
+        env.set(new Mal.Sym("*ARGV*"), new Mal.List(ARGV));
+
+        if (args.length > 1) {
+            var contents = new GLib.List<Mal.Val>();
+            contents.prepend(new Mal.String(args[1]));
+            contents.prepend(new Mal.Sym("load-file"));
+            try {
+                EVAL(new Mal.List(contents), env);
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return 1;
+            }
+        } else {
+            while (!eof) {
+                try {
+                    rep(env);
+                } catch (Mal.Error err) {
+                    GLib.stderr.printf("%s\n", err.message);
+                }
+            }
+        }
+        return 0;
+    }
+}
diff --git a/vala/step9_try.vala b/vala/step9_try.vala
new file mode 100644 (file)
index 0000000..1ccf3dd
--- /dev/null
@@ -0,0 +1,402 @@
+class Mal.BuiltinFunctionEval : Mal.BuiltinFunction {
+    public Mal.Env env;
+    public BuiltinFunctionEval(Mal.Env env_) { env = env_; }
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionEval(env);
+    }
+    public override string name() { return "eval"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return Mal.Main.EVAL(args.vs.data, env);
+    }
+}
+
+class Mal.Main : GLib.Object {
+    static bool eof;
+
+    static construct {
+        eof = false;
+    }
+
+    public static Mal.Val? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+
+            try {
+                return Reader.read_str(line);
+            } catch (Mal.Error err) {
+                Mal.BuiltinFunctionThrow.clear();
+                GLib.stderr.printf("%s\n", err.message);
+                return null;
+            }
+        } else {
+            stdout.printf("\n");
+            eof = true;
+            return null;
+        }
+    }
+
+    public static Mal.Val eval_ast(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.Sym)
+            return env.get(ast as Mal.Sym);
+        if (ast is Mal.List) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.List).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.List(results);
+        }
+        if (ast is Mal.Vector) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.Vector).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.Vector.from_list(results);
+        }
+        if (ast is Mal.Hashmap) {
+            var result = new Mal.Hashmap();
+            var map = (ast as Mal.Hashmap).vs;
+            foreach (var key in map.get_keys())
+                result.insert(key, EVAL(map[key], env));
+            return result;
+        }
+        return ast;
+    }
+
+    private static Mal.Val define_eval(Mal.Val key, Mal.Val value,
+                                       Mal.Env eval_env, Mal.Env def_env,
+                                       bool is_macro = false)
+    throws Mal.Error {
+        var symkey = key as Mal.Sym;
+        if (symkey == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "let*: expected a symbol to define");
+        var val = EVAL(value, eval_env);
+        if (val is Mal.Function)
+            (val as Mal.Function).is_macro = is_macro;
+        def_env.set(symkey, val);
+        return val;
+    }
+
+    public static Mal.Val quasiquote(Mal.Val ast)
+    throws Mal.Error {
+        if (!is_pair(ast)) {
+            var list = new GLib.List<Mal.Val>();
+            list.append(new Mal.Sym("quote"));
+            list.append(ast);
+            return new Mal.List(list);
+        }
+
+        var iter = (ast as Mal.Listlike).iter();
+        var first = iter.deref();
+        if (first is Mal.Sym && (first as Mal.Sym).v == "unquote") {
+            if (iter.step().empty())
+                throw new Mal.Error.BAD_PARAMS(
+                    "unquote: expected two values");
+            return iter.deref();
+        }
+
+        if (is_pair(first)) {
+            var fiter = (first as Mal.Listlike).iter();
+            var ffirst = fiter.deref();
+            if (ffirst is Mal.Sym &&
+                (ffirst as Mal.Sym).v == "splice-unquote") {
+                var list = new GLib.List<Mal.Val>();
+                list.append(new Mal.Sym("concat"));
+                if (fiter.step().empty())
+                    throw new Mal.Error.BAD_PARAMS(
+                        "unquote: expected two values");
+                list.append(fiter.deref());
+                var sublist = new GLib.List<Mal.Val>();
+                while (!iter.step().empty())
+                    sublist.append(iter.deref());
+                list.append(quasiquote(new Mal.List(sublist)));
+                return new Mal.List(list);
+            }
+        }
+
+        var list = new GLib.List<Mal.Val>();
+        list.append(new Mal.Sym("cons"));
+        list.append(quasiquote(first));
+        var sublist = new GLib.List<Mal.Val>();
+        while (!iter.step().empty())
+            sublist.append(iter.deref());
+        list.append(quasiquote(new Mal.List(sublist)));
+        return new Mal.List(list);
+    }
+
+    public static bool is_macro_call(Mal.Val v, Mal.Env env)
+    throws Mal.Error {
+        var list = v as Mal.List;
+        if (list == null || list.vs == null || !(list.vs.data is Mal.Sym))
+            return false;
+        try {
+            var fn = env.get(list.vs.data as Mal.Sym) as Mal.Function;
+            return (fn != null && fn.is_macro);
+        } catch (Mal.Error.ENV_LOOKUP_FAILED err) {
+            return false;
+        }
+    }
+
+    public static Mal.Val macroexpand(Mal.Val ast_, Mal.Env env)
+    throws Mal.Error {
+        // Copy the parameter into an owned variable (see comment in EVAL).
+        Mal.Val ast = ast_;
+        while (is_macro_call(ast, env)) {
+            var call = ast as Mal.List;
+            var macro = (env.get(call.vs.data as Mal.Sym) as Mal.Function);
+            var macroargs = new Mal.List(call.vs.copy());
+            macroargs.vs.remove_link(macroargs.vs.first());
+            var fnenv = new Mal.Env.funcall(
+                macro.env, macro.parameters, macroargs);
+            ast = Mal.Main.EVAL(macro.body, fnenv);
+        }
+        return ast;
+    }
+
+    public static Mal.Val EVAL(Mal.Val ast_, Mal.Env env_)
+    throws Mal.Error {
+        // Copy the implicitly 'unowned' function arguments into
+        // ordinary owned variables which increment the objects'
+        // reference counts. This is so that when we overwrite these
+        // variables within the loop (for TCO) the objects we assign
+        // into them don't immediately get garbage-collected.
+        Mal.Val ast = ast_;
+        Mal.Env env = env_;
+        while (true) {
+            ast = macroexpand(ast, env);
+            if (ast is Mal.List) {
+                unowned GLib.List<Mal.Val> list = (ast as Mal.List).vs;
+                if (list.first() == null)
+                    return ast;
+
+                var first = list.first().data;
+                if (first is Mal.Sym) {
+                    var sym = first as Mal.Sym;
+                    switch (sym.v) {
+                    case "def!":
+                    case "defmacro!":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "def!: expected two values");
+                        return define_eval(list.next.data, list.next.next.data,
+                                           env, env, sym.v == "defmacro!");
+                    case "let*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected two values");
+                        var defns = list.nth(1).data;
+                        env = new Mal.Env.within(env);
+
+                        if (defns is Mal.List) {
+                            for (unowned GLib.List<Mal.Val> iter =
+                                     (defns as Mal.List).vs;
+                                 iter != null; iter = iter.next.next) {
+                                if (iter.next == null)
+                                    throw new Mal.Error.BAD_PARAMS(
+                                        "let*: expected an even-length list" +
+                                        " of definitions");
+                                define_eval(iter.data, iter.next.data,
+                                            env, env);
+                            }
+                        } else if (defns is Mal.Vector) {
+                            var vec = (defns as Mal.Vector).vs;
+                            if (vec.length % 2 != 0)
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "let*: expected an even-length vector" +
+                                    " of definitions");
+                            for (var i = 0; i < vec.length; i += 2)
+                                define_eval(vec[i], vec[i+1], env, env);
+                        } else {
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected a list or vector of definitions");
+                        }
+                        ast = list.nth(2).data;
+                        continue;      // tail-call optimisation
+                    case "do":
+                        Mal.Val result = null;
+                        for (list = list.next; list != null; list = list.next)
+                            result = EVAL(list.data, env);
+                        if (result == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "do: expected at least one argument");
+                        return result;
+                    case "if":
+                        if (list.length() != 3 && list.length() != 4)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "if: expected two or three arguments");
+                        list = list.next;
+                        var cond = EVAL(list.data, env);
+                        list = list.next;
+                        if (!cond.truth_value()) {
+                            // Skip to the else clause, which defaults to nil.
+                            list = list.next;
+                            if (list == null)
+                                return new Mal.Nil();
+                        }
+                        ast = list.data;
+                        continue;      // tail-call optimisation
+                    case "fn*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected two arguments");
+                        var binds = list.next.data as Mal.Listlike;
+                        var body = list.next.next.data;
+                        if (binds == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected a list of parameter names");
+                        for (var iter = binds.iter(); iter.nonempty();
+                             iter.step())
+                            if (!(iter.deref() is Mal.Sym))
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "fn*: expected parameter name to be "+
+                                    "symbol");
+                        return new Mal.Function(binds, body, env);
+                    case "quote":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "quote: expected one argument");
+                        return list.next.data;
+                    case "quasiquote":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "quasiquote: expected one argument");
+                        ast = quasiquote(list.next.data);
+                        continue;      // tail-call optimisation
+                    case "macroexpand":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "macroexpand: expected one argument");
+                        return macroexpand(list.next.data, env);
+                    case "try*":
+                        if (list.length() != 2 && list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "try*: expected one or two arguments");
+                        var trybody = list.next.data;
+                        if (list.length() == 2) {
+                            // Trivial catchless form of try
+                            ast = trybody;
+                            continue;  // tail-call optimisation
+                        }
+                        var catchclause = list.next.next.data as Mal.List;
+                        if (!(catchclause.vs.data is Mal.Sym) ||
+                            (catchclause.vs.data as Mal.Sym).v != "catch*")
+                            throw new Mal.Error.BAD_PARAMS(
+                                "try*: expected catch*");
+                        if (catchclause.vs.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "catch*: expected two arguments");
+                        var catchparam = catchclause.vs.next.data as Mal.Sym;
+                        if (catchparam == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "catch*: expected a parameter name");
+                        var catchbody = catchclause.vs.next.next.data;
+                        try {
+                            return EVAL(trybody, env);
+                        } catch (Mal.Error exc) {
+                            var catchenv = new Mal.Env.within(env);
+                            catchenv.set(catchparam, Mal.BuiltinFunctionThrow.
+                                         thrown_value(exc));
+                            ast = catchbody;
+                            env = catchenv;
+                            continue;  // tail-call optimisation
+                        }
+                    }
+                }
+
+                var newlist = eval_ast(ast, env) as Mal.List;
+                unowned GLib.List<Mal.Val> firstlink = newlist.vs.first();
+                Mal.Val firstdata = firstlink.data;
+                newlist.vs.remove_link(firstlink);
+
+                if (firstdata is Mal.BuiltinFunction) {
+                    return (firstdata as Mal.BuiltinFunction).call(newlist);
+                } else if (firstdata is Mal.Function) {
+                    var fn = firstdata as Mal.Function;
+                    env = new Mal.Env.funcall(fn.env, fn.parameters, newlist);
+                    ast = fn.body;
+                    continue;      // tail-call optimisation
+                } else {
+                    throw new Mal.Error.CANNOT_APPLY(
+                        "bad value at start of list");
+                }
+            } else {
+                return eval_ast(ast, env);
+            }
+        }
+    }
+
+    public static void PRINT(Mal.Val value) {
+        stdout.printf("%s\n", pr_str(value));
+    }
+
+    public static void rep(Mal.Env env) throws Mal.Error {
+        Mal.Val? val = READ();
+        if (val != null) {
+            val = EVAL(val, env);
+            PRINT(val);
+        }
+    }
+
+    public static void setup(string line, Mal.Env env) {
+        try {
+            EVAL(Reader.read_str(line), env);
+        } catch (Mal.Error err) {
+            stderr.printf("Error during setup:\n%s\n-> %s\n",
+                          line, err.message);
+            GLib.Process.exit(1);
+        }
+    }
+
+    public static int main(string[] args) {
+        var env = new Mal.Env();
+
+        Mal.Core.make_ns();
+        foreach (var key in Mal.Core.ns.get_keys())
+            env.set(new Mal.Sym(key), Mal.Core.ns[key]);
+        env.set(new Mal.Sym("eval"), new Mal.BuiltinFunctionEval(env));
+
+        setup("(def! not (fn* (a) (if a false true)))", env);
+        setup("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", env);
+        setup("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))", env);
+        setup("(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))", env);
+
+        var ARGV = new GLib.List<Mal.Val>();
+        if (args.length > 1) {
+            for (int i = args.length - 1; i >= 2; i--)
+                ARGV.prepend(new Mal.String(args[i]));
+        }
+        env.set(new Mal.Sym("*ARGV*"), new Mal.List(ARGV));
+
+        if (args.length > 1) {
+            var contents = new GLib.List<Mal.Val>();
+            contents.prepend(new Mal.String(args[1]));
+            contents.prepend(new Mal.Sym("load-file"));
+            try {
+                EVAL(new Mal.List(contents), env);
+            } catch (Mal.Error.EXCEPTION_THROWN exc) {
+                GLib.stderr.printf(
+                    "uncaught exception: %s\n",
+                    pr_str(Mal.BuiltinFunctionThrow.thrown_value(exc)));
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return 1;
+            }
+        } else {
+            while (!eof) {
+                try {
+                    rep(env);
+                } catch (Mal.Error.EXCEPTION_THROWN exc) {
+                    GLib.stderr.printf(
+                        "uncaught exception: %s\n",
+                        pr_str(Mal.BuiltinFunctionThrow.thrown_value(exc)));
+                } catch (Mal.Error err) {
+                    GLib.stderr.printf("%s\n", err.message);
+                }
+            }
+        }
+        return 0;
+    }
+}
diff --git a/vala/stepA_mal.vala b/vala/stepA_mal.vala
new file mode 100644 (file)
index 0000000..54b0136
--- /dev/null
@@ -0,0 +1,406 @@
+class Mal.BuiltinFunctionEval : Mal.BuiltinFunction {
+    public Mal.Env env;
+    public BuiltinFunctionEval(Mal.Env env_) { env = env_; }
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.BuiltinFunctionEval(env);
+    }
+    public override string name() { return "eval"; }
+    public override Mal.Val call(Mal.List args) throws Mal.Error {
+        if (args.vs.length() != 1)
+            throw new Mal.Error.BAD_PARAMS("%s: expected one argument", name());
+        return Mal.Main.EVAL(args.vs.data, env);
+    }
+}
+
+class Mal.Main : GLib.Object {
+    static bool eof;
+
+    static construct {
+        eof = false;
+    }
+
+    public static Mal.Val? READ() {
+        string? line = Readline.readline("user> ");
+        if (line != null) {
+            if (line.length > 0)
+                Readline.History.add(line);
+
+            try {
+                return Reader.read_str(line);
+            } catch (Mal.Error err) {
+                Mal.BuiltinFunctionThrow.clear();
+                GLib.stderr.printf("%s\n", err.message);
+                return null;
+            }
+        } else {
+            stdout.printf("\n");
+            eof = true;
+            return null;
+        }
+    }
+
+    public static Mal.Val eval_ast(Mal.Val ast, Mal.Env env)
+    throws Mal.Error {
+        if (ast is Mal.Sym)
+            return env.get(ast as Mal.Sym);
+        if (ast is Mal.List) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.List).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.List(results);
+        }
+        if (ast is Mal.Vector) {
+            var results = new GLib.List<Mal.Val>();
+            foreach (var elt in (ast as Mal.Vector).vs)
+                results.append(EVAL(elt, env));
+            return new Mal.Vector.from_list(results);
+        }
+        if (ast is Mal.Hashmap) {
+            var result = new Mal.Hashmap();
+            var map = (ast as Mal.Hashmap).vs;
+            foreach (var key in map.get_keys())
+                result.insert(key, EVAL(map[key], env));
+            return result;
+        }
+        return ast;
+    }
+
+    private static Mal.Val define_eval(Mal.Val key, Mal.Val value,
+                                       Mal.Env eval_env, Mal.Env def_env,
+                                       bool is_macro = false)
+    throws Mal.Error {
+        var symkey = key as Mal.Sym;
+        if (symkey == null)
+            throw new Mal.Error.BAD_PARAMS(
+                "let*: expected a symbol to define");
+        var val = EVAL(value, eval_env);
+        if (val is Mal.Function)
+            (val as Mal.Function).is_macro = is_macro;
+        def_env.set(symkey, val);
+        return val;
+    }
+
+    public static Mal.Val quasiquote(Mal.Val ast)
+    throws Mal.Error {
+        if (!is_pair(ast)) {
+            var list = new GLib.List<Mal.Val>();
+            list.append(new Mal.Sym("quote"));
+            list.append(ast);
+            return new Mal.List(list);
+        }
+
+        var iter = (ast as Mal.Listlike).iter();
+        var first = iter.deref();
+        if (first is Mal.Sym && (first as Mal.Sym).v == "unquote") {
+            if (iter.step().empty())
+                throw new Mal.Error.BAD_PARAMS(
+                    "unquote: expected two values");
+            return iter.deref();
+        }
+
+        if (is_pair(first)) {
+            var fiter = (first as Mal.Listlike).iter();
+            var ffirst = fiter.deref();
+            if (ffirst is Mal.Sym &&
+                (ffirst as Mal.Sym).v == "splice-unquote") {
+                var list = new GLib.List<Mal.Val>();
+                list.append(new Mal.Sym("concat"));
+                if (fiter.step().empty())
+                    throw new Mal.Error.BAD_PARAMS(
+                        "unquote: expected two values");
+                list.append(fiter.deref());
+                var sublist = new GLib.List<Mal.Val>();
+                while (!iter.step().empty())
+                    sublist.append(iter.deref());
+                list.append(quasiquote(new Mal.List(sublist)));
+                return new Mal.List(list);
+            }
+        }
+
+        var list = new GLib.List<Mal.Val>();
+        list.append(new Mal.Sym("cons"));
+        list.append(quasiquote(first));
+        var sublist = new GLib.List<Mal.Val>();
+        while (!iter.step().empty())
+            sublist.append(iter.deref());
+        list.append(quasiquote(new Mal.List(sublist)));
+        return new Mal.List(list);
+    }
+
+    public static bool is_macro_call(Mal.Val v, Mal.Env env)
+    throws Mal.Error {
+        var list = v as Mal.List;
+        if (list == null || list.vs == null || !(list.vs.data is Mal.Sym))
+            return false;
+        try {
+            var fn = env.get(list.vs.data as Mal.Sym) as Mal.Function;
+            return (fn != null && fn.is_macro);
+        } catch (Mal.Error.ENV_LOOKUP_FAILED err) {
+            return false;
+        }
+    }
+
+    public static Mal.Val macroexpand(Mal.Val ast_, Mal.Env env)
+    throws Mal.Error {
+        // Copy the parameter into an owned variable (see comment in EVAL).
+        Mal.Val ast = ast_;
+        while (is_macro_call(ast, env)) {
+            var call = ast as Mal.List;
+            var macro = (env.get(call.vs.data as Mal.Sym) as Mal.Function);
+            var macroargs = new Mal.List(call.vs.copy());
+            macroargs.vs.remove_link(macroargs.vs.first());
+            var fnenv = new Mal.Env.funcall(
+                macro.env, macro.parameters, macroargs);
+            ast = Mal.Main.EVAL(macro.body, fnenv);
+        }
+        return ast;
+    }
+
+    public static Mal.Val EVAL(Mal.Val ast_, Mal.Env env_)
+    throws Mal.Error {
+        // Copy the implicitly 'unowned' function arguments into
+        // ordinary owned variables which increment the objects'
+        // reference counts. This is so that when we overwrite these
+        // variables within the loop (for TCO) the objects we assign
+        // into them don't immediately get garbage-collected.
+        Mal.Val ast = ast_;
+        Mal.Env env = env_;
+        while (true) {
+            ast = macroexpand(ast, env);
+            if (ast is Mal.List) {
+                unowned GLib.List<Mal.Val> list = (ast as Mal.List).vs;
+                if (list.first() == null)
+                    return ast;
+
+                var first = list.first().data;
+                if (first is Mal.Sym) {
+                    var sym = first as Mal.Sym;
+                    switch (sym.v) {
+                    case "def!":
+                    case "defmacro!":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "def!: expected two values");
+                        return define_eval(list.next.data, list.next.next.data,
+                                           env, env, sym.v == "defmacro!");
+                    case "let*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected two values");
+                        var defns = list.nth(1).data;
+                        env = new Mal.Env.within(env);
+
+                        if (defns is Mal.List) {
+                            for (unowned GLib.List<Mal.Val> iter =
+                                     (defns as Mal.List).vs;
+                                 iter != null; iter = iter.next.next) {
+                                if (iter.next == null)
+                                    throw new Mal.Error.BAD_PARAMS(
+                                        "let*: expected an even-length list" +
+                                        " of definitions");
+                                define_eval(iter.data, iter.next.data,
+                                            env, env);
+                            }
+                        } else if (defns is Mal.Vector) {
+                            var vec = (defns as Mal.Vector).vs;
+                            if (vec.length % 2 != 0)
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "let*: expected an even-length vector" +
+                                    " of definitions");
+                            for (var i = 0; i < vec.length; i += 2)
+                                define_eval(vec[i], vec[i+1], env, env);
+                        } else {
+                            throw new Mal.Error.BAD_PARAMS(
+                                "let*: expected a list or vector of definitions");
+                        }
+                        ast = list.nth(2).data;
+                        continue;      // tail-call optimisation
+                    case "do":
+                        Mal.Val result = null;
+                        for (list = list.next; list != null; list = list.next)
+                            result = EVAL(list.data, env);
+                        if (result == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "do: expected at least one argument");
+                        return result;
+                    case "if":
+                        if (list.length() != 3 && list.length() != 4)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "if: expected two or three arguments");
+                        list = list.next;
+                        var cond = EVAL(list.data, env);
+                        list = list.next;
+                        if (!cond.truth_value()) {
+                            // Skip to the else clause, which defaults to nil.
+                            list = list.next;
+                            if (list == null)
+                                return new Mal.Nil();
+                        }
+                        ast = list.data;
+                        continue;      // tail-call optimisation
+                    case "fn*":
+                        if (list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected two arguments");
+                        var binds = list.next.data as Mal.Listlike;
+                        var body = list.next.next.data;
+                        if (binds == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "fn*: expected a list of parameter names");
+                        for (var iter = binds.iter(); iter.nonempty();
+                             iter.step())
+                            if (!(iter.deref() is Mal.Sym))
+                                throw new Mal.Error.BAD_PARAMS(
+                                    "fn*: expected parameter name to be "+
+                                    "symbol");
+                        return new Mal.Function(binds, body, env);
+                    case "quote":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "quote: expected one argument");
+                        return list.next.data;
+                    case "quasiquote":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "quasiquote: expected one argument");
+                        ast = quasiquote(list.next.data);
+                        continue;      // tail-call optimisation
+                    case "macroexpand":
+                        if (list.length() != 2)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "macroexpand: expected one argument");
+                        return macroexpand(list.next.data, env);
+                    case "try*":
+                        if (list.length() != 2 && list.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "try*: expected one or two arguments");
+                        var trybody = list.next.data;
+                        if (list.length() == 2) {
+                            // Trivial catchless form of try
+                            ast = trybody;
+                            continue;  // tail-call optimisation
+                        }
+                        var catchclause = list.next.next.data as Mal.List;
+                        if (!(catchclause.vs.data is Mal.Sym) ||
+                            (catchclause.vs.data as Mal.Sym).v != "catch*")
+                            throw new Mal.Error.BAD_PARAMS(
+                                "try*: expected catch*");
+                        if (catchclause.vs.length() != 3)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "catch*: expected two arguments");
+                        var catchparam = catchclause.vs.next.data as Mal.Sym;
+                        if (catchparam == null)
+                            throw new Mal.Error.BAD_PARAMS(
+                                "catch*: expected a parameter name");
+                        var catchbody = catchclause.vs.next.next.data;
+                        try {
+                            return EVAL(trybody, env);
+                        } catch (Mal.Error exc) {
+                            var catchenv = new Mal.Env.within(env);
+                            catchenv.set(catchparam, Mal.BuiltinFunctionThrow.
+                                         thrown_value(exc));
+                            ast = catchbody;
+                            env = catchenv;
+                            continue;  // tail-call optimisation
+                        }
+                    }
+                }
+
+                var newlist = eval_ast(ast, env) as Mal.List;
+                unowned GLib.List<Mal.Val> firstlink = newlist.vs.first();
+                Mal.Val firstdata = firstlink.data;
+                newlist.vs.remove_link(firstlink);
+
+                if (firstdata is Mal.BuiltinFunction) {
+                    return (firstdata as Mal.BuiltinFunction).call(newlist);
+                } else if (firstdata is Mal.Function) {
+                    var fn = firstdata as Mal.Function;
+                    env = new Mal.Env.funcall(fn.env, fn.parameters, newlist);
+                    ast = fn.body;
+                    continue;      // tail-call optimisation
+                } else {
+                    throw new Mal.Error.CANNOT_APPLY(
+                        "bad value at start of list");
+                }
+            } else {
+                return eval_ast(ast, env);
+            }
+        }
+    }
+
+    public static void PRINT(Mal.Val value) {
+        stdout.printf("%s\n", pr_str(value));
+    }
+
+    public static void rep(Mal.Env env) throws Mal.Error {
+        Mal.Val? val = READ();
+        if (val != null) {
+            val = EVAL(val, env);
+            PRINT(val);
+        }
+    }
+
+    public static void setup(string line, Mal.Env env) {
+        try {
+            EVAL(Reader.read_str(line), env);
+        } catch (Mal.Error err) {
+            stderr.printf("Error during setup:\n%s\n-> %s\n",
+                          line, err.message);
+            GLib.Process.exit(1);
+        }
+    }
+
+    public static int main(string[] args) {
+        var env = new Mal.Env();
+
+        Mal.Core.make_ns();
+        foreach (var key in Mal.Core.ns.get_keys())
+            env.set(new Mal.Sym(key), Mal.Core.ns[key]);
+        env.set(new Mal.Sym("eval"), new Mal.BuiltinFunctionEval(env));
+        env.set(new Mal.Sym("*host-language*"), new Mal.String("vala"));
+
+        setup("(def! not (fn* (a) (if a false true)))", env);
+        setup("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", env);
+        setup("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))", env);
+        setup("(def! *gensym-counter* (atom 0))", env);
+        setup("(def! gensym (fn* [] (symbol (str \"G__\" (swap! *gensym-counter* (fn* [x] (+ 1 x)))))))", env);
+        setup("(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) (let* (condvar (gensym)) `(let* (~condvar ~(first xs)) (if ~condvar ~condvar (or ~@(rest xs)))))))))", env);
+
+        var ARGV = new GLib.List<Mal.Val>();
+        if (args.length > 1) {
+            for (int i = args.length - 1; i >= 2; i--)
+                ARGV.prepend(new Mal.String(args[i]));
+        }
+        env.set(new Mal.Sym("*ARGV*"), new Mal.List(ARGV));
+
+        if (args.length > 1) {
+            var contents = new GLib.List<Mal.Val>();
+            contents.prepend(new Mal.String(args[1]));
+            contents.prepend(new Mal.Sym("load-file"));
+            try {
+                EVAL(new Mal.List(contents), env);
+            } catch (Mal.Error.EXCEPTION_THROWN exc) {
+                GLib.stderr.printf(
+                    "uncaught exception: %s\n",
+                    pr_str(Mal.BuiltinFunctionThrow.thrown_value(exc)));
+            } catch (Mal.Error err) {
+                GLib.stderr.printf("%s\n", err.message);
+                return 1;
+            }
+        } else {
+            setup("(println (str \"Mal [\" *host-language* \"]\"))", env);
+            while (!eof) {
+                try {
+                    rep(env);
+                } catch (Mal.Error.EXCEPTION_THROWN exc) {
+                    GLib.stderr.printf(
+                        "uncaught exception: %s\n",
+                        pr_str(Mal.BuiltinFunctionThrow.thrown_value(exc)));
+                } catch (Mal.Error err) {
+                    GLib.stderr.printf("%s\n", err.message);
+                }
+            }
+        }
+        return 0;
+    }
+}
diff --git a/vala/types.vala b/vala/types.vala
new file mode 100644 (file)
index 0000000..babac43
--- /dev/null
@@ -0,0 +1,265 @@
+public errordomain Mal.Error {
+    BAD_TOKEN,
+    PARSE_ERROR,
+    HASH_KEY_TYPE_ERROR,
+    ENV_LOOKUP_FAILED,
+    BAD_PARAMS,
+    CANNOT_APPLY,
+    EXCEPTION_THROWN,
+    NOT_IMPLEMENTED_IN_THIS_STEP,
+}
+
+abstract class Mal.Val : GLib.Object {
+    public abstract bool truth_value();
+}
+
+abstract class Mal.Hashable : Mal.Val {
+    public string hashkey;
+    public static uint hash(Hashable h) { return str_hash(h.hashkey); }
+    public static bool equal(Hashable hl, Hashable hr) {
+        return hl.hashkey == hr.hashkey;
+    }
+}
+
+class Mal.Bool : Mal.Hashable {
+    public bool v;
+    public Bool(bool value) {
+        v = value;
+        hashkey = value ? "bt" : "bf";
+    }
+    public override bool truth_value() { return v; }
+}
+
+// Mal.Listlike is a subclass of Mal.Val which includes both lists and
+// vectors, and provides a common iterator API so that core functions
+// and special forms can treat them the same.
+//
+// Most core functions that take a list argument also accept nil. To
+// make that easy, Mal.Nil also derives from Mal.Listlike.
+abstract class Mal.Listlike : Mal.ValWithMetadata {
+    public abstract Mal.Iterator iter();
+    public GLib.List<Mal.Val> as_glib_list() {
+        var newlist = new GLib.List<Mal.Val>();
+        for (var it = iter(); it.nonempty(); it.step())
+            newlist.append(it.deref());
+        return newlist;
+    }
+}
+
+abstract class Mal.Iterator : GLib.Object {
+    public abstract Mal.Val? deref();
+    public abstract Mal.Iterator step();
+    public bool empty() { return deref() == null; }
+    public bool nonempty() { return deref() != null; }
+}
+
+// ValWithMetadata is a subclass of Mal.Val which includes every value
+// type you can put metadata on. Value types implementing this class
+// must provide a copy() method, because with-meta has to make a copy
+// of the value with new metadata.
+//
+// The subclass Mal.Listlike (including lists, vectors and nil)
+// descends from ValWithMetadata. This means there's a tricky moment
+// during construction where we initialise the metadata to nil by
+// default, which involves constructing a Mal.Nil, which is also a
+// ValWithMetadata! To avoid an infinite recursion, Mal.Nil has to
+// point to _itself_ as its default metadata, instead of a separately
+// constructed Mal.Nil.
+abstract class Mal.ValWithMetadata : Mal.Val {
+    public Mal.Val metadata;
+    construct {
+        if (this is Mal.Nil)
+            metadata = this;
+        else
+            metadata = new Mal.Nil();
+    }
+    public abstract Mal.ValWithMetadata copy();
+}
+
+class Mal.Nil : Mal.Listlike {
+    public override bool truth_value() { return false; }
+    public override Mal.Iterator iter() { return new Mal.NilIterator(); }
+    public override Mal.ValWithMetadata copy() { return new Mal.Nil(); }
+}
+
+class Mal.NilIterator : Mal.Iterator {
+    public override Mal.Val? deref() { return null; }
+    public override Mal.Iterator step() { return this; }
+}
+
+class Mal.List : Mal.Listlike {
+    public GLib.List<Val> vs;
+    public List(GLib.List<Val> values) {
+        foreach (var value in values) {
+            vs.append(value);
+        }
+    }
+    public List.empty() {
+    }
+    public override bool truth_value() { return true; }
+    public override Mal.Iterator iter() {
+        var toret = new Mal.ListIterator();
+        toret.node = vs;
+        return toret;
+    }
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.List(vs);
+    }        
+}
+
+class Mal.ListIterator : Mal.Iterator {
+    public unowned GLib.List<Mal.Val>? node;
+    public override Mal.Val? deref() {
+        return node == null ? null : node.data;
+    }
+    public override Mal.Iterator step() {
+        if (node != null)
+            node = node.next;
+        return this;
+    }
+}
+
+class Mal.Vector : Mal.Listlike {
+    public Val[] vs;
+    public Vector.from_list(GLib.List<Val> values) {
+        vs = new Val[values.length()];
+        int i = 0;
+        foreach (var value in values) {
+            vs[i++] = value;
+        }
+    }
+    public Vector.with_size(uint size) {
+        vs = new Val[size];
+    }
+    private Vector.copy_of(Vector v) {
+        vs = v.vs;
+    }
+    public override bool truth_value() { return true; }
+    public override Mal.Iterator iter() {
+        var toret = new Mal.VectorIterator();
+        toret.vec = this;
+        toret.pos = 0;
+        return toret;
+    }
+    public override Mal.ValWithMetadata copy() {
+        return new Mal.Vector.copy_of(this);
+    }        
+}
+
+class Mal.VectorIterator : Mal.Iterator {
+    public Mal.Vector vec;
+    public int pos;
+    public override Mal.Val? deref() {
+        return pos >= vec.vs.length ? null : vec.vs[pos];
+    }
+    public override Mal.Iterator step() {
+        if (pos < vec.vs.length) pos++;
+        return this;
+    }
+}
+
+class Mal.Num : Mal.Hashable {
+    public int64 v;
+    public Num(int64 value) {
+        v = value;
+        hashkey = "N" + v.to_string();
+    }
+    public override bool truth_value() { return true; }
+}
+
+abstract class Mal.SymBase : Mal.Hashable {
+    public string v;
+    public override bool truth_value() { return true; }
+}
+
+class Mal.Sym : Mal.SymBase {
+    public Sym(string value) {
+        v = value;
+        hashkey = "'" + v;
+    }
+}
+
+class Mal.Keyword : Mal.SymBase {
+    public Keyword(string value) {
+        v = value;
+        hashkey = ":" + v;
+    }
+}
+
+class Mal.String : Mal.Hashable {
+    public string v;
+    public String(string value) {
+        v = value;
+        hashkey = "\"" + v;
+    }
+    public override bool truth_value() { return true; }
+}
+
+class Mal.Hashmap : Mal.ValWithMetadata {
+    public GLib.HashTable<Mal.Hashable, Mal.Val> vs;
+    construct {
+        vs = new GLib.HashTable<Mal.Hashable, Mal.Val>(
+            Mal.Hashable.hash, Mal.Hashable.equal);
+    }
+    public void insert(Mal.Val key, Mal.Val value) throws Mal.Error {
+        var hkey = key as Mal.Hashable;
+        if (hkey == null)
+            throw new Error.HASH_KEY_TYPE_ERROR("bad type as hash key");
+        vs[hkey] = value;
+    }
+    public void remove(Mal.Val key) throws Mal.Error {
+        var hkey = key as Mal.Hashable;
+        if (hkey == null)
+            throw new Error.HASH_KEY_TYPE_ERROR("bad type as hash key");
+        vs.remove(hkey);
+    }
+    public override bool truth_value() { return true; }
+    public override Mal.ValWithMetadata copy() {
+        var toret = new Mal.Hashmap();
+        toret.vs = vs;
+        return toret;
+    }        
+}
+
+abstract class Mal.BuiltinFunction : Mal.ValWithMetadata {
+    public abstract string name();
+    public abstract Mal.Val call(Mal.List args) throws Mal.Error;
+    public override bool truth_value() { return true; }
+}
+
+class Mal.Function : Mal.ValWithMetadata {
+    public bool is_macro;
+#if !NO_ENV
+    public Mal.Listlike parameters;
+    public Mal.Val body;
+    public Mal.Env env;
+    public Function(Mal.Listlike parameters_, Mal.Val body_, Mal.Env env_) {
+        parameters = parameters_;
+        body = body_;
+        env = env_;
+        is_macro = false;
+    }
+#endif
+    public override Mal.ValWithMetadata copy() {
+#if !NO_ENV
+        var copied = new Mal.Function(parameters, body, env);
+        copied.is_macro = is_macro;
+        return copied;
+#else
+        throw new Mal.Error.NOT_IMPLEMENTED_IN_THIS_STEP(
+            "can't copy a Mal.Function without Mal.Env existing");
+#endif
+    }
+    public override bool truth_value() { return true; }
+}
+
+class Mal.Atom : Mal.Val {
+    public Mal.Val v;
+    public Atom(Mal.Val v_) { v = v_; }
+    public override bool truth_value() { return true; }
+}
+
+bool is_pair(Mal.Val v) {
+    var listlike = v as Mal.Listlike;
+    return listlike != null && listlike.iter().nonempty();
+}