Add Skew implementation
authorDov Murik <dov.murik@gmail.com>
Tue, 8 Nov 2016 21:40:18 +0000 (21:40 +0000)
committerDov Murik <dov.murik@gmail.com>
Sun, 20 Nov 2016 10:10:41 +0000 (10:10 +0000)
See http://skew-lang.org/ for details on the Skew language. Currently
Mal only compiles to Javascript, as there are some issues with the C#
backend for Skew (https://github.com/evanw/skew/issues/19).

Tested with Skew 0.7.42.

25 files changed:
.gitignore
.travis.yml
Makefile
README.md
skew/Dockerfile [new file with mode: 0644]
skew/Makefile [new file with mode: 0644]
skew/core.sk [new file with mode: 0644]
skew/env.sk [new file with mode: 0644]
skew/printer.sk [new file with mode: 0644]
skew/reader.sk [new file with mode: 0644]
skew/run [new file with mode: 0755]
skew/step0_repl.sk [new file with mode: 0644]
skew/step1_read_print.sk [new file with mode: 0644]
skew/step2_eval.sk [new file with mode: 0644]
skew/step3_env.sk [new file with mode: 0644]
skew/step4_if_fn_do.sk [new file with mode: 0644]
skew/step5_tco.sk [new file with mode: 0644]
skew/step6_file.sk [new file with mode: 0644]
skew/step7_quote.sk [new file with mode: 0644]
skew/step8_macros.sk [new file with mode: 0644]
skew/step9_try.sk [new file with mode: 0644]
skew/stepA_mal.sk [new file with mode: 0644]
skew/tests/step5_tco.mal [new file with mode: 0644]
skew/types.sk [new file with mode: 0644]
skew/util.sk [new file with mode: 0644]

index 54c0158..870c195 100644 (file)
@@ -101,6 +101,7 @@ r/lib
 scala/mal.jar
 scala/target
 scala/project
+skew/*.js
 tcl/mal.tcl
 vb/*.exe
 vb/*.dll
index 97ee821..ae425bb 100644 (file)
@@ -62,6 +62,7 @@ matrix:
     - {env: IMPL=ruby,      services: [docker]}
     - {env: IMPL=rust,      services: [docker]}
     - {env: IMPL=scala,     services: [docker]}
+    - {env: IMPL=skew,      services: [docker]}
     - {env: IMPL=swift NO_DOCKER=1, os: osx, osx_image: xcode7}
     - {env: IMPL=swift3,    services: [docker]}
     - {env: IMPL=swift3 NO_DOCKER=1, os: osx, osx_image: xcode8}
index eb810be..f015de0 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -81,7 +81,7 @@ IMPLS = ada awk bash basic c d chuck clojure coffee clisp cpp crystal cs dart \
        erlang elisp elixir es6 factor forth fsharp go groovy guile haskell \
        haxe io java julia js kotlin logo lua make mal ocaml matlab miniMAL \
        nim objc objpascal perl perl6 php pil plpgsql plsql powershell ps \
-       python r racket rpython ruby rust scala swift swift3 tcl vb vhdl \
+       python r racket rpython ruby rust scala skew swift swift3 tcl vb vhdl \
        vimscript
 
 step0 = step0_repl
@@ -198,6 +198,7 @@ rpython_STEP_TO_PROG = rpython/$($(1))
 ruby_STEP_TO_PROG =    ruby/$($(1)).rb
 rust_STEP_TO_PROG =    rust/target/release/$($(1))
 scala_STEP_TO_PROG =   scala/target/scala-2.11/classes/$($(1)).class
+skew_STEP_TO_PROG =    skew/$($(1)).js
 swift_STEP_TO_PROG =   swift/$($(1))
 swift3_STEP_TO_PROG =  swift3/$($(1))
 tcl_STEP_TO_PROG =     tcl/$($(1)).tcl
index a73cf21..eefcf9d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
 
 Mal is a Clojure inspired Lisp interpreter.
 
-Mal is implemented in 62 languages:
+Mal is implemented in 63 languages:
 
 * Ada
 * GNU awk
@@ -64,6 +64,7 @@ Mal is implemented in 62 languages:
 * Ruby
 * Rust
 * Scala
+* Skew
 * Swift
 * Swift 3
 * Tcl
@@ -802,6 +803,19 @@ sbt compile
 scala -classpath target/scala*/classes stepX_YYY
 ```
 
+### Skew ###
+
+*The Skew implementation was created by [Dov Murik](https://github.com/dubek)*
+
+The Skew implementation of mal has been tested with Skew 0.7.42.
+
+```
+cd skew
+make
+node stepX_YYY.js
+```
+
+
 ### Swift
 
 *The Swift implementation was created by [Keith Rollin](https://github.com/keith-rollin)*
diff --git a/skew/Dockerfile b/skew/Dockerfile
new file mode 100644 (file)
index 0000000..8e689fd
--- /dev/null
@@ -0,0 +1,39 @@
+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
+##########################################################
+
+# For building node modules
+RUN apt-get -y install g++
+
+# Add nodesource apt repo config for 0.12 stable
+RUN curl -sL https://deb.nodesource.com/setup_0.12 | bash -
+
+# Install nodejs
+RUN apt-get -y install nodejs
+
+# Link common name
+RUN ln -sf nodejs /usr/bin/node
+
+ENV NPM_CONFIG_CACHE /mal/.npm
+
+# Skew
+RUN npm install -g skew
diff --git a/skew/Makefile b/skew/Makefile
new file mode 100644 (file)
index 0000000..b13d5f5
--- /dev/null
@@ -0,0 +1,36 @@
+STEPS = step0_repl step1_read_print step2_eval step3_env step4_if_fn_do \
+        step5_tco step6_file step7_quote step8_macros step9_try stepA_mal
+
+SOURCES_BASE = util.sk types.sk reader.sk printer.sk
+SOURCES_LISP = env.sk core.sk stepA_mal.sk
+SOURCES = $(SOURCES_BASE) $(SOURCES_LISP)
+
+STEP3_DEPS = $(SOURCES_BASE) env.sk
+STEP4_DEPS = $(STEP3_DEPS) core.sk
+
+all: $(foreach s,$(STEPS),$(s).js) dist
+
+dist: mal
+
+step0_repl.js step1_read_print.js step2_eval.js step3_env.js: $(STEP3_DEPS)
+step4_if_fn_do.js step5_tco.js step6_file.js step7_quote.js step8_macros.js step9_try.js stepA_mal.js: $(STEP4_DEPS)
+
+%.js: %.sk
+       skewc --target=js --release --output-file=$@ $^
+
+mal: stepA_mal.js
+       echo "#!/usr/bin/env node" > $@
+       cat $< >> $@
+       chmod +x $@
+
+clean:
+       rm -rf step*.js mal
+
+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]"
+
+.PHONY: all dist clean stats stats-lisp
diff --git a/skew/core.sk b/skew/core.sk
new file mode 100644 (file)
index 0000000..3390d8e
--- /dev/null
@@ -0,0 +1,98 @@
+def _printLn(s string) MalVal {
+  printLn(s)
+  return gNil
+}
+
+const ns StringMap<fn(List<MalVal>) MalVal> = {
+  "eval":  (a List<MalVal>) => EVAL(a[0], repl_env),
+  "=":     (a List<MalVal>) => MalVal.fromBool(a[0].equal(a[1])),
+  "throw": (a List<MalVal>) => { throw MalUserError.new(a[0]) },
+
+  "nil?":     (a List<MalVal>) => MalVal.fromBool(a[0] is MalNil),
+  "true?":    (a List<MalVal>) => MalVal.fromBool(a[0] is MalTrue),
+  "false?":   (a List<MalVal>) => MalVal.fromBool(a[0] is MalFalse),
+  "string?":  (a List<MalVal>) => MalVal.fromBool(a[0] is MalString),
+  "symbol":   (a List<MalVal>) => MalSymbol.new((a[0] as MalString).val),
+  "symbol?":  (a List<MalVal>) => MalVal.fromBool(a[0] is MalSymbol),
+  "keyword":  (a List<MalVal>) => MalKeyword.new((a[0] as MalString).val),
+  "keyword?": (a List<MalVal>) => MalVal.fromBool(a[0] is MalKeyword),
+
+  "pr-str":  (a List<MalVal>) => MalString.new(" ".join(a.map<string>(e => pr_str(e, true)))),
+  "str":     (a List<MalVal>) => MalString.new("".join(a.map<string>(e => pr_str(e, false)))),
+  "prn":     (a List<MalVal>) => _printLn(" ".join(a.map<string>(e => pr_str(e, true)))),
+  "println": (a List<MalVal>) => _printLn(" ".join(a.map<string>(e => pr_str(e, false)))),
+  "read-string": (a List<MalVal>) => read_str((a[0] as MalString).val),
+  "readline":    (a List<MalVal>) => {
+                   const line = readLine((a[0] as MalString).val)
+                   return line == null ? gNil : MalString.new(line)
+                 },
+  "slurp":   (a List<MalVal>) => MalString.new(readFile((a[0] as MalString).val)),
+
+  "<":  (a List<MalVal>) => MalVal.fromBool((a[0] as MalNumber).val < (a[1] as MalNumber).val),
+  "<=": (a List<MalVal>) => MalVal.fromBool((a[0] as MalNumber).val <= (a[1] as MalNumber).val),
+  ">":  (a List<MalVal>) => MalVal.fromBool((a[0] as MalNumber).val > (a[1] as MalNumber).val),
+  ">=": (a List<MalVal>) => MalVal.fromBool((a[0] as MalNumber).val >= (a[1] as MalNumber).val),
+  "+":  (a List<MalVal>) => MalNumber.new((a[0] as MalNumber).val + (a[1] as MalNumber).val),
+  "-":  (a List<MalVal>) => MalNumber.new((a[0] as MalNumber).val - (a[1] as MalNumber).val),
+  "*":  (a List<MalVal>) => MalNumber.new((a[0] as MalNumber).val * (a[1] as MalNumber).val),
+  "/":  (a List<MalVal>) => MalNumber.new((a[0] as MalNumber).val / (a[1] as MalNumber).val),
+  "time-ms": (a List<MalVal>) => MalNumber.new(timeMs),
+
+  "list":      (a List<MalVal>) => MalList.new(a),
+  "list?":     (a List<MalVal>) => MalVal.fromBool(a[0] is MalList),
+  "vector":    (a List<MalVal>) => MalVector.new(a),
+  "vector?":   (a List<MalVal>) => MalVal.fromBool(a[0] is MalVector),
+  "hash-map":  (a List<MalVal>) => MalHashMap.fromList(a),
+  "map?":      (a List<MalVal>) => MalVal.fromBool(a[0] is MalHashMap),
+  "assoc":     (a List<MalVal>) => (a[0] as MalHashMap).assoc(a.slice(1)),
+  "dissoc":    (a List<MalVal>) => (a[0] as MalHashMap).dissoc(a.slice(1)),
+  "get":       (a List<MalVal>) => a[0] is MalNil ? gNil : (a[0] as MalHashMap).get(a[1]),
+  "contains?": (a List<MalVal>) => MalVal.fromBool((a[0] as MalHashMap).contains(a[1])),
+  "keys":      (a List<MalVal>) => MalList.new((a[0] as MalHashMap).keys),
+  "vals":      (a List<MalVal>) => MalList.new((a[0] as MalHashMap).vals),
+
+  "sequential?": (a List<MalVal>) => MalVal.fromBool(a[0] is MalSequential),
+  "cons":   (a List<MalVal>) => {
+              var list List<MalVal> = (a[1] as MalSequential).val.clone
+              list.prepend(a[0])
+              return MalList.new(list)
+            },
+  "concat": (a List<MalVal>) => {
+              var list List<MalVal> = []
+              a.each(e => list.append((e as MalSequential).val))
+              return MalList.new(list)
+            },
+  "nth":    (a List<MalVal>) => (a[0] as MalSequential).nth((a[1] as MalNumber).val),
+  "first":  (a List<MalVal>) => a[0] is MalNil ? gNil : (a[0] as MalSequential).first,
+  "rest":   (a List<MalVal>) => a[0] is MalNil ? MalList.new([]) : (a[0] as MalSequential).rest,
+  "empty?": (a List<MalVal>) => MalVal.fromBool((a[0] as MalSequential).count == 0),
+  "count":  (a List<MalVal>) => a[0] is MalNil ? MalNumber.new(0) : MalNumber.new((a[0] as MalSequential).count),
+  "apply":  (a List<MalVal>) => {
+              const f = a[0] as MalCallable
+              var args = a.slice(1, a.count - 1)
+              args.append((a[a.count - 1] as MalSequential).val)
+             return f.call(args)
+            },
+  "map":    (a List<MalVal>) => {
+              const f = a[0] as MalCallable
+              return MalList.new((a[1] as MalSequential).val.map<MalVal>(e => f.call([e])))
+            },
+
+  "conj": (a List<MalVal>) => (a[0] as MalSequential).conj(a.slice(1)),
+  "seq":  (a List<MalVal>) => a[0].seq,
+
+  "meta":      (a List<MalVal>) => a[0].meta,
+  "with-meta": (a List<MalVal>) => a[0].withMeta(a[1]),
+  "atom":      (a List<MalVal>) => MalAtom.new(a[0]),
+  "atom?":     (a List<MalVal>) => MalVal.fromBool(a[0] is MalAtom),
+  "deref":     (a List<MalVal>) => (a[0] as MalAtom).val,
+  "reset!":    (a List<MalVal>) => (a[0] as MalAtom).resetBang(a[1]),
+  "swap!":     (a List<MalVal>) => {
+                 var atom = a[0] as MalAtom
+                 const oldVal = atom.val
+                 var callArgs = a.slice(2)
+                 callArgs.prepend(oldVal)
+                 const newVal = (a[1] as MalCallable).call(callArgs)
+                 return atom.resetBang(newVal)
+               },
+}
diff --git a/skew/env.sk b/skew/env.sk
new file mode 100644 (file)
index 0000000..2f4afb9
--- /dev/null
@@ -0,0 +1,38 @@
+class Env {
+  const _outer Env
+  var _data StringMap<MalVal> = {}
+
+  def new(outer Env) {
+    _outer = outer
+  }
+
+  def new(outer Env, binds List<MalVal>, exprs List<MalVal>) {
+    _outer = outer
+    for i in 0..binds.count {
+      const name = (binds[i] as MalSymbol).val
+      if name == "&" {
+        const restName = (binds[i + 1] as MalSymbol).val
+        _data[restName] = MalList.new(exprs.slice(i))
+        break
+      } else {
+        _data[name] = exprs[i]
+      }
+    }
+  }
+
+  def find(key MalSymbol) Env {
+    if key.val in _data { return self }
+    return _outer?.find(key)
+  }
+
+  def get(key MalSymbol) MalVal {
+    const env = find(key)
+    if env == null { throw MalError.new("'" + key.val + "' not found") }
+    return env._data[key.val]
+  }
+
+  def set(key MalSymbol, value MalVal) MalVal {
+    _data[key.val] = value
+    return value
+  }
+}
diff --git a/skew/printer.sk b/skew/printer.sk
new file mode 100644 (file)
index 0000000..bd767a0
--- /dev/null
@@ -0,0 +1,3 @@
+def pr_str(obj MalVal, readable bool) string {
+  return obj.print(readable)
+}
diff --git a/skew/reader.sk b/skew/reader.sk
new file mode 100644 (file)
index 0000000..457e888
--- /dev/null
@@ -0,0 +1,139 @@
+class Reader {
+  const tokens List<string>
+  var position = 0
+
+  def peek string {
+    if position >= tokens.count {
+      return null
+    }
+    return tokens[position]
+  }
+
+  def next string {
+    const token = peek
+    position++
+    return token
+  }
+}
+
+def tokenize(str string) List<string> {
+  var re = RegExp.new("[\\s,]*(~@|[\\[\\]{}()'`~^@]|\"(?:\\\\.|[^\\\\\"])*\"|;.*|[^\\s\\[\\]{}('\"`,;)]*)", "g")
+  var tokens List<string> = []
+  var match string
+  while (match = re.exec(str)[1]) != "" {
+    if match[0] == ';' {
+      continue
+    }
+    tokens.append(match)
+  }
+  return tokens
+}
+
+def unescape(s string) string {
+  return s.replaceAll("\\\"", "\"").replaceAll("\\n", "\n").replaceAll("\\\\", "\\")
+}
+
+def read_atom(rdr Reader) MalVal {
+  const token = rdr.peek
+  if token == "nil" {
+    rdr.next
+    return gNil
+  }
+  if token == "true" {
+    rdr.next
+    return gTrue
+  }
+  if token == "false" {
+    rdr.next
+    return gFalse
+  }
+  switch token[0] {
+    case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' { return MalNumber.new(stringToInt(rdr.next)) }
+    case '-' {
+      if token.count <= 1 { return MalSymbol.new(rdr.next) }
+      switch token[1] {
+        case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' { return MalNumber.new(stringToInt(rdr.next)) }
+        default { return MalSymbol.new(rdr.next) }
+      }
+    }
+    case '"' {
+      const s = rdr.next
+      if s[s.count - 1] == '"' {
+        return MalString.new(unescape(s.slice(1, s.count - 1)))
+      } else {
+        throw MalError.new("expected '\"', got EOF")
+      }
+    }
+    case ':' { return MalKeyword.new(rdr.next.slice(1)) }
+    default { return MalSymbol.new(rdr.next) }
+  }
+}
+
+def read_sequence(rdr Reader, open string, close string) List<MalVal> {
+  if rdr.next != open {
+    throw MalError.new("expected '" + open + "'")
+  }
+  var token string
+  var items List<MalVal> = []
+  while (token = rdr.peek) != close {
+    if token == null {
+      throw MalError.new("expected '" + close + "', got EOF")
+    }
+    items.append(read_form(rdr))
+  }
+  rdr.next # consume the close paren/bracket/brace
+  return items
+}
+
+def read_list(rdr Reader) MalList {
+  return MalList.new(read_sequence(rdr, "(", ")"))
+}
+
+def read_vector(rdr Reader) MalVector {
+  return MalVector.new(read_sequence(rdr, "[", "]"))
+}
+
+def read_hash_map(rdr Reader) MalHashMap {
+  return MalHashMap.fromList(read_sequence(rdr, "{", "}"))
+}
+
+def reader_macro(rdr Reader, symbol_name string) MalVal {
+  rdr.next
+  return MalList.new([MalSymbol.new(symbol_name), read_form(rdr)])
+}
+
+def read_form(rdr Reader) MalVal {
+  switch rdr.peek[0] {
+    case '\'' { return reader_macro(rdr, "quote") }
+    case '`' { return reader_macro(rdr, "quasiquote") }
+    case '~' {
+      if rdr.peek == "~" { return reader_macro(rdr, "unquote") }
+      else if rdr.peek == "~@" { return reader_macro(rdr, "splice-unquote") }
+      else { return read_atom(rdr) }
+    }
+    case '^' {
+      rdr.next
+      const meta = read_form(rdr)
+      return MalList.new([MalSymbol.new("with-meta"), read_form(rdr), meta])
+    }
+    case '@' { return reader_macro(rdr, "deref") }
+    case ')' { throw MalError.new("unexpected ')'") }
+    case '(' { return read_list(rdr) }
+    case ']' { throw MalError.new("unexpected ']'") }
+    case '[' { return read_vector(rdr) }
+    case '}' { throw MalError.new("unexpected '}'") }
+    case '{' { return read_hash_map(rdr) }
+    default { return read_atom(rdr) }
+  }
+}
+
+def read_str(str string) MalVal {
+  const tokens = tokenize(str)
+  if tokens.isEmpty { return null }
+  var rdr = Reader.new(tokens)
+  return read_form(rdr)
+}
+
+@import {
+  const RegExp dynamic
+}
diff --git a/skew/run b/skew/run
new file mode 100755 (executable)
index 0000000..6605303
--- /dev/null
+++ b/skew/run
@@ -0,0 +1,2 @@
+#!/bin/bash
+exec node $(dirname $0)/${STEP:-stepA_mal}.js "${@}"
diff --git a/skew/step0_repl.sk b/skew/step0_repl.sk
new file mode 100644 (file)
index 0000000..4afc931
--- /dev/null
@@ -0,0 +1,24 @@
+def READ(str string) string {
+  return str
+}
+
+def EVAL(ast string, env StringMap<string>) string {
+  return ast
+}
+
+def PRINT(exp string) string {
+  return exp
+}
+
+def REP(str string) string {
+  return PRINT(EVAL(READ(str), {}))
+}
+
+@entry
+def main {
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    printLn(REP(line))
+  }
+}
diff --git a/skew/step1_read_print.sk b/skew/step1_read_print.sk
new file mode 100644 (file)
index 0000000..30ead7b
--- /dev/null
@@ -0,0 +1,29 @@
+def READ(str string) MalVal {
+  return read_str(str)
+}
+
+def EVAL(ast MalVal, env StringMap<string>) MalVal {
+  return ast
+}
+
+def PRINT(exp MalVal) string {
+  return exp?.print(true)
+}
+
+def REP(str string) string {
+  return PRINT(EVAL(READ(str), {}))
+}
+
+@entry
+def main {
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    try {
+      printLn(REP(line))
+    }
+    catch e MalError {
+      printLn("Error: \(e.message)")
+    }
+  }
+}
diff --git a/skew/step2_eval.sk b/skew/step2_eval.sk
new file mode 100644 (file)
index 0000000..dc7a606
--- /dev/null
@@ -0,0 +1,64 @@
+def READ(str string) MalVal {
+  return read_str(str)
+}
+
+def eval_ast(ast MalVal, env StringMap<MalVal>) MalVal {
+  if ast is MalSymbol {
+    const name = (ast as MalSymbol).val
+    if !(name in env) {
+      throw MalError.new("'" + name + "' not found")
+    }
+    return env[name]
+  } else if ast is MalList {
+    return MalList.new((ast as MalList).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalVector {
+    return MalVector.new((ast as MalVector).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalHashMap {
+    var result List<MalVal> = []
+    (ast as MalHashMap).val.each((k string, v MalVal) => {
+      result.append(EVAL(MalVal.fromHashKey(k), env))
+      result.append(EVAL(v, env))
+    })
+    return MalHashMap.fromList(result)
+  } else {
+    return ast
+  }
+}
+
+def EVAL(ast MalVal, env StringMap<MalVal>) MalVal {
+  if !(ast is MalList) { return eval_ast(ast, env) }
+  var astList = ast as MalList
+  if astList.isEmpty { return ast }
+  var evaledList = eval_ast(ast, env) as MalList
+  var fn = evaledList[0] as MalNativeFunc
+  return fn.call(evaledList.val.slice(1))
+}
+
+def PRINT(exp MalVal) string {
+  return exp?.print(true)
+}
+
+var repl_env StringMap<MalVal> = {
+  "+": MalNativeFunc.new((args List<MalVal>) MalVal => MalNumber.new((args[0] as MalNumber).val + (args[1] as MalNumber).val)),
+  "-": MalNativeFunc.new((args List<MalVal>) MalVal => MalNumber.new((args[0] as MalNumber).val - (args[1] as MalNumber).val)),
+  "*": MalNativeFunc.new((args List<MalVal>) MalVal => MalNumber.new((args[0] as MalNumber).val * (args[1] as MalNumber).val)),
+  "/": MalNativeFunc.new((args List<MalVal>) MalVal => MalNumber.new((args[0] as MalNumber).val / (args[1] as MalNumber).val)),
+}
+
+def REP(str string) string {
+  return PRINT(EVAL(READ(str), repl_env))
+}
+
+@entry
+def main {
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    try {
+      printLn(REP(line))
+    }
+    catch e MalError {
+      printLn("Error: \(e.message)")
+    }
+  }
+}
diff --git a/skew/step3_env.sk b/skew/step3_env.sk
new file mode 100644 (file)
index 0000000..02e4595
--- /dev/null
@@ -0,0 +1,72 @@
+def READ(str string) MalVal {
+  return read_str(str)
+}
+
+def eval_ast(ast MalVal, env Env) MalVal {
+  if ast is MalSymbol {
+    return env.get(ast as MalSymbol)
+  } else if ast is MalList {
+    return MalList.new((ast as MalList).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalVector {
+    return MalVector.new((ast as MalVector).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalHashMap {
+    var result List<MalVal> = []
+    (ast as MalHashMap).val.each((k string, v MalVal) => {
+      result.append(EVAL(MalVal.fromHashKey(k), env))
+      result.append(EVAL(v, env))
+    })
+    return MalHashMap.fromList(result)
+  } else {
+    return ast
+  }
+}
+
+def EVAL(ast MalVal, env Env) MalVal {
+  if !(ast is MalList) { return eval_ast(ast, env) }
+  const astList = ast as MalList
+  if astList.isEmpty { return ast }
+  const a0sym = astList[0] as MalSymbol
+  if a0sym.val == "def!" {
+    return env.set(astList[1] as MalSymbol, EVAL(astList[2], env))
+  } else if a0sym.val == "let*" {
+    var letenv = Env.new(env)
+    const assigns = astList[1] as MalSequential
+    for i = 0; i < assigns.count; i += 2 {
+      letenv.set(assigns[i] as MalSymbol, EVAL(assigns[i + 1], letenv))
+    }
+    return EVAL(astList[2], letenv)
+  } else {
+    const evaledList = eval_ast(ast, env) as MalList
+    const fn = evaledList[0] as MalNativeFunc
+    return fn.call(evaledList.val.slice(1))
+  }
+}
+
+def PRINT(exp MalVal) string {
+  return exp?.print(true)
+}
+
+var repl_env = Env.new(null)
+
+def REP(str string) string {
+  return PRINT(EVAL(READ(str), repl_env))
+}
+
+@entry
+def main {
+  repl_env.set(MalSymbol.new("+"), MalNativeFunc.new((args List<MalVal>) MalVal => MalNumber.new((args[0] as MalNumber).val + (args[1] as MalNumber).val)))
+  repl_env.set(MalSymbol.new("-"), MalNativeFunc.new((args List<MalVal>) MalVal => MalNumber.new((args[0] as MalNumber).val - (args[1] as MalNumber).val)))
+  repl_env.set(MalSymbol.new("*"), MalNativeFunc.new((args List<MalVal>) MalVal => MalNumber.new((args[0] as MalNumber).val * (args[1] as MalNumber).val)))
+  repl_env.set(MalSymbol.new("/"), MalNativeFunc.new((args List<MalVal>) MalVal => MalNumber.new((args[0] as MalNumber).val / (args[1] as MalNumber).val)))
+
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    try {
+      printLn(REP(line))
+    }
+    catch e MalError {
+      printLn("Error: \(e.message)")
+    }
+  }
+}
diff --git a/skew/step4_if_fn_do.sk b/skew/step4_if_fn_do.sk
new file mode 100644 (file)
index 0000000..58ab70e
--- /dev/null
@@ -0,0 +1,90 @@
+def READ(str string) MalVal {
+  return read_str(str)
+}
+
+def eval_ast(ast MalVal, env Env) MalVal {
+  if ast is MalSymbol {
+    return env.get(ast as MalSymbol)
+  } else if ast is MalList {
+    return MalList.new((ast as MalList).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalVector {
+    return MalVector.new((ast as MalVector).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalHashMap {
+    var result List<MalVal> = []
+    (ast as MalHashMap).val.each((k string, v MalVal) => {
+      result.append(EVAL(MalVal.fromHashKey(k), env))
+      result.append(EVAL(v, env))
+    })
+    return MalHashMap.fromList(result)
+  } else {
+    return ast
+  }
+}
+
+def EVAL(ast MalVal, env Env) MalVal {
+  if !(ast is MalList) { return eval_ast(ast, env) }
+  const astList = ast as MalList
+  if astList.isEmpty { return ast }
+  const a0sym = astList[0] as MalSymbol
+  if a0sym.val == "def!" {
+    return env.set(astList[1] as MalSymbol, EVAL(astList[2], env))
+  } else if a0sym.val == "let*" {
+    var letenv = Env.new(env)
+    const assigns = astList[1] as MalSequential
+    for i = 0; i < assigns.count; i += 2 {
+      letenv.set(assigns[i] as MalSymbol, EVAL(assigns[i + 1], letenv))
+    }
+    return EVAL(astList[2], letenv)
+  } else if a0sym.val == "do" {
+    const r = eval_ast(MalList.new(astList.val.slice(1)), env) as MalList
+    return r[r.count - 1]
+  } else if a0sym.val == "if" {
+    const condRes = EVAL(astList[1], env)
+    if condRes is MalNil || condRes is MalFalse {
+      return astList.count > 3 ? EVAL(astList[3], env) : gNil
+    } else {
+      return EVAL(astList[2], env)
+    }
+  } else if a0sym.val == "fn*" {
+    const argsNames = (astList[1] as MalSequential).val
+    return MalNativeFunc.new((args List<MalVal>) => EVAL(astList[2], Env.new(env, argsNames, args)))
+  } else {
+    const evaledList = eval_ast(ast, env) as MalList
+    const fn = evaledList[0] as MalNativeFunc
+    return fn.call(evaledList.val.slice(1))
+  }
+}
+
+def PRINT(exp MalVal) string {
+  return exp?.print(true)
+}
+
+var repl_env = Env.new(null)
+
+def RE(str string) MalVal {
+  return EVAL(READ(str), repl_env)
+}
+
+def REP(str string) string {
+  return PRINT(RE(str))
+}
+
+@entry
+def main {
+  # core.sk: defined using Skew
+  ns.each((name, func) => repl_env.set(MalSymbol.new(name), MalNativeFunc.new(func)))
+
+  # core.mal: defined using the language itself
+  RE("(def! not (fn* (a) (if a false true)))")
+
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    try {
+      printLn(REP(line))
+    }
+    catch e MalError {
+      printLn("Error: \(e.message)")
+    }
+  }
+}
diff --git a/skew/step5_tco.sk b/skew/step5_tco.sk
new file mode 100644 (file)
index 0000000..40985c5
--- /dev/null
@@ -0,0 +1,110 @@
+def READ(str string) MalVal {
+  return read_str(str)
+}
+
+def eval_ast(ast MalVal, env Env) MalVal {
+  if ast is MalSymbol {
+    return env.get(ast as MalSymbol)
+  } else if ast is MalList {
+    return MalList.new((ast as MalList).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalVector {
+    return MalVector.new((ast as MalVector).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalHashMap {
+    var result List<MalVal> = []
+    (ast as MalHashMap).val.each((k string, v MalVal) => {
+      result.append(EVAL(MalVal.fromHashKey(k), env))
+      result.append(EVAL(v, env))
+    })
+    return MalHashMap.fromList(result)
+  } else {
+    return ast
+  }
+}
+
+def EVAL(ast MalVal, env Env) MalVal {
+  while true {
+    if !(ast is MalList) { return eval_ast(ast, env) }
+    const astList = ast as MalList
+    if astList.isEmpty { return ast }
+    const a0sym = astList[0] as MalSymbol
+    if a0sym.val == "def!" {
+      return env.set(astList[1] as MalSymbol, EVAL(astList[2], env))
+    } else if a0sym.val == "let*" {
+      var letenv = Env.new(env)
+      const assigns = astList[1] as MalSequential
+      for i = 0; i < assigns.count; i += 2 {
+        letenv.set(assigns[i] as MalSymbol, EVAL(assigns[i + 1], letenv))
+      }
+      ast = astList[2]
+      env = letenv
+      continue # TCO
+    } else if a0sym.val == "do" {
+      const parts = astList.val.slice(1)
+      eval_ast(MalList.new(parts.slice(0, parts.count - 1)), env)
+      ast = parts[parts.count - 1]
+      continue # TCO
+    } else if a0sym.val == "if" {
+      const condRes = EVAL(astList[1], env)
+      if condRes is MalNil || condRes is MalFalse {
+        ast = astList.count > 3 ? astList[3] : gNil
+      } else {
+        ast = astList[2]
+      }
+      continue # TCO
+    } else if a0sym.val == "fn*" {
+      const argsNames = astList[1] as MalSequential
+      return MalFunc.new(astList[2], argsNames, env, (args List<MalVal>) => EVAL(astList[2], Env.new(env, argsNames.val, args)))
+    } else {
+      const evaledList = eval_ast(ast, env) as MalList
+      const fn = evaledList[0]
+      const callArgs = evaledList.val.slice(1)
+      if fn is MalNativeFunc {
+        return (fn as MalNativeFunc).call(callArgs)
+      } else if fn is MalFunc {
+        const f = fn as MalFunc
+        ast = f.ast
+        env = Env.new(f.env, f.params.val, callArgs)
+        continue # TCO
+      } else {
+        throw MalError.new("Expected function as head of list")
+      }
+    }
+  }
+}
+
+def PRINT(exp MalVal) string {
+  return exp?.print(true)
+}
+
+var repl_env = Env.new(null)
+
+def RE(str string) MalVal {
+  return EVAL(READ(str), repl_env)
+}
+
+def REP(str string) string {
+  return PRINT(RE(str))
+}
+
+@entry
+def main {
+  # core.sk: defined using Skew
+  ns.each((name, func) => repl_env.set(MalSymbol.new(name), MalNativeFunc.new(func)))
+
+  # core.mal: defined using the language itself
+  RE("(def! not (fn* (a) (if a false true)))")
+
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    try {
+      printLn(REP(line))
+    }
+    catch e MalError {
+      printLn("Error: \(e.message)")
+    }
+    catch e Error {
+      printLn("Error: \(e.message)")
+    }
+  }
+}
diff --git a/skew/step6_file.sk b/skew/step6_file.sk
new file mode 100644 (file)
index 0000000..702fb2f
--- /dev/null
@@ -0,0 +1,117 @@
+def READ(str string) MalVal {
+  return read_str(str)
+}
+
+def eval_ast(ast MalVal, env Env) MalVal {
+  if ast is MalSymbol {
+    return env.get(ast as MalSymbol)
+  } else if ast is MalList {
+    return MalList.new((ast as MalList).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalVector {
+    return MalVector.new((ast as MalVector).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalHashMap {
+    var result List<MalVal> = []
+    (ast as MalHashMap).val.each((k string, v MalVal) => {
+      result.append(EVAL(MalVal.fromHashKey(k), env))
+      result.append(EVAL(v, env))
+    })
+    return MalHashMap.fromList(result)
+  } else {
+    return ast
+  }
+}
+
+def EVAL(ast MalVal, env Env) MalVal {
+  while true {
+    if !(ast is MalList) { return eval_ast(ast, env) }
+    const astList = ast as MalList
+    if astList.isEmpty { return ast }
+    const a0sym = astList[0] as MalSymbol
+    if a0sym.val == "def!" {
+      return env.set(astList[1] as MalSymbol, EVAL(astList[2], env))
+    } else if a0sym.val == "let*" {
+      var letenv = Env.new(env)
+      const assigns = astList[1] as MalSequential
+      for i = 0; i < assigns.count; i += 2 {
+        letenv.set(assigns[i] as MalSymbol, EVAL(assigns[i + 1], letenv))
+      }
+      ast = astList[2]
+      env = letenv
+      continue # TCO
+    } else if a0sym.val == "do" {
+      const parts = astList.val.slice(1)
+      eval_ast(MalList.new(parts.slice(0, parts.count - 1)), env)
+      ast = parts[parts.count - 1]
+      continue # TCO
+    } else if a0sym.val == "if" {
+      const condRes = EVAL(astList[1], env)
+      if condRes is MalNil || condRes is MalFalse {
+        ast = astList.count > 3 ? astList[3] : gNil
+      } else {
+        ast = astList[2]
+      }
+      continue # TCO
+    } else if a0sym.val == "fn*" {
+      const argsNames = astList[1] as MalSequential
+      return MalFunc.new(astList[2], argsNames, env, (args List<MalVal>) => EVAL(astList[2], Env.new(env, argsNames.val, args)))
+    } else {
+      const evaledList = eval_ast(ast, env) as MalList
+      const fn = evaledList[0]
+      const callArgs = evaledList.val.slice(1)
+      if fn is MalNativeFunc {
+        return (fn as MalNativeFunc).call(callArgs)
+      } else if fn is MalFunc {
+        const f = fn as MalFunc
+        ast = f.ast
+        env = Env.new(f.env, f.params.val, callArgs)
+        continue # TCO
+      } else {
+        throw MalError.new("Expected function as head of list")
+      }
+    }
+  }
+}
+
+def PRINT(exp MalVal) string {
+  return exp?.print(true)
+}
+
+var repl_env = Env.new(null)
+
+def RE(str string) MalVal {
+  return EVAL(READ(str), repl_env)
+}
+
+def REP(str string) string {
+  return PRINT(RE(str))
+}
+
+@entry
+def main {
+  # core.sk: defined using Skew
+  ns.each((name, func) => repl_env.set(MalSymbol.new(name), MalNativeFunc.new(func)))
+  repl_env.set(MalSymbol.new("*ARGV*"), MalList.new(argv.isEmpty ? [] : argv.slice(1).map<MalVal>(e => MalString.new(e))))
+
+  # core.mal: defined using the language itself
+  RE("(def! not (fn* (a) (if a false true)))")
+  RE("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))")
+
+  if argv.count > 0 {
+    RE("(load-file \"" + argv[0] + "\")")
+    return
+  }
+
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    try {
+      printLn(REP(line))
+    }
+    catch e MalError {
+      printLn("Error: \(e.message)")
+    }
+    catch e Error {
+      printLn("Error: \(e.message)")
+    }
+  }
+}
diff --git a/skew/step7_quote.sk b/skew/step7_quote.sk
new file mode 100644 (file)
index 0000000..8594e61
--- /dev/null
@@ -0,0 +1,144 @@
+def READ(str string) MalVal {
+  return read_str(str)
+}
+
+def isPair(a MalVal) bool {
+  return a is MalSequential && !(a as MalSequential).isEmpty
+}
+
+def quasiquote(ast MalVal) MalVal {
+  if !isPair(ast) {
+    return MalList.new([MalSymbol.new("quote"), ast])
+  }
+  const astSeq = ast as MalSequential
+  const a0 = astSeq[0]
+  if a0.isSymbol("unquote") {
+    return astSeq[1]
+  }
+  if isPair(a0) {
+    const a0Seq = a0 as MalSequential
+    if a0Seq[0].isSymbol("splice-unquote") {
+      return MalList.new([MalSymbol.new("concat"), a0Seq[1], quasiquote(astSeq.rest)])
+    }
+  }
+  return MalList.new([MalSymbol.new("cons"), quasiquote(a0), quasiquote(astSeq.rest)])
+}
+
+def eval_ast(ast MalVal, env Env) MalVal {
+  if ast is MalSymbol {
+    return env.get(ast as MalSymbol)
+  } else if ast is MalList {
+    return MalList.new((ast as MalList).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalVector {
+    return MalVector.new((ast as MalVector).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalHashMap {
+    var result List<MalVal> = []
+    (ast as MalHashMap).val.each((k string, v MalVal) => {
+      result.append(EVAL(MalVal.fromHashKey(k), env))
+      result.append(EVAL(v, env))
+    })
+    return MalHashMap.fromList(result)
+  } else {
+    return ast
+  }
+}
+
+def EVAL(ast MalVal, env Env) MalVal {
+  while true {
+    if !(ast is MalList) { return eval_ast(ast, env) }
+    const astList = ast as MalList
+    if astList.isEmpty { return ast }
+    const a0sym = astList[0] as MalSymbol
+    if a0sym.val == "def!" {
+      return env.set(astList[1] as MalSymbol, EVAL(astList[2], env))
+    } else if a0sym.val == "let*" {
+      var letenv = Env.new(env)
+      const assigns = astList[1] as MalSequential
+      for i = 0; i < assigns.count; i += 2 {
+        letenv.set(assigns[i] as MalSymbol, EVAL(assigns[i + 1], letenv))
+      }
+      ast = astList[2]
+      env = letenv
+      continue # TCO
+    } else if a0sym.val == "quote" {
+      return astList[1]
+    } else if a0sym.val == "quasiquote" {
+      ast = quasiquote(astList[1])
+      continue # TCO
+    } else if a0sym.val == "do" {
+      const parts = astList.val.slice(1)
+      eval_ast(MalList.new(parts.slice(0, parts.count - 1)), env)
+      ast = parts[parts.count - 1]
+      continue # TCO
+    } else if a0sym.val == "if" {
+      const condRes = EVAL(astList[1], env)
+      if condRes is MalNil || condRes is MalFalse {
+        ast = astList.count > 3 ? astList[3] : gNil
+      } else {
+        ast = astList[2]
+      }
+      continue # TCO
+    } else if a0sym.val == "fn*" {
+      const argsNames = astList[1] as MalSequential
+      return MalFunc.new(astList[2], argsNames, env, (args List<MalVal>) => EVAL(astList[2], Env.new(env, argsNames.val, args)))
+    } else {
+      const evaledList = eval_ast(ast, env) as MalList
+      const fn = evaledList[0]
+      const callArgs = evaledList.val.slice(1)
+      if fn is MalNativeFunc {
+        return (fn as MalNativeFunc).call(callArgs)
+      } else if fn is MalFunc {
+        const f = fn as MalFunc
+        ast = f.ast
+        env = Env.new(f.env, f.params.val, callArgs)
+        continue # TCO
+      } else {
+        throw MalError.new("Expected function as head of list")
+      }
+    }
+  }
+}
+
+def PRINT(exp MalVal) string {
+  return exp?.print(true)
+}
+
+var repl_env = Env.new(null)
+
+def RE(str string) MalVal {
+  return EVAL(READ(str), repl_env)
+}
+
+def REP(str string) string {
+  return PRINT(RE(str))
+}
+
+@entry
+def main {
+  # core.sk: defined using Skew
+  ns.each((name, func) => repl_env.set(MalSymbol.new(name), MalNativeFunc.new(func)))
+  repl_env.set(MalSymbol.new("*ARGV*"), MalList.new(argv.isEmpty ? [] : argv.slice(1).map<MalVal>(e => MalString.new(e))))
+
+  # core.mal: defined using the language itself
+  RE("(def! not (fn* (a) (if a false true)))")
+  RE("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))")
+
+  if argv.count > 0 {
+    RE("(load-file \"" + argv[0] + "\")")
+    return
+  }
+
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    try {
+      printLn(REP(line))
+    }
+    catch e MalError {
+      printLn("Error: \(e.message)")
+    }
+    catch e Error {
+      printLn("Error: \(e.message)")
+    }
+  }
+}
diff --git a/skew/step8_macros.sk b/skew/step8_macros.sk
new file mode 100644 (file)
index 0000000..4456597
--- /dev/null
@@ -0,0 +1,176 @@
+def READ(str string) MalVal {
+  return read_str(str)
+}
+
+def isPair(a MalVal) bool {
+  return a is MalSequential && !(a as MalSequential).isEmpty
+}
+
+def quasiquote(ast MalVal) MalVal {
+  if !isPair(ast) {
+    return MalList.new([MalSymbol.new("quote"), ast])
+  }
+  const astSeq = ast as MalSequential
+  const a0 = astSeq[0]
+  if a0.isSymbol("unquote") {
+    return astSeq[1]
+  }
+  if isPair(a0) {
+    const a0Seq = a0 as MalSequential
+    if a0Seq[0].isSymbol("splice-unquote") {
+      return MalList.new([MalSymbol.new("concat"), a0Seq[1], quasiquote(astSeq.rest)])
+    }
+  }
+  return MalList.new([MalSymbol.new("cons"), quasiquote(a0), quasiquote(astSeq.rest)])
+}
+
+def isMacro(ast MalVal, env Env) bool {
+  if !(ast is MalList) { return false }
+  const astList = ast as MalList
+  if astList.isEmpty { return false }
+  const a0 = astList[0]
+  if !(a0 is MalSymbol) { return false }
+  const a0Sym = a0 as MalSymbol
+  if env.find(a0Sym) == null { return false }
+  const f = env.get(a0Sym)
+  if !(f is MalFunc) { return false }
+  return (f as MalFunc).isMacro
+}
+
+def macroexpand(ast MalVal, env Env) MalVal {
+  while isMacro(ast, env) {
+    const astList = ast as MalList
+    const mac = env.get(astList[0] as MalSymbol) as MalFunc
+    ast = mac.call((astList.rest as MalSequential).val)
+  }
+  return ast
+}
+
+def eval_ast(ast MalVal, env Env) MalVal {
+  if ast is MalSymbol {
+    return env.get(ast as MalSymbol)
+  } else if ast is MalList {
+    return MalList.new((ast as MalList).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalVector {
+    return MalVector.new((ast as MalVector).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalHashMap {
+    var result List<MalVal> = []
+    (ast as MalHashMap).val.each((k string, v MalVal) => {
+      result.append(EVAL(MalVal.fromHashKey(k), env))
+      result.append(EVAL(v, env))
+    })
+    return MalHashMap.fromList(result)
+  } else {
+    return ast
+  }
+}
+
+def EVAL(ast MalVal, env Env) MalVal {
+  while true {
+    if !(ast is MalList) { return eval_ast(ast, env) }
+    ast = macroexpand(ast, env)
+    if !(ast is MalList) { return eval_ast(ast, env) }
+    const astList = ast as MalList
+    if astList.isEmpty { return ast }
+    const a0sym = astList[0] as MalSymbol
+    if a0sym.val == "def!" {
+      return env.set(astList[1] as MalSymbol, EVAL(astList[2], env))
+    } else if a0sym.val == "let*" {
+      var letenv = Env.new(env)
+      const assigns = astList[1] as MalSequential
+      for i = 0; i < assigns.count; i += 2 {
+        letenv.set(assigns[i] as MalSymbol, EVAL(assigns[i + 1], letenv))
+      }
+      ast = astList[2]
+      env = letenv
+      continue # TCO
+    } else if a0sym.val == "quote" {
+      return astList[1]
+    } else if a0sym.val == "quasiquote" {
+      ast = quasiquote(astList[1])
+      continue # TCO
+    } else if a0sym.val == "defmacro!" {
+      var macro = EVAL(astList[2], env) as MalFunc
+      macro.setAsMacro
+      return env.set(astList[1] as MalSymbol, macro)
+    } else if a0sym.val == "macroexpand" {
+      return macroexpand(astList[1], env)
+    } else if a0sym.val == "do" {
+      const parts = astList.val.slice(1)
+      eval_ast(MalList.new(parts.slice(0, parts.count - 1)), env)
+      ast = parts[parts.count - 1]
+      continue # TCO
+    } else if a0sym.val == "if" {
+      const condRes = EVAL(astList[1], env)
+      if condRes is MalNil || condRes is MalFalse {
+        ast = astList.count > 3 ? astList[3] : gNil
+      } else {
+        ast = astList[2]
+      }
+      continue # TCO
+    } else if a0sym.val == "fn*" {
+      const argsNames = astList[1] as MalSequential
+      return MalFunc.new(astList[2], argsNames, env, (args List<MalVal>) => EVAL(astList[2], Env.new(env, argsNames.val, args)))
+    } else {
+      const evaledList = eval_ast(ast, env) as MalList
+      const fn = evaledList[0]
+      const callArgs = evaledList.val.slice(1)
+      if fn is MalNativeFunc {
+        return (fn as MalNativeFunc).call(callArgs)
+      } else if fn is MalFunc {
+        const f = fn as MalFunc
+        ast = f.ast
+        env = Env.new(f.env, f.params.val, callArgs)
+        continue # TCO
+      } else {
+        throw MalError.new("Expected function as head of list")
+      }
+    }
+  }
+}
+
+def PRINT(exp MalVal) string {
+  return exp?.print(true)
+}
+
+var repl_env = Env.new(null)
+
+def RE(str string) MalVal {
+  return EVAL(READ(str), repl_env)
+}
+
+def REP(str string) string {
+  return PRINT(RE(str))
+}
+
+@entry
+def main {
+  # core.sk: defined using Skew
+  ns.each((name, func) => repl_env.set(MalSymbol.new(name), MalNativeFunc.new(func)))
+  repl_env.set(MalSymbol.new("*ARGV*"), MalList.new(argv.isEmpty ? [] : argv.slice(1).map<MalVal>(e => MalString.new(e))))
+
+  # core.mal: defined using the language itself
+  RE("(def! not (fn* (a) (if a false true)))")
+  RE("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))")
+  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)))))))")
+  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))))))))")
+
+  if argv.count > 0 {
+    RE("(load-file \"" + argv[0] + "\")")
+    return
+  }
+
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    try {
+      printLn(REP(line))
+    }
+    catch e MalError {
+      printLn("Error: \(e.message)")
+    }
+    catch e Error {
+      printLn("Error: \(e.message)")
+    }
+  }
+}
diff --git a/skew/step9_try.sk b/skew/step9_try.sk
new file mode 100644 (file)
index 0000000..179575e
--- /dev/null
@@ -0,0 +1,190 @@
+def READ(str string) MalVal {
+  return read_str(str)
+}
+
+def isPair(a MalVal) bool {
+  return a is MalSequential && !(a as MalSequential).isEmpty
+}
+
+def quasiquote(ast MalVal) MalVal {
+  if !isPair(ast) {
+    return MalList.new([MalSymbol.new("quote"), ast])
+  }
+  const astSeq = ast as MalSequential
+  const a0 = astSeq[0]
+  if a0.isSymbol("unquote") {
+    return astSeq[1]
+  }
+  if isPair(a0) {
+    const a0Seq = a0 as MalSequential
+    if a0Seq[0].isSymbol("splice-unquote") {
+      return MalList.new([MalSymbol.new("concat"), a0Seq[1], quasiquote(astSeq.rest)])
+    }
+  }
+  return MalList.new([MalSymbol.new("cons"), quasiquote(a0), quasiquote(astSeq.rest)])
+}
+
+def isMacro(ast MalVal, env Env) bool {
+  if !(ast is MalList) { return false }
+  const astList = ast as MalList
+  if astList.isEmpty { return false }
+  const a0 = astList[0]
+  if !(a0 is MalSymbol) { return false }
+  const a0Sym = a0 as MalSymbol
+  if env.find(a0Sym) == null { return false }
+  const f = env.get(a0Sym)
+  if !(f is MalFunc) { return false }
+  return (f as MalFunc).isMacro
+}
+
+def macroexpand(ast MalVal, env Env) MalVal {
+  while isMacro(ast, env) {
+    const astList = ast as MalList
+    const mac = env.get(astList[0] as MalSymbol) as MalFunc
+    ast = mac.call((astList.rest as MalSequential).val)
+  }
+  return ast
+}
+
+def eval_ast(ast MalVal, env Env) MalVal {
+  if ast is MalSymbol {
+    return env.get(ast as MalSymbol)
+  } else if ast is MalList {
+    return MalList.new((ast as MalList).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalVector {
+    return MalVector.new((ast as MalVector).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalHashMap {
+    var result List<MalVal> = []
+    (ast as MalHashMap).val.each((k string, v MalVal) => {
+      result.append(EVAL(MalVal.fromHashKey(k), env))
+      result.append(EVAL(v, env))
+    })
+    return MalHashMap.fromList(result)
+  } else {
+    return ast
+  }
+}
+
+def EVAL(ast MalVal, env Env) MalVal {
+  while true {
+    if !(ast is MalList) { return eval_ast(ast, env) }
+    ast = macroexpand(ast, env)
+    if !(ast is MalList) { return eval_ast(ast, env) }
+    const astList = ast as MalList
+    if astList.isEmpty { return ast }
+    const a0sym = astList[0] as MalSymbol
+    if a0sym.val == "def!" {
+      return env.set(astList[1] as MalSymbol, EVAL(astList[2], env))
+    } else if a0sym.val == "let*" {
+      var letenv = Env.new(env)
+      const assigns = astList[1] as MalSequential
+      for i = 0; i < assigns.count; i += 2 {
+        letenv.set(assigns[i] as MalSymbol, EVAL(assigns[i + 1], letenv))
+      }
+      ast = astList[2]
+      env = letenv
+      continue # TCO
+    } else if a0sym.val == "quote" {
+      return astList[1]
+    } else if a0sym.val == "quasiquote" {
+      ast = quasiquote(astList[1])
+      continue # TCO
+    } else if a0sym.val == "defmacro!" {
+      var macro = EVAL(astList[2], env) as MalFunc
+      macro.setAsMacro
+      return env.set(astList[1] as MalSymbol, macro)
+    } else if a0sym.val == "macroexpand" {
+      return macroexpand(astList[1], env)
+    } else if a0sym.val == "try*" {
+      var exc MalVal
+      try {
+        return EVAL(astList[1], env)
+      }
+      catch e MalUserError { exc = e.data }
+      catch e MalError { exc = MalString.new(e.message) }
+      catch e Error { exc = MalString.new(e.message) }
+      const catchClause = astList[2] as MalList
+      var catchEnv = Env.new(env, [catchClause[1] as MalSymbol], [exc])
+      return EVAL(catchClause[2], catchEnv)
+    } else if a0sym.val == "do" {
+      const parts = astList.val.slice(1)
+      eval_ast(MalList.new(parts.slice(0, parts.count - 1)), env)
+      ast = parts[parts.count - 1]
+      continue # TCO
+    } else if a0sym.val == "if" {
+      const condRes = EVAL(astList[1], env)
+      if condRes is MalNil || condRes is MalFalse {
+        ast = astList.count > 3 ? astList[3] : gNil
+      } else {
+        ast = astList[2]
+      }
+      continue # TCO
+    } else if a0sym.val == "fn*" {
+      const argsNames = astList[1] as MalSequential
+      return MalFunc.new(astList[2], argsNames, env, (args List<MalVal>) => EVAL(astList[2], Env.new(env, argsNames.val, args)))
+    } else {
+      const evaledList = eval_ast(ast, env) as MalList
+      const fn = evaledList[0]
+      const callArgs = evaledList.val.slice(1)
+      if fn is MalNativeFunc {
+        return (fn as MalNativeFunc).call(callArgs)
+      } else if fn is MalFunc {
+        const f = fn as MalFunc
+        ast = f.ast
+        env = Env.new(f.env, f.params.val, callArgs)
+        continue # TCO
+      } else {
+        throw MalError.new("Expected function as head of list")
+      }
+    }
+  }
+}
+
+def PRINT(exp MalVal) string {
+  return exp?.print(true)
+}
+
+var repl_env = Env.new(null)
+
+def RE(str string) MalVal {
+  return EVAL(READ(str), repl_env)
+}
+
+def REP(str string) string {
+  return PRINT(RE(str))
+}
+
+@entry
+def main {
+  # core.sk: defined using Skew
+  ns.each((name, func) => repl_env.set(MalSymbol.new(name), MalNativeFunc.new(func)))
+  repl_env.set(MalSymbol.new("*ARGV*"), MalList.new(argv.isEmpty ? [] : argv.slice(1).map<MalVal>(e => MalString.new(e))))
+
+  # core.mal: defined using the language itself
+  RE("(def! not (fn* (a) (if a false true)))")
+  RE("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))")
+  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)))))))")
+  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))))))))")
+
+  if argv.count > 0 {
+    RE("(load-file \"" + argv[0] + "\")")
+    return
+  }
+
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    try {
+      printLn(REP(line))
+    }
+    catch e MalUserError {
+      printLn("Error: \(e.data.print(false))")
+    }
+    catch e MalError {
+      printLn("Error: \(e.message)")
+    }
+    catch e Error {
+      printLn("Error: \(e.message)")
+    }
+  }
+}
diff --git a/skew/stepA_mal.sk b/skew/stepA_mal.sk
new file mode 100644 (file)
index 0000000..99d8a39
--- /dev/null
@@ -0,0 +1,194 @@
+def READ(str string) MalVal {
+  return read_str(str)
+}
+
+def isPair(a MalVal) bool {
+  return a is MalSequential && !(a as MalSequential).isEmpty
+}
+
+def quasiquote(ast MalVal) MalVal {
+  if !isPair(ast) {
+    return MalList.new([MalSymbol.new("quote"), ast])
+  }
+  const astSeq = ast as MalSequential
+  const a0 = astSeq[0]
+  if a0.isSymbol("unquote") {
+    return astSeq[1]
+  }
+  if isPair(a0) {
+    const a0Seq = a0 as MalSequential
+    if a0Seq[0].isSymbol("splice-unquote") {
+      return MalList.new([MalSymbol.new("concat"), a0Seq[1], quasiquote(astSeq.rest)])
+    }
+  }
+  return MalList.new([MalSymbol.new("cons"), quasiquote(a0), quasiquote(astSeq.rest)])
+}
+
+def isMacro(ast MalVal, env Env) bool {
+  if !(ast is MalList) { return false }
+  const astList = ast as MalList
+  if astList.isEmpty { return false }
+  const a0 = astList[0]
+  if !(a0 is MalSymbol) { return false }
+  const a0Sym = a0 as MalSymbol
+  if env.find(a0Sym) == null { return false }
+  const f = env.get(a0Sym)
+  if !(f is MalFunc) { return false }
+  return (f as MalFunc).isMacro
+}
+
+def macroexpand(ast MalVal, env Env) MalVal {
+  while isMacro(ast, env) {
+    const astList = ast as MalList
+    const mac = env.get(astList[0] as MalSymbol) as MalFunc
+    ast = mac.call((astList.rest as MalSequential).val)
+  }
+  return ast
+}
+
+def eval_ast(ast MalVal, env Env) MalVal {
+  if ast is MalSymbol {
+    return env.get(ast as MalSymbol)
+  } else if ast is MalList {
+    return MalList.new((ast as MalList).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalVector {
+    return MalVector.new((ast as MalVector).val.map<MalVal>(e => EVAL(e, env)))
+  } else if ast is MalHashMap {
+    var result List<MalVal> = []
+    (ast as MalHashMap).val.each((k string, v MalVal) => {
+      result.append(EVAL(MalVal.fromHashKey(k), env))
+      result.append(EVAL(v, env))
+    })
+    return MalHashMap.fromList(result)
+  } else {
+    return ast
+  }
+}
+
+def EVAL(ast MalVal, env Env) MalVal {
+  while true {
+    if !(ast is MalList) { return eval_ast(ast, env) }
+    ast = macroexpand(ast, env)
+    if !(ast is MalList) { return eval_ast(ast, env) }
+    const astList = ast as MalList
+    if astList.isEmpty { return ast }
+    const a0sym = astList[0] as MalSymbol
+    if a0sym.val == "def!" {
+      return env.set(astList[1] as MalSymbol, EVAL(astList[2], env))
+    } else if a0sym.val == "let*" {
+      var letenv = Env.new(env)
+      const assigns = astList[1] as MalSequential
+      for i = 0; i < assigns.count; i += 2 {
+        letenv.set(assigns[i] as MalSymbol, EVAL(assigns[i + 1], letenv))
+      }
+      ast = astList[2]
+      env = letenv
+      continue # TCO
+    } else if a0sym.val == "quote" {
+      return astList[1]
+    } else if a0sym.val == "quasiquote" {
+      ast = quasiquote(astList[1])
+      continue # TCO
+    } else if a0sym.val == "defmacro!" {
+      var macro = EVAL(astList[2], env) as MalFunc
+      macro.setAsMacro
+      return env.set(astList[1] as MalSymbol, macro)
+    } else if a0sym.val == "macroexpand" {
+      return macroexpand(astList[1], env)
+    } else if a0sym.val == "try*" {
+      var exc MalVal
+      try {
+        return EVAL(astList[1], env)
+      }
+      catch e MalUserError { exc = e.data }
+      catch e MalError { exc = MalString.new(e.message) }
+      catch e Error { exc = MalString.new(e.message) }
+      const catchClause = astList[2] as MalList
+      var catchEnv = Env.new(env, [catchClause[1] as MalSymbol], [exc])
+      return EVAL(catchClause[2], catchEnv)
+    } else if a0sym.val == "do" {
+      const parts = astList.val.slice(1)
+      eval_ast(MalList.new(parts.slice(0, parts.count - 1)), env)
+      ast = parts[parts.count - 1]
+      continue # TCO
+    } else if a0sym.val == "if" {
+      const condRes = EVAL(astList[1], env)
+      if condRes is MalNil || condRes is MalFalse {
+        ast = astList.count > 3 ? astList[3] : gNil
+      } else {
+        ast = astList[2]
+      }
+      continue # TCO
+    } else if a0sym.val == "fn*" {
+      const argsNames = astList[1] as MalSequential
+      return MalFunc.new(astList[2], argsNames, env, (args List<MalVal>) => EVAL(astList[2], Env.new(env, argsNames.val, args)))
+    } else {
+      const evaledList = eval_ast(ast, env) as MalList
+      const fn = evaledList[0]
+      const callArgs = evaledList.val.slice(1)
+      if fn is MalNativeFunc {
+        return (fn as MalNativeFunc).call(callArgs)
+      } else if fn is MalFunc {
+        const f = fn as MalFunc
+        ast = f.ast
+        env = Env.new(f.env, f.params.val, callArgs)
+        continue # TCO
+      } else {
+        throw MalError.new("Expected function as head of list")
+      }
+    }
+  }
+}
+
+def PRINT(exp MalVal) string {
+  return exp?.print(true)
+}
+
+var repl_env = Env.new(null)
+
+def RE(str string) MalVal {
+  return EVAL(READ(str), repl_env)
+}
+
+def REP(str string) string {
+  return PRINT(RE(str))
+}
+
+@entry
+def main {
+  # core.sk: defined using Skew
+  ns.each((name, func) => repl_env.set(MalSymbol.new(name), MalNativeFunc.new(func)))
+  repl_env.set(MalSymbol.new("*ARGV*"), MalList.new(argv.isEmpty ? [] : argv.slice(1).map<MalVal>(e => MalString.new(e))))
+
+  # core.mal: defined using the language itself
+  RE("(def! *host-language* \"skew\")")
+  RE("(def! not (fn* (a) (if a false true)))")
+  RE("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))")
+  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)))))))")
+  RE("(def! *gensym-counter* (atom 0))")
+  RE("(def! gensym (fn* [] (symbol (str \"G__\" (swap! *gensym-counter* (fn* [x] (+ 1 x)))))))")
+  RE("(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)))))))))")
+
+  if argv.count > 0 {
+    RE("(load-file \"" + argv[0] + "\")")
+    return
+  }
+
+  RE("(println (str \"Mal [\" *host-language* \"]\"))")
+  var line string
+  while (line = readLine("user> ")) != null {
+    if line == "" { continue }
+    try {
+      printLn(REP(line))
+    }
+    catch e MalUserError {
+      printLn("Error: \(e.data.print(false))")
+    }
+    catch e MalError {
+      printLn("Error: \(e.message)")
+    }
+    catch e Error {
+      printLn("Error: \(e.message)")
+    }
+  }
+}
diff --git a/skew/tests/step5_tco.mal b/skew/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/skew/types.sk b/skew/types.sk
new file mode 100644 (file)
index 0000000..f12ef88
--- /dev/null
@@ -0,0 +1,250 @@
+class MalError {
+  const message string
+}
+
+class MalUserError {
+  const data MalVal
+}
+
+class MalVal {
+  var _meta MalVal = gNil
+  def toHashKey string { throw MalError.new("Not allowed as hash map key") }
+  def print(readable bool) string
+  def equal(o MalVal) bool
+  def isSymbol(name string) bool { return false }
+  def seq MalVal { throw MalError.new("seq: called on non-sequence") }
+  def meta MalVal { return _meta }
+  def _setMeta(newMeta MalVal) { _meta = newMeta }
+  def withMeta(newMeta MalVal) MalVal {
+    var res = self.clone
+    res._setMeta(newMeta)
+    return res
+  }
+  def clone MalVal
+}
+
+namespace MalVal {
+  def fromHashKey(key string) MalVal {
+    if key.startsWith("S_") { return MalString.new(key.slice(2)) }
+    else if key.startsWith("K_") { return MalKeyword.new(key.slice(2)) }
+    else { throw "Illegal hash key string" }
+  }
+  def fromBool(b bool) MalVal { return b ? gTrue : gFalse }
+}
+
+class MalNil : MalVal {
+  over print(readable bool) string { return "nil" }
+  over equal(o MalVal) bool { return o is MalNil }
+  over seq MalVal { return gNil }
+  over clone MalVal { return self }
+}
+const gNil = MalNil.new
+
+class MalTrue : MalVal {
+  over print(readable bool) string { return "true" }
+  over equal(o MalVal) bool { return o is MalTrue }
+  over clone MalVal { return self }
+}
+const gTrue = MalTrue.new
+
+class MalFalse : MalVal {
+  over print(readable bool) string { return "false" }
+  over equal(o MalVal) bool { return o is MalFalse }
+  over clone MalVal { return self }
+}
+const gFalse = MalFalse.new
+
+class MalNumber : MalVal {
+  const _data int
+  over print(readable bool) string { return _data.toString }
+  def val int { return _data }
+  over equal(o MalVal) bool { return o is MalNumber && (o as MalNumber).val == val }
+  over clone MalVal { return self }
+}
+
+class MalSymbol : MalVal {
+  const _data string
+  over print(readable bool) string { return _data }
+  def val string { return _data }
+  over equal(o MalVal) bool { return o is MalSymbol && (o as MalSymbol).val == val }
+  over isSymbol(name string) bool { return _data == name }
+  over clone MalVal { return MalSymbol.new(_data) }
+}
+
+class MalString : MalVal {
+  const _data string
+  over print(readable bool) string { return readable ? "\"\(escaped_data)\"" : _data }
+  over toHashKey string { return "S_\(_data)" }
+  def val string { return _data }
+  over equal(o MalVal) bool { return o is MalString && (o as MalString).val == val }
+  def escaped_data string {
+    return _data.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n")
+  }
+  over seq MalVal { return _data.count == 0 ? gNil : MalList.new(_data.split("").map<MalVal>(e => MalString.new(e))) }
+  over clone MalVal { return MalString.new(_data) }
+}
+
+class MalKeyword : MalVal {
+  const _data string
+  over print(readable bool) string { return ":\(_data)" }
+  over toHashKey string { return "K_\(_data)" }
+  def val string { return _data }
+  over equal(o MalVal) bool { return o is MalKeyword && (o as MalKeyword).val == val }
+  over clone MalVal { return MalKeyword.new(_data) }
+}
+
+class MalSequential : MalVal {
+  const _data List<MalVal>
+  def val List<MalVal> { return _data }
+  def isEmpty bool { return _data.isEmpty }
+  def asOneString(readable bool) string {
+    return " ".join(_data.map<string>(v => v.print(readable)))
+  }
+  def count int { return _data.count }
+  def [](index int) MalVal { return _data[index] }
+  over equal(o MalVal) bool {
+    if !(o is MalSequential) { return false }
+    const oval = (o as MalSequential).val
+    if val.count != oval.count { return false }
+    for i in 0..val.count {
+      if !val[i].equal(oval[i]) { return false }
+    }
+    return true
+  }
+  def nth(position int) MalVal {
+    if position >= count { throw MalError.new("nth: index out of range") }
+    return val[position]
+  }
+  def first MalVal {
+    if isEmpty { return gNil }
+    return val[0]
+  }
+  def rest MalVal {
+    if isEmpty { return MalList.new([]) }
+    return MalList.new(val.slice(1))
+  }
+  def conj(args List<MalVal>) MalVal
+}
+
+class MalList : MalSequential {
+  over print(readable bool) string { return "(" + asOneString(readable) + ")" }
+  over seq MalVal { return isEmpty ? gNil : self }
+  over conj(args List<MalVal>) MalVal {
+    var res = args.clone
+    res.reverse
+    res.append(_data)
+    return MalList.new(res)
+  }
+  over clone MalVal { return MalList.new(_data) }
+}
+
+class MalVector : MalSequential {
+  over print(readable bool) string { return "[" + asOneString(readable) + "]" }
+  over seq MalVal { return isEmpty ? gNil : MalList.new(_data) }
+  over conj(args List<MalVal>) MalVal {
+    var res = _data.clone
+    res.append(args)
+    return MalVector.new(res)
+  }
+  over clone MalVal { return MalVector.new(_data) }
+}
+
+class MalHashMap : MalVal {
+  const _data StringMap<MalVal>
+  over print(readable bool) string {
+    var pairs List<string> = []
+    _data.each((k string, v MalVal) => pairs.append("\(MalVal.fromHashKey(k).print(readable)) \(v.print(readable))"))
+    return "{" + " ".join(pairs) + "}"
+  }
+  def val StringMap<MalVal> { return _data }
+  over equal(o MalVal) bool {
+    if !(o is MalHashMap) { return false }
+    const oh = o as MalHashMap
+    if oh.val.count != val.count { return false }
+    var allEqual = true
+    _data.each((k string, v MalVal) => {
+      if !(k in oh.val) || !(v.equal(oh.val[k])) {
+        allEqual = false
+      }
+    })
+    return allEqual
+  }
+  def assoc(kv_list List<MalVal>) MalVal {
+    var new_data = _data.clone
+    for i = 0; i < kv_list.count; i += 2 {
+      new_data[kv_list[i].toHashKey] = kv_list[i + 1]
+    }
+    return MalHashMap.new(new_data)
+  }
+  def dissoc(keys List<MalVal>) MalVal {
+    var new_data = _data.clone
+    for key in keys {
+      new_data.remove(key.toHashKey)
+    }
+    return MalHashMap.new(new_data)
+  }
+  def get(key MalVal) MalVal { return _data.get(key.toHashKey, gNil) }
+  def contains(key MalVal) bool { return key.toHashKey in _data }
+  def keys List<MalVal> {
+    return _data.keys.map<MalVal>(k => MalVal.fromHashKey(k))
+  }
+  def vals List<MalVal> { return _data.values }
+  over clone MalVal { return MalHashMap.new(_data) }
+}
+
+namespace MalHashMap {
+  def fromList(kv_list List<MalVal>) MalHashMap {
+    var result StringMap<MalVal> = {}
+    for i = 0; i < kv_list.count; i += 2 {
+      result[kv_list[i].toHashKey] = kv_list[i + 1]
+    }
+    return MalHashMap.new(result)
+  }
+}
+
+class MalCallable : MalVal {
+  const func fn(List<MalVal>) MalVal
+  def call(args List<MalVal>) MalVal {
+    return func(args)
+  }
+}
+
+class MalNativeFunc : MalCallable {
+  over print(readable bool) string { return "#<NativeFunction>" }
+  over equal(o MalVal) bool { return false }
+  over clone MalVal { return MalNativeFunc.new(func) }
+}
+
+class MalFunc : MalCallable {
+  const ast MalVal
+  const params MalSequential
+  const env Env
+  var _macro bool = false
+  def new(aAst MalVal, aParams MalSequential, aEnv Env, aFunc fn(List<MalVal>) MalVal) {
+    super(aFunc)
+    ast = aAst
+    params = aParams
+    env = aEnv
+  }
+  def isMacro bool { return _macro }
+  def setAsMacro { _macro = true }
+  over print(readable bool) string { return "#<Function args=" + params.print(true) + ">" }
+  over equal(o MalVal) bool { return false }
+  over clone MalVal {
+    var f = MalFunc.new(ast, params, env, func)
+    if isMacro { f.setAsMacro }
+    return f
+  }
+}
+
+class MalAtom : MalVal {
+  var _data MalVal
+  over print(readable bool) string { return "(atom \(_data.print(readable)))" }
+  def val MalVal { return _data }
+  over equal(o MalVal) bool { return o is MalAtom && val.equal((o as MalAtom).val) }
+  def resetBang(newData MalVal) MalVal {
+    _data = newData
+    return _data
+  }
+  over clone MalVal { return MalAtom.new(_data) }
+}
diff --git a/skew/util.sk b/skew/util.sk
new file mode 100644 (file)
index 0000000..04fdfc1
--- /dev/null
@@ -0,0 +1,55 @@
+def argv List<string> {
+  return process.argv.slice(2)
+}
+
+def timeMs int {
+  return Date.new.getTime()
+}
+
+var fs = require("fs")
+
+def readFile(filename string) string {
+  return fs.readFileSync(filename, "utf-8")
+}
+
+def writeString(s string) {
+  fs.writeSync(1, s)
+}
+
+def printLn(s string) {
+  writeString(s)
+  writeString("\n")
+}
+
+def readLine(prompt string) string {
+  writeString(prompt)
+  var buffer = Buffer.new(1024) # in newer Node this should be Buffer.alloc
+  var stdin = fs.openSync("/dev/stdin", "rs")
+  var bytesread int
+  var anycharseen = false
+  var total = 0
+  while (bytesread = fs.readSync(stdin, buffer, total, 1)) > 0 {
+    anycharseen = true
+    var lastchar = buffer.slice(total, total + bytesread).toString()
+    if lastchar == "\n" {
+      break
+    }
+    total += bytesread
+  }
+  fs.closeSync(stdin)
+  return anycharseen ? buffer.slice(0, total).toString() : null
+}
+
+def stringToInt(str string) int {
+  return parseInt(str)
+}
+
+@import {
+  const process dynamic
+  const Buffer dynamic
+  const Date dynamic
+  const Error dynamic
+
+  def parseInt(str string) int
+  def require(name string) dynamic
+}