diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Makefile | 11 | ||||
| -rw-r--r-- | README.md | 99 | ||||
| -rw-r--r-- | clojure/project.clj | 2 | ||||
| -rw-r--r-- | clojure/src/stepA_mal.clj | 2 | ||||
| -rw-r--r-- | docs/TODO | 6 | ||||
| -rw-r--r-- | make/tests/stepA_mal.mal | 2 | ||||
| -rw-r--r-- | matlab/+types/Reader.m (renamed from matlab/Reader.m) | 0 | ||||
| -rw-r--r-- | matlab/Makefile | 2 | ||||
| -rw-r--r-- | matlab/reader.m | 2 | ||||
| -rw-r--r-- | miniMAL/package.json | 8 | ||||
| -rw-r--r-- | nim/Makefile | 33 | ||||
| -rw-r--r-- | nim/core.nim | 216 | ||||
| -rw-r--r-- | nim/env.nim | 25 | ||||
| -rw-r--r-- | nim/mal.nimble | 11 | ||||
| -rw-r--r-- | nim/nim.cfg | 2 | ||||
| -rw-r--r-- | nim/printer.nim | 27 | ||||
| -rw-r--r-- | nim/reader.nim | 108 | ||||
| -rw-r--r-- | nim/step0_repl.nim | 11 | ||||
| -rw-r--r-- | nim/step1_read_print.nim | 14 | ||||
| -rw-r--r-- | nim/step2_eval.nim | 52 | ||||
| -rw-r--r-- | nim/step3_env.nim | 70 | ||||
| -rw-r--r-- | nim/step4_if_fn_do.nim | 104 | ||||
| -rw-r--r-- | nim/step5_tco.nim | 115 | ||||
| -rw-r--r-- | nim/step6_file.nim | 125 | ||||
| -rw-r--r-- | nim/step7_quote.nim | 146 | ||||
| -rw-r--r-- | nim/step8_macros.nim | 171 | ||||
| -rw-r--r-- | nim/step9_try.nim | 190 | ||||
| -rw-r--r-- | nim/stepA_mal.nim | 191 | ||||
| -rw-r--r-- | nim/types.nim | 148 | ||||
| -rw-r--r-- | ocaml/core.ml | 5 | ||||
| -rw-r--r-- | process/guide.md | 45 | ||||
| -rwxr-xr-x | runtest.py | 54 |
33 files changed, 1942 insertions, 56 deletions
@@ -1,5 +1,6 @@ */experiments make/mal.mk +miniMAL/node_modules js/node_modules js/mal.js js/mal_web.js @@ -12,7 +12,7 @@ PYTHON = python IMPLS = bash c clojure coffee cs forth go haskell java js lua make mal \ ocaml matlab miniMAL perl php ps python r racket ruby rust \ - scala vb + scala vb nim step0 = step0_repl step1 = step1_read_print @@ -38,12 +38,13 @@ EXCLUDE_TESTS += test^racket^step5 # test completes EXCLUDE_TESTS += test^ruby^step5 # test completes, even at 100,000 EXCLUDE_TESTS += test^rust^step5 # no catching stack overflows EXCLUDE_TESTS += test^ocaml^step5 # test completes, even at 1,000,000 +EXCLUDE_TESTS += test^nim^step5 # test completes, even at 100,000 # interop tests now implemented yet EXCLUDE_TESTS += test^cs^stepA test^go^stepA test^haskell^stepA \ test^java^stepA test^mal^stepA test^mal^step0 \ test^php^stepA test^ps^stepA test^python^stepA \ - test^ruby^stepA + test^ruby^stepA test^rust^stepA test^vb^stepA EXCLUDE_PERFS = perf^mal # TODO: fix this @@ -79,6 +80,7 @@ ruby_STEP_TO_PROG = ruby/$($(1)).rb rust_STEP_TO_PROG = rust/target/$($(1)) scala_STEP_TO_PROG = scala/$($(1)).scala vb_STEP_TO_PROG = vb/$($(1)).exe +nim_STEP_TO_PROG = nim/$($(1)) # Needed some argument munging COMMA = , @@ -112,11 +114,12 @@ ruby_RUNSTEP = ruby ../$(2) $(3) rust_RUNSTEP = ../$(2) $(3) scala_RUNSTEP = sbt 'run-main $($(1))$(if $(3), $(3),)' vb_RUNSTEP = mono ../$(2) --raw $(3) +nim_RUNSTEP = ../$(2) $(3) # Extra options to pass to runtest.py -cs_TEST_OPTS = --redirect +cs_TEST_OPTS = --mono mal_TEST_OPTS = --start-timeout 60 --test-timeout 120 -vb_TEST_OPTS = --redirect +vb_TEST_OPTS = --mono # Derived lists @@ -4,7 +4,7 @@ Mal is a Clojure inspired Lisp interpreter. -Mal is implemented in 26 different languages: +Mal is implemented in 27 different languages: * Bash shell * C @@ -21,6 +21,7 @@ Mal is implemented in 26 different languages: * mal itself * MATLAB * [miniMAL](https://github.com/kanaka/miniMAL) +* Nim * OCaml * Perl * PHP @@ -34,10 +35,11 @@ Mal is implemented in 26 different languages: * Visual Basic.NET -Mal is a [learning tool](process/guide.md). Each implementation of mal is separated into 11 -incremental, self-contained (and testable) steps that demonstrate core -concepts of Lisp. The last step is capable of self-hosting (running -the mal implemenation of mal). +Mal is a learning tool. See the ([make-a-lisp process +guide](process/guide.md)). Each implementation of mal is separated +into 11 incremental, self-contained (and testable) steps that +demonstrate core concepts of Lisp. The last step is capable of +self-hosting (running the mal implemenation of mal). The mal (make a lisp) steps are: @@ -190,6 +192,19 @@ cd make make -f stepX_YYY.mk ``` +### Nim 0.10.3 + +Running the Nim implementation of mal requires Nim's current devel branch +(0.10.3) or later, and the nre library installed. + +``` +cd nim +make + # OR +nimble build +./stepX_YYY +``` + ### OCaml 4.01.0 ``` @@ -220,11 +235,11 @@ implemented in less than 1024 bytes of JavaScript. To run the miniMAL implementation of mal you need to download/install the miniMAL interpreter (which requires Node.js). ``` -# Download miniMAL itself -git clone https://github.com/kanaka/miniMAL ../miniMAL.git -export PATH=`pwd`/miniMAL.git:$PATH -# Now run mal implementated in miniMAL cd miniMAL +# Download miniMAL and dependencies +npm install +export PATH=`pwd`/node_modules/minimal-lisp/:$PATH +# Now run mal implementation in miniMAL miniMAL ./stepX_YYY ``` @@ -334,12 +349,14 @@ mono ./stepX_YYY.exe ## Running tests -The are nearly 500 generic Mal tests (for all implementations) in the -`tests/` directory. Each step has a corresponding test file containing -tests specific to that step. The `runtest.py` test harness uses -pexpect to launch a Mal step implementation and then feeds the tests -one at a time to the implementation and compares the output/return -value to the expected output/return value. +### Functional tests + +The are nearly 500 generic functional tests (for all implementations) +in the `tests/` directory. Each step has a corresponding test file +containing tests specific to that step. The `runtest.py` test harness +uses pexpect to launch a Mal step implementation and then feeds the +tests one at a time to the implementation and compares the +output/return value to the expected output/return value. To simplify the process of running tests, a top level Makefile is provided with convenient test targets. @@ -370,7 +387,7 @@ make test^step2 make test^step7 ``` -* To run a specifc step against a single implementation: +* To run tests for a specifc step against a single implementation: ``` make test^IMPL^stepX @@ -380,6 +397,56 @@ make test^ruby^step3 make test^ps^step4 ``` +### Self-hosted functional tests + +* To run the functional tests in self-hosted mode, you specify `mal` + as the test implementation and use the `MAL_IMPL` make variable + to change the underlying host language (default is JavaScript): +``` +make MAL_IMPL=IMPL test^mal^step2 + +# e.g. +make test^mal^step2 # js is default +make MAL_IMPL=ruby test^mal^step2 +make MAL_IMPL=python test^mal^step2 +``` + + +### Performance tests + +* To run performance tests against a single implementation: +``` +make perf^IMPL + +# e.g. +make perf^js +``` + +* To run performance tests against all implementations: +``` +make perf +``` + +### Generating language statistics + +* To report line and byte stastics for a single implementation: +``` +make stats^IMPL + +# e.g. +make stats^js +``` + +* To report line and bytes stastics for general Lisp code (env, core + and stepA): +``` +make stats-lisp^IMPL + +# e.g. +make stats-lisp^js +``` + + ## License Mal (make-a-lisp) is licensed under the MPL 2.0 (Mozilla Public diff --git a/clojure/project.clj b/clojure/project.clj index 3d7ff2e..447d643 100644 --- a/clojure/project.clj +++ b/clojure/project.clj @@ -19,7 +19,7 @@ :step7 {:main step7-quote} :step8 {:main step8-macros} :step9 {:main step9-try} - :stepA {:main stepA-interop}} + :stepA {:main stepA-mal}} :main stepA-more) diff --git a/clojure/src/stepA_mal.clj b/clojure/src/stepA_mal.clj index 6ed9964..ce289b6 100644 --- a/clojure/src/stepA_mal.clj +++ b/clojure/src/stepA_mal.clj @@ -1,4 +1,4 @@ -(ns stepA-interop +(ns stepA-mal (:refer-clojure :exclude [macroexpand]) (:require [clojure.repl] [readline] @@ -5,12 +5,12 @@ All: - test to make sure slurp captures final newline - make sure errors propagate/print properly when self-hosted - - change perf test to run for 10 seconds and then calculate number + * change perf test to run for 10 seconds and then calculate number of iterations per second - redefine (defmacro!) as (def! (macro*)) - runtest expect fixes: * stop using expect, so we can drop --raw option - - fix C#, VB + * fix C#, VB - fix long line splitting in runtest - regular expression matching in runtest - add re (use in rep) everywhere and use that (to avoid printing) @@ -85,7 +85,7 @@ Java: - MAL formatting is a bit off with mal/clojurewest2014.mal Javascript: - - interop: callbacks using Mal functions + - interop: adopt techniques from miniMAL - fix "user> " prompt with mal/clojurewest2014.mal Lua: diff --git a/make/tests/stepA_mal.mal b/make/tests/stepA_mal.mal index 9b1a2f9..768a929 100644 --- a/make/tests/stepA_mal.mal +++ b/make/tests/stepA_mal.mal @@ -14,6 +14,6 @@ (make* "$(foreach v,a b c,X$(v)Y)") ;=>"XaY XbY XcY" -(read-string (make* "($(foreach v,1 2 3,$(call gmsl_plus,1,$(v))))")) +(read-string (make* "($(foreach v,1 2 3,$(call int_add,1,$(v))))")) ;=>(2 3 4) diff --git a/matlab/Reader.m b/matlab/+types/Reader.m index c18ea54..c18ea54 100644 --- a/matlab/Reader.m +++ b/matlab/+types/Reader.m diff --git a/matlab/Makefile b/matlab/Makefile index f03d94d..eba8fa4 100644 --- a/matlab/Makefile +++ b/matlab/Makefile @@ -1,7 +1,7 @@ SOURCES_BASE = types.m types/Nil.m types/MalException.m \ types/Symbol.m types/List.m types/Vector.m \ types/HashMap.m types/Function.m types/Atom.m \ - Reader.m reader.m printer.m + types/Reader.m reader.m printer.m SOURCES_LISP = Env.m core.m stepA_mal.m SOURCES = $(SOURCES_BASE) $(SOURCES_LISP) diff --git a/matlab/reader.m b/matlab/reader.m index 84f6806..e0018ca 100644 --- a/matlab/reader.m +++ b/matlab/reader.m @@ -115,7 +115,7 @@ classdef reader function ast = read_str(str) %fprintf('in read_str\n'); tokens = reader.tokenize(str); - rdr = Reader(tokens); + rdr = types.Reader(tokens); ast = reader.read_form(rdr); end end diff --git a/miniMAL/package.json b/miniMAL/package.json new file mode 100644 index 0000000..24d7a03 --- /dev/null +++ b/miniMAL/package.json @@ -0,0 +1,8 @@ +{ + "name": "mal-miniMAL", + "version": "0.0.1", + "description": "Make a Lisp (mal) language implemented in miniMAL", + "dependencies": { + "minimal-lisp": "0.0.3" + } +} diff --git a/nim/Makefile b/nim/Makefile new file mode 100644 index 0000000..e8c450e --- /dev/null +++ b/nim/Makefile @@ -0,0 +1,33 @@ +##################### + +SOURCES_BASE = types.nim reader.nim printer.nim +SOURCES_LISP = env.nim core.nim stepA_mal.nim +SOURCES = $(SOURCES_BASE) $(SOURCES_LISP) + +##################### + +SRCS = step0_repl.nim step1_read_print.nim step2_eval.nim step3_env.nim \ + step4_if_fn_do.nim step5_tco.nim step6_file.nim step7_quote.nim \ + step8_macros.nim step9_try.nim stepA_mal.nim +BINS = $(SRCS:%.nim=%) + +##################### + +all: $(BINS) mal + +mal: $(word $(words $(BINS)),$(BINS)) + cp $< $@ + +$(BINS): %: %.nim + nim -d:release c $@ + +clean: + rm -rf nimcache/ $(BINS) + rm -f mal + +.PHONY: stats stats-lisp + +stats: $(SOURCES) + @wc $^ +stats-lisp: $(SOURCES_LISP) + @wc $^ diff --git a/nim/core.nim b/nim/core.nim new file mode 100644 index 0000000..e5ca517 --- /dev/null +++ b/nim/core.nim @@ -0,0 +1,216 @@ +import strutils, rdstdin, tables, algorithm, times, types, printer, reader + +type MalError* = object of Exception + t*: MalType + +# String functions +proc pr_str(xs: varargs[MalType]): MalType = + str(xs.map(proc(x: MalType): string = x.pr_str(true)).join(" ")) + +proc do_str(xs: varargs[MalType]): MalType = + str(xs.map(proc(x: MalType): string = x.pr_str(false)).join) + +proc prn(xs: varargs[MalType]): MalType = + echo xs.map(proc(x: MalType): string = x.pr_str(true)).join(" ") + result = nilObj + +proc println(xs: varargs[MalType]): MalType = + let line = xs.map(proc(x: MalType): string = x.pr_str(false)).join(" ") + echo line.replace("\\n", "\n") + result = nilObj + +proc read_str(xs: varargs[MalType]): MalType = + read_str(xs[0].str) + +proc readline(xs: varargs[MalType]): MalType = + str readLineFromStdin(xs[0].str) + +proc slurp(xs: varargs[MalType]): MalType = + str readFile(xs[0].str) + +proc cons(xs: varargs[MalType]): MalType = + result = list(xs[0]) + for x in xs[1].list: result.list.add x + +proc concat(xs: varargs[MalType]): MalType = + result = list() + for x in xs: + for i in x.list: + result.list.add i + +proc nth(xs: varargs[MalType]): MalType = + if xs[1].number < xs[0].list.len: return xs[0].list[xs[1].number] + else: raise newException(ValueError, "nth: index out of range") + +proc first(xs: varargs[MalType]): MalType = + if xs[0].kind in {List, Vector} and xs[0].list.len > 0: + xs[0].list[0] + else: nilObj + +proc rest(xs: varargs[MalType]): MalType = + if xs[0].kind in {List, Vector} and xs[0].list.len > 0: + list xs[0].list[1 .. -1] + else: list() + +proc throw(xs: varargs[MalType]): MalType = + raise (ref MalError)(t: list xs) + +proc assoc(xs: varargs[MalType]): MalType = + result = hash_map() + result.hash_map = xs[0].hash_map + for i in countup(1, xs.high, 2): + result.hash_map[xs[i].str] = xs[i+1] + +proc dissoc(xs: varargs[MalType]): MalType = + result = hash_map() + result.hash_map = xs[0].hash_map + for i in 1 .. xs.high: + if result.hash_map.hasKey(xs[i].str): result.hash_map.del(xs[i].str) + +proc get(xs: varargs[MalType]): MalType = + if xs[0].kind == HashMap: + result = xs[0].hash_map[xs[1].str] + if not result.isNil: return + + result = nilObj + +proc contains_q(xs: varargs[MalType]): MalType = + boolObj xs[0].hash_map.hasKey(xs[1].str) + +proc keys(xs: varargs[MalType]): MalType = + result = list() + for key in xs[0].hash_map.keys: + result.list.add str(key) + +proc vals(xs: varargs[MalType]): MalType = + result = list() + for value in xs[0].hash_map.values: + result.list.add value + +proc conj(xs: varargs[MalType]): MalType = + if xs[0].kind == List: + result = list() + for i in countdown(xs.high, 1): + result.list.add xs[i] + result.list.add xs[0].list + else: + result = vector() + result.list.add xs[0].list + for i in 1..xs.high: + result.list.add xs[i] + result.meta = xs[0].meta + +proc apply(xs: varargs[MalType]): MalType = + var s = newSeq[MalType]() + if xs.len > 2: + for j in 1 .. xs.high-1: + s.add xs[j] + s.add xs[xs.high].list + xs[0].getFun()(s) + +proc map(xs: varargs[MalType]): MalType = + result = list() + for i in 0 .. xs[1].list.high: + result.list.add xs[0].getFun()(xs[1].list[i]) + +proc with_meta(xs: varargs[MalType]): MalType = + new result + result[] = xs[0][] + result.meta = xs[1] + +proc meta(xs: varargs[MalType]): MalType = + if not xs[0].meta.isNil: xs[0].meta + else: nilObj + +proc deref(xs: varargs[MalType]): MalType = + xs[0].val + +proc reset_bang(xs: varargs[MalType]): MalType = + xs[0].val = xs[1] + result = xs[0].val + +proc swap_bang(xs: varargs[MalType]): MalType = + var args = @[xs[0].val] + for i in 2 .. xs.high: + args.add xs[i] + xs[0].val = xs[1].getFun()(args) + result = xs[0].val + +proc time_ms(xs: varargs[MalType]): MalType = + number int(epochTime() * 1000) + +template wrapNumberFun(op: expr): expr = + fun proc(xs: varargs[MalType]): MalType = + number op(xs[0].number, xs[1].number) + +template wrapBoolFun(op: expr): expr = + fun proc(xs: varargs[MalType]): MalType = + if op(xs[0].number, xs[1].number): trueObj else: falseObj + +let ns* = { + "+": wrapNumberFun(`+`), + "-": wrapNumberFun(`-`), + "*": wrapNumberFun(`*`), + "/": wrapNumberFun(`div`), + + "<": wrapBoolFun(`<`), + "<=": wrapBoolFun(`<=`), + ">": wrapBoolFun(`>`), + ">=": wrapBoolFun(`>=`), + + "list": fun list, + "list?": fun list_q, + "vector": fun vector, + "vector?": fun vector_q, + "hash-map": fun hash_map, + "map?": fun hash_map_q, + "empty?": fun empty_q, + "assoc": fun assoc, + "dissoc": fun dissoc, + "get": fun get, + "contains?": fun contains_q, + "keys": fun keys, + "vals": fun vals, + + "=": fun equal, + + "pr-str": fun pr_str, + "str": fun do_str, + "prn": fun prn, + "println": fun println, + + "read-string": fun read_str, + "readline": fun readline, + "slurp": fun slurp, + + "sequential?": fun seq_q, + "cons": fun cons, + "concat": fun concat, + "count": fun count, + "nth": fun nth, + "first": fun first, + "rest": fun rest, + "conj": fun conj, + "apply": fun apply, + "map": fun map, + + "throw": fun throw, + + "nil?": fun nil_q, + "true?": fun true_q, + "false?": fun false_q, + "symbol": fun symbol, + "symbol?": fun symbol_q, + "keyword": fun keyword, + "keyword?": fun keyword_q, + + "with-meta": fun with_meta, + "meta": fun meta, + "atom": fun atom, + "atom?": fun atom_q, + "deref": fun deref, + "reset!": fun reset_bang, + "swap!": fun swap_bang, + + "time-ms": fun time_ms, +} diff --git a/nim/env.nim b/nim/env.nim new file mode 100644 index 0000000..7432d09 --- /dev/null +++ b/nim/env.nim @@ -0,0 +1,25 @@ +import tables, types + +proc initEnv*(outer: Env = nil, binds, exprs: MalType = nilObj): Env = + result = Env(data: initTable[string, MalType](), outer: outer) + + if binds.kind in {List, Vector}: + for i, e in binds.list: + if e.str == "&": + result.data[binds.list[i+1].str] = list(exprs.list[i .. -1]) + break + else: + result.data[e.str] = exprs.list[i] + +proc set*(e: var Env, key: string, value: MalType): MalType {.discardable.} = + e.data[key] = value + value + +proc find*(e: Env, key: string): Env = + if e.data.hasKey(key): return e + if e.outer != nil: return e.outer.find(key) + +proc get*(e: Env, key: string): MalType = + let env = e.find(key) + if env == nil: raise newException(ValueError, "'" & key & "' not found") + env.data[key] diff --git a/nim/mal.nimble b/nim/mal.nimble new file mode 100644 index 0000000..5ec4b7f --- /dev/null +++ b/nim/mal.nimble @@ -0,0 +1,11 @@ +[Package] +name = "mal" +version = "1.0" +author = "Dennis Felsing" +description = "Mal code in Nim" +license = "MIT" + +bin = "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" + +[Deps] +Requires = "nim >= 0.10.3, nre >= 0.6.0" diff --git a/nim/nim.cfg b/nim/nim.cfg new file mode 100644 index 0000000..dbf197a --- /dev/null +++ b/nim/nim.cfg @@ -0,0 +1,2 @@ +deadCodeElim: off +gc: markandsweep diff --git a/nim/printer.nim b/nim/printer.nim new file mode 100644 index 0000000..912b8a9 --- /dev/null +++ b/nim/printer.nim @@ -0,0 +1,27 @@ +import strutils, sequtils, tables, types + +proc str_handle(x: string, pr = true): string = + if x.len > 0 and x[0] == '\xff': + result = ":" & x[1 .. x.high] + elif pr: result = "\"" & x.replace("\\", "\\\\").replace("\"", "\\\"") & "\"" + else: result = x + +proc pr_str*(m: MalType, pr = true): string = + case m.kind + of Nil: result = "nil" + of True: result = "true" + of False: result = "false" + of Fun: result = "#<function>" + of MalFun: result = "#<malfun>" + of Atom: result = "(atom " & m.val.pr_str & ")" + of Symbol: result = m.str + of String: result = m.str.str_handle(pr) + of Number: result = $m.number + of List: result = "(" & m.list.mapIt(string, it.pr_str(pr)).join(" ") & ")" + of Vector: result = "[" & m.list.mapIt(string, it.pr_str(pr)).join(" ") & "]" + of HashMap: + result = "{" + for key, val in m.hash_map.pairs: + if result.len > 1: result.add " " + result.add key.str_handle & " " & val.pr_str(pr) + result.add "}" diff --git a/nim/reader.nim b/nim/reader.nim new file mode 100644 index 0000000..10fce10 --- /dev/null +++ b/nim/reader.nim @@ -0,0 +1,108 @@ +import nre, optional_t, strutils, types + +let + tokenRE = re"""[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)""" + intRE = re"-?[0-9]+$" + +type + Blank* = object of Exception + + Reader = object + tokens: seq[string] + position: int + +proc next(r: var Reader): string = + if r.position >= r.tokens.len: + result = nil + else: + result = r.tokens[r.position] + inc r.position + +proc peek(r: Reader): string = + if r.position >= r.tokens.len: nil + else: r.tokens[r.position] + +proc tokenize(str: string): seq[string] = + result = @[] + for match in str.findIter(tokenRE): + if match.captures[0][0] != ';': + result.add match.captures[0] + +proc read_form(r: var Reader): MalType + +proc read_seq(r: var Reader, fr, to: string): seq[MalType] = + result = @[] + var t = r.next + if t != fr: raise newException(ValueError, "expected '" & fr & "'") + + t = r.peek + while t != to: + if t == nil: raise newException(ValueError, "expected '" & to & "', got EOF") + result.add r.read_form + t = r.peek + discard r.next + +proc read_list(r: var Reader): MalType = + result = list r.read_seq("(", ")") + +proc read_vector(r: var Reader): MalType = + result = vector r.read_seq("[", "]") + +proc read_hash_map(r: var Reader): MalType = + result = hash_map r.read_seq("{", "}") + +proc read_atom(r: var Reader): MalType = + let t = r.next + if t.match(intRE): number t.parseInt + elif t[0] == '"': str t[1 .. <t.high].replace("\\\"", "\"") + elif t[0] == ':': keyword t[1 .. t.high] + elif t == "nil": nilObj + elif t == "true": trueObj + elif t == "false": falseObj + else: symbol t + +proc read_form(r: var Reader): MalType = + if r.peek[0] == ';': + discard r.next + return nilObj + case r.peek + of "'": + discard r.next + result = list(symbol "quote", r.read_form) + of "`": + discard r.next + result = list(symbol "quasiquote", r.read_form) + of "~": + discard r.next + result = list(symbol "unquote", r.read_form) + of "~@": + discard r.next + result = list(symbol "splice-unquote", r.read_form) + of "^": + discard r.next + let meta = r.read_form + result = list(symbol "with-meta", r.read_form, meta) + of "@": + discard r.next + result = list(symbol "deref", r.read_form) + + # list + of "(": result = r.read_list + of ")": raise newException(ValueError, "unexpected ')'") + + # vector + of "[": result = r.read_vector + of "]": raise newException(ValueError, "unexpected ']'") + + # hash-map + of "{": result = r.read_hash_map + of "}": raise newException(ValueError, "unexpected '}'") + + # atom + else: result = r.read_atom + +proc read_str*(str: string): MalType = + var r = Reader(tokens: str.tokenize) + if r.tokens.len == 0: + raise newException(Blank, "Blank line") + r.read_form diff --git a/nim/step0_repl.nim b/nim/step0_repl.nim new file mode 100644 index 0000000..6ae7d89 --- /dev/null +++ b/nim/step0_repl.nim @@ -0,0 +1,11 @@ +import rdstdin + +proc read(str: string): string = str + +proc eval(ast: string): string = ast + +proc print(exp: string): string = exp + +while true: + let line = readLineFromStdin("user> ") + echo line.read.eval.print diff --git a/nim/step1_read_print.nim b/nim/step1_read_print.nim new file mode 100644 index 0000000..4be5898 --- /dev/null +++ b/nim/step1_read_print.nim @@ -0,0 +1,14 @@ +import rdstdin, types, reader, printer + +proc read(str: string): MalType = str.read_str + +proc eval(ast: MalType): MalType = ast + +proc print(exp: MalType): string = exp.pr_str + +while true: + try: + let line = readLineFromStdin("user> ") + echo line.read.eval.print + except: + echo getCurrentExceptionMsg() diff --git a/nim/step2_eval.nim b/nim/step2_eval.nim new file mode 100644 index 0000000..075b4ac --- /dev/null +++ b/nim/step2_eval.nim @@ -0,0 +1,52 @@ +import rdstdin, tables, sequtils, types, reader, printer + +proc read(str: string): MalType = str.read_str + +proc eval(ast: MalType, env: Table[string, MalType]): MalType + +proc eval_ast(ast: MalType, env: Table[string, MalType]): MalType = + case ast.kind + of Symbol: + if not env.hasKey(ast.str): + raise newException(ValueError, "'" & ast.str & "' not found") + result = env[ast.str] + of List: + result = list ast.list.mapIt(MalType, it.eval(env)) + of Vector: + result = vector ast.list.mapIt(MalType, it.eval(env)) + of HashMap: + result = hash_map() + for k, v in ast.hash_map.pairs: + result.hash_map[k] = v.eval(env) + else: + result = ast + +proc eval(ast: MalType, env: Table[string, MalType]): MalType = + case ast.kind + of List: + let el = ast.eval_ast(env) + el.list[0].fun(el.list[1 .. -1]) + else: + ast.eval_ast(env) + +proc print(exp: MalType): string = exp.pr_str + +template wrapNumberFun(op: expr): expr = + fun proc(xs: varargs[MalType]): MalType = number op(xs[0].number, xs[1].number) + +let repl_env = toTable({ + "+": wrapNumberFun `+`, + "-": wrapNumberFun `-`, + "*": wrapNumberFun `*`, + "/": wrapNumberFun `div`, +}) + +proc rep(str: string): string = + str.read.eval(repl_env).print + +while true: + try: + let line = readLineFromStdin("user> ") + echo line.rep + except: + echo getCurrentExceptionMsg() diff --git a/nim/step3_env.nim b/nim/step3_env.nim new file mode 100644 index 0000000..98438fe --- /dev/null +++ b/nim/step3_env.nim @@ -0,0 +1,70 @@ +import rdstdin, tables, sequtils, types, reader, printer, env + +proc read(str: string): MalType = str.read_str + +proc eval(ast: MalType, env: var Env): MalType + +proc eval_ast(ast: MalType, env: var Env): MalType = + case ast.kind + of Symbol: + result = env.get(ast.str) + of List: + result = list ast.list.mapIt(MalType, it.eval(env)) + of Vector: + result = vector ast.list.mapIt(MalType, it.eval(env)) + of HashMap: + result = hash_map() + for k, v in ast.hash_map.pairs: + result.hash_map[k] = v.eval(env) + else: + result = ast + +proc eval(ast: MalType, env: var Env): MalType = + case ast.kind + of List: + let + a0 = ast.list[0] + a1 = ast.list[1] + a2 = ast.list[2] + + case a0.str + of "def!": + result = env.set(a1.str, a2.eval(env)) + of "let*": + var letEnv: Env + letEnv.deepCopy(env) + case a1.kind + of List, Vector: + for i in countup(0, a1.list.high, 2): + letEnv.set(a1.list[i].str, a1.list[i+1].eval(letEnv)) + else: discard + result = a2.eval(letEnv) + else: + let el = ast.eval_ast(env) + result = el.list[0].fun(el.list[1 .. -1]) + else: + result = ast.eval_ast(env) + +proc print(exp: MalType): string = exp.pr_str + +template wrapNumberFun(op: expr): expr = + fun proc(xs: varargs[MalType]): MalType = number op(xs[0].number, xs[1].number) + +var repl_env = initEnv() + +repl_env.set("+", wrapNumberFun(`+`)) +repl_env.set("-", wrapNumberFun(`-`)) +repl_env.set("*", wrapNumberFun(`*`)) +repl_env.set("/", wrapNumberFun(`div`)) +#repl_env.set("/", wrapNumberFun(proc(x,y: int): int = int(x.float / y.float))) + +proc rep(str: string): string = + str.read.eval(repl_env).print + +while true: + try: + let line = readLineFromStdin("user> ") + echo line.rep + except: + echo getCurrentExceptionMsg() + echo getCurrentException().getStackTrace() diff --git a/nim/step4_if_fn_do.nim b/nim/step4_if_fn_do.nim new file mode 100644 index 0000000..a4a8dd1 --- /dev/null +++ b/nim/step4_if_fn_do.nim @@ -0,0 +1,104 @@ +import rdstdin, tables, sequtils, types, reader, printer, env, core + +proc read(str: string): MalType = str.read_str + +proc eval(ast: MalType, env: var Env): MalType + +proc eval_ast(ast: MalType, env: var Env): MalType = + case ast.kind + of Symbol: + result = env.get(ast.str) + of List: + result = list ast.list.mapIt(MalType, it.eval(env)) + of Vector: + result = vector ast.list.mapIt(MalType, it.eval(env)) + of HashMap: + result = hash_map() + for k, v in ast.hash_map.pairs: + result.hash_map[k] = v.eval(env) + else: + result = ast + +proc eval(ast: MalType, env: var Env): MalType = + case ast.kind + of List: + let a0 = ast.list[0] + case a0.kind + of Symbol: + case a0.str + of "def!": + let + a1 = ast.list[1] + a2 = ast.list[2] + result = env.set(a1.str, a2.eval(env)) + + of "let*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var letEnv: Env + letEnv.deepCopy(env) + + case a1.kind + of List, Vector: + for i in countup(0, a1.list.high, 2): + letEnv.set(a1.list[i].str, a1.list[i+1].eval(letEnv)) + else: discard + result = a2.eval(letEnv) + + of "do": + let el = (list ast.list[1 .. -1]).eval_ast(env) + result = el.list[el.list.high] + + of "if": + let + a1 = ast.list[1] + a2 = ast.list[2] + cond = a1.eval(env) + + if cond.kind in {Nil, False}: + if ast.list.len > 3: result = ast.list[3].eval(env) + else: result = nilObj + else: result = a2.eval(env) + + of "fn*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var env2 = env + result = fun(proc(a: varargs[MalType]): MalType = + var newEnv = initEnv(env2, a1, list(a)) + a2.eval(newEnv)) + + else: + let el = ast.eval_ast(env) + result = el.list[0].fun(el.list[1 .. -1]) + + else: + let el = ast.eval_ast(env) + result = el.list[0].fun(el.list[1 .. -1]) + + else: + result = ast.eval_ast(env) + +proc print(exp: MalType): string = exp.pr_str + +var repl_env = initEnv() + +for k, v in ns.items: + repl_env.set(k, v) + +# core.nim: defined using nim +proc rep(str: string): string = + str.read.eval(repl_env).print + +# core.mal: defined using mal itself +discard rep "(def! not (fn* (a) (if a false true)))" + +while true: + try: + let line = readLineFromStdin("user> ") + echo line.rep + except: + echo getCurrentExceptionMsg() + echo getCurrentException().getStackTrace() diff --git a/nim/step5_tco.nim b/nim/step5_tco.nim new file mode 100644 index 0000000..bbbb160 --- /dev/null +++ b/nim/step5_tco.nim @@ -0,0 +1,115 @@ +import rdstdin, tables, sequtils, types, reader, printer, env, core + +proc read(str: string): MalType = str.read_str + +proc eval(ast: MalType, env: var Env): MalType + +proc eval_ast(ast: MalType, env: var Env): MalType = + case ast.kind + of Symbol: + result = env.get(ast.str) + of List: + result = list ast.list.mapIt(MalType, it.eval(env)) + of Vector: + result = vector ast.list.mapIt(MalType, it.eval(env)) + of HashMap: + result = hash_map() + for k, v in ast.hash_map.pairs: + result.hash_map[k] = v.eval(env) + else: + result = ast + +proc eval(ast: MalType, env: var Env): MalType = + var ast = ast + + template defaultApply = + let el = ast.eval_ast(env) + let f = el.list[0] + case f.kind + of MalFun: + ast = f.malfun.ast + env = initEnv(f.malfun.env, f.malfun.params, list(el.list[1 .. -1])) + else: + return f.fun(el.list[1 .. -1]) + + while true: + if ast.kind != List: return ast.eval_ast(env) + + let a0 = ast.list[0] + case a0.kind + of Symbol: + case a0.str + of "def!": + let + a1 = ast.list[1] + a2 = ast.list[2] + return env.set(a1.str, a2.eval(env)) + + of "let*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var let_env = Env(env) + case a1.kind + of List, Vector: + for i in countup(0, a1.list.high, 2): + let_env.set(a1.list[i].str, a1.list[i+1].eval(let_env)) + else: raise newException(ValueError, "Illegal kind in let*") + ast = a2 + env = let_env + # Continue loop (TCO) + + of "do": + let last = ast.list.high + let el = (list ast.list[1 .. <last]).eval_ast(env) + ast = ast.list[last] + # Continue loop (TCO) + + of "if": + let + a1 = ast.list[1] + a2 = ast.list[2] + cond = a1.eval(env) + + if cond.kind in {Nil, False}: + if ast.list.len > 3: ast = ast.list[3] + else: ast = nilObj + else: ast = a2 + + of "fn*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var env2 = env + let fn = proc(a: varargs[MalType]): MalType = + var newEnv = initEnv(env2, a1, list(a)) + a2.eval(newEnv) + return malfun(fn, a2, a1, env) + + else: + defaultApply() + + else: + defaultApply() + +proc print(exp: MalType): string = exp.pr_str + +var repl_env = initEnv() + +for k, v in ns.items: + repl_env.set(k, v) + +# core.nim: defined using nim +proc rep(str: string): string = + str.read.eval(repl_env).print + +# core.mal: defined using mal itself +discard rep "(def! not (fn* (a) (if a false true)))" + +while true: + try: + let line = readLineFromStdin("user> ") + echo line.rep + except: + echo getCurrentExceptionMsg() + echo getCurrentException().getStackTrace() diff --git a/nim/step6_file.nim b/nim/step6_file.nim new file mode 100644 index 0000000..a0a569e --- /dev/null +++ b/nim/step6_file.nim @@ -0,0 +1,125 @@ +import rdstdin, tables, sequtils, os, types, reader, printer, env, core + +proc read(str: string): MalType = str.read_str + +proc eval(ast: MalType, env: var Env): MalType + +proc eval_ast(ast: MalType, env: var Env): MalType = + case ast.kind + of Symbol: + result = env.get(ast.str) + of List: + result = list ast.list.mapIt(MalType, it.eval(env)) + of Vector: + result = vector ast.list.mapIt(MalType, it.eval(env)) + of HashMap: + result = hash_map() + for k, v in ast.hash_map.pairs: + result.hash_map[k] = v.eval(env) + else: + result = ast + +proc eval(ast: MalType, env: var Env): MalType = + var ast = ast + + template defaultApply = + let el = ast.eval_ast(env) + let f = el.list[0] + case f.kind + of MalFun: + ast = f.malfun.ast + env = initEnv(f.malfun.env, f.malfun.params, list(el.list[1 .. -1])) + else: + return f.fun(el.list[1 .. -1]) + + while true: + if ast.kind != List: return ast.eval_ast(env) + + let a0 = ast.list[0] + case a0.kind + of Symbol: + case a0.str + of "def!": + let + a1 = ast.list[1] + a2 = ast.list[2] + return env.set(a1.str, a2.eval(env)) + + of "let*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var let_env = Env(env) + case a1.kind + of List, Vector: + for i in countup(0, a1.list.high, 2): + let_env.set(a1.list[i].str, a1.list[i+1].eval(let_env)) + else: raise newException(ValueError, "Illegal kind in let*") + ast = a2 + env = let_env + # Continue loop (TCO) + + of "do": + let last = ast.list.high + let el = (list ast.list[1 .. <last]).eval_ast(env) + ast = ast.list[last] + # Continue loop (TCO) + + of "if": + let + a1 = ast.list[1] + a2 = ast.list[2] + cond = a1.eval(env) + + if cond.kind in {Nil, False}: + if ast.list.len > 3: ast = ast.list[3] + else: ast = nilObj + else: ast = a2 + + of "fn*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var env2 = env + let fn = proc(a: varargs[MalType]): MalType = + var newEnv = initEnv(env2, a1, list(a)) + a2.eval(newEnv) + return malfun(fn, a2, a1, env) + + else: + defaultApply() + + else: + defaultApply() + +proc print(exp: MalType): string = exp.pr_str + +var repl_env = initEnv() + +for k, v in ns.items: + repl_env.set(k, v) +repl_env.set("eval", fun(proc(xs: varargs[MalType]): MalType = eval(xs[0], repl_env))) +var ps = commandLineParams() +repl_env.set("*ARGV*", list((if paramCount() > 1: ps[1..ps.high] else: @[]).map(str))) + + +# core.nim: defined using nim +proc rep(str: string): string {.discardable.} = + str.read.eval(repl_env).print + +# core.mal: defined using mal itself +rep "(def! not (fn* (a) (if a false true)))" +rep "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))" + +if paramCount() >= 1: + rep "(load-file \"" & paramStr(1) & "\")" + quit() + +while true: + try: + let line = readLineFromStdin("user> ") + echo line.rep + except Blank: discard + except: + echo getCurrentExceptionMsg() + echo getCurrentException().getStackTrace() diff --git a/nim/step7_quote.nim b/nim/step7_quote.nim new file mode 100644 index 0000000..848913d --- /dev/null +++ b/nim/step7_quote.nim @@ -0,0 +1,146 @@ +import rdstdin, tables, sequtils, os, types, reader, printer, env, core + +proc read(str: string): MalType = str.read_str + +proc is_pair(x: MalType): bool = + x.kind in {List, Vector} and x.list.len > 0 + +proc quasiquote(ast: MalType): MalType = + if not ast.is_pair: + return list(symbol "quote", ast) + elif ast.list[0] == symbol "unquote": + return ast.list[1] + elif ast.list[0].is_pair and ast.list[0].list[0] == symbol "splice-unquote": + return list(symbol "concat", ast.list[0].list[1], + quasiquote(list ast.list[1 .. -1])) + else: + return list(symbol "cons", quasiquote(ast.list[0]), quasiquote(list(ast.list[1 .. -1]))) + +proc eval(ast: MalType, env: var Env): MalType + +proc eval_ast(ast: MalType, env: var Env): MalType = + case ast.kind + of Symbol: + result = env.get(ast.str) + of List: + result = list ast.list.mapIt(MalType, it.eval(env)) + of Vector: + result = vector ast.list.mapIt(MalType, it.eval(env)) + of HashMap: + result = hash_map() + for k, v in ast.hash_map.pairs: + result.hash_map[k] = v.eval(env) + else: + result = ast + +proc eval(ast: MalType, env: var Env): MalType = + var ast = ast + + template defaultApply = + let el = ast.eval_ast(env) + let f = el.list[0] + case f.kind + of MalFun: + ast = f.malfun.ast + env = initEnv(f.malfun.env, f.malfun.params, list(el.list[1 .. -1])) + else: + return f.fun(el.list[1 .. -1]) + + while true: + if ast.kind != List: return ast.eval_ast(env) + + let a0 = ast.list[0] + case a0.kind + of Symbol: + case a0.str + of "def!": + let + a1 = ast.list[1] + a2 = ast.list[2] + return env.set(a1.str, a2.eval(env)) + + of "let*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var let_env = Env(env) + case a1.kind + of List, Vector: + for i in countup(0, a1.list.high, 2): + let_env.set(a1.list[i].str, a1.list[i+1].eval(let_env)) + else: raise newException(ValueError, "Illegal kind in let*") + ast = a2 + env = let_env + # Continue loop (TCO) + + of "quote": + return ast.list[1] + + of "quasiquote": + ast = ast.list[1].quasiquote + # Continue loop (TCO) + + of "do": + let last = ast.list.high + let el = (list ast.list[1 .. <last]).eval_ast(env) + ast = ast.list[last] + # Continue loop (TCO) + + of "if": + let + a1 = ast.list[1] + a2 = ast.list[2] + cond = a1.eval(env) + + if cond.kind in {Nil, False}: + if ast.list.len > 3: ast = ast.list[3] + else: ast = nilObj + else: ast = a2 + + of "fn*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var env2 = env + let fn = proc(a: varargs[MalType]): MalType = + var newEnv = initEnv(env2, a1, list(a)) + a2.eval(newEnv) + return malfun(fn, a2, a1, env) + + else: + defaultApply() + + else: + defaultApply() + +proc print(exp: MalType): string = exp.pr_str + +var repl_env = initEnv() + +for k, v in ns.items: + repl_env.set(k, v) +repl_env.set("eval", fun(proc(xs: varargs[MalType]): MalType = eval(xs[0], repl_env))) +var ps = commandLineParams() +repl_env.set("*ARGV*", list((if paramCount() > 1: ps[1..ps.high] else: @[]).map(str))) + + +# core.nim: defined using nim +proc rep(str: string): string {.discardable.} = + str.read.eval(repl_env).print + +# core.mal: defined using mal itself +rep "(def! not (fn* (a) (if a false true)))" +rep "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))" + +if paramCount() >= 1: + rep "(load-file \"" & paramStr(1) & "\")" + quit() + +while true: + try: + let line = readLineFromStdin("user> ") + echo line.rep + except Blank: discard + except: + echo getCurrentExceptionMsg() + echo getCurrentException().getStackTrace() diff --git a/nim/step8_macros.nim b/nim/step8_macros.nim new file mode 100644 index 0000000..96981a7 --- /dev/null +++ b/nim/step8_macros.nim @@ -0,0 +1,171 @@ +import rdstdin, tables, sequtils, os, types, reader, printer, env, core + +proc read(str: string): MalType = str.read_str + +proc is_pair(x: MalType): bool = + x.kind in {List, Vector} and x.list.len > 0 + +proc quasiquote(ast: MalType): MalType = + if not ast.is_pair: + return list(symbol "quote", ast) + elif ast.list[0] == symbol "unquote": + return ast.list[1] + elif ast.list[0].is_pair and ast.list[0].list[0] == symbol "splice-unquote": + return list(symbol "concat", ast.list[0].list[1], + quasiquote(list ast.list[1 .. -1])) + else: + return list(symbol "cons", quasiquote(ast.list[0]), quasiquote(list(ast.list[1 .. -1]))) + +proc is_macro_call(ast: MalType, env: Env): bool = + ast.kind == List and ast.list[0].kind == Symbol and + env.find(ast.list[0].str) != nil and env.get(ast.list[0].str).macro_q + +proc macroexpand(ast: MalType, env: Env): MalType = + result = ast + while result.is_macro_call(env): + let mac = env.get(result.list[0].str) + result = mac.malfun.fn(result.list[1 .. -1]).macroexpand(env) + +proc eval(ast: MalType, env: Env): MalType + +proc eval_ast(ast: MalType, env: var Env): MalType = + case ast.kind + of Symbol: + result = env.get(ast.str) + of List: + result = list ast.list.mapIt(MalType, it.eval(env)) + of Vector: + result = vector ast.list.mapIt(MalType, it.eval(env)) + of HashMap: + result = hash_map() + for k, v in ast.hash_map.pairs: + result.hash_map[k] = v.eval(env) + else: + result = ast + +proc eval(ast: MalType, env: Env): MalType = + var ast = ast + var env = env + + template defaultApply = + let el = ast.eval_ast(env) + let f = el.list[0] + case f.kind + of MalFun: + ast = f.malfun.ast + env = initEnv(f.malfun.env, f.malfun.params, list(el.list[1 .. -1])) + else: + return f.fun(el.list[1 .. -1]) + + while true: + if ast.kind != List: return ast.eval_ast(env) + + ast = ast.macroexpand(env) + if ast.kind != List: return ast + if ast.list.len == 0: return ast + + let a0 = ast.list[0] + case a0.kind + of Symbol: + case a0.str + of "def!": + let + a1 = ast.list[1] + a2 = ast.list[2] + return env.set(a1.str, a2.eval(env)) + + of "let*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var let_env = Env(env) + case a1.kind + of List, Vector: + for i in countup(0, a1.list.high, 2): + let_env.set(a1.list[i].str, a1.list[i+1].eval(let_env)) + else: raise newException(ValueError, "Illegal kind in let*") + ast = a2 + env = let_env + # Continue loop (TCO) + + of "quote": + return ast.list[1] + + of "quasiquote": + ast = ast.list[1].quasiquote + # Continue loop (TCO) + + of "defmacro!": + var fun = ast.list[2].eval(env) + fun.malfun.is_macro = true + return env.set(ast.list[1].str, fun) + + of "macroexpand": + return ast.list[1].macroexpand(env) + + of "do": + let last = ast.list.high + let el = (list ast.list[1 .. <last]).eval_ast(env) + ast = ast.list[last] + # Continue loop (TCO) + + of "if": + let + a1 = ast.list[1] + a2 = ast.list[2] + cond = a1.eval(env) + + if cond.kind in {Nil, False}: + if ast.list.len > 3: ast = ast.list[3] + else: ast = nilObj + else: ast = a2 + + of "fn*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var env2 = env + let fn = proc(a: varargs[MalType]): MalType = + var newEnv = initEnv(env2, a1, list(a)) + a2.eval(newEnv) + return malfun(fn, a2, a1, env) + + else: + defaultApply() + + else: + defaultApply() + +proc print(exp: MalType): string = exp.pr_str + +var repl_env = initEnv() + +for k, v in ns.items: + repl_env.set(k, v) +repl_env.set("eval", fun(proc(xs: varargs[MalType]): MalType = eval(xs[0], repl_env))) +var ps = commandLineParams() +repl_env.set("*ARGV*", list((if paramCount() > 1: ps[1..ps.high] else: @[]).map(str))) + + +# core.nim: defined using nim +proc rep(str: string): string {.discardable.} = + str.read.eval(repl_env).print + +# core.mal: defined using mal itself +rep "(def! not (fn* (a) (if a false true)))" +rep "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))" +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)))))))" +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))))))))" + +if paramCount() >= 1: + rep "(load-file \"" & paramStr(1) & "\")" + quit() + +while true: + try: + let line = readLineFromStdin("user> ") + echo line.rep + except Blank: discard + except: + echo getCurrentExceptionMsg() + echo getCurrentException().getStackTrace() diff --git a/nim/step9_try.nim b/nim/step9_try.nim new file mode 100644 index 0000000..6d77ac7 --- /dev/null +++ b/nim/step9_try.nim @@ -0,0 +1,190 @@ +import rdstdin, tables, sequtils, os, types, reader, printer, env, core + +proc read(str: string): MalType = str.read_str + +proc is_pair(x: MalType): bool = + x.kind in {List, Vector} and x.list.len > 0 + +proc quasiquote(ast: MalType): MalType = + if not ast.is_pair: + return list(symbol "quote", ast) + elif ast.list[0] == symbol "unquote": + return ast.list[1] + elif ast.list[0].is_pair and ast.list[0].list[0] == symbol "splice-unquote": + return list(symbol "concat", ast.list[0].list[1], + quasiquote(list ast.list[1 .. -1])) + else: + return list(symbol "cons", quasiquote(ast.list[0]), quasiquote(list(ast.list[1 .. -1]))) + +proc is_macro_call(ast: MalType, env: Env): bool = + ast.kind == List and ast.list[0].kind == Symbol and + env.find(ast.list[0].str) != nil and env.get(ast.list[0].str).macro_q + +proc macroexpand(ast: MalType, env: Env): MalType = + result = ast + while result.is_macro_call(env): + let mac = env.get(result.list[0].str) + result = mac.malfun.fn(result.list[1 .. -1]).macroexpand(env) + +proc eval(ast: MalType, env: Env): MalType + +proc eval_ast(ast: MalType, env: var Env): MalType = + case ast.kind + of Symbol: + result = env.get(ast.str) + of List: + result = list ast.list.mapIt(MalType, it.eval(env)) + of Vector: + result = vector ast.list.mapIt(MalType, it.eval(env)) + of HashMap: + result = hash_map() + for k, v in ast.hash_map.pairs: + result.hash_map[k] = v.eval(env) + else: + result = ast + +proc eval(ast: MalType, env: Env): MalType = + var ast = ast + var env = env + + template defaultApply = + let el = ast.eval_ast(env) + let f = el.list[0] + case f.kind + of MalFun: + ast = f.malfun.ast + env = initEnv(f.malfun.env, f.malfun.params, list(el.list[1 .. -1])) + else: + return f.fun(el.list[1 .. -1]) + + while true: + if ast.kind != List: return ast.eval_ast(env) + + ast = ast.macroexpand(env) + if ast.kind != List: return ast + if ast.list.len == 0: return ast + + let a0 = ast.list[0] + case a0.kind + of Symbol: + case a0.str + of "def!": + let + a1 = ast.list[1] + a2 = ast.list[2] + res = a2.eval(env) + return env.set(a1.str, res) + + of "let*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var let_env = Env(env) + case a1.kind + of List, Vector: + for i in countup(0, a1.list.high, 2): + let_env.set(a1.list[i].str, a1.list[i+1].eval(let_env)) + else: raise newException(ValueError, "Illegal kind in let*") + ast = a2 + env = let_env + # Continue loop (TCO) + + of "quote": + return ast.list[1] + + of "quasiquote": + ast = ast.list[1].quasiquote + # Continue loop (TCO) + + of "defmacro!": + var fun = ast.list[2].eval(env) + fun.malfun.is_macro = true + return env.set(ast.list[1].str, fun) + + of "macroexpand": + return ast.list[1].macroexpand(env) + + of "try*": + let + a1 = ast.list[1] + a2 = ast.list[2] + if a2.list[0].str == "catch*": + try: + return a1.eval(env) + except MalError: + let exc = (ref MalError) getCurrentException() + var catchEnv = initEnv(env, list a2.list[1], exc.t) + return a2.list[2].eval(catchEnv) + except: + let exc = getCurrentExceptionMsg() + var catchEnv = initEnv(env, list a2.list[1], list str(exc)) + return a2.list[2].eval(catchEnv) + else: + return a1.eval(env) + + of "do": + let last = ast.list.high + let el = (list ast.list[1 .. <last]).eval_ast(env) + ast = ast.list[last] + # Continue loop (TCO) + + of "if": + let + a1 = ast.list[1] + a2 = ast.list[2] + cond = a1.eval(env) + + if cond.kind in {Nil, False}: + if ast.list.len > 3: ast = ast.list[3] + else: ast = nilObj + else: ast = a2 + + of "fn*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var env2 = env + let fn = proc(a: varargs[MalType]): MalType = + var newEnv = initEnv(env2, a1, list(a)) + a2.eval(newEnv) + return malfun(fn, a2, a1, env) + + else: + defaultApply() + + else: + defaultApply() + +proc print(exp: MalType): string = exp.pr_str + +var repl_env = initEnv() + +for k, v in ns.items: + repl_env.set(k, v) +repl_env.set("eval", fun(proc(xs: varargs[MalType]): MalType = eval(xs[0], repl_env))) +var ps = commandLineParams() +repl_env.set("*ARGV*", list((if paramCount() > 1: ps[1..ps.high] else: @[]).map(str))) + + +# core.nim: defined using nim +proc rep(str: string): string {.discardable.} = + str.read.eval(repl_env).print + +# core.mal: defined using mal itself +rep "(def! not (fn* (a) (if a false true)))" +rep "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))" +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)))))))" +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))))))))" + +if paramCount() >= 1: + rep "(load-file \"" & paramStr(1) & "\")" + quit() + +while true: + try: + let line = readLineFromStdin("user> ") + echo line.rep + except Blank: discard + except: + echo getCurrentExceptionMsg() + echo getCurrentException().getStackTrace() diff --git a/nim/stepA_mal.nim b/nim/stepA_mal.nim new file mode 100644 index 0000000..b138bd9 --- /dev/null +++ b/nim/stepA_mal.nim @@ -0,0 +1,191 @@ +import rdstdin, tables, sequtils, os, types, reader, printer, env, core + +proc read(str: string): MalType = str.read_str + +proc is_pair(x: MalType): bool = + x.kind in {List, Vector} and x.list.len > 0 + +proc quasiquote(ast: MalType): MalType = + if not ast.is_pair: + return list(symbol "quote", ast) + elif ast.list[0] == symbol "unquote": + return ast.list[1] + elif ast.list[0].is_pair and ast.list[0].list[0] == symbol "splice-unquote": + return list(symbol "concat", ast.list[0].list[1], + quasiquote(list ast.list[1 .. -1])) + else: + return list(symbol "cons", quasiquote(ast.list[0]), quasiquote(list(ast.list[1 .. -1]))) + +proc is_macro_call(ast: MalType, env: Env): bool = + ast.kind == List and ast.list.len > 0 and ast.list[0].kind == Symbol and + env.find(ast.list[0].str) != nil and env.get(ast.list[0].str).macro_q + +proc macroexpand(ast: MalType, env: Env): MalType = + result = ast + while result.is_macro_call(env): + let mac = env.get(result.list[0].str) + result = mac.malfun.fn(result.list[1 .. -1]).macroexpand(env) + +proc eval(ast: MalType, env: Env): MalType + +proc eval_ast(ast: MalType, env: var Env): MalType = + case ast.kind + of Symbol: + result = env.get(ast.str) + of List: + result = list ast.list.mapIt(MalType, it.eval(env)) + of Vector: + result = vector ast.list.mapIt(MalType, it.eval(env)) + of HashMap: + result = hash_map() + for k, v in ast.hash_map.pairs: + result.hash_map[k] = v.eval(env) + else: + result = ast + +proc eval(ast: MalType, env: Env): MalType = + var ast = ast + var env = env + + template defaultApply = + let el = ast.eval_ast(env) + let f = el.list[0] + case f.kind + of MalFun: + ast = f.malfun.ast + env = initEnv(f.malfun.env, f.malfun.params, list(el.list[1 .. -1])) + else: + return f.fun(el.list[1 .. -1]) + + while true: + if ast.kind != List: return ast.eval_ast(env) + + ast = ast.macroexpand(env) + if ast.kind != List: return ast + if ast.list.len == 0: return ast + + let a0 = ast.list[0] + case a0.kind + of Symbol: + case a0.str + of "def!": + let + a1 = ast.list[1] + a2 = ast.list[2] + res = a2.eval(env) + return env.set(a1.str, res) + + of "let*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var let_env = Env(env) + case a1.kind + of List, Vector: + for i in countup(0, a1.list.high, 2): + let_env.set(a1.list[i].str, a1.list[i+1].eval(let_env)) + else: raise newException(ValueError, "Illegal kind in let*") + ast = a2 + env = let_env + # Continue loop (TCO) + + of "quote": + return ast.list[1] + + of "quasiquote": + ast = ast.list[1].quasiquote + # Continue loop (TCO) + + of "defmacro!": + var fun = ast.list[2].eval(env) + fun.malfun.is_macro = true + return env.set(ast.list[1].str, fun) + + of "macroexpand": + return ast.list[1].macroexpand(env) + + of "try*": + let + a1 = ast.list[1] + a2 = ast.list[2] + if a2.list[0].str == "catch*": + try: + return a1.eval(env) + except MalError: + let exc = (ref MalError) getCurrentException() + var catchEnv = initEnv(env, list a2.list[1], exc.t) + return a2.list[2].eval(catchEnv) + except: + let exc = getCurrentExceptionMsg() + var catchEnv = initEnv(env, list a2.list[1], list str(exc)) + return a2.list[2].eval(catchEnv) + else: + return a1.eval(env) + + of "do": + let last = ast.list.high + let el = (list ast.list[1 .. <last]).eval_ast(env) + ast = ast.list[last] + # Continue loop (TCO) + + of "if": + let + a1 = ast.list[1] + a2 = ast.list[2] + cond = a1.eval(env) + + if cond.kind in {Nil, False}: + if ast.list.len > 3: ast = ast.list[3] + else: ast = nilObj + else: ast = a2 + + of "fn*": + let + a1 = ast.list[1] + a2 = ast.list[2] + var env2 = env + let fn = proc(a: varargs[MalType]): MalType = + var newEnv = initEnv(env2, a1, list(a)) + a2.eval(newEnv) + return malfun(fn, a2, a1, env) + + else: defaultApply() + + else: defaultApply() + +proc print(exp: MalType): string = exp.pr_str + +var repl_env = initEnv() + +for k, v in ns.items: + repl_env.set(k, v) +repl_env.set("eval", fun(proc(xs: varargs[MalType]): MalType = eval(xs[0], repl_env))) +var ps = commandLineParams() +repl_env.set("*ARGV*", list((if paramCount() > 1: ps[1..ps.high] else: @[]).map(str))) + + +# core.nim: defined using nim +proc rep(str: string): string {.discardable.} = + str.read.eval(repl_env).print + +# core.mal: defined using mal itself +rep "(def! not (fn* (a) (if a false true)))" +rep "(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))" +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)))))))" +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))))))))" +rep "(def! *host-language* \"nim\")" + +if paramCount() >= 1: + rep "(load-file \"" & paramStr(1) & "\")" + quit() + +rep "(println (str \"Mal [\" *host-language* \"]\"))" + +while true: + try: + let line = readLineFromStdin("user> ") + echo line.rep + except Blank: discard + except: + echo getCurrentExceptionMsg() + echo getCurrentException().getStackTrace() diff --git a/nim/types.nim b/nim/types.nim new file mode 100644 index 0000000..56983ff --- /dev/null +++ b/nim/types.nim @@ -0,0 +1,148 @@ +import tables, strutils + +type + MalTypeKind* = enum Nil, True, False, Number, Symbol, String, + List, Vector, HashMap, Fun, MalFun, Atom + + FunType = proc(a: varargs[MalType]): MalType + + MalFunType* = ref object + fn*: FunType + ast*: MalType + params*: MalType + env*: Env + is_macro*: bool + + MalType* = ref object + case kind*: MalTypeKind + of Nil, True, False: nil + of Number: number*: int + of String, Symbol: str*: string + of List, Vector: list*: seq[MalType] + of HashMap: hash_map*: Table[string, MalType] + of Fun: + fun*: FunType + is_macro*: bool + of MalFun: malfun*: MalFunType + of Atom: val*: MalType + + meta*: MalType + + Env* = ref object + data*: Table[string, MalType] + outer*: Env + +let nilObj* = MalType(kind: Nil) +let trueObj* = MalType(kind: True) +let falseObj* = MalType(kind: False) + +proc number*(x: int): MalType = MalType(kind: Number, number: x) + +proc symbol*(x: string): MalType = MalType(kind: Symbol, str: x) + +proc str*(x: string): MalType {.procvar.} = MalType(kind: String, str: x) + +proc keyword*(x: string): MalType = MalType(kind: String, str: "\xff" & x) + +proc atom*(x: MalType): MalType = + result = MalType(kind: Atom) + result.val = x + +proc list*(xs: varargs[MalType]): MalType {.procvar.} = + result = MalType(kind: List, list: newSeq[MalType](xs.len)) + for i, x in xs: result.list[i] = x + +proc vector*(xs: varargs[MalType]): MalType {.procvar.} = + result = MalType(kind: Vector, list: newSeq[MalType](xs.len)) + for i, x in xs: result.list[i] = x + +proc hash_map*(xs: varargs[MalType]): MalType {.procvar.} = + result = MalType(kind: HashMap, hash_map: initTable[string, MalType]()) + for i in countup(0, xs.high, 2): + let s = case xs[i].kind + of String: xs[i].str + else: xs[i].str + result.hash_map[s] = xs[i+1] + +proc macro_q*(x: MalType): bool = + if x.kind == Fun: result = x.is_macro + elif x.kind == MalFun: result = x.malfun.is_macro + else: raise newException(ValueError, "no function") + +proc getFun*(x: MalType): FunType = + if x.kind == Fun: result = x.fun + elif x.kind == MalFun: result = x.malfun.fn + else: raise newException(ValueError, "no function") + +proc fun*(x: proc(xs: varargs[MalType]): MalType, is_macro = false): MalType = + MalType(kind: Fun, fun: x, is_macro: is_macro) + +proc malfun*(fn: auto, ast, params: MalType, + env: Env, is_macro = false): MalType = + MalType(kind: MalFun, malfun: MalFunType(fn: fn, ast: ast, params: params, + env: env, is_macro: is_macro)) + +proc boolObj*(b: bool): MalType = + if b: trueObj else: falseObj + +proc list_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0].kind == List + +proc vector_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0].kind == Vector + +proc seq_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0].kind in {List, Vector} + +proc hash_map_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0].kind == HashMap + +proc empty_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0].list.len == 0 + +proc nil_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0].kind == Nil + +proc true_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0].kind == True + +proc false_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0].kind == False + +proc symbol*(xs: varargs[MalType]): MalType {.procvar.} = + symbol(xs[0].str) + +proc symbol_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0].kind == Symbol + +proc keyword*(xs: varargs[MalType]): MalType {.procvar.} = + keyword(xs[0].str) + +proc keyword_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj(xs[0].kind == String and xs[0].str[0] == '\xff') + +proc atom*(xs: varargs[MalType]): MalType {.procvar.} = + atom(xs[0]) + +proc atom_q*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0].kind == Atom + +proc count*(xs: varargs[MalType]): MalType {.procvar.} = + number if xs[0].kind == Nil: 0 else: xs[0].list.len + +proc `==`*(x, y: MalType): bool = + if not (x.kind in {List, Vector} and y.kind in {List, Vector}): + if x.kind != y.kind: return false + result = case x.kind + of Nil, True, False: true + of Number: x.number == y.number + of Symbol, String: x.str == y.str + of List, Vector: x.list == y.list + of HashMap: x.hash_map == y.hash_map + of Fun: x.fun == y.fun and + x.is_macro == y.is_macro + of MalFun: x.malfun == y.malfun + of Atom: x.val == y.val + +proc equal*(xs: varargs[MalType]): MalType {.procvar.} = + boolObj xs[0] == xs[1] diff --git a/ocaml/core.ml b/ocaml/core.ml index 20f68b6..12bf3c3 100644 --- a/ocaml/core.ml +++ b/ocaml/core.ml @@ -64,7 +64,10 @@ let init env = begin Env.set env (Types.symbol "vector?") (Types.fn (function [T.Vector _] -> T.Bool true | _ -> T.Bool false)); Env.set env (Types.symbol "empty?") - (Types.fn (function [T.List {T.value = []}] -> T.Bool true | _ -> T.Bool false)); + (Types.fn (function + | [T.List {T.value = []}] -> T.Bool true + | [T.Vector {T.value = []}] -> T.Bool true + | _ -> T.Bool false)); Env.set env (Types.symbol "count") (Types.fn (function | [T.List {T.value = xs}] diff --git a/process/guide.md b/process/guide.md index 6c6887c..df7eb5e 100644 --- a/process/guide.md +++ b/process/guide.md @@ -104,6 +104,7 @@ compared/described: * http://learnxinyminutes.com/ * http://hyperpolyglot.org/ * http://rosettacode.org/ +* http://rigaux.org/language-study/syntax-across-languages/ Do not let yourself be bogged down by specific problems. While the make-a-lisp process is structured as a series of steps, the reality is @@ -161,10 +162,11 @@ This step is basically just creating a skeleton of your interpreter. language is a statically typed) and `rep` calls them in order passing the return to the input of the next. -* Add a main loop that repeatedly prints a prompt, gets a line of - input from the user, calls `rep` with that line of input, and then - prints out the result from `rep`. It should also exit when you send - it an EOF (often Ctrl-D). +* Add a main loop that repeatedly prints a prompt (needs to be + "user> " for later tests to pass), gets a line of input from the + user, calls `rep` with that line of input, and then prints out the + result from `rep`. It should also exit when you send it an EOF + (often Ctrl-D). * If you are using a compiled (ahead-of-time rather than just-in-time) language, then create a Makefile (or appropriate project definition @@ -366,7 +368,7 @@ and each step will give progressively more bang for the buck. * keyword: just a string stored with unicode prefix (or char 127 if no unicode support). * vector: can be implemented with same underlying type as list if - there is some mechanism for marking/distringuishing from a list. + there is some mechanism for marking/distinguishing from a list. * hash-map: only need to implement string keys (which enables keyword keys since they are just special strings). @@ -483,7 +485,7 @@ the changes that will be made during this step: diff -urp ../process/step2_eval.txt ../process/step3_env.txt ``` -* Copy `step2_eval.qx` to `step2_env.qx`. +* Copy `step2_eval.qx` to `step3_env.qx`. * Create `env.qx` to hold the environment definition. @@ -1109,6 +1111,37 @@ Now go to the top level, run the step 8 tests: make test^quux^step8 ``` +There is a reasonably good chance that the macro tests will not pass +the first time. Although the implementation of macros is fairly +simple, debugging runtime bugs with macros can be fairly tricky. If +you do run into subtle problems that are difficult to solve, let me +recommend a couple of approaches: + +* Use the macroexpand special form to eliminate one of the layers of + indirection (to expand but skip evaluate). This will often reveal + the source of the issue. +* Add a debug print statement to the top of your main `eval` function + (inside the TCO loop) to print the current value of `ast` (hint use + `pr_str` to get easier to debug output). Pull up the step8 + implementation from another language and uncomment its `eval` + function (yes, I give you permission to violate the rule this once). + Run the two side-by-side. The first difference is likely to point to + the bug. + +Congratulations! You now have a Lisp interpreter with a super power +that most non-Lisp languages can only dream of (I have it on good +authority that languages dream when you are not using them). If you +are not already familiar with Lisp macros, I suggest the following +excercise: write a recursive macro that handles postfixed mal code +(with the function as the last parameter instead of the first). Or +not. I have not actually done so myself, but I have heard it is an +interesting excercise. + +In the next step you will add try/catch style exception handling to +your implementation in addition to some new core functions. After +step9 you will be very close to having a fully self-hosting mal +implementation. Let us continue! + ### Optional @@ -3,7 +3,7 @@ import os, sys, re import argparse, time -import pty +import pty, signal, atexit from subprocess import Popen, STDOUT, PIPE from select import select @@ -22,8 +22,8 @@ parser.add_argument('--test-timeout', default=20, type=int, help="default timeout for each individual test action") parser.add_argument('--pre-eval', default=None, type=str, help="Mal code to evaluate prior to running the test") -parser.add_argument('--redirect', action='store_true', - help="Run implementation in bash and redirect output to /dev/null") +parser.add_argument('--mono', action='store_true', + help="Use workarounds Mono/.Net Console misbehaviors") parser.add_argument('test_file', type=argparse.FileType('r'), help="a test file formatted as with mal test data") @@ -32,17 +32,25 @@ parser.add_argument('mal_cmd', nargs="*", "specify a Mal command line with dashed options.") class Runner(): - def __init__(self, args, redirect=False): - print "args: %s" % repr(args) - if redirect: - print "using redirect" - self.p = Popen(args, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=STDOUT) + def __init__(self, args, mono=False): + #print "args: %s" % repr(args) + self.mono = mono + + # Cleanup child process on exit + atexit.register(self.cleanup) + + if mono: + self.p = Popen(args, bufsize=0, + stdin=PIPE, stdout=PIPE, stderr=STDOUT, + preexec_fn=os.setsid) self.stdin = self.p.stdin self.stdout = self.p.stdout else: # provide tty to get 'interactive' readline to work master, slave = pty.openpty() - self.p = Popen(args, bufsize=0, stdin=slave, stdout=slave, stderr=STDOUT) + self.p = Popen(args, bufsize=0, + stdin=slave, stdout=slave, stderr=STDOUT, + preexec_fn=os.setsid) self.stdin = os.fdopen(master, 'r+b', 0) self.stdout = self.stdin @@ -53,11 +61,14 @@ class Runner(): def read_to_prompt(self, prompts, timeout): end_time = time.time() + timeout while time.time() < end_time: - [outs,_,_] = select([self.stdin], [], [], 1) - if self.stdin in outs: - new_data = self.stdin.read(1) + [outs,_,_] = select([self.stdout], [], [], 1) + if self.stdout in outs: + new_data = self.stdout.read(1) #print "new_data: '%s'" % new_data - self.buf += new_data + if self.mono: + self.buf += new_data.replace("\n", "\r\n") + else: + self.buf += new_data for prompt in prompts: regexp = re.compile(prompt) match = regexp.search(self.buf) @@ -69,12 +80,16 @@ class Runner(): return buf return None - def write(self, str): - self.stdout.write(str) + def writeline(self, str): + self.stdin.write(str + "\n") + if self.mono: + # Simulate echo + self.buf += str + "\r\n" def cleanup(self): + #print "cleaning up" if self.p: - self.p.terminate() + os.killpg(self.p.pid, signal.SIGTERM) self.p = None @@ -83,7 +98,7 @@ test_data = args.test_file.read().split('\n') if args.rundir: os.chdir(args.rundir) -r = Runner(args.mal_cmd, redirect=args.redirect) +r = Runner(args.mal_cmd, mono=args.mono) test_idx = 0 @@ -133,7 +148,6 @@ def assert_prompt(timeout): else: print "Did not get 'user> ' or 'mal-user> ' prompt" print " Got : %s" % repr(r.buf) - r.cleanup() sys.exit(1) @@ -156,7 +170,7 @@ while test_data: sys.stdout.flush() expected = "%s%s%s%s" % (form, sep, out, ret) - r.write(form + "\n") + r.writeline(form) try: res = r.read_to_prompt(['\r\nuser> ', '\nuser> ', '\r\nmal-user> ', '\nmal-user> '], @@ -171,9 +185,7 @@ while test_data: fail_cnt += 1 except: print "Got Exception" - r.cleanup() sys.exit(1) -r.cleanup() if fail_cnt > 0: print "FAILURES: %d" % fail_cnt |
