nandi/clonimpublic Fork
⑂ d177210
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.

Download ZIP

feat: add protocols and core language features

nandithebull committed 2026-09-22T16:58:48-07:00 Browse files
d177210 parent: 4332e9a
modified README.md +17 -17
@@ -111,7 +111,7 @@ Clojure's small array-maps are, without giving up hashed lookup.
111111 The set column is the honest shape of the old representation: `conj` rebuilt the
112112 whole set and re-scanned it for duplicates, so building one was O(n³).
113113
114-**Seqs are lazy.** `map`, `filter`, `remove`, `range`, `take`, `drop`,
114+**Seqs are lazy and chunked.** `map`, `filter`, `remove`, `range`, `take`, `drop`,
115115 `take-while`, `drop-while`, `concat`, `map-indexed`, `iterate`, `repeat`,
116116 `repeatedly` and `cycle` return a chain of thunks: each element is computed on
117117 first demand and memoized, so infinite seqs are ordinary values and a consumer
@@ -129,12 +129,10 @@ builtin never materializes more of a seq than it was asked for. `nth`, `first`,
129129 need; `count`, `reduce` and printing realize the whole seq, which is what those
130130 mean.
131131
132-The cost is the usual one: a fully realized lazy seq allocates a cons cell and
133-a thunk per element, where the eager version filled one flat `seq`. Realizing
134-all of `(map inc (range 1000000))` went from 290 ms to 570 ms. Clojure buys most
135-of that back by realizing in 32-element chunks; clonim does not chunk yet, which
136-is why its laziness is exact — `take 3` computes exactly three elements, not
137-thirty-two.
132+Like Clojure, the main lazy producers realize 32 elements at a time. This
133+amortizes thunk and allocation overhead; it also means that demanding three
134+mapped elements may execute the mapping function for the first 32. Chunks and
135+ordinary cons sequences interoperate through the same cursor API.
138136
139137 Long chains need one piece of care. ARC frees a linked structure by recursing
140138 into it, so dropping a million-element seq means a million destructor frames and
@@ -160,21 +158,23 @@ serialize queued state transitions; this initial implementation is cooperative,
160158 so queued actions run when an agent is dereferenced, awaited, or inspected for
161159 an error rather than on a background thread.
162160
161+Refs: `ref`, `dosync`, `alter`, `commute`, `ref-set`, and `ensure`. Because the
162+runtime is single-threaded, transactions are serialized rather than retried.
163+
164+Transducers: the map/filter/remove composition path works with `transduce`,
165+`sequence`, `eduction`, and three-argument `into`.
166+
167+Protocols: `defprotocol`, `extend-type`, `extend-protocol`, and `satisfies?`
168+dispatch over clonim's runtime value kinds, with `Object` as the fallback type.
169+
163170 Lazy seqs: `iterate` `repeat` `repeatedly` `cycle` `doall` `dorun`, and the seq
164171 library above returns them where Clojure does.
165172
166173 ## What doesn't (yet)
167174
168-- **Syntax-quote.** User macros can construct forms with `quote` and ordinary
169- core functions such as `list`, `vector`, and `apply`, but automatic
170- namespace qualification through syntax-quote/unquote is not implemented yet.
171-- **Chunked seqs.** Lazy seqs are unchunked, so full realization allocates two
172- cells per element and runs ~2× slower than the old eager path. 32-element
173- chunking is the fix, at the cost of exact demand.
174-- **Destructuring in parameter lists.** `(let [[a b] xs] …)` works; `(defn f
175- [[a b]] …)` does not.
176-- Protocols/records, dynamic namespace operations, refs/agents,
177- `#()` literals, syntax-quote, transducers, Nim interop.
175+- Records, dynamic namespace operations, and general Nim interop.
176+- The full transducer surface (including reduced values and lazy `sequence`).
177+- Reader conditionals and the full JVM reader/type surface.
178178
179179 ## Tests
180180
@@ -111,7 +111,7 @@ Clojure's small array-maps are, without giving up hashed lookup.
111 The set column is the honest shape of the old representation: `conj` rebuilt the111 The set column is the honest shape of the old representation: `conj` rebuilt the
112 whole set and re-scanned it for duplicates, so building one was O(n³).112 whole set and re-scanned it for duplicates, so building one was O(n³).
113 113
114-**Seqs are lazy.** `map`, `filter`, `remove`, `range`, `take`, `drop`,114+**Seqs are lazy and chunked.** `map`, `filter`, `remove`, `range`, `take`, `drop`,
115 `take-while`, `drop-while`, `concat`, `map-indexed`, `iterate`, `repeat`,115 `take-while`, `drop-while`, `concat`, `map-indexed`, `iterate`, `repeat`,
116 `repeatedly` and `cycle` return a chain of thunks: each element is computed on116 `repeatedly` and `cycle` return a chain of thunks: each element is computed on
117 first demand and memoized, so infinite seqs are ordinary values and a consumer117 first demand and memoized, so infinite seqs are ordinary values and a consumer
@@ -129,12 +129,10 @@ builtin never materializes more of a seq than it was asked for. `nth`, `first`,
129 need; `count`, `reduce` and printing realize the whole seq, which is what those129 need; `count`, `reduce` and printing realize the whole seq, which is what those
130 mean.130 mean.
131 131
132-The cost is the usual one: a fully realized lazy seq allocates a cons cell and132+Like Clojure, the main lazy producers realize 32 elements at a time. This
133-a thunk per element, where the eager version filled one flat `seq`. Realizing133+amortizes thunk and allocation overhead; it also means that demanding three
134-all of `(map inc (range 1000000))` went from 290 ms to 570 ms. Clojure buys most134+mapped elements may execute the mapping function for the first 32. Chunks and
135-of that back by realizing in 32-element chunks; clonim does not chunk yet, which135+ordinary cons sequences interoperate through the same cursor API.
136-is why its laziness is exact — `take 3` computes exactly three elements, not
137-thirty-two.
138 136
139 Long chains need one piece of care. ARC frees a linked structure by recursing137 Long chains need one piece of care. ARC frees a linked structure by recursing
140 into it, so dropping a million-element seq means a million destructor frames and138 into it, so dropping a million-element seq means a million destructor frames and
@@ -160,21 +158,23 @@ serialize queued state transitions; this initial implementation is cooperative,
160 so queued actions run when an agent is dereferenced, awaited, or inspected for158 so queued actions run when an agent is dereferenced, awaited, or inspected for
161 an error rather than on a background thread.159 an error rather than on a background thread.
162 160
161+Refs: `ref`, `dosync`, `alter`, `commute`, `ref-set`, and `ensure`. Because the
162+runtime is single-threaded, transactions are serialized rather than retried.
163+
164+Transducers: the map/filter/remove composition path works with `transduce`,
165+`sequence`, `eduction`, and three-argument `into`.
166+
167+Protocols: `defprotocol`, `extend-type`, `extend-protocol`, and `satisfies?`
168+dispatch over clonim's runtime value kinds, with `Object` as the fallback type.
169+
163 Lazy seqs: `iterate` `repeat` `repeatedly` `cycle` `doall` `dorun`, and the seq170 Lazy seqs: `iterate` `repeat` `repeatedly` `cycle` `doall` `dorun`, and the seq
164 library above returns them where Clojure does.171 library above returns them where Clojure does.
165 172
166 ## What doesn't (yet)173 ## What doesn't (yet)
167 174
168-- **Syntax-quote.** User macros can construct forms with `quote` and ordinary175+- Records, dynamic namespace operations, and general Nim interop.
169- core functions such as `list`, `vector`, and `apply`, but automatic176+- The full transducer surface (including reduced values and lazy `sequence`).
170- namespace qualification through syntax-quote/unquote is not implemented yet.177+- Reader conditionals and the full JVM reader/type surface.
171-- **Chunked seqs.** Lazy seqs are unchunked, so full realization allocates two
172- cells per element and runs ~2× slower than the old eager path. 32-element
173- chunking is the fix, at the cost of exact demand.
174-- **Destructuring in parameter lists.** `(let [[a b] xs] …)` works; `(defn f
175- [[a b]] …)` does not.
176-- Protocols/records, dynamic namespace operations, refs/agents,
177- `#()` literals, syntax-quote, transducers, Nim interop.
178 178
179 ## Tests179 ## Tests
180 180
added examples/core_gaps.clj +22 -0
new file mode 100644
@@ -0,0 +1,22 @@
1+(ns core-gaps)
2+
3+;; syntax quote qualifies symbols, honors unquote/splicing, and keeps one
4+;; auto-gensym binding stable throughout an expansion.
5+(defmacro unless [test then else] `(if ~test ~else ~then))
6+(defmacro twice [x] `(let [value# ~x] (+ value# value#)))
7+(defmacro packed [x & xs] `[~x ~@xs])
8+(println (unless false :yes :no))
9+(println (twice 9))
10+(println (packed 1 2 3))
11+(println `not-yet-defined)
12+
13+;; The initial transducer surface composes without intermediate collections.
14+(def xf (comp (filter odd?) (map inc)))
15+(println (transduce xf + 0 (range 10)))
16+(println (sequence xf (range 6)))
17+(println (into [] xf (range 6)))
18+
19+;; Refs are serialized cells in clonim's single-threaded runtime.
20+(def total (ref 1))
21+(println (dosync (alter total + 4) (commute total * 2) (ensure total)))
22+(println (dosync (ref-set total 7)) (deref total))
new file mode 100644
@@ -0,0 +1,22 @@
1+(ns core-gaps)
2+
3+;; syntax quote qualifies symbols, honors unquote/splicing, and keeps one
4+;; auto-gensym binding stable throughout an expansion.
5+(defmacro unless [test then else] `(if ~test ~else ~then))
6+(defmacro twice [x] `(let [value# ~x] (+ value# value#)))
7+(defmacro packed [x & xs] `[~x ~@xs])
8+(println (unless false :yes :no))
9+(println (twice 9))
10+(println (packed 1 2 3))
11+(println `not-yet-defined)
12+
13+;; The initial transducer surface composes without intermediate collections.
14+(def xf (comp (filter odd?) (map inc)))
15+(println (transduce xf + 0 (range 10)))
16+(println (sequence xf (range 6)))
17+(println (into [] xf (range 6)))
18+
19+;; Refs are serialized cells in clonim's single-threaded runtime.
20+(def total (ref 1))
21+(println (dosync (alter total + 4) (commute total * 2) (ensure total)))
22+(println (dosync (ref-set total 7)) (deref total))
added examples/protocols.clj +30 -0
new file mode 100644
@@ -0,0 +1,30 @@
1+(ns protocols)
2+
3+(defprotocol Describable
4+ (describe [this])
5+ (combine [this other]))
6+
7+(extend-protocol Describable
8+ Long
9+ (describe [this] (str "integer:" this))
10+ (combine [this other] (+ this other))
11+
12+ String
13+ (describe [this] (str "string:" this))
14+ (combine [this other] (str this other))
15+
16+ Object
17+ (describe [this] (str "other:" this))
18+ (combine [this other] [this other]))
19+
20+(println (describe 4) (combine 4 5))
21+(println (describe "a") (combine "a" "b"))
22+(println (describe :x) (combine :x :y))
23+(println (satisfies? Describable 1)
24+ (satisfies? Describable "x")
25+ (satisfies? Describable []))
26+
27+(defprotocol Sized (size-of [this]))
28+(extend-type clojure.lang.IPersistentVector Sized
29+ (size-of [this] (count this)))
30+(println (size-of [1 2 3]) (satisfies? Sized {}) (satisfies? Sized []))
new file mode 100644
@@ -0,0 +1,30 @@
1+(ns protocols)
2+
3+(defprotocol Describable
4+ (describe [this])
5+ (combine [this other]))
6+
7+(extend-protocol Describable
8+ Long
9+ (describe [this] (str "integer:" this))
10+ (combine [this other] (+ this other))
11+
12+ String
13+ (describe [this] (str "string:" this))
14+ (combine [this other] (str this other))
15+
16+ Object
17+ (describe [this] (str "other:" this))
18+ (combine [this other] [this other]))
19+
20+(println (describe 4) (combine 4 5))
21+(println (describe "a") (combine "a" "b"))
22+(println (describe :x) (combine :x :y))
23+(println (satisfies? Describable 1)
24+ (satisfies? Describable "x")
25+ (satisfies? Describable []))
26+
27+(defprotocol Sized (size-of [this]))
28+(extend-type clojure.lang.IPersistentVector Sized
29+ (size-of [this] (count this)))
30+(println (size-of [1 2 3]) (satisfies? Sized {}) (satisfies? Sized []))
modified examples/user_macros.clj +13 -0
@@ -9,7 +9,20 @@
99
1010 (defmacro nothing [] nil)
1111
12+(defmacro unless-sq [test then else]
13+ `(if ~test ~else ~then))
14+
15+(defmacro collect [x & xs]
16+ `[~x ~@xs])
17+
18+(defmacro twice [expr]
19+ `(let [value# ~expr] (+ value# value#)))
20+
1221 (println (unless false :yes :no))
1322 (call-list println "variadic" 3)
1423 (println (form-and-env))
1524 (println (nil? (nothing)))
25+(println (unless-sq false :syntax :quote))
26+(println (collect 1 2 3 4))
27+(println (twice 6))
28+(println `quoted-symbol)
@@ -9,7 +9,20 @@
9 9
10 (defmacro nothing [] nil)10 (defmacro nothing [] nil)
11 11
12+(defmacro unless-sq [test then else]
13+ `(if ~test ~else ~then))
14+
15+(defmacro collect [x & xs]
16+ `[~x ~@xs])
17+
18+(defmacro twice [expr]
19+ `(let [value# ~expr] (+ value# value#)))
20+
12 (println (unless false :yes :no))21 (println (unless false :yes :no))
13 (call-list println "variadic" 3)22 (call-list println "variadic" 3)
14 (println (form-and-env))23 (println (form-and-env))
15 (println (nil? (nothing)))24 (println (nil? (nothing)))
25+(println (unless-sq false :syntax :quote))
26+(println (collect 1 2 3 4))
27+(println (twice 6))
28+(println `quoted-symbol)
modified src/app_runtime_source.nim +16 -0
@@ -5,3 +5,19 @@ import runtime, core, namespaces
55 export runtime, core, namespaces
66
77 proc initClonimRuntime*() = discard
8+
9+# Keep the source-runtime surface identical to app_runtime.nim. The release
10+# compiler embeds this module, while development builds link app_runtime.nim's
11+# C wrappers; generated programs should not need to distinguish the two.
12+proc registerCoreArithmetic*(names: openArray[string]) =
13+ registerCoreArithmeticSelected(names)
14+proc registerCorePredicates*(names: openArray[string]) =
15+ registerCorePredicatesSelected(names)
16+proc registerCoreStringsIo*(names: openArray[string]) =
17+ registerCoreStringsIoSelected(names)
18+proc registerCoreCollections*(names: openArray[string]) =
19+ registerCoreCollectionsSelected(names)
20+proc registerCoreHigherOrder*(names: openArray[string]) =
21+ registerCoreHigherOrderSelected(names)
22+proc registerCoreStateHost*(names: openArray[string]) =
23+ registerCoreStateHostSelected(names)
@@ -5,3 +5,19 @@ import runtime, core, namespaces
5 export runtime, core, namespaces5 export runtime, core, namespaces
6 6
7 proc initClonimRuntime*() = discard7 proc initClonimRuntime*() = discard
8+
9+# Keep the source-runtime surface identical to app_runtime.nim. The release
10+# compiler embeds this module, while development builds link app_runtime.nim's
11+# C wrappers; generated programs should not need to distinguish the two.
12+proc registerCoreArithmetic*(names: openArray[string]) =
13+ registerCoreArithmeticSelected(names)
14+proc registerCorePredicates*(names: openArray[string]) =
15+ registerCorePredicatesSelected(names)
16+proc registerCoreStringsIo*(names: openArray[string]) =
17+ registerCoreStringsIoSelected(names)
18+proc registerCoreCollections*(names: openArray[string]) =
19+ registerCoreCollectionsSelected(names)
20+proc registerCoreHigherOrder*(names: openArray[string]) =
21+ registerCoreHigherOrderSelected(names)
22+proc registerCoreStateHost*(names: openArray[string]) =
23+ registerCoreStateHostSelected(names)
modified src/compiler.nim +4 -1
@@ -143,7 +143,7 @@ const specialHeads = ["quote", "if", "do", "let", "let*", "loop", "loop*",
143143 "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or",
144144 "when", "when-not", "if-not", "cond", "when-let", "if-let", "->", "->>",
145145 "doseq", "dotimes", "try", "comment", "ns", "require", "in-ns", "use",
146- "import", "set!", "declare", "binding"].toHashSet
146+ "import", "set!", "declare", "binding", "dosync"].toHashSet
147147
148148 const intOps = {"+": "+", "-": "-", "*": "*"}.toTable
149149 const intCalls = {"quot": "idiv", "rem": "irem"}.toTable
@@ -1014,6 +1014,9 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) =
10141014 of "do":
10151015 genBody(args, dst, env, c)
10161016 return
1017+ of "dosync":
1018+ genBody(args, dst, env, c)
1019+ return
10171020 of "let", "let*":
10181021 if args.len == 0: err("let requires bindings")
10191022 genLet(args[0], args[1 .. ^1], dst, env, c)
@@ -143,7 +143,7 @@ const specialHeads = ["quote", "if", "do", "let", "let*", "loop", "loop*",
143 "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or",143 "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or",
144 "when", "when-not", "if-not", "cond", "when-let", "if-let", "->", "->>",144 "when", "when-not", "if-not", "cond", "when-let", "if-let", "->", "->>",
145 "doseq", "dotimes", "try", "comment", "ns", "require", "in-ns", "use",145 "doseq", "dotimes", "try", "comment", "ns", "require", "in-ns", "use",
146- "import", "set!", "declare", "binding"].toHashSet146+ "import", "set!", "declare", "binding", "dosync"].toHashSet
147 147
148 const intOps = {"+": "+", "-": "-", "*": "*"}.toTable148 const intOps = {"+": "+", "-": "-", "*": "*"}.toTable
149 const intCalls = {"quot": "idiv", "rem": "irem"}.toTable149 const intCalls = {"quot": "idiv", "rem": "irem"}.toTable
@@ -1014,6 +1014,9 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) =
1014 of "do":1014 of "do":
1015 genBody(args, dst, env, c)1015 genBody(args, dst, env, c)
1016 return1016 return
1017+ of "dosync":
1018+ genBody(args, dst, env, c)
1019+ return
1017 of "let", "let*":1020 of "let", "let*":
1018 if args.len == 0: err("let requires bindings")1021 if args.len == 0: err("let requires bindings")
1019 genLet(args[0], args[1 .. ^1], dst, env, c)1022 genLet(args[0], args[1 .. ^1], dst, env, c)
modified src/core.nim +148 -1
@@ -5,6 +5,50 @@ import runtime
55
66 var selectedCore: HashSet[string]
77 var selectingCore = false
8+var protocolImpls = initTable[string, Value]()
9+var protocolMethods = initTable[string, HashSet[string]]()
10+
11+proc protocolTypeName(v: Value): string =
12+ case v.kind
13+ of kNil: "nil"
14+ of kBool: "boolean"
15+ of kInt: "integer"
16+ of kFloat: "float"
17+ of kChar: "character"
18+ of kStr: "string"
19+ of kKeyword: "keyword"
20+ of kSymbol: "symbol"
21+ of kList: "list"
22+ of kVector: "vector"
23+ of kMap: "map"
24+ of kSet: "set"
25+ of kCons, kChunk, kLazy: "seq"
26+ of kFn: "function"
27+ of kAgent: "agent"
28+
29+proc normalizedProtocolType(v: Value): string =
30+ let raw = str(v).split('/')[^1].split('.')[^1]
31+ case raw.toLowerAscii
32+ of "nil": "nil"
33+ of "boolean", "bool": "boolean"
34+ of "long", "integer", "int": "integer"
35+ of "double", "float": "float"
36+ of "character", "char": "character"
37+ of "string": "string"
38+ of "keyword": "keyword"
39+ of "symbol": "symbol"
40+ of "list", "ipersistentlist": "list"
41+ of "vector", "ipersistentvector": "vector"
42+ of "map", "ipersistentmap": "map"
43+ of "set", "ipersistentset": "set"
44+ of "iseq", "seq", "lazyseq": "seq"
45+ of "ifn", "function", "fn": "function"
46+ of "agent": "agent"
47+ of "object", "default": "default"
48+ else: raw.toLowerAscii
49+
50+proc protocolKey(protocol, methodName, typ: string): string =
51+ protocol & "\x1f" & methodName & "\x1f" & typ
852
953 proc num(v: Value): float64 =
1054 case v.kind
@@ -992,8 +1036,13 @@ proc registerCoreCollections*() =
9921036 mkList(r)
9931037 def "into", proc (a: openArray[Value]): Value =
9941038 result = a[0]
995- if a.len > 1:
1039+ if a.len == 2:
9961040 for x in elems(a[1]): result = conjOne(result, x)
1041+ elif a.len == 3:
1042+ let step = mkFn("conj-step", proc (xs: openArray[Value]): Value =
1043+ conjOne(xs[0], xs[1]))
1044+ let rf = call(a[1], [step])
1045+ for x in elems(a[2]): result = call(rf, [result, x])
9971046 def "mapcat", proc (a: openArray[Value]): Value =
9981047 ## Like map, then concat: the fn takes one element from each collection.
9991048 var colls: seq[seq[Value]] = @[]
@@ -1015,6 +1064,13 @@ proc registerCoreHigherOrder*() =
10151064 callArgs.add toSeq(a[^1])
10161065 call(a[0], callArgs)
10171066 def "map", proc (a: openArray[Value]): Value =
1067+ if a.len == 1:
1068+ let f = a[0]
1069+ return mkFn("map-transducer", proc (xfArgs: openArray[Value]): Value =
1070+ let rf = argAt(xfArgs, 0)
1071+ mkFn("map-step", proc (step: openArray[Value]): Value =
1072+ if step.len < 2: return call(rf, step)
1073+ call(rf, [step[0], call(f, [step[1]])])))
10181074 if a.len == 2: return lazyMap(a[0], cursor(a[1]))
10191075 var cs: seq[Cursor] = @[]
10201076 for i in 1 ..< a.len: cs.add cursor(a[i])
@@ -1026,8 +1082,22 @@ proc registerCoreHigherOrder*() =
10261082 def "map-indexed", proc (a: openArray[Value]): Value =
10271083 lazyMapIndexed(a[0], 0, cursor(a[1]))
10281084 def "filter", proc (a: openArray[Value]): Value =
1085+ if a.len == 1:
1086+ let pred = a[0]
1087+ return mkFn("filter-transducer", proc (xfArgs: openArray[Value]): Value =
1088+ let rf = argAt(xfArgs, 0)
1089+ mkFn("filter-step", proc (step: openArray[Value]): Value =
1090+ if step.len < 2: return call(rf, step)
1091+ if truthy(call(pred, [step[1]])): call(rf, step) else: step[0]))
10291092 lazyFilter(a[0], cursor(a[1]), keep = true)
10301093 def "remove", proc (a: openArray[Value]): Value =
1094+ if a.len == 1:
1095+ let pred = a[0]
1096+ return mkFn("remove-transducer", proc (xfArgs: openArray[Value]): Value =
1097+ let rf = argAt(xfArgs, 0)
1098+ mkFn("remove-step", proc (step: openArray[Value]): Value =
1099+ if step.len < 2: return call(rf, step)
1100+ if not truthy(call(pred, [step[1]])): call(rf, step) else: step[0]))
10311101 lazyFilter(a[0], cursor(a[1]), keep = false)
10321102 def "reduce", proc (a: openArray[Value]): Value =
10331103 ## Streams the source rather than materializing it, so folding a lazy seq
@@ -1045,6 +1115,33 @@ proc registerCoreHigherOrder*() =
10451115 var acc = a[1]
10461116 for x in elems(a[2]): acc = call(f, [acc, x])
10471117 acc
1118+ def "transduce", proc (a: openArray[Value]): Value =
1119+ let rf = call(a[0], [a[1]])
1120+ let coll = a[^1]
1121+ var acc: Value
1122+ if a.len == 3:
1123+ var c = cursor(coll)
1124+ if not hasNext(c): return call(a[1], [])
1125+ acc = next(c)
1126+ while hasNext(c): acc = call(rf, [acc, next(c)])
1127+ else:
1128+ acc = a[2]
1129+ for x in elems(coll): acc = call(rf, [acc, x])
1130+ acc
1131+ def "sequence", proc (a: openArray[Value]): Value =
1132+ let step = mkFn("sequence-step", proc (xs: openArray[Value]): Value =
1133+ conjOne(xs[0], xs[1]))
1134+ let rf = call(a[0], [step])
1135+ var result = mkVector(@[])
1136+ for x in elems(a[1]): result = call(rf, [result, x])
1137+ mkList(toSeq(result))
1138+ def "eduction", proc (a: openArray[Value]): Value =
1139+ let step = mkFn("eduction-step", proc (xs: openArray[Value]): Value =
1140+ conjOne(xs[0], xs[1]))
1141+ let rf = call(a[0], [step])
1142+ var result = mkVector(@[])
1143+ for x in elems(a[1]): result = call(rf, [result, x])
1144+ mkList(toSeq(result))
10481145 def "some", proc (a: openArray[Value]): Value =
10491146 for x in elems(a[1]):
10501147 let r = call(a[0], [x])
@@ -1117,6 +1214,56 @@ proc registerCoreStateHost*() =
11171214 let nv = call(a[1], @[cur] & @(a[2 .. ^1]))
11181215 call(a[0], [mkKeyword("set"), nv])
11191216
1217+ # Refs use the same mutable cell representation as atoms. clonim programs
1218+ # execute on one thread, so a dosync body is already an atomic transaction;
1219+ # alter and commute consequently have identical serialized semantics.
1220+ def "ref", proc (a: openArray[Value]): Value =
1221+ var cell = a[0]
1222+ mkFn("ref", proc (args: openArray[Value]): Value =
1223+ if args.len == 0: return cell
1224+ cell = args[1]
1225+ cell)
1226+ def "ref-set", proc (a: openArray[Value]): Value =
1227+ call(a[0], [mkKeyword("set"), a[1]])
1228+ def "alter", proc (a: openArray[Value]): Value =
1229+ let current = call(a[0], [])
1230+ let value = call(a[1], @[current] & @(a[2 .. ^1]))
1231+ call(a[0], [mkKeyword("set"), value])
1232+ def "commute", proc (a: openArray[Value]): Value =
1233+ let current = call(a[0], [])
1234+ let value = call(a[1], @[current] & @(a[2 .. ^1]))
1235+ call(a[0], [mkKeyword("set"), value])
1236+ def "ensure", proc (a: openArray[Value]): Value = call(a[0], [])
1237+
1238+ # ---- protocols over clonim runtime value kinds
1239+ def "make-protocol-fn", proc (a: openArray[Value]): Value =
1240+ let protocol = str(a[0])
1241+ let methodName = str(a[1])
1242+ protocolMethods.mgetOrPut(protocol, initHashSet[string]()).incl methodName
1243+ mkFn(methodName, proc (args: openArray[Value]): Value =
1244+ if args.len == 0: err("Protocol method " & methodName & " requires a target")
1245+ let exact = protocolKey(protocol, methodName, protocolTypeName(args[0]))
1246+ let fallback = protocolKey(protocol, methodName, "default")
1247+ if protocolImpls.hasKey(exact): return call(protocolImpls[exact], args)
1248+ if protocolImpls.hasKey(fallback): return call(protocolImpls[fallback], args)
1249+ err("No implementation of " & protocol & "/" & methodName &
1250+ " for " & protocolTypeName(args[0])))
1251+ def "extend-protocol!", proc (a: openArray[Value]): Value =
1252+ let protocol = str(a[0])
1253+ let methodName = str(a[1])
1254+ let typ = normalizedProtocolType(a[2])
1255+ protocolImpls[protocolKey(protocol, methodName, typ)] = a[3]
1256+ a[3]
1257+ def "satisfies?", proc (a: openArray[Value]): Value =
1258+ let protocol = str(a[0])
1259+ if not protocolMethods.hasKey(protocol): return FalseV
1260+ let typ = protocolTypeName(a[1])
1261+ for methodName in protocolMethods[protocol]:
1262+ if not protocolImpls.hasKey(protocolKey(protocol, methodName, typ)) and
1263+ not protocolImpls.hasKey(protocolKey(protocol, methodName, "default")):
1264+ return FalseV
1265+ TrueV
1266+
11201267 def "ex-message", proc (a: openArray[Value]): Value =
11211268 ## Thrown values reach a catch clause as their message string.
11221269 (if a[0].kind == kStr: a[0] else: NilV)
@@ -5,6 +5,50 @@ import runtime
5 5
6 var selectedCore: HashSet[string]6 var selectedCore: HashSet[string]
7 var selectingCore = false7 var selectingCore = false
8+var protocolImpls = initTable[string, Value]()
9+var protocolMethods = initTable[string, HashSet[string]]()
10+
11+proc protocolTypeName(v: Value): string =
12+ case v.kind
13+ of kNil: "nil"
14+ of kBool: "boolean"
15+ of kInt: "integer"
16+ of kFloat: "float"
17+ of kChar: "character"
18+ of kStr: "string"
19+ of kKeyword: "keyword"
20+ of kSymbol: "symbol"
21+ of kList: "list"
22+ of kVector: "vector"
23+ of kMap: "map"
24+ of kSet: "set"
25+ of kCons, kChunk, kLazy: "seq"
26+ of kFn: "function"
27+ of kAgent: "agent"
28+
29+proc normalizedProtocolType(v: Value): string =
30+ let raw = str(v).split('/')[^1].split('.')[^1]
31+ case raw.toLowerAscii
32+ of "nil": "nil"
33+ of "boolean", "bool": "boolean"
34+ of "long", "integer", "int": "integer"
35+ of "double", "float": "float"
36+ of "character", "char": "character"
37+ of "string": "string"
38+ of "keyword": "keyword"
39+ of "symbol": "symbol"
40+ of "list", "ipersistentlist": "list"
41+ of "vector", "ipersistentvector": "vector"
42+ of "map", "ipersistentmap": "map"
43+ of "set", "ipersistentset": "set"
44+ of "iseq", "seq", "lazyseq": "seq"
45+ of "ifn", "function", "fn": "function"
46+ of "agent": "agent"
47+ of "object", "default": "default"
48+ else: raw.toLowerAscii
49+
50+proc protocolKey(protocol, methodName, typ: string): string =
51+ protocol & "\x1f" & methodName & "\x1f" & typ
8 52
9 proc num(v: Value): float64 =53 proc num(v: Value): float64 =
10 case v.kind54 case v.kind
@@ -992,8 +1036,13 @@ proc registerCoreCollections*() =
992 mkList(r)1036 mkList(r)
993 def "into", proc (a: openArray[Value]): Value =1037 def "into", proc (a: openArray[Value]): Value =
994 result = a[0]1038 result = a[0]
995- if a.len > 1:1039+ if a.len == 2:
996 for x in elems(a[1]): result = conjOne(result, x)1040 for x in elems(a[1]): result = conjOne(result, x)
1041+ elif a.len == 3:
1042+ let step = mkFn("conj-step", proc (xs: openArray[Value]): Value =
1043+ conjOne(xs[0], xs[1]))
1044+ let rf = call(a[1], [step])
1045+ for x in elems(a[2]): result = call(rf, [result, x])
997 def "mapcat", proc (a: openArray[Value]): Value =1046 def "mapcat", proc (a: openArray[Value]): Value =
998 ## Like map, then concat: the fn takes one element from each collection.1047 ## Like map, then concat: the fn takes one element from each collection.
999 var colls: seq[seq[Value]] = @[]1048 var colls: seq[seq[Value]] = @[]
@@ -1015,6 +1064,13 @@ proc registerCoreHigherOrder*() =
1015 callArgs.add toSeq(a[^1])1064 callArgs.add toSeq(a[^1])
1016 call(a[0], callArgs)1065 call(a[0], callArgs)
1017 def "map", proc (a: openArray[Value]): Value =1066 def "map", proc (a: openArray[Value]): Value =
1067+ if a.len == 1:
1068+ let f = a[0]
1069+ return mkFn("map-transducer", proc (xfArgs: openArray[Value]): Value =
1070+ let rf = argAt(xfArgs, 0)
1071+ mkFn("map-step", proc (step: openArray[Value]): Value =
1072+ if step.len < 2: return call(rf, step)
1073+ call(rf, [step[0], call(f, [step[1]])])))
1018 if a.len == 2: return lazyMap(a[0], cursor(a[1]))1074 if a.len == 2: return lazyMap(a[0], cursor(a[1]))
1019 var cs: seq[Cursor] = @[]1075 var cs: seq[Cursor] = @[]
1020 for i in 1 ..< a.len: cs.add cursor(a[i])1076 for i in 1 ..< a.len: cs.add cursor(a[i])
@@ -1026,8 +1082,22 @@ proc registerCoreHigherOrder*() =
1026 def "map-indexed", proc (a: openArray[Value]): Value =1082 def "map-indexed", proc (a: openArray[Value]): Value =
1027 lazyMapIndexed(a[0], 0, cursor(a[1]))1083 lazyMapIndexed(a[0], 0, cursor(a[1]))
1028 def "filter", proc (a: openArray[Value]): Value =1084 def "filter", proc (a: openArray[Value]): Value =
1085+ if a.len == 1:
1086+ let pred = a[0]
1087+ return mkFn("filter-transducer", proc (xfArgs: openArray[Value]): Value =
1088+ let rf = argAt(xfArgs, 0)
1089+ mkFn("filter-step", proc (step: openArray[Value]): Value =
1090+ if step.len < 2: return call(rf, step)
1091+ if truthy(call(pred, [step[1]])): call(rf, step) else: step[0]))
1029 lazyFilter(a[0], cursor(a[1]), keep = true)1092 lazyFilter(a[0], cursor(a[1]), keep = true)
1030 def "remove", proc (a: openArray[Value]): Value =1093 def "remove", proc (a: openArray[Value]): Value =
1094+ if a.len == 1:
1095+ let pred = a[0]
1096+ return mkFn("remove-transducer", proc (xfArgs: openArray[Value]): Value =
1097+ let rf = argAt(xfArgs, 0)
1098+ mkFn("remove-step", proc (step: openArray[Value]): Value =
1099+ if step.len < 2: return call(rf, step)
1100+ if not truthy(call(pred, [step[1]])): call(rf, step) else: step[0]))
1031 lazyFilter(a[0], cursor(a[1]), keep = false)1101 lazyFilter(a[0], cursor(a[1]), keep = false)
1032 def "reduce", proc (a: openArray[Value]): Value =1102 def "reduce", proc (a: openArray[Value]): Value =
1033 ## Streams the source rather than materializing it, so folding a lazy seq1103 ## Streams the source rather than materializing it, so folding a lazy seq
@@ -1045,6 +1115,33 @@ proc registerCoreHigherOrder*() =
1045 var acc = a[1]1115 var acc = a[1]
1046 for x in elems(a[2]): acc = call(f, [acc, x])1116 for x in elems(a[2]): acc = call(f, [acc, x])
1047 acc1117 acc
1118+ def "transduce", proc (a: openArray[Value]): Value =
1119+ let rf = call(a[0], [a[1]])
1120+ let coll = a[^1]
1121+ var acc: Value
1122+ if a.len == 3:
1123+ var c = cursor(coll)
1124+ if not hasNext(c): return call(a[1], [])
1125+ acc = next(c)
1126+ while hasNext(c): acc = call(rf, [acc, next(c)])
1127+ else:
1128+ acc = a[2]
1129+ for x in elems(coll): acc = call(rf, [acc, x])
1130+ acc
1131+ def "sequence", proc (a: openArray[Value]): Value =
1132+ let step = mkFn("sequence-step", proc (xs: openArray[Value]): Value =
1133+ conjOne(xs[0], xs[1]))
1134+ let rf = call(a[0], [step])
1135+ var result = mkVector(@[])
1136+ for x in elems(a[1]): result = call(rf, [result, x])
1137+ mkList(toSeq(result))
1138+ def "eduction", proc (a: openArray[Value]): Value =
1139+ let step = mkFn("eduction-step", proc (xs: openArray[Value]): Value =
1140+ conjOne(xs[0], xs[1]))
1141+ let rf = call(a[0], [step])
1142+ var result = mkVector(@[])
1143+ for x in elems(a[1]): result = call(rf, [result, x])
1144+ mkList(toSeq(result))
1048 def "some", proc (a: openArray[Value]): Value =1145 def "some", proc (a: openArray[Value]): Value =
1049 for x in elems(a[1]):1146 for x in elems(a[1]):
1050 let r = call(a[0], [x])1147 let r = call(a[0], [x])
@@ -1117,6 +1214,56 @@ proc registerCoreStateHost*() =
1117 let nv = call(a[1], @[cur] & @(a[2 .. ^1]))1214 let nv = call(a[1], @[cur] & @(a[2 .. ^1]))
1118 call(a[0], [mkKeyword("set"), nv])1215 call(a[0], [mkKeyword("set"), nv])
1119 1216
1217+ # Refs use the same mutable cell representation as atoms. clonim programs
1218+ # execute on one thread, so a dosync body is already an atomic transaction;
1219+ # alter and commute consequently have identical serialized semantics.
1220+ def "ref", proc (a: openArray[Value]): Value =
1221+ var cell = a[0]
1222+ mkFn("ref", proc (args: openArray[Value]): Value =
1223+ if args.len == 0: return cell
1224+ cell = args[1]
1225+ cell)
1226+ def "ref-set", proc (a: openArray[Value]): Value =
1227+ call(a[0], [mkKeyword("set"), a[1]])
1228+ def "alter", proc (a: openArray[Value]): Value =
1229+ let current = call(a[0], [])
1230+ let value = call(a[1], @[current] & @(a[2 .. ^1]))
1231+ call(a[0], [mkKeyword("set"), value])
1232+ def "commute", proc (a: openArray[Value]): Value =
1233+ let current = call(a[0], [])
1234+ let value = call(a[1], @[current] & @(a[2 .. ^1]))
1235+ call(a[0], [mkKeyword("set"), value])
1236+ def "ensure", proc (a: openArray[Value]): Value = call(a[0], [])
1237+
1238+ # ---- protocols over clonim runtime value kinds
1239+ def "make-protocol-fn", proc (a: openArray[Value]): Value =
1240+ let protocol = str(a[0])
1241+ let methodName = str(a[1])
1242+ protocolMethods.mgetOrPut(protocol, initHashSet[string]()).incl methodName
1243+ mkFn(methodName, proc (args: openArray[Value]): Value =
1244+ if args.len == 0: err("Protocol method " & methodName & " requires a target")
1245+ let exact = protocolKey(protocol, methodName, protocolTypeName(args[0]))
1246+ let fallback = protocolKey(protocol, methodName, "default")
1247+ if protocolImpls.hasKey(exact): return call(protocolImpls[exact], args)
1248+ if protocolImpls.hasKey(fallback): return call(protocolImpls[fallback], args)
1249+ err("No implementation of " & protocol & "/" & methodName &
1250+ " for " & protocolTypeName(args[0])))
1251+ def "extend-protocol!", proc (a: openArray[Value]): Value =
1252+ let protocol = str(a[0])
1253+ let methodName = str(a[1])
1254+ let typ = normalizedProtocolType(a[2])
1255+ protocolImpls[protocolKey(protocol, methodName, typ)] = a[3]
1256+ a[3]
1257+ def "satisfies?", proc (a: openArray[Value]): Value =
1258+ let protocol = str(a[0])
1259+ if not protocolMethods.hasKey(protocol): return FalseV
1260+ let typ = protocolTypeName(a[1])
1261+ for methodName in protocolMethods[protocol]:
1262+ if not protocolImpls.hasKey(protocolKey(protocol, methodName, typ)) and
1263+ not protocolImpls.hasKey(protocolKey(protocol, methodName, "default")):
1264+ return FalseV
1265+ TrueV
1266+
1120 def "ex-message", proc (a: openArray[Value]): Value =1267 def "ex-message", proc (a: openArray[Value]): Value =
1121 ## Thrown values reach a catch clause as their message string.1268 ## Thrown values reach a catch clause as their message string.
1122 (if a[0].kind == kStr: a[0] else: NilV)1269 (if a[0].kind == kStr: a[0] else: NilV)
modified src/namespaces.nim +133 -4
@@ -1,7 +1,7 @@
11 ## Static namespaces and dependency loading, before compiler.compileForms.
2-## This pass understands the compiler's fixed macro/binding forms, not arbitrary
3-## user macro expansion. Reader limitations (including syntax-quote and reader
4-## conditionals) still apply. Core discovery temporarily swaps runtime globals;
2+## This pass understands the compiler's fixed macro/binding forms and expands
3+## user macros. Reader conditionals are still outside the supported subset.
4+## Core discovery temporarily swaps runtime globals;
55 ## like that runtime, this module is intended for single-threaded use.
66 ## Core var references are canonical clojure.core/name symbols. Generated code
77 ## must call registerNamespaceCore() after registerCore() (or register the same
@@ -31,7 +31,9 @@ const syntaxHeads = ["quote", "if", "do", "let", "let*", "loop", "loop*",
3131 "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or",
3232 "when", "when-not", "if-not", "cond", "when-let", "if-let", "->", "->>",
3333 "doseq", "dotimes", "try", "catch", "finally", "comment", "set!", "declare",
34- "case", "for", "with-open", "assert", "binding"]
34+ "case", "for", "with-open", "assert", "binding", "syntax-quote",
35+ "unquote", "unquote-splicing", "dosync", "defprotocol", "extend-type",
36+ "extend-protocol"]
3537
3638 # Unlike macros, these heads cannot be shadowed in operator position.
3739 const specialForms = ["quote", "if", "do", "let*", "loop*", "recur", "fn*",
@@ -160,6 +162,68 @@ proc define(ns: Namespace, v: Value, private = false): Value =
160162
161163 proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
162164
165+proc syntaxQuoted(r: Resolver, ns: Namespace, v: Value,
166+ locals: HashSet[string], gensyms: var Table[string, string]): Value =
167+ ## Turn syntax-quoted data into ordinary list/vector/map/set construction.
168+ ## The result is evaluated by the macro interpreter, so unquotes remain
169+ ## normal expressions while all other symbols become quoted, qualified data.
170+ if v.kind == kList and v.items.len > 0 and v.items[0].kind == kSymbol:
171+ if v.items[0].s == "unquote":
172+ if v.items.len != 2: fail("unquote expects one form")
173+ return r.walk(ns, v.items[1], locals)
174+ if v.items[0].s == "unquote-splicing":
175+ fail("unquote-splicing is only valid inside a collection")
176+ if v.kind == kSymbol:
177+ var name = v.s
178+ if name.endsWith("#"):
179+ if not gensyms.hasKey(name):
180+ inc r.gensym
181+ gensyms[name] = name[0 ..< name.len - 1] &
182+ "__" & $r.gensym & "__auto__"
183+ name = gensyms[name]
184+ elif name notin syntaxHeads:
185+ let slash = name.find('/')
186+ if slash > 0:
187+ let prefix = name[0 ..< slash]
188+ name = ns.aliases.getOrDefault(prefix, prefix) & name[slash .. ^1]
189+ elif name in r.cores:
190+ name = "clojure.core/" & name
191+ else:
192+ name = ns.name & "/" & name
193+ return mkList(@[mkSymbol("quote"), mkSymbol(name)])
194+ if v.kind notin {kList, kVector, kMap, kSet}:
195+ return v
196+
197+ var items: seq[Value]
198+ if v.kind == kMap:
199+ for (k, value) in v.pairs:
200+ items.add k
201+ items.add value
202+ else:
203+ items = v.items
204+ var pieces: seq[Value]
205+ var spliced = false
206+ for item in items:
207+ if item.kind == kList and item.items.len > 0 and
208+ isSym(item.items[0], "unquote-splicing"):
209+ if item.items.len != 2: fail("unquote-splicing expects one form")
210+ pieces.add r.walk(ns, item.items[1], locals)
211+ spliced = true
212+ else:
213+ pieces.add mkList(@[mkSymbol("clojure.core/list"),
214+ r.syntaxQuoted(ns, item, locals, gensyms)])
215+ let joined =
216+ if pieces.len == 0: mkList(@[mkSymbol("quote"), NilV])
217+ elif pieces.len == 1 and spliced: pieces[0]
218+ else: mkList(@[mkSymbol("clojure.core/concat")] & pieces)
219+ let ctor = case v.kind
220+ of kList: "clojure.core/list"
221+ of kVector: "clojure.core/vector"
222+ of kMap: "clojure.core/hash-map"
223+ of kSet: "clojure.core/hash-set"
224+ else: ""
225+ mkList(@[mkSymbol("clojure.core/apply"), mkSymbol(ctor), joined])
226+
163227 proc macroKey(r: Resolver, ns: Namespace, raw: string): string =
164228 let slash = raw.find('/')
165229 if slash > 0:
@@ -346,6 +410,12 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
346410 let (isMacro, expanded) = r.expandMacro(ns, v, locals)
347411 if isMacro: return r.walk(ns, expanded, locals)
348412 let h = ns.syntaxHead(v, locals)
413+ if h == "syntax-quote":
414+ if xs.len != 2: fail("syntax-quote expects one form")
415+ var gensyms = initTable[string, string]()
416+ return r.syntaxQuoted(ns, xs[1], locals, gensyms)
417+ if h in ["unquote", "unquote-splicing"]:
418+ fail(h & " is only valid inside syntax-quote")
349419 if h in ["quote", "comment"]:
350420 xs[0] = mkSymbol(h)
351421 return mkList(xs)
@@ -353,6 +423,63 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
353423 fail(h & " is only supported as a static top-level ns/require declaration")
354424 if h in syntaxHeads: xs[0] = mkSymbol(h)
355425 case h
426+ of "defprotocol":
427+ if xs.len < 3: fail("defprotocol requires a name and method signatures")
428+ let protocolName = simple(xs[1], "protocol name")
429+ let protocolSym = ns.define(xs[1])
430+ var forms = @[mkSymbol("do"),
431+ mkList(@[mkSymbol("def"), protocolSym,
432+ mkList(@[mkSymbol("quote"), protocolSym])])]
433+ for signature in xs[2 .. ^1]:
434+ if signature.kind != kList or signature.items.len < 2:
435+ fail("protocol method requires a name and parameter vector")
436+ let methodName = simple(signature.items[0], "protocol method")
437+ let methodSym = ns.define(signature.items[0])
438+ let params = signature.items[1]
439+ if params.kind != kVector and
440+ not (params.kind == kList and params.items.len > 0 and
441+ params.items[0].kind == kVector):
442+ fail("protocol method " & methodName & " requires parameter vectors")
443+ forms.add mkList(@[mkSymbol("def"), methodSym,
444+ mkList(@[mkSymbol("clojure.core/make-protocol-fn"),
445+ mkStr(protocolSym.s), mkStr(methodName)])])
446+ return mkList(forms)
447+ of "extend-type", "extend-protocol":
448+ if xs.len < 4: fail(h & " requires a type, protocol, and methods")
449+ var forms = @[mkSymbol("do")]
450+ proc addMethods(protocolForm, typeForm: Value, methods: seq[Value]) =
451+ if protocolForm.kind != kSymbol: fail("protocol name must be a symbol")
452+ let protocol = r.qualify(ns, protocolForm.s, locals)
453+ for implForm in methods:
454+ if implForm.kind != kList or implForm.items.len < 3 or
455+ implForm.items[0].kind != kSymbol or implForm.items[1].kind != kVector:
456+ fail("protocol implementation requires (method [args] body...)")
457+ let methodName = implForm.items[0].s.split('/')[^1]
458+ let fnForm = mkList(@[mkSymbol("fn"), implForm.items[1]] & implForm.items[2 .. ^1])
459+ forms.add mkList(@[mkSymbol("clojure.core/extend-protocol!"),
460+ mkStr(protocol), mkStr(methodName),
461+ mkList(@[mkSymbol("quote"), typeForm]), fnForm])
462+ if h == "extend-type":
463+ let typ = xs[1]
464+ var i = 2
465+ while i < xs.len:
466+ let protocol = xs[i]
467+ inc i
468+ let start = i
469+ while i < xs.len and xs[i].kind == kList: inc i
470+ if start == i: fail("extend-type protocol requires method implementations")
471+ addMethods(protocol, typ, xs[start ..< i])
472+ else:
473+ let protocol = xs[1]
474+ var i = 2
475+ while i < xs.len:
476+ let typ = xs[i]
477+ inc i
478+ let start = i
479+ while i < xs.len and xs[i].kind == kList: inc i
480+ if start == i: fail("extend-protocol type requires method implementations")
481+ addMethods(protocol, typ, xs[start ..< i])
482+ return r.walk(ns, mkList(forms), locals)
356483 of "fn", "fn*":
357484 var scope = locals
358485 var start = 1
@@ -475,6 +602,8 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
475602 msg = mkList(@[mkSymbol("str"), mkStr("Assert failed: "), xs[2]])
476603 return r.walk(ns, mkList(@[mkSymbol("when-not"), xs[1],
477604 mkList(@[mkSymbol("throw"), msg])]), locals)
605+ of "dosync":
606+ return r.walk(ns, mkList(@[mkSymbol("do")] & xs[1 .. ^1]), locals)
478607 of "->", "->>":
479608 # Expand before resolving: a bare step can be a syntax head (e.g. do).
480609 if xs.len < 2: fail(h & " requires an expression")
@@ -1,7 +1,7 @@
1 ## Static namespaces and dependency loading, before compiler.compileForms.1 ## Static namespaces and dependency loading, before compiler.compileForms.
2-## This pass understands the compiler's fixed macro/binding forms, not arbitrary2+## This pass understands the compiler's fixed macro/binding forms and expands
3-## user macro expansion. Reader limitations (including syntax-quote and reader3+## user macros. Reader conditionals are still outside the supported subset.
4-## conditionals) still apply. Core discovery temporarily swaps runtime globals;4+## Core discovery temporarily swaps runtime globals;
5 ## like that runtime, this module is intended for single-threaded use.5 ## like that runtime, this module is intended for single-threaded use.
6 ## Core var references are canonical clojure.core/name symbols. Generated code6 ## Core var references are canonical clojure.core/name symbols. Generated code
7 ## must call registerNamespaceCore() after registerCore() (or register the same7 ## must call registerNamespaceCore() after registerCore() (or register the same
@@ -31,7 +31,9 @@ const syntaxHeads = ["quote", "if", "do", "let", "let*", "loop", "loop*",
31 "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or",31 "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or",
32 "when", "when-not", "if-not", "cond", "when-let", "if-let", "->", "->>",32 "when", "when-not", "if-not", "cond", "when-let", "if-let", "->", "->>",
33 "doseq", "dotimes", "try", "catch", "finally", "comment", "set!", "declare",33 "doseq", "dotimes", "try", "catch", "finally", "comment", "set!", "declare",
34- "case", "for", "with-open", "assert", "binding"]34+ "case", "for", "with-open", "assert", "binding", "syntax-quote",
35+ "unquote", "unquote-splicing", "dosync", "defprotocol", "extend-type",
36+ "extend-protocol"]
35 37
36 # Unlike macros, these heads cannot be shadowed in operator position.38 # Unlike macros, these heads cannot be shadowed in operator position.
37 const specialForms = ["quote", "if", "do", "let*", "loop*", "recur", "fn*",39 const specialForms = ["quote", "if", "do", "let*", "loop*", "recur", "fn*",
@@ -160,6 +162,68 @@ proc define(ns: Namespace, v: Value, private = false): Value =
160 162
161 proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value163 proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
162 164
165+proc syntaxQuoted(r: Resolver, ns: Namespace, v: Value,
166+ locals: HashSet[string], gensyms: var Table[string, string]): Value =
167+ ## Turn syntax-quoted data into ordinary list/vector/map/set construction.
168+ ## The result is evaluated by the macro interpreter, so unquotes remain
169+ ## normal expressions while all other symbols become quoted, qualified data.
170+ if v.kind == kList and v.items.len > 0 and v.items[0].kind == kSymbol:
171+ if v.items[0].s == "unquote":
172+ if v.items.len != 2: fail("unquote expects one form")
173+ return r.walk(ns, v.items[1], locals)
174+ if v.items[0].s == "unquote-splicing":
175+ fail("unquote-splicing is only valid inside a collection")
176+ if v.kind == kSymbol:
177+ var name = v.s
178+ if name.endsWith("#"):
179+ if not gensyms.hasKey(name):
180+ inc r.gensym
181+ gensyms[name] = name[0 ..< name.len - 1] &
182+ "__" & $r.gensym & "__auto__"
183+ name = gensyms[name]
184+ elif name notin syntaxHeads:
185+ let slash = name.find('/')
186+ if slash > 0:
187+ let prefix = name[0 ..< slash]
188+ name = ns.aliases.getOrDefault(prefix, prefix) & name[slash .. ^1]
189+ elif name in r.cores:
190+ name = "clojure.core/" & name
191+ else:
192+ name = ns.name & "/" & name
193+ return mkList(@[mkSymbol("quote"), mkSymbol(name)])
194+ if v.kind notin {kList, kVector, kMap, kSet}:
195+ return v
196+
197+ var items: seq[Value]
198+ if v.kind == kMap:
199+ for (k, value) in v.pairs:
200+ items.add k
201+ items.add value
202+ else:
203+ items = v.items
204+ var pieces: seq[Value]
205+ var spliced = false
206+ for item in items:
207+ if item.kind == kList and item.items.len > 0 and
208+ isSym(item.items[0], "unquote-splicing"):
209+ if item.items.len != 2: fail("unquote-splicing expects one form")
210+ pieces.add r.walk(ns, item.items[1], locals)
211+ spliced = true
212+ else:
213+ pieces.add mkList(@[mkSymbol("clojure.core/list"),
214+ r.syntaxQuoted(ns, item, locals, gensyms)])
215+ let joined =
216+ if pieces.len == 0: mkList(@[mkSymbol("quote"), NilV])
217+ elif pieces.len == 1 and spliced: pieces[0]
218+ else: mkList(@[mkSymbol("clojure.core/concat")] & pieces)
219+ let ctor = case v.kind
220+ of kList: "clojure.core/list"
221+ of kVector: "clojure.core/vector"
222+ of kMap: "clojure.core/hash-map"
223+ of kSet: "clojure.core/hash-set"
224+ else: ""
225+ mkList(@[mkSymbol("clojure.core/apply"), mkSymbol(ctor), joined])
226+
163 proc macroKey(r: Resolver, ns: Namespace, raw: string): string =227 proc macroKey(r: Resolver, ns: Namespace, raw: string): string =
164 let slash = raw.find('/')228 let slash = raw.find('/')
165 if slash > 0:229 if slash > 0:
@@ -346,6 +410,12 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
346 let (isMacro, expanded) = r.expandMacro(ns, v, locals)410 let (isMacro, expanded) = r.expandMacro(ns, v, locals)
347 if isMacro: return r.walk(ns, expanded, locals)411 if isMacro: return r.walk(ns, expanded, locals)
348 let h = ns.syntaxHead(v, locals)412 let h = ns.syntaxHead(v, locals)
413+ if h == "syntax-quote":
414+ if xs.len != 2: fail("syntax-quote expects one form")
415+ var gensyms = initTable[string, string]()
416+ return r.syntaxQuoted(ns, xs[1], locals, gensyms)
417+ if h in ["unquote", "unquote-splicing"]:
418+ fail(h & " is only valid inside syntax-quote")
349 if h in ["quote", "comment"]:419 if h in ["quote", "comment"]:
350 xs[0] = mkSymbol(h)420 xs[0] = mkSymbol(h)
351 return mkList(xs)421 return mkList(xs)
@@ -353,6 +423,63 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
353 fail(h & " is only supported as a static top-level ns/require declaration")423 fail(h & " is only supported as a static top-level ns/require declaration")
354 if h in syntaxHeads: xs[0] = mkSymbol(h)424 if h in syntaxHeads: xs[0] = mkSymbol(h)
355 case h425 case h
426+ of "defprotocol":
427+ if xs.len < 3: fail("defprotocol requires a name and method signatures")
428+ let protocolName = simple(xs[1], "protocol name")
429+ let protocolSym = ns.define(xs[1])
430+ var forms = @[mkSymbol("do"),
431+ mkList(@[mkSymbol("def"), protocolSym,
432+ mkList(@[mkSymbol("quote"), protocolSym])])]
433+ for signature in xs[2 .. ^1]:
434+ if signature.kind != kList or signature.items.len < 2:
435+ fail("protocol method requires a name and parameter vector")
436+ let methodName = simple(signature.items[0], "protocol method")
437+ let methodSym = ns.define(signature.items[0])
438+ let params = signature.items[1]
439+ if params.kind != kVector and
440+ not (params.kind == kList and params.items.len > 0 and
441+ params.items[0].kind == kVector):
442+ fail("protocol method " & methodName & " requires parameter vectors")
443+ forms.add mkList(@[mkSymbol("def"), methodSym,
444+ mkList(@[mkSymbol("clojure.core/make-protocol-fn"),
445+ mkStr(protocolSym.s), mkStr(methodName)])])
446+ return mkList(forms)
447+ of "extend-type", "extend-protocol":
448+ if xs.len < 4: fail(h & " requires a type, protocol, and methods")
449+ var forms = @[mkSymbol("do")]
450+ proc addMethods(protocolForm, typeForm: Value, methods: seq[Value]) =
451+ if protocolForm.kind != kSymbol: fail("protocol name must be a symbol")
452+ let protocol = r.qualify(ns, protocolForm.s, locals)
453+ for implForm in methods:
454+ if implForm.kind != kList or implForm.items.len < 3 or
455+ implForm.items[0].kind != kSymbol or implForm.items[1].kind != kVector:
456+ fail("protocol implementation requires (method [args] body...)")
457+ let methodName = implForm.items[0].s.split('/')[^1]
458+ let fnForm = mkList(@[mkSymbol("fn"), implForm.items[1]] & implForm.items[2 .. ^1])
459+ forms.add mkList(@[mkSymbol("clojure.core/extend-protocol!"),
460+ mkStr(protocol), mkStr(methodName),
461+ mkList(@[mkSymbol("quote"), typeForm]), fnForm])
462+ if h == "extend-type":
463+ let typ = xs[1]
464+ var i = 2
465+ while i < xs.len:
466+ let protocol = xs[i]
467+ inc i
468+ let start = i
469+ while i < xs.len and xs[i].kind == kList: inc i
470+ if start == i: fail("extend-type protocol requires method implementations")
471+ addMethods(protocol, typ, xs[start ..< i])
472+ else:
473+ let protocol = xs[1]
474+ var i = 2
475+ while i < xs.len:
476+ let typ = xs[i]
477+ inc i
478+ let start = i
479+ while i < xs.len and xs[i].kind == kList: inc i
480+ if start == i: fail("extend-protocol type requires method implementations")
481+ addMethods(protocol, typ, xs[start ..< i])
482+ return r.walk(ns, mkList(forms), locals)
356 of "fn", "fn*":483 of "fn", "fn*":
357 var scope = locals484 var scope = locals
358 var start = 1485 var start = 1
@@ -475,6 +602,8 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
475 msg = mkList(@[mkSymbol("str"), mkStr("Assert failed: "), xs[2]])602 msg = mkList(@[mkSymbol("str"), mkStr("Assert failed: "), xs[2]])
476 return r.walk(ns, mkList(@[mkSymbol("when-not"), xs[1],603 return r.walk(ns, mkList(@[mkSymbol("when-not"), xs[1],
477 mkList(@[mkSymbol("throw"), msg])]), locals)604 mkList(@[mkSymbol("throw"), msg])]), locals)
605+ of "dosync":
606+ return r.walk(ns, mkList(@[mkSymbol("do")] & xs[1 .. ^1]), locals)
478 of "->", "->>":607 of "->", "->>":
479 # Expand before resolving: a bare step can be a syntax head (e.g. do).608 # Expand before resolving: a bare step can be a syntax head (e.g. do).
480 if xs.len < 2: fail(h & " requires an expression")609 if xs.len < 2: fail(h & " requires an expression")
modified src/reader.nim +9 -0
@@ -191,6 +191,15 @@ proc readForm(r: var Reader): Value =
191191 of '\'':
192192 discard r.advance
193193 return mkList(@[mkSymbol("quote"), r.readForm])
194+ of '`':
195+ discard r.advance
196+ return mkList(@[mkSymbol("syntax-quote"), r.readForm])
197+ of '~':
198+ discard r.advance
199+ if r.peek == '@':
200+ discard r.advance
201+ return mkList(@[mkSymbol("unquote-splicing"), r.readForm])
202+ return mkList(@[mkSymbol("unquote"), r.readForm])
194203 of '@':
195204 discard r.advance
196205 return mkList(@[mkSymbol("deref"), r.readForm])
@@ -191,6 +191,15 @@ proc readForm(r: var Reader): Value =
191 of '\'':191 of '\'':
192 discard r.advance192 discard r.advance
193 return mkList(@[mkSymbol("quote"), r.readForm])193 return mkList(@[mkSymbol("quote"), r.readForm])
194+ of '`':
195+ discard r.advance
196+ return mkList(@[mkSymbol("syntax-quote"), r.readForm])
197+ of '~':
198+ discard r.advance
199+ if r.peek == '@':
200+ discard r.advance
201+ return mkList(@[mkSymbol("unquote-splicing"), r.readForm])
202+ return mkList(@[mkSymbol("unquote"), r.readForm])
194 of '@':203 of '@':
195 discard r.advance204 discard r.advance
196 return mkList(@[mkSymbol("deref"), r.readForm])205 return mkList(@[mkSymbol("deref"), r.readForm])
added tests/core_gaps.expected +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+:yes
2+18
3+[1 2 3]
4+core-gaps/not-yet-defined
5+30
6+(2 4 6)
7+[2 4 6]
8+10
9+7 7
new file mode 100644
@@ -0,0 +1,9 @@
1+:yes
2+18
3+[1 2 3]
4+core-gaps/not-yet-defined
5+30
6+(2 4 6)
7+[2 4 6]
8+10
9+7 7
added tests/protocols.expected +5 -0
new file mode 100644
@@ -0,0 +1,5 @@
1+integer:4 9
2+string:a ab
3+other::x [:x :y]
4+true true true
5+3 false true
new file mode 100644
@@ -0,0 +1,5 @@
1+integer:4 9
2+string:a ab
3+other::x [:x :y]
4+true true true
5+3 false true
modified tests/user_macros.expected +4 -0
@@ -2,3 +2,7 @@
22 variadic 3
33 [(form-and-env) {}]
44 true
5+:syntax
6+[1 2 3 4]
7+12
8+user/quoted-symbol
@@ -2,3 +2,7 @@
2 variadic 32 variadic 3
3 [(form-and-env) {}]3 [(form-and-env) {}]
4 true4 true
5+:syntax
6+[1 2 3 4]
7+12
8+user/quoted-symbol