Add Fantom implementation
authorDov Murik <dov.murik@gmail.com>
Sun, 29 Apr 2018 12:53:32 +0000 (12:53 +0000)
committerDov Murik <dov.murik@gmail.com>
Sun, 3 Jun 2018 20:08:19 +0000 (20:08 +0000)
36 files changed:
.gitignore
Makefile
README.md
fantom/Dockerfile [new file with mode: 0644]
fantom/Makefile [new file with mode: 0644]
fantom/run [new file with mode: 0755]
fantom/src/mallib/build.fan [new file with mode: 0644]
fantom/src/mallib/fan/core.fan [new file with mode: 0644]
fantom/src/mallib/fan/env.fan [new file with mode: 0644]
fantom/src/mallib/fan/interop.fan [new file with mode: 0644]
fantom/src/mallib/fan/reader.fan [new file with mode: 0644]
fantom/src/mallib/fan/types.fan [new file with mode: 0644]
fantom/src/step0_repl/build.fan [new file with mode: 0644]
fantom/src/step0_repl/fan/main.fan [new file with mode: 0644]
fantom/src/step1_read_print/build.fan [new file with mode: 0644]
fantom/src/step1_read_print/fan/main.fan [new file with mode: 0644]
fantom/src/step2_eval/build.fan [new file with mode: 0644]
fantom/src/step2_eval/fan/main.fan [new file with mode: 0644]
fantom/src/step3_env/build.fan [new file with mode: 0644]
fantom/src/step3_env/fan/main.fan [new file with mode: 0644]
fantom/src/step4_if_fn_do/build.fan [new file with mode: 0644]
fantom/src/step4_if_fn_do/fan/main.fan [new file with mode: 0644]
fantom/src/step5_tco/build.fan [new file with mode: 0644]
fantom/src/step5_tco/fan/main.fan [new file with mode: 0644]
fantom/src/step6_file/build.fan [new file with mode: 0644]
fantom/src/step6_file/fan/main.fan [new file with mode: 0644]
fantom/src/step7_quote/build.fan [new file with mode: 0644]
fantom/src/step7_quote/fan/main.fan [new file with mode: 0644]
fantom/src/step8_macros/build.fan [new file with mode: 0644]
fantom/src/step8_macros/fan/main.fan [new file with mode: 0644]
fantom/src/step9_try/build.fan [new file with mode: 0644]
fantom/src/step9_try/fan/main.fan [new file with mode: 0644]
fantom/src/stepA_mal/build.fan [new file with mode: 0644]
fantom/src/stepA_mal/fan/main.fan [new file with mode: 0644]
fantom/tests/step5_tco.mal [new file with mode: 0644]
fantom/tests/stepA_mal.mal [new file with mode: 0644]

index 5ecac39..1b8b6e4 100644 (file)
@@ -54,6 +54,7 @@ erlang/src/*.beam
 es6/mal.js
 es6/.esm-cache
 factor/mal.factor
+fantom/lib
 forth/mal.fs
 fsharp/*.exe
 fsharp/*.dll
index d88cd89..0b061af 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -79,7 +79,7 @@ DOCKERIZE =
 #
 
 IMPLS = ada awk bash basic c chuck clojure coffee common-lisp cpp crystal cs d dart \
-       elisp elixir elm erlang es6 factor forth fsharp go groovy gst guile haskell \
+       elisp elixir elm erlang es6 factor fantom forth fsharp go groovy gst guile haskell \
        haxe hy io java js julia kotlin livescript logo lua make mal matlab miniMAL \
        nasm nim objc objpascal ocaml perl perl6 php pil plpgsql plsql powershell ps \
        python r racket rexx rpython ruby rust scala scheme skew swift swift3 tcl \
@@ -191,6 +191,7 @@ elm_STEP_TO_PROG =     elm/$($(1)).js
 erlang_STEP_TO_PROG =  erlang/$($(1))
 es6_STEP_TO_PROG =     es6/$($(1)).mjs
 factor_STEP_TO_PROG =  factor/$($(1))/$($(1)).factor
+fantom_STEP_TO_PROG =  fantom/lib/fan/$($(1)).pod
 forth_STEP_TO_PROG =   forth/$($(1)).fs
 fsharp_STEP_TO_PROG =  fsharp/$($(1)).exe
 go_STEP_TO_PROG =      go/$($(1))
index cc88deb..d35c810 100644 (file)
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 Mal is a Clojure inspired Lisp interpreter.
 
-Mal is implemented in 72 languages:
+Mal is implemented in 73 languages:
 
 * Ada
 * GNU awk
@@ -29,6 +29,7 @@ Mal is implemented in 72 languages:
 * ES6 (ECMAScript 6 / ECMAScript 2015)
 * F#
 * Factor
+* Fantom
 * Forth
 * Go
 * Groovy
@@ -419,6 +420,18 @@ cd factor
 FACTOR_ROOTS=. factor -run=stepX_YYY
 ```
 
+### Fantom
+
+*The Fantom implementation was created by [Dov Murik](https://github.com/dubek)*
+
+The Fantom implementation of mal has been tested with Fantom 1.0.70.
+
+```
+cd fantom
+make lib/fan/stepX_YYY.pod
+STEP=stepX_YYY ./run
+```
+
 ### Forth
 
 *The Forth implementation was created by [Chris Houser (chouser)](https://github.com/chouser)*
diff --git a/fantom/Dockerfile b/fantom/Dockerfile
new file mode 100644 (file)
index 0000000..a52d7e0
--- /dev/null
@@ -0,0 +1,37 @@
+FROM ubuntu:bionic
+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
+##########################################################
+
+# Java and Unzip
+RUN apt-get -y install openjdk-8-jdk unzip
+
+# Fantom and JLine
+RUN cd /tmp && curl -sfLO https://bitbucket.org/fantom/fan-1.0/downloads/fantom-1.0.70.zip \
+    && unzip -q fantom-1.0.70.zip \
+    && rm fantom-1.0.70.zip \
+    && mv fantom-1.0.70 /opt/fantom \
+    && cd /opt/fantom \
+    && bash adm/unixsetup \
+    && curl -sfL -o /opt/fantom/lib/java/jline.jar https://repo1.maven.org/maven2/jline/jline/2.14.6/jline-2.14.6.jar
+
+ENV PATH /opt/fantom/bin:$PATH
+ENV HOME /mal
diff --git a/fantom/Makefile b/fantom/Makefile
new file mode 100644 (file)
index 0000000..97ebecc
--- /dev/null
@@ -0,0 +1,31 @@
+SOURCES_BASE = src/mallib/fan/interop.fan src/mallib/fan/reader.fan src/mallib/fan/types.fan
+SOURCES_LISP = src/mallib/fan/env.fan src/mallib/fan/core.fan src/stepA_mal/fan/main.fan
+SOURCES = $(SOURCES_BASE) $(SOURCES_LISP)
+
+all: dist
+
+dist: lib/fan/mal.pod
+
+lib/fan:
+       mkdir -p $@
+
+lib/fan/mal.pod: lib/fan/stepA_mal.pod
+       cp -a $< $@
+
+lib/fan/step%.pod: src/step%/build.fan src/step%/fan/*.fan lib/fan/mallib.pod
+       FAN_ENV=util::PathEnv FAN_ENV_PATH=. fan $<
+
+lib/fan/mallib.pod: src/mallib/build.fan src/mallib/fan/*.fan lib/fan
+       FAN_ENV=util::PathEnv FAN_ENV_PATH=. fan $<
+
+clean:
+       rm -rf lib
+
+.PHONY: stats
+
+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]"
diff --git a/fantom/run b/fantom/run
new file mode 100755 (executable)
index 0000000..3d75d6e
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/bash
+export FAN_ENV=util::PathEnv
+export FAN_ENV_PATH="$(dirname $0)"
+exec fan ${STEP:-stepA_mal} "$@"
diff --git a/fantom/src/mallib/build.fan b/fantom/src/mallib/build.fan
new file mode 100644 (file)
index 0000000..275b9da
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "mallib"
+    summary = "mal library pod"
+    depends = ["sys 1.0", "compiler 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/mallib/fan/core.fan b/fantom/src/mallib/fan/core.fan
new file mode 100644 (file)
index 0000000..4331db4
--- /dev/null
@@ -0,0 +1,117 @@
+class Core
+{
+  static private MalVal prn(MalVal[] a)
+  {
+    echo(a.join(" ") { it.toString(true) })
+    return MalNil.INSTANCE
+  }
+
+  static private MalVal println(MalVal[] a)
+  {
+    echo(a.join(" ") { it.toString(false) })
+    return MalNil.INSTANCE
+  }
+
+  static private MalVal readline(MalVal[] a)
+  {
+    line := Env.cur.prompt((a[0] as MalString).value)
+    return line == null ? MalNil.INSTANCE : MalString.make(line)
+  }
+
+  static private MalVal concat(MalVal[] a)
+  {
+    return MalList(a.reduce(MalVal[,]) |MalVal[] r, MalSeq v -> MalVal[]| { return r.addAll(v.value) })
+  }
+
+  static private MalVal apply(MalVal[] a)
+  {
+    f := a[0] as MalFunc
+    args := a[1..-2]
+    args.addAll(((MalSeq)a[-1]).value)
+    return f.call(args)
+  }
+
+  static private MalVal swap_bang(MalVal[] a)
+  {
+    atom := a[0] as MalAtom
+    MalVal[] args := [atom.value]
+    args.addAll(a[2..-1])
+    f := a[1] as MalFunc
+    return atom.set(f.call(args))
+  }
+
+  static Str:MalFunc ns()
+  {
+    return [
+      "=":           MalFunc { MalTypes.toMalBool(it[0] == it[1]) },
+      "throw":       MalFunc { throw MalException(it[0]) },
+
+      "nil?":        MalFunc { MalTypes.toMalBool(it[0] is MalNil) },
+      "true?":       MalFunc { MalTypes.toMalBool(it[0] is MalTrue) },
+      "false?":      MalFunc { MalTypes.toMalBool(it[0] is MalFalse) },
+      "string?":     MalFunc { MalTypes.toMalBool(it[0] is MalString && !((MalString)it[0]).isKeyword) },
+      "symbol":      MalFunc { MalSymbol.makeFromVal(it[0]) },
+      "symbol?":     MalFunc { MalTypes.toMalBool(it[0] is MalSymbol) },
+      "keyword":     MalFunc { MalString.makeKeyword((it[0] as MalString).value) },
+      "keyword?":    MalFunc { MalTypes.toMalBool(it[0] is MalString && ((MalString)it[0]).isKeyword) },
+      "number?":     MalFunc { MalTypes.toMalBool(it[0] is MalInteger) },
+      "fn?":         MalFunc { MalTypes.toMalBool(it[0] is MalFunc && !((it[0] as MalUserFunc)?->isMacro ?: false)) },
+      "macro?":      MalFunc { MalTypes.toMalBool(it[0] is MalUserFunc && ((MalUserFunc)it[0]).isMacro) },
+
+      "pr-str":      MalFunc { MalString.make(it.join(" ") |MalVal e -> Str| { e.toString(true) }) },
+      "str":         MalFunc { MalString.make(it.join("") |MalVal e -> Str| { e.toString(false) }) },
+      "prn":         MalFunc(#prn.func),
+      "println":     MalFunc(#println.func),
+      "read-string": MalFunc { Reader.read_str((it[0] as MalString).value) },
+      "readline":    MalFunc(#readline.func),
+      "slurp":       MalFunc { MalString.make(File((it[0] as MalString).value.toUri).readAllStr) },
+
+      "<":           MalFunc { MalTypes.toMalBool((it[0] as MalInteger).value < (it[1] as MalInteger).value) },
+      "<=":          MalFunc { MalTypes.toMalBool((it[0] as MalInteger).value <= (it[1] as MalInteger).value) },
+      ">":           MalFunc { MalTypes.toMalBool((it[0] as MalInteger).value > (it[1] as MalInteger).value) },
+      ">=":          MalFunc { MalTypes.toMalBool((it[0] as MalInteger).value >= (it[1] as MalInteger).value) },
+      "+":           MalFunc { MalInteger((it[0] as MalInteger).value + (it[1] as MalInteger).value) },
+      "-":           MalFunc { MalInteger((it[0] as MalInteger).value - (it[1] as MalInteger).value) },
+      "*":           MalFunc { MalInteger((it[0] as MalInteger).value * (it[1] as MalInteger).value) },
+      "/":           MalFunc { MalInteger((it[0] as MalInteger).value / (it[1] as MalInteger).value) },
+      "time-ms":     MalFunc { MalInteger(DateTime.nowTicks / 1000000) },
+
+      "list":        MalFunc { MalList(it) },
+      "list?":       MalFunc { MalTypes.toMalBool(it[0] is MalList) },
+      "vector":      MalFunc { MalVector(it) },
+      "vector?":     MalFunc { MalTypes.toMalBool(it[0] is MalVector) },
+      "hash-map":    MalFunc { MalHashMap.fromList(it) },
+      "map?":        MalFunc { MalTypes.toMalBool(it[0] is MalHashMap) },
+      "assoc":       MalFunc { (it[0] as MalHashMap).assoc(it[1..-1]) },
+      "dissoc":      MalFunc { (it[0] as MalHashMap).dissoc(it[1..-1]) },
+      "get":         MalFunc { it[0] is MalNil ? MalNil.INSTANCE : (it[0] as MalHashMap).get2((MalString)it[1], MalNil.INSTANCE) },
+      "contains?":   MalFunc { MalTypes.toMalBool((it[0] as MalHashMap).containsKey((MalString)it[1])) },
+      "keys":        MalFunc { MalList((it[0] as MalHashMap).keys) },
+      "vals":        MalFunc { MalList((it[0] as MalHashMap).vals) },
+
+      "sequential?": MalFunc { MalTypes.toMalBool(it[0] is MalSeq) },
+      "cons":        MalFunc { MalList([it[0]].addAll((it[1] as MalSeq).value)) },
+      "concat":      MalFunc(#concat.func),
+      "nth":         MalFunc { (it[0] as MalSeq).nth((it[1] as MalInteger).value) },
+      "first":       MalFunc { (it[0] as MalSeq)?.first ?: MalNil.INSTANCE },
+      "rest":        MalFunc { (it[0] as MalSeq)?.rest ?: MalList([,]) },
+      "empty?":      MalFunc { MalTypes.toMalBool((it[0] as MalSeq).isEmpty) },
+      "count":       MalFunc { MalInteger(it[0].count) },
+      "apply":       MalFunc(#apply.func),
+      "map":         MalFunc { (it[1] as MalSeq).map(it[0]) },
+
+      "conj":        MalFunc { (it[0] as MalSeq).conj(it[1..-1]) },
+      "seq":         MalFunc { it[0].seq },
+
+      "meta":        MalFunc { it[0].meta() },
+      "with-meta":   MalFunc { it[0].with_meta(it[1]) },
+      "atom":        MalFunc { MalAtom(it[0]) },
+      "atom?":       MalFunc { MalTypes.toMalBool(it[0] is MalAtom) },
+      "deref":       MalFunc { (it[0] as MalAtom).value },
+      "reset!":      MalFunc { (it[0] as MalAtom).set(it[1]) },
+      "swap!":       MalFunc(#swap_bang.func),
+
+      "fantom-eval": MalFunc { Interop.fantomEvaluate((it[0] as MalString).value) }
+    ]
+  }
+}
diff --git a/fantom/src/mallib/fan/env.fan b/fantom/src/mallib/fan/env.fan
new file mode 100644 (file)
index 0000000..644c181
--- /dev/null
@@ -0,0 +1,40 @@
+class MalEnv
+{
+  private Str:MalVal data := [:]
+  private MalEnv? outer
+
+  new make(MalEnv? outer := null, MalSeq? binds := null, MalSeq? exprs := null)
+  {
+    this.outer = outer
+    if (binds != null && exprs != null)
+    {
+      for (i := 0; i < binds.count; i++)
+      {
+        if ((binds[i] as MalSymbol).value == "&")
+        {
+          set(binds[i + 1], MalList(exprs[i..-1]))
+          break
+        }
+        else
+          set(binds[i], exprs[i])
+      }
+    }
+  }
+
+  MalVal set(MalSymbol key, MalVal value)
+  {
+    data[key.value] = value
+    return value
+  }
+
+  MalEnv? find(MalSymbol key)
+  {
+    return data.containsKey(key.value) ? this : outer?.find(key)
+  }
+
+  MalVal get(MalSymbol key)
+  {
+    foundEnv := find(key) ?: throw Err("'$key.value' not found")
+    return (MalVal)foundEnv.data[key.value]
+  }
+}
diff --git a/fantom/src/mallib/fan/interop.fan b/fantom/src/mallib/fan/interop.fan
new file mode 100644 (file)
index 0000000..71b0d99
--- /dev/null
@@ -0,0 +1,61 @@
+using compiler
+
+internal class Interop
+{
+  static Pod? compile(Str innerBody)
+  {
+    ci := CompilerInput
+    {
+      podName     = "mal_fantom_interop_${DateTime.nowUnique}"
+      summary     = ""
+      isScript    = true
+      version     = Version.defVal
+      log.level   = LogLevel.silent
+      output      = CompilerOutputMode.transientPod
+      mode        = CompilerInputMode.str
+      srcStr      = "class InteropDummyClass {\nstatic Obj? _evalfunc() {\n $innerBody \n}\n}"
+      srcStrLoc   = Loc("mal_fantom_interop")
+    }
+    try
+      return Compiler(ci).compile.transientPod
+    catch (CompilerErr e)
+      return null
+  }
+
+  static Obj? evaluate(Str line)
+  {
+    p := compile(line)
+    if (p == null)
+      p = compile("return $line")
+    if (p == null)
+      p = compile("$line\nreturn null")
+    if (p == null)
+      return null
+    method := p.types.first.method("_evalfunc")
+    try
+      return method.call()
+    catch (Err e)
+      return null
+  }
+
+  static MalVal fantomToMal(Obj? obj)
+  {
+    if (obj == null)
+      return MalNil.INSTANCE
+    else if (obj is Bool)
+      return MalTypes.toMalBool((Bool)obj)
+    else if (obj is Int)
+      return MalInteger((Int)obj)
+    else if (obj is List)
+      return MalList((obj as List).map { fantomToMal(it) })
+    else if (obj is Map)
+      return MalHashMap.fromMap((obj as Map).map { fantomToMal(it) })
+    else
+      return MalString.make(obj.toStr)
+  }
+
+  static MalVal fantomEvaluate(Str line)
+  {
+    return fantomToMal(evaluate(line))
+  }
+}
diff --git a/fantom/src/mallib/fan/reader.fan b/fantom/src/mallib/fan/reader.fan
new file mode 100644 (file)
index 0000000..edf9fe1
--- /dev/null
@@ -0,0 +1,104 @@
+internal class TokenReader
+{
+  const Str[] tokens
+  private Int position := 0
+
+  new make(Str[] new_tokens) { tokens = new_tokens }
+
+  Str? peek()
+  {
+    if (position >= tokens.size) return null
+    return tokens[position]
+  }
+
+  Str next() { return tokens[position++] }
+}
+
+class Reader
+{
+  private static Str[] tokenize(Str s)
+  {
+    r := Regex <|[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)|>
+    m := r.matcher(s)
+    tokens := Str[,]
+    while (m.find())
+    {
+      token := m.group(1)
+      if (token.isEmpty || token[0] == ';') continue
+      tokens.add(m.group(1))
+    }
+    return tokens
+  }
+
+  private static Str unescape_str(Str s)
+  {
+    return s.replace("\\\\", "\u029e").replace("\\\"", "\"").replace("\\n", "\n").replace("\u029e", "\\")
+  }
+
+  private static MalVal read_atom(TokenReader reader)
+  {
+    token := reader.next
+    intRegex := Regex <|^-?\d+$|>
+    if (token == "nil") return MalNil.INSTANCE
+    if (token == "true") return MalTrue.INSTANCE
+    if (token == "false") return MalFalse.INSTANCE
+    if (intRegex.matches(token)) return MalInteger(token.toInt)
+    if (token[0] == '"') return MalString.make(unescape_str(token[1..-2]))
+    if (token[0] == ':') return MalString.makeKeyword(token[1..-1])
+    return MalSymbol(token)
+  }
+
+  private static MalVal[] read_seq(TokenReader reader, Str open, Str close)
+  {
+    reader.next
+    values := MalVal[,]
+    token := reader.peek
+    while (token != close)
+    {
+      if (token == null) throw Err("expected '$close', got EOF")
+      values.add(read_form(reader))
+      token = reader.peek
+    }
+    if (token != close) throw Err("Missing '$close'")
+    reader.next
+    return values
+  }
+
+  private static MalVal read_form(TokenReader reader)
+  {
+    switch (reader.peek)
+    {
+      case "\'":
+        reader.next
+        return MalList([MalSymbol("quote"), read_form(reader)])
+      case "`":
+        reader.next
+        return MalList([MalSymbol("quasiquote"), read_form(reader)])
+      case "~":
+        reader.next
+        return MalList([MalSymbol("unquote"), read_form(reader)])
+      case "~@":
+        reader.next
+        return MalList([MalSymbol("splice-unquote"), read_form(reader)])
+      case "^":
+        reader.next
+        meta := read_form(reader)
+        return MalList([MalSymbol("with-meta"), read_form(reader), meta])
+      case "@":
+        reader.next
+        return MalList([MalSymbol("deref"), read_form(reader)])
+      case "(": return MalList(read_seq(reader, "(", ")"))
+      case ")": throw Err("unexpected ')'")
+      case "[": return MalVector(read_seq(reader, "[", "]"))
+      case "]": throw Err("unexpected ']'")
+      case "{": return MalHashMap.fromList(read_seq(reader, "{", "}"))
+      case "}": throw Err("unexpected '}'")
+      default:  return read_atom(reader)
+    }
+  }
+
+  static MalVal read_str(Str s)
+  {
+    return read_form(TokenReader(tokenize(s)));
+  }
+}
diff --git a/fantom/src/mallib/fan/types.fan b/fantom/src/mallib/fan/types.fan
new file mode 100644 (file)
index 0000000..c30ff12
--- /dev/null
@@ -0,0 +1,234 @@
+mixin MalVal
+{
+  virtual Str toString(Bool readable) { return toStr }
+  virtual Int count() { throw Err("count not implemented") }
+  virtual MalVal seq() { throw Err("seq not implemented") }
+  abstract MalVal meta()
+  abstract MalVal with_meta(MalVal newMeta)
+}
+
+const mixin MalValNoMeta : MalVal
+{
+  override MalVal meta() { return MalNil.INSTANCE }
+  override MalVal with_meta(MalVal newMeta) { return this }
+}
+
+const mixin MalFalseyVal
+{
+}
+
+const class MalNil : MalValNoMeta, MalFalseyVal
+{
+  static const MalNil INSTANCE := MalNil()
+  override Bool equals(Obj? that) { return that is MalNil }
+  override Str toString(Bool readable) { return "nil" }
+  override Int count() { return 0 }
+  override MalVal seq() { return this }
+}
+
+const class MalTrue : MalValNoMeta
+{
+  static const MalTrue INSTANCE := MalTrue()
+  override Bool equals(Obj? that) { return that is MalTrue }
+  override Str toString(Bool readable) { return "true" }
+}
+
+const class MalFalse : MalValNoMeta, MalFalseyVal
+{
+  static const MalFalse INSTANCE := MalFalse()
+  override Bool equals(Obj? that) { return that is MalFalse }
+  override Str toString(Bool readable) { return "false" }
+}
+
+const class MalInteger : MalValNoMeta
+{
+  const Int value
+  new make(Int v) { value = v }
+  override Bool equals(Obj? that) { return that is MalInteger && (that as MalInteger).value == value }
+  override Str toString(Bool readable) { return value.toStr }
+}
+
+abstract class MalValBase : MalVal
+{
+  private MalVal? metaVal := null
+  override Str toString(Bool readable) { return toStr }
+  override Int count() { throw Err("count not implemented") }
+  override MalVal seq() { throw Err("seq not implemented") }
+  abstract This dup()
+  override MalVal meta() { return metaVal ?: MalNil.INSTANCE }
+  override MalVal with_meta(MalVal newMeta)
+  {
+    v := dup
+    v.metaVal = newMeta
+    return v
+  }
+}
+
+class MalSymbol : MalValBase
+{
+  const Str value
+  new make(Str v) { value = v }
+  new makeFromVal(MalVal v)
+  {
+    if (v is MalSymbol) return v
+    value = (v as MalString).value
+  }
+  override Bool equals(Obj? that) { return that is MalSymbol && (that as MalSymbol).value == value }
+  override Str toString(Bool readable) { return value }
+  override This dup() { return make(value) }
+}
+
+class MalString : MalValBase
+{
+  const Str value
+  new make(Str v) { value = v }
+  new makeKeyword(Str v) { value = "\u029e$v" }
+  override Bool equals(Obj? that) { return that is MalString && (that as MalString).value == value }
+  override Str toString(Bool readable)
+  {
+    if (isKeyword) return ":${value[1..-1]}"
+    if (readable)
+      return "\"${escapeStr(value)}\""
+    else
+      return value
+  }
+  Bool isKeyword() { return !value.isEmpty && value[0] == '\u029e' }
+  static Str escapeStr(Str s)
+  {
+    return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n")
+  }
+  override MalVal seq()
+  {
+    if (value.size == 0) return MalNil.INSTANCE
+    return MalList(value.chars.map |Int c -> MalString| { MalString.make(Str.fromChars([c])) })
+  }
+  override This dup() { return make(value) }
+}
+
+abstract class MalSeq : MalValBase
+{
+  MalVal[] value { protected set }
+  new make(MalVal[] v) { value = v.ro }
+  override Bool equals(Obj? that) { return that is MalSeq && (that as MalSeq).value == value }
+  Bool isEmpty() { return value.isEmpty }
+  override Int count() { return value.size }
+  @Operator MalVal get(Int index) { return value[index] }
+  @Operator MalVal[] getRange(Range range) { return value[range] }
+  protected Str serialize(Bool readable) { return value.join(" ") { it.toString(readable) } }
+  abstract MalSeq drop(Int n)
+  MalVal nth(Int index) { return index < count ? get(index) : throw Err("nth: index out of range") }
+  MalVal first() { return isEmpty ? MalNil.INSTANCE : value[0] }
+  MalList rest() { return MalList(isEmpty ? [,] : value[1..-1]) }
+  MalList map(MalFunc f) { return MalList(value.map { f.call([it]) } ) }
+  abstract MalSeq conj(MalVal[] args)
+}
+
+class MalList : MalSeq
+{
+  new make(MalVal[] v) : super.make(v) {}
+  override Str toString(Bool readable) { return "(${serialize(readable)})" }
+  override MalList drop(Int n) { return make(value[n..-1]) }
+  override MalVal seq() { return isEmpty ? MalNil.INSTANCE : this }
+  override MalList conj(MalVal[] args) { return MalList(value.rw.insertAll(0, args.reverse)) }
+  override This dup() { return make(value) }
+}
+
+class MalVector : MalSeq
+{
+  new make(MalVal[] v) : super.make(v) {}
+  override Str toString(Bool readable) { return "[${serialize(readable)}]" }
+  override MalVector drop(Int n) { return make(value[n..-1]) }
+  override MalVal seq() { return isEmpty ? MalNil.INSTANCE : MalList(value) }
+  override MalVector conj(MalVal[] args) { return MalVector(value.rw.addAll(args)) }
+  override This dup() { return make(value) }
+}
+
+class MalHashMap : MalValBase
+{
+  Str:MalVal value { private set }
+  new fromList(MalVal[] lst) {
+    m := [Str:MalVal][:]
+    for (i := 0; i < lst.size; i += 2)
+      m.add((lst[i] as MalString).value, (MalVal)lst[i + 1])
+    value = m.ro
+  }
+  new fromMap(Str:MalVal m) { value = m.ro }
+  override Bool equals(Obj? that) { return that is MalHashMap && (that as MalHashMap).value == value }
+  override Str toString(Bool readable)
+  {
+    elements := Str[,]
+    value.each(|MalVal v, Str k| { elements.add(MalString.make(k).toString(readable)); elements.add(v.toString(readable)) })
+    s := elements.join(" ")
+    return "{$s}"
+  }
+  override Int count() { return value.size }
+  @Operator MalVal get(Str key) { return value[key] }
+  MalVal get2(MalString key, MalVal? def := null) { return value.get(key.value, def) }
+  Bool containsKey(MalString key) { return value.containsKey(key.value) }
+  MalVal[] keys() { return value.keys.map { MalString.make(it) } }
+  MalVal[] vals() { return value.vals }
+  MalHashMap assoc(MalVal[] args)
+  {
+    newValue := value.dup
+    for (i := 0; i < args.size; i += 2)
+      newValue.set((args[i] as MalString).value, args[i + 1])
+    return fromMap(newValue)
+  }
+  MalHashMap dissoc(MalVal[] args)
+  {
+    newValue := value.dup
+    args.each { newValue.remove((it as MalString).value) }
+    return fromMap(newValue)
+  }
+  override This dup() { return fromMap(value) }
+}
+
+class MalFunc : MalValBase
+{
+  protected |MalVal[] a -> MalVal| f
+  new make(|MalVal[] a -> MalVal| func) { f = func }
+  MalVal call(MalVal[] a) { return f(a) }
+  override Str toString(Bool readable) { return "<Function>" }
+  override This dup() { return make(f) }
+}
+
+class MalUserFunc : MalFunc
+{
+  MalVal ast { private set }
+  private MalEnv env
+  private MalSeq params
+  Bool isMacro := false
+  new make(MalVal ast, MalEnv env, MalSeq params, |MalVal[] a -> MalVal| func, Bool isMacro := false) : super.make(func)
+  {
+    this.ast = ast
+    this.env = env
+    this.params = params
+    this.isMacro = isMacro
+  }
+  MalEnv genEnv(MalSeq args) { return MalEnv(env, params, args) }
+  override Str toString(Bool readable) { return "<Function:args=${params.toString(readable)}, isMacro=${isMacro}>" }
+  override This dup() { return make(ast, env, params, f, isMacro) }
+}
+
+class MalAtom : MalValBase
+{
+  MalVal value
+  new make(MalVal v) { value = v }
+  override Str toString(Bool readable) { return "(atom ${value.toString(readable)})" }
+  override Bool equals(Obj? that) { return that is MalAtom && (that as MalAtom).value == value }
+  MalVal set(MalVal v) { value = v; return value }
+  override This dup() { return make(value) }
+}
+
+class MalTypes
+{
+  static MalVal toMalBool(Bool cond) { return cond ? MalTrue.INSTANCE : MalFalse.INSTANCE }
+  static Bool isPair(MalVal a) { return a is MalSeq && !(a as MalSeq).isEmpty }
+}
+
+const class MalException : Err
+{
+  const Str serializedValue
+  new make(MalVal v) : super.make("Mal exception") { serializedValue = v.toString(true) }
+  MalVal getValue() { return Reader.read_str(serializedValue) }
+}
diff --git a/fantom/src/step0_repl/build.fan b/fantom/src/step0_repl/build.fan
new file mode 100644 (file)
index 0000000..e16a2a3
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "step0_repl"
+    summary = "mal step0_repl pod"
+    depends = ["sys 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/step0_repl/fan/main.fan b/fantom/src/step0_repl/fan/main.fan
new file mode 100644 (file)
index 0000000..efccdeb
--- /dev/null
@@ -0,0 +1,32 @@
+class Main
+{
+  static Str READ(Str s)
+  {
+    return s
+  }
+
+  static Str EVAL(Str ast, Str env)
+  {
+    return ast
+  }
+
+  static Str PRINT(Str exp)
+  {
+    return exp
+  }
+
+  static Str REP(Str s, Str env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main()
+  {
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      echo(REP(line, ""))
+    }
+  }
+}
diff --git a/fantom/src/step1_read_print/build.fan b/fantom/src/step1_read_print/build.fan
new file mode 100644 (file)
index 0000000..3bb3998
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "step1_read_print"
+    summary = "mal step1_read_print pod"
+    depends = ["sys 1.0", "mallib 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/step1_read_print/fan/main.fan b/fantom/src/step1_read_print/fan/main.fan
new file mode 100644 (file)
index 0000000..5e6f27d
--- /dev/null
@@ -0,0 +1,37 @@
+using mallib
+
+class Main
+{
+  static MalVal READ(Str s)
+  {
+    return Reader.read_str(s)
+  }
+
+  static MalVal EVAL(MalVal ast, Str env)
+  {
+    return ast
+  }
+
+  static Str PRINT(MalVal exp)
+  {
+    return exp.toString(true)
+  }
+
+  static Str REP(Str s, Str env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main()
+  {
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      try
+        echo(REP(line, ""))
+      catch (Err e)
+        echo("Error: $e.msg")
+    }
+  }
+}
diff --git a/fantom/src/step2_eval/build.fan b/fantom/src/step2_eval/build.fan
new file mode 100644 (file)
index 0000000..792a7f7
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "step2_eval"
+    summary = "mal step2_eval pod"
+    depends = ["sys 1.0", "mallib 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/step2_eval/fan/main.fan b/fantom/src/step2_eval/fan/main.fan
new file mode 100644 (file)
index 0000000..0ea7bb6
--- /dev/null
@@ -0,0 +1,70 @@
+using mallib
+
+class Main
+{
+  static MalVal READ(Str s)
+  {
+    return Reader.read_str(s)
+  }
+
+  static MalVal eval_ast(MalVal ast, Str:MalFunc env)
+  {
+    switch (ast.typeof)
+    {
+      case MalSymbol#:
+        varName := (ast as MalSymbol).value
+        varVal := env[varName] ?: throw Err("'$varName' not found")
+        return (MalVal)varVal
+      case MalList#:
+        newElements := (ast as MalList).value.map { EVAL(it, env) }
+        return MalList(newElements)
+      case MalVector#:
+        newElements := (ast as MalVector).value.map { EVAL(it, env) }
+        return MalVector(newElements)
+      case MalHashMap#:
+        newElements := (ast as MalHashMap).value.map |MalVal v -> MalVal| { return EVAL(v, env) }
+        return MalHashMap.fromMap(newElements)
+      default:
+        return ast
+    }
+  }
+
+  static MalVal EVAL(MalVal ast, Str:MalFunc env)
+  {
+    if (!(ast is MalList)) return eval_ast(ast, env)
+    astList := ast as MalList
+    if (astList.isEmpty) return ast
+    evaled_ast := eval_ast(ast, env) as MalList
+    f := evaled_ast[0] as MalFunc
+    return f.call(evaled_ast[1..-1])
+  }
+
+  static Str PRINT(MalVal exp)
+  {
+    return exp.toString(true)
+  }
+
+  static Str REP(Str s, Str:MalFunc env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main()
+  {
+    env := [
+      "+": MalFunc { MalInteger((it[0] as MalInteger).value + (it[1] as MalInteger).value) },
+      "-": MalFunc { MalInteger((it[0] as MalInteger).value - (it[1] as MalInteger).value) },
+      "*": MalFunc { MalInteger((it[0] as MalInteger).value * (it[1] as MalInteger).value) },
+      "/": MalFunc { MalInteger((it[0] as MalInteger).value / (it[1] as MalInteger).value) }
+    ]
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      try
+        echo(REP(line, env))
+      catch (Err e)
+        echo("Error: $e.msg")
+    }
+  }
+}
diff --git a/fantom/src/step3_env/build.fan b/fantom/src/step3_env/build.fan
new file mode 100644 (file)
index 0000000..598092f
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "step3_env"
+    summary = "mal step3_env pod"
+    depends = ["sys 1.0", "mallib 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/step3_env/fan/main.fan b/fantom/src/step3_env/fan/main.fan
new file mode 100644 (file)
index 0000000..4f8294b
--- /dev/null
@@ -0,0 +1,79 @@
+using mallib
+
+class Main
+{
+  static MalVal READ(Str s)
+  {
+    return Reader.read_str(s)
+  }
+
+  static MalVal eval_ast(MalVal ast, MalEnv env)
+  {
+    switch (ast.typeof)
+    {
+      case MalSymbol#:
+        return env.get(ast)
+      case MalList#:
+        newElements := (ast as MalList).value.map { EVAL(it, env) }
+        return MalList(newElements)
+      case MalVector#:
+        newElements := (ast as MalVector).value.map { EVAL(it, env) }
+        return MalVector(newElements)
+      case MalHashMap#:
+        newElements := (ast as MalHashMap).value.map |MalVal v -> MalVal| { return EVAL(v, env) }
+        return MalHashMap.fromMap(newElements)
+      default:
+        return ast
+    }
+  }
+
+  static MalVal EVAL(MalVal ast, MalEnv env)
+  {
+    if (!(ast is MalList)) return eval_ast(ast, env)
+    astList := ast as MalList
+    if (astList.isEmpty) return ast
+    switch ((astList[0] as MalSymbol).value)
+    {
+      case "def!":
+        return env.set(astList[1], EVAL(astList[2], env))
+      case "let*":
+        let_env := MalEnv(env)
+        varList := (astList[1] as MalSeq)
+        for (i := 0; i < varList.count; i += 2)
+          let_env.set(varList[i], EVAL(varList[i + 1], let_env))
+        return EVAL(astList[2], let_env)
+      default:
+        evaled_ast := eval_ast(ast, env) as MalList
+        f := evaled_ast[0] as MalFunc
+        return f.call(evaled_ast[1..-1])
+    }
+  }
+
+  static Str PRINT(MalVal exp)
+  {
+    return exp.toString(true)
+  }
+
+  static Str REP(Str s, MalEnv env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main()
+  {
+    repl_env := MalEnv()
+    repl_env.set(MalSymbol("+"), MalFunc { MalInteger((it[0] as MalInteger).value + (it[1] as MalInteger).value) })
+    repl_env.set(MalSymbol("-"), MalFunc { MalInteger((it[0] as MalInteger).value - (it[1] as MalInteger).value) })
+    repl_env.set(MalSymbol("*"), MalFunc { MalInteger((it[0] as MalInteger).value * (it[1] as MalInteger).value) })
+    repl_env.set(MalSymbol("/"), MalFunc { MalInteger((it[0] as MalInteger).value / (it[1] as MalInteger).value) })
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      try
+        echo(REP(line, repl_env))
+      catch (Err e)
+        echo("Error: $e.msg")
+    }
+  }
+}
diff --git a/fantom/src/step4_if_fn_do/build.fan b/fantom/src/step4_if_fn_do/build.fan
new file mode 100644 (file)
index 0000000..7cf25b3
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "step4_if_fn_do"
+    summary = "mal step4_if_fn_do pod"
+    depends = ["sys 1.0", "mallib 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/step4_if_fn_do/fan/main.fan b/fantom/src/step4_if_fn_do/fan/main.fan
new file mode 100644 (file)
index 0000000..22d6ea4
--- /dev/null
@@ -0,0 +1,91 @@
+using mallib
+
+class Main
+{
+  static MalVal READ(Str s)
+  {
+    return Reader.read_str(s)
+  }
+
+  static MalVal eval_ast(MalVal ast, MalEnv env)
+  {
+    switch (ast.typeof)
+    {
+      case MalSymbol#:
+        return env.get(ast)
+      case MalList#:
+        newElements := (ast as MalList).value.map { EVAL(it, env) }
+        return MalList(newElements)
+      case MalVector#:
+        newElements := (ast as MalVector).value.map { EVAL(it, env) }
+        return MalVector(newElements)
+      case MalHashMap#:
+        newElements := (ast as MalHashMap).value.map |MalVal v -> MalVal| { return EVAL(v, env) }
+        return MalHashMap.fromMap(newElements)
+      default:
+        return ast
+    }
+  }
+
+  static MalVal EVAL(MalVal ast, MalEnv env)
+  {
+    if (!(ast is MalList)) return eval_ast(ast, env)
+    astList := ast as MalList
+    if (astList.isEmpty) return ast
+    switch ((astList[0] as MalSymbol)?.value)
+    {
+      case "def!":
+        return env.set(astList[1], EVAL(astList[2], env))
+      case "let*":
+        let_env := MalEnv(env)
+        varList := astList[1] as MalSeq
+        for (i := 0; i < varList.count; i += 2)
+          let_env.set(varList[i], EVAL(varList[i + 1], let_env))
+        return EVAL(astList[2], let_env)
+      case "do":
+        eval_ast(MalList(astList[1..-2]), env)
+        return EVAL(astList[-1], env)
+      case "if":
+        if (EVAL(astList[1], env) is MalFalseyVal)
+          return astList.count > 3 ? EVAL(astList[3], env) : MalNil.INSTANCE
+        else
+          return EVAL(astList[2], env)
+      case "fn*":
+        return MalFunc { EVAL(astList[2], MalEnv(env, (astList[1] as MalSeq), MalList(it))) }
+      default:
+        evaled_ast := eval_ast(ast, env) as MalList
+        f := evaled_ast[0] as MalFunc
+        return f.call(evaled_ast[1..-1])
+    }
+  }
+
+  static Str PRINT(MalVal exp)
+  {
+    return exp.toString(true)
+  }
+
+  static Str REP(Str s, MalEnv env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main()
+  {
+    repl_env := MalEnv()
+    // core.fan: defined using Fantom
+    Core.ns.each |MalFunc V, Str K| { repl_env.set(MalSymbol(K), V) }
+
+    // core.mal: defined using the language itself
+    REP("(def! not (fn* (a) (if a false true)))", repl_env)
+
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      try
+        echo(REP(line, repl_env))
+      catch (Err e)
+        echo("Error: $e.msg")
+    }
+  }
+}
diff --git a/fantom/src/step5_tco/build.fan b/fantom/src/step5_tco/build.fan
new file mode 100644 (file)
index 0000000..d96402c
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "step5_tco"
+    summary = "mal step5_tco pod"
+    depends = ["sys 1.0", "mallib 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/step5_tco/fan/main.fan b/fantom/src/step5_tco/fan/main.fan
new file mode 100644 (file)
index 0000000..eeab71a
--- /dev/null
@@ -0,0 +1,113 @@
+using mallib
+
+class Main
+{
+  static MalVal READ(Str s)
+  {
+    return Reader.read_str(s)
+  }
+
+  static MalVal eval_ast(MalVal ast, MalEnv env)
+  {
+    switch (ast.typeof)
+    {
+      case MalSymbol#:
+        return env.get(ast)
+      case MalList#:
+        newElements := (ast as MalList).value.map { EVAL(it, env) }
+        return MalList(newElements)
+      case MalVector#:
+        newElements := (ast as MalVector).value.map { EVAL(it, env) }
+        return MalVector(newElements)
+      case MalHashMap#:
+        newElements := (ast as MalHashMap).value.map |MalVal v -> MalVal| { return EVAL(v, env) }
+        return MalHashMap.fromMap(newElements)
+      default:
+        return ast
+    }
+  }
+
+  static MalVal EVAL(MalVal ast, MalEnv env)
+  {
+    while (true)
+    {
+      if (!(ast is MalList)) return eval_ast(ast, env)
+      astList := ast as MalList
+      if (astList.isEmpty) return ast
+      switch ((astList[0] as MalSymbol)?.value)
+      {
+        case "def!":
+          return env.set(astList[1], EVAL(astList[2], env))
+        case "let*":
+          let_env := MalEnv(env)
+          varList := astList[1] as MalSeq
+          for (i := 0; i < varList.count; i += 2)
+            let_env.set(varList[i], EVAL(varList[i + 1], let_env))
+          env = let_env
+          ast = astList[2]
+          // TCO
+        case "do":
+          eval_ast(MalList(astList[1..-2]), env)
+          ast = astList[-1]
+          // TCO
+        case "if":
+          if (EVAL(astList[1], env) is MalFalseyVal)
+            ast = astList.count > 3 ? astList[3] : MalNil.INSTANCE
+          else
+            ast = astList[2]
+          // TCO
+        case "fn*":
+          f := |MalVal[] a -> MalVal|
+          {
+            return EVAL(astList[2], MalEnv(env, (astList[1] as MalSeq), MalList(a)))
+          }
+          return MalUserFunc(astList[2], env, (MalSeq)astList[1], f)
+        default:
+          evaled_ast := eval_ast(ast, env) as MalList
+          switch (evaled_ast[0].typeof)
+          {
+            case MalUserFunc#:
+              user_fn := evaled_ast[0] as MalUserFunc
+              ast = user_fn.ast
+              env = user_fn.genEnv(evaled_ast.drop(1))
+              // TCO
+            case MalFunc#:
+              return (evaled_ast[0] as MalFunc).call(evaled_ast[1..-1])
+            default:
+              throw Err("Unknown type")
+          }
+      }
+    }
+    return MalNil.INSTANCE // never reached
+  }
+
+  static Str PRINT(MalVal exp)
+  {
+    return exp.toString(true)
+  }
+
+  static Str REP(Str s, MalEnv env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main()
+  {
+    repl_env := MalEnv()
+    // core.fan: defined using Fantom
+    Core.ns.each |MalFunc V, Str K| { repl_env.set(MalSymbol(K), V) }
+
+    // core.mal: defined using the language itself
+    REP("(def! not (fn* (a) (if a false true)))", repl_env)
+
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      try
+        echo(REP(line, repl_env))
+      catch (Err e)
+        echo("Error: $e.msg")
+    }
+  }
+}
diff --git a/fantom/src/step6_file/build.fan b/fantom/src/step6_file/build.fan
new file mode 100644 (file)
index 0000000..93e255f
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "step6_file"
+    summary = "mal step6_file pod"
+    depends = ["sys 1.0", "mallib 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/step6_file/fan/main.fan b/fantom/src/step6_file/fan/main.fan
new file mode 100644 (file)
index 0000000..aad0366
--- /dev/null
@@ -0,0 +1,122 @@
+using mallib
+
+class Main
+{
+  static MalVal READ(Str s)
+  {
+    return Reader.read_str(s)
+  }
+
+  static MalVal eval_ast(MalVal ast, MalEnv env)
+  {
+    switch (ast.typeof)
+    {
+      case MalSymbol#:
+        return env.get(ast)
+      case MalList#:
+        newElements := (ast as MalList).value.map { EVAL(it, env) }
+        return MalList(newElements)
+      case MalVector#:
+        newElements := (ast as MalVector).value.map { EVAL(it, env) }
+        return MalVector(newElements)
+      case MalHashMap#:
+        newElements := (ast as MalHashMap).value.map |MalVal v -> MalVal| { return EVAL(v, env) }
+        return MalHashMap.fromMap(newElements)
+      default:
+        return ast
+    }
+  }
+
+  static MalVal EVAL(MalVal ast, MalEnv env)
+  {
+    while (true)
+    {
+      if (!(ast is MalList)) return eval_ast(ast, env)
+      astList := ast as MalList
+      if (astList.isEmpty) return ast
+      switch ((astList[0] as MalSymbol)?.value)
+      {
+        case "def!":
+          return env.set(astList[1], EVAL(astList[2], env))
+        case "let*":
+          let_env := MalEnv(env)
+          varList := astList[1] as MalSeq
+          for (i := 0; i < varList.count; i += 2)
+            let_env.set(varList[i], EVAL(varList[i + 1], let_env))
+          env = let_env
+          ast = astList[2]
+          // TCO
+        case "do":
+          eval_ast(MalList(astList[1..-2]), env)
+          ast = astList[-1]
+          // TCO
+        case "if":
+          if (EVAL(astList[1], env) is MalFalseyVal)
+            ast = astList.count > 3 ? astList[3] : MalNil.INSTANCE
+          else
+            ast = astList[2]
+          // TCO
+        case "fn*":
+          f := |MalVal[] a -> MalVal|
+          {
+            return EVAL(astList[2], MalEnv(env, (astList[1] as MalSeq), MalList(a)))
+          }
+          return MalUserFunc(astList[2], env, (MalSeq)astList[1], f)
+        default:
+          evaled_ast := eval_ast(ast, env) as MalList
+          switch (evaled_ast[0].typeof)
+          {
+            case MalUserFunc#:
+              user_fn := evaled_ast[0] as MalUserFunc
+              ast = user_fn.ast
+              env = user_fn.genEnv(evaled_ast.drop(1))
+              // TCO
+            case MalFunc#:
+              return (evaled_ast[0] as MalFunc).call(evaled_ast[1..-1])
+            default:
+              throw Err("Unknown type")
+          }
+      }
+    }
+    return MalNil.INSTANCE // never reached
+  }
+
+  static Str PRINT(MalVal exp)
+  {
+    return exp.toString(true)
+  }
+
+  static Str REP(Str s, MalEnv env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main(Str[] args)
+  {
+    repl_env := MalEnv()
+    // core.fan: defined using Fantom
+    Core.ns.each |MalFunc V, Str K| { repl_env.set(MalSymbol(K), V) }
+    repl_env.set(MalSymbol("eval"), MalFunc { EVAL(it[0], repl_env) })
+    repl_env.set(MalSymbol("*ARGV*"), MalList((args.isEmpty ? args : args[1..-1]).map { MalString.make(it) }))
+
+    // core.mal: defined using the language itself
+    REP("(def! not (fn* (a) (if a false true)))", repl_env)
+    REP("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env)
+
+    if (!args.isEmpty)
+    {
+      REP("(load-file \"${args[0]}\")", repl_env)
+      return
+    }
+
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      try
+        echo(REP(line, repl_env))
+      catch (Err e)
+        echo("Error: $e.msg")
+    }
+  }
+}
diff --git a/fantom/src/step7_quote/build.fan b/fantom/src/step7_quote/build.fan
new file mode 100644 (file)
index 0000000..a32dfca
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "step7_quote"
+    summary = "mal step7_quote pod"
+    depends = ["sys 1.0", "mallib 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/step7_quote/fan/main.fan b/fantom/src/step7_quote/fan/main.fan
new file mode 100644 (file)
index 0000000..7158f03
--- /dev/null
@@ -0,0 +1,143 @@
+using mallib
+
+class Main
+{
+  static MalVal quasiquote(MalVal ast)
+  {
+    if (!MalTypes.isPair(ast))
+      return MalList(MalVal[MalSymbol("quote"), ast])
+    astSeq := ast as MalSeq
+    if ((astSeq[0] as MalSymbol)?.value == "unquote")
+      return astSeq[1]
+    if (MalTypes.isPair(astSeq[0]))
+    {
+      ast0Seq := astSeq[0] as MalSeq
+      if ((ast0Seq[0] as MalSymbol)?.value == "splice-unquote")
+        return MalList(MalVal[MalSymbol("concat"), ast0Seq[1], quasiquote(astSeq.drop(1))])
+    }
+    return MalList(MalVal[MalSymbol("cons"), quasiquote(astSeq[0]), quasiquote(astSeq.drop(1))])
+  }
+
+  static MalVal READ(Str s)
+  {
+    return Reader.read_str(s)
+  }
+
+  static MalVal eval_ast(MalVal ast, MalEnv env)
+  {
+    switch (ast.typeof)
+    {
+      case MalSymbol#:
+        return env.get(ast)
+      case MalList#:
+        newElements := (ast as MalList).value.map { EVAL(it, env) }
+        return MalList(newElements)
+      case MalVector#:
+        newElements := (ast as MalVector).value.map { EVAL(it, env) }
+        return MalVector(newElements)
+      case MalHashMap#:
+        newElements := (ast as MalHashMap).value.map |MalVal v -> MalVal| { return EVAL(v, env) }
+        return MalHashMap.fromMap(newElements)
+      default:
+        return ast
+    }
+  }
+
+  static MalVal EVAL(MalVal ast, MalEnv env)
+  {
+    while (true)
+    {
+      if (!(ast is MalList)) return eval_ast(ast, env)
+      astList := ast as MalList
+      if (astList.isEmpty) return ast
+      switch ((astList[0] as MalSymbol)?.value)
+      {
+        case "def!":
+          return env.set(astList[1], EVAL(astList[2], env))
+        case "let*":
+          let_env := MalEnv(env)
+          varList := astList[1] as MalSeq
+          for (i := 0; i < varList.count; i += 2)
+            let_env.set(varList[i], EVAL(varList[i + 1], let_env))
+          env = let_env
+          ast = astList[2]
+          // TCO
+        case "quote":
+          return astList[1]
+        case "quasiquote":
+          ast = quasiquote(astList[1])
+          // TCO
+        case "do":
+          eval_ast(MalList(astList[1..-2]), env)
+          ast = astList[-1]
+          // TCO
+        case "if":
+          if (EVAL(astList[1], env) is MalFalseyVal)
+            ast = astList.count > 3 ? astList[3] : MalNil.INSTANCE
+          else
+            ast = astList[2]
+          // TCO
+        case "fn*":
+          f := |MalVal[] a -> MalVal|
+          {
+            return EVAL(astList[2], MalEnv(env, (astList[1] as MalSeq), MalList(a)))
+          }
+          return MalUserFunc(astList[2], env, (MalSeq)astList[1], f)
+        default:
+          evaled_ast := eval_ast(ast, env) as MalList
+          switch (evaled_ast[0].typeof)
+          {
+            case MalUserFunc#:
+              user_fn := evaled_ast[0] as MalUserFunc
+              ast = user_fn.ast
+              env = user_fn.genEnv(evaled_ast.drop(1))
+              // TCO
+            case MalFunc#:
+              return (evaled_ast[0] as MalFunc).call(evaled_ast[1..-1])
+            default:
+              throw Err("Unknown type")
+          }
+      }
+    }
+    return MalNil.INSTANCE // never reached
+  }
+
+  static Str PRINT(MalVal exp)
+  {
+    return exp.toString(true)
+  }
+
+  static Str REP(Str s, MalEnv env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main(Str[] args)
+  {
+    repl_env := MalEnv()
+    // core.fan: defined using Fantom
+    Core.ns.each |MalFunc V, Str K| { repl_env.set(MalSymbol(K), V) }
+    repl_env.set(MalSymbol("eval"), MalFunc { EVAL(it[0], repl_env) })
+    repl_env.set(MalSymbol("*ARGV*"), MalList((args.isEmpty ? args : args[1..-1]).map { MalString.make(it) }))
+
+    // core.mal: defined using the language itself
+    REP("(def! not (fn* (a) (if a false true)))", repl_env)
+    REP("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env)
+
+    if (!args.isEmpty)
+    {
+      REP("(load-file \"${args[0]}\")", repl_env)
+      return
+    }
+
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      try
+        echo(REP(line, repl_env))
+      catch (Err e)
+        echo("Error: $e.msg")
+    }
+  }
+}
diff --git a/fantom/src/step8_macros/build.fan b/fantom/src/step8_macros/build.fan
new file mode 100644 (file)
index 0000000..d6333c9
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "step8_macros"
+    summary = "mal step8_macros pod"
+    depends = ["sys 1.0", "mallib 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/step8_macros/fan/main.fan b/fantom/src/step8_macros/fan/main.fan
new file mode 100644 (file)
index 0000000..243295c
--- /dev/null
@@ -0,0 +1,174 @@
+using mallib
+
+class Main
+{
+  static MalVal quasiquote(MalVal ast)
+  {
+    if (!MalTypes.isPair(ast))
+      return MalList(MalVal[MalSymbol("quote"), ast])
+    astSeq := ast as MalSeq
+    if ((astSeq[0] as MalSymbol)?.value == "unquote")
+      return astSeq[1]
+    if (MalTypes.isPair(astSeq[0]))
+    {
+      ast0Seq := astSeq[0] as MalSeq
+      if ((ast0Seq[0] as MalSymbol)?.value == "splice-unquote")
+        return MalList(MalVal[MalSymbol("concat"), ast0Seq[1], quasiquote(astSeq.drop(1))])
+    }
+    return MalList(MalVal[MalSymbol("cons"), quasiquote(astSeq[0]), quasiquote(astSeq.drop(1))])
+  }
+
+  static Bool isMacroCall(MalVal ast, MalEnv env)
+  {
+    if (!(ast is MalList)) return false
+    astList := ast as MalList
+    if (astList.isEmpty) return false
+    if (!(astList[0] is MalSymbol)) return false
+    ast0 := astList[0] as MalSymbol
+    f := env.find(ast0)?.get(ast0)
+    return (f as MalUserFunc)?.isMacro ?: false
+  }
+
+  static MalVal macroexpand(MalVal ast, MalEnv env)
+  {
+    while (isMacroCall(ast, env))
+    {
+      mac := env.get((ast as MalList)[0]) as MalUserFunc
+      ast = mac.call((ast as MalSeq).drop(1).value)
+    }
+    return ast
+  }
+
+  static MalVal READ(Str s)
+  {
+    return Reader.read_str(s)
+  }
+
+  static MalVal eval_ast(MalVal ast, MalEnv env)
+  {
+    switch (ast.typeof)
+    {
+      case MalSymbol#:
+        return env.get(ast)
+      case MalList#:
+        newElements := (ast as MalList).value.map { EVAL(it, env) }
+        return MalList(newElements)
+      case MalVector#:
+        newElements := (ast as MalVector).value.map { EVAL(it, env) }
+        return MalVector(newElements)
+      case MalHashMap#:
+        newElements := (ast as MalHashMap).value.map |MalVal v -> MalVal| { return EVAL(v, env) }
+        return MalHashMap.fromMap(newElements)
+      default:
+        return ast
+    }
+  }
+
+  static MalVal EVAL(MalVal ast, MalEnv env)
+  {
+    while (true)
+    {
+      if (!(ast is MalList)) return eval_ast(ast, env)
+      ast = macroexpand(ast, env)
+      if (!(ast is MalList)) return eval_ast(ast, env)
+      astList := ast as MalList
+      if (astList.isEmpty) return ast
+      switch ((astList[0] as MalSymbol)?.value)
+      {
+        case "def!":
+          return env.set(astList[1], EVAL(astList[2], env))
+        case "let*":
+          let_env := MalEnv(env)
+          varList := astList[1] as MalSeq
+          for (i := 0; i < varList.count; i += 2)
+            let_env.set(varList[i], EVAL(varList[i + 1], let_env))
+          env = let_env
+          ast = astList[2]
+          // TCO
+        case "quote":
+          return astList[1]
+        case "quasiquote":
+          ast = quasiquote(astList[1])
+          // TCO
+        case "defmacro!":
+          f := EVAL(astList[2], env) as MalUserFunc
+          f.isMacro = true
+          return env.set(astList[1], f)
+        case "macroexpand":
+          return macroexpand(astList[1], env)
+        case "do":
+          eval_ast(MalList(astList[1..-2]), env)
+          ast = astList[-1]
+          // TCO
+        case "if":
+          if (EVAL(astList[1], env) is MalFalseyVal)
+            ast = astList.count > 3 ? astList[3] : MalNil.INSTANCE
+          else
+            ast = astList[2]
+          // TCO
+        case "fn*":
+          f := |MalVal[] a -> MalVal|
+          {
+            return EVAL(astList[2], MalEnv(env, (astList[1] as MalSeq), MalList(a)))
+          }
+          return MalUserFunc(astList[2], env, (MalSeq)astList[1], f)
+        default:
+          evaled_ast := eval_ast(ast, env) as MalList
+          switch (evaled_ast[0].typeof)
+          {
+            case MalUserFunc#:
+              user_fn := evaled_ast[0] as MalUserFunc
+              ast = user_fn.ast
+              env = user_fn.genEnv(evaled_ast.drop(1))
+              // TCO
+            case MalFunc#:
+              return (evaled_ast[0] as MalFunc).call(evaled_ast[1..-1])
+            default:
+              throw Err("Unknown type")
+          }
+      }
+    }
+    return MalNil.INSTANCE // never reached
+  }
+
+  static Str PRINT(MalVal exp)
+  {
+    return exp.toString(true)
+  }
+
+  static Str REP(Str s, MalEnv env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main(Str[] args)
+  {
+    repl_env := MalEnv()
+    // core.fan: defined using Fantom
+    Core.ns.each |MalFunc V, Str K| { repl_env.set(MalSymbol(K), V) }
+    repl_env.set(MalSymbol("eval"), MalFunc { EVAL(it[0], repl_env) })
+    repl_env.set(MalSymbol("*ARGV*"), MalList((args.isEmpty ? args : args[1..-1]).map { MalString.make(it) }))
+
+    // core.mal: defined using the language itself
+    REP("(def! not (fn* (a) (if a false true)))", repl_env)
+    REP("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env)
+    REP("(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)
+    REP("(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.isEmpty)
+    {
+      REP("(load-file \"${args[0]}\")", repl_env)
+      return
+    }
+
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      try
+        echo(REP(line, repl_env))
+      catch (Err e)
+        echo("Error: $e.msg")
+    }
+  }
+}
diff --git a/fantom/src/step9_try/build.fan b/fantom/src/step9_try/build.fan
new file mode 100644 (file)
index 0000000..8d3b048
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "step9_try"
+    summary = "mal step9_try pod"
+    depends = ["sys 1.0", "mallib 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/step9_try/fan/main.fan b/fantom/src/step9_try/fan/main.fan
new file mode 100644 (file)
index 0000000..1cede16
--- /dev/null
@@ -0,0 +1,186 @@
+using mallib
+
+class Main
+{
+  static MalVal quasiquote(MalVal ast)
+  {
+    if (!MalTypes.isPair(ast))
+      return MalList(MalVal[MalSymbol("quote"), ast])
+    astSeq := ast as MalSeq
+    if ((astSeq[0] as MalSymbol)?.value == "unquote")
+      return astSeq[1]
+    if (MalTypes.isPair(astSeq[0]))
+    {
+      ast0Seq := astSeq[0] as MalSeq
+      if ((ast0Seq[0] as MalSymbol)?.value == "splice-unquote")
+        return MalList(MalVal[MalSymbol("concat"), ast0Seq[1], quasiquote(astSeq.drop(1))])
+    }
+    return MalList(MalVal[MalSymbol("cons"), quasiquote(astSeq[0]), quasiquote(astSeq.drop(1))])
+  }
+
+  static Bool isMacroCall(MalVal ast, MalEnv env)
+  {
+    if (!(ast is MalList)) return false
+    astList := ast as MalList
+    if (astList.isEmpty) return false
+    if (!(astList[0] is MalSymbol)) return false
+    ast0 := astList[0] as MalSymbol
+    f := env.find(ast0)?.get(ast0)
+    return (f as MalUserFunc)?.isMacro ?: false
+  }
+
+  static MalVal macroexpand(MalVal ast, MalEnv env)
+  {
+    while (isMacroCall(ast, env))
+    {
+      mac := env.get((ast as MalList)[0]) as MalUserFunc
+      ast = mac.call((ast as MalSeq).drop(1).value)
+    }
+    return ast
+  }
+
+  static MalVal READ(Str s)
+  {
+    return Reader.read_str(s)
+  }
+
+  static MalVal eval_ast(MalVal ast, MalEnv env)
+  {
+    switch (ast.typeof)
+    {
+      case MalSymbol#:
+        return env.get(ast)
+      case MalList#:
+        newElements := (ast as MalList).value.map { EVAL(it, env) }
+        return MalList(newElements)
+      case MalVector#:
+        newElements := (ast as MalVector).value.map { EVAL(it, env) }
+        return MalVector(newElements)
+      case MalHashMap#:
+        newElements := (ast as MalHashMap).value.map |MalVal v -> MalVal| { return EVAL(v, env) }
+        return MalHashMap.fromMap(newElements)
+      default:
+        return ast
+    }
+  }
+
+  static MalVal EVAL(MalVal ast, MalEnv env)
+  {
+    while (true)
+    {
+      if (!(ast is MalList)) return eval_ast(ast, env)
+      ast = macroexpand(ast, env)
+      if (!(ast is MalList)) return eval_ast(ast, env)
+      astList := ast as MalList
+      if (astList.isEmpty) return ast
+      switch ((astList[0] as MalSymbol)?.value)
+      {
+        case "def!":
+          return env.set(astList[1], EVAL(astList[2], env))
+        case "let*":
+          let_env := MalEnv(env)
+          varList := astList[1] as MalSeq
+          for (i := 0; i < varList.count; i += 2)
+            let_env.set(varList[i], EVAL(varList[i + 1], let_env))
+          env = let_env
+          ast = astList[2]
+          // TCO
+        case "quote":
+          return astList[1]
+        case "quasiquote":
+          ast = quasiquote(astList[1])
+          // TCO
+        case "defmacro!":
+          f := EVAL(astList[2], env) as MalUserFunc
+          f.isMacro = true
+          return env.set(astList[1], f)
+        case "macroexpand":
+          return macroexpand(astList[1], env)
+        case "try*":
+          MalVal exc := MalNil.INSTANCE
+          try
+            return EVAL(astList[1], env)
+          catch (MalException e)
+            exc = e.getValue
+          catch (Err e)
+            exc = MalString.make(e.msg)
+          catchClause := astList[2] as MalList
+          return EVAL(catchClause[2], MalEnv(env, MalList([catchClause[1]]), MalList([exc])))
+        case "do":
+          eval_ast(MalList(astList[1..-2]), env)
+          ast = astList[-1]
+          // TCO
+        case "if":
+          if (EVAL(astList[1], env) is MalFalseyVal)
+            ast = astList.count > 3 ? astList[3] : MalNil.INSTANCE
+          else
+            ast = astList[2]
+          // TCO
+        case "fn*":
+          f := |MalVal[] a -> MalVal|
+          {
+            return EVAL(astList[2], MalEnv(env, (astList[1] as MalSeq), MalList(a)))
+          }
+          return MalUserFunc(astList[2], env, (MalSeq)astList[1], f)
+        default:
+          evaled_ast := eval_ast(ast, env) as MalList
+          switch (evaled_ast[0].typeof)
+          {
+            case MalUserFunc#:
+              user_fn := evaled_ast[0] as MalUserFunc
+              ast = user_fn.ast
+              env = user_fn.genEnv(evaled_ast.drop(1))
+              // TCO
+            case MalFunc#:
+              return (evaled_ast[0] as MalFunc).call(evaled_ast[1..-1])
+            default:
+              throw Err("Unknown type")
+          }
+      }
+    }
+    return MalNil.INSTANCE // never reached
+  }
+
+  static Str PRINT(MalVal exp)
+  {
+    return exp.toString(true)
+  }
+
+  static Str REP(Str s, MalEnv env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main(Str[] args)
+  {
+    repl_env := MalEnv()
+    // core.fan: defined using Fantom
+    Core.ns.each |MalFunc V, Str K| { repl_env.set(MalSymbol(K), V) }
+    repl_env.set(MalSymbol("eval"), MalFunc { EVAL(it[0], repl_env) })
+    repl_env.set(MalSymbol("*ARGV*"), MalList((args.isEmpty ? args : args[1..-1]).map { MalString.make(it) }))
+
+    // core.mal: defined using the language itself
+    REP("(def! not (fn* (a) (if a false true)))", repl_env)
+    REP("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env)
+    REP("(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)
+    REP("(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.isEmpty)
+    {
+      REP("(load-file \"${args[0]}\")", repl_env)
+      return
+    }
+
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      try
+        echo(REP(line, repl_env))
+      catch (MalException e)
+        echo("Error: ${e.serializedValue}")
+      catch (Err e)
+        echo("Error: $e.msg")
+    }
+  }
+}
diff --git a/fantom/src/stepA_mal/build.fan b/fantom/src/stepA_mal/build.fan
new file mode 100644 (file)
index 0000000..a4c40d7
--- /dev/null
@@ -0,0 +1,11 @@
+class Build : build::BuildPod
+{
+  new make()
+  {
+    podName = "stepA_mal"
+    summary = "mal stepA_mal pod"
+    depends = ["sys 1.0", "mallib 1.0"]
+    srcDirs = [`fan/`]
+    outPodDir = `lib/fan/`
+  }
+}
diff --git a/fantom/src/stepA_mal/fan/main.fan b/fantom/src/stepA_mal/fan/main.fan
new file mode 100644 (file)
index 0000000..c42b659
--- /dev/null
@@ -0,0 +1,190 @@
+using mallib
+
+class Main
+{
+  static MalVal quasiquote(MalVal ast)
+  {
+    if (!MalTypes.isPair(ast))
+      return MalList(MalVal[MalSymbol("quote"), ast])
+    astSeq := ast as MalSeq
+    if ((astSeq[0] as MalSymbol)?.value == "unquote")
+      return astSeq[1]
+    if (MalTypes.isPair(astSeq[0]))
+    {
+      ast0Seq := astSeq[0] as MalSeq
+      if ((ast0Seq[0] as MalSymbol)?.value == "splice-unquote")
+        return MalList(MalVal[MalSymbol("concat"), ast0Seq[1], quasiquote(astSeq.drop(1))])
+    }
+    return MalList(MalVal[MalSymbol("cons"), quasiquote(astSeq[0]), quasiquote(astSeq.drop(1))])
+  }
+
+  static Bool isMacroCall(MalVal ast, MalEnv env)
+  {
+    if (!(ast is MalList)) return false
+    astList := ast as MalList
+    if (astList.isEmpty) return false
+    if (!(astList[0] is MalSymbol)) return false
+    ast0 := astList[0] as MalSymbol
+    f := env.find(ast0)?.get(ast0)
+    return (f as MalUserFunc)?.isMacro ?: false
+  }
+
+  static MalVal macroexpand(MalVal ast, MalEnv env)
+  {
+    while (isMacroCall(ast, env))
+    {
+      mac := env.get((ast as MalList)[0]) as MalUserFunc
+      ast = mac.call((ast as MalSeq).drop(1).value)
+    }
+    return ast
+  }
+
+  static MalVal READ(Str s)
+  {
+    return Reader.read_str(s)
+  }
+
+  static MalVal eval_ast(MalVal ast, MalEnv env)
+  {
+    switch (ast.typeof)
+    {
+      case MalSymbol#:
+        return env.get(ast)
+      case MalList#:
+        newElements := (ast as MalList).value.map { EVAL(it, env) }
+        return MalList(newElements)
+      case MalVector#:
+        newElements := (ast as MalVector).value.map { EVAL(it, env) }
+        return MalVector(newElements)
+      case MalHashMap#:
+        newElements := (ast as MalHashMap).value.map |MalVal v -> MalVal| { return EVAL(v, env) }
+        return MalHashMap.fromMap(newElements)
+      default:
+        return ast
+    }
+  }
+
+  static MalVal EVAL(MalVal ast, MalEnv env)
+  {
+    while (true)
+    {
+      if (!(ast is MalList)) return eval_ast(ast, env)
+      ast = macroexpand(ast, env)
+      if (!(ast is MalList)) return eval_ast(ast, env)
+      astList := ast as MalList
+      if (astList.isEmpty) return ast
+      switch ((astList[0] as MalSymbol)?.value)
+      {
+        case "def!":
+          return env.set(astList[1], EVAL(astList[2], env))
+        case "let*":
+          let_env := MalEnv(env)
+          varList := astList[1] as MalSeq
+          for (i := 0; i < varList.count; i += 2)
+            let_env.set(varList[i], EVAL(varList[i + 1], let_env))
+          env = let_env
+          ast = astList[2]
+          // TCO
+        case "quote":
+          return astList[1]
+        case "quasiquote":
+          ast = quasiquote(astList[1])
+          // TCO
+        case "defmacro!":
+          f := EVAL(astList[2], env) as MalUserFunc
+          f.isMacro = true
+          return env.set(astList[1], f)
+        case "macroexpand":
+          return macroexpand(astList[1], env)
+        case "try*":
+          MalVal exc := MalNil.INSTANCE
+          try
+            return EVAL(astList[1], env)
+          catch (MalException e)
+            exc = e.getValue
+          catch (Err e)
+            exc = MalString.make(e.msg)
+          catchClause := astList[2] as MalList
+          return EVAL(catchClause[2], MalEnv(env, MalList([catchClause[1]]), MalList([exc])))
+        case "do":
+          eval_ast(MalList(astList[1..-2]), env)
+          ast = astList[-1]
+          // TCO
+        case "if":
+          if (EVAL(astList[1], env) is MalFalseyVal)
+            ast = astList.count > 3 ? astList[3] : MalNil.INSTANCE
+          else
+            ast = astList[2]
+          // TCO
+        case "fn*":
+          f := |MalVal[] a -> MalVal|
+          {
+            return EVAL(astList[2], MalEnv(env, (astList[1] as MalSeq), MalList(a)))
+          }
+          return MalUserFunc(astList[2], env, (MalSeq)astList[1], f)
+        default:
+          evaled_ast := eval_ast(ast, env) as MalList
+          switch (evaled_ast[0].typeof)
+          {
+            case MalUserFunc#:
+              user_fn := evaled_ast[0] as MalUserFunc
+              ast = user_fn.ast
+              env = user_fn.genEnv(evaled_ast.drop(1))
+              // TCO
+            case MalFunc#:
+              return (evaled_ast[0] as MalFunc).call(evaled_ast[1..-1])
+            default:
+              throw Err("Unknown type")
+          }
+      }
+    }
+    return MalNil.INSTANCE // never reached
+  }
+
+  static Str PRINT(MalVal exp)
+  {
+    return exp.toString(true)
+  }
+
+  static Str REP(Str s, MalEnv env)
+  {
+    return PRINT(EVAL(READ(s), env))
+  }
+
+  static Void main(Str[] args)
+  {
+    repl_env := MalEnv()
+    // core.fan: defined using Fantom
+    Core.ns.each |MalFunc V, Str K| { repl_env.set(MalSymbol(K), V) }
+    repl_env.set(MalSymbol("eval"), MalFunc { EVAL(it[0], repl_env) })
+    repl_env.set(MalSymbol("*ARGV*"), MalList((args.isEmpty ? args : args[1..-1]).map { MalString.make(it) }))
+
+    // core.mal: defined using the language itself
+    REP("(def! *host-language* \"fantom\")", repl_env)
+    REP("(def! not (fn* (a) (if a false true)))", repl_env)
+    REP("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env)
+    REP("(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)
+    REP("(def! *gensym-counter* (atom 0))", repl_env)
+    REP("(def! gensym (fn* [] (symbol (str \"G__\" (swap! *gensym-counter* (fn* [x] (+ 1 x)))))))", repl_env)
+    REP("(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) (let* (condvar (gensym)) `(let* (~condvar ~(first xs)) (if ~condvar ~condvar (or ~@(rest xs)))))))))", repl_env)
+
+    if (!args.isEmpty)
+    {
+      REP("(load-file \"${args[0]}\")", repl_env)
+      return
+    }
+
+    REP("(println (str \"Mal [\" *host-language* \"]\"))", repl_env)
+    while (true) {
+      line := Env.cur.prompt("user> ")
+      if (line == null) break
+      if (line.isSpace) continue
+      try
+        echo(REP(line, repl_env))
+      catch (MalException e)
+        echo("Error: ${e.serializedValue}")
+      catch (Err e)
+        echo("Error: $e.msg")
+    }
+  }
+}
diff --git a/fantom/tests/step5_tco.mal b/fantom/tests/step5_tco.mal
new file mode 100644 (file)
index 0000000..d20df25
--- /dev/null
@@ -0,0 +1,15 @@
+;; Test recursive non-tail call function
+
+(def! sum-to (fn* (n) (if (= n 0) 0 (+ n (sum-to (- n 1))))))
+
+(sum-to 10)
+;=>55
+
+;;; no try* yet, so test completion of side-effects
+(def! res1 nil)
+;=>nil
+;;; For implementations without their own TCO this should fail and
+;;; leave res1 unchanged
+(def! res1 (sum-to 10000))
+res1
+;=>nil
diff --git a/fantom/tests/stepA_mal.mal b/fantom/tests/stepA_mal.mal
new file mode 100644 (file)
index 0000000..a8c37d8
--- /dev/null
@@ -0,0 +1,32 @@
+;; Testing basic fantom interop
+
+(fantom-eval "7")
+;=>7
+
+(fantom-eval "return 3 * 9")
+;=>27
+
+(fantom-eval "\"7\"")
+;=>"7"
+
+(fantom-eval "\"abcd\".upper")
+;=>"ABCD"
+
+(fantom-eval "[7,8,9]")
+;=>(7 8 9)
+
+(fantom-eval "[\"abc\": 789]")
+;=>{"abc" 789}
+
+(fantom-eval "echo(\"hello\")")
+; hello
+;=>nil
+
+(fantom-eval "[\"a\",\"b\",\"c\"].join(\" \") { \"X${it}Y\" }")
+;=>"XaY XbY XcY"
+
+(fantom-eval "[1,2,3].map { 1 + it }")
+;=>(2 3 4)
+
+(fantom-eval "Env.cur.runtime")
+;=>"java"