nandi/clonimpublic Fork 0
980e0ef
Commits
Clone
git clone https://git.rickub.com/nandi/clonim.git
git clone ssh://git@rickub.com/nandi/clonim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

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>
nandi committed 2026-09-20T15:31:41-07:00 Browse files
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