clonim: a Clojure compiler hosted on Nim
Reader, analyzer and Nim codegen, plus a ~140-fn clojure.core in Nim. Tail recursion compiles to native loops; vars are cells resolved once in the prelude rather than hashed per call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
980e0ef added
.gitignore +3 -0 | new file mode 100644 | ||
| @@ -0,0 +1,3 @@ | ||
| 1 | +bin/ | |
| 2 | +nimcache/ | |
| 3 | +*.o | |
| new file mode 100644 | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | +bin/ | ||
| 2 | +nimcache/ | ||
| 3 | +*.o | ||
added
README.md +71 -0 | new file mode 100644 | ||
| @@ -0,0 +1,71 @@ | ||
| 1 | +# clonim | |
| 2 | + | |
| 3 | +A Clojure compiler hosted on Nim. | |
| 4 | + | |
| 5 | +Inspired by [jank](https://jank-lang.org/), which compiles Clojure onto C++/LLVM | |
| 6 | +and gets C++'s codegen, inlining and native interop for free. clonim takes the | |
| 7 | +same bet with a smaller host: **read → analyze → emit Nim → let `nim c` do the | |
| 8 | +hard part.** The output is a single native binary with no VM and no JVM. | |
| 9 | + | |
| 10 | +```bash | |
| 11 | +nim c --hints:off -o:bin/clonim src/clonim.nim # build the compiler | |
| 12 | + | |
| 13 | +./bin/clonim run examples/tour.clj # compile + run | |
| 14 | +./bin/clonim build examples/tour.clj # native binary (-d:release) | |
| 15 | +./bin/clonim emit examples/tour.clj # show the generated Nim | |
| 16 | +``` | |
| 17 | + | |
| 18 | +## Pipeline | |
| 19 | + | |
| 20 | +| stage | file | what it does | | |
| 21 | +|---|---|---| | |
| 22 | +| reader | `src/reader.nim` | text → data. Forms *are* runtime values (homoiconic), as in Clojure | | |
| 23 | +| analyzer + codegen | `src/compiler.nim` | expands the macro set to core special forms, emits Nim statements | | |
| 24 | +| runtime | `src/runtime.nim` | the `Value` tagged union, equality, printing, var cells, `call` | | |
| 25 | +| core | `src/core.nim` | ~140 `clojure.core` builtins as Nim closures | | |
| 26 | +| driver | `src/clonim.nim` | shells out to `nim c`, times each phase | | |
| 27 | + | |
| 28 | +Codegen is statement-oriented: every form is compiled as "emit statements that | |
| 29 | +assign into this destination slot". That keeps Clojure's expression semantics | |
| 30 | +intact without fighting Nim's statement/expression split, and it makes | |
| 31 | +`recur` trivially correct. | |
| 32 | + | |
| 33 | +### Two things worth pointing at | |
| 34 | + | |
| 35 | +**`recur` becomes a real loop.** A `loop`/`fn` recur target emits `while true:` | |
| 36 | +over mutable Nim locals; `recur` assigns the new values and `continue`s. No | |
| 37 | +stack growth, no trampoline — `(sum-to 1000000)` runs in constant space. | |
| 38 | + | |
| 39 | +**Vars are cells, resolved once.** Each referenced var becomes a `VarCell` | |
| 40 | +resolved in the program prelude, so a call site is a pointer deref rather than a | |
| 41 | +hash lookup, while `def` can still rebind it later. Worth ~20% on call-heavy | |
| 42 | +code. | |
| 43 | + | |
| 44 | +## What works | |
| 45 | + | |
| 46 | +`def` `defn` (multi-arity, varargs, docstrings) `fn` (named, self-recursive) | |
| 47 | +`let` `loop`/`recur` `if` `when` `when-not` `if-not` `cond` `when-let` `if-let` | |
| 48 | +`do` `and` `or` `->` `->>` `doseq` `dotimes` `try`/`catch`/`finally` `quote` | |
| 49 | + | |
| 50 | +Destructuring: sequential `[a b & rest]` and associative `{:keys [x y]}` in `let`. | |
| 51 | + | |
| 52 | +Data: nil, bool, int, float, string, keyword, symbol, list, vector, map, set — | |
| 53 | +with structural equality and Clojure-shaped printing. Atoms, closures, `comp`, | |
| 54 | +`partial`, `juxt`, the usual seq library, `clojure.string/*`. | |
| 55 | + | |
| 56 | +## What doesn't (yet) | |
| 57 | + | |
| 58 | +- **`defmacro`.** The macro set is fixed and expanded by the compiler. User | |
| 59 | + macros need the compiler to be able to *evaluate* code at compile time — | |
| 60 | + the honest fix is to bootstrap clonim in itself, or embed an interpreter. | |
| 61 | +- **Laziness.** `map`/`filter`/`range` are eager. Infinite seqs will hang. | |
| 62 | +- **Persistent data structures.** Vectors and maps are copy-on-write `seq`s, so | |
| 63 | + `assoc` is O(n), not O(log₃₂ n). This is the first thing to replace. | |
| 64 | +- Protocols/records, namespaces (`ns` is parsed and ignored), refs/agents, | |
| 65 | + `#()` literals, syntax-quote, transducers, Nim interop. | |
| 66 | + | |
| 67 | +## Tests | |
| 68 | + | |
| 69 | +```bash | |
| 70 | +./run-tests.sh # builds the compiler, diffs every example against tests/*.expected | |
| 71 | +``` | |
| new file mode 100644 | |||
| @@ -0,0 +1,71 @@ | |||
| 1 | +# clonim | ||
| 2 | + | ||
| 3 | +A Clojure compiler hosted on Nim. | ||
| 4 | + | ||
| 5 | +Inspired by [jank](https://jank-lang.org/), which compiles Clojure onto C++/LLVM | ||
| 6 | +and gets C++'s codegen, inlining and native interop for free. clonim takes the | ||
| 7 | +same bet with a smaller host: **read → analyze → emit Nim → let `nim c` do the | ||
| 8 | +hard part.** The output is a single native binary with no VM and no JVM. | ||
| 9 | + | ||
| 10 | +```bash | ||
| 11 | +nim c --hints:off -o:bin/clonim src/clonim.nim # build the compiler | ||
| 12 | + | ||
| 13 | +./bin/clonim run examples/tour.clj # compile + run | ||
| 14 | +./bin/clonim build examples/tour.clj # native binary (-d:release) | ||
| 15 | +./bin/clonim emit examples/tour.clj # show the generated Nim | ||
| 16 | +``` | ||
| 17 | + | ||
| 18 | +## Pipeline | ||
| 19 | + | ||
| 20 | +| stage | file | what it does | | ||
| 21 | +|---|---|---| | ||
| 22 | +| reader | `src/reader.nim` | text → data. Forms *are* runtime values (homoiconic), as in Clojure | | ||
| 23 | +| analyzer + codegen | `src/compiler.nim` | expands the macro set to core special forms, emits Nim statements | | ||
| 24 | +| runtime | `src/runtime.nim` | the `Value` tagged union, equality, printing, var cells, `call` | | ||
| 25 | +| core | `src/core.nim` | ~140 `clojure.core` builtins as Nim closures | | ||
| 26 | +| driver | `src/clonim.nim` | shells out to `nim c`, times each phase | | ||
| 27 | + | ||
| 28 | +Codegen is statement-oriented: every form is compiled as "emit statements that | ||
| 29 | +assign into this destination slot". That keeps Clojure's expression semantics | ||
| 30 | +intact without fighting Nim's statement/expression split, and it makes | ||
| 31 | +`recur` trivially correct. | ||
| 32 | + | ||
| 33 | +### Two things worth pointing at | ||
| 34 | + | ||
| 35 | +**`recur` becomes a real loop.** A `loop`/`fn` recur target emits `while true:` | ||
| 36 | +over mutable Nim locals; `recur` assigns the new values and `continue`s. No | ||
| 37 | +stack growth, no trampoline — `(sum-to 1000000)` runs in constant space. | ||
| 38 | + | ||
| 39 | +**Vars are cells, resolved once.** Each referenced var becomes a `VarCell` | ||
| 40 | +resolved in the program prelude, so a call site is a pointer deref rather than a | ||
| 41 | +hash lookup, while `def` can still rebind it later. Worth ~20% on call-heavy | ||
| 42 | +code. | ||
| 43 | + | ||
| 44 | +## What works | ||
| 45 | + | ||
| 46 | +`def` `defn` (multi-arity, varargs, docstrings) `fn` (named, self-recursive) | ||
| 47 | +`let` `loop`/`recur` `if` `when` `when-not` `if-not` `cond` `when-let` `if-let` | ||
| 48 | +`do` `and` `or` `->` `->>` `doseq` `dotimes` `try`/`catch`/`finally` `quote` | ||
| 49 | + | ||
| 50 | +Destructuring: sequential `[a b & rest]` and associative `{:keys [x y]}` in `let`. | ||
| 51 | + | ||
| 52 | +Data: nil, bool, int, float, string, keyword, symbol, list, vector, map, set — | ||
| 53 | +with structural equality and Clojure-shaped printing. Atoms, closures, `comp`, | ||
| 54 | +`partial`, `juxt`, the usual seq library, `clojure.string/*`. | ||
| 55 | + | ||
| 56 | +## What doesn't (yet) | ||
| 57 | + | ||
| 58 | +- **`defmacro`.** The macro set is fixed and expanded by the compiler. User | ||
| 59 | + macros need the compiler to be able to *evaluate* code at compile time — | ||
| 60 | + the honest fix is to bootstrap clonim in itself, or embed an interpreter. | ||
| 61 | +- **Laziness.** `map`/`filter`/`range` are eager. Infinite seqs will hang. | ||
| 62 | +- **Persistent data structures.** Vectors and maps are copy-on-write `seq`s, so | ||
| 63 | + `assoc` is O(n), not O(log₃₂ n). This is the first thing to replace. | ||
| 64 | +- Protocols/records, namespaces (`ns` is parsed and ignored), refs/agents, | ||
| 65 | + `#()` literals, syntax-quote, transducers, Nim interop. | ||
| 66 | + | ||
| 67 | +## Tests | ||
| 68 | + | ||
| 69 | +```bash | ||
| 70 | +./run-tests.sh # builds the compiler, diffs every example against tests/*.expected | ||
| 71 | +``` | ||
added
examples/bench.clj +3 -0 | new file mode 100644 | ||
| @@ -0,0 +1,3 @@ | ||
| 1 | +(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) | |
| 2 | +(let [t (now-ms)] | |
| 3 | + (println "fib 30 =" (fib 30) "in" (- (now-ms) t) "ms")) | |
| new file mode 100644 | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | +(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) | ||
| 2 | +(let [t (now-ms)] | ||
| 3 | + (println "fib 30 =" (fib 30) "in" (- (now-ms) t) "ms")) | ||
added
examples/hello.clj +8 -0 | new file mode 100644 | ||
| @@ -0,0 +1,8 @@ | ||
| 1 | +;; a first taste | |
| 2 | +(defn greet [name] | |
| 3 | + (str "Hello, " name "!")) | |
| 4 | + | |
| 5 | +(println (greet "world")) | |
| 6 | +(println (+ 1 2 3) (* 2 21) (/ 10 4) (/ 10.0 4)) | |
| 7 | +(println (map inc [1 2 3]) (filter even? (range 10))) | |
| 8 | +(println {:a 1 :b [2 3]} #{1 2} '(quoted list)) | |
| new file mode 100644 | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | +;; a first taste | ||
| 2 | +(defn greet [name] | ||
| 3 | + (str "Hello, " name "!")) | ||
| 4 | + | ||
| 5 | +(println (greet "world")) | ||
| 6 | +(println (+ 1 2 3) (* 2 21) (/ 10 4) (/ 10.0 4)) | ||
| 7 | +(println (map inc [1 2 3]) (filter even? (range 10))) | ||
| 8 | +(println {:a 1 :b [2 3]} #{1 2} '(quoted list)) | ||
added
examples/tour.clj +60 -0 | new file mode 100644 | ||
| @@ -0,0 +1,60 @@ | ||
| 1 | +;; ---- tail recursion compiles to a Nim loop, no stack growth | |
| 2 | +(defn sum-to [n] | |
| 3 | + (loop [i 0 acc 0] | |
| 4 | + (if (> i n) | |
| 5 | + acc | |
| 6 | + (recur (inc i) (+ acc i))))) | |
| 7 | + | |
| 8 | +(println "sum 1..1e6 =" (sum-to 1000000)) | |
| 9 | + | |
| 10 | +;; ---- closures | |
| 11 | +(defn adder [n] (fn [x] (+ x n))) | |
| 12 | +(def add5 (adder 5)) | |
| 13 | +(println "add5 10 =" (add5 10)) | |
| 14 | + | |
| 15 | +;; ---- multi-arity + varargs | |
| 16 | +(defn hi | |
| 17 | + ([] (hi "stranger")) | |
| 18 | + ([who] (str "hi " who)) | |
| 19 | + ([who & more] (str "hi " who " and " (count more) " others"))) | |
| 20 | +(println (hi) "|" (hi "ann") "|" (hi "ann" "bo" "cy")) | |
| 21 | + | |
| 22 | +;; ---- self-recursive fn by name | |
| 23 | +(def fact (fn f [n] (if (<= n 1) 1 (* n (f (dec n)))))) | |
| 24 | +(println "20! =" (fact 20)) | |
| 25 | + | |
| 26 | +;; ---- destructuring | |
| 27 | +(let [[a b & rest] [1 2 3 4 5] | |
| 28 | + {:keys [x y]} {:x 10 :y 20}] | |
| 29 | + (println a b rest x y)) | |
| 30 | + | |
| 31 | +;; ---- threading macros | |
| 32 | +(println (-> 5 inc (* 3) (- 2))) | |
| 33 | +(println (->> (range 20) (filter odd?) (map #_skipped (fn [n] (* n n))) (reduce +))) | |
| 34 | + | |
| 35 | +;; ---- atoms | |
| 36 | +(def counter (atom 0)) | |
| 37 | +(dotimes [_ 5] (swap! counter inc)) | |
| 38 | +(println "counter =" @counter) | |
| 39 | + | |
| 40 | +;; ---- maps, sorting, grouping | |
| 41 | +(def people [{:name "ada" :age 36} {:name "bo" :age 9} {:name "cy" :age 52}]) | |
| 42 | +(println (map :name (sort-by :age people))) | |
| 43 | +(println (group-by (fn [p] (if (< (:age p) 18) :kid :adult)) people)) | |
| 44 | + | |
| 45 | +;; ---- cond / when / case-ish dispatch | |
| 46 | +(defn classify [n] | |
| 47 | + (cond | |
| 48 | + (zero? n) "zero" | |
| 49 | + (neg? n) "negative" | |
| 50 | + (even? n) "even" | |
| 51 | + :else "odd")) | |
| 52 | +(println (map classify [0 -3 4 7])) | |
| 53 | + | |
| 54 | +;; ---- exceptions | |
| 55 | +(println (try (/ 1 0) (catch Exception e (str "caught: " e)))) | |
| 56 | + | |
| 57 | +;; ---- higher order composition | |
| 58 | +(def inc-then-double (comp (partial * 2) inc)) | |
| 59 | +(println (inc-then-double 20)) | |
| 60 | +(println (apply + (range 101))) | |
| new file mode 100644 | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | +;; ---- tail recursion compiles to a Nim loop, no stack growth | ||
| 2 | +(defn sum-to [n] | ||
| 3 | + (loop [i 0 acc 0] | ||
| 4 | + (if (> i n) | ||
| 5 | + acc | ||
| 6 | + (recur (inc i) (+ acc i))))) | ||
| 7 | + | ||
| 8 | +(println "sum 1..1e6 =" (sum-to 1000000)) | ||
| 9 | + | ||
| 10 | +;; ---- closures | ||
| 11 | +(defn adder [n] (fn [x] (+ x n))) | ||
| 12 | +(def add5 (adder 5)) | ||
| 13 | +(println "add5 10 =" (add5 10)) | ||
| 14 | + | ||
| 15 | +;; ---- multi-arity + varargs | ||
| 16 | +(defn hi | ||
| 17 | + ([] (hi "stranger")) | ||
| 18 | + ([who] (str "hi " who)) | ||
| 19 | + ([who & more] (str "hi " who " and " (count more) " others"))) | ||
| 20 | +(println (hi) "|" (hi "ann") "|" (hi "ann" "bo" "cy")) | ||
| 21 | + | ||
| 22 | +;; ---- self-recursive fn by name | ||
| 23 | +(def fact (fn f [n] (if (<= n 1) 1 (* n (f (dec n)))))) | ||
| 24 | +(println "20! =" (fact 20)) | ||
| 25 | + | ||
| 26 | +;; ---- destructuring | ||
| 27 | +(let [[a b & rest] [1 2 3 4 5] | ||
| 28 | + {:keys [x y]} {:x 10 :y 20}] | ||
| 29 | + (println a b rest x y)) | ||
| 30 | + | ||
| 31 | +;; ---- threading macros | ||
| 32 | +(println (-> 5 inc (* 3) (- 2))) | ||
| 33 | +(println (->> (range 20) (filter odd?) (map #_skipped (fn [n] (* n n))) (reduce +))) | ||
| 34 | + | ||
| 35 | +;; ---- atoms | ||
| 36 | +(def counter (atom 0)) | ||
| 37 | +(dotimes [_ 5] (swap! counter inc)) | ||
| 38 | +(println "counter =" @counter) | ||
| 39 | + | ||
| 40 | +;; ---- maps, sorting, grouping | ||
| 41 | +(def people [{:name "ada" :age 36} {:name "bo" :age 9} {:name "cy" :age 52}]) | ||
| 42 | +(println (map :name (sort-by :age people))) | ||
| 43 | +(println (group-by (fn [p] (if (< (:age p) 18) :kid :adult)) people)) | ||
| 44 | + | ||
| 45 | +;; ---- cond / when / case-ish dispatch | ||
| 46 | +(defn classify [n] | ||
| 47 | + (cond | ||
| 48 | + (zero? n) "zero" | ||
| 49 | + (neg? n) "negative" | ||
| 50 | + (even? n) "even" | ||
| 51 | + :else "odd")) | ||
| 52 | +(println (map classify [0 -3 4 7])) | ||
| 53 | + | ||
| 54 | +;; ---- exceptions | ||
| 55 | +(println (try (/ 1 0) (catch Exception e (str "caught: " e)))) | ||
| 56 | + | ||
| 57 | +;; ---- higher order composition | ||
| 58 | +(def inc-then-double (comp (partial * 2) inc)) | ||
| 59 | +(println (inc-then-double 20)) | ||
| 60 | +(println (apply + (range 101))) | ||
added
run-tests.sh +18 -0 | new file mode 100755 | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# Compile and run every example, diffing against tests/<name>.expected | |
| 3 | +set -u | |
| 4 | +cd "$(dirname "$0")" | |
| 5 | +nim c --hints:off --warnings:off -o:bin/clonim src/clonim.nim || exit 1 | |
| 6 | +fail=0 | |
| 7 | +for f in examples/*.clj; do | |
| 8 | + name=$(basename "$f" .clj) | |
| 9 | + exp="tests/$name.expected" | |
| 10 | + [ -f "$exp" ] || continue | |
| 11 | + got=$(./bin/clonim run "$f" 2>&1) | |
| 12 | + if [ "$got" = "$(cat "$exp")" ]; then | |
| 13 | + echo "ok $name" | |
| 14 | + else | |
| 15 | + echo "FAIL $name"; diff <(echo "$got") "$exp" | head -20; fail=1 | |
| 16 | + fi | |
| 17 | +done | |
| 18 | +exit $fail | |
| new file mode 100755 | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +# Compile and run every example, diffing against tests/<name>.expected | ||
| 3 | +set -u | ||
| 4 | +cd "$(dirname "$0")" | ||
| 5 | +nim c --hints:off --warnings:off -o:bin/clonim src/clonim.nim || exit 1 | ||
| 6 | +fail=0 | ||
| 7 | +for f in examples/*.clj; do | ||
| 8 | + name=$(basename "$f" .clj) | ||
| 9 | + exp="tests/$name.expected" | ||
| 10 | + [ -f "$exp" ] || continue | ||
| 11 | + got=$(./bin/clonim run "$f" 2>&1) | ||
| 12 | + if [ "$got" = "$(cat "$exp")" ]; then | ||
| 13 | + echo "ok $name" | ||
| 14 | + else | ||
| 15 | + echo "FAIL $name"; diff <(echo "$got") "$exp" | head -20; fail=1 | ||
| 16 | + fi | ||
| 17 | +done | ||
| 18 | +exit $fail | ||
added
src/clonim.nim +103 -0 | new file mode 100644 | ||
| @@ -0,0 +1,103 @@ | ||
| 1 | +## clonim — a Clojure compiler hosted on Nim. | |
| 2 | +## | |
| 3 | +## clonim run foo.clj compile and run | |
| 4 | +## clonim build foo.clj [-o bin] compile to a native binary | |
| 5 | +## clonim emit foo.clj print the generated Nim | |
| 6 | +import std/[os, osproc, strutils, times] | |
| 7 | +import runtime, compiler | |
| 8 | + | |
| 9 | +proc usage() = | |
| 10 | + echo """clonim — a Clojure compiler hosted on Nim | |
| 11 | + | |
| 12 | +usage: | |
| 13 | + clonim run <file.clj> compile to Nim, build, and run | |
| 14 | + clonim build <file.clj> [-o <bin>] build a native binary | |
| 15 | + clonim emit <file.clj> print the generated Nim source | |
| 16 | + | |
| 17 | +options: | |
| 18 | + -o <path> output binary path (build) | |
| 19 | + -v show the nim build command and timings | |
| 20 | + -d build with -d:release (default for build, off for run)""" | |
| 21 | + quit(1) | |
| 22 | + | |
| 23 | +proc srcDir(): string = | |
| 24 | + ## where runtime.nim / core.nim live, so generated code can import them | |
| 25 | + for cand in [getAppDir(), getAppDir().parentDir / "src", | |
| 26 | + getAppDir().parentDir.parentDir / "src"]: | |
| 27 | + if fileExists(cand / "runtime.nim"): return cand | |
| 28 | + getAppDir() | |
| 29 | + | |
| 30 | +proc main() = | |
| 31 | + let argv = commandLineParams() | |
| 32 | + if argv.len < 2: usage() | |
| 33 | + let cmd = argv[0] | |
| 34 | + let file = argv[1] | |
| 35 | + if not fileExists(file): | |
| 36 | + stderr.writeLine("clonim: no such file: " & file) | |
| 37 | + quit(1) | |
| 38 | + | |
| 39 | + var outBin = "" | |
| 40 | + var verbose = false | |
| 41 | + var release = cmd == "build" | |
| 42 | + var i = 2 | |
| 43 | + while i < argv.len: | |
| 44 | + case argv[i] | |
| 45 | + of "-o": | |
| 46 | + inc i | |
| 47 | + if i >= argv.len: usage() | |
| 48 | + outBin = argv[i] | |
| 49 | + of "-v": verbose = true | |
| 50 | + of "-d": release = true | |
| 51 | + else: usage() | |
| 52 | + inc i | |
| 53 | + | |
| 54 | + let t0 = epochTime() | |
| 55 | + var nimSrc = "" | |
| 56 | + try: | |
| 57 | + nimSrc = compileSource(readFile(file)) | |
| 58 | + except CljError as e: | |
| 59 | + stderr.writeLine("clonim: " & e.msg) | |
| 60 | + quit(1) | |
| 61 | + let tCompile = epochTime() - t0 | |
| 62 | + | |
| 63 | + if cmd == "emit": | |
| 64 | + stdout.write nimSrc | |
| 65 | + return | |
| 66 | + | |
| 67 | + let stem = file.splitFile.name | |
| 68 | + let work = getTempDir() / ("clonim-" & stem & "-" & $getCurrentProcessId()) | |
| 69 | + createDir(work) | |
| 70 | + defer: removeDir(work) | |
| 71 | + let nimFile = work / (stem & ".nim") | |
| 72 | + writeFile(nimFile, nimSrc) | |
| 73 | + | |
| 74 | + if outBin.len == 0: | |
| 75 | + outBin = (if cmd == "build": stem else: work / stem) | |
| 76 | + outBin = outBin.absolutePath | |
| 77 | + | |
| 78 | + var nimCmd = @["nim", "c", "--hints:off", "--warnings:off", | |
| 79 | + "--path:" & srcDir(), "--nimcache:" & (work / "cache"), | |
| 80 | + "-o:" & outBin] | |
| 81 | + if release: nimCmd.add "-d:release" | |
| 82 | + nimCmd.add nimFile | |
| 83 | + if verbose: echo "clonim: " & nimCmd.join(" ") | |
| 84 | + | |
| 85 | + let t1 = epochTime() | |
| 86 | + let (output, code) = execCmdEx(nimCmd.join(" ")) | |
| 87 | + let tBuild = epochTime() - t1 | |
| 88 | + if code != 0: | |
| 89 | + stderr.writeLine("clonim: Nim backend failed\n" & output) | |
| 90 | + stderr.writeLine("--- generated source ---\n" & nimSrc) | |
| 91 | + quit(1) | |
| 92 | + if verbose: | |
| 93 | + echo "clonim: analyze ", (tCompile * 1000).formatFloat(ffDecimal, 1), "ms ", | |
| 94 | + "nim ", (tBuild * 1000).formatFloat(ffDecimal, 1), "ms" | |
| 95 | + | |
| 96 | + case cmd | |
| 97 | + of "build": | |
| 98 | + echo "clonim: wrote " & outBin | |
| 99 | + of "run": | |
| 100 | + quit(execShellCmd(quoteShell(outBin))) | |
| 101 | + else: usage() | |
| 102 | + | |
| 103 | +main() | |
| new file mode 100644 | |||
| @@ -0,0 +1,103 @@ | |||
| 1 | +## clonim — a Clojure compiler hosted on Nim. | ||
| 2 | +## | ||
| 3 | +## clonim run foo.clj compile and run | ||
| 4 | +## clonim build foo.clj [-o bin] compile to a native binary | ||
| 5 | +## clonim emit foo.clj print the generated Nim | ||
| 6 | +import std/[os, osproc, strutils, times] | ||
| 7 | +import runtime, compiler | ||
| 8 | + | ||
| 9 | +proc usage() = | ||
| 10 | + echo """clonim — a Clojure compiler hosted on Nim | ||
| 11 | + | ||
| 12 | +usage: | ||
| 13 | + clonim run <file.clj> compile to Nim, build, and run | ||
| 14 | + clonim build <file.clj> [-o <bin>] build a native binary | ||
| 15 | + clonim emit <file.clj> print the generated Nim source | ||
| 16 | + | ||
| 17 | +options: | ||
| 18 | + -o <path> output binary path (build) | ||
| 19 | + -v show the nim build command and timings | ||
| 20 | + -d build with -d:release (default for build, off for run)""" | ||
| 21 | + quit(1) | ||
| 22 | + | ||
| 23 | +proc srcDir(): string = | ||
| 24 | + ## where runtime.nim / core.nim live, so generated code can import them | ||
| 25 | + for cand in [getAppDir(), getAppDir().parentDir / "src", | ||
| 26 | + getAppDir().parentDir.parentDir / "src"]: | ||
| 27 | + if fileExists(cand / "runtime.nim"): return cand | ||
| 28 | + getAppDir() | ||
| 29 | + | ||
| 30 | +proc main() = | ||
| 31 | + let argv = commandLineParams() | ||
| 32 | + if argv.len < 2: usage() | ||
| 33 | + let cmd = argv[0] | ||
| 34 | + let file = argv[1] | ||
| 35 | + if not fileExists(file): | ||
| 36 | + stderr.writeLine("clonim: no such file: " & file) | ||
| 37 | + quit(1) | ||
| 38 | + | ||
| 39 | + var outBin = "" | ||
| 40 | + var verbose = false | ||
| 41 | + var release = cmd == "build" | ||
| 42 | + var i = 2 | ||
| 43 | + while i < argv.len: | ||
| 44 | + case argv[i] | ||
| 45 | + of "-o": | ||
| 46 | + inc i | ||
| 47 | + if i >= argv.len: usage() | ||
| 48 | + outBin = argv[i] | ||
| 49 | + of "-v": verbose = true | ||
| 50 | + of "-d": release = true | ||
| 51 | + else: usage() | ||
| 52 | + inc i | ||
| 53 | + | ||
| 54 | + let t0 = epochTime() | ||
| 55 | + var nimSrc = "" | ||
| 56 | + try: | ||
| 57 | + nimSrc = compileSource(readFile(file)) | ||
| 58 | + except CljError as e: | ||
| 59 | + stderr.writeLine("clonim: " & e.msg) | ||
| 60 | + quit(1) | ||
| 61 | + let tCompile = epochTime() - t0 | ||
| 62 | + | ||
| 63 | + if cmd == "emit": | ||
| 64 | + stdout.write nimSrc | ||
| 65 | + return | ||
| 66 | + | ||
| 67 | + let stem = file.splitFile.name | ||
| 68 | + let work = getTempDir() / ("clonim-" & stem & "-" & $getCurrentProcessId()) | ||
| 69 | + createDir(work) | ||
| 70 | + defer: removeDir(work) | ||
| 71 | + let nimFile = work / (stem & ".nim") | ||
| 72 | + writeFile(nimFile, nimSrc) | ||
| 73 | + | ||
| 74 | + if outBin.len == 0: | ||
| 75 | + outBin = (if cmd == "build": stem else: work / stem) | ||
| 76 | + outBin = outBin.absolutePath | ||
| 77 | + | ||
| 78 | + var nimCmd = @["nim", "c", "--hints:off", "--warnings:off", | ||
| 79 | + "--path:" & srcDir(), "--nimcache:" & (work / "cache"), | ||
| 80 | + "-o:" & outBin] | ||
| 81 | + if release: nimCmd.add "-d:release" | ||
| 82 | + nimCmd.add nimFile | ||
| 83 | + if verbose: echo "clonim: " & nimCmd.join(" ") | ||
| 84 | + | ||
| 85 | + let t1 = epochTime() | ||
| 86 | + let (output, code) = execCmdEx(nimCmd.join(" ")) | ||
| 87 | + let tBuild = epochTime() - t1 | ||
| 88 | + if code != 0: | ||
| 89 | + stderr.writeLine("clonim: Nim backend failed\n" & output) | ||
| 90 | + stderr.writeLine("--- generated source ---\n" & nimSrc) | ||
| 91 | + quit(1) | ||
| 92 | + if verbose: | ||
| 93 | + echo "clonim: analyze ", (tCompile * 1000).formatFloat(ffDecimal, 1), "ms ", | ||
| 94 | + "nim ", (tBuild * 1000).formatFloat(ffDecimal, 1), "ms" | ||
| 95 | + | ||
| 96 | + case cmd | ||
| 97 | + of "build": | ||
| 98 | + echo "clonim: wrote " & outBin | ||
| 99 | + of "run": | ||
| 100 | + quit(execShellCmd(quoteShell(outBin))) | ||
| 101 | + else: usage() | ||
| 102 | + | ||
| 103 | +main() | ||
added
src/compiler.nim +619 -0 | new file mode 100644 | ||
| @@ -0,0 +1,619 @@ | ||
| 1 | +## clonim compiler — Clojure forms -> Nim source. | |
| 2 | +## | |
| 3 | +## Shape borrowed from jank: read to data, analyze into a small set of core | |
| 4 | +## special forms (everything else is expanded), then emit host-language code | |
| 5 | +## and let the host compiler do register allocation, inlining and codegen. | |
| 6 | +import std/[tables, strutils, sets] | |
| 7 | +import runtime, reader | |
| 8 | + | |
| 9 | +type | |
| 10 | + Env = ref object | |
| 11 | + parent: Env | |
| 12 | + locals: Table[string, string] # clojure name -> nim identifier | |
| 13 | + | |
| 14 | + Ctx = ref object | |
| 15 | + body: seq[string] # emitted lines | |
| 16 | + indent: int | |
| 17 | + counter: int | |
| 18 | + recurStack: seq[seq[string]] # nim idents of the enclosing recur target | |
| 19 | + defined: HashSet[string] # names def'd so far (for nicer errors) | |
| 20 | + prelude: seq[string] # hoisted var-cell resolutions | |
| 21 | + cells: Table[string, string] # clojure var name -> nim cell ident | |
| 22 | + | |
| 23 | +proc newEnv(parent: Env = nil): Env = | |
| 24 | + Env(parent: parent, locals: initTable[string, string]()) | |
| 25 | + | |
| 26 | +proc lookup(env: Env, name: string): string = | |
| 27 | + var e = env | |
| 28 | + while e != nil: | |
| 29 | + if e.locals.hasKey(name): return e.locals[name] | |
| 30 | + e = e.parent | |
| 31 | + "" | |
| 32 | + | |
| 33 | +proc line(c: Ctx, s: string) = | |
| 34 | + c.body.add repeat(" ", c.indent) & s | |
| 35 | + | |
| 36 | +proc push(c: Ctx) = inc c.indent | |
| 37 | +proc pop(c: Ctx) = dec c.indent | |
| 38 | + | |
| 39 | +proc gensym(c: Ctx, prefix: string): string = | |
| 40 | + inc c.counter | |
| 41 | + prefix & "_" & $c.counter | |
| 42 | + | |
| 43 | +proc mangle(name: string): string = | |
| 44 | + result = "" | |
| 45 | + for ch in name: | |
| 46 | + if ch in {'a'..'z', 'A'..'Z', '0'..'9'}: result.add ch | |
| 47 | + elif ch == '-': result.add 'X' | |
| 48 | + else: result.add 'Y' | |
| 49 | + if result.len == 0 or result[0] in {'0'..'9'}: result = "v" & result | |
| 50 | + | |
| 51 | +proc nimStr(s: string): string = | |
| 52 | + result = "\"" | |
| 53 | + for ch in s: | |
| 54 | + case ch | |
| 55 | + of '"': result.add "\\\"" | |
| 56 | + of '\\': result.add "\\\\" | |
| 57 | + of '\n': result.add "\\n" | |
| 58 | + of '\t': result.add "\\t" | |
| 59 | + of '\r': result.add "\\r" | |
| 60 | + else: | |
| 61 | + if ch.ord < 32: result.add "\\x" & toHex(ch.ord, 2) | |
| 62 | + else: result.add ch | |
| 63 | + result.add "\"" | |
| 64 | + | |
| 65 | +proc cellFor(c: Ctx, name: string): string = | |
| 66 | + ## Resolve each referenced var exactly once, at program start. | |
| 67 | + if c.cells.hasKey(name): return c.cells[name] | |
| 68 | + inc c.counter | |
| 69 | + result = "c_" & $c.counter | |
| 70 | + c.cells[name] = result | |
| 71 | + c.prelude.add " let " & result & " = varCell(" & nimStr(name) & ")" | |
| 72 | + | |
| 73 | +proc isSym(v: Value, name: string): bool = | |
| 74 | + not v.isNil and v.kind == kSymbol and v.s == name | |
| 75 | + | |
| 76 | +proc symName(v: Value): string = | |
| 77 | + if v.isNil or v.kind != kSymbol: err("Expected a symbol, got: " & prStr(v)) | |
| 78 | + v.s | |
| 79 | + | |
| 80 | +# ------------------------------------------------------------- quoted data | |
| 81 | +proc quoteLit(v: Value): string = | |
| 82 | + if v.isNil: return "NilV" | |
| 83 | + case v.kind | |
| 84 | + of kNil: "NilV" | |
| 85 | + of kBool: (if v.b: "TrueV" else: "FalseV") | |
| 86 | + of kInt: "mkInt(" & $v.i & ")" | |
| 87 | + of kFloat: "mkFloat(" & $v.f & ")" | |
| 88 | + of kStr: "mkStr(" & nimStr(v.s) & ")" | |
| 89 | + of kKeyword: "mkKeyword(" & nimStr(v.s) & ")" | |
| 90 | + of kSymbol: "mkSymbol(" & nimStr(v.s) & ")" | |
| 91 | + of kList, kVector, kSet: | |
| 92 | + var parts: seq[string] = @[] | |
| 93 | + for x in v.items: parts.add quoteLit(x) | |
| 94 | + let ctor = (case v.kind | |
| 95 | + of kList: "mkList" | |
| 96 | + of kVector: "mkVector" | |
| 97 | + else: "mkSet") | |
| 98 | + ctor & "(@[" & parts.join(", ") & "])" & | |
| 99 | + (if parts.len == 0: "" else: "") | |
| 100 | + of kMap: | |
| 101 | + var parts: seq[string] = @[] | |
| 102 | + for (k, val) in v.pairs: parts.add "(" & quoteLit(k) & ", " & quoteLit(val) & ")" | |
| 103 | + "mkMap(@[" & parts.join(", ") & "])" | |
| 104 | + of kFn: err("Can't quote a function") | |
| 105 | + | |
| 106 | +proc emptySeqFix(s: string, elemType: string): string = | |
| 107 | + ## `@[]` has no inferable element type in Nim; annotate it. | |
| 108 | + s.replace("@[]", "newSeq[" & elemType & "]()") | |
| 109 | + | |
| 110 | +# ------------------------------------------------------------- code gen | |
| 111 | +proc genInto(f: Value, dst: string, env: Env, c: Ctx) | |
| 112 | + | |
| 113 | +proc genExpr(f: Value, env: Env, c: Ctx): string = | |
| 114 | + result = c.gensym("t") | |
| 115 | + c.line("var " & result & ": Value = NilV") | |
| 116 | + genInto(f, result, env, c) | |
| 117 | + | |
| 118 | +proc genBody(forms: seq[Value], dst: string, env: Env, c: Ctx) = | |
| 119 | + if forms.len == 0: | |
| 120 | + c.line(dst & " = NilV") | |
| 121 | + return | |
| 122 | + for i in 0 ..< forms.len - 1: | |
| 123 | + discard genExpr(forms[i], env, c) | |
| 124 | + genInto(forms[^1], dst, env, c) | |
| 125 | + | |
| 126 | +type | |
| 127 | + FnClause = object | |
| 128 | + params: seq[string] | |
| 129 | + restParam: string | |
| 130 | + body: seq[Value] | |
| 131 | + | |
| 132 | +proc parseParams(v: Value): FnClause = | |
| 133 | + if v.isNil or v.kind != kVector: err("Parameter list must be a vector, got: " & prStr(v)) | |
| 134 | + result = FnClause(params: @[], restParam: "", body: @[]) | |
| 135 | + var i = 0 | |
| 136 | + while i < v.items.len: | |
| 137 | + let p = v.items[i] | |
| 138 | + if isSym(p, "&"): | |
| 139 | + if i + 1 >= v.items.len: err("Missing symbol after &") | |
| 140 | + result.restParam = symName(v.items[i + 1]) | |
| 141 | + break | |
| 142 | + result.params.add symName(p) | |
| 143 | + inc i | |
| 144 | + | |
| 145 | +proc genFn(name: string, clauses: seq[FnClause], selfIdent: string, env: Env, c: Ctx, dst: string) = | |
| 146 | + let argsIdent = c.gensym("args") | |
| 147 | + c.line(dst & " = mkFn(" & nimStr(name) & ", proc (" & argsIdent & ": seq[Value]): Value =") | |
| 148 | + c.push | |
| 149 | + var first = true | |
| 150 | + for cl in clauses: | |
| 151 | + let cond = | |
| 152 | + if cl.restParam.len > 0: argsIdent & ".len >= " & $cl.params.len | |
| 153 | + else: argsIdent & ".len == " & $cl.params.len | |
| 154 | + c.line((if first: "if " else: "elif ") & cond & ":") | |
| 155 | + first = false | |
| 156 | + c.push | |
| 157 | + let fenv = newEnv(env) | |
| 158 | + if selfIdent.len > 0 and name.len > 0: | |
| 159 | + fenv.locals[name] = selfIdent | |
| 160 | + var recurIdents: seq[string] = @[] | |
| 161 | + for i, p in cl.params: | |
| 162 | + let id = c.gensym("p" & mangle(p)) | |
| 163 | + c.line("var " & id & ": Value = argAt(" & argsIdent & ", " & $i & ")") | |
| 164 | + fenv.locals[p] = id | |
| 165 | + recurIdents.add id | |
| 166 | + if cl.restParam.len > 0: | |
| 167 | + let id = c.gensym("p" & mangle(cl.restParam)) | |
| 168 | + c.line("var " & id & ": Value = restArgs(" & argsIdent & ", " & $cl.params.len & ")") | |
| 169 | + fenv.locals[cl.restParam] = id | |
| 170 | + let res = c.gensym("res") | |
| 171 | + c.line("var " & res & ": Value = NilV") | |
| 172 | + c.line("while true:") | |
| 173 | + c.push | |
| 174 | + c.recurStack.add recurIdents | |
| 175 | + genBody(cl.body, res, fenv, c) | |
| 176 | + discard c.recurStack.pop | |
| 177 | + c.line("break") | |
| 178 | + c.pop | |
| 179 | + c.line("return " & res) | |
| 180 | + c.pop | |
| 181 | + c.line("else:") | |
| 182 | + c.push | |
| 183 | + c.line("err(\"Wrong number of args (\" & $" & argsIdent & ".len & \") passed to " & | |
| 184 | + (if name.len > 0: name else: "fn") & "\")") | |
| 185 | + c.pop | |
| 186 | + c.pop | |
| 187 | + c.line(")") | |
| 188 | + | |
| 189 | +proc genFnForm(args: seq[Value], env: Env, c: Ctx, dst: string, defName: string) = | |
| 190 | + ## (fn name? [params] body...) or (fn name? ([params] body...) ...) | |
| 191 | + var i = 0 | |
| 192 | + var name = defName | |
| 193 | + var selfIdent = "" | |
| 194 | + if i < args.len and not args[i].isNil and args[i].kind == kSymbol: | |
| 195 | + name = symName(args[i]); inc i | |
| 196 | + var clauses: seq[FnClause] = @[] | |
| 197 | + if i < args.len and args[i].kind == kVector: | |
| 198 | + var cl = parseParams(args[i]) | |
| 199 | + cl.body = args[i + 1 .. ^1] | |
| 200 | + clauses.add cl | |
| 201 | + else: | |
| 202 | + while i < args.len: | |
| 203 | + let cf = args[i] | |
| 204 | + if cf.kind != kList or cf.items.len == 0: err("Bad fn arity form: " & prStr(cf)) | |
| 205 | + var cl = parseParams(cf.items[0]) | |
| 206 | + cl.body = cf.items[1 .. ^1] | |
| 207 | + clauses.add cl | |
| 208 | + inc i | |
| 209 | + if clauses.len == 0: err("fn requires at least one arity") | |
| 210 | + if name.len > 0: | |
| 211 | + # bind the fn to a local so it can recur by name | |
| 212 | + selfIdent = c.gensym("self" & mangle(name)) | |
| 213 | + c.line("var " & selfIdent & ": Value = NilV") | |
| 214 | + genFn(name, clauses, selfIdent, env, c, selfIdent) | |
| 215 | + c.line(dst & " = " & selfIdent) | |
| 216 | + else: | |
| 217 | + genFn("fn", clauses, "", env, c, dst) | |
| 218 | + | |
| 219 | +proc genLet(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = | |
| 220 | + if bindings.isNil or bindings.kind != kVector: | |
| 221 | + err("let requires a vector for its bindings") | |
| 222 | + if bindings.items.len mod 2 != 0: | |
| 223 | + err("let requires an even number of forms in its binding vector") | |
| 224 | + let lenv = newEnv(env) | |
| 225 | + var i = 0 | |
| 226 | + while i < bindings.items.len: | |
| 227 | + let target = bindings.items[i] | |
| 228 | + let initForm = bindings.items[i + 1] | |
| 229 | + let v = genExpr(initForm, lenv, c) | |
| 230 | + if target.kind == kSymbol: | |
| 231 | + let id = c.gensym("l" & mangle(target.s)) | |
| 232 | + c.line("var " & id & ": Value = " & v) | |
| 233 | + lenv.locals[target.s] = id | |
| 234 | + elif target.kind == kVector: | |
| 235 | + # sequential destructuring: [a b & rest] | |
| 236 | + var idx = 0 | |
| 237 | + var j = 0 | |
| 238 | + while j < target.items.len: | |
| 239 | + let p = target.items[j] | |
| 240 | + if isSym(p, "&"): | |
| 241 | + let restSym = symName(target.items[j + 1]) | |
| 242 | + let id = c.gensym("l" & mangle(restSym)) | |
| 243 | + c.line("var " & id & ": Value = mkList(toSeq(" & v & ")[min(" & $idx & | |
| 244 | + ", toSeq(" & v & ").len) .. ^1])") | |
| 245 | + lenv.locals[restSym] = id | |
| 246 | + break | |
| 247 | + let id = c.gensym("l" & mangle(symName(p))) | |
| 248 | + c.line("var " & id & ": Value = call(getVar(\"nth\"), @[" & v & ", mkInt(" & | |
| 249 | + $idx & "), NilV])") | |
| 250 | + lenv.locals[symName(p)] = id | |
| 251 | + inc idx; inc j | |
| 252 | + elif target.kind == kMap: | |
| 253 | + # associative destructuring: {a :a, :keys [b c]} | |
| 254 | + for (k, valForm) in target.pairs: | |
| 255 | + if k.kind == kKeyword and k.s == "keys": | |
| 256 | + for ks in valForm.items: | |
| 257 | + let nm = symName(ks) | |
| 258 | + let id = c.gensym("l" & mangle(nm)) | |
| 259 | + c.line("var " & id & ": Value = call(getVar(\"get\"), @[" & v & | |
| 260 | + ", mkKeyword(" & nimStr(nm) & ")])") | |
| 261 | + lenv.locals[nm] = id | |
| 262 | + else: | |
| 263 | + let nm = symName(k) | |
| 264 | + let id = c.gensym("l" & mangle(nm)) | |
| 265 | + let kv = genExpr(valForm, lenv, c) | |
| 266 | + c.line("var " & id & ": Value = call(getVar(\"get\"), @[" & v & ", " & kv & "])") | |
| 267 | + lenv.locals[nm] = id | |
| 268 | + else: | |
| 269 | + err("Unsupported binding form: " & prStr(target)) | |
| 270 | + i += 2 | |
| 271 | + genBody(body, dst, lenv, c) | |
| 272 | + | |
| 273 | +proc genLoop(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = | |
| 274 | + if bindings.isNil or bindings.kind != kVector or bindings.items.len mod 2 != 0: | |
| 275 | + err("loop requires an even-sized binding vector") | |
| 276 | + let lenv = newEnv(env) | |
| 277 | + var idents: seq[string] = @[] | |
| 278 | + var i = 0 | |
| 279 | + while i < bindings.items.len: | |
| 280 | + let nm = symName(bindings.items[i]) | |
| 281 | + let v = genExpr(bindings.items[i + 1], lenv, c) | |
| 282 | + let id = c.gensym("l" & mangle(nm)) | |
| 283 | + c.line("var " & id & ": Value = " & v) | |
| 284 | + lenv.locals[nm] = id | |
| 285 | + idents.add id | |
| 286 | + i += 2 | |
| 287 | + c.line("while true:") | |
| 288 | + c.push | |
| 289 | + c.recurStack.add idents | |
| 290 | + genBody(body, dst, lenv, c) | |
| 291 | + discard c.recurStack.pop | |
| 292 | + c.line("break") | |
| 293 | + c.pop | |
| 294 | + | |
| 295 | +proc genCall(f: Value, args: seq[Value], dst: string, env: Env, c: Ctx) = | |
| 296 | + let fv = genExpr(f, env, c) | |
| 297 | + var argIdents: seq[string] = @[] | |
| 298 | + for a in args: argIdents.add genExpr(a, env, c) | |
| 299 | + if argIdents.len == 0: | |
| 300 | + c.line(dst & " = call(" & fv & ", emptyArgs)") | |
| 301 | + else: | |
| 302 | + c.line(dst & " = call(" & fv & ", @[" & argIdents.join(", ") & "])") | |
| 303 | + | |
| 304 | +proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | |
| 305 | + if f.isNil: | |
| 306 | + c.line(dst & " = NilV"); return | |
| 307 | + case f.kind | |
| 308 | + of kNil, kBool, kInt, kFloat, kStr, kKeyword: | |
| 309 | + c.line(dst & " = " & quoteLit(f)) | |
| 310 | + of kSymbol: | |
| 311 | + let local = env.lookup(f.s) | |
| 312 | + if local.len > 0: c.line(dst & " = " & local) | |
| 313 | + else: c.line(dst & " = cellGet(" & c.cellFor(f.s) & ")") | |
| 314 | + of kVector: | |
| 315 | + var ids: seq[string] = @[] | |
| 316 | + for x in f.items: ids.add genExpr(x, env, c) | |
| 317 | + c.line(dst & " = mkVector(" & | |
| 318 | + (if ids.len == 0: "newSeq[Value]()" else: "@[" & ids.join(", ") & "]") & ")") | |
| 319 | + of kSet: | |
| 320 | + var ids: seq[string] = @[] | |
| 321 | + for x in f.items: ids.add genExpr(x, env, c) | |
| 322 | + c.line(dst & " = mkSet(" & | |
| 323 | + (if ids.len == 0: "newSeq[Value]()" else: "@[" & ids.join(", ") & "]") & ")") | |
| 324 | + of kMap: | |
| 325 | + var parts: seq[string] = @[] | |
| 326 | + for (k, v) in f.pairs: | |
| 327 | + let ki = genExpr(k, env, c) | |
| 328 | + let vi = genExpr(v, env, c) | |
| 329 | + parts.add "(" & ki & ", " & vi & ")" | |
| 330 | + c.line(dst & " = mkMap(" & | |
| 331 | + (if parts.len == 0: "newSeq[(Value, Value)]()" else: "@[" & parts.join(", ") & "]") & ")") | |
| 332 | + of kFn: | |
| 333 | + err("Can't emit a function literal") | |
| 334 | + of kList: | |
| 335 | + if f.items.len == 0: | |
| 336 | + c.line(dst & " = mkList(newSeq[Value]())"); return | |
| 337 | + let head = f.items[0] | |
| 338 | + let args = f.items[1 .. ^1] | |
| 339 | + if head.kind == kSymbol: | |
| 340 | + case head.s | |
| 341 | + of "quote": | |
| 342 | + c.line(dst & " = " & quoteLit(args[0])) | |
| 343 | + return | |
| 344 | + of "if": | |
| 345 | + if args.len < 2: err("Too few arguments to if") | |
| 346 | + let cv = genExpr(args[0], env, c) | |
| 347 | + c.line("if truthy(" & cv & "):") | |
| 348 | + c.push; genInto(args[1], dst, env, c); c.pop | |
| 349 | + c.line("else:") | |
| 350 | + c.push | |
| 351 | + if args.len > 2: genInto(args[2], dst, env, c) | |
| 352 | + else: c.line(dst & " = NilV") | |
| 353 | + c.pop | |
| 354 | + return | |
| 355 | + of "do": | |
| 356 | + genBody(args, dst, env, c) | |
| 357 | + return | |
| 358 | + of "let", "let*": | |
| 359 | + if args.len == 0: err("let requires bindings") | |
| 360 | + genLet(args[0], args[1 .. ^1], dst, env, c) | |
| 361 | + return | |
| 362 | + of "loop", "loop*": | |
| 363 | + if args.len == 0: err("loop requires bindings") | |
| 364 | + genLoop(args[0], args[1 .. ^1], dst, env, c) | |
| 365 | + return | |
| 366 | + of "recur": | |
| 367 | + if c.recurStack.len == 0: err("recur outside of loop or fn") | |
| 368 | + let targets = c.recurStack[^1] | |
| 369 | + if targets.len != args.len: | |
| 370 | + err("Mismatched argument count to recur: expected " & $targets.len & | |
| 371 | + ", got " & $args.len) | |
| 372 | + var tmps: seq[string] = @[] | |
| 373 | + for a in args: tmps.add genExpr(a, env, c) | |
| 374 | + for i, t in tmps: c.line(targets[i] & " = " & t) | |
| 375 | + c.line("continue") | |
| 376 | + return | |
| 377 | + of "fn", "fn*": | |
| 378 | + genFnForm(args, env, c, dst, "") | |
| 379 | + return | |
| 380 | + of "def": | |
| 381 | + if args.len == 0: err("def requires a name") | |
| 382 | + let nm = symName(args[0]) | |
| 383 | + c.defined.incl nm | |
| 384 | + var body = args[1 .. ^1] | |
| 385 | + # drop a docstring: (def x "doc" val) / (defn ...) handled separately | |
| 386 | + if body.len == 0: | |
| 387 | + c.line(dst & " = setVar(" & nimStr(nm) & ", NilV)") | |
| 388 | + else: | |
| 389 | + let v = genExpr(body[^1], env, c) | |
| 390 | + c.line(dst & " = setVar(" & nimStr(nm) & ", " & v & ")") | |
| 391 | + return | |
| 392 | + of "defn", "defn-": | |
| 393 | + if args.len < 2: err("defn requires a name and a parameter vector") | |
| 394 | + let nm = symName(args[0]) | |
| 395 | + c.defined.incl nm | |
| 396 | + var rest = args[1 .. ^1] | |
| 397 | + if rest.len > 0 and rest[0].kind == kStr: rest = rest[1 .. ^1] # docstring | |
| 398 | + if rest.len > 0 and rest[0].kind == kMap: rest = rest[1 .. ^1] # attr map | |
| 399 | + let fv = c.gensym("fn") | |
| 400 | + c.line("var " & fv & ": Value = NilV") | |
| 401 | + genFnForm(rest, env, c, fv, nm) | |
| 402 | + c.line(dst & " = setVar(" & nimStr(nm) & ", " & fv & ")") | |
| 403 | + return | |
| 404 | + of "defmacro": | |
| 405 | + err("defmacro is not supported yet (clonim expands a fixed macro set)") | |
| 406 | + of "and": | |
| 407 | + if args.len == 0: c.line(dst & " = TrueV"); return | |
| 408 | + c.line(dst & " = TrueV") | |
| 409 | + var depth = 0 | |
| 410 | + for i, a in args: | |
| 411 | + genInto(a, dst, env, c) | |
| 412 | + if i < args.len - 1: | |
| 413 | + c.line("if truthy(" & dst & "):") | |
| 414 | + c.push; inc depth | |
| 415 | + for _ in 0 ..< depth: c.pop | |
| 416 | + return | |
| 417 | + of "or": | |
| 418 | + if args.len == 0: c.line(dst & " = NilV"); return | |
| 419 | + c.line(dst & " = NilV") | |
| 420 | + var depth = 0 | |
| 421 | + for i, a in args: | |
| 422 | + genInto(a, dst, env, c) | |
| 423 | + if i < args.len - 1: | |
| 424 | + c.line("if not truthy(" & dst & "):") | |
| 425 | + c.push; inc depth | |
| 426 | + for _ in 0 ..< depth: c.pop | |
| 427 | + return | |
| 428 | + of "when": | |
| 429 | + if args.len == 0: err("when requires a test") | |
| 430 | + let cv = genExpr(args[0], env, c) | |
| 431 | + c.line("if truthy(" & cv & "):") | |
| 432 | + c.push; genBody(args[1 .. ^1], dst, env, c); c.pop | |
| 433 | + c.line("else:") | |
| 434 | + c.push; c.line(dst & " = NilV"); c.pop | |
| 435 | + return | |
| 436 | + of "when-not": | |
| 437 | + let cv = genExpr(args[0], env, c) | |
| 438 | + c.line("if not truthy(" & cv & "):") | |
| 439 | + c.push; genBody(args[1 .. ^1], dst, env, c); c.pop | |
| 440 | + c.line("else:") | |
| 441 | + c.push; c.line(dst & " = NilV"); c.pop | |
| 442 | + return | |
| 443 | + of "if-not": | |
| 444 | + let cv = genExpr(args[0], env, c) | |
| 445 | + c.line("if not truthy(" & cv & "):") | |
| 446 | + c.push; genInto(args[1], dst, env, c); c.pop | |
| 447 | + c.line("else:") | |
| 448 | + c.push | |
| 449 | + if args.len > 2: genInto(args[2], dst, env, c) else: c.line(dst & " = NilV") | |
| 450 | + c.pop | |
| 451 | + return | |
| 452 | + of "cond": | |
| 453 | + if args.len mod 2 != 0: err("cond requires an even number of forms") | |
| 454 | + c.line(dst & " = NilV") | |
| 455 | + var depth = 0 | |
| 456 | + var i = 0 | |
| 457 | + while i < args.len: | |
| 458 | + if isSym(args[i], "else") or (args[i].kind == kKeyword and args[i].s == "else"): | |
| 459 | + genInto(args[i + 1], dst, env, c) | |
| 460 | + break | |
| 461 | + let cv = genExpr(args[i], env, c) | |
| 462 | + c.line("if truthy(" & cv & "):") | |
| 463 | + c.push | |
| 464 | + genInto(args[i + 1], dst, env, c) | |
| 465 | + c.pop | |
| 466 | + c.line("else:") | |
| 467 | + c.push; inc depth | |
| 468 | + i += 2 | |
| 469 | + for _ in 0 ..< depth: c.pop | |
| 470 | + return | |
| 471 | + of "when-let", "if-let": | |
| 472 | + let b = args[0] | |
| 473 | + if b.kind != kVector or b.items.len != 2: err(head.s & " requires [sym test]") | |
| 474 | + let nm = symName(b.items[0]) | |
| 475 | + let tv = genExpr(b.items[1], env, c) | |
| 476 | + c.line("if truthy(" & tv & "):") | |
| 477 | + c.push | |
| 478 | + let benv = newEnv(env) | |
| 479 | + let id = c.gensym("l" & mangle(nm)) | |
| 480 | + c.line("var " & id & ": Value = " & tv) | |
| 481 | + benv.locals[nm] = id | |
| 482 | + if head.s == "when-let": genBody(args[1 .. ^1], dst, benv, c) | |
| 483 | + else: genInto(args[1], dst, benv, c) | |
| 484 | + c.pop | |
| 485 | + c.line("else:") | |
| 486 | + c.push | |
| 487 | + if head.s == "if-let" and args.len > 2: genInto(args[2], dst, env, c) | |
| 488 | + else: c.line(dst & " = NilV") | |
| 489 | + c.pop | |
| 490 | + return | |
| 491 | + of "->": | |
| 492 | + var acc = args[0] | |
| 493 | + for i in 1 ..< args.len: | |
| 494 | + let step = args[i] | |
| 495 | + if step.kind == kList: | |
| 496 | + acc = mkList(@[step.items[0], acc] & step.items[1 .. ^1]) | |
| 497 | + else: | |
| 498 | + acc = mkList(@[step, acc]) | |
| 499 | + genInto(acc, dst, env, c) | |
| 500 | + return | |
| 501 | + of "->>": | |
| 502 | + var acc = args[0] | |
| 503 | + for i in 1 ..< args.len: | |
| 504 | + let step = args[i] | |
| 505 | + if step.kind == kList: | |
| 506 | + acc = mkList(step.items & @[acc]) | |
| 507 | + else: | |
| 508 | + acc = mkList(@[step, acc]) | |
| 509 | + genInto(acc, dst, env, c) | |
| 510 | + return | |
| 511 | + of "doseq": | |
| 512 | + let b = args[0] | |
| 513 | + if b.kind != kVector or b.items.len != 2: err("doseq requires [sym coll]") | |
| 514 | + let nm = symName(b.items[0]) | |
| 515 | + let cv = genExpr(b.items[1], env, c) | |
| 516 | + let it = c.gensym("it") | |
| 517 | + c.line("for " & it & " in toSeq(" & cv & "):") | |
| 518 | + c.push | |
| 519 | + let benv = newEnv(env) | |
| 520 | + let id = c.gensym("l" & mangle(nm)) | |
| 521 | + c.line("var " & id & ": Value = " & it) | |
| 522 | + benv.locals[nm] = id | |
| 523 | + let throwaway = c.gensym("t") | |
| 524 | + c.line("var " & throwaway & ": Value = NilV") | |
| 525 | + genBody(args[1 .. ^1], throwaway, benv, c) | |
| 526 | + c.pop | |
| 527 | + c.line(dst & " = NilV") | |
| 528 | + return | |
| 529 | + of "dotimes": | |
| 530 | + let b = args[0] | |
| 531 | + if b.kind != kVector or b.items.len != 2: err("dotimes requires [sym n]") | |
| 532 | + let nm = symName(b.items[0]) | |
| 533 | + let cv = genExpr(b.items[1], env, c) | |
| 534 | + let it = c.gensym("i") | |
| 535 | + c.line("for " & it & " in 0 ..< int(" & cv & ".i):") | |
| 536 | + c.push | |
| 537 | + let benv = newEnv(env) | |
| 538 | + let id = c.gensym("l" & mangle(nm)) | |
| 539 | + c.line("var " & id & ": Value = mkInt(int64(" & it & "))") | |
| 540 | + benv.locals[nm] = id | |
| 541 | + let throwaway = c.gensym("t") | |
| 542 | + c.line("var " & throwaway & ": Value = NilV") | |
| 543 | + genBody(args[1 .. ^1], throwaway, benv, c) | |
| 544 | + c.pop | |
| 545 | + c.line(dst & " = NilV") | |
| 546 | + return | |
| 547 | + of "try": | |
| 548 | + var bodyForms: seq[Value] = @[] | |
| 549 | + var catchSym = "" | |
| 550 | + var catchBody: seq[Value] = @[] | |
| 551 | + var finallyBody: seq[Value] = @[] | |
| 552 | + for a in args: | |
| 553 | + if a.kind == kList and a.items.len > 0 and isSym(a.items[0], "catch"): | |
| 554 | + catchSym = symName(a.items[2]) | |
| 555 | + catchBody = a.items[3 .. ^1] | |
| 556 | + elif a.kind == kList and a.items.len > 0 and isSym(a.items[0], "finally"): | |
| 557 | + finallyBody = a.items[1 .. ^1] | |
| 558 | + else: | |
| 559 | + bodyForms.add a | |
| 560 | + c.line("try:") | |
| 561 | + c.push; genBody(bodyForms, dst, env, c); c.pop | |
| 562 | + if catchSym.len > 0: | |
| 563 | + c.line("except CatchableError as " & c.gensym("e") & "X:") | |
| 564 | + c.push | |
| 565 | + let benv = newEnv(env) | |
| 566 | + let id = c.gensym("l" & mangle(catchSym)) | |
| 567 | + c.line("var " & id & ": Value = mkStr(getCurrentExceptionMsg())") | |
| 568 | + benv.locals[catchSym] = id | |
| 569 | + genBody(catchBody, dst, benv, c) | |
| 570 | + c.pop | |
| 571 | + if finallyBody.len > 0: | |
| 572 | + c.line("finally:") | |
| 573 | + c.push | |
| 574 | + let throwaway = c.gensym("t") | |
| 575 | + c.line("var " & throwaway & ": Value = NilV") | |
| 576 | + genBody(finallyBody, throwaway, env, c) | |
| 577 | + c.pop | |
| 578 | + return | |
| 579 | + of "comment": | |
| 580 | + c.line(dst & " = NilV") | |
| 581 | + return | |
| 582 | + of "ns", "require", "in-ns", "use", "import", "set!", "declare": | |
| 583 | + c.line(dst & " = NilV") | |
| 584 | + return | |
| 585 | + else: discard | |
| 586 | + genCall(head, args, dst, env, c) | |
| 587 | + | |
| 588 | +# ------------------------------------------------------------- entry point | |
| 589 | +const preamble = """ | |
| 590 | +## Generated by clonim. Do not edit. | |
| 591 | +import runtime, core | |
| 592 | + | |
| 593 | +proc cljMain() = | |
| 594 | +""" | |
| 595 | + | |
| 596 | +proc compileForms*(forms: seq[Value]): string = | |
| 597 | + let c = Ctx(body: @[], indent: 1, counter: 0, recurStack: @[], | |
| 598 | + defined: initHashSet[string](), prelude: @[], | |
| 599 | + cells: initTable[string, string]()) | |
| 600 | + let env = newEnv() | |
| 601 | + for f in forms: | |
| 602 | + let t = c.gensym("top") | |
| 603 | + c.line("var " & t & ": Value = NilV") | |
| 604 | + genInto(f, t, env, c) | |
| 605 | + c.line("discard " & t) | |
| 606 | + var src = preamble & " registerCore()\n" & c.prelude.join("\n") & "\n" & | |
| 607 | + c.body.join("\n") & "\n\n" | |
| 608 | + src &= """ | |
| 609 | +when isMainModule: | |
| 610 | + try: | |
| 611 | + cljMain() | |
| 612 | + except CljError as e: | |
| 613 | + stderr.writeLine("clonim: " & e.msg) | |
| 614 | + quit(1) | |
| 615 | +""" | |
| 616 | + src | |
| 617 | + | |
| 618 | +proc compileSource*(src: string): string = | |
| 619 | + compileForms(readAll(src)) | |
| new file mode 100644 | |||
| @@ -0,0 +1,619 @@ | |||
| 1 | +## clonim compiler — Clojure forms -> Nim source. | ||
| 2 | +## | ||
| 3 | +## Shape borrowed from jank: read to data, analyze into a small set of core | ||
| 4 | +## special forms (everything else is expanded), then emit host-language code | ||
| 5 | +## and let the host compiler do register allocation, inlining and codegen. | ||
| 6 | +import std/[tables, strutils, sets] | ||
| 7 | +import runtime, reader | ||
| 8 | + | ||
| 9 | +type | ||
| 10 | + Env = ref object | ||
| 11 | + parent: Env | ||
| 12 | + locals: Table[string, string] # clojure name -> nim identifier | ||
| 13 | + | ||
| 14 | + Ctx = ref object | ||
| 15 | + body: seq[string] # emitted lines | ||
| 16 | + indent: int | ||
| 17 | + counter: int | ||
| 18 | + recurStack: seq[seq[string]] # nim idents of the enclosing recur target | ||
| 19 | + defined: HashSet[string] # names def'd so far (for nicer errors) | ||
| 20 | + prelude: seq[string] # hoisted var-cell resolutions | ||
| 21 | + cells: Table[string, string] # clojure var name -> nim cell ident | ||
| 22 | + | ||
| 23 | +proc newEnv(parent: Env = nil): Env = | ||
| 24 | + Env(parent: parent, locals: initTable[string, string]()) | ||
| 25 | + | ||
| 26 | +proc lookup(env: Env, name: string): string = | ||
| 27 | + var e = env | ||
| 28 | + while e != nil: | ||
| 29 | + if e.locals.hasKey(name): return e.locals[name] | ||
| 30 | + e = e.parent | ||
| 31 | + "" | ||
| 32 | + | ||
| 33 | +proc line(c: Ctx, s: string) = | ||
| 34 | + c.body.add repeat(" ", c.indent) & s | ||
| 35 | + | ||
| 36 | +proc push(c: Ctx) = inc c.indent | ||
| 37 | +proc pop(c: Ctx) = dec c.indent | ||
| 38 | + | ||
| 39 | +proc gensym(c: Ctx, prefix: string): string = | ||
| 40 | + inc c.counter | ||
| 41 | + prefix & "_" & $c.counter | ||
| 42 | + | ||
| 43 | +proc mangle(name: string): string = | ||
| 44 | + result = "" | ||
| 45 | + for ch in name: | ||
| 46 | + if ch in {'a'..'z', 'A'..'Z', '0'..'9'}: result.add ch | ||
| 47 | + elif ch == '-': result.add 'X' | ||
| 48 | + else: result.add 'Y' | ||
| 49 | + if result.len == 0 or result[0] in {'0'..'9'}: result = "v" & result | ||
| 50 | + | ||
| 51 | +proc nimStr(s: string): string = | ||
| 52 | + result = "\"" | ||
| 53 | + for ch in s: | ||
| 54 | + case ch | ||
| 55 | + of '"': result.add "\\\"" | ||
| 56 | + of '\\': result.add "\\\\" | ||
| 57 | + of '\n': result.add "\\n" | ||
| 58 | + of '\t': result.add "\\t" | ||
| 59 | + of '\r': result.add "\\r" | ||
| 60 | + else: | ||
| 61 | + if ch.ord < 32: result.add "\\x" & toHex(ch.ord, 2) | ||
| 62 | + else: result.add ch | ||
| 63 | + result.add "\"" | ||
| 64 | + | ||
| 65 | +proc cellFor(c: Ctx, name: string): string = | ||
| 66 | + ## Resolve each referenced var exactly once, at program start. | ||
| 67 | + if c.cells.hasKey(name): return c.cells[name] | ||
| 68 | + inc c.counter | ||
| 69 | + result = "c_" & $c.counter | ||
| 70 | + c.cells[name] = result | ||
| 71 | + c.prelude.add " let " & result & " = varCell(" & nimStr(name) & ")" | ||
| 72 | + | ||
| 73 | +proc isSym(v: Value, name: string): bool = | ||
| 74 | + not v.isNil and v.kind == kSymbol and v.s == name | ||
| 75 | + | ||
| 76 | +proc symName(v: Value): string = | ||
| 77 | + if v.isNil or v.kind != kSymbol: err("Expected a symbol, got: " & prStr(v)) | ||
| 78 | + v.s | ||
| 79 | + | ||
| 80 | +# ------------------------------------------------------------- quoted data | ||
| 81 | +proc quoteLit(v: Value): string = | ||
| 82 | + if v.isNil: return "NilV" | ||
| 83 | + case v.kind | ||
| 84 | + of kNil: "NilV" | ||
| 85 | + of kBool: (if v.b: "TrueV" else: "FalseV") | ||
| 86 | + of kInt: "mkInt(" & $v.i & ")" | ||
| 87 | + of kFloat: "mkFloat(" & $v.f & ")" | ||
| 88 | + of kStr: "mkStr(" & nimStr(v.s) & ")" | ||
| 89 | + of kKeyword: "mkKeyword(" & nimStr(v.s) & ")" | ||
| 90 | + of kSymbol: "mkSymbol(" & nimStr(v.s) & ")" | ||
| 91 | + of kList, kVector, kSet: | ||
| 92 | + var parts: seq[string] = @[] | ||
| 93 | + for x in v.items: parts.add quoteLit(x) | ||
| 94 | + let ctor = (case v.kind | ||
| 95 | + of kList: "mkList" | ||
| 96 | + of kVector: "mkVector" | ||
| 97 | + else: "mkSet") | ||
| 98 | + ctor & "(@[" & parts.join(", ") & "])" & | ||
| 99 | + (if parts.len == 0: "" else: "") | ||
| 100 | + of kMap: | ||
| 101 | + var parts: seq[string] = @[] | ||
| 102 | + for (k, val) in v.pairs: parts.add "(" & quoteLit(k) & ", " & quoteLit(val) & ")" | ||
| 103 | + "mkMap(@[" & parts.join(", ") & "])" | ||
| 104 | + of kFn: err("Can't quote a function") | ||
| 105 | + | ||
| 106 | +proc emptySeqFix(s: string, elemType: string): string = | ||
| 107 | + ## `@[]` has no inferable element type in Nim; annotate it. | ||
| 108 | + s.replace("@[]", "newSeq[" & elemType & "]()") | ||
| 109 | + | ||
| 110 | +# ------------------------------------------------------------- code gen | ||
| 111 | +proc genInto(f: Value, dst: string, env: Env, c: Ctx) | ||
| 112 | + | ||
| 113 | +proc genExpr(f: Value, env: Env, c: Ctx): string = | ||
| 114 | + result = c.gensym("t") | ||
| 115 | + c.line("var " & result & ": Value = NilV") | ||
| 116 | + genInto(f, result, env, c) | ||
| 117 | + | ||
| 118 | +proc genBody(forms: seq[Value], dst: string, env: Env, c: Ctx) = | ||
| 119 | + if forms.len == 0: | ||
| 120 | + c.line(dst & " = NilV") | ||
| 121 | + return | ||
| 122 | + for i in 0 ..< forms.len - 1: | ||
| 123 | + discard genExpr(forms[i], env, c) | ||
| 124 | + genInto(forms[^1], dst, env, c) | ||
| 125 | + | ||
| 126 | +type | ||
| 127 | + FnClause = object | ||
| 128 | + params: seq[string] | ||
| 129 | + restParam: string | ||
| 130 | + body: seq[Value] | ||
| 131 | + | ||
| 132 | +proc parseParams(v: Value): FnClause = | ||
| 133 | + if v.isNil or v.kind != kVector: err("Parameter list must be a vector, got: " & prStr(v)) | ||
| 134 | + result = FnClause(params: @[], restParam: "", body: @[]) | ||
| 135 | + var i = 0 | ||
| 136 | + while i < v.items.len: | ||
| 137 | + let p = v.items[i] | ||
| 138 | + if isSym(p, "&"): | ||
| 139 | + if i + 1 >= v.items.len: err("Missing symbol after &") | ||
| 140 | + result.restParam = symName(v.items[i + 1]) | ||
| 141 | + break | ||
| 142 | + result.params.add symName(p) | ||
| 143 | + inc i | ||
| 144 | + | ||
| 145 | +proc genFn(name: string, clauses: seq[FnClause], selfIdent: string, env: Env, c: Ctx, dst: string) = | ||
| 146 | + let argsIdent = c.gensym("args") | ||
| 147 | + c.line(dst & " = mkFn(" & nimStr(name) & ", proc (" & argsIdent & ": seq[Value]): Value =") | ||
| 148 | + c.push | ||
| 149 | + var first = true | ||
| 150 | + for cl in clauses: | ||
| 151 | + let cond = | ||
| 152 | + if cl.restParam.len > 0: argsIdent & ".len >= " & $cl.params.len | ||
| 153 | + else: argsIdent & ".len == " & $cl.params.len | ||
| 154 | + c.line((if first: "if " else: "elif ") & cond & ":") | ||
| 155 | + first = false | ||
| 156 | + c.push | ||
| 157 | + let fenv = newEnv(env) | ||
| 158 | + if selfIdent.len > 0 and name.len > 0: | ||
| 159 | + fenv.locals[name] = selfIdent | ||
| 160 | + var recurIdents: seq[string] = @[] | ||
| 161 | + for i, p in cl.params: | ||
| 162 | + let id = c.gensym("p" & mangle(p)) | ||
| 163 | + c.line("var " & id & ": Value = argAt(" & argsIdent & ", " & $i & ")") | ||
| 164 | + fenv.locals[p] = id | ||
| 165 | + recurIdents.add id | ||
| 166 | + if cl.restParam.len > 0: | ||
| 167 | + let id = c.gensym("p" & mangle(cl.restParam)) | ||
| 168 | + c.line("var " & id & ": Value = restArgs(" & argsIdent & ", " & $cl.params.len & ")") | ||
| 169 | + fenv.locals[cl.restParam] = id | ||
| 170 | + let res = c.gensym("res") | ||
| 171 | + c.line("var " & res & ": Value = NilV") | ||
| 172 | + c.line("while true:") | ||
| 173 | + c.push | ||
| 174 | + c.recurStack.add recurIdents | ||
| 175 | + genBody(cl.body, res, fenv, c) | ||
| 176 | + discard c.recurStack.pop | ||
| 177 | + c.line("break") | ||
| 178 | + c.pop | ||
| 179 | + c.line("return " & res) | ||
| 180 | + c.pop | ||
| 181 | + c.line("else:") | ||
| 182 | + c.push | ||
| 183 | + c.line("err(\"Wrong number of args (\" & $" & argsIdent & ".len & \") passed to " & | ||
| 184 | + (if name.len > 0: name else: "fn") & "\")") | ||
| 185 | + c.pop | ||
| 186 | + c.pop | ||
| 187 | + c.line(")") | ||
| 188 | + | ||
| 189 | +proc genFnForm(args: seq[Value], env: Env, c: Ctx, dst: string, defName: string) = | ||
| 190 | + ## (fn name? [params] body...) or (fn name? ([params] body...) ...) | ||
| 191 | + var i = 0 | ||
| 192 | + var name = defName | ||
| 193 | + var selfIdent = "" | ||
| 194 | + if i < args.len and not args[i].isNil and args[i].kind == kSymbol: | ||
| 195 | + name = symName(args[i]); inc i | ||
| 196 | + var clauses: seq[FnClause] = @[] | ||
| 197 | + if i < args.len and args[i].kind == kVector: | ||
| 198 | + var cl = parseParams(args[i]) | ||
| 199 | + cl.body = args[i + 1 .. ^1] | ||
| 200 | + clauses.add cl | ||
| 201 | + else: | ||
| 202 | + while i < args.len: | ||
| 203 | + let cf = args[i] | ||
| 204 | + if cf.kind != kList or cf.items.len == 0: err("Bad fn arity form: " & prStr(cf)) | ||
| 205 | + var cl = parseParams(cf.items[0]) | ||
| 206 | + cl.body = cf.items[1 .. ^1] | ||
| 207 | + clauses.add cl | ||
| 208 | + inc i | ||
| 209 | + if clauses.len == 0: err("fn requires at least one arity") | ||
| 210 | + if name.len > 0: | ||
| 211 | + # bind the fn to a local so it can recur by name | ||
| 212 | + selfIdent = c.gensym("self" & mangle(name)) | ||
| 213 | + c.line("var " & selfIdent & ": Value = NilV") | ||
| 214 | + genFn(name, clauses, selfIdent, env, c, selfIdent) | ||
| 215 | + c.line(dst & " = " & selfIdent) | ||
| 216 | + else: | ||
| 217 | + genFn("fn", clauses, "", env, c, dst) | ||
| 218 | + | ||
| 219 | +proc genLet(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = | ||
| 220 | + if bindings.isNil or bindings.kind != kVector: | ||
| 221 | + err("let requires a vector for its bindings") | ||
| 222 | + if bindings.items.len mod 2 != 0: | ||
| 223 | + err("let requires an even number of forms in its binding vector") | ||
| 224 | + let lenv = newEnv(env) | ||
| 225 | + var i = 0 | ||
| 226 | + while i < bindings.items.len: | ||
| 227 | + let target = bindings.items[i] | ||
| 228 | + let initForm = bindings.items[i + 1] | ||
| 229 | + let v = genExpr(initForm, lenv, c) | ||
| 230 | + if target.kind == kSymbol: | ||
| 231 | + let id = c.gensym("l" & mangle(target.s)) | ||
| 232 | + c.line("var " & id & ": Value = " & v) | ||
| 233 | + lenv.locals[target.s] = id | ||
| 234 | + elif target.kind == kVector: | ||
| 235 | + # sequential destructuring: [a b & rest] | ||
| 236 | + var idx = 0 | ||
| 237 | + var j = 0 | ||
| 238 | + while j < target.items.len: | ||
| 239 | + let p = target.items[j] | ||
| 240 | + if isSym(p, "&"): | ||
| 241 | + let restSym = symName(target.items[j + 1]) | ||
| 242 | + let id = c.gensym("l" & mangle(restSym)) | ||
| 243 | + c.line("var " & id & ": Value = mkList(toSeq(" & v & ")[min(" & $idx & | ||
| 244 | + ", toSeq(" & v & ").len) .. ^1])") | ||
| 245 | + lenv.locals[restSym] = id | ||
| 246 | + break | ||
| 247 | + let id = c.gensym("l" & mangle(symName(p))) | ||
| 248 | + c.line("var " & id & ": Value = call(getVar(\"nth\"), @[" & v & ", mkInt(" & | ||
| 249 | + $idx & "), NilV])") | ||
| 250 | + lenv.locals[symName(p)] = id | ||
| 251 | + inc idx; inc j | ||
| 252 | + elif target.kind == kMap: | ||
| 253 | + # associative destructuring: {a :a, :keys [b c]} | ||
| 254 | + for (k, valForm) in target.pairs: | ||
| 255 | + if k.kind == kKeyword and k.s == "keys": | ||
| 256 | + for ks in valForm.items: | ||
| 257 | + let nm = symName(ks) | ||
| 258 | + let id = c.gensym("l" & mangle(nm)) | ||
| 259 | + c.line("var " & id & ": Value = call(getVar(\"get\"), @[" & v & | ||
| 260 | + ", mkKeyword(" & nimStr(nm) & ")])") | ||
| 261 | + lenv.locals[nm] = id | ||
| 262 | + else: | ||
| 263 | + let nm = symName(k) | ||
| 264 | + let id = c.gensym("l" & mangle(nm)) | ||
| 265 | + let kv = genExpr(valForm, lenv, c) | ||
| 266 | + c.line("var " & id & ": Value = call(getVar(\"get\"), @[" & v & ", " & kv & "])") | ||
| 267 | + lenv.locals[nm] = id | ||
| 268 | + else: | ||
| 269 | + err("Unsupported binding form: " & prStr(target)) | ||
| 270 | + i += 2 | ||
| 271 | + genBody(body, dst, lenv, c) | ||
| 272 | + | ||
| 273 | +proc genLoop(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = | ||
| 274 | + if bindings.isNil or bindings.kind != kVector or bindings.items.len mod 2 != 0: | ||
| 275 | + err("loop requires an even-sized binding vector") | ||
| 276 | + let lenv = newEnv(env) | ||
| 277 | + var idents: seq[string] = @[] | ||
| 278 | + var i = 0 | ||
| 279 | + while i < bindings.items.len: | ||
| 280 | + let nm = symName(bindings.items[i]) | ||
| 281 | + let v = genExpr(bindings.items[i + 1], lenv, c) | ||
| 282 | + let id = c.gensym("l" & mangle(nm)) | ||
| 283 | + c.line("var " & id & ": Value = " & v) | ||
| 284 | + lenv.locals[nm] = id | ||
| 285 | + idents.add id | ||
| 286 | + i += 2 | ||
| 287 | + c.line("while true:") | ||
| 288 | + c.push | ||
| 289 | + c.recurStack.add idents | ||
| 290 | + genBody(body, dst, lenv, c) | ||
| 291 | + discard c.recurStack.pop | ||
| 292 | + c.line("break") | ||
| 293 | + c.pop | ||
| 294 | + | ||
| 295 | +proc genCall(f: Value, args: seq[Value], dst: string, env: Env, c: Ctx) = | ||
| 296 | + let fv = genExpr(f, env, c) | ||
| 297 | + var argIdents: seq[string] = @[] | ||
| 298 | + for a in args: argIdents.add genExpr(a, env, c) | ||
| 299 | + if argIdents.len == 0: | ||
| 300 | + c.line(dst & " = call(" & fv & ", emptyArgs)") | ||
| 301 | + else: | ||
| 302 | + c.line(dst & " = call(" & fv & ", @[" & argIdents.join(", ") & "])") | ||
| 303 | + | ||
| 304 | +proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | ||
| 305 | + if f.isNil: | ||
| 306 | + c.line(dst & " = NilV"); return | ||
| 307 | + case f.kind | ||
| 308 | + of kNil, kBool, kInt, kFloat, kStr, kKeyword: | ||
| 309 | + c.line(dst & " = " & quoteLit(f)) | ||
| 310 | + of kSymbol: | ||
| 311 | + let local = env.lookup(f.s) | ||
| 312 | + if local.len > 0: c.line(dst & " = " & local) | ||
| 313 | + else: c.line(dst & " = cellGet(" & c.cellFor(f.s) & ")") | ||
| 314 | + of kVector: | ||
| 315 | + var ids: seq[string] = @[] | ||
| 316 | + for x in f.items: ids.add genExpr(x, env, c) | ||
| 317 | + c.line(dst & " = mkVector(" & | ||
| 318 | + (if ids.len == 0: "newSeq[Value]()" else: "@[" & ids.join(", ") & "]") & ")") | ||
| 319 | + of kSet: | ||
| 320 | + var ids: seq[string] = @[] | ||
| 321 | + for x in f.items: ids.add genExpr(x, env, c) | ||
| 322 | + c.line(dst & " = mkSet(" & | ||
| 323 | + (if ids.len == 0: "newSeq[Value]()" else: "@[" & ids.join(", ") & "]") & ")") | ||
| 324 | + of kMap: | ||
| 325 | + var parts: seq[string] = @[] | ||
| 326 | + for (k, v) in f.pairs: | ||
| 327 | + let ki = genExpr(k, env, c) | ||
| 328 | + let vi = genExpr(v, env, c) | ||
| 329 | + parts.add "(" & ki & ", " & vi & ")" | ||
| 330 | + c.line(dst & " = mkMap(" & | ||
| 331 | + (if parts.len == 0: "newSeq[(Value, Value)]()" else: "@[" & parts.join(", ") & "]") & ")") | ||
| 332 | + of kFn: | ||
| 333 | + err("Can't emit a function literal") | ||
| 334 | + of kList: | ||
| 335 | + if f.items.len == 0: | ||
| 336 | + c.line(dst & " = mkList(newSeq[Value]())"); return | ||
| 337 | + let head = f.items[0] | ||
| 338 | + let args = f.items[1 .. ^1] | ||
| 339 | + if head.kind == kSymbol: | ||
| 340 | + case head.s | ||
| 341 | + of "quote": | ||
| 342 | + c.line(dst & " = " & quoteLit(args[0])) | ||
| 343 | + return | ||
| 344 | + of "if": | ||
| 345 | + if args.len < 2: err("Too few arguments to if") | ||
| 346 | + let cv = genExpr(args[0], env, c) | ||
| 347 | + c.line("if truthy(" & cv & "):") | ||
| 348 | + c.push; genInto(args[1], dst, env, c); c.pop | ||
| 349 | + c.line("else:") | ||
| 350 | + c.push | ||
| 351 | + if args.len > 2: genInto(args[2], dst, env, c) | ||
| 352 | + else: c.line(dst & " = NilV") | ||
| 353 | + c.pop | ||
| 354 | + return | ||
| 355 | + of "do": | ||
| 356 | + genBody(args, dst, env, c) | ||
| 357 | + return | ||
| 358 | + of "let", "let*": | ||
| 359 | + if args.len == 0: err("let requires bindings") | ||
| 360 | + genLet(args[0], args[1 .. ^1], dst, env, c) | ||
| 361 | + return | ||
| 362 | + of "loop", "loop*": | ||
| 363 | + if args.len == 0: err("loop requires bindings") | ||
| 364 | + genLoop(args[0], args[1 .. ^1], dst, env, c) | ||
| 365 | + return | ||
| 366 | + of "recur": | ||
| 367 | + if c.recurStack.len == 0: err("recur outside of loop or fn") | ||
| 368 | + let targets = c.recurStack[^1] | ||
| 369 | + if targets.len != args.len: | ||
| 370 | + err("Mismatched argument count to recur: expected " & $targets.len & | ||
| 371 | + ", got " & $args.len) | ||
| 372 | + var tmps: seq[string] = @[] | ||
| 373 | + for a in args: tmps.add genExpr(a, env, c) | ||
| 374 | + for i, t in tmps: c.line(targets[i] & " = " & t) | ||
| 375 | + c.line("continue") | ||
| 376 | + return | ||
| 377 | + of "fn", "fn*": | ||
| 378 | + genFnForm(args, env, c, dst, "") | ||
| 379 | + return | ||
| 380 | + of "def": | ||
| 381 | + if args.len == 0: err("def requires a name") | ||
| 382 | + let nm = symName(args[0]) | ||
| 383 | + c.defined.incl nm | ||
| 384 | + var body = args[1 .. ^1] | ||
| 385 | + # drop a docstring: (def x "doc" val) / (defn ...) handled separately | ||
| 386 | + if body.len == 0: | ||
| 387 | + c.line(dst & " = setVar(" & nimStr(nm) & ", NilV)") | ||
| 388 | + else: | ||
| 389 | + let v = genExpr(body[^1], env, c) | ||
| 390 | + c.line(dst & " = setVar(" & nimStr(nm) & ", " & v & ")") | ||
| 391 | + return | ||
| 392 | + of "defn", "defn-": | ||
| 393 | + if args.len < 2: err("defn requires a name and a parameter vector") | ||
| 394 | + let nm = symName(args[0]) | ||
| 395 | + c.defined.incl nm | ||
| 396 | + var rest = args[1 .. ^1] | ||
| 397 | + if rest.len > 0 and rest[0].kind == kStr: rest = rest[1 .. ^1] # docstring | ||
| 398 | + if rest.len > 0 and rest[0].kind == kMap: rest = rest[1 .. ^1] # attr map | ||
| 399 | + let fv = c.gensym("fn") | ||
| 400 | + c.line("var " & fv & ": Value = NilV") | ||
| 401 | + genFnForm(rest, env, c, fv, nm) | ||
| 402 | + c.line(dst & " = setVar(" & nimStr(nm) & ", " & fv & ")") | ||
| 403 | + return | ||
| 404 | + of "defmacro": | ||
| 405 | + err("defmacro is not supported yet (clonim expands a fixed macro set)") | ||
| 406 | + of "and": | ||
| 407 | + if args.len == 0: c.line(dst & " = TrueV"); return | ||
| 408 | + c.line(dst & " = TrueV") | ||
| 409 | + var depth = 0 | ||
| 410 | + for i, a in args: | ||
| 411 | + genInto(a, dst, env, c) | ||
| 412 | + if i < args.len - 1: | ||
| 413 | + c.line("if truthy(" & dst & "):") | ||
| 414 | + c.push; inc depth | ||
| 415 | + for _ in 0 ..< depth: c.pop | ||
| 416 | + return | ||
| 417 | + of "or": | ||
| 418 | + if args.len == 0: c.line(dst & " = NilV"); return | ||
| 419 | + c.line(dst & " = NilV") | ||
| 420 | + var depth = 0 | ||
| 421 | + for i, a in args: | ||
| 422 | + genInto(a, dst, env, c) | ||
| 423 | + if i < args.len - 1: | ||
| 424 | + c.line("if not truthy(" & dst & "):") | ||
| 425 | + c.push; inc depth | ||
| 426 | + for _ in 0 ..< depth: c.pop | ||
| 427 | + return | ||
| 428 | + of "when": | ||
| 429 | + if args.len == 0: err("when requires a test") | ||
| 430 | + let cv = genExpr(args[0], env, c) | ||
| 431 | + c.line("if truthy(" & cv & "):") | ||
| 432 | + c.push; genBody(args[1 .. ^1], dst, env, c); c.pop | ||
| 433 | + c.line("else:") | ||
| 434 | + c.push; c.line(dst & " = NilV"); c.pop | ||
| 435 | + return | ||
| 436 | + of "when-not": | ||
| 437 | + let cv = genExpr(args[0], env, c) | ||
| 438 | + c.line("if not truthy(" & cv & "):") | ||
| 439 | + c.push; genBody(args[1 .. ^1], dst, env, c); c.pop | ||
| 440 | + c.line("else:") | ||
| 441 | + c.push; c.line(dst & " = NilV"); c.pop | ||
| 442 | + return | ||
| 443 | + of "if-not": | ||
| 444 | + let cv = genExpr(args[0], env, c) | ||
| 445 | + c.line("if not truthy(" & cv & "):") | ||
| 446 | + c.push; genInto(args[1], dst, env, c); c.pop | ||
| 447 | + c.line("else:") | ||
| 448 | + c.push | ||
| 449 | + if args.len > 2: genInto(args[2], dst, env, c) else: c.line(dst & " = NilV") | ||
| 450 | + c.pop | ||
| 451 | + return | ||
| 452 | + of "cond": | ||
| 453 | + if args.len mod 2 != 0: err("cond requires an even number of forms") | ||
| 454 | + c.line(dst & " = NilV") | ||
| 455 | + var depth = 0 | ||
| 456 | + var i = 0 | ||
| 457 | + while i < args.len: | ||
| 458 | + if isSym(args[i], "else") or (args[i].kind == kKeyword and args[i].s == "else"): | ||
| 459 | + genInto(args[i + 1], dst, env, c) | ||
| 460 | + break | ||
| 461 | + let cv = genExpr(args[i], env, c) | ||
| 462 | + c.line("if truthy(" & cv & "):") | ||
| 463 | + c.push | ||
| 464 | + genInto(args[i + 1], dst, env, c) | ||
| 465 | + c.pop | ||
| 466 | + c.line("else:") | ||
| 467 | + c.push; inc depth | ||
| 468 | + i += 2 | ||
| 469 | + for _ in 0 ..< depth: c.pop | ||
| 470 | + return | ||
| 471 | + of "when-let", "if-let": | ||
| 472 | + let b = args[0] | ||
| 473 | + if b.kind != kVector or b.items.len != 2: err(head.s & " requires [sym test]") | ||
| 474 | + let nm = symName(b.items[0]) | ||
| 475 | + let tv = genExpr(b.items[1], env, c) | ||
| 476 | + c.line("if truthy(" & tv & "):") | ||
| 477 | + c.push | ||
| 478 | + let benv = newEnv(env) | ||
| 479 | + let id = c.gensym("l" & mangle(nm)) | ||
| 480 | + c.line("var " & id & ": Value = " & tv) | ||
| 481 | + benv.locals[nm] = id | ||
| 482 | + if head.s == "when-let": genBody(args[1 .. ^1], dst, benv, c) | ||
| 483 | + else: genInto(args[1], dst, benv, c) | ||
| 484 | + c.pop | ||
| 485 | + c.line("else:") | ||
| 486 | + c.push | ||
| 487 | + if head.s == "if-let" and args.len > 2: genInto(args[2], dst, env, c) | ||
| 488 | + else: c.line(dst & " = NilV") | ||
| 489 | + c.pop | ||
| 490 | + return | ||
| 491 | + of "->": | ||
| 492 | + var acc = args[0] | ||
| 493 | + for i in 1 ..< args.len: | ||
| 494 | + let step = args[i] | ||
| 495 | + if step.kind == kList: | ||
| 496 | + acc = mkList(@[step.items[0], acc] & step.items[1 .. ^1]) | ||
| 497 | + else: | ||
| 498 | + acc = mkList(@[step, acc]) | ||
| 499 | + genInto(acc, dst, env, c) | ||
| 500 | + return | ||
| 501 | + of "->>": | ||
| 502 | + var acc = args[0] | ||
| 503 | + for i in 1 ..< args.len: | ||
| 504 | + let step = args[i] | ||
| 505 | + if step.kind == kList: | ||
| 506 | + acc = mkList(step.items & @[acc]) | ||
| 507 | + else: | ||
| 508 | + acc = mkList(@[step, acc]) | ||
| 509 | + genInto(acc, dst, env, c) | ||
| 510 | + return | ||
| 511 | + of "doseq": | ||
| 512 | + let b = args[0] | ||
| 513 | + if b.kind != kVector or b.items.len != 2: err("doseq requires [sym coll]") | ||
| 514 | + let nm = symName(b.items[0]) | ||
| 515 | + let cv = genExpr(b.items[1], env, c) | ||
| 516 | + let it = c.gensym("it") | ||
| 517 | + c.line("for " & it & " in toSeq(" & cv & "):") | ||
| 518 | + c.push | ||
| 519 | + let benv = newEnv(env) | ||
| 520 | + let id = c.gensym("l" & mangle(nm)) | ||
| 521 | + c.line("var " & id & ": Value = " & it) | ||
| 522 | + benv.locals[nm] = id | ||
| 523 | + let throwaway = c.gensym("t") | ||
| 524 | + c.line("var " & throwaway & ": Value = NilV") | ||
| 525 | + genBody(args[1 .. ^1], throwaway, benv, c) | ||
| 526 | + c.pop | ||
| 527 | + c.line(dst & " = NilV") | ||
| 528 | + return | ||
| 529 | + of "dotimes": | ||
| 530 | + let b = args[0] | ||
| 531 | + if b.kind != kVector or b.items.len != 2: err("dotimes requires [sym n]") | ||
| 532 | + let nm = symName(b.items[0]) | ||
| 533 | + let cv = genExpr(b.items[1], env, c) | ||
| 534 | + let it = c.gensym("i") | ||
| 535 | + c.line("for " & it & " in 0 ..< int(" & cv & ".i):") | ||
| 536 | + c.push | ||
| 537 | + let benv = newEnv(env) | ||
| 538 | + let id = c.gensym("l" & mangle(nm)) | ||
| 539 | + c.line("var " & id & ": Value = mkInt(int64(" & it & "))") | ||
| 540 | + benv.locals[nm] = id | ||
| 541 | + let throwaway = c.gensym("t") | ||
| 542 | + c.line("var " & throwaway & ": Value = NilV") | ||
| 543 | + genBody(args[1 .. ^1], throwaway, benv, c) | ||
| 544 | + c.pop | ||
| 545 | + c.line(dst & " = NilV") | ||
| 546 | + return | ||
| 547 | + of "try": | ||
| 548 | + var bodyForms: seq[Value] = @[] | ||
| 549 | + var catchSym = "" | ||
| 550 | + var catchBody: seq[Value] = @[] | ||
| 551 | + var finallyBody: seq[Value] = @[] | ||
| 552 | + for a in args: | ||
| 553 | + if a.kind == kList and a.items.len > 0 and isSym(a.items[0], "catch"): | ||
| 554 | + catchSym = symName(a.items[2]) | ||
| 555 | + catchBody = a.items[3 .. ^1] | ||
| 556 | + elif a.kind == kList and a.items.len > 0 and isSym(a.items[0], "finally"): | ||
| 557 | + finallyBody = a.items[1 .. ^1] | ||
| 558 | + else: | ||
| 559 | + bodyForms.add a | ||
| 560 | + c.line("try:") | ||
| 561 | + c.push; genBody(bodyForms, dst, env, c); c.pop | ||
| 562 | + if catchSym.len > 0: | ||
| 563 | + c.line("except CatchableError as " & c.gensym("e") & "X:") | ||
| 564 | + c.push | ||
| 565 | + let benv = newEnv(env) | ||
| 566 | + let id = c.gensym("l" & mangle(catchSym)) | ||
| 567 | + c.line("var " & id & ": Value = mkStr(getCurrentExceptionMsg())") | ||
| 568 | + benv.locals[catchSym] = id | ||
| 569 | + genBody(catchBody, dst, benv, c) | ||
| 570 | + c.pop | ||
| 571 | + if finallyBody.len > 0: | ||
| 572 | + c.line("finally:") | ||
| 573 | + c.push | ||
| 574 | + let throwaway = c.gensym("t") | ||
| 575 | + c.line("var " & throwaway & ": Value = NilV") | ||
| 576 | + genBody(finallyBody, throwaway, env, c) | ||
| 577 | + c.pop | ||
| 578 | + return | ||
| 579 | + of "comment": | ||
| 580 | + c.line(dst & " = NilV") | ||
| 581 | + return | ||
| 582 | + of "ns", "require", "in-ns", "use", "import", "set!", "declare": | ||
| 583 | + c.line(dst & " = NilV") | ||
| 584 | + return | ||
| 585 | + else: discard | ||
| 586 | + genCall(head, args, dst, env, c) | ||
| 587 | + | ||
| 588 | +# ------------------------------------------------------------- entry point | ||
| 589 | +const preamble = """ | ||
| 590 | +## Generated by clonim. Do not edit. | ||
| 591 | +import runtime, core | ||
| 592 | + | ||
| 593 | +proc cljMain() = | ||
| 594 | +""" | ||
| 595 | + | ||
| 596 | +proc compileForms*(forms: seq[Value]): string = | ||
| 597 | + let c = Ctx(body: @[], indent: 1, counter: 0, recurStack: @[], | ||
| 598 | + defined: initHashSet[string](), prelude: @[], | ||
| 599 | + cells: initTable[string, string]()) | ||
| 600 | + let env = newEnv() | ||
| 601 | + for f in forms: | ||
| 602 | + let t = c.gensym("top") | ||
| 603 | + c.line("var " & t & ": Value = NilV") | ||
| 604 | + genInto(f, t, env, c) | ||
| 605 | + c.line("discard " & t) | ||
| 606 | + var src = preamble & " registerCore()\n" & c.prelude.join("\n") & "\n" & | ||
| 607 | + c.body.join("\n") & "\n\n" | ||
| 608 | + src &= """ | ||
| 609 | +when isMainModule: | ||
| 610 | + try: | ||
| 611 | + cljMain() | ||
| 612 | + except CljError as e: | ||
| 613 | + stderr.writeLine("clonim: " & e.msg) | ||
| 614 | + quit(1) | ||
| 615 | +""" | ||
| 616 | + src | ||
| 617 | + | ||
| 618 | +proc compileSource*(src: string): string = | ||
| 619 | + compileForms(readAll(src)) | ||
added
src/core.nim +539 -0 | new file mode 100644 | ||
| @@ -0,0 +1,539 @@ | ||
| 1 | +## clonim core — clojure.core builtins, registered into the global var table. | |
| 2 | +import std/[strutils, math, times, random] | |
| 3 | +import runtime | |
| 4 | + | |
| 5 | +proc num(v: Value): float64 = | |
| 6 | + case v.kind | |
| 7 | + of kInt: float64(v.i) | |
| 8 | + of kFloat: v.f | |
| 9 | + else: err("Not a number: " & prStr(v)) | |
| 10 | + | |
| 11 | +proc isFloaty(vs: seq[Value]): bool = | |
| 12 | + for v in vs: | |
| 13 | + if v.kind == kFloat: return true | |
| 14 | + false | |
| 15 | + | |
| 16 | +proc intOf(v: Value): int64 = | |
| 17 | + case v.kind | |
| 18 | + of kInt: v.i | |
| 19 | + of kFloat: int64(v.f) | |
| 20 | + else: err("Not a number: " & prStr(v)) | |
| 21 | + | |
| 22 | +proc arith(name: string, args: seq[Value], unit: int64, | |
| 23 | + fi: proc (a, b: int64): int64, ff: proc (a, b: float64): float64): Value = | |
| 24 | + if args.len == 0: return mkInt(unit) | |
| 25 | + if isFloaty(args): | |
| 26 | + var acc = (if args.len == 1: float64(unit) else: num(args[0])) | |
| 27 | + let start = (if args.len == 1: 0 else: 1) | |
| 28 | + for i in start ..< args.len: acc = ff(acc, num(args[i])) | |
| 29 | + return mkFloat(acc) | |
| 30 | + var acc = (if args.len == 1: unit else: args[0].i) | |
| 31 | + let start = (if args.len == 1: 0 else: 1) | |
| 32 | + for i in start ..< args.len: acc = fi(acc, args[i].i) | |
| 33 | + mkInt(acc) | |
| 34 | + | |
| 35 | +proc cmpChain(args: seq[Value], ok: proc (c: int): bool): Value = | |
| 36 | + for i in 0 ..< args.len - 1: | |
| 37 | + let a = num(args[i]) | |
| 38 | + let b = num(args[i + 1]) | |
| 39 | + let c = (if a < b: -1 elif a > b: 1 else: 0) | |
| 40 | + if not ok(c): return FalseV | |
| 41 | + TrueV | |
| 42 | + | |
| 43 | +proc getIn(coll, k, dflt: Value): Value = | |
| 44 | + if coll.isNil or coll.kind == kNil: return dflt | |
| 45 | + case coll.kind | |
| 46 | + of kMap: | |
| 47 | + for (kk, vv) in coll.pairs: | |
| 48 | + if equals(kk, k): return vv | |
| 49 | + dflt | |
| 50 | + of kVector, kList: | |
| 51 | + if k.kind != kInt: return dflt | |
| 52 | + let i = int(k.i) | |
| 53 | + if i < 0 or i >= coll.items.len: dflt else: coll.items[i] | |
| 54 | + of kSet: | |
| 55 | + for x in coll.items: | |
| 56 | + if equals(x, k): return x | |
| 57 | + dflt | |
| 58 | + of kStr: | |
| 59 | + if k.kind != kInt: return dflt | |
| 60 | + let i = int(k.i) | |
| 61 | + if i < 0 or i >= coll.s.len: dflt else: mkStr($coll.s[i]) | |
| 62 | + else: dflt | |
| 63 | + | |
| 64 | +proc assocOne(coll, k, v: Value): Value = | |
| 65 | + if coll.isNil or coll.kind == kNil: | |
| 66 | + return Value(kind: kMap, pairs: @[(k, v)]) | |
| 67 | + case coll.kind | |
| 68 | + of kMap: | |
| 69 | + var ps = coll.pairs | |
| 70 | + for i in 0 ..< ps.len: | |
| 71 | + if equals(ps[i][0], k): | |
| 72 | + ps[i] = (k, v) | |
| 73 | + return Value(kind: kMap, pairs: ps) | |
| 74 | + ps.add (k, v) | |
| 75 | + Value(kind: kMap, pairs: ps) | |
| 76 | + of kVector: | |
| 77 | + if k.kind != kInt: err("Vector index must be an integer") | |
| 78 | + var xs = coll.items | |
| 79 | + let i = int(k.i) | |
| 80 | + if i == xs.len: xs.add v | |
| 81 | + elif i >= 0 and i < xs.len: xs[i] = v | |
| 82 | + else: err("Index out of bounds: " & $i) | |
| 83 | + mkVector(xs) | |
| 84 | + else: err("assoc not supported on " & prStr(coll)) | |
| 85 | + | |
| 86 | +proc conjOne(coll, x: Value): Value = | |
| 87 | + if coll.isNil or coll.kind == kNil: return mkList(@[x]) | |
| 88 | + case coll.kind | |
| 89 | + of kVector: mkVector(coll.items & @[x]) | |
| 90 | + of kList: mkList(@[x] & coll.items) | |
| 91 | + of kSet: mkSet(coll.items & @[x]) | |
| 92 | + of kMap: | |
| 93 | + if x.kind in {kVector, kList} and x.items.len == 2: | |
| 94 | + assocOne(coll, x.items[0], x.items[1]) | |
| 95 | + elif x.kind == kMap: | |
| 96 | + var m = coll | |
| 97 | + for (k, v) in x.pairs: m = assocOne(m, k, v) | |
| 98 | + m | |
| 99 | + else: err("conj on map needs a pair") | |
| 100 | + else: err("conj not supported on " & prStr(coll)) | |
| 101 | + | |
| 102 | +proc def(name: string, f: proc (args: seq[Value]): Value {.closure.}) = | |
| 103 | + setVar(name, mkFn(name, f)) | |
| 104 | + | |
| 105 | +proc registerCore*() = | |
| 106 | + # ---- arithmetic | |
| 107 | + def "+", proc (a: seq[Value]): Value = | |
| 108 | + arith("+", a, 0, proc (x, y: int64): int64 = x + y, proc (x, y: float64): float64 = x + y) | |
| 109 | + def "-", proc (a: seq[Value]): Value = | |
| 110 | + arith("-", a, 0, proc (x, y: int64): int64 = x - y, proc (x, y: float64): float64 = x - y) | |
| 111 | + def "*", proc (a: seq[Value]): Value = | |
| 112 | + arith("*", a, 1, proc (x, y: int64): int64 = x * y, proc (x, y: float64): float64 = x * y) | |
| 113 | + def "/", proc (a: seq[Value]): Value = | |
| 114 | + if isFloaty(a) or a.len == 1: | |
| 115 | + arith("/", a, 1, proc (x, y: int64): int64 = x div y, proc (x, y: float64): float64 = x / y) | |
| 116 | + else: | |
| 117 | + for i in 1 ..< a.len: | |
| 118 | + if a[i].kind == kInt and a[i].i == 0: err("Divide by zero") | |
| 119 | + arith("/", a, 1, proc (x, y: int64): int64 = x div y, proc (x, y: float64): float64 = x / y) | |
| 120 | + def "quot", proc (a: seq[Value]): Value = mkInt(intOf(a[0]) div intOf(a[1])) | |
| 121 | + def "rem", proc (a: seq[Value]): Value = mkInt(intOf(a[0]) mod intOf(a[1])) | |
| 122 | + def "mod", proc (a: seq[Value]): Value = | |
| 123 | + let x = intOf(a[0]); let y = intOf(a[1]) | |
| 124 | + var r = x mod y | |
| 125 | + if r != 0 and ((r < 0) != (y < 0)): r += y | |
| 126 | + mkInt(r) | |
| 127 | + def "inc", proc (a: seq[Value]): Value = | |
| 128 | + (if a[0].kind == kFloat: mkFloat(a[0].f + 1.0) else: mkInt(a[0].i + 1)) | |
| 129 | + def "dec", proc (a: seq[Value]): Value = | |
| 130 | + (if a[0].kind == kFloat: mkFloat(a[0].f - 1.0) else: mkInt(a[0].i - 1)) | |
| 131 | + def "max", proc (a: seq[Value]): Value = | |
| 132 | + result = a[0] | |
| 133 | + for x in a: (if num(x) > num(result): result = x) | |
| 134 | + def "min", proc (a: seq[Value]): Value = | |
| 135 | + result = a[0] | |
| 136 | + for x in a: (if num(x) < num(result): result = x) | |
| 137 | + def "abs", proc (a: seq[Value]): Value = | |
| 138 | + (if a[0].kind == kFloat: mkFloat(abs(a[0].f)) else: mkInt(abs(a[0].i))) | |
| 139 | + def "Math/sqrt", proc (a: seq[Value]): Value = mkFloat(sqrt(num(a[0]))) | |
| 140 | + def "Math/pow", proc (a: seq[Value]): Value = mkFloat(pow(num(a[0]), num(a[1]))) | |
| 141 | + def "rand-int", proc (a: seq[Value]): Value = mkInt(rand(int(intOf(a[0])) - 1)) | |
| 142 | + def "double", proc (a: seq[Value]): Value = mkFloat(num(a[0])) | |
| 143 | + def "int", proc (a: seq[Value]): Value = mkInt(intOf(a[0])) | |
| 144 | + | |
| 145 | + # ---- comparison / predicates | |
| 146 | + def "=", proc (a: seq[Value]): Value = | |
| 147 | + for i in 0 ..< a.len - 1: | |
| 148 | + if not equals(a[i], a[i + 1]): return FalseV | |
| 149 | + TrueV | |
| 150 | + def "not=", proc (a: seq[Value]): Value = | |
| 151 | + for i in 0 ..< a.len - 1: | |
| 152 | + if not equals(a[i], a[i + 1]): return TrueV | |
| 153 | + FalseV | |
| 154 | + def "<", proc (a: seq[Value]): Value = cmpChain(a, proc (c: int): bool = c < 0) | |
| 155 | + def ">", proc (a: seq[Value]): Value = cmpChain(a, proc (c: int): bool = c > 0) | |
| 156 | + def "<=", proc (a: seq[Value]): Value = cmpChain(a, proc (c: int): bool = c <= 0) | |
| 157 | + def ">=", proc (a: seq[Value]): Value = cmpChain(a, proc (c: int): bool = c >= 0) | |
| 158 | + def "not", proc (a: seq[Value]): Value = mkBool(not truthy(a[0])) | |
| 159 | + def "nil?", proc (a: seq[Value]): Value = mkBool(a[0].isNil or a[0].kind == kNil) | |
| 160 | + def "some?", proc (a: seq[Value]): Value = mkBool(not (a[0].isNil or a[0].kind == kNil)) | |
| 161 | + def "true?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kBool and a[0].b) | |
| 162 | + def "false?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kBool and not a[0].b) | |
| 163 | + def "zero?", proc (a: seq[Value]): Value = mkBool(num(a[0]) == 0.0) | |
| 164 | + def "pos?", proc (a: seq[Value]): Value = mkBool(num(a[0]) > 0.0) | |
| 165 | + def "neg?", proc (a: seq[Value]): Value = mkBool(num(a[0]) < 0.0) | |
| 166 | + def "even?", proc (a: seq[Value]): Value = mkBool(intOf(a[0]) mod 2 == 0) | |
| 167 | + def "odd?", proc (a: seq[Value]): Value = mkBool(intOf(a[0]) mod 2 != 0) | |
| 168 | + def "string?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kStr) | |
| 169 | + def "number?", proc (a: seq[Value]): Value = mkBool(a[0].kind in {kInt, kFloat}) | |
| 170 | + def "int?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kInt) | |
| 171 | + def "keyword?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kKeyword) | |
| 172 | + def "symbol?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kSymbol) | |
| 173 | + def "vector?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kVector) | |
| 174 | + def "list?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kList) | |
| 175 | + def "map?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kMap) | |
| 176 | + def "set?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kSet) | |
| 177 | + def "coll?", proc (a: seq[Value]): Value = | |
| 178 | + mkBool(a[0].kind in {kList, kVector, kMap, kSet}) | |
| 179 | + def "fn?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kFn) | |
| 180 | + def "empty?", proc (a: seq[Value]): Value = mkBool(toSeq(a[0]).len == 0) | |
| 181 | + def "contains?", proc (a: seq[Value]): Value = | |
| 182 | + let c = a[0] | |
| 183 | + if c.isNil or c.kind == kNil: return FalseV | |
| 184 | + case c.kind | |
| 185 | + of kMap: | |
| 186 | + for (k, _) in c.pairs: | |
| 187 | + if equals(k, a[1]): return TrueV | |
| 188 | + FalseV | |
| 189 | + of kSet: | |
| 190 | + for x in c.items: | |
| 191 | + if equals(x, a[1]): return TrueV | |
| 192 | + FalseV | |
| 193 | + of kVector: | |
| 194 | + mkBool(a[1].kind == kInt and a[1].i >= 0 and a[1].i < c.items.len) | |
| 195 | + else: FalseV | |
| 196 | + | |
| 197 | + # ---- strings / IO | |
| 198 | + def "str", proc (a: seq[Value]): Value = | |
| 199 | + var s = "" | |
| 200 | + for x in a: s &= str(x) | |
| 201 | + mkStr(s) | |
| 202 | + def "pr-str", proc (a: seq[Value]): Value = | |
| 203 | + var parts: seq[string] = @[] | |
| 204 | + for x in a: parts.add prStr(x) | |
| 205 | + mkStr(parts.join(" ")) | |
| 206 | + def "println", proc (a: seq[Value]): Value = | |
| 207 | + var parts: seq[string] = @[] | |
| 208 | + for x in a: parts.add str(x) | |
| 209 | + echo parts.join(" ") | |
| 210 | + NilV | |
| 211 | + def "prn", proc (a: seq[Value]): Value = | |
| 212 | + var parts: seq[string] = @[] | |
| 213 | + for x in a: parts.add prStr(x) | |
| 214 | + echo parts.join(" ") | |
| 215 | + NilV | |
| 216 | + def "print", proc (a: seq[Value]): Value = | |
| 217 | + var parts: seq[string] = @[] | |
| 218 | + for x in a: parts.add str(x) | |
| 219 | + stdout.write parts.join(" ") | |
| 220 | + NilV | |
| 221 | + def "name", proc (a: seq[Value]): Value = | |
| 222 | + case a[0].kind | |
| 223 | + of kKeyword, kSymbol, kStr: mkStr(a[0].s) | |
| 224 | + else: err("name expects keyword/symbol/string") | |
| 225 | + def "keyword", proc (a: seq[Value]): Value = mkKeyword(str(a[0])) | |
| 226 | + def "symbol", proc (a: seq[Value]): Value = mkSymbol(str(a[0])) | |
| 227 | + def "subs", proc (a: seq[Value]): Value = | |
| 228 | + let s = a[0].s | |
| 229 | + let st = int(intOf(a[1])) | |
| 230 | + let en = (if a.len > 2: int(intOf(a[2])) else: s.len) | |
| 231 | + mkStr(s[st ..< en]) | |
| 232 | + def "clojure.string/upper-case", proc (a: seq[Value]): Value = mkStr(a[0].s.toUpperAscii) | |
| 233 | + def "clojure.string/lower-case", proc (a: seq[Value]): Value = mkStr(a[0].s.toLowerAscii) | |
| 234 | + def "clojure.string/trim", proc (a: seq[Value]): Value = mkStr(a[0].s.strip) | |
| 235 | + def "clojure.string/split", proc (a: seq[Value]): Value = | |
| 236 | + var r: seq[Value] = @[] | |
| 237 | + for piece in a[0].s.split(a[1].s): r.add mkStr(piece) | |
| 238 | + mkVector(r) | |
| 239 | + def "clojure.string/join", proc (a: seq[Value]): Value = | |
| 240 | + let sep = (if a.len > 1: str(a[0]) else: "") | |
| 241 | + let coll = (if a.len > 1: a[1] else: a[0]) | |
| 242 | + var parts: seq[string] = @[] | |
| 243 | + for x in toSeq(coll): parts.add str(x) | |
| 244 | + mkStr(parts.join(sep)) | |
| 245 | + def "read-line", proc (a: seq[Value]): Value = | |
| 246 | + try: mkStr(stdin.readLine()) except CatchableError: NilV | |
| 247 | + def "slurp", proc (a: seq[Value]): Value = mkStr(readFile(a[0].s)) | |
| 248 | + def "spit", proc (a: seq[Value]): Value = | |
| 249 | + writeFile(a[0].s, str(a[1])); NilV | |
| 250 | + def "now-ms", proc (a: seq[Value]): Value = mkInt(int64(epochTime() * 1000)) | |
| 251 | + | |
| 252 | + # ---- collections | |
| 253 | + def "list", proc (a: seq[Value]): Value = mkList(a) | |
| 254 | + def "vector", proc (a: seq[Value]): Value = mkVector(a) | |
| 255 | + def "hash-map", proc (a: seq[Value]): Value = | |
| 256 | + var m: Value = Value(kind: kMap, pairs: @[]) | |
| 257 | + var i = 0 | |
| 258 | + while i + 1 < a.len: | |
| 259 | + m = assocOne(m, a[i], a[i + 1]); i += 2 | |
| 260 | + m | |
| 261 | + def "hash-set", proc (a: seq[Value]): Value = mkSet(a) | |
| 262 | + def "set", proc (a: seq[Value]): Value = mkSet(toSeq(a[0])) | |
| 263 | + def "vec", proc (a: seq[Value]): Value = mkVector(toSeq(a[0])) | |
| 264 | + def "seq", proc (a: seq[Value]): Value = | |
| 265 | + let s = toSeq(a[0]) | |
| 266 | + (if s.len == 0: NilV else: mkList(s)) | |
| 267 | + def "count", proc (a: seq[Value]): Value = | |
| 268 | + if a[0].isNil or a[0].kind == kNil: return mkInt(0) | |
| 269 | + if a[0].kind == kStr: return mkInt(a[0].s.len) | |
| 270 | + if a[0].kind == kMap: return mkInt(a[0].pairs.len) | |
| 271 | + mkInt(toSeq(a[0]).len) | |
| 272 | + def "conj", proc (a: seq[Value]): Value = | |
| 273 | + result = a[0] | |
| 274 | + for i in 1 ..< a.len: result = conjOne(result, a[i]) | |
| 275 | + def "cons", proc (a: seq[Value]): Value = mkList(@[a[0]] & toSeq(a[1])) | |
| 276 | + def "first", proc (a: seq[Value]): Value = | |
| 277 | + let s = toSeq(a[0]) | |
| 278 | + (if s.len == 0: NilV else: s[0]) | |
| 279 | + def "second", proc (a: seq[Value]): Value = | |
| 280 | + let s = toSeq(a[0]) | |
| 281 | + (if s.len < 2: NilV else: s[1]) | |
| 282 | + def "last", proc (a: seq[Value]): Value = | |
| 283 | + let s = toSeq(a[0]) | |
| 284 | + (if s.len == 0: NilV else: s[^1]) | |
| 285 | + def "rest", proc (a: seq[Value]): Value = | |
| 286 | + let s = toSeq(a[0]) | |
| 287 | + (if s.len <= 1: mkList(@[]) else: mkList(s[1 .. ^1])) | |
| 288 | + def "next", proc (a: seq[Value]): Value = | |
| 289 | + let s = toSeq(a[0]) | |
| 290 | + (if s.len <= 1: NilV else: mkList(s[1 .. ^1])) | |
| 291 | + def "nth", proc (a: seq[Value]): Value = | |
| 292 | + let s = toSeq(a[0]) | |
| 293 | + let i = int(intOf(a[1])) | |
| 294 | + if i >= 0 and i < s.len: s[i] | |
| 295 | + elif a.len > 2: a[2] | |
| 296 | + else: err("Index out of bounds: " & $i) | |
| 297 | + def "get", proc (a: seq[Value]): Value = | |
| 298 | + getIn(a[0], a[1], (if a.len > 2: a[2] else: NilV)) | |
| 299 | + def "get-in", proc (a: seq[Value]): Value = | |
| 300 | + var cur = a[0] | |
| 301 | + for k in toSeq(a[1]): | |
| 302 | + cur = getIn(cur, k, NilV) | |
| 303 | + (if (cur.isNil or cur.kind == kNil) and a.len > 2: a[2] else: cur) | |
| 304 | + def "assoc", proc (a: seq[Value]): Value = | |
| 305 | + result = a[0] | |
| 306 | + var i = 1 | |
| 307 | + while i + 1 < a.len: | |
| 308 | + result = assocOne(result, a[i], a[i + 1]); i += 2 | |
| 309 | + def "dissoc", proc (a: seq[Value]): Value = | |
| 310 | + var ps = a[0].pairs | |
| 311 | + for i in 1 ..< a.len: | |
| 312 | + var keep: seq[(Value, Value)] = @[] | |
| 313 | + for (k, v) in ps: | |
| 314 | + if not equals(k, a[i]): keep.add (k, v) | |
| 315 | + ps = keep | |
| 316 | + Value(kind: kMap, pairs: ps) | |
| 317 | + def "update", proc (a: seq[Value]): Value = | |
| 318 | + let cur = getIn(a[0], a[1], NilV) | |
| 319 | + assocOne(a[0], a[1], call(a[2], @[cur] & a[3 .. ^1])) | |
| 320 | + def "keys", proc (a: seq[Value]): Value = | |
| 321 | + var r: seq[Value] = @[] | |
| 322 | + for (k, _) in a[0].pairs: r.add k | |
| 323 | + (if r.len == 0: NilV else: mkList(r)) | |
| 324 | + def "vals", proc (a: seq[Value]): Value = | |
| 325 | + var r: seq[Value] = @[] | |
| 326 | + for (_, v) in a[0].pairs: r.add v | |
| 327 | + (if r.len == 0: NilV else: mkList(r)) | |
| 328 | + def "reverse", proc (a: seq[Value]): Value = | |
| 329 | + var s = toSeq(a[0]) | |
| 330 | + var r: seq[Value] = @[] | |
| 331 | + for i in countdown(s.len - 1, 0): r.add s[i] | |
| 332 | + mkList(r) | |
| 333 | + def "range", proc (a: seq[Value]): Value = | |
| 334 | + var lo: int64 = 0 | |
| 335 | + var hi: int64 = 0 | |
| 336 | + var step: int64 = 1 | |
| 337 | + if a.len == 1: hi = intOf(a[0]) | |
| 338 | + elif a.len >= 2: | |
| 339 | + lo = intOf(a[0]); hi = intOf(a[1]) | |
| 340 | + if a.len > 2: step = intOf(a[2]) | |
| 341 | + var r: seq[Value] = @[] | |
| 342 | + if step > 0: | |
| 343 | + var i = lo | |
| 344 | + while i < hi: r.add mkInt(i); i += step | |
| 345 | + elif step < 0: | |
| 346 | + var i = lo | |
| 347 | + while i > hi: r.add mkInt(i); i += step | |
| 348 | + mkList(r) | |
| 349 | + def "take", proc (a: seq[Value]): Value = | |
| 350 | + let n = int(intOf(a[0])) | |
| 351 | + let s = toSeq(a[1]) | |
| 352 | + mkList(s[0 ..< min(n, s.len)]) | |
| 353 | + def "drop", proc (a: seq[Value]): Value = | |
| 354 | + let n = int(intOf(a[0])) | |
| 355 | + let s = toSeq(a[1]) | |
| 356 | + (if n >= s.len: mkList(@[]) else: mkList(s[n .. ^1])) | |
| 357 | + def "concat", proc (a: seq[Value]): Value = | |
| 358 | + var r: seq[Value] = @[] | |
| 359 | + for x in a: r.add toSeq(x) | |
| 360 | + mkList(r) | |
| 361 | + def "sort", proc (a: seq[Value]): Value = | |
| 362 | + var s = toSeq(a[^1]) | |
| 363 | + let cmpFn = (if a.len > 1: a[0] else: NilV) | |
| 364 | + # insertion sort keeps it simple and stable | |
| 365 | + for i in 1 ..< s.len: | |
| 366 | + var j = i | |
| 367 | + while j > 0: | |
| 368 | + let before = | |
| 369 | + if cmpFn.kind == kFn: truthy(call(cmpFn, @[s[j], s[j - 1]])) | |
| 370 | + elif s[j].kind == kStr: s[j].s < s[j - 1].s | |
| 371 | + else: num(s[j]) < num(s[j - 1]) | |
| 372 | + if not before: break | |
| 373 | + swap(s[j], s[j - 1]); dec j | |
| 374 | + mkList(s) | |
| 375 | + def "sort-by", proc (a: seq[Value]): Value = | |
| 376 | + var s = toSeq(a[^1]) | |
| 377 | + let kf = a[0] | |
| 378 | + for i in 1 ..< s.len: | |
| 379 | + var j = i | |
| 380 | + while j > 0: | |
| 381 | + let ka = call(kf, @[s[j]]) | |
| 382 | + let kb = call(kf, @[s[j - 1]]) | |
| 383 | + let before = (if ka.kind == kStr: ka.s < kb.s else: num(ka) < num(kb)) | |
| 384 | + if not before: break | |
| 385 | + swap(s[j], s[j - 1]); dec j | |
| 386 | + mkList(s) | |
| 387 | + def "distinct", proc (a: seq[Value]): Value = | |
| 388 | + var r: seq[Value] = @[] | |
| 389 | + for x in toSeq(a[0]): | |
| 390 | + var dup = false | |
| 391 | + for y in r: | |
| 392 | + if equals(x, y): dup = true; break | |
| 393 | + if not dup: r.add x | |
| 394 | + mkList(r) | |
| 395 | + def "interpose", proc (a: seq[Value]): Value = | |
| 396 | + var r: seq[Value] = @[] | |
| 397 | + for x in toSeq(a[1]): | |
| 398 | + if r.len > 0: r.add a[0] | |
| 399 | + r.add x | |
| 400 | + mkList(r) | |
| 401 | + def "partition", proc (a: seq[Value]): Value = | |
| 402 | + let n = int(intOf(a[0])) | |
| 403 | + let s = toSeq(a[^1]) | |
| 404 | + var r: seq[Value] = @[] | |
| 405 | + var i = 0 | |
| 406 | + while i + n <= s.len: | |
| 407 | + r.add mkList(s[i ..< i + n]); i += n | |
| 408 | + mkList(r) | |
| 409 | + | |
| 410 | + # ---- higher order | |
| 411 | + def "apply", proc (a: seq[Value]): Value = | |
| 412 | + var callArgs: seq[Value] = @[] | |
| 413 | + for i in 1 ..< a.len - 1: callArgs.add a[i] | |
| 414 | + callArgs.add toSeq(a[^1]) | |
| 415 | + call(a[0], callArgs) | |
| 416 | + def "map", proc (a: seq[Value]): Value = | |
| 417 | + let f = a[0] | |
| 418 | + if a.len == 2: | |
| 419 | + var r: seq[Value] = @[] | |
| 420 | + for x in toSeq(a[1]): r.add call(f, @[x]) | |
| 421 | + return mkList(r) | |
| 422 | + var colls: seq[seq[Value]] = @[] | |
| 423 | + for i in 1 ..< a.len: colls.add toSeq(a[i]) | |
| 424 | + var n = colls[0].len | |
| 425 | + for c in colls: n = min(n, c.len) | |
| 426 | + var r: seq[Value] = @[] | |
| 427 | + for i in 0 ..< n: | |
| 428 | + var args: seq[Value] = @[] | |
| 429 | + for c in colls: args.add c[i] | |
| 430 | + r.add call(f, args) | |
| 431 | + mkList(r) | |
| 432 | + def "mapv", proc (a: seq[Value]): Value = | |
| 433 | + var r: seq[Value] = @[] | |
| 434 | + for x in toSeq(a[1]): r.add call(a[0], @[x]) | |
| 435 | + mkVector(r) | |
| 436 | + def "map-indexed", proc (a: seq[Value]): Value = | |
| 437 | + var r: seq[Value] = @[] | |
| 438 | + var i = 0 | |
| 439 | + for x in toSeq(a[1]): | |
| 440 | + r.add call(a[0], @[mkInt(i), x]); inc i | |
| 441 | + mkList(r) | |
| 442 | + def "filter", proc (a: seq[Value]): Value = | |
| 443 | + var r: seq[Value] = @[] | |
| 444 | + for x in toSeq(a[1]): | |
| 445 | + if truthy(call(a[0], @[x])): r.add x | |
| 446 | + mkList(r) | |
| 447 | + def "remove", proc (a: seq[Value]): Value = | |
| 448 | + var r: seq[Value] = @[] | |
| 449 | + for x in toSeq(a[1]): | |
| 450 | + if not truthy(call(a[0], @[x])): r.add x | |
| 451 | + mkList(r) | |
| 452 | + def "reduce", proc (a: seq[Value]): Value = | |
| 453 | + let f = a[0] | |
| 454 | + if a.len == 2: | |
| 455 | + let s = toSeq(a[1]) | |
| 456 | + if s.len == 0: return call(f, @[]) | |
| 457 | + var acc = s[0] | |
| 458 | + for i in 1 ..< s.len: acc = call(f, @[acc, s[i]]) | |
| 459 | + return acc | |
| 460 | + var acc = a[1] | |
| 461 | + for x in toSeq(a[2]): acc = call(f, @[acc, x]) | |
| 462 | + acc | |
| 463 | + def "some", proc (a: seq[Value]): Value = | |
| 464 | + for x in toSeq(a[1]): | |
| 465 | + let r = call(a[0], @[x]) | |
| 466 | + if truthy(r): return r | |
| 467 | + NilV | |
| 468 | + def "every?", proc (a: seq[Value]): Value = | |
| 469 | + for x in toSeq(a[1]): | |
| 470 | + if not truthy(call(a[0], @[x])): return FalseV | |
| 471 | + TrueV | |
| 472 | + def "take-while", proc (a: seq[Value]): Value = | |
| 473 | + var r: seq[Value] = @[] | |
| 474 | + for x in toSeq(a[1]): | |
| 475 | + if not truthy(call(a[0], @[x])): break | |
| 476 | + r.add x | |
| 477 | + mkList(r) | |
| 478 | + def "drop-while", proc (a: seq[Value]): Value = | |
| 479 | + var r: seq[Value] = @[] | |
| 480 | + var dropping = true | |
| 481 | + for x in toSeq(a[1]): | |
| 482 | + if dropping and truthy(call(a[0], @[x])): continue | |
| 483 | + dropping = false | |
| 484 | + r.add x | |
| 485 | + mkList(r) | |
| 486 | + def "group-by", proc (a: seq[Value]): Value = | |
| 487 | + var m: Value = Value(kind: kMap, pairs: @[]) | |
| 488 | + for x in toSeq(a[1]): | |
| 489 | + let k = call(a[0], @[x]) | |
| 490 | + let cur = getIn(m, k, mkVector(@[])) | |
| 491 | + m = assocOne(m, k, conjOne(cur, x)) | |
| 492 | + m | |
| 493 | + def "frequencies", proc (a: seq[Value]): Value = | |
| 494 | + var m: Value = Value(kind: kMap, pairs: @[]) | |
| 495 | + for x in toSeq(a[0]): | |
| 496 | + let cur = getIn(m, x, mkInt(0)) | |
| 497 | + m = assocOne(m, x, mkInt(cur.i + 1)) | |
| 498 | + m | |
| 499 | + def "identity", proc (a: seq[Value]): Value = a[0] | |
| 500 | + def "comp", proc (a: seq[Value]): Value = | |
| 501 | + let fs = a | |
| 502 | + mkFn("comp", proc (args: seq[Value]): Value = | |
| 503 | + if fs.len == 0: return argAt(args, 0) | |
| 504 | + var v = call(fs[^1], args) | |
| 505 | + for i in countdown(fs.len - 2, 0): v = call(fs[i], @[v]) | |
| 506 | + v) | |
| 507 | + def "partial", proc (a: seq[Value]): Value = | |
| 508 | + let f = a[0] | |
| 509 | + let bound = a[1 .. ^1] | |
| 510 | + mkFn("partial", proc (args: seq[Value]): Value = call(f, bound & args)) | |
| 511 | + def "juxt", proc (a: seq[Value]): Value = | |
| 512 | + let fs = a | |
| 513 | + mkFn("juxt", proc (args: seq[Value]): Value = | |
| 514 | + var r: seq[Value] = @[] | |
| 515 | + for f in fs: r.add call(f, args) | |
| 516 | + mkVector(r)) | |
| 517 | + def "constantly", proc (a: seq[Value]): Value = | |
| 518 | + let v = a[0] | |
| 519 | + mkFn("constantly", proc (args: seq[Value]): Value = v) | |
| 520 | + | |
| 521 | + # ---- atoms (mutable boxes, modelled as a 1-slot vector) | |
| 522 | + def "atom", proc (a: seq[Value]): Value = | |
| 523 | + var cell = a[0] | |
| 524 | + mkFn("atom", proc (args: seq[Value]): Value = | |
| 525 | + # (a) -> deref | |
| 526 | + # (a :set v) -> reset | |
| 527 | + if args.len == 0: return cell | |
| 528 | + cell = args[1] | |
| 529 | + cell) | |
| 530 | + def "deref", proc (a: seq[Value]): Value = call(a[0], @[]) | |
| 531 | + def "reset!", proc (a: seq[Value]): Value = call(a[0], @[mkKeyword("set"), a[1]]) | |
| 532 | + def "swap!", proc (a: seq[Value]): Value = | |
| 533 | + let cur = call(a[0], @[]) | |
| 534 | + let nv = call(a[1], @[cur] & a[2 .. ^1]) | |
| 535 | + call(a[0], @[mkKeyword("set"), nv]) | |
| 536 | + | |
| 537 | + def "throw", proc (a: seq[Value]): Value = err(str(a[0])) | |
| 538 | + def "ex-info", proc (a: seq[Value]): Value = mkStr(str(a[0])) | |
| 539 | + def "time-ms", proc (a: seq[Value]): Value = mkInt(int64(epochTime() * 1000)) | |
| new file mode 100644 | |||
| @@ -0,0 +1,539 @@ | |||
| 1 | +## clonim core — clojure.core builtins, registered into the global var table. | ||
| 2 | +import std/[strutils, math, times, random] | ||
| 3 | +import runtime | ||
| 4 | + | ||
| 5 | +proc num(v: Value): float64 = | ||
| 6 | + case v.kind | ||
| 7 | + of kInt: float64(v.i) | ||
| 8 | + of kFloat: v.f | ||
| 9 | + else: err("Not a number: " & prStr(v)) | ||
| 10 | + | ||
| 11 | +proc isFloaty(vs: seq[Value]): bool = | ||
| 12 | + for v in vs: | ||
| 13 | + if v.kind == kFloat: return true | ||
| 14 | + false | ||
| 15 | + | ||
| 16 | +proc intOf(v: Value): int64 = | ||
| 17 | + case v.kind | ||
| 18 | + of kInt: v.i | ||
| 19 | + of kFloat: int64(v.f) | ||
| 20 | + else: err("Not a number: " & prStr(v)) | ||
| 21 | + | ||
| 22 | +proc arith(name: string, args: seq[Value], unit: int64, | ||
| 23 | + fi: proc (a, b: int64): int64, ff: proc (a, b: float64): float64): Value = | ||
| 24 | + if args.len == 0: return mkInt(unit) | ||
| 25 | + if isFloaty(args): | ||
| 26 | + var acc = (if args.len == 1: float64(unit) else: num(args[0])) | ||
| 27 | + let start = (if args.len == 1: 0 else: 1) | ||
| 28 | + for i in start ..< args.len: acc = ff(acc, num(args[i])) | ||
| 29 | + return mkFloat(acc) | ||
| 30 | + var acc = (if args.len == 1: unit else: args[0].i) | ||
| 31 | + let start = (if args.len == 1: 0 else: 1) | ||
| 32 | + for i in start ..< args.len: acc = fi(acc, args[i].i) | ||
| 33 | + mkInt(acc) | ||
| 34 | + | ||
| 35 | +proc cmpChain(args: seq[Value], ok: proc (c: int): bool): Value = | ||
| 36 | + for i in 0 ..< args.len - 1: | ||
| 37 | + let a = num(args[i]) | ||
| 38 | + let b = num(args[i + 1]) | ||
| 39 | + let c = (if a < b: -1 elif a > b: 1 else: 0) | ||
| 40 | + if not ok(c): return FalseV | ||
| 41 | + TrueV | ||
| 42 | + | ||
| 43 | +proc getIn(coll, k, dflt: Value): Value = | ||
| 44 | + if coll.isNil or coll.kind == kNil: return dflt | ||
| 45 | + case coll.kind | ||
| 46 | + of kMap: | ||
| 47 | + for (kk, vv) in coll.pairs: | ||
| 48 | + if equals(kk, k): return vv | ||
| 49 | + dflt | ||
| 50 | + of kVector, kList: | ||
| 51 | + if k.kind != kInt: return dflt | ||
| 52 | + let i = int(k.i) | ||
| 53 | + if i < 0 or i >= coll.items.len: dflt else: coll.items[i] | ||
| 54 | + of kSet: | ||
| 55 | + for x in coll.items: | ||
| 56 | + if equals(x, k): return x | ||
| 57 | + dflt | ||
| 58 | + of kStr: | ||
| 59 | + if k.kind != kInt: return dflt | ||
| 60 | + let i = int(k.i) | ||
| 61 | + if i < 0 or i >= coll.s.len: dflt else: mkStr($coll.s[i]) | ||
| 62 | + else: dflt | ||
| 63 | + | ||
| 64 | +proc assocOne(coll, k, v: Value): Value = | ||
| 65 | + if coll.isNil or coll.kind == kNil: | ||
| 66 | + return Value(kind: kMap, pairs: @[(k, v)]) | ||
| 67 | + case coll.kind | ||
| 68 | + of kMap: | ||
| 69 | + var ps = coll.pairs | ||
| 70 | + for i in 0 ..< ps.len: | ||
| 71 | + if equals(ps[i][0], k): | ||
| 72 | + ps[i] = (k, v) | ||
| 73 | + return Value(kind: kMap, pairs: ps) | ||
| 74 | + ps.add (k, v) | ||
| 75 | + Value(kind: kMap, pairs: ps) | ||
| 76 | + of kVector: | ||
| 77 | + if k.kind != kInt: err("Vector index must be an integer") | ||
| 78 | + var xs = coll.items | ||
| 79 | + let i = int(k.i) | ||
| 80 | + if i == xs.len: xs.add v | ||
| 81 | + elif i >= 0 and i < xs.len: xs[i] = v | ||
| 82 | + else: err("Index out of bounds: " & $i) | ||
| 83 | + mkVector(xs) | ||
| 84 | + else: err("assoc not supported on " & prStr(coll)) | ||
| 85 | + | ||
| 86 | +proc conjOne(coll, x: Value): Value = | ||
| 87 | + if coll.isNil or coll.kind == kNil: return mkList(@[x]) | ||
| 88 | + case coll.kind | ||
| 89 | + of kVector: mkVector(coll.items & @[x]) | ||
| 90 | + of kList: mkList(@[x] & coll.items) | ||
| 91 | + of kSet: mkSet(coll.items & @[x]) | ||
| 92 | + of kMap: | ||
| 93 | + if x.kind in {kVector, kList} and x.items.len == 2: | ||
| 94 | + assocOne(coll, x.items[0], x.items[1]) | ||
| 95 | + elif x.kind == kMap: | ||
| 96 | + var m = coll | ||
| 97 | + for (k, v) in x.pairs: m = assocOne(m, k, v) | ||
| 98 | + m | ||
| 99 | + else: err("conj on map needs a pair") | ||
| 100 | + else: err("conj not supported on " & prStr(coll)) | ||
| 101 | + | ||
| 102 | +proc def(name: string, f: proc (args: seq[Value]): Value {.closure.}) = | ||
| 103 | + setVar(name, mkFn(name, f)) | ||
| 104 | + | ||
| 105 | +proc registerCore*() = | ||
| 106 | + # ---- arithmetic | ||
| 107 | + def "+", proc (a: seq[Value]): Value = | ||
| 108 | + arith("+", a, 0, proc (x, y: int64): int64 = x + y, proc (x, y: float64): float64 = x + y) | ||
| 109 | + def "-", proc (a: seq[Value]): Value = | ||
| 110 | + arith("-", a, 0, proc (x, y: int64): int64 = x - y, proc (x, y: float64): float64 = x - y) | ||
| 111 | + def "*", proc (a: seq[Value]): Value = | ||
| 112 | + arith("*", a, 1, proc (x, y: int64): int64 = x * y, proc (x, y: float64): float64 = x * y) | ||
| 113 | + def "/", proc (a: seq[Value]): Value = | ||
| 114 | + if isFloaty(a) or a.len == 1: | ||
| 115 | + arith("/", a, 1, proc (x, y: int64): int64 = x div y, proc (x, y: float64): float64 = x / y) | ||
| 116 | + else: | ||
| 117 | + for i in 1 ..< a.len: | ||
| 118 | + if a[i].kind == kInt and a[i].i == 0: err("Divide by zero") | ||
| 119 | + arith("/", a, 1, proc (x, y: int64): int64 = x div y, proc (x, y: float64): float64 = x / y) | ||
| 120 | + def "quot", proc (a: seq[Value]): Value = mkInt(intOf(a[0]) div intOf(a[1])) | ||
| 121 | + def "rem", proc (a: seq[Value]): Value = mkInt(intOf(a[0]) mod intOf(a[1])) | ||
| 122 | + def "mod", proc (a: seq[Value]): Value = | ||
| 123 | + let x = intOf(a[0]); let y = intOf(a[1]) | ||
| 124 | + var r = x mod y | ||
| 125 | + if r != 0 and ((r < 0) != (y < 0)): r += y | ||
| 126 | + mkInt(r) | ||
| 127 | + def "inc", proc (a: seq[Value]): Value = | ||
| 128 | + (if a[0].kind == kFloat: mkFloat(a[0].f + 1.0) else: mkInt(a[0].i + 1)) | ||
| 129 | + def "dec", proc (a: seq[Value]): Value = | ||
| 130 | + (if a[0].kind == kFloat: mkFloat(a[0].f - 1.0) else: mkInt(a[0].i - 1)) | ||
| 131 | + def "max", proc (a: seq[Value]): Value = | ||
| 132 | + result = a[0] | ||
| 133 | + for x in a: (if num(x) > num(result): result = x) | ||
| 134 | + def "min", proc (a: seq[Value]): Value = | ||
| 135 | + result = a[0] | ||
| 136 | + for x in a: (if num(x) < num(result): result = x) | ||
| 137 | + def "abs", proc (a: seq[Value]): Value = | ||
| 138 | + (if a[0].kind == kFloat: mkFloat(abs(a[0].f)) else: mkInt(abs(a[0].i))) | ||
| 139 | + def "Math/sqrt", proc (a: seq[Value]): Value = mkFloat(sqrt(num(a[0]))) | ||
| 140 | + def "Math/pow", proc (a: seq[Value]): Value = mkFloat(pow(num(a[0]), num(a[1]))) | ||
| 141 | + def "rand-int", proc (a: seq[Value]): Value = mkInt(rand(int(intOf(a[0])) - 1)) | ||
| 142 | + def "double", proc (a: seq[Value]): Value = mkFloat(num(a[0])) | ||
| 143 | + def "int", proc (a: seq[Value]): Value = mkInt(intOf(a[0])) | ||
| 144 | + | ||
| 145 | + # ---- comparison / predicates | ||
| 146 | + def "=", proc (a: seq[Value]): Value = | ||
| 147 | + for i in 0 ..< a.len - 1: | ||
| 148 | + if not equals(a[i], a[i + 1]): return FalseV | ||
| 149 | + TrueV | ||
| 150 | + def "not=", proc (a: seq[Value]): Value = | ||
| 151 | + for i in 0 ..< a.len - 1: | ||
| 152 | + if not equals(a[i], a[i + 1]): return TrueV | ||
| 153 | + FalseV | ||
| 154 | + def "<", proc (a: seq[Value]): Value = cmpChain(a, proc (c: int): bool = c < 0) | ||
| 155 | + def ">", proc (a: seq[Value]): Value = cmpChain(a, proc (c: int): bool = c > 0) | ||
| 156 | + def "<=", proc (a: seq[Value]): Value = cmpChain(a, proc (c: int): bool = c <= 0) | ||
| 157 | + def ">=", proc (a: seq[Value]): Value = cmpChain(a, proc (c: int): bool = c >= 0) | ||
| 158 | + def "not", proc (a: seq[Value]): Value = mkBool(not truthy(a[0])) | ||
| 159 | + def "nil?", proc (a: seq[Value]): Value = mkBool(a[0].isNil or a[0].kind == kNil) | ||
| 160 | + def "some?", proc (a: seq[Value]): Value = mkBool(not (a[0].isNil or a[0].kind == kNil)) | ||
| 161 | + def "true?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kBool and a[0].b) | ||
| 162 | + def "false?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kBool and not a[0].b) | ||
| 163 | + def "zero?", proc (a: seq[Value]): Value = mkBool(num(a[0]) == 0.0) | ||
| 164 | + def "pos?", proc (a: seq[Value]): Value = mkBool(num(a[0]) > 0.0) | ||
| 165 | + def "neg?", proc (a: seq[Value]): Value = mkBool(num(a[0]) < 0.0) | ||
| 166 | + def "even?", proc (a: seq[Value]): Value = mkBool(intOf(a[0]) mod 2 == 0) | ||
| 167 | + def "odd?", proc (a: seq[Value]): Value = mkBool(intOf(a[0]) mod 2 != 0) | ||
| 168 | + def "string?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kStr) | ||
| 169 | + def "number?", proc (a: seq[Value]): Value = mkBool(a[0].kind in {kInt, kFloat}) | ||
| 170 | + def "int?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kInt) | ||
| 171 | + def "keyword?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kKeyword) | ||
| 172 | + def "symbol?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kSymbol) | ||
| 173 | + def "vector?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kVector) | ||
| 174 | + def "list?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kList) | ||
| 175 | + def "map?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kMap) | ||
| 176 | + def "set?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kSet) | ||
| 177 | + def "coll?", proc (a: seq[Value]): Value = | ||
| 178 | + mkBool(a[0].kind in {kList, kVector, kMap, kSet}) | ||
| 179 | + def "fn?", proc (a: seq[Value]): Value = mkBool(a[0].kind == kFn) | ||
| 180 | + def "empty?", proc (a: seq[Value]): Value = mkBool(toSeq(a[0]).len == 0) | ||
| 181 | + def "contains?", proc (a: seq[Value]): Value = | ||
| 182 | + let c = a[0] | ||
| 183 | + if c.isNil or c.kind == kNil: return FalseV | ||
| 184 | + case c.kind | ||
| 185 | + of kMap: | ||
| 186 | + for (k, _) in c.pairs: | ||
| 187 | + if equals(k, a[1]): return TrueV | ||
| 188 | + FalseV | ||
| 189 | + of kSet: | ||
| 190 | + for x in c.items: | ||
| 191 | + if equals(x, a[1]): return TrueV | ||
| 192 | + FalseV | ||
| 193 | + of kVector: | ||
| 194 | + mkBool(a[1].kind == kInt and a[1].i >= 0 and a[1].i < c.items.len) | ||
| 195 | + else: FalseV | ||
| 196 | + | ||
| 197 | + # ---- strings / IO | ||
| 198 | + def "str", proc (a: seq[Value]): Value = | ||
| 199 | + var s = "" | ||
| 200 | + for x in a: s &= str(x) | ||
| 201 | + mkStr(s) | ||
| 202 | + def "pr-str", proc (a: seq[Value]): Value = | ||
| 203 | + var parts: seq[string] = @[] | ||
| 204 | + for x in a: parts.add prStr(x) | ||
| 205 | + mkStr(parts.join(" ")) | ||
| 206 | + def "println", proc (a: seq[Value]): Value = | ||
| 207 | + var parts: seq[string] = @[] | ||
| 208 | + for x in a: parts.add str(x) | ||
| 209 | + echo parts.join(" ") | ||
| 210 | + NilV | ||
| 211 | + def "prn", proc (a: seq[Value]): Value = | ||
| 212 | + var parts: seq[string] = @[] | ||
| 213 | + for x in a: parts.add prStr(x) | ||
| 214 | + echo parts.join(" ") | ||
| 215 | + NilV | ||
| 216 | + def "print", proc (a: seq[Value]): Value = | ||
| 217 | + var parts: seq[string] = @[] | ||
| 218 | + for x in a: parts.add str(x) | ||
| 219 | + stdout.write parts.join(" ") | ||
| 220 | + NilV | ||
| 221 | + def "name", proc (a: seq[Value]): Value = | ||
| 222 | + case a[0].kind | ||
| 223 | + of kKeyword, kSymbol, kStr: mkStr(a[0].s) | ||
| 224 | + else: err("name expects keyword/symbol/string") | ||
| 225 | + def "keyword", proc (a: seq[Value]): Value = mkKeyword(str(a[0])) | ||
| 226 | + def "symbol", proc (a: seq[Value]): Value = mkSymbol(str(a[0])) | ||
| 227 | + def "subs", proc (a: seq[Value]): Value = | ||
| 228 | + let s = a[0].s | ||
| 229 | + let st = int(intOf(a[1])) | ||
| 230 | + let en = (if a.len > 2: int(intOf(a[2])) else: s.len) | ||
| 231 | + mkStr(s[st ..< en]) | ||
| 232 | + def "clojure.string/upper-case", proc (a: seq[Value]): Value = mkStr(a[0].s.toUpperAscii) | ||
| 233 | + def "clojure.string/lower-case", proc (a: seq[Value]): Value = mkStr(a[0].s.toLowerAscii) | ||
| 234 | + def "clojure.string/trim", proc (a: seq[Value]): Value = mkStr(a[0].s.strip) | ||
| 235 | + def "clojure.string/split", proc (a: seq[Value]): Value = | ||
| 236 | + var r: seq[Value] = @[] | ||
| 237 | + for piece in a[0].s.split(a[1].s): r.add mkStr(piece) | ||
| 238 | + mkVector(r) | ||
| 239 | + def "clojure.string/join", proc (a: seq[Value]): Value = | ||
| 240 | + let sep = (if a.len > 1: str(a[0]) else: "") | ||
| 241 | + let coll = (if a.len > 1: a[1] else: a[0]) | ||
| 242 | + var parts: seq[string] = @[] | ||
| 243 | + for x in toSeq(coll): parts.add str(x) | ||
| 244 | + mkStr(parts.join(sep)) | ||
| 245 | + def "read-line", proc (a: seq[Value]): Value = | ||
| 246 | + try: mkStr(stdin.readLine()) except CatchableError: NilV | ||
| 247 | + def "slurp", proc (a: seq[Value]): Value = mkStr(readFile(a[0].s)) | ||
| 248 | + def "spit", proc (a: seq[Value]): Value = | ||
| 249 | + writeFile(a[0].s, str(a[1])); NilV | ||
| 250 | + def "now-ms", proc (a: seq[Value]): Value = mkInt(int64(epochTime() * 1000)) | ||
| 251 | + | ||
| 252 | + # ---- collections | ||
| 253 | + def "list", proc (a: seq[Value]): Value = mkList(a) | ||
| 254 | + def "vector", proc (a: seq[Value]): Value = mkVector(a) | ||
| 255 | + def "hash-map", proc (a: seq[Value]): Value = | ||
| 256 | + var m: Value = Value(kind: kMap, pairs: @[]) | ||
| 257 | + var i = 0 | ||
| 258 | + while i + 1 < a.len: | ||
| 259 | + m = assocOne(m, a[i], a[i + 1]); i += 2 | ||
| 260 | + m | ||
| 261 | + def "hash-set", proc (a: seq[Value]): Value = mkSet(a) | ||
| 262 | + def "set", proc (a: seq[Value]): Value = mkSet(toSeq(a[0])) | ||
| 263 | + def "vec", proc (a: seq[Value]): Value = mkVector(toSeq(a[0])) | ||
| 264 | + def "seq", proc (a: seq[Value]): Value = | ||
| 265 | + let s = toSeq(a[0]) | ||
| 266 | + (if s.len == 0: NilV else: mkList(s)) | ||
| 267 | + def "count", proc (a: seq[Value]): Value = | ||
| 268 | + if a[0].isNil or a[0].kind == kNil: return mkInt(0) | ||
| 269 | + if a[0].kind == kStr: return mkInt(a[0].s.len) | ||
| 270 | + if a[0].kind == kMap: return mkInt(a[0].pairs.len) | ||
| 271 | + mkInt(toSeq(a[0]).len) | ||
| 272 | + def "conj", proc (a: seq[Value]): Value = | ||
| 273 | + result = a[0] | ||
| 274 | + for i in 1 ..< a.len: result = conjOne(result, a[i]) | ||
| 275 | + def "cons", proc (a: seq[Value]): Value = mkList(@[a[0]] & toSeq(a[1])) | ||
| 276 | + def "first", proc (a: seq[Value]): Value = | ||
| 277 | + let s = toSeq(a[0]) | ||
| 278 | + (if s.len == 0: NilV else: s[0]) | ||
| 279 | + def "second", proc (a: seq[Value]): Value = | ||
| 280 | + let s = toSeq(a[0]) | ||
| 281 | + (if s.len < 2: NilV else: s[1]) | ||
| 282 | + def "last", proc (a: seq[Value]): Value = | ||
| 283 | + let s = toSeq(a[0]) | ||
| 284 | + (if s.len == 0: NilV else: s[^1]) | ||
| 285 | + def "rest", proc (a: seq[Value]): Value = | ||
| 286 | + let s = toSeq(a[0]) | ||
| 287 | + (if s.len <= 1: mkList(@[]) else: mkList(s[1 .. ^1])) | ||
| 288 | + def "next", proc (a: seq[Value]): Value = | ||
| 289 | + let s = toSeq(a[0]) | ||
| 290 | + (if s.len <= 1: NilV else: mkList(s[1 .. ^1])) | ||
| 291 | + def "nth", proc (a: seq[Value]): Value = | ||
| 292 | + let s = toSeq(a[0]) | ||
| 293 | + let i = int(intOf(a[1])) | ||
| 294 | + if i >= 0 and i < s.len: s[i] | ||
| 295 | + elif a.len > 2: a[2] | ||
| 296 | + else: err("Index out of bounds: " & $i) | ||
| 297 | + def "get", proc (a: seq[Value]): Value = | ||
| 298 | + getIn(a[0], a[1], (if a.len > 2: a[2] else: NilV)) | ||
| 299 | + def "get-in", proc (a: seq[Value]): Value = | ||
| 300 | + var cur = a[0] | ||
| 301 | + for k in toSeq(a[1]): | ||
| 302 | + cur = getIn(cur, k, NilV) | ||
| 303 | + (if (cur.isNil or cur.kind == kNil) and a.len > 2: a[2] else: cur) | ||
| 304 | + def "assoc", proc (a: seq[Value]): Value = | ||
| 305 | + result = a[0] | ||
| 306 | + var i = 1 | ||
| 307 | + while i + 1 < a.len: | ||
| 308 | + result = assocOne(result, a[i], a[i + 1]); i += 2 | ||
| 309 | + def "dissoc", proc (a: seq[Value]): Value = | ||
| 310 | + var ps = a[0].pairs | ||
| 311 | + for i in 1 ..< a.len: | ||
| 312 | + var keep: seq[(Value, Value)] = @[] | ||
| 313 | + for (k, v) in ps: | ||
| 314 | + if not equals(k, a[i]): keep.add (k, v) | ||
| 315 | + ps = keep | ||
| 316 | + Value(kind: kMap, pairs: ps) | ||
| 317 | + def "update", proc (a: seq[Value]): Value = | ||
| 318 | + let cur = getIn(a[0], a[1], NilV) | ||
| 319 | + assocOne(a[0], a[1], call(a[2], @[cur] & a[3 .. ^1])) | ||
| 320 | + def "keys", proc (a: seq[Value]): Value = | ||
| 321 | + var r: seq[Value] = @[] | ||
| 322 | + for (k, _) in a[0].pairs: r.add k | ||
| 323 | + (if r.len == 0: NilV else: mkList(r)) | ||
| 324 | + def "vals", proc (a: seq[Value]): Value = | ||
| 325 | + var r: seq[Value] = @[] | ||
| 326 | + for (_, v) in a[0].pairs: r.add v | ||
| 327 | + (if r.len == 0: NilV else: mkList(r)) | ||
| 328 | + def "reverse", proc (a: seq[Value]): Value = | ||
| 329 | + var s = toSeq(a[0]) | ||
| 330 | + var r: seq[Value] = @[] | ||
| 331 | + for i in countdown(s.len - 1, 0): r.add s[i] | ||
| 332 | + mkList(r) | ||
| 333 | + def "range", proc (a: seq[Value]): Value = | ||
| 334 | + var lo: int64 = 0 | ||
| 335 | + var hi: int64 = 0 | ||
| 336 | + var step: int64 = 1 | ||
| 337 | + if a.len == 1: hi = intOf(a[0]) | ||
| 338 | + elif a.len >= 2: | ||
| 339 | + lo = intOf(a[0]); hi = intOf(a[1]) | ||
| 340 | + if a.len > 2: step = intOf(a[2]) | ||
| 341 | + var r: seq[Value] = @[] | ||
| 342 | + if step > 0: | ||
| 343 | + var i = lo | ||
| 344 | + while i < hi: r.add mkInt(i); i += step | ||
| 345 | + elif step < 0: | ||
| 346 | + var i = lo | ||
| 347 | + while i > hi: r.add mkInt(i); i += step | ||
| 348 | + mkList(r) | ||
| 349 | + def "take", proc (a: seq[Value]): Value = | ||
| 350 | + let n = int(intOf(a[0])) | ||
| 351 | + let s = toSeq(a[1]) | ||
| 352 | + mkList(s[0 ..< min(n, s.len)]) | ||
| 353 | + def "drop", proc (a: seq[Value]): Value = | ||
| 354 | + let n = int(intOf(a[0])) | ||
| 355 | + let s = toSeq(a[1]) | ||
| 356 | + (if n >= s.len: mkList(@[]) else: mkList(s[n .. ^1])) | ||
| 357 | + def "concat", proc (a: seq[Value]): Value = | ||
| 358 | + var r: seq[Value] = @[] | ||
| 359 | + for x in a: r.add toSeq(x) | ||
| 360 | + mkList(r) | ||
| 361 | + def "sort", proc (a: seq[Value]): Value = | ||
| 362 | + var s = toSeq(a[^1]) | ||
| 363 | + let cmpFn = (if a.len > 1: a[0] else: NilV) | ||
| 364 | + # insertion sort keeps it simple and stable | ||
| 365 | + for i in 1 ..< s.len: | ||
| 366 | + var j = i | ||
| 367 | + while j > 0: | ||
| 368 | + let before = | ||
| 369 | + if cmpFn.kind == kFn: truthy(call(cmpFn, @[s[j], s[j - 1]])) | ||
| 370 | + elif s[j].kind == kStr: s[j].s < s[j - 1].s | ||
| 371 | + else: num(s[j]) < num(s[j - 1]) | ||
| 372 | + if not before: break | ||
| 373 | + swap(s[j], s[j - 1]); dec j | ||
| 374 | + mkList(s) | ||
| 375 | + def "sort-by", proc (a: seq[Value]): Value = | ||
| 376 | + var s = toSeq(a[^1]) | ||
| 377 | + let kf = a[0] | ||
| 378 | + for i in 1 ..< s.len: | ||
| 379 | + var j = i | ||
| 380 | + while j > 0: | ||
| 381 | + let ka = call(kf, @[s[j]]) | ||
| 382 | + let kb = call(kf, @[s[j - 1]]) | ||
| 383 | + let before = (if ka.kind == kStr: ka.s < kb.s else: num(ka) < num(kb)) | ||
| 384 | + if not before: break | ||
| 385 | + swap(s[j], s[j - 1]); dec j | ||
| 386 | + mkList(s) | ||
| 387 | + def "distinct", proc (a: seq[Value]): Value = | ||
| 388 | + var r: seq[Value] = @[] | ||
| 389 | + for x in toSeq(a[0]): | ||
| 390 | + var dup = false | ||
| 391 | + for y in r: | ||
| 392 | + if equals(x, y): dup = true; break | ||
| 393 | + if not dup: r.add x | ||
| 394 | + mkList(r) | ||
| 395 | + def "interpose", proc (a: seq[Value]): Value = | ||
| 396 | + var r: seq[Value] = @[] | ||
| 397 | + for x in toSeq(a[1]): | ||
| 398 | + if r.len > 0: r.add a[0] | ||
| 399 | + r.add x | ||
| 400 | + mkList(r) | ||
| 401 | + def "partition", proc (a: seq[Value]): Value = | ||
| 402 | + let n = int(intOf(a[0])) | ||
| 403 | + let s = toSeq(a[^1]) | ||
| 404 | + var r: seq[Value] = @[] | ||
| 405 | + var i = 0 | ||
| 406 | + while i + n <= s.len: | ||
| 407 | + r.add mkList(s[i ..< i + n]); i += n | ||
| 408 | + mkList(r) | ||
| 409 | + | ||
| 410 | + # ---- higher order | ||
| 411 | + def "apply", proc (a: seq[Value]): Value = | ||
| 412 | + var callArgs: seq[Value] = @[] | ||
| 413 | + for i in 1 ..< a.len - 1: callArgs.add a[i] | ||
| 414 | + callArgs.add toSeq(a[^1]) | ||
| 415 | + call(a[0], callArgs) | ||
| 416 | + def "map", proc (a: seq[Value]): Value = | ||
| 417 | + let f = a[0] | ||
| 418 | + if a.len == 2: | ||
| 419 | + var r: seq[Value] = @[] | ||
| 420 | + for x in toSeq(a[1]): r.add call(f, @[x]) | ||
| 421 | + return mkList(r) | ||
| 422 | + var colls: seq[seq[Value]] = @[] | ||
| 423 | + for i in 1 ..< a.len: colls.add toSeq(a[i]) | ||
| 424 | + var n = colls[0].len | ||
| 425 | + for c in colls: n = min(n, c.len) | ||
| 426 | + var r: seq[Value] = @[] | ||
| 427 | + for i in 0 ..< n: | ||
| 428 | + var args: seq[Value] = @[] | ||
| 429 | + for c in colls: args.add c[i] | ||
| 430 | + r.add call(f, args) | ||
| 431 | + mkList(r) | ||
| 432 | + def "mapv", proc (a: seq[Value]): Value = | ||
| 433 | + var r: seq[Value] = @[] | ||
| 434 | + for x in toSeq(a[1]): r.add call(a[0], @[x]) | ||
| 435 | + mkVector(r) | ||
| 436 | + def "map-indexed", proc (a: seq[Value]): Value = | ||
| 437 | + var r: seq[Value] = @[] | ||
| 438 | + var i = 0 | ||
| 439 | + for x in toSeq(a[1]): | ||
| 440 | + r.add call(a[0], @[mkInt(i), x]); inc i | ||
| 441 | + mkList(r) | ||
| 442 | + def "filter", proc (a: seq[Value]): Value = | ||
| 443 | + var r: seq[Value] = @[] | ||
| 444 | + for x in toSeq(a[1]): | ||
| 445 | + if truthy(call(a[0], @[x])): r.add x | ||
| 446 | + mkList(r) | ||
| 447 | + def "remove", proc (a: seq[Value]): Value = | ||
| 448 | + var r: seq[Value] = @[] | ||
| 449 | + for x in toSeq(a[1]): | ||
| 450 | + if not truthy(call(a[0], @[x])): r.add x | ||
| 451 | + mkList(r) | ||
| 452 | + def "reduce", proc (a: seq[Value]): Value = | ||
| 453 | + let f = a[0] | ||
| 454 | + if a.len == 2: | ||
| 455 | + let s = toSeq(a[1]) | ||
| 456 | + if s.len == 0: return call(f, @[]) | ||
| 457 | + var acc = s[0] | ||
| 458 | + for i in 1 ..< s.len: acc = call(f, @[acc, s[i]]) | ||
| 459 | + return acc | ||
| 460 | + var acc = a[1] | ||
| 461 | + for x in toSeq(a[2]): acc = call(f, @[acc, x]) | ||
| 462 | + acc | ||
| 463 | + def "some", proc (a: seq[Value]): Value = | ||
| 464 | + for x in toSeq(a[1]): | ||
| 465 | + let r = call(a[0], @[x]) | ||
| 466 | + if truthy(r): return r | ||
| 467 | + NilV | ||
| 468 | + def "every?", proc (a: seq[Value]): Value = | ||
| 469 | + for x in toSeq(a[1]): | ||
| 470 | + if not truthy(call(a[0], @[x])): return FalseV | ||
| 471 | + TrueV | ||
| 472 | + def "take-while", proc (a: seq[Value]): Value = | ||
| 473 | + var r: seq[Value] = @[] | ||
| 474 | + for x in toSeq(a[1]): | ||
| 475 | + if not truthy(call(a[0], @[x])): break | ||
| 476 | + r.add x | ||
| 477 | + mkList(r) | ||
| 478 | + def "drop-while", proc (a: seq[Value]): Value = | ||
| 479 | + var r: seq[Value] = @[] | ||
| 480 | + var dropping = true | ||
| 481 | + for x in toSeq(a[1]): | ||
| 482 | + if dropping and truthy(call(a[0], @[x])): continue | ||
| 483 | + dropping = false | ||
| 484 | + r.add x | ||
| 485 | + mkList(r) | ||
| 486 | + def "group-by", proc (a: seq[Value]): Value = | ||
| 487 | + var m: Value = Value(kind: kMap, pairs: @[]) | ||
| 488 | + for x in toSeq(a[1]): | ||
| 489 | + let k = call(a[0], @[x]) | ||
| 490 | + let cur = getIn(m, k, mkVector(@[])) | ||
| 491 | + m = assocOne(m, k, conjOne(cur, x)) | ||
| 492 | + m | ||
| 493 | + def "frequencies", proc (a: seq[Value]): Value = | ||
| 494 | + var m: Value = Value(kind: kMap, pairs: @[]) | ||
| 495 | + for x in toSeq(a[0]): | ||
| 496 | + let cur = getIn(m, x, mkInt(0)) | ||
| 497 | + m = assocOne(m, x, mkInt(cur.i + 1)) | ||
| 498 | + m | ||
| 499 | + def "identity", proc (a: seq[Value]): Value = a[0] | ||
| 500 | + def "comp", proc (a: seq[Value]): Value = | ||
| 501 | + let fs = a | ||
| 502 | + mkFn("comp", proc (args: seq[Value]): Value = | ||
| 503 | + if fs.len == 0: return argAt(args, 0) | ||
| 504 | + var v = call(fs[^1], args) | ||
| 505 | + for i in countdown(fs.len - 2, 0): v = call(fs[i], @[v]) | ||
| 506 | + v) | ||
| 507 | + def "partial", proc (a: seq[Value]): Value = | ||
| 508 | + let f = a[0] | ||
| 509 | + let bound = a[1 .. ^1] | ||
| 510 | + mkFn("partial", proc (args: seq[Value]): Value = call(f, bound & args)) | ||
| 511 | + def "juxt", proc (a: seq[Value]): Value = | ||
| 512 | + let fs = a | ||
| 513 | + mkFn("juxt", proc (args: seq[Value]): Value = | ||
| 514 | + var r: seq[Value] = @[] | ||
| 515 | + for f in fs: r.add call(f, args) | ||
| 516 | + mkVector(r)) | ||
| 517 | + def "constantly", proc (a: seq[Value]): Value = | ||
| 518 | + let v = a[0] | ||
| 519 | + mkFn("constantly", proc (args: seq[Value]): Value = v) | ||
| 520 | + | ||
| 521 | + # ---- atoms (mutable boxes, modelled as a 1-slot vector) | ||
| 522 | + def "atom", proc (a: seq[Value]): Value = | ||
| 523 | + var cell = a[0] | ||
| 524 | + mkFn("atom", proc (args: seq[Value]): Value = | ||
| 525 | + # (a) -> deref | ||
| 526 | + # (a :set v) -> reset | ||
| 527 | + if args.len == 0: return cell | ||
| 528 | + cell = args[1] | ||
| 529 | + cell) | ||
| 530 | + def "deref", proc (a: seq[Value]): Value = call(a[0], @[]) | ||
| 531 | + def "reset!", proc (a: seq[Value]): Value = call(a[0], @[mkKeyword("set"), a[1]]) | ||
| 532 | + def "swap!", proc (a: seq[Value]): Value = | ||
| 533 | + let cur = call(a[0], @[]) | ||
| 534 | + let nv = call(a[1], @[cur] & a[2 .. ^1]) | ||
| 535 | + call(a[0], @[mkKeyword("set"), nv]) | ||
| 536 | + | ||
| 537 | + def "throw", proc (a: seq[Value]): Value = err(str(a[0])) | ||
| 538 | + def "ex-info", proc (a: seq[Value]): Value = mkStr(str(a[0])) | ||
| 539 | + def "time-ms", proc (a: seq[Value]): Value = mkInt(int64(epochTime() * 1000)) | ||
added
src/reader.nim +153 -0 | new file mode 100644 | ||
| @@ -0,0 +1,153 @@ | ||
| 1 | +## clonim reader — text -> data (forms are ordinary runtime Values, as in Clojure). | |
| 2 | +import std/[strutils] | |
| 3 | +import runtime | |
| 4 | + | |
| 5 | +type | |
| 6 | + Reader = object | |
| 7 | + src: string | |
| 8 | + pos: int | |
| 9 | + line: int | |
| 10 | + | |
| 11 | +proc peek(r: Reader): char = | |
| 12 | + (if r.pos < r.src.len: r.src[r.pos] else: '\0') | |
| 13 | + | |
| 14 | +proc peek2(r: Reader): char = | |
| 15 | + (if r.pos + 1 < r.src.len: r.src[r.pos + 1] else: '\0') | |
| 16 | + | |
| 17 | +proc advance(r: var Reader): char = | |
| 18 | + result = r.src[r.pos] | |
| 19 | + if result == '\n': inc r.line | |
| 20 | + inc r.pos | |
| 21 | + | |
| 22 | +proc readerErr(r: Reader, msg: string) {.noreturn.} = | |
| 23 | + err("Reader error (line " & $r.line & "): " & msg) | |
| 24 | + | |
| 25 | +const macroChars = {'(', ')', '[', ']', '{', '}', '"', ';', '\'', '`', '~', '@', '^'} | |
| 26 | + | |
| 27 | +proc skipWs(r: var Reader) = | |
| 28 | + while r.pos < r.src.len: | |
| 29 | + let c = r.peek | |
| 30 | + if c in {' ', '\t', '\n', '\r', ','}: | |
| 31 | + discard r.advance | |
| 32 | + elif c == ';': | |
| 33 | + while r.pos < r.src.len and r.peek != '\n': discard r.advance | |
| 34 | + else: | |
| 35 | + break | |
| 36 | + | |
| 37 | +proc readForm(r: var Reader): Value | |
| 38 | + | |
| 39 | +proc readDelimited(r: var Reader, closing: char): seq[Value] = | |
| 40 | + result = @[] | |
| 41 | + while true: | |
| 42 | + r.skipWs | |
| 43 | + if r.pos >= r.src.len: r.readerErr("EOF while reading, expected '" & closing & "'") | |
| 44 | + if r.peek == closing: | |
| 45 | + discard r.advance | |
| 46 | + return | |
| 47 | + result.add r.readForm | |
| 48 | + | |
| 49 | +proc readString(r: var Reader): Value = | |
| 50 | + discard r.advance # opening quote | |
| 51 | + var s = "" | |
| 52 | + while true: | |
| 53 | + if r.pos >= r.src.len: r.readerErr("EOF while reading string") | |
| 54 | + let c = r.advance | |
| 55 | + if c == '"': break | |
| 56 | + if c == '\\': | |
| 57 | + let e = r.advance | |
| 58 | + case e | |
| 59 | + of 'n': s.add '\n' | |
| 60 | + of 't': s.add '\t' | |
| 61 | + of 'r': s.add '\r' | |
| 62 | + of '\\': s.add '\\' | |
| 63 | + of '"': s.add '"' | |
| 64 | + of '0': s.add '\0' | |
| 65 | + else: r.readerErr("Unsupported escape: \\" & e) | |
| 66 | + else: | |
| 67 | + s.add c | |
| 68 | + mkStr(s) | |
| 69 | + | |
| 70 | +proc readToken(r: var Reader): string = | |
| 71 | + result = "" | |
| 72 | + while r.pos < r.src.len: | |
| 73 | + let c = r.peek | |
| 74 | + if c in {' ', '\t', '\n', '\r', ','} or (c in macroChars and c != '\''): | |
| 75 | + break | |
| 76 | + result.add r.advance | |
| 77 | + | |
| 78 | +proc parseAtom(r: Reader, tok: string): Value = | |
| 79 | + if tok == "nil": return NilV | |
| 80 | + if tok == "true": return TrueV | |
| 81 | + if tok == "false": return FalseV | |
| 82 | + if tok.len > 1 and tok[0] == ':': return mkKeyword(tok[1 .. ^1]) | |
| 83 | + # number? | |
| 84 | + let body = (if tok[0] in {'-', '+'} and tok.len > 1: tok[1 .. ^1] else: tok) | |
| 85 | + if body.len > 0 and body[0] in Digits: | |
| 86 | + if '.' in tok or 'e' in tok or 'E' in tok: | |
| 87 | + try: return mkFloat(parseFloat(tok)) | |
| 88 | + except ValueError: discard | |
| 89 | + else: | |
| 90 | + try: return mkInt(parseBiggestInt(tok)) | |
| 91 | + except ValueError: discard | |
| 92 | + mkSymbol(tok) | |
| 93 | + | |
| 94 | +proc readForm(r: var Reader): Value = | |
| 95 | + r.skipWs | |
| 96 | + if r.pos >= r.src.len: r.readerErr("EOF while reading") | |
| 97 | + let c = r.peek | |
| 98 | + case c | |
| 99 | + of '(': | |
| 100 | + discard r.advance | |
| 101 | + return mkList(r.readDelimited(')')) | |
| 102 | + of '[': | |
| 103 | + discard r.advance | |
| 104 | + return mkVector(r.readDelimited(']')) | |
| 105 | + of '{': | |
| 106 | + discard r.advance | |
| 107 | + let xs = r.readDelimited('}') | |
| 108 | + if xs.len mod 2 != 0: r.readerErr("Map literal must contain an even number of forms") | |
| 109 | + var ps: seq[(Value, Value)] = @[] | |
| 110 | + var i = 0 | |
| 111 | + while i < xs.len: | |
| 112 | + ps.add (xs[i], xs[i + 1]); i += 2 | |
| 113 | + return Value(kind: kMap, pairs: ps) | |
| 114 | + of ')', ']', '}': | |
| 115 | + r.readerErr("Unmatched delimiter: " & c) | |
| 116 | + of '"': | |
| 117 | + return r.readString | |
| 118 | + of '\'': | |
| 119 | + discard r.advance | |
| 120 | + return mkList(@[mkSymbol("quote"), r.readForm]) | |
| 121 | + of '@': | |
| 122 | + discard r.advance | |
| 123 | + return mkList(@[mkSymbol("deref"), r.readForm]) | |
| 124 | + of '^': | |
| 125 | + # metadata: read it and discard | |
| 126 | + discard r.advance | |
| 127 | + discard r.readForm | |
| 128 | + return r.readForm | |
| 129 | + of '#': | |
| 130 | + if r.peek2 == '{': | |
| 131 | + discard r.advance; discard r.advance | |
| 132 | + return mkSet(r.readDelimited('}')) | |
| 133 | + if r.peek2 == '_': | |
| 134 | + discard r.advance; discard r.advance | |
| 135 | + discard r.readForm | |
| 136 | + r.skipWs | |
| 137 | + return r.readForm | |
| 138 | + if r.peek2 == '(': | |
| 139 | + r.readerErr("#() anonymous fn literals are not supported; use (fn [x] ...)") | |
| 140 | + r.readerErr("Unsupported dispatch: #" & r.peek2 | |
| 141 | + ) | |
| 142 | + else: | |
| 143 | + let tok = r.readToken | |
| 144 | + if tok.len == 0: r.readerErr("Unexpected character: " & c) | |
| 145 | + return r.parseAtom(tok) | |
| 146 | + | |
| 147 | +proc readAll*(src: string): seq[Value] = | |
| 148 | + var r = Reader(src: src, pos: 0, line: 1) | |
| 149 | + result = @[] | |
| 150 | + while true: | |
| 151 | + r.skipWs | |
| 152 | + if r.pos >= r.src.len: break | |
| 153 | + result.add r.readForm | |
| new file mode 100644 | |||
| @@ -0,0 +1,153 @@ | |||
| 1 | +## clonim reader — text -> data (forms are ordinary runtime Values, as in Clojure). | ||
| 2 | +import std/[strutils] | ||
| 3 | +import runtime | ||
| 4 | + | ||
| 5 | +type | ||
| 6 | + Reader = object | ||
| 7 | + src: string | ||
| 8 | + pos: int | ||
| 9 | + line: int | ||
| 10 | + | ||
| 11 | +proc peek(r: Reader): char = | ||
| 12 | + (if r.pos < r.src.len: r.src[r.pos] else: '\0') | ||
| 13 | + | ||
| 14 | +proc peek2(r: Reader): char = | ||
| 15 | + (if r.pos + 1 < r.src.len: r.src[r.pos + 1] else: '\0') | ||
| 16 | + | ||
| 17 | +proc advance(r: var Reader): char = | ||
| 18 | + result = r.src[r.pos] | ||
| 19 | + if result == '\n': inc r.line | ||
| 20 | + inc r.pos | ||
| 21 | + | ||
| 22 | +proc readerErr(r: Reader, msg: string) {.noreturn.} = | ||
| 23 | + err("Reader error (line " & $r.line & "): " & msg) | ||
| 24 | + | ||
| 25 | +const macroChars = {'(', ')', '[', ']', '{', '}', '"', ';', '\'', '`', '~', '@', '^'} | ||
| 26 | + | ||
| 27 | +proc skipWs(r: var Reader) = | ||
| 28 | + while r.pos < r.src.len: | ||
| 29 | + let c = r.peek | ||
| 30 | + if c in {' ', '\t', '\n', '\r', ','}: | ||
| 31 | + discard r.advance | ||
| 32 | + elif c == ';': | ||
| 33 | + while r.pos < r.src.len and r.peek != '\n': discard r.advance | ||
| 34 | + else: | ||
| 35 | + break | ||
| 36 | + | ||
| 37 | +proc readForm(r: var Reader): Value | ||
| 38 | + | ||
| 39 | +proc readDelimited(r: var Reader, closing: char): seq[Value] = | ||
| 40 | + result = @[] | ||
| 41 | + while true: | ||
| 42 | + r.skipWs | ||
| 43 | + if r.pos >= r.src.len: r.readerErr("EOF while reading, expected '" & closing & "'") | ||
| 44 | + if r.peek == closing: | ||
| 45 | + discard r.advance | ||
| 46 | + return | ||
| 47 | + result.add r.readForm | ||
| 48 | + | ||
| 49 | +proc readString(r: var Reader): Value = | ||
| 50 | + discard r.advance # opening quote | ||
| 51 | + var s = "" | ||
| 52 | + while true: | ||
| 53 | + if r.pos >= r.src.len: r.readerErr("EOF while reading string") | ||
| 54 | + let c = r.advance | ||
| 55 | + if c == '"': break | ||
| 56 | + if c == '\\': | ||
| 57 | + let e = r.advance | ||
| 58 | + case e | ||
| 59 | + of 'n': s.add '\n' | ||
| 60 | + of 't': s.add '\t' | ||
| 61 | + of 'r': s.add '\r' | ||
| 62 | + of '\\': s.add '\\' | ||
| 63 | + of '"': s.add '"' | ||
| 64 | + of '0': s.add '\0' | ||
| 65 | + else: r.readerErr("Unsupported escape: \\" & e) | ||
| 66 | + else: | ||
| 67 | + s.add c | ||
| 68 | + mkStr(s) | ||
| 69 | + | ||
| 70 | +proc readToken(r: var Reader): string = | ||
| 71 | + result = "" | ||
| 72 | + while r.pos < r.src.len: | ||
| 73 | + let c = r.peek | ||
| 74 | + if c in {' ', '\t', '\n', '\r', ','} or (c in macroChars and c != '\''): | ||
| 75 | + break | ||
| 76 | + result.add r.advance | ||
| 77 | + | ||
| 78 | +proc parseAtom(r: Reader, tok: string): Value = | ||
| 79 | + if tok == "nil": return NilV | ||
| 80 | + if tok == "true": return TrueV | ||
| 81 | + if tok == "false": return FalseV | ||
| 82 | + if tok.len > 1 and tok[0] == ':': return mkKeyword(tok[1 .. ^1]) | ||
| 83 | + # number? | ||
| 84 | + let body = (if tok[0] in {'-', '+'} and tok.len > 1: tok[1 .. ^1] else: tok) | ||
| 85 | + if body.len > 0 and body[0] in Digits: | ||
| 86 | + if '.' in tok or 'e' in tok or 'E' in tok: | ||
| 87 | + try: return mkFloat(parseFloat(tok)) | ||
| 88 | + except ValueError: discard | ||
| 89 | + else: | ||
| 90 | + try: return mkInt(parseBiggestInt(tok)) | ||
| 91 | + except ValueError: discard | ||
| 92 | + mkSymbol(tok) | ||
| 93 | + | ||
| 94 | +proc readForm(r: var Reader): Value = | ||
| 95 | + r.skipWs | ||
| 96 | + if r.pos >= r.src.len: r.readerErr("EOF while reading") | ||
| 97 | + let c = r.peek | ||
| 98 | + case c | ||
| 99 | + of '(': | ||
| 100 | + discard r.advance | ||
| 101 | + return mkList(r.readDelimited(')')) | ||
| 102 | + of '[': | ||
| 103 | + discard r.advance | ||
| 104 | + return mkVector(r.readDelimited(']')) | ||
| 105 | + of '{': | ||
| 106 | + discard r.advance | ||
| 107 | + let xs = r.readDelimited('}') | ||
| 108 | + if xs.len mod 2 != 0: r.readerErr("Map literal must contain an even number of forms") | ||
| 109 | + var ps: seq[(Value, Value)] = @[] | ||
| 110 | + var i = 0 | ||
| 111 | + while i < xs.len: | ||
| 112 | + ps.add (xs[i], xs[i + 1]); i += 2 | ||
| 113 | + return Value(kind: kMap, pairs: ps) | ||
| 114 | + of ')', ']', '}': | ||
| 115 | + r.readerErr("Unmatched delimiter: " & c) | ||
| 116 | + of '"': | ||
| 117 | + return r.readString | ||
| 118 | + of '\'': | ||
| 119 | + discard r.advance | ||
| 120 | + return mkList(@[mkSymbol("quote"), r.readForm]) | ||
| 121 | + of '@': | ||
| 122 | + discard r.advance | ||
| 123 | + return mkList(@[mkSymbol("deref"), r.readForm]) | ||
| 124 | + of '^': | ||
| 125 | + # metadata: read it and discard | ||
| 126 | + discard r.advance | ||
| 127 | + discard r.readForm | ||
| 128 | + return r.readForm | ||
| 129 | + of '#': | ||
| 130 | + if r.peek2 == '{': | ||
| 131 | + discard r.advance; discard r.advance | ||
| 132 | + return mkSet(r.readDelimited('}')) | ||
| 133 | + if r.peek2 == '_': | ||
| 134 | + discard r.advance; discard r.advance | ||
| 135 | + discard r.readForm | ||
| 136 | + r.skipWs | ||
| 137 | + return r.readForm | ||
| 138 | + if r.peek2 == '(': | ||
| 139 | + r.readerErr("#() anonymous fn literals are not supported; use (fn [x] ...)") | ||
| 140 | + r.readerErr("Unsupported dispatch: #" & r.peek2 | ||
| 141 | + ) | ||
| 142 | + else: | ||
| 143 | + let tok = r.readToken | ||
| 144 | + if tok.len == 0: r.readerErr("Unexpected character: " & c) | ||
| 145 | + return r.parseAtom(tok) | ||
| 146 | + | ||
| 147 | +proc readAll*(src: string): seq[Value] = | ||
| 148 | + var r = Reader(src: src, pos: 0, line: 1) | ||
| 149 | + result = @[] | ||
| 150 | + while true: | ||
| 151 | + r.skipWs | ||
| 152 | + if r.pos >= r.src.len: break | ||
| 153 | + result.add r.readForm | ||
added
src/runtime.nim +230 -0 | new file mode 100644 | ||
| @@ -0,0 +1,230 @@ | ||
| 1 | +## clonim runtime — persistent-ish Clojure values for compiled Nim code. | |
| 2 | +import std/[tables, strutils] | |
| 3 | + | |
| 4 | +type | |
| 5 | + Kind* = enum | |
| 6 | + kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol, | |
| 7 | + kList, kVector, kMap, kSet, kFn | |
| 8 | + | |
| 9 | + Value* = ref object | |
| 10 | + case kind*: Kind | |
| 11 | + of kNil: discard | |
| 12 | + of kBool: b*: bool | |
| 13 | + of kInt: i*: int64 | |
| 14 | + of kFloat: f*: float64 | |
| 15 | + of kStr, kKeyword, kSymbol: s*: string | |
| 16 | + of kList, kVector, kSet: items*: seq[Value] | |
| 17 | + of kMap: pairs*: seq[(Value, Value)] | |
| 18 | + of kFn: | |
| 19 | + fn*: proc (args: seq[Value]): Value {.closure.} | |
| 20 | + name*: string | |
| 21 | + | |
| 22 | + CljError* = object of CatchableError | |
| 23 | + | |
| 24 | +let NilV* = Value(kind: kNil) | |
| 25 | +let TrueV* = Value(kind: kBool, b: true) | |
| 26 | +let FalseV* = Value(kind: kBool, b: false) | |
| 27 | + | |
| 28 | +proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV) | |
| 29 | +proc mkInt*(x: int64): Value = Value(kind: kInt, i: x) | |
| 30 | +proc mkFloat*(x: float64): Value = Value(kind: kFloat, f: x) | |
| 31 | +proc mkStr*(x: string): Value = Value(kind: kStr, s: x) | |
| 32 | +proc mkKeyword*(x: string): Value = Value(kind: kKeyword, s: x) | |
| 33 | +proc mkSymbol*(x: string): Value = Value(kind: kSymbol, s: x) | |
| 34 | +proc mkList*(xs: seq[Value]): Value = Value(kind: kList, items: xs) | |
| 35 | +proc mkVector*(xs: seq[Value]): Value = Value(kind: kVector, items: xs) | |
| 36 | +proc mkSet*(xs: seq[Value]): Value | |
| 37 | +proc mkFn*(name: string, f: proc (args: seq[Value]): Value {.closure.}): Value = | |
| 38 | + Value(kind: kFn, fn: f, name: name) | |
| 39 | + | |
| 40 | +proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg) | |
| 41 | + | |
| 42 | +proc truthy*(v: Value): bool = | |
| 43 | + if v == nil: return false | |
| 44 | + case v.kind | |
| 45 | + of kNil: false | |
| 46 | + of kBool: v.b | |
| 47 | + else: true | |
| 48 | + | |
| 49 | +# ---------------------------------------------------------------- equality | |
| 50 | +proc equals*(a, b: Value): bool = | |
| 51 | + if a.isNil or b.isNil: return a.isNil and b.isNil | |
| 52 | + # numeric tower: int and float compare across types | |
| 53 | + if a.kind == kInt and b.kind == kFloat: return float64(a.i) == b.f | |
| 54 | + if a.kind == kFloat and b.kind == kInt: return a.f == float64(b.i) | |
| 55 | + # lists and vectors are sequentially equal in Clojure | |
| 56 | + if a.kind in {kList, kVector} and b.kind in {kList, kVector}: | |
| 57 | + if a.items.len != b.items.len: return false | |
| 58 | + for i in 0 ..< a.items.len: | |
| 59 | + if not equals(a.items[i], b.items[i]): return false | |
| 60 | + return true | |
| 61 | + if a.kind != b.kind: return false | |
| 62 | + case a.kind | |
| 63 | + of kNil: true | |
| 64 | + of kBool: a.b == b.b | |
| 65 | + of kInt: a.i == b.i | |
| 66 | + of kFloat: a.f == b.f | |
| 67 | + of kStr, kKeyword, kSymbol: a.s == b.s | |
| 68 | + of kSet: | |
| 69 | + if a.items.len != b.items.len: return false | |
| 70 | + for x in a.items: | |
| 71 | + var found = false | |
| 72 | + for y in b.items: | |
| 73 | + if equals(x, y): found = true; break | |
| 74 | + if not found: return false | |
| 75 | + true | |
| 76 | + of kMap: | |
| 77 | + if a.pairs.len != b.pairs.len: return false | |
| 78 | + for (k, v) in a.pairs: | |
| 79 | + var found = false | |
| 80 | + for (k2, v2) in b.pairs: | |
| 81 | + if equals(k, k2): | |
| 82 | + if not equals(v, v2): return false | |
| 83 | + found = true; break | |
| 84 | + if not found: return false | |
| 85 | + true | |
| 86 | + of kFn: a == b | |
| 87 | + of kList, kVector: false # handled above | |
| 88 | + | |
| 89 | +proc mkSet*(xs: seq[Value]): Value = | |
| 90 | + var acc: seq[Value] = @[] | |
| 91 | + for x in xs: | |
| 92 | + var dup = false | |
| 93 | + for y in acc: | |
| 94 | + if equals(x, y): dup = true; break | |
| 95 | + if not dup: acc.add x | |
| 96 | + Value(kind: kSet, items: acc) | |
| 97 | + | |
| 98 | +# ---------------------------------------------------------------- printing | |
| 99 | +proc escapeStr(s: string): string = | |
| 100 | + result = "\"" | |
| 101 | + for c in s: | |
| 102 | + case c | |
| 103 | + of '"': result.add "\\\"" | |
| 104 | + of '\\': result.add "\\\\" | |
| 105 | + of '\n': result.add "\\n" | |
| 106 | + of '\t': result.add "\\t" | |
| 107 | + of '\r': result.add "\\r" | |
| 108 | + else: result.add c | |
| 109 | + result.add "\"" | |
| 110 | + | |
| 111 | +proc toStr*(v: Value, readable: bool): string = | |
| 112 | + if v.isNil: return "nil" | |
| 113 | + case v.kind | |
| 114 | + of kNil: "nil" | |
| 115 | + of kBool: (if v.b: "true" else: "false") | |
| 116 | + of kInt: $v.i | |
| 117 | + of kFloat: | |
| 118 | + var s = $v.f | |
| 119 | + if '.' notin s and 'e' notin s and 'n' notin s and 'i' notin s: s &= ".0" | |
| 120 | + s | |
| 121 | + of kStr: (if readable: escapeStr(v.s) else: v.s) | |
| 122 | + of kKeyword: ":" & v.s | |
| 123 | + of kSymbol: v.s | |
| 124 | + of kList: | |
| 125 | + var parts: seq[string] = @[] | |
| 126 | + for x in v.items: parts.add toStr(x, readable) | |
| 127 | + "(" & parts.join(" ") & ")" | |
| 128 | + of kVector: | |
| 129 | + var parts: seq[string] = @[] | |
| 130 | + for x in v.items: parts.add toStr(x, readable) | |
| 131 | + "[" & parts.join(" ") & "]" | |
| 132 | + of kSet: | |
| 133 | + var parts: seq[string] = @[] | |
| 134 | + for x in v.items: parts.add toStr(x, readable) | |
| 135 | + "#{" & parts.join(" ") & "}" | |
| 136 | + of kMap: | |
| 137 | + var parts: seq[string] = @[] | |
| 138 | + for (k, val) in v.pairs: parts.add toStr(k, readable) & " " & toStr(val, readable) | |
| 139 | + "{" & parts.join(", ") & "}" | |
| 140 | + of kFn: "#<fn " & v.name & ">" | |
| 141 | + | |
| 142 | +proc prStr*(v: Value): string = toStr(v, true) | |
| 143 | +proc str*(v: Value): string = toStr(v, false) | |
| 144 | + | |
| 145 | +# ---------------------------------------------------------------- vars | |
| 146 | +## Vars are cells, as in Clojure: compiled code resolves the cell once and | |
| 147 | +## reads through it, so a call site costs one pointer deref, not a hash lookup, | |
| 148 | +## while `def` can still rebind the var later. | |
| 149 | +type VarCell* = ref object | |
| 150 | + name*: string | |
| 151 | + bound*: bool | |
| 152 | + v*: Value | |
| 153 | + | |
| 154 | +var globals*: Table[string, VarCell] = initTable[string, VarCell]() | |
| 155 | + | |
| 156 | +proc varCell*(name: string): VarCell = | |
| 157 | + if globals.hasKey(name): return globals[name] | |
| 158 | + result = VarCell(name: name, bound: false, v: NilV) | |
| 159 | + globals[name] = result | |
| 160 | + | |
| 161 | +proc setVar*(name: string, v: Value): Value {.discardable.} = | |
| 162 | + let c = varCell(name) | |
| 163 | + c.v = v | |
| 164 | + c.bound = true | |
| 165 | + v | |
| 166 | + | |
| 167 | +proc cellGet*(c: VarCell): Value {.inline.} = | |
| 168 | + if not c.bound: err("Unable to resolve symbol: " & c.name) | |
| 169 | + c.v | |
| 170 | + | |
| 171 | +proc getVar*(name: string): Value = | |
| 172 | + cellGet(varCell(name)) | |
| 173 | + | |
| 174 | +proc hasVar*(name: string): bool = | |
| 175 | + globals.hasKey(name) and globals[name].bound | |
| 176 | + | |
| 177 | +# ---------------------------------------------------------------- calling | |
| 178 | +proc call*(f: Value, args: seq[Value]): Value = | |
| 179 | + if f.isNil: err("Can't call nil") | |
| 180 | + case f.kind | |
| 181 | + of kFn: f.fn(args) | |
| 182 | + of kKeyword: | |
| 183 | + # (:k m) => lookup | |
| 184 | + if args.len == 0: err("Wrong number of args to keyword") | |
| 185 | + let m = args[0] | |
| 186 | + if m.isNil or m.kind != kMap: return NilV | |
| 187 | + for (k, v) in m.pairs: | |
| 188 | + if equals(k, f): return v | |
| 189 | + (if args.len > 1: args[1] else: NilV) | |
| 190 | + of kMap: | |
| 191 | + if args.len == 0: err("Wrong number of args to map") | |
| 192 | + for (k, v) in f.pairs: | |
| 193 | + if equals(k, args[0]): return v | |
| 194 | + (if args.len > 1: args[1] else: NilV) | |
| 195 | + of kVector: | |
| 196 | + if args.len != 1 or args[0].kind != kInt: err("Vector lookup needs one int") | |
| 197 | + let i = int(args[0].i) | |
| 198 | + if i < 0 or i >= f.items.len: err("Index out of bounds: " & $i) | |
| 199 | + f.items[i] | |
| 200 | + else: err("Can't call value of kind " & $f.kind & ": " & prStr(f)) | |
| 201 | + | |
| 202 | +proc argAt*(args: seq[Value], i: int): Value = | |
| 203 | + if i < args.len: args[i] else: NilV | |
| 204 | + | |
| 205 | +proc restArgs*(args: seq[Value], i: int): Value = | |
| 206 | + if i >= args.len: return NilV | |
| 207 | + mkList(args[i .. ^1]) | |
| 208 | + | |
| 209 | +proc arity*(name: string, args: seq[Value], n: int) = | |
| 210 | + if args.len != n: | |
| 211 | + err("Wrong number of args (" & $args.len & ") passed to " & name) | |
| 212 | + | |
| 213 | +# ---------------------------------------------------------------- seqs | |
| 214 | +proc toSeq*(v: Value): seq[Value] = | |
| 215 | + if v.isNil: return @[] | |
| 216 | + case v.kind | |
| 217 | + of kNil: @[] | |
| 218 | + of kList, kVector, kSet: v.items | |
| 219 | + of kStr: | |
| 220 | + var r: seq[Value] = @[] | |
| 221 | + for c in v.s: r.add mkStr($c) | |
| 222 | + r | |
| 223 | + of kMap: | |
| 224 | + var r: seq[Value] = @[] | |
| 225 | + for (k, val) in v.pairs: r.add mkVector(@[k, val]) | |
| 226 | + r | |
| 227 | + else: err("Don't know how to create seq from: " & prStr(v)) | |
| 228 | + | |
| 229 | +proc mkMap*(ps: seq[(Value, Value)]): Value = Value(kind: kMap, pairs: ps) | |
| 230 | +let emptyArgs*: seq[Value] = @[] | |
| new file mode 100644 | |||
| @@ -0,0 +1,230 @@ | |||
| 1 | +## clonim runtime — persistent-ish Clojure values for compiled Nim code. | ||
| 2 | +import std/[tables, strutils] | ||
| 3 | + | ||
| 4 | +type | ||
| 5 | + Kind* = enum | ||
| 6 | + kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol, | ||
| 7 | + kList, kVector, kMap, kSet, kFn | ||
| 8 | + | ||
| 9 | + Value* = ref object | ||
| 10 | + case kind*: Kind | ||
| 11 | + of kNil: discard | ||
| 12 | + of kBool: b*: bool | ||
| 13 | + of kInt: i*: int64 | ||
| 14 | + of kFloat: f*: float64 | ||
| 15 | + of kStr, kKeyword, kSymbol: s*: string | ||
| 16 | + of kList, kVector, kSet: items*: seq[Value] | ||
| 17 | + of kMap: pairs*: seq[(Value, Value)] | ||
| 18 | + of kFn: | ||
| 19 | + fn*: proc (args: seq[Value]): Value {.closure.} | ||
| 20 | + name*: string | ||
| 21 | + | ||
| 22 | + CljError* = object of CatchableError | ||
| 23 | + | ||
| 24 | +let NilV* = Value(kind: kNil) | ||
| 25 | +let TrueV* = Value(kind: kBool, b: true) | ||
| 26 | +let FalseV* = Value(kind: kBool, b: false) | ||
| 27 | + | ||
| 28 | +proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV) | ||
| 29 | +proc mkInt*(x: int64): Value = Value(kind: kInt, i: x) | ||
| 30 | +proc mkFloat*(x: float64): Value = Value(kind: kFloat, f: x) | ||
| 31 | +proc mkStr*(x: string): Value = Value(kind: kStr, s: x) | ||
| 32 | +proc mkKeyword*(x: string): Value = Value(kind: kKeyword, s: x) | ||
| 33 | +proc mkSymbol*(x: string): Value = Value(kind: kSymbol, s: x) | ||
| 34 | +proc mkList*(xs: seq[Value]): Value = Value(kind: kList, items: xs) | ||
| 35 | +proc mkVector*(xs: seq[Value]): Value = Value(kind: kVector, items: xs) | ||
| 36 | +proc mkSet*(xs: seq[Value]): Value | ||
| 37 | +proc mkFn*(name: string, f: proc (args: seq[Value]): Value {.closure.}): Value = | ||
| 38 | + Value(kind: kFn, fn: f, name: name) | ||
| 39 | + | ||
| 40 | +proc err*(msg: string) {.noreturn.} = raise newException(CljError, msg) | ||
| 41 | + | ||
| 42 | +proc truthy*(v: Value): bool = | ||
| 43 | + if v == nil: return false | ||
| 44 | + case v.kind | ||
| 45 | + of kNil: false | ||
| 46 | + of kBool: v.b | ||
| 47 | + else: true | ||
| 48 | + | ||
| 49 | +# ---------------------------------------------------------------- equality | ||
| 50 | +proc equals*(a, b: Value): bool = | ||
| 51 | + if a.isNil or b.isNil: return a.isNil and b.isNil | ||
| 52 | + # numeric tower: int and float compare across types | ||
| 53 | + if a.kind == kInt and b.kind == kFloat: return float64(a.i) == b.f | ||
| 54 | + if a.kind == kFloat and b.kind == kInt: return a.f == float64(b.i) | ||
| 55 | + # lists and vectors are sequentially equal in Clojure | ||
| 56 | + if a.kind in {kList, kVector} and b.kind in {kList, kVector}: | ||
| 57 | + if a.items.len != b.items.len: return false | ||
| 58 | + for i in 0 ..< a.items.len: | ||
| 59 | + if not equals(a.items[i], b.items[i]): return false | ||
| 60 | + return true | ||
| 61 | + if a.kind != b.kind: return false | ||
| 62 | + case a.kind | ||
| 63 | + of kNil: true | ||
| 64 | + of kBool: a.b == b.b | ||
| 65 | + of kInt: a.i == b.i | ||
| 66 | + of kFloat: a.f == b.f | ||
| 67 | + of kStr, kKeyword, kSymbol: a.s == b.s | ||
| 68 | + of kSet: | ||
| 69 | + if a.items.len != b.items.len: return false | ||
| 70 | + for x in a.items: | ||
| 71 | + var found = false | ||
| 72 | + for y in b.items: | ||
| 73 | + if equals(x, y): found = true; break | ||
| 74 | + if not found: return false | ||
| 75 | + true | ||
| 76 | + of kMap: | ||
| 77 | + if a.pairs.len != b.pairs.len: return false | ||
| 78 | + for (k, v) in a.pairs: | ||
| 79 | + var found = false | ||
| 80 | + for (k2, v2) in b.pairs: | ||
| 81 | + if equals(k, k2): | ||
| 82 | + if not equals(v, v2): return false | ||
| 83 | + found = true; break | ||
| 84 | + if not found: return false | ||
| 85 | + true | ||
| 86 | + of kFn: a == b | ||
| 87 | + of kList, kVector: false # handled above | ||
| 88 | + | ||
| 89 | +proc mkSet*(xs: seq[Value]): Value = | ||
| 90 | + var acc: seq[Value] = @[] | ||
| 91 | + for x in xs: | ||
| 92 | + var dup = false | ||
| 93 | + for y in acc: | ||
| 94 | + if equals(x, y): dup = true; break | ||
| 95 | + if not dup: acc.add x | ||
| 96 | + Value(kind: kSet, items: acc) | ||
| 97 | + | ||
| 98 | +# ---------------------------------------------------------------- printing | ||
| 99 | +proc escapeStr(s: string): string = | ||
| 100 | + result = "\"" | ||
| 101 | + for c in s: | ||
| 102 | + case c | ||
| 103 | + of '"': result.add "\\\"" | ||
| 104 | + of '\\': result.add "\\\\" | ||
| 105 | + of '\n': result.add "\\n" | ||
| 106 | + of '\t': result.add "\\t" | ||
| 107 | + of '\r': result.add "\\r" | ||
| 108 | + else: result.add c | ||
| 109 | + result.add "\"" | ||
| 110 | + | ||
| 111 | +proc toStr*(v: Value, readable: bool): string = | ||
| 112 | + if v.isNil: return "nil" | ||
| 113 | + case v.kind | ||
| 114 | + of kNil: "nil" | ||
| 115 | + of kBool: (if v.b: "true" else: "false") | ||
| 116 | + of kInt: $v.i | ||
| 117 | + of kFloat: | ||
| 118 | + var s = $v.f | ||
| 119 | + if '.' notin s and 'e' notin s and 'n' notin s and 'i' notin s: s &= ".0" | ||
| 120 | + s | ||
| 121 | + of kStr: (if readable: escapeStr(v.s) else: v.s) | ||
| 122 | + of kKeyword: ":" & v.s | ||
| 123 | + of kSymbol: v.s | ||
| 124 | + of kList: | ||
| 125 | + var parts: seq[string] = @[] | ||
| 126 | + for x in v.items: parts.add toStr(x, readable) | ||
| 127 | + "(" & parts.join(" ") & ")" | ||
| 128 | + of kVector: | ||
| 129 | + var parts: seq[string] = @[] | ||
| 130 | + for x in v.items: parts.add toStr(x, readable) | ||
| 131 | + "[" & parts.join(" ") & "]" | ||
| 132 | + of kSet: | ||
| 133 | + var parts: seq[string] = @[] | ||
| 134 | + for x in v.items: parts.add toStr(x, readable) | ||
| 135 | + "#{" & parts.join(" ") & "}" | ||
| 136 | + of kMap: | ||
| 137 | + var parts: seq[string] = @[] | ||
| 138 | + for (k, val) in v.pairs: parts.add toStr(k, readable) & " " & toStr(val, readable) | ||
| 139 | + "{" & parts.join(", ") & "}" | ||
| 140 | + of kFn: "#<fn " & v.name & ">" | ||
| 141 | + | ||
| 142 | +proc prStr*(v: Value): string = toStr(v, true) | ||
| 143 | +proc str*(v: Value): string = toStr(v, false) | ||
| 144 | + | ||
| 145 | +# ---------------------------------------------------------------- vars | ||
| 146 | +## Vars are cells, as in Clojure: compiled code resolves the cell once and | ||
| 147 | +## reads through it, so a call site costs one pointer deref, not a hash lookup, | ||
| 148 | +## while `def` can still rebind the var later. | ||
| 149 | +type VarCell* = ref object | ||
| 150 | + name*: string | ||
| 151 | + bound*: bool | ||
| 152 | + v*: Value | ||
| 153 | + | ||
| 154 | +var globals*: Table[string, VarCell] = initTable[string, VarCell]() | ||
| 155 | + | ||
| 156 | +proc varCell*(name: string): VarCell = | ||
| 157 | + if globals.hasKey(name): return globals[name] | ||
| 158 | + result = VarCell(name: name, bound: false, v: NilV) | ||
| 159 | + globals[name] = result | ||
| 160 | + | ||
| 161 | +proc setVar*(name: string, v: Value): Value {.discardable.} = | ||
| 162 | + let c = varCell(name) | ||
| 163 | + c.v = v | ||
| 164 | + c.bound = true | ||
| 165 | + v | ||
| 166 | + | ||
| 167 | +proc cellGet*(c: VarCell): Value {.inline.} = | ||
| 168 | + if not c.bound: err("Unable to resolve symbol: " & c.name) | ||
| 169 | + c.v | ||
| 170 | + | ||
| 171 | +proc getVar*(name: string): Value = | ||
| 172 | + cellGet(varCell(name)) | ||
| 173 | + | ||
| 174 | +proc hasVar*(name: string): bool = | ||
| 175 | + globals.hasKey(name) and globals[name].bound | ||
| 176 | + | ||
| 177 | +# ---------------------------------------------------------------- calling | ||
| 178 | +proc call*(f: Value, args: seq[Value]): Value = | ||
| 179 | + if f.isNil: err("Can't call nil") | ||
| 180 | + case f.kind | ||
| 181 | + of kFn: f.fn(args) | ||
| 182 | + of kKeyword: | ||
| 183 | + # (:k m) => lookup | ||
| 184 | + if args.len == 0: err("Wrong number of args to keyword") | ||
| 185 | + let m = args[0] | ||
| 186 | + if m.isNil or m.kind != kMap: return NilV | ||
| 187 | + for (k, v) in m.pairs: | ||
| 188 | + if equals(k, f): return v | ||
| 189 | + (if args.len > 1: args[1] else: NilV) | ||
| 190 | + of kMap: | ||
| 191 | + if args.len == 0: err("Wrong number of args to map") | ||
| 192 | + for (k, v) in f.pairs: | ||
| 193 | + if equals(k, args[0]): return v | ||
| 194 | + (if args.len > 1: args[1] else: NilV) | ||
| 195 | + of kVector: | ||
| 196 | + if args.len != 1 or args[0].kind != kInt: err("Vector lookup needs one int") | ||
| 197 | + let i = int(args[0].i) | ||
| 198 | + if i < 0 or i >= f.items.len: err("Index out of bounds: " & $i) | ||
| 199 | + f.items[i] | ||
| 200 | + else: err("Can't call value of kind " & $f.kind & ": " & prStr(f)) | ||
| 201 | + | ||
| 202 | +proc argAt*(args: seq[Value], i: int): Value = | ||
| 203 | + if i < args.len: args[i] else: NilV | ||
| 204 | + | ||
| 205 | +proc restArgs*(args: seq[Value], i: int): Value = | ||
| 206 | + if i >= args.len: return NilV | ||
| 207 | + mkList(args[i .. ^1]) | ||
| 208 | + | ||
| 209 | +proc arity*(name: string, args: seq[Value], n: int) = | ||
| 210 | + if args.len != n: | ||
| 211 | + err("Wrong number of args (" & $args.len & ") passed to " & name) | ||
| 212 | + | ||
| 213 | +# ---------------------------------------------------------------- seqs | ||
| 214 | +proc toSeq*(v: Value): seq[Value] = | ||
| 215 | + if v.isNil: return @[] | ||
| 216 | + case v.kind | ||
| 217 | + of kNil: @[] | ||
| 218 | + of kList, kVector, kSet: v.items | ||
| 219 | + of kStr: | ||
| 220 | + var r: seq[Value] = @[] | ||
| 221 | + for c in v.s: r.add mkStr($c) | ||
| 222 | + r | ||
| 223 | + of kMap: | ||
| 224 | + var r: seq[Value] = @[] | ||
| 225 | + for (k, val) in v.pairs: r.add mkVector(@[k, val]) | ||
| 226 | + r | ||
| 227 | + else: err("Don't know how to create seq from: " & prStr(v)) | ||
| 228 | + | ||
| 229 | +proc mkMap*(ps: seq[(Value, Value)]): Value = Value(kind: kMap, pairs: ps) | ||
| 230 | +let emptyArgs*: seq[Value] = @[] | ||
added
tests/hello.expected +4 -0 | new file mode 100644 | ||
| @@ -0,0 +1,4 @@ | ||
| 1 | +Hello, world! | |
| 2 | +6 42 2 2.5 | |
| 3 | +(2 3 4) (0 2 4 6 8) | |
| 4 | +{:a 1, :b [2 3]} #{1 2} (quoted list) | |
| new file mode 100644 | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | +Hello, world! | ||
| 2 | +6 42 2 2.5 | ||
| 3 | +(2 3 4) (0 2 4 6 8) | ||
| 4 | +{:a 1, :b [2 3]} #{1 2} (quoted list) | ||
added
tests/tour.expected +14 -0 | new file mode 100644 | ||
| @@ -0,0 +1,14 @@ | ||
| 1 | +sum 1..1e6 = 500000500000 | |
| 2 | +add5 10 = 15 | |
| 3 | +hi stranger | hi ann | hi ann and 2 others | |
| 4 | +20! = 2432902008176640000 | |
| 5 | +1 2 (3 4 5) 10 20 | |
| 6 | +16 | |
| 7 | +1330 | |
| 8 | +counter = 5 | |
| 9 | +(bo ada cy) | |
| 10 | +{:adult [{:name ada, :age 36} {:name cy, :age 52}], :kid [{:name bo, :age 9}]} | |
| 11 | +(zero negative even odd) | |
| 12 | +caught: Divide by zero | |
| 13 | +42 | |
| 14 | +5050 | |
| new file mode 100644 | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | +sum 1..1e6 = 500000500000 | ||
| 2 | +add5 10 = 15 | ||
| 3 | +hi stranger | hi ann | hi ann and 2 others | ||
| 4 | +20! = 2432902008176640000 | ||
| 5 | +1 2 (3 4 5) 10 20 | ||
| 6 | +16 | ||
| 7 | +1330 | ||
| 8 | +counter = 5 | ||
| 9 | +(bo ada cy) | ||
| 10 | +{:adult [{:name ada, :age 36} {:name cy, :age 52}], :kid [{:name bo, :age 9}]} | ||
| 11 | +(zero negative even odd) | ||
| 12 | +caught: Divide by zero | ||
| 13 | +42 | ||
| 14 | +5050 | ||