D implementation
authorDov Murik <dov.murik@gmail.com>
Tue, 17 Nov 2015 03:17:13 +0000 (22:17 -0500)
committerDov Murik <dov.murik@gmail.com>
Wed, 2 Dec 2015 20:53:02 +0000 (15:53 -0500)
23 files changed:
.gitignore
Makefile
README.md
d/Dockerfile [new file with mode: 0644]
d/Makefile [new file with mode: 0644]
d/env.d [new file with mode: 0644]
d/main.di [new file with mode: 0644]
d/mal_core.d [new file with mode: 0644]
d/printer.d [new file with mode: 0644]
d/reader.d [new file with mode: 0644]
d/readline.d [new file with mode: 0644]
d/step0_repl.d [new file with mode: 0644]
d/step1_read_print.d [new file with mode: 0644]
d/step2_eval.d [new file with mode: 0644]
d/step3_env.d [new file with mode: 0644]
d/step4_if_fn_do.d [new file with mode: 0644]
d/step5_tco.d [new file with mode: 0644]
d/step6_file.d [new file with mode: 0644]
d/step7_quote.d [new file with mode: 0644]
d/step8_macros.d [new file with mode: 0644]
d/step9_try.d [new file with mode: 0644]
d/stepA_mal.d [new file with mode: 0644]
d/types.d [new file with mode: 0644]

index ebf1070..f0a8fdd 100644 (file)
@@ -6,6 +6,7 @@ bash/mal.sh
 c/mal
 coffee/mal.coffee
 crystal/mal
+d/mal
 haskell/mal
 js/mal.js
 js/web/mal.js
index 69dc20c..5e1efd8 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,7 @@ mal_TEST_OPTS = --start-timeout 60 --test-timeout 120
 # Settings
 #
 
-IMPLS = awk bash c clojure coffee cpp crystal cs erlang elixir es6 factor forth fsharp go groovy \
+IMPLS = awk bash c clojure coffee cpp crystal cs erlang elixir es6 factor forth fsharp go groovy \
        guile haskell java julia js kotlin lua make mal ocaml matlab miniMAL nim \
        perl php ps python r racket rpython ruby rust scala swift tcl vb vimscript
 
@@ -37,6 +37,7 @@ EXCLUDE_TESTS += test^bash^step5 # no stack exhaustion or completion
 EXCLUDE_TESTS += test^c^step5    # segfault
 EXCLUDE_TESTS += test^cpp^step5  # completes at 10,000
 EXCLUDE_TESTS += test^cs^step5   # fatal stack overflow fault
+EXCLUDE_TESTS += test^d^step5    # completes at 10,000, fatal stack overflow at 1,000,000
 EXCLUDE_TESTS += test^erlang^step5 # erlang is TCO, test passes
 EXCLUDE_TESTS += test^elixir^step5 # elixir is TCO, test passes
 EXCLUDE_TESTS += test^fsharp^step5 # completes at 10,000, fatal stack overflow at 100,000
@@ -65,6 +66,7 @@ STEP_TEST_FILES = $(strip $(wildcard $(1)/tests/$($(2)).mal) $(wildcard tests/$(
 awk_STEP_TO_PROG =     awk/$($(1)).awk
 bash_STEP_TO_PROG =    bash/$($(1)).sh
 c_STEP_TO_PROG =       c/$($(1))
+d_STEP_TO_PROG =       d/$($(1))
 clojure_STEP_TO_PROG = clojure/src/$($(1)).clj
 coffee_STEP_TO_PROG =  coffee/$($(1)).coffee
 cpp_STEP_TO_PROG =     cpp/$($(1))
@@ -115,6 +117,7 @@ export FACTOR_ROOTS := .
 awk_RUNSTEP =     awk -O -f ../$(2) $(3)
 bash_RUNSTEP =    bash ../$(2) $(3)
 c_RUNSTEP =       ../$(2) $(3)
+d_RUNSTEP =       ../$(2) $(3)
 clojure_RUNSTEP = lein with-profile +$(1) trampoline run $(3)
 coffee_RUNSTEP =  coffee ../$(2) $(3)
 cpp_RUNSTEP =     ../$(2) $(3)
index 4564bad..3aeeadc 100644 (file)
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 Mal is a Clojure inspired Lisp interpreter.
 
-Mal is implemented in 43 different languages:
+Mal is implemented in 44 different languages:
 
 * GNU awk
 * Bash shell
@@ -16,6 +16,7 @@ Mal is implemented in 43 different languages:
 * Clojure
 * CoffeeScript
 * Crystal
+* D
 * Elixir
 * Erlang
 * ES6 (ECMAScript 6 / ECMAScript 2015)
@@ -175,6 +176,19 @@ make   # needed to run tests
 ./stepX_YYY
 ```
 
+### D
+
+*The D implementation was created by [Dov Murik](https://github.com/dubek)*
+
+The D implementation of mal was tested with GDC 4.8.  It requires the GNU
+readline library.
+
+```
+cd d
+make
+./stepX_YYY
+```
+
 ### Elixir
 
 *The Elixir implementation was created by [Martin Ek (ekmartin)](https://github.com/ekmartin)*
diff --git a/d/Dockerfile b/d/Dockerfile
new file mode 100644 (file)
index 0000000..a11d7ab
--- /dev/null
@@ -0,0 +1,26 @@
+FROM ubuntu:vivid
+MAINTAINER Joel Martin <github@martintribe.org>
+
+##########################################################
+# General requirements for testing or common across many
+# implementations
+##########################################################
+
+RUN apt-get -y update
+
+# Required for running tests
+RUN apt-get -y install make python
+
+# Some typical implementation and test requirements
+RUN apt-get -y install curl libreadline-dev libedit-dev
+
+RUN mkdir -p /mal
+WORKDIR /mal
+
+##########################################################
+# Specific implementation requirements
+##########################################################
+
+RUN apt-get -y install gdc
+
+ENV HOME /mal
diff --git a/d/Makefile b/d/Makefile
new file mode 100644 (file)
index 0000000..77594c4
--- /dev/null
@@ -0,0 +1,57 @@
+CFLAGS += -g -O2 -Wall
+LDFLAGS += -lreadline
+
+#####################
+
+TESTS =
+
+SOURCES_BASE = readline.d types.d reader.d printer.d
+SOURCES_LISP = env.d mal_core.d stepA_mal.d
+SOURCES = $(SOURCES_BASE) $(SOURCES_LISP)
+
+#####################
+
+EARLY_SRCS = step0_repl.d step1_read_print.d step2_eval.d
+LATE_SRCS = step3_env.d step4_if_fn_do.d step5_tco.d step6_file.d \
+           step7_quote.d step8_macros.d step9_try.d stepA_mal.d
+SRCS = $(EARLY_SRCS) $(LATE_SRCS)
+OBJS = $(SRCS:%.d=%.o)
+BINS = $(OBJS:%.o=%)
+EARLY_OBJS = types.o readline.o reader.o printer.o env.o
+OTHER_OBJS = $(EARLY_OBJS) mal_core.o
+EARLY_STEPS_BINS = $(EARLY_SRCS:%.d=%)
+LATE_STEPS_BINS = $(LATE_SRCS:%.d=%)
+
+#####################
+
+all: $(BINS) mal
+
+mal: $(word $(words $(BINS)),$(BINS))
+       cp $< $@
+
+$(OBJS) $(OTHER_OBJS): %.o: %.d
+       gdc $(CFLAGS) -c $(@:%.o=%.d) -o $@
+
+$(EARLY_STEPS_BINS): $(EARLY_OBJS)
+$(LATE_STEPS_BINS): $(OTHER_OBJS)
+
+$(BINS): %: %.o
+       gdc $+ -o $@ $(LDFLAGS)
+
+clean:
+       rm -f $(OBJS) $(BINS) $(OTHER_OBJS) mal
+
+.PHONY: stats stats-lisp tests $(TESTS)
+
+stats: $(SOURCES)
+       @wc $^
+       @printf "%5s %5s %5s %s\n" `grep -E "^[[:space:]]*//|^[[:space:]]*$$" $^ | wc` "[comments/blanks]"
+stats-lisp: $(SOURCES_LISP)
+       @wc $^
+       @printf "%5s %5s %5s %s\n" `grep -E "^[[:space:]]*//|^[[:space:]]*$$" $^ | wc` "[comments/blanks]"
+
+tests: $(TESTS)
+
+$(TESTS):
+       @echo "Running $@"; \
+       ./$@ || exit 1; \
diff --git a/d/env.d b/d/env.d
new file mode 100644 (file)
index 0000000..d2e341b
--- /dev/null
+++ b/d/env.d
@@ -0,0 +1,53 @@
+import types;
+
+class Env {
+    Env outer;
+    MalType[MalSymbol] data;
+
+    this(Env outer_v, MalType[] binds = [], MalType[] exprs = [])
+    {
+        outer = outer_v;
+        foreach (int i, MalType b; binds)
+        {
+            auto arg_name = verify_cast!MalSymbol(b);
+            if (arg_name.name == "&")
+            {
+                auto rest_arg_name = verify_cast!MalSymbol(binds[i + 1]);
+                auto rest_exprs = new MalList(exprs[i..$]);
+                set(rest_arg_name, rest_exprs);
+                break;
+            }
+            else
+            {
+                set(arg_name, exprs[i]);
+            }
+        }
+    }
+
+    MalType set(MalSymbol key, MalType val)
+    {
+        data[key] = val;
+        return val;
+    }
+
+    Env find(MalSymbol key)
+    {
+        auto val = (key in data);
+        if (val !is null) {
+            return this;
+        } else if (outer is null) {
+            return null;
+        } else {
+            return outer.find(key);
+        }
+    }
+
+    MalType get(MalSymbol key)
+    {
+        auto found = find(key);
+        if (found is null) {
+            throw new Exception("'" ~ key.print(true) ~ "' not found");
+        }
+        return found.data[key];
+    }
+}
diff --git a/d/main.di b/d/main.di
new file mode 100644 (file)
index 0000000..15aac77
--- /dev/null
+++ b/d/main.di
@@ -0,0 +1,4 @@
+import types : MalType;
+import env : Env;
+
+MalType EVAL(MalType ast, Env env);
diff --git a/d/mal_core.d b/d/mal_core.d
new file mode 100644 (file)
index 0000000..4a4b9fc
--- /dev/null
@@ -0,0 +1,380 @@
+import core.time;
+import std.algorithm;
+import std.array;
+import std.datetime;
+import std.file;
+import std.stdio;
+import env;
+import main;
+import reader;
+import readline;
+import types;
+import printer;
+
+static MalType mal_equal(MalType[] a ...)
+{
+    verify_args_count(a, 2);
+    return bool_to_mal(a[0] == a[1]);
+}
+
+static MalType mal_throw(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    throw new MalException(a[0]);
+}
+
+static MalType mal_symbol(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    auto s = verify_cast!MalString(a[0]);
+    return new MalSymbol(s.val);
+}
+
+static MalType mal_keyword(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    auto s = verify_cast!MalString(a[0]);
+    return new MalString("\u029e" ~ s.val);
+}
+
+static MalType mal_keyword_q(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    auto s = cast(MalString) a[0];
+    if (s is null) return mal_false;
+    return bool_to_mal(s.is_keyword());
+}
+
+static MalType mal_pr_str(MalType[] a ...)
+{
+    auto items_strs = a.map!(e => pr_str(e, true));
+    return new MalString(array(items_strs).join(" "));
+}
+
+static MalType mal_str(MalType[] a ...)
+{
+    auto items_strs = a.map!(e => pr_str(e, false));
+    return new MalString(array(items_strs).join(""));
+}
+
+static MalType mal_prn(MalType[] a ...)
+{
+    auto items_strs = a.map!(e => pr_str(e, true));
+    writeln(array(items_strs).join(" "));
+    return mal_nil;
+}
+
+static MalType mal_println(MalType[] a ...)
+{
+    auto items_strs = a.map!(e => pr_str(e, false));
+    writeln(array(items_strs).join(" "));
+    return mal_nil;
+}
+
+static MalType mal_read_string(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    auto s = verify_cast!MalString(a[0]);
+    return read_str(s.val);
+}
+
+static MalType mal_readline(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    auto s = verify_cast!MalString(a[0]);
+    auto line = _readline(s.val);
+    return line is null ? mal_nil : new MalString(line);
+}
+
+static MalType mal_slurp(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    auto filename = verify_cast!MalString(a[0]).val;
+    auto content = cast(string) std.file.read(filename);
+    return new MalString(content);
+}
+
+alias TwoIntFunc = MalType function(long x, long y);
+
+MalType binary_int_op(TwoIntFunc f, MalType[] a ...)
+{
+    verify_args_count(a, 2);
+    MalInteger i0 = verify_cast!MalInteger(a[0]);
+    MalInteger i1 = verify_cast!MalInteger(a[1]);
+    return f(i0.val, i1.val);
+}
+
+static MalType mal_time_ms(MalType[] a ...)
+{
+    immutable epoch = SysTime(unixTimeToStdTime(0));
+    immutable hnsecs_since_epoch = Clock.currTime(UTC()) - epoch;
+    immutable ms = hnsecs_since_epoch.total!"msecs"();
+    return new MalInteger(ms);
+}
+
+static bool is_nil(MalType v)
+{
+    return cast(MalNil)(v) !is null;
+}
+
+static MalType mal_assoc(MalType[] a ...)
+{
+    verify_min_args_count(a, 1);
+    auto hm = verify_cast!MalHashmap(a[0]);
+    auto new_hm = new MalHashmap(hm.data.dup);
+    new_hm.put_kv_list(a[1..$]);
+    return new_hm;
+}
+
+static MalType mal_dissoc(MalType[] a ...)
+{
+    verify_min_args_count(a, 1);
+    auto hm = verify_cast!MalHashmap(a[0]);
+    auto new_hm = new MalHashmap(hm.data.dup);
+    foreach (k; a[1..$])
+    {
+        new_hm.remove(k);
+    }
+    return new_hm;
+}
+
+static MalType mal_get(MalType[] a ...)
+{
+    verify_args_count(a, 2);
+    if (is_nil(a[0])) return mal_nil;
+    auto hm = verify_cast!MalHashmap(a[0]);
+    return hm.get(a[1]);
+}
+
+static MalType mal_contains_q(MalType[] a ...)
+{
+    verify_args_count(a, 2);
+    if (is_nil(a[0])) return mal_false;
+    auto hm = verify_cast!MalHashmap(a[0]);
+    return bool_to_mal(hm.contains(a[1]));
+}
+
+static MalType mal_keys(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    auto hm = verify_cast!MalHashmap(a[0]);
+    auto keys = hm.data.keys.map!(s => cast(MalType)(new MalString(s)));
+    return new MalList(array(keys));
+}
+
+static MalType mal_vals(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    auto hm = verify_cast!MalHashmap(a[0]);
+    return new MalList(hm.data.values);
+}
+
+static MalType mal_cons(MalType[] a ...)
+{
+    verify_args_count(a, 2);
+    auto lst = verify_cast!MalSequential(a[1]);
+    return new MalList([a[0]] ~ lst.elements);
+}
+
+static MalType mal_concat(MalType[] a ...)
+{
+    MalType[] res;
+    foreach (e; a)
+    {
+        auto lst = verify_cast!MalSequential(e);
+        res ~= lst.elements;
+    }
+    return new MalList(res);
+}
+
+static MalType mal_nth(MalType[] a ...)
+{
+    verify_args_count(a, 2);
+    if (is_nil(a[0]))
+    {
+        throw new Exception("nth: index out of range");
+    }
+    auto seq = verify_cast!MalSequential(a[0]);
+    auto index = verify_cast!MalInteger(a[1]).val;
+    if (index >= seq.elements.length)
+    {
+        throw new Exception("nth: index out of range");
+    }
+    return seq.elements[index];
+}
+
+static MalType mal_first(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    if (is_nil(a[0])) return mal_nil;
+    auto seq = verify_cast!MalSequential(a[0]);
+    if (seq.elements.length == 0) return mal_nil;
+    return seq.elements[0];
+}
+
+static MalType mal_rest(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    if (is_nil(a[0])) return new MalList([]);
+    auto seq = verify_cast!MalSequential(a[0]);
+    if (seq.elements.length == 0) return new MalList([]);
+    return new MalList(seq.elements[1..$]);
+}
+
+
+static MalType mal_empty_q(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    if (is_nil(a[0]))
+    {
+        return mal_true;
+    }
+    auto s = verify_cast!MalSequential(a[0]);
+    return bool_to_mal(s.elements.length == 0);
+}
+
+static MalType mal_count(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    if (is_nil(a[0]))
+    {
+        return new MalInteger(0);
+    }
+    auto s = verify_cast!MalSequential(a[0]);
+    return new MalInteger(cast(int)(s.elements.length));
+}
+
+static MalType mal_apply(MalType[] a ...)
+{
+    verify_min_args_count(a, 2);
+    auto last_seq_elems = verify_cast!MalSequential(a[$-1]).elements;
+    auto funcargs = a.length == 2 ? last_seq_elems : (a[1..$-1] ~ last_seq_elems);
+
+    auto builtinfn = cast(MalBuiltinFunc) a[0];
+    if (builtinfn !is null)
+    {
+        return builtinfn.fn(funcargs);
+    }
+
+    auto malfunc = verify_cast!MalFunc(a[0]);
+    auto callenv = new Env(malfunc.def_env, malfunc.arg_names, funcargs);
+
+    return EVAL(malfunc.func_body, callenv);
+}
+
+static MalType mal_map(MalType[] a ...)
+{
+    verify_args_count(a, 2);
+    auto seq = verify_cast!MalSequential(a[1]);
+    auto mapped_items = seq.elements.map!(e => mal_apply(a[0], new MalList([e])));
+    return new MalList(array(mapped_items));
+}
+
+static MalType mal_conj(MalType[] a ...)
+{
+    verify_min_args_count(a, 1);
+    auto seq = verify_cast!MalSequential(a[0]);
+    return reduce!((s,e) => s.conj(e))(seq, a[1..$]);
+}
+
+static MalType mal_meta(MalType[] a ...)
+{
+    verify_args_count(a, 1);
+    auto metaobj = cast(MalMeta) a[0];
+    if (metaobj is null) return mal_nil;
+    return metaobj.meta();
+}
+
+static MalType mal_with_meta(MalType[] a ...)
+{
+    verify_args_count(a, 2);
+    auto metaobj = cast(MalMeta) a[0];
+    if (metaobj is null) return a[0];
+    return metaobj.with_meta(a[1]);
+}
+
+static MalType mal_reset_bang(MalType[] a ...)
+{
+    verify_args_count(a, 2);
+    verify_cast!MalAtom(a[0]).val = a[1];
+    return a[1];
+}
+
+static MalType mal_swap_bang(MalType[] a ...)
+{
+    verify_min_args_count(a, 2);
+    auto atom = verify_cast!MalAtom(a[0]);
+    auto args = [atom.val] ~ a[2..$];
+    auto newval = mal_apply([a[1], new MalList(args)]);
+    return mal_reset_bang([atom, newval]);
+}
+
+BuiltinStaticFuncType[string] core_ns;
+
+static this()
+{
+    core_ns = [
+        "=":        &mal_equal,
+        "throw":    &mal_throw,
+
+        "nil?":     (a ...) => mal_type_q!MalNil(a),
+        "true?":    (a ...) => mal_type_q!MalTrue(a),
+        "false?":   (a ...) => mal_type_q!MalFalse(a),
+        "symbol":   &mal_symbol,
+        "symbol?":  (a ...) => mal_type_q!MalSymbol(a),
+        "keyword":  &mal_keyword,
+        "keyword?": &mal_keyword_q,
+
+        "pr-str":   &mal_pr_str,
+        "str":      &mal_str,
+        "prn":      &mal_prn,
+        "println":  &mal_println,
+        "read-string": &mal_read_string,
+        "readline": &mal_readline,
+        "slurp":    &mal_slurp,
+
+        "<":        (a ...) => binary_int_op((x,y) => bool_to_mal(x < y), a),
+        "<=":       (a ...) => binary_int_op((x,y) => bool_to_mal(x <= y), a),
+        ">":        (a ...) => binary_int_op((x,y) => bool_to_mal(x > y), a),
+        ">=":       (a ...) => binary_int_op((x,y) => bool_to_mal(x >= y), a),
+        "+":        (a ...) => binary_int_op((x,y) => new MalInteger(x + y), a),
+        "-":        (a ...) => binary_int_op((x,y) => new MalInteger(x - y), a),
+        "*":        (a ...) => binary_int_op((x,y) => new MalInteger(x * y), a),
+        "/":        (a ...) => binary_int_op((x,y) => new MalInteger(x / y), a),
+        "time-ms":  &mal_time_ms,
+
+        "list":     (a ...) => new MalList(a),
+        "list?":    (a ...) => mal_type_q!MalList(a),
+        "vector":   (a ...) => new MalVector(a),
+        "vector?":  (a ...) => mal_type_q!MalVector(a),
+        "hash-map": (a ...) => new MalHashmap(a),
+        "map?":     (a ...) => mal_type_q!MalHashmap(a),
+        "assoc":    &mal_assoc,
+        "dissoc":   &mal_dissoc,
+        "get":      &mal_get,
+        "contains?": &mal_contains_q,
+        "keys":     &mal_keys,
+        "vals":     &mal_vals,
+
+        "sequential?": (a ...) => mal_type_q!MalSequential(a),
+        "cons":     &mal_cons,
+        "concat":   &mal_concat,
+        "nth":      &mal_nth,
+        "first":    &mal_first,
+        "rest":     &mal_rest,
+        "empty?":   &mal_empty_q,
+        "count":    &mal_count,
+        "apply":    &mal_apply,
+        "map":      &mal_map,
+
+        "conj":     &mal_conj,
+
+        "meta":     &mal_meta,
+        "with-meta": &mal_with_meta,
+        "atom":     (a ...) => new MalAtom(verify_args_count(a, 1)[0]),
+        "atom?":    (a ...) => mal_type_q!MalAtom(a),
+        "deref":    (a ...) => verify_cast!MalAtom(verify_args_count(a, 1)[0]).val,
+        "reset!":   &mal_reset_bang,
+        "swap!":    &mal_swap_bang
+    ];
+}
diff --git a/d/printer.d b/d/printer.d
new file mode 100644 (file)
index 0000000..ed2da24
--- /dev/null
@@ -0,0 +1,6 @@
+import types;
+
+string pr_str(MalType obj, bool readable = true)
+{
+    return obj.print(readable);
+}
diff --git a/d/reader.d b/d/reader.d
new file mode 100644 (file)
index 0000000..9cd3965
--- /dev/null
@@ -0,0 +1,182 @@
+import std.array;
+import std.regex;
+import std.stdio;
+import types;
+
+MalSymbol sym_quote;
+MalSymbol sym_quasiquote;
+MalSymbol sym_unquote;
+MalSymbol sym_splice_unquote;
+MalSymbol sym_deref;
+MalSymbol sym_with_meta;
+
+static this()
+{
+    sym_quote = new MalSymbol("quote");
+    sym_quasiquote = new MalSymbol("quasiquote");
+    sym_unquote = new MalSymbol("unquote");
+    sym_splice_unquote = new MalSymbol("splice-unquote");
+    sym_deref = new MalSymbol("deref");
+    sym_with_meta = new MalSymbol("with-meta");
+}
+
+class Reader
+{
+    int pos = 0;
+    const string[] tokens;
+
+    this(string[] the_tokens)
+    {
+        tokens = the_tokens.dup;
+    }
+
+    string peek()
+    {
+        if (pos >= tokens.length) return null;
+        return tokens[pos];
+    }
+
+    string next()
+    {
+        auto token = peek();
+        pos++;
+        return token;
+    }
+}
+
+auto tokenize_ctr = ctRegex!(r"[\s,]*(~@|[\[\]{}()'`~^@]|" `"` `(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"` r"`,;)]*)");
+
+string[] tokenize(string str)
+{
+    string[] tokens;
+    foreach(c; matchAll(str, tokenize_ctr))
+    {
+        auto token = c[1];
+        if (token.length == 0) continue;
+        if (token[0] == ';') continue;
+        tokens ~= token;
+    }
+    return tokens;
+}
+
+MalString parse_string(string token)
+{
+    string unescaped = 
+        token[1..$-1] // Remove surrounding quotes
+        .replace("\\n", "\n")
+        .replace("\\\"", "\"")
+        .replace("\\\\", "\\");
+    return new MalString(unescaped);
+}
+
+auto integer_ctr = ctRegex!(r"^-?[0-9]+$");
+
+MalType read_atom(Reader reader)
+{
+    auto token = reader.next();
+    switch (token)
+    {
+        case "nil": return mal_nil;
+        case "false": return mal_false;
+        case "true": return mal_true;
+        default:
+            switch (token[0]) {
+                case ':':
+                    return new MalString("\u029e" ~ token[1..$]);
+                case '"':
+                    return parse_string(token);
+                default:
+                    auto captures = matchFirst(token, integer_ctr);
+                    if (!captures.empty())
+                    {
+                        return new MalInteger(token);
+                    }
+
+                    return new MalSymbol(token);
+            }
+    }
+}
+
+MalType[] read_items(Reader reader, string start, string end)
+{
+    auto open_paren = reader.next();
+    if (open_paren != start) throw new Exception("expected '" ~ start ~ "'");
+
+    string token;
+    MalType[] res;
+    while ((token = reader.peek()) != end)
+    {
+        if (token is null)
+        {
+            throw new Exception("expected '" ~ end ~ "'");
+        }
+        res ~= read_form(reader);
+    }
+    reader.next(); // consume the ')'
+    return res;
+}
+
+MalList read_list(Reader reader)
+{
+    return new MalList(read_items(reader, "(", ")"));
+}
+
+MalVector read_vector(Reader reader)
+{
+    return new MalVector(read_items(reader, "[", "]"));
+}
+
+MalHashmap read_hashmap(Reader reader)
+{
+    return new MalHashmap(read_items(reader, "{", "}"));
+}
+
+MalList read_quote_shortcut(Reader reader, MalSymbol sym)
+{
+    reader.next(); // consume the special quote char
+    return new MalList([sym, read_form(reader)]);
+}
+
+MalType read_form(Reader reader)
+{
+    auto token = reader.peek();
+    if (token is null) return new MalNil();
+    switch(token)
+    {
+        case "'":
+            return read_quote_shortcut(reader, sym_quote);
+        case "`":
+            return read_quote_shortcut(reader, sym_quasiquote);
+        case "~":
+            return read_quote_shortcut(reader, sym_unquote);
+        case "~@":
+            return read_quote_shortcut(reader, sym_splice_unquote);
+        case "@":
+            return read_quote_shortcut(reader, sym_deref);
+        case "^":
+            reader.next(); // consume the caret char
+            auto meta = read_form(reader);
+            return new MalList([sym_with_meta, read_form(reader), meta]);
+        case "(":
+            return read_list(reader);
+        case ")":
+            throw new Exception("unexpected ')'");
+        case "[":
+            return read_vector(reader);
+        case "]":
+            throw new Exception("unexpected ']'");
+        case "{":
+            return read_hashmap(reader);
+        case "}":
+            throw new Exception("unexpected '}'");
+        default:
+            return read_atom(reader);
+    }
+}
+
+MalType read_str(string str)
+{
+    auto tokens = tokenize(str);
+    auto reader = new Reader(tokens);
+    return read_form(reader);
+}
diff --git a/d/readline.d b/d/readline.d
new file mode 100644 (file)
index 0000000..1907def
--- /dev/null
@@ -0,0 +1,56 @@
+import std.string;
+import std.path;
+
+// readline/readline.h
+extern (C) char* readline(const char* prompt);
+
+// readline/history.h
+extern (C) void using_history();
+extern (C) void add_history(const char *line);
+extern (C) int read_history(const char *filename);
+extern (C) int append_history(int nelement, const char *filename);
+
+bool history_loaded = false;
+const string history_file = "~/.mal-history";
+
+void load_history()
+{
+    if (history_loaded) return;
+    using_history();
+    string hf = expandTilde(history_file);
+    std.file.append(hf, ""); // Create the file if needed
+    read_history(toStringz(hf));
+    history_loaded = true;
+}
+
+void append_to_history()
+{
+    string hf = expandTilde(history_file);
+    append_history(1, toStringz(hf));
+}
+
+// Convert from C-string to D-string (making a copy)
+pure string fromCstr(char* cstr)
+{
+    auto len = core.stdc.string.strlen(cstr);
+    if (len == 0) return "";
+    string line = cstr[0..len].dup;
+    return line;
+}
+
+string _readline(in string prompt)
+{
+    load_history();
+
+    auto cstr = readline(toStringz(prompt));
+    if (cstr is null) return null;
+    scope(exit) { core.stdc.stdlib.free(cstr); }
+
+    if (cstr[0] != '\0')
+    {
+        add_history(cstr);   // Add input to in-memory history
+        append_to_history(); // Flush new line of history to disk
+    }
+
+    return fromCstr(cstr);
+}
diff --git a/d/step0_repl.d b/d/step0_repl.d
new file mode 100644 (file)
index 0000000..475dcca
--- /dev/null
@@ -0,0 +1,35 @@
+import std.stdio;
+import std.string;
+import readline;
+
+string READ(string str)
+{
+    return str;
+}
+
+string EVAL(string ast)
+{
+    return ast;
+}
+
+string PRINT(string ast)
+{
+    return ast;
+}
+
+string rep(string str)
+{
+    return PRINT(EVAL(READ(str)));
+}
+
+void main()
+{
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        writeln(rep(line));
+    }
+    writeln("");
+}
diff --git a/d/step1_read_print.d b/d/step1_read_print.d
new file mode 100644 (file)
index 0000000..1f73ec9
--- /dev/null
@@ -0,0 +1,45 @@
+import std.stdio;
+import std.string;
+import readline;
+import reader;
+import printer;
+import types;
+
+MalType READ(string str)
+{
+    return read_str(str);
+}
+
+MalType EVAL(MalType ast)
+{
+    return ast;
+}
+
+string PRINT(MalType ast)
+{
+    return pr_str(ast);
+}
+
+string rep(string str)
+{
+    return PRINT(EVAL(READ(str)));
+}
+
+void main()
+{
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        try
+        {
+            writeln(rep(line));
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+        }
+    }
+    writeln("");
+}
diff --git a/d/step2_eval.d b/d/step2_eval.d
new file mode 100644 (file)
index 0000000..94ff384
--- /dev/null
@@ -0,0 +1,132 @@
+import std.algorithm;
+import std.array;
+import std.stdio;
+import std.string;
+import readline;
+import reader;
+import printer;
+import types;
+
+alias Env = MalType[string];
+
+MalType READ(string str)
+{
+    return read_str(str);
+}
+
+MalType eval_ast(MalType ast, Env env)
+{
+    if (typeid(ast) == typeid(MalSymbol))
+    {
+        MalSymbol sym = verify_cast!MalSymbol(ast);
+        auto v = (sym.name in env);
+        if (v is null) throw new Exception("'" ~ sym.name ~ "' not found");
+        return *v;
+    }
+    else if (typeid(ast) == typeid(MalList))
+    {
+        auto lst = verify_cast!MalList(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalList(el);
+    }
+    else if (typeid(ast) == typeid(MalVector))
+    {
+        auto lst = verify_cast!MalVector(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalVector(el);
+    }
+    else if (typeid(ast) == typeid(MalHashmap))
+    {
+        auto hm = verify_cast!MalHashmap(ast);
+        typeof(hm.data) new_data;
+        foreach (string k, MalType v; hm.data)
+        {
+            new_data[k] = EVAL(v, env);
+        }
+        return new MalHashmap(new_data);
+    }
+    else
+    {
+        return ast;
+    }
+}
+
+MalType EVAL(MalType ast, Env env)
+{
+    if (typeid(ast) != typeid(MalList))
+    {
+        return eval_ast(ast, env);
+    }
+
+    auto el = verify_cast!MalList(eval_ast(ast, env));
+    auto fobj = verify_cast!MalBuiltinFunc(el.elements[0]);
+    auto args = el.elements[1..$];
+    return fobj.fn(args);
+}
+
+string PRINT(MalType ast)
+{
+    return pr_str(ast);
+}
+
+string rep(string str, Env env)
+{
+    return PRINT(EVAL(READ(str), env));
+}
+
+static MalType mal_add(MalType[] a ...)
+{
+     verify_args_count(a, 2);
+     MalInteger i0 = verify_cast!MalInteger(a[0]);
+     MalInteger i1 = verify_cast!MalInteger(a[1]);
+     return new MalInteger(i0.val + i1.val);
+}
+
+static MalType mal_sub(MalType[] a ...)
+{
+     verify_args_count(a, 2);
+     MalInteger i0 = verify_cast!MalInteger(a[0]);
+     MalInteger i1 = verify_cast!MalInteger(a[1]);
+     return new MalInteger(i0.val - i1.val);
+}
+
+static MalType mal_mul(MalType[] a ...)
+{
+     verify_args_count(a, 2);
+     MalInteger i0 = verify_cast!MalInteger(a[0]);
+     MalInteger i1 = verify_cast!MalInteger(a[1]);
+     return new MalInteger(i0.val * i1.val);
+}
+
+static MalType mal_div(MalType[] a ...)
+{
+     verify_args_count(a, 2);
+     MalInteger i0 = verify_cast!MalInteger(a[0]);
+     MalInteger i1 = verify_cast!MalInteger(a[1]);
+     return new MalInteger(i0.val / i1.val);
+}
+
+void main()
+{
+    Env repl_env;
+    repl_env["+"] = new MalBuiltinFunc(&mal_add, "+");
+    repl_env["-"] = new MalBuiltinFunc(&mal_sub, "-");
+    repl_env["*"] = new MalBuiltinFunc(&mal_mul, "*");
+    repl_env["/"] = new MalBuiltinFunc(&mal_div, "/");
+
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        try
+        {
+            writeln(rep(line, repl_env));
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+        }
+    }
+    writeln("");
+}
diff --git a/d/step3_env.d b/d/step3_env.d
new file mode 100644 (file)
index 0000000..c76cbf1
--- /dev/null
@@ -0,0 +1,153 @@
+module main;
+
+import std.algorithm;
+import std.array;
+import std.range;
+import std.stdio;
+import std.string;
+import env;
+import readline;
+import reader;
+import printer;
+import types;
+
+MalType READ(string str)
+{
+    return read_str(str);
+}
+
+MalType eval_ast(MalType ast, Env env)
+{
+    if (typeid(ast) == typeid(MalSymbol))
+    {
+        auto sym = verify_cast!MalSymbol(ast);
+        return env.get(sym);
+    }
+    else if (typeid(ast) == typeid(MalList))
+    {
+        auto lst = verify_cast!MalList(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalList(el);
+    }
+    else if (typeid(ast) == typeid(MalVector))
+    {
+        auto lst = verify_cast!MalVector(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalVector(el);
+    }
+    else if (typeid(ast) == typeid(MalHashmap))
+    {
+        auto hm = verify_cast!MalHashmap(ast);
+        typeof(hm.data) new_data;
+        foreach (string k, MalType v; hm.data)
+        {
+            new_data[k] = EVAL(v, env);
+        }
+        return new MalHashmap(new_data);
+    }
+    else
+    {
+        return ast;
+    }
+}
+
+MalType EVAL(MalType ast, Env env)
+{
+    MalList ast_list = cast(MalList) ast;
+    if (ast_list is null)
+    {
+        return eval_ast(ast, env);
+    }
+
+    auto a0_sym = verify_cast!MalSymbol(ast_list.elements[0]);
+    switch (a0_sym.name)
+    {
+        case "def!":
+            auto a1 = verify_cast!MalSymbol(ast_list.elements[1]);
+            return env.set(a1, EVAL(ast_list.elements[2], env));
+
+        case "let*":
+            auto a1 = verify_cast!MalSequential(ast_list.elements[1]);
+            auto let_env = new Env(env);
+            foreach (kv; chunks(a1.elements, 2))
+            {
+                if (kv.length < 2) throw new Exception("let* requires even number of elements");
+                auto var_name = verify_cast!MalSymbol(kv[0]);
+                let_env.set(var_name, EVAL(kv[1], let_env));
+            }
+            return EVAL(ast_list.elements[2], let_env);
+
+        default:
+            auto el = verify_cast!MalList(eval_ast(ast_list, env));
+            auto fobj = verify_cast!MalBuiltinFunc(el.elements[0]);
+            auto args = el.elements[1..$];
+            return fobj.fn(args);
+    }
+}
+
+string PRINT(MalType ast)
+{
+    return pr_str(ast);
+}
+
+string rep(string str, Env env)
+{
+    return PRINT(EVAL(READ(str), env));
+}
+
+static MalType mal_add(MalType[] a ...)
+{
+     verify_args_count(a, 2);
+     MalInteger i0 = verify_cast!MalInteger(a[0]);
+     MalInteger i1 = verify_cast!MalInteger(a[1]);
+     return new MalInteger(i0.val + i1.val);
+}
+
+static MalType mal_sub(MalType[] a ...)
+{
+     verify_args_count(a, 2);
+     MalInteger i0 = verify_cast!MalInteger(a[0]);
+     MalInteger i1 = verify_cast!MalInteger(a[1]);
+     return new MalInteger(i0.val - i1.val);
+}
+
+static MalType mal_mul(MalType[] a ...)
+{
+     verify_args_count(a, 2);
+     MalInteger i0 = verify_cast!MalInteger(a[0]);
+     MalInteger i1 = verify_cast!MalInteger(a[1]);
+     return new MalInteger(i0.val * i1.val);
+}
+
+static MalType mal_div(MalType[] a ...)
+{
+     verify_args_count(a, 2);
+     MalInteger i0 = verify_cast!MalInteger(a[0]);
+     MalInteger i1 = verify_cast!MalInteger(a[1]);
+     return new MalInteger(i0.val / i1.val);
+}
+
+void main()
+{
+    auto repl_env = new Env(null);
+    repl_env.set(new MalSymbol("+"), new MalBuiltinFunc(&mal_add, "+"));
+    repl_env.set(new MalSymbol("-"), new MalBuiltinFunc(&mal_sub, "-"));
+    repl_env.set(new MalSymbol("*"), new MalBuiltinFunc(&mal_mul, "*"));
+    repl_env.set(new MalSymbol("/"), new MalBuiltinFunc(&mal_div, "/"));
+
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        try
+        {
+            writeln(rep(line, repl_env));
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+        }
+    }
+    writeln("");
+}
diff --git a/d/step4_if_fn_do.d b/d/step4_if_fn_do.d
new file mode 100644 (file)
index 0000000..a4c21c4
--- /dev/null
@@ -0,0 +1,169 @@
+module main;
+
+import std.algorithm;
+import std.array;
+import std.range;
+import std.stdio;
+import std.string;
+import env;
+import mal_core;
+import readline;
+import reader;
+import printer;
+import types;
+
+MalType READ(string str)
+{
+    return read_str(str);
+}
+
+MalType eval_ast(MalType ast, Env env)
+{
+    if (typeid(ast) == typeid(MalSymbol))
+    {
+        auto sym = verify_cast!MalSymbol(ast);
+        return env.get(sym);
+    }
+    else if (typeid(ast) == typeid(MalList))
+    {
+        auto lst = verify_cast!MalList(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalList(el);
+    }
+    else if (typeid(ast) == typeid(MalVector))
+    {
+        auto lst = verify_cast!MalVector(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalVector(el);
+    }
+    else if (typeid(ast) == typeid(MalHashmap))
+    {
+        auto hm = verify_cast!MalHashmap(ast);
+        typeof(hm.data) new_data;
+        foreach (string k, MalType v; hm.data)
+        {
+            new_data[k] = EVAL(v, env);
+        }
+        return new MalHashmap(new_data);
+    }
+    else
+    {
+        return ast;
+    }
+}
+
+MalType EVAL(MalType ast, Env env)
+{
+    MalList ast_list = cast(MalList) ast;
+    if (ast_list is null)
+    {
+        return eval_ast(ast, env);
+    }
+
+    auto aste = ast_list.elements;
+    auto a0_sym = cast(MalSymbol) aste[0];
+    auto sym_name = a0_sym is null ? "" : a0_sym.name;
+    switch (sym_name)
+    {
+        case "def!":
+            auto a1 = verify_cast!MalSymbol(aste[1]);
+            return env.set(a1, EVAL(aste[2], env));
+
+        case "let*":
+            auto a1 = verify_cast!MalSequential(aste[1]);
+            auto let_env = new Env(env);
+            foreach (kv; chunks(a1.elements, 2))
+            {
+                if (kv.length < 2) throw new Exception("let* requires even number of elements");
+                auto var_name = verify_cast!MalSymbol(kv[0]);
+                let_env.set(var_name, EVAL(kv[1], let_env));
+            }
+            return EVAL(aste[2], let_env);
+
+        case "do":
+            auto rest = new MalList(aste[1..$]);
+            auto el = verify_cast!MalList(eval_ast(rest, env));
+            return el.elements[$-1];
+
+        case "if":
+            auto cond = EVAL(aste[1], env);
+            if (cond.is_truthy())
+                return EVAL(aste[2], env);
+            else
+                if (aste.length > 3)
+                    return EVAL(aste[3], env);
+                else
+                    return mal_nil;
+
+        case "fn*":
+            auto args_list = verify_cast!MalSequential(aste[1]);
+            return new MalFunc(args_list.elements, aste[2], env);
+
+        default:
+            auto el = verify_cast!MalList(eval_ast(ast, env));
+            if (el.elements.length == 0)
+            {
+                throw new Exception("Expected a non-empty list");
+            }
+            auto first = el.elements[0];
+            auto rest = el.elements[1..$];
+            if (typeid(first) == typeid(MalFunc))
+            {
+                auto funcobj = verify_cast!MalFunc(first);
+                auto callenv = new Env(funcobj.def_env, funcobj.arg_names, rest);
+                return EVAL(funcobj.func_body, callenv);
+            }
+            else if (typeid(first) == typeid(MalBuiltinFunc))
+            {
+                auto builtinfuncobj = verify_cast!MalBuiltinFunc(first);
+                return builtinfuncobj.fn(rest);
+            }
+            else
+            {
+                throw new Exception("Expected a function");
+            }
+    }
+}
+
+string PRINT(MalType ast)
+{
+    return pr_str(ast);
+}
+
+MalType re(string str, Env env)
+{
+    return EVAL(READ(str), env);
+}
+
+string rep(string str, Env env)
+{
+    return PRINT(re(str, env));
+}
+
+void main()
+{
+    auto repl_env = new Env(null);
+    foreach (string sym_name, BuiltinStaticFuncType f; core_ns)
+    {
+        repl_env.set(new MalSymbol(sym_name), new MalBuiltinFunc(f, sym_name));
+    }
+
+    // core.mal: defined using the language itself
+    re("(def! not (fn* (a) (if a false true)))", repl_env);
+
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        try
+        {
+            writeln(rep(line, repl_env));
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+        }
+    }
+    writeln("");
+}
diff --git a/d/step5_tco.d b/d/step5_tco.d
new file mode 100644 (file)
index 0000000..8cef6fa
--- /dev/null
@@ -0,0 +1,185 @@
+module main;
+
+import std.algorithm;
+import std.array;
+import std.range;
+import std.stdio;
+import std.string;
+import env;
+import mal_core;
+import readline;
+import reader;
+import printer;
+import types;
+
+MalType READ(string str)
+{
+    return read_str(str);
+}
+
+MalType eval_ast(MalType ast, Env env)
+{
+    if (typeid(ast) == typeid(MalSymbol))
+    {
+        auto sym = verify_cast!MalSymbol(ast);
+        return env.get(sym);
+    }
+    else if (typeid(ast) == typeid(MalList))
+    {
+        auto lst = verify_cast!MalList(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalList(el);
+    }
+    else if (typeid(ast) == typeid(MalVector))
+    {
+        auto lst = verify_cast!MalVector(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalVector(el);
+    }
+    else if (typeid(ast) == typeid(MalHashmap))
+    {
+        auto hm = verify_cast!MalHashmap(ast);
+        typeof(hm.data) new_data;
+        foreach (string k, MalType v; hm.data)
+        {
+            new_data[k] = EVAL(v, env);
+        }
+        return new MalHashmap(new_data);
+    }
+    else
+    {
+        return ast;
+    }
+}
+
+MalType EVAL(MalType ast, Env env)
+{
+    for (;;)
+    {
+        MalList ast_list = cast(MalList) ast;
+        if (ast_list is null)
+        {
+            return eval_ast(ast, env);
+        }
+
+        auto aste = ast_list.elements;
+        auto a0_sym = cast(MalSymbol) aste[0];
+        auto sym_name = a0_sym is null ? "" : a0_sym.name;
+        switch (sym_name)
+        {
+            case "def!":
+                auto a1 = verify_cast!MalSymbol(aste[1]);
+                return env.set(a1, EVAL(aste[2], env));
+
+            case "let*":
+                auto a1 = verify_cast!MalSequential(aste[1]);
+                auto let_env = new Env(env);
+                foreach (kv; chunks(a1.elements, 2))
+                {
+                    if (kv.length < 2) throw new Exception("let* requires even number of elements");
+                    auto var_name = verify_cast!MalSymbol(kv[0]);
+                    let_env.set(var_name, EVAL(kv[1], let_env));
+                }
+                ast = aste[2];
+                env = let_env;
+                continue; // TCO
+
+            case "do":
+                auto all_but_last = new MalList(aste[1..$-1]);
+                eval_ast(all_but_last, env);
+                ast = aste[$-1];
+                continue; // TCO
+
+            case "if":
+                auto cond = EVAL(aste[1], env);
+                if (cond.is_truthy())
+                {
+                    ast = aste[2];
+                    continue; // TCO
+                }
+                else
+                    if (aste.length > 3)
+                    {
+                        ast = aste[3];
+                        continue; // TCO
+                    }
+                    else
+                    {
+                        return mal_nil;
+                    }
+
+            case "fn*":
+                auto args_list = verify_cast!MalSequential(aste[1]);
+                return new MalFunc(args_list.elements, aste[2], env);
+
+            default:
+                auto el = verify_cast!MalList(eval_ast(ast, env));
+                if (el.elements.length == 0)
+                {
+                    throw new Exception("Expected a non-empty list");
+                }
+                auto first = el.elements[0];
+                auto rest = el.elements[1..$];
+                if (typeid(first) == typeid(MalFunc))
+                {
+                    auto funcobj = verify_cast!MalFunc(first);
+                    auto callenv = new Env(funcobj.def_env, funcobj.arg_names, rest);
+                    ast = funcobj.func_body;
+                    env = callenv;
+                    continue; // TCO
+                }
+                else if (typeid(first) == typeid(MalBuiltinFunc))
+                {
+                    auto builtinfuncobj = verify_cast!MalBuiltinFunc(first);
+                    return builtinfuncobj.fn(rest);
+                }
+                else
+                {
+                    throw new Exception("Expected a function");
+                }
+        }
+    }
+}
+
+string PRINT(MalType ast)
+{
+    return pr_str(ast);
+}
+
+MalType re(string str, Env env)
+{
+    return EVAL(READ(str), env);
+}
+
+string rep(string str, Env env)
+{
+    return PRINT(re(str, env));
+}
+
+void main()
+{
+    auto repl_env = new Env(null);
+    foreach (string sym_name, BuiltinStaticFuncType f; core_ns)
+    {
+        repl_env.set(new MalSymbol(sym_name), new MalBuiltinFunc(f, sym_name));
+    }
+
+    // core.mal: defined using the language itself
+    re("(def! not (fn* (a) (if a false true)))", repl_env);
+
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        try
+        {
+            writeln(rep(line, repl_env));
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+        }
+    }
+    writeln("");
+}
diff --git a/d/step6_file.d b/d/step6_file.d
new file mode 100644 (file)
index 0000000..8a93a0e
--- /dev/null
@@ -0,0 +1,214 @@
+module main;
+
+import std.algorithm;
+import std.array;
+import std.range;
+import std.stdio;
+import std.string;
+import std.c.process;
+import env;
+import mal_core;
+import readline;
+import reader;
+import printer;
+import types;
+
+MalType READ(string str)
+{
+    return read_str(str);
+}
+
+MalType eval_ast(MalType ast, Env env)
+{
+    if (typeid(ast) == typeid(MalSymbol))
+    {
+        auto sym = verify_cast!MalSymbol(ast);
+        return env.get(sym);
+    }
+    else if (typeid(ast) == typeid(MalList))
+    {
+        auto lst = verify_cast!MalList(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalList(el);
+    }
+    else if (typeid(ast) == typeid(MalVector))
+    {
+        auto lst = verify_cast!MalVector(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalVector(el);
+    }
+    else if (typeid(ast) == typeid(MalHashmap))
+    {
+        auto hm = verify_cast!MalHashmap(ast);
+        typeof(hm.data) new_data;
+        foreach (string k, MalType v; hm.data)
+        {
+            new_data[k] = EVAL(v, env);
+        }
+        return new MalHashmap(new_data);
+    }
+    else
+    {
+        return ast;
+    }
+}
+
+MalType EVAL(MalType ast, Env env)
+{
+    for (;;)
+    {
+        MalList ast_list = cast(MalList) ast;
+        if (ast_list is null)
+        {
+            return eval_ast(ast, env);
+        }
+
+        auto aste = ast_list.elements;
+        auto a0_sym = cast(MalSymbol) aste[0];
+        auto sym_name = a0_sym is null ? "" : a0_sym.name;
+        switch (sym_name)
+        {
+            case "def!":
+                auto a1 = verify_cast!MalSymbol(aste[1]);
+                return env.set(a1, EVAL(aste[2], env));
+
+            case "let*":
+                auto a1 = verify_cast!MalSequential(aste[1]);
+                auto let_env = new Env(env);
+                foreach (kv; chunks(a1.elements, 2))
+                {
+                    if (kv.length < 2) throw new Exception("let* requires even number of elements");
+                    auto var_name = verify_cast!MalSymbol(kv[0]);
+                    let_env.set(var_name, EVAL(kv[1], let_env));
+                }
+                ast = aste[2];
+                env = let_env;
+                continue; // TCO
+
+            case "do":
+                auto all_but_last = new MalList(aste[1..$-1]);
+                eval_ast(all_but_last, env);
+                ast = aste[$-1];
+                continue; // TCO
+
+            case "if":
+                auto cond = EVAL(aste[1], env);
+                if (cond.is_truthy())
+                {
+                    ast = aste[2];
+                    continue; // TCO
+                }
+                else
+                    if (aste.length > 3)
+                    {
+                        ast = aste[3];
+                        continue; // TCO
+                    }
+                    else
+                    {
+                        return mal_nil;
+                    }
+
+            case "fn*":
+                auto args_list = verify_cast!MalSequential(aste[1]);
+                return new MalFunc(args_list.elements, aste[2], env);
+
+            default:
+                auto el = verify_cast!MalList(eval_ast(ast, env));
+                if (el.elements.length == 0)
+                {
+                    throw new Exception("Expected a non-empty list");
+                }
+                auto first = el.elements[0];
+                auto rest = el.elements[1..$];
+                if (typeid(first) == typeid(MalFunc))
+                {
+                    auto funcobj = verify_cast!MalFunc(first);
+                    auto callenv = new Env(funcobj.def_env, funcobj.arg_names, rest);
+                    ast = funcobj.func_body;
+                    env = callenv;
+                    continue; // TCO
+                }
+                else if (typeid(first) == typeid(MalBuiltinFunc))
+                {
+                    auto builtinfuncobj = verify_cast!MalBuiltinFunc(first);
+                    return builtinfuncobj.fn(rest);
+                }
+                else
+                {
+                    throw new Exception("Expected a function");
+                }
+        }
+    }
+}
+
+string PRINT(MalType ast)
+{
+    return pr_str(ast);
+}
+
+MalType re(string str, Env env)
+{
+    return EVAL(READ(str), env);
+}
+
+string rep(string str, Env env)
+{
+    return PRINT(re(str, env));
+}
+
+static MalList create_argv_list(string[] args)
+{
+    if (args.length <= 2) return new MalList([]);
+    return new MalList(array(args[2..$].map!(s => cast(MalType)(new MalString(s)))));
+}
+
+void main(string[] args)
+{
+    Env repl_env = new Env(null);
+    foreach (string sym_name, BuiltinStaticFuncType f; core_ns)
+    {
+        repl_env.set(new MalSymbol(sym_name), new MalBuiltinFunc(f, sym_name));
+    }
+
+    BuiltinFuncType eval_func = (a ...) {
+        verify_args_count(a, 1);
+        return EVAL(a[0], repl_env);
+    };
+    repl_env.set(new MalSymbol("eval"), new MalBuiltinFunc(eval_func, "eval"));
+    repl_env.set(new MalSymbol("*ARGV*"), create_argv_list(args));
+
+    // core.mal: defined using the language itself
+    re("(def! not (fn* (a) (if a false true)))", repl_env);
+    re("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env);
+
+    if (args.length > 1)
+    {
+        try
+        {
+            rep("(load-file \"" ~ args[1] ~ "\")", repl_env);
+            return;
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+            std.c.process.exit(1);
+        }
+    }
+
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        try
+        {
+            writeln(rep(line, repl_env));
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+        }
+    }
+    writeln("");
+}
diff --git a/d/step7_quote.d b/d/step7_quote.d
new file mode 100644 (file)
index 0000000..d346b26
--- /dev/null
@@ -0,0 +1,253 @@
+module main;
+
+import std.algorithm;
+import std.array;
+import std.range;
+import std.stdio;
+import std.string;
+import std.c.process;
+import env;
+import mal_core;
+import readline;
+import reader;
+import printer;
+import types;
+
+bool is_pair(MalType ast)
+{
+    auto lst = cast(MalSequential) ast;
+    if (lst is null) return false;
+    return lst.elements.length > 0;
+}
+
+MalType quasiquote(MalType ast)
+{
+    if (!is_pair(ast))
+    {
+        return new MalList([sym_quote, ast]);
+    }
+    auto ast_seq = verify_cast!MalSequential(ast);
+    auto aste = ast_seq.elements;
+    if (aste[0] == sym_unquote)
+    {
+        return aste[1];
+    }
+
+    if (is_pair(aste[0]))
+    {
+        auto ast0_seq = verify_cast!MalSequential(aste[0]);
+        if (ast0_seq.elements[0] == sym_splice_unquote)
+        {
+            return new MalList([new MalSymbol("concat"), ast0_seq.elements[1], quasiquote(new MalList(aste[1..$]))]);
+        }
+    }
+
+    return new MalList([new MalSymbol("cons"), quasiquote(aste[0]), quasiquote(new MalList(aste[1..$]))]);
+}
+
+MalType READ(string str)
+{
+    return read_str(str);
+}
+
+MalType eval_ast(MalType ast, Env env)
+{
+    if (typeid(ast) == typeid(MalSymbol))
+    {
+        auto sym = verify_cast!MalSymbol(ast);
+        return env.get(sym);
+    }
+    else if (typeid(ast) == typeid(MalList))
+    {
+        auto lst = verify_cast!MalList(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalList(el);
+    }
+    else if (typeid(ast) == typeid(MalVector))
+    {
+        auto lst = verify_cast!MalVector(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalVector(el);
+    }
+    else if (typeid(ast) == typeid(MalHashmap))
+    {
+        auto hm = verify_cast!MalHashmap(ast);
+        typeof(hm.data) new_data;
+        foreach (string k, MalType v; hm.data)
+        {
+            new_data[k] = EVAL(v, env);
+        }
+        return new MalHashmap(new_data);
+    }
+    else
+    {
+        return ast;
+    }
+}
+
+MalType EVAL(MalType ast, Env env)
+{
+    for (;;)
+    {
+        MalList ast_list = cast(MalList) ast;
+        if (ast_list is null)
+        {
+            return eval_ast(ast, env);
+        }
+
+        auto aste = ast_list.elements;
+        auto a0_sym = cast(MalSymbol) aste[0];
+        auto sym_name = a0_sym is null ? "" : a0_sym.name;
+        switch (sym_name)
+        {
+            case "def!":
+                auto a1 = verify_cast!MalSymbol(aste[1]);
+                return env.set(a1, EVAL(aste[2], env));
+
+            case "let*":
+                auto a1 = verify_cast!MalSequential(aste[1]);
+                auto let_env = new Env(env);
+                foreach (kv; chunks(a1.elements, 2))
+                {
+                    if (kv.length < 2) throw new Exception("let* requires even number of elements");
+                    auto var_name = verify_cast!MalSymbol(kv[0]);
+                    let_env.set(var_name, EVAL(kv[1], let_env));
+                }
+                ast = aste[2];
+                env = let_env;
+                continue; // TCO
+
+            case "quote":
+                return aste[1];
+
+            case "quasiquote":
+                ast = quasiquote(aste[1]);
+                continue; // TCO
+
+            case "do":
+                auto all_but_last = new MalList(aste[1..$-1]);
+                eval_ast(all_but_last, env);
+                ast = aste[$-1];
+                continue; // TCO
+
+            case "if":
+                auto cond = EVAL(aste[1], env);
+                if (cond.is_truthy())
+                {
+                    ast = aste[2];
+                    continue; // TCO
+                }
+                else
+                    if (aste.length > 3)
+                    {
+                        ast = aste[3];
+                        continue; // TCO
+                    }
+                    else
+                    {
+                        return mal_nil;
+                    }
+
+            case "fn*":
+                auto args_list = verify_cast!MalSequential(aste[1]);
+                return new MalFunc(args_list.elements, aste[2], env);
+
+            default:
+                auto el = verify_cast!MalList(eval_ast(ast, env));
+                if (el.elements.length == 0)
+                {
+                    throw new Exception("Expected a non-empty list");
+                }
+                auto first = el.elements[0];
+                auto rest = el.elements[1..$];
+                if (typeid(first) == typeid(MalFunc))
+                {
+                    auto funcobj = verify_cast!MalFunc(first);
+                    auto callenv = new Env(funcobj.def_env, funcobj.arg_names, rest);
+                    ast = funcobj.func_body;
+                    env = callenv;
+                    continue; // TCO
+                }
+                else if (typeid(first) == typeid(MalBuiltinFunc))
+                {
+                    auto builtinfuncobj = verify_cast!MalBuiltinFunc(first);
+                    return builtinfuncobj.fn(rest);
+                }
+                else
+                {
+                    throw new Exception("Expected a function");
+                }
+        }
+    }
+}
+
+string PRINT(MalType ast)
+{
+    return pr_str(ast);
+}
+
+MalType re(string str, Env env)
+{
+    return EVAL(READ(str), env);
+}
+
+string rep(string str, Env env)
+{
+    return PRINT(re(str, env));
+}
+
+static MalList create_argv_list(string[] args)
+{
+    if (args.length <= 2) return new MalList([]);
+    return new MalList(array(args[2..$].map!(s => cast(MalType)(new MalString(s)))));
+}
+
+void main(string[] args)
+{
+    Env repl_env = new Env(null);
+    foreach (string sym_name, BuiltinStaticFuncType f; core_ns)
+    {
+        repl_env.set(new MalSymbol(sym_name), new MalBuiltinFunc(f, sym_name));
+    }
+
+    BuiltinFuncType eval_func = (a ...) {
+        verify_args_count(a, 1);
+        return EVAL(a[0], repl_env);
+    };
+    repl_env.set(new MalSymbol("eval"), new MalBuiltinFunc(eval_func, "eval"));
+    repl_env.set(new MalSymbol("*ARGV*"), create_argv_list(args));
+
+    // core.mal: defined using the language itself
+    re("(def! not (fn* (a) (if a false true)))", repl_env);
+    re("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env);
+
+    if (args.length > 1)
+    {
+        try
+        {
+            rep("(load-file \"" ~ args[1] ~ "\")", repl_env);
+            return;
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+            std.c.process.exit(1);
+        }
+    }
+
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        try
+        {
+            writeln(rep(line, repl_env));
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+        }
+    }
+    writeln("");
+}
diff --git a/d/step8_macros.d b/d/step8_macros.d
new file mode 100644 (file)
index 0000000..9dee00e
--- /dev/null
@@ -0,0 +1,299 @@
+module main;
+
+import std.algorithm;
+import std.array;
+import std.range;
+import std.stdio;
+import std.string;
+import std.c.process;
+import env;
+import mal_core;
+import readline;
+import reader;
+import printer;
+import types;
+
+bool is_pair(MalType ast)
+{
+    auto lst = cast(MalSequential) ast;
+    if (lst is null) return false;
+    return lst.elements.length > 0;
+}
+
+MalType quasiquote(MalType ast)
+{
+    if (!is_pair(ast))
+    {
+        return new MalList([sym_quote, ast]);
+    }
+    auto ast_seq = verify_cast!MalSequential(ast);
+    auto aste = ast_seq.elements;
+    if (aste[0] == sym_unquote)
+    {
+        return aste[1];
+    }
+
+    if (is_pair(aste[0]))
+    {
+        auto ast0_seq = verify_cast!MalSequential(aste[0]);
+        if (ast0_seq.elements[0] == sym_splice_unquote)
+        {
+            return new MalList([new MalSymbol("concat"), ast0_seq.elements[1], quasiquote(new MalList(aste[1..$]))]);
+        }
+    }
+
+    return new MalList([new MalSymbol("cons"), quasiquote(aste[0]), quasiquote(new MalList(aste[1..$]))]);
+}
+
+bool is_macro_call(MalType ast, Env env)
+{
+    auto lst = cast(MalList) ast;
+    if (lst is null) return false;
+    if (lst.elements.length == 0) return false;
+    auto sym0 = cast(MalSymbol) lst.elements[0];
+    if (sym0 is null) return false;
+    if (env.find(sym0) is null) return false;
+    auto val = env.get(sym0);
+    auto val_func = cast(MalFunc) val;
+    if (val_func is null) return false;
+    return val_func.is_macro;
+}
+
+MalType macroexpand(MalType ast, Env env)
+{
+    while (is_macro_call(ast, env))
+    {
+        auto ast_list = verify_cast!MalList(ast);
+        auto sym0 = verify_cast!MalSymbol(ast_list.elements[0]);
+        auto macrofunc = verify_cast!MalFunc(env.get(sym0));
+        auto rest = ast_list.elements[1..$];
+        auto callenv = new Env(macrofunc.def_env, macrofunc.arg_names, rest);
+        ast = EVAL(macrofunc.func_body, callenv);
+    }
+    return ast;
+}
+
+MalType READ(string str)
+{
+    return read_str(str);
+}
+
+MalType eval_ast(MalType ast, Env env)
+{
+    if (typeid(ast) == typeid(MalSymbol))
+    {
+        auto sym = verify_cast!MalSymbol(ast);
+        return env.get(sym);
+    }
+    else if (typeid(ast) == typeid(MalList))
+    {
+        auto lst = verify_cast!MalList(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalList(el);
+    }
+    else if (typeid(ast) == typeid(MalVector))
+    {
+        auto lst = verify_cast!MalVector(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalVector(el);
+    }
+    else if (typeid(ast) == typeid(MalHashmap))
+    {
+        auto hm = verify_cast!MalHashmap(ast);
+        typeof(hm.data) new_data;
+        foreach (string k, MalType v; hm.data)
+        {
+            new_data[k] = EVAL(v, env);
+        }
+        return new MalHashmap(new_data);
+    }
+    else
+    {
+        return ast;
+    }
+}
+
+MalType EVAL(MalType ast, Env env)
+{
+    for (;;)
+    {
+        MalList ast_list = cast(MalList) ast;
+        if (ast_list is null)
+        {
+            return eval_ast(ast, env);
+        }
+
+        ast = macroexpand(ast, env);
+        ast_list = cast(MalList) ast;
+        if (ast_list is null)
+        {
+            return ast;
+        }
+
+        auto aste = ast_list.elements;
+        auto a0_sym = cast(MalSymbol) aste[0];
+        auto sym_name = a0_sym is null ? "" : a0_sym.name;
+        switch (sym_name)
+        {
+            case "def!":
+                auto a1 = verify_cast!MalSymbol(aste[1]);
+                return env.set(a1, EVAL(aste[2], env));
+
+            case "let*":
+                auto a1 = verify_cast!MalSequential(aste[1]);
+                auto let_env = new Env(env);
+                foreach (kv; chunks(a1.elements, 2))
+                {
+                    if (kv.length < 2) throw new Exception("let* requires even number of elements");
+                    auto var_name = verify_cast!MalSymbol(kv[0]);
+                    let_env.set(var_name, EVAL(kv[1], let_env));
+                }
+                ast = aste[2];
+                env = let_env;
+                continue; // TCO
+
+            case "quote":
+                return aste[1];
+
+            case "quasiquote":
+                ast = quasiquote(aste[1]);
+                continue; // TCO
+
+            case "defmacro!":
+                auto a1 = verify_cast!MalSymbol(aste[1]);
+                auto mac = verify_cast!MalFunc(EVAL(aste[2], env));
+                mac.is_macro = true;
+                return env.set(a1, mac);
+
+            case "macroexpand":
+                return macroexpand(aste[1], env);
+
+            case "do":
+                auto all_but_last = new MalList(aste[1..$-1]);
+                eval_ast(all_but_last, env);
+                ast = aste[$-1];
+                continue; // TCO
+
+            case "if":
+                auto cond = EVAL(aste[1], env);
+                if (cond.is_truthy())
+                {
+                    ast = aste[2];
+                    continue; // TCO
+                }
+                else
+                    if (aste.length > 3)
+                    {
+                        ast = aste[3];
+                        continue; // TCO
+                    }
+                    else
+                    {
+                        return mal_nil;
+                    }
+
+            case "fn*":
+                auto args_list = verify_cast!MalSequential(aste[1]);
+                return new MalFunc(args_list.elements, aste[2], env);
+
+            default:
+                auto el = verify_cast!MalList(eval_ast(ast, env));
+                if (el.elements.length == 0)
+                {
+                    throw new Exception("Expected a non-empty list");
+                }
+                auto first = el.elements[0];
+                auto rest = el.elements[1..$];
+                if (typeid(first) == typeid(MalFunc))
+                {
+                    auto funcobj = verify_cast!MalFunc(first);
+                    auto callenv = new Env(funcobj.def_env, funcobj.arg_names, rest);
+                    ast = funcobj.func_body;
+                    env = callenv;
+                    continue; // TCO
+                }
+                else if (typeid(first) == typeid(MalBuiltinFunc))
+                {
+                    auto builtinfuncobj = verify_cast!MalBuiltinFunc(first);
+                    return builtinfuncobj.fn(rest);
+                }
+                else
+                {
+                    throw new Exception("Expected a function");
+                }
+        }
+    }
+}
+
+string PRINT(MalType ast)
+{
+    return pr_str(ast);
+}
+
+MalType re(string str, Env env)
+{
+    return EVAL(READ(str), env);
+}
+
+string rep(string str, Env env)
+{
+    return PRINT(re(str, env));
+}
+
+static MalList create_argv_list(string[] args)
+{
+    if (args.length <= 2) return new MalList([]);
+    return new MalList(array(args[2..$].map!(s => cast(MalType)(new MalString(s)))));
+}
+
+void main(string[] args)
+{
+    Env repl_env = new Env(null);
+    foreach (string sym_name, BuiltinStaticFuncType f; core_ns)
+    {
+        repl_env.set(new MalSymbol(sym_name), new MalBuiltinFunc(f, sym_name));
+    }
+
+    BuiltinFuncType eval_func = (a ...) {
+        verify_args_count(a, 1);
+        return EVAL(a[0], repl_env);
+    };
+    repl_env.set(new MalSymbol("eval"), new MalBuiltinFunc(eval_func, "eval"));
+    repl_env.set(new MalSymbol("*ARGV*"), create_argv_list(args));
+
+    // core.mal: defined using the language itself
+    re("(def! not (fn* (a) (if a false true)))", repl_env);
+    re("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env);
+    re("(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)))))))", repl_env);
+    re("(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))))))))", repl_env);
+
+    if (args.length > 1)
+    {
+        try
+        {
+            rep("(load-file \"" ~ args[1] ~ "\")", repl_env);
+            return;
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+            std.c.process.exit(1);
+        }
+    }
+
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        try
+        {
+            writeln(rep(line, repl_env));
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+        }
+    }
+    writeln("");
+}
diff --git a/d/step9_try.d b/d/step9_try.d
new file mode 100644 (file)
index 0000000..1fd21ca
--- /dev/null
@@ -0,0 +1,318 @@
+module main;
+
+import std.algorithm;
+import std.array;
+import std.range;
+import std.stdio;
+import std.string;
+import std.c.process;
+import env;
+import mal_core;
+import readline;
+import reader;
+import printer;
+import types;
+
+bool is_pair(MalType ast)
+{
+    auto lst = cast(MalSequential) ast;
+    if (lst is null) return false;
+    return lst.elements.length > 0;
+}
+
+MalType quasiquote(MalType ast)
+{
+    if (!is_pair(ast))
+    {
+        return new MalList([sym_quote, ast]);
+    }
+    auto ast_seq = verify_cast!MalSequential(ast);
+    auto aste = ast_seq.elements;
+    if (aste[0] == sym_unquote)
+    {
+        return aste[1];
+    }
+
+    if (is_pair(aste[0]))
+    {
+        auto ast0_seq = verify_cast!MalSequential(aste[0]);
+        if (ast0_seq.elements[0] == sym_splice_unquote)
+        {
+            return new MalList([new MalSymbol("concat"), ast0_seq.elements[1], quasiquote(new MalList(aste[1..$]))]);
+        }
+    }
+
+    return new MalList([new MalSymbol("cons"), quasiquote(aste[0]), quasiquote(new MalList(aste[1..$]))]);
+}
+
+bool is_macro_call(MalType ast, Env env)
+{
+    auto lst = cast(MalList) ast;
+    if (lst is null) return false;
+    if (lst.elements.length == 0) return false;
+    auto sym0 = cast(MalSymbol) lst.elements[0];
+    if (sym0 is null) return false;
+    if (env.find(sym0) is null) return false;
+    auto val = env.get(sym0);
+    auto val_func = cast(MalFunc) val;
+    if (val_func is null) return false;
+    return val_func.is_macro;
+}
+
+MalType macroexpand(MalType ast, Env env)
+{
+    while (is_macro_call(ast, env))
+    {
+        auto ast_list = verify_cast!MalList(ast);
+        auto sym0 = verify_cast!MalSymbol(ast_list.elements[0]);
+        auto macrofunc = verify_cast!MalFunc(env.get(sym0));
+        auto rest = ast_list.elements[1..$];
+        auto callenv = new Env(macrofunc.def_env, macrofunc.arg_names, rest);
+        ast = EVAL(macrofunc.func_body, callenv);
+    }
+    return ast;
+}
+
+MalType READ(string str)
+{
+    return read_str(str);
+}
+
+MalType eval_ast(MalType ast, Env env)
+{
+    if (typeid(ast) == typeid(MalSymbol))
+    {
+        auto sym = verify_cast!MalSymbol(ast);
+        return env.get(sym);
+    }
+    else if (typeid(ast) == typeid(MalList))
+    {
+        auto lst = verify_cast!MalList(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalList(el);
+    }
+    else if (typeid(ast) == typeid(MalVector))
+    {
+        auto lst = verify_cast!MalVector(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalVector(el);
+    }
+    else if (typeid(ast) == typeid(MalHashmap))
+    {
+        auto hm = verify_cast!MalHashmap(ast);
+        typeof(hm.data) new_data;
+        foreach (string k, MalType v; hm.data)
+        {
+            new_data[k] = EVAL(v, env);
+        }
+        return new MalHashmap(new_data);
+    }
+    else
+    {
+        return ast;
+    }
+}
+
+MalType EVAL(MalType ast, Env env)
+{
+    for (;;)
+    {
+        MalList ast_list = cast(MalList) ast;
+        if (ast_list is null)
+        {
+            return eval_ast(ast, env);
+        }
+
+        ast = macroexpand(ast, env);
+        ast_list = cast(MalList) ast;
+        if (ast_list is null)
+        {
+            return ast;
+        }
+
+        auto aste = ast_list.elements;
+        auto a0_sym = cast(MalSymbol) aste[0];
+        auto sym_name = a0_sym is null ? "" : a0_sym.name;
+        switch (sym_name)
+        {
+            case "def!":
+                auto a1 = verify_cast!MalSymbol(aste[1]);
+                return env.set(a1, EVAL(aste[2], env));
+
+            case "let*":
+                auto a1 = verify_cast!MalSequential(aste[1]);
+                auto let_env = new Env(env);
+                foreach (kv; chunks(a1.elements, 2))
+                {
+                    if (kv.length < 2) throw new Exception("let* requires even number of elements");
+                    auto var_name = verify_cast!MalSymbol(kv[0]);
+                    let_env.set(var_name, EVAL(kv[1], let_env));
+                }
+                ast = aste[2];
+                env = let_env;
+                continue; // TCO
+
+            case "quote":
+                return aste[1];
+
+            case "quasiquote":
+                ast = quasiquote(aste[1]);
+                continue; // TCO
+
+            case "defmacro!":
+                auto a1 = verify_cast!MalSymbol(aste[1]);
+                auto mac = verify_cast!MalFunc(EVAL(aste[2], env));
+                mac.is_macro = true;
+                return env.set(a1, mac);
+
+            case "macroexpand":
+                return macroexpand(aste[1], env);
+
+            case "try*":
+                MalType exc;
+                try
+                {
+                    return EVAL(aste[1], env);
+                }
+                catch (MalException e)
+                {
+                    exc = e.data;
+                }
+                catch (Exception e)
+                {
+                    exc = new MalString(e.msg);
+                }
+                if (aste.length < 3) return mal_nil;
+                auto catch_clause = verify_cast!MalList(aste[2]);
+                auto catch_env = new Env(env, [catch_clause.elements[1]], [exc]);
+                return EVAL(catch_clause.elements[2], catch_env);
+
+            case "do":
+                auto all_but_last = new MalList(aste[1..$-1]);
+                eval_ast(all_but_last, env);
+                ast = aste[$-1];
+                continue; // TCO
+
+            case "if":
+                auto cond = EVAL(aste[1], env);
+                if (cond.is_truthy())
+                {
+                    ast = aste[2];
+                    continue; // TCO
+                }
+                else
+                    if (aste.length > 3)
+                    {
+                        ast = aste[3];
+                        continue; // TCO
+                    }
+                    else
+                    {
+                        return mal_nil;
+                    }
+
+            case "fn*":
+                auto args_list = verify_cast!MalSequential(aste[1]);
+                return new MalFunc(args_list.elements, aste[2], env);
+
+            default:
+                auto el = verify_cast!MalList(eval_ast(ast, env));
+                if (el.elements.length == 0)
+                {
+                    throw new Exception("Expected a non-empty list");
+                }
+                auto first = el.elements[0];
+                auto rest = el.elements[1..$];
+                if (typeid(first) == typeid(MalFunc))
+                {
+                    auto funcobj = verify_cast!MalFunc(first);
+                    auto callenv = new Env(funcobj.def_env, funcobj.arg_names, rest);
+                    ast = funcobj.func_body;
+                    env = callenv;
+                    continue; // TCO
+                }
+                else if (typeid(first) == typeid(MalBuiltinFunc))
+                {
+                    auto builtinfuncobj = verify_cast!MalBuiltinFunc(first);
+                    return builtinfuncobj.fn(rest);
+                }
+                else
+                {
+                    throw new Exception("Expected a function");
+                }
+        }
+    }
+}
+
+string PRINT(MalType ast)
+{
+    return pr_str(ast);
+}
+
+MalType re(string str, Env env)
+{
+    return EVAL(READ(str), env);
+}
+
+string rep(string str, Env env)
+{
+    return PRINT(re(str, env));
+}
+
+static MalList create_argv_list(string[] args)
+{
+    if (args.length <= 2) return new MalList([]);
+    return new MalList(array(args[2..$].map!(s => cast(MalType)(new MalString(s)))));
+}
+
+void main(string[] args)
+{
+    Env repl_env = new Env(null);
+    foreach (string sym_name, BuiltinStaticFuncType f; core_ns)
+    {
+        repl_env.set(new MalSymbol(sym_name), new MalBuiltinFunc(f, sym_name));
+    }
+
+    BuiltinFuncType eval_func = (a ...) {
+        verify_args_count(a, 1);
+        return EVAL(a[0], repl_env);
+    };
+    repl_env.set(new MalSymbol("eval"), new MalBuiltinFunc(eval_func, "eval"));
+    repl_env.set(new MalSymbol("*ARGV*"), create_argv_list(args));
+
+    // core.mal: defined using the language itself
+    re("(def! not (fn* (a) (if a false true)))", repl_env);
+    re("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env);
+    re("(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)))))))", repl_env);
+    re("(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))))))))", repl_env);
+
+    if (args.length > 1)
+    {
+        try
+        {
+            rep("(load-file \"" ~ args[1] ~ "\")", repl_env);
+            return;
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+            std.c.process.exit(1);
+        }
+    }
+
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        try
+        {
+            writeln(rep(line, repl_env));
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+        }
+    }
+    writeln("");
+}
diff --git a/d/stepA_mal.d b/d/stepA_mal.d
new file mode 100644 (file)
index 0000000..1fc3368
--- /dev/null
@@ -0,0 +1,320 @@
+module main;
+
+import std.algorithm;
+import std.array;
+import std.range;
+import std.stdio;
+import std.string;
+import std.c.process;
+import env;
+import mal_core;
+import readline;
+import reader;
+import printer;
+import types;
+
+bool is_pair(MalType ast)
+{
+    auto lst = cast(MalSequential) ast;
+    if (lst is null) return false;
+    return lst.elements.length > 0;
+}
+
+MalType quasiquote(MalType ast)
+{
+    if (!is_pair(ast))
+    {
+        return new MalList([sym_quote, ast]);
+    }
+    auto ast_seq = verify_cast!MalSequential(ast);
+    auto aste = ast_seq.elements;
+    if (aste[0] == sym_unquote)
+    {
+        return aste[1];
+    }
+
+    if (is_pair(aste[0]))
+    {
+        auto ast0_seq = verify_cast!MalSequential(aste[0]);
+        if (ast0_seq.elements[0] == sym_splice_unquote)
+        {
+            return new MalList([new MalSymbol("concat"), ast0_seq.elements[1], quasiquote(new MalList(aste[1..$]))]);
+        }
+    }
+
+    return new MalList([new MalSymbol("cons"), quasiquote(aste[0]), quasiquote(new MalList(aste[1..$]))]);
+}
+
+bool is_macro_call(MalType ast, Env env)
+{
+    auto lst = cast(MalList) ast;
+    if (lst is null) return false;
+    if (lst.elements.length == 0) return false;
+    auto sym0 = cast(MalSymbol) lst.elements[0];
+    if (sym0 is null) return false;
+    if (env.find(sym0) is null) return false;
+    auto val = env.get(sym0);
+    auto val_func = cast(MalFunc) val;
+    if (val_func is null) return false;
+    return val_func.is_macro;
+}
+
+MalType macroexpand(MalType ast, Env env)
+{
+    while (is_macro_call(ast, env))
+    {
+        auto ast_list = verify_cast!MalList(ast);
+        auto sym0 = verify_cast!MalSymbol(ast_list.elements[0]);
+        auto macrofunc = verify_cast!MalFunc(env.get(sym0));
+        auto rest = ast_list.elements[1..$];
+        auto callenv = new Env(macrofunc.def_env, macrofunc.arg_names, rest);
+        ast = EVAL(macrofunc.func_body, callenv);
+    }
+    return ast;
+}
+
+MalType READ(string str)
+{
+    return read_str(str);
+}
+
+MalType eval_ast(MalType ast, Env env)
+{
+    if (typeid(ast) == typeid(MalSymbol))
+    {
+        auto sym = verify_cast!MalSymbol(ast);
+        return env.get(sym);
+    }
+    else if (typeid(ast) == typeid(MalList))
+    {
+        auto lst = verify_cast!MalList(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalList(el);
+    }
+    else if (typeid(ast) == typeid(MalVector))
+    {
+        auto lst = verify_cast!MalVector(ast);
+        auto el = array(lst.elements.map!(e => EVAL(e, env)));
+        return new MalVector(el);
+    }
+    else if (typeid(ast) == typeid(MalHashmap))
+    {
+        auto hm = verify_cast!MalHashmap(ast);
+        typeof(hm.data) new_data;
+        foreach (string k, MalType v; hm.data)
+        {
+            new_data[k] = EVAL(v, env);
+        }
+        return new MalHashmap(new_data);
+    }
+    else
+    {
+        return ast;
+    }
+}
+
+MalType EVAL(MalType ast, Env env)
+{
+    for (;;)
+    {
+        MalList ast_list = cast(MalList) ast;
+        if (ast_list is null)
+        {
+            return eval_ast(ast, env);
+        }
+
+        ast = macroexpand(ast, env);
+        ast_list = cast(MalList) ast;
+        if (ast_list is null)
+        {
+            return ast;
+        }
+
+        auto aste = ast_list.elements;
+        auto a0_sym = cast(MalSymbol) aste[0];
+        auto sym_name = a0_sym is null ? "" : a0_sym.name;
+        switch (sym_name)
+        {
+            case "def!":
+                auto a1 = verify_cast!MalSymbol(aste[1]);
+                return env.set(a1, EVAL(aste[2], env));
+
+            case "let*":
+                auto a1 = verify_cast!MalSequential(aste[1]);
+                auto let_env = new Env(env);
+                foreach (kv; chunks(a1.elements, 2))
+                {
+                    if (kv.length < 2) throw new Exception("let* requires even number of elements");
+                    auto var_name = verify_cast!MalSymbol(kv[0]);
+                    let_env.set(var_name, EVAL(kv[1], let_env));
+                }
+                ast = aste[2];
+                env = let_env;
+                continue; // TCO
+
+            case "quote":
+                return aste[1];
+
+            case "quasiquote":
+                ast = quasiquote(aste[1]);
+                continue; // TCO
+
+            case "defmacro!":
+                auto a1 = verify_cast!MalSymbol(aste[1]);
+                auto mac = verify_cast!MalFunc(EVAL(aste[2], env));
+                mac.is_macro = true;
+                return env.set(a1, mac);
+
+            case "macroexpand":
+                return macroexpand(aste[1], env);
+
+            case "try*":
+                MalType exc;
+                try
+                {
+                    return EVAL(aste[1], env);
+                }
+                catch (MalException e)
+                {
+                    exc = e.data;
+                }
+                catch (Exception e)
+                {
+                    exc = new MalString(e.msg);
+                }
+                if (aste.length < 3) return mal_nil;
+                auto catch_clause = verify_cast!MalList(aste[2]);
+                auto catch_env = new Env(env, [catch_clause.elements[1]], [exc]);
+                return EVAL(catch_clause.elements[2], catch_env);
+
+            case "do":
+                auto all_but_last = new MalList(aste[1..$-1]);
+                eval_ast(all_but_last, env);
+                ast = aste[$-1];
+                continue; // TCO
+
+            case "if":
+                auto cond = EVAL(aste[1], env);
+                if (cond.is_truthy())
+                {
+                    ast = aste[2];
+                    continue; // TCO
+                }
+                else
+                    if (aste.length > 3)
+                    {
+                        ast = aste[3];
+                        continue; // TCO
+                    }
+                    else
+                    {
+                        return mal_nil;
+                    }
+
+            case "fn*":
+                auto args_list = verify_cast!MalSequential(aste[1]);
+                return new MalFunc(args_list.elements, aste[2], env);
+
+            default:
+                auto el = verify_cast!MalList(eval_ast(ast, env));
+                if (el.elements.length == 0)
+                {
+                    throw new Exception("Expected a non-empty list");
+                }
+                auto first = el.elements[0];
+                auto rest = el.elements[1..$];
+                if (typeid(first) == typeid(MalFunc))
+                {
+                    auto funcobj = verify_cast!MalFunc(first);
+                    auto callenv = new Env(funcobj.def_env, funcobj.arg_names, rest);
+                    ast = funcobj.func_body;
+                    env = callenv;
+                    continue; // TCO
+                }
+                else if (typeid(first) == typeid(MalBuiltinFunc))
+                {
+                    auto builtinfuncobj = verify_cast!MalBuiltinFunc(first);
+                    return builtinfuncobj.fn(rest);
+                }
+                else
+                {
+                    throw new Exception("Expected a function");
+                }
+        }
+    }
+}
+
+string PRINT(MalType ast)
+{
+    return pr_str(ast);
+}
+
+MalType re(string str, Env env)
+{
+    return EVAL(READ(str), env);
+}
+
+string rep(string str, Env env)
+{
+    return PRINT(re(str, env));
+}
+
+static MalList create_argv_list(string[] args)
+{
+    if (args.length <= 2) return new MalList([]);
+    return new MalList(array(args[2..$].map!(s => cast(MalType)(new MalString(s)))));
+}
+
+void main(string[] args)
+{
+    Env repl_env = new Env(null);
+    foreach (string sym_name, BuiltinStaticFuncType f; core_ns)
+    {
+        repl_env.set(new MalSymbol(sym_name), new MalBuiltinFunc(f, sym_name));
+    }
+
+    BuiltinFuncType eval_func = (a ...) {
+        verify_args_count(a, 1);
+        return EVAL(a[0], repl_env);
+    };
+    repl_env.set(new MalSymbol("eval"), new MalBuiltinFunc(eval_func, "eval"));
+    repl_env.set(new MalSymbol("*ARGV*"), create_argv_list(args));
+
+    // core.mal: defined using the language itself
+    re("(def! *host-language* \"d\")", repl_env);
+    re("(def! not (fn* (a) (if a false true)))", repl_env);
+    re("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env);
+    re("(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)))))))", repl_env);
+    re("(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))))))))", repl_env);
+
+    if (args.length > 1)
+    {
+        try
+        {
+            rep("(load-file \"" ~ args[1] ~ "\")", repl_env);
+            return;
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+            std.c.process.exit(1);
+        }
+    }
+
+    re("(println (str \"Mal [\" *host-language* \"]\"))", repl_env);
+    for (;;)
+    {
+        string line = _readline("user> ");
+        if (line is null) break;
+        if (line.length == 0) continue;
+        try
+        {
+            writeln(rep(line, repl_env));
+        }
+        catch (Exception e)
+        {
+            writeln("Error: ", e.msg);
+        }
+    }
+    writeln("");
+}
diff --git a/d/types.d b/d/types.d
new file mode 100644 (file)
index 0000000..a983482
--- /dev/null
+++ b/d/types.d
@@ -0,0 +1,438 @@
+import std.algorithm;
+import std.array;
+import std.conv;
+import std.functional;
+import std.range;
+import env;
+
+abstract class MalType
+{
+    string print(bool readable) const;
+    bool is_truthy() const { return true; }
+}
+
+interface MalMeta
+{
+    MalType meta();
+    MalType with_meta(MalType new_meta);
+}
+
+class MalNil : MalType
+{
+    override string print(bool readable) const { return "nil"; }
+    override bool is_truthy() const { return false; }
+    override bool opEquals(Object o) { return (cast(MalNil)(o) !is null); }
+}
+
+class MalFalse : MalType
+{
+    override string print(bool readable) const { return "false"; }
+    override bool is_truthy() const { return false; }
+    override bool opEquals(Object o) { return (cast(MalFalse)(o) !is null); }
+}
+
+class MalTrue : MalType
+{
+    override string print(bool readable) const { return "true"; }
+    override bool opEquals(Object o) { return (cast(MalTrue)(o) !is null); }
+}
+
+MalNil mal_nil;
+MalFalse mal_false;
+MalTrue mal_true;
+
+static this()
+{
+    mal_nil = new MalNil;
+    mal_false = new MalFalse;
+    mal_true = new MalTrue;
+}
+
+MalType bool_to_mal(in bool b)
+{
+    return b ? mal_true : mal_false;
+}
+
+class MalSymbol : MalType
+{
+    const string name;
+    this(in string token) { name = token; }
+    override string print(bool readable) const { return name; }
+
+    override size_t toHash()
+    {
+        return typeid(name).getHash(&name);
+    }
+
+    override int opCmp(Object other)
+    {
+        MalSymbol o = cast(MalSymbol) other;
+        return cmp(name, o.name);
+    }
+
+    override bool opEquals(Object other)
+    {
+        auto o = cast(MalSymbol) other;
+        return (o !is null && name == o.name);
+    }
+}
+
+class MalInteger : MalType
+{
+    const long val;
+    this(string token) { val = to!long(token); }
+    this(long v) { val = v; }
+    override string print(bool readable) const { return to!string(val); }
+
+    override bool opEquals(Object o)
+    {
+        auto oint = cast(MalInteger)(o);
+        return (oint !is null && val == oint.val);
+    }
+}
+
+class MalString : MalType
+{
+    const string val;
+    this(in string token) { val = token; }
+    override string print(bool readable) const
+    {
+        if (is_keyword()) return ":" ~ val[2..$];
+        if (readable)
+        {
+            string escaped = val.replace("\\", "\\\\")
+                                .replace("\"", "\\\"")
+                                .replace("\n", "\\n");
+            return "\"" ~ escaped ~ "\"";
+        }
+        else
+        {
+            return val;
+        }
+    }
+
+    bool is_keyword() const
+    {
+        return val.length > 1 && val[0..2] == "\u029e";
+    }
+
+    override bool opEquals(Object o)
+    {
+        auto ostr = cast(MalString)(o);
+        return (ostr !is null && val == ostr.val);
+    }
+}
+
+abstract class MalSequential : MalType, MalMeta
+{
+    MalType[] elements;
+    MalType meta_val;
+
+    this(MalType[] lst) {
+        elements = lst;
+        meta_val = mal_nil;
+    }
+
+    override bool opEquals(Object o)
+    {
+        auto oseq = cast(MalSequential)(o);
+        return (oseq !is null && elements == oseq.elements);
+    }
+
+    MalSequential conj(MalType element);
+}
+
+class MalList : MalSequential, MalMeta
+{
+    this(MalType[] lst) { super(lst); }
+    this(MalList that, MalType new_meta)
+    {
+        super(that.elements);
+        meta_val = new_meta;
+    }
+
+    override string print(bool readable) const
+    {
+        auto items_strs = elements.map!(e => e.print(readable));
+        return "(" ~ array(items_strs).join(" ") ~ ")";
+    }
+
+    override MalSequential conj(MalType element)
+    {
+        return new MalList([element] ~ elements);
+    }
+
+    override MalType meta() { return meta_val; }
+    override MalType with_meta(MalType new_meta)
+    {
+        return new MalList(this, new_meta);
+    }
+}
+
+class MalVector : MalSequential, MalMeta
+{
+    this(MalType[] lst) { super(lst); }
+    this(MalVector that, MalType new_meta)
+    {
+        super(that.elements);
+        meta_val = new_meta;
+    }
+
+    override string print(bool readable) const
+    {
+        auto items_strs = elements.map!(e => e.print(readable));
+        return "[" ~ array(items_strs).join(" ") ~ "]";
+    }
+
+    override MalSequential conj(MalType element)
+    {
+        return new MalVector(elements ~ [element]);
+    }
+
+    override MalType meta() { return meta_val; }
+    override MalType with_meta(MalType new_meta)
+    {
+        return new MalVector(this, new_meta);
+    }
+}
+
+class MalHashmap : MalType, MalMeta
+{
+    MalType[string] data;
+    MalType meta_val;
+
+    this(MalType[string] map)
+    {
+        data = map;
+        meta_val = mal_nil;
+    }
+    this(MalType[] lst)
+    {
+        put_kv_list(lst);
+        meta_val = mal_nil;
+    }
+    this(MalHashmap that, MalType new_meta)
+    {
+        data = that.data;
+        meta_val = new_meta;
+    }
+
+    bool contains(in MalType key)
+    {
+        auto valp = (make_hash_key(key) in data);
+        return valp !is null;
+    }
+
+    MalType get(in MalType key)
+    {
+        auto valp = (make_hash_key(key) in data);
+        return valp is null ? mal_nil : *valp;
+    }
+
+    void remove(in MalType key)
+    {
+        data.remove(make_hash_key(key));
+    }
+
+    void put(in MalType key, MalType val)
+    {
+        data[make_hash_key(key)] = val;
+    }
+
+    void put_kv_list(MalType[] lst)
+    {
+        foreach (kv; chunks(lst, 2))
+        {
+            if (kv.length < 2) throw new Exception("requires even number of elements");
+            put(kv[0], kv[1]);
+        }
+    }
+
+    private string make_hash_key(in MalType key)
+    {
+        return verify_cast!MalString(key).val;
+    }
+
+    override string print(bool readable) const
+    {
+        string[] parts;
+        foreach (k, v; data)
+        {
+            parts ~= (new MalString(k)).print(readable);
+            parts ~= v.print(readable);
+        }
+        return "{" ~ parts.join(" ") ~ "}";
+    }
+
+    override bool opEquals(Object o)
+    {
+        auto ohm = cast(MalHashmap)(o);
+        return (ohm !is null && data == ohm.data);
+    }
+
+    override MalType meta() { return meta_val; }
+    override MalType with_meta(MalType new_meta)
+    {
+        return new MalHashmap(this, new_meta);
+    }
+}
+
+alias BuiltinStaticFuncType = MalType function(MalType[] a ...);
+alias BuiltinFuncType = MalType delegate(MalType[] a ...);
+
+class MalBuiltinFunc : MalType, MalMeta
+{
+    const BuiltinFuncType fn;
+    const string name;
+    MalType meta_val;
+
+    this(in BuiltinFuncType fn_v, in string name_v)
+    {
+        fn = fn_v;
+        name = name_v;
+        meta_val = mal_nil;
+    }
+
+    this(in BuiltinStaticFuncType static_fn_v, in string name_v)
+    {
+        fn = toDelegate(static_fn_v);
+        name = name_v;
+        meta_val = mal_nil;
+    }
+
+    this(MalBuiltinFunc that, MalType new_meta)
+    {
+        fn = that.fn;
+        name = that.name;
+        meta_val = new_meta;
+    }
+
+    override string print(bool readable) const
+    {
+        return "<BuiltinFunc:" ~ name ~ ">";
+    }
+
+    override MalType meta() { return meta_val; }
+
+    override MalType with_meta(MalType new_meta)
+    {
+        return new MalBuiltinFunc(this, new_meta);
+    }
+}
+
+class MalFunc : MalType, MalMeta
+{
+    MalType[] arg_names;
+    MalType func_body;
+    Env def_env;
+    bool is_macro;
+    MalType meta_val;
+
+    this(MalType[] arg_names_v, MalType func_body_v, Env def_env_v)
+    {
+        arg_names = arg_names_v;
+        func_body = func_body_v;
+        def_env = def_env_v;
+        is_macro = false;
+        meta_val = mal_nil;
+    }
+
+    this(MalFunc that, MalType new_meta)
+    {
+        arg_names = that.arg_names;
+        func_body = that.func_body;
+        def_env = that.def_env;
+        is_macro = that.is_macro;
+        meta_val = new_meta;
+    }
+
+    override string print(bool readable) const
+    {
+        return "<Function:args=" ~ array(arg_names.map!(e => e.print(true))).join(",") ~ ">";
+    }
+
+    override MalType meta() { return meta_val; }
+
+    override MalType with_meta(MalType new_meta)
+    {
+        return new MalFunc(this, new_meta);
+    }
+}
+
+class MalAtom : MalType, MalMeta
+{
+    MalType val;
+    MalType meta_val;
+
+    this(MalType v)
+    {
+        val = v;
+        meta_val = mal_nil;
+    }
+
+    this(MalAtom that, MalType new_meta)
+    {
+        val = that.val;
+        meta_val = new_meta;
+    }
+
+    override string print(bool readable) const
+    {
+        return "(atom " ~ val.print(readable) ~ ")";
+    }
+
+    override bool opEquals(Object other)
+    {
+        auto o = cast(MalAtom) other;
+        return (o !is null && val == o.val);
+    }
+
+    override MalType meta() { return meta_val; }
+
+    override MalType with_meta(MalType new_meta)
+    {
+        return new MalAtom(this, new_meta);
+    }
+}
+
+class MalException : Exception
+{
+    MalType data;
+
+    this(MalType val)
+    {
+        super("MalException");
+        data = val;
+    }
+}
+
+T verify_cast(T)(in MalType v)
+{
+    T res = cast(T) v;
+    if (res is null) throw new Exception("Expected " ~ typeid(T).name);
+    return res;
+}
+
+MalType mal_type_q(T)(in MalType[] a)
+{
+    verify_args_count(a, 1);
+    T res = cast(T) a[0];
+    return bool_to_mal(res !is null);
+}
+
+inout(MalType[]) verify_args_count(inout MalType[] args, in int expected_length)
+{
+    if (args.length != expected_length)
+    {
+        throw new Exception("Expected " ~ to!string(expected_length) ~ " arguments");
+    }
+    return args;
+}
+
+void verify_min_args_count(in MalType[] args, in int min_expected_length)
+{
+    if (args.length < min_expected_length)
+    {
+        throw new Exception("Expected at least " ~ to!string(min_expected_length) ~ " arguments");
+    }
+}