nandi/clonimpublic Fork
⑂ 2377830
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 case, for, assert, with-open and binding

The first four desugar in the resolver onto forms the compiler already has:
case becomes a let over cond with quoted constants, for becomes map and
mapcat, with-open a try/finally, assert a when-not. binding needs the
compiler, since it saves and restores var cells around the body.

Output now goes through *out*, so (binding [*out* *err*] ...) redirects it.
Adds a mutable StringBuilder, a write handle behind clojure.java.io's
output-stream, and .append/.write/.close/.toString/.length.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-21T01:07:56-07:00 Browse files
2377830 parent: c408078
added examples/macros.clj +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+(ns macros (:require [clojure.java.io :as io]))
2+
3+;; case: literal constants, grouped clauses, optional default
4+(defn kind [x] (case x 0 :zero 1 :one (2 3) :few :other))
5+(println (kind 0) (kind 1) (kind 3) (kind 9)) ; :zero :one :few :other
6+(println (case \n \n :nl \t :tab :none)) ; :nl
7+(println (case :east :east 1 :west 2 3)) ; 1
8+(println (try (case 5 0 :z) (catch Exception e (ex-message e)))) ; No matching clause: 5
9+
10+;; for: one or more bindings, eager
11+(println (for [n (range 4)] (* n n))) ; (0 1 4 9)
12+(println (for [a [1 2] b [:x :y]] [a b])) ; ([1 :x] [1 :y] [2 :x] [2 :y])
13+(println (apply str (for [c "abc"] c))) ; abc
14+
15+;; assert
16+(println (try (assert (= 1 2) "nope") (catch Exception e (ex-message e))))
17+(println (try (assert (= 1 2)) (catch Exception e (ex-message e))))
18+
19+;; binding: dynamic scope, unwound on every exit
20+(def ^:dynamic *depth* 0)
21+(defn show [] (println "depth" *depth*))
22+(show)
23+(binding [*depth* 3] (show))
24+(show)
25+(println (try (binding [*depth* 9] (throw (ex-info "x" {})))
26+ (catch Exception e *depth*))) ; 0
27+
28+;; StringBuilder, with-open and a write handle
29+(let [b (StringBuilder.)]
30+ (.append b \h) (.append b "ello")
31+ (println (str b) (.length b))) ; hello 5
32+(with-open [o (io/output-stream "macros.tmp")]
33+ (.write o (byte-array (map unchecked-byte [104 105]))))
34+(println (slurp "macros.tmp")) ; hi
35+(io/delete-file "macros.tmp")
new file mode 100644
@@ -0,0 +1,35 @@
1+(ns macros (:require [clojure.java.io :as io]))
2+
3+;; case: literal constants, grouped clauses, optional default
4+(defn kind [x] (case x 0 :zero 1 :one (2 3) :few :other))
5+(println (kind 0) (kind 1) (kind 3) (kind 9)) ; :zero :one :few :other
6+(println (case \n \n :nl \t :tab :none)) ; :nl
7+(println (case :east :east 1 :west 2 3)) ; 1
8+(println (try (case 5 0 :z) (catch Exception e (ex-message e)))) ; No matching clause: 5
9+
10+;; for: one or more bindings, eager
11+(println (for [n (range 4)] (* n n))) ; (0 1 4 9)
12+(println (for [a [1 2] b [:x :y]] [a b])) ; ([1 :x] [1 :y] [2 :x] [2 :y])
13+(println (apply str (for [c "abc"] c))) ; abc
14+
15+;; assert
16+(println (try (assert (= 1 2) "nope") (catch Exception e (ex-message e))))
17+(println (try (assert (= 1 2)) (catch Exception e (ex-message e))))
18+
19+;; binding: dynamic scope, unwound on every exit
20+(def ^:dynamic *depth* 0)
21+(defn show [] (println "depth" *depth*))
22+(show)
23+(binding [*depth* 3] (show))
24+(show)
25+(println (try (binding [*depth* 9] (throw (ex-info "x" {})))
26+ (catch Exception e *depth*))) ; 0
27+
28+;; StringBuilder, with-open and a write handle
29+(let [b (StringBuilder.)]
30+ (.append b \h) (.append b "ello")
31+ (println (str b) (.length b))) ; hello 5
32+(with-open [o (io/output-stream "macros.tmp")]
33+ (.write o (byte-array (map unchecked-byte [104 105]))))
34+(println (slurp "macros.tmp")) ; hi
35+(io/delete-file "macros.tmp")
modified src/compiler.nim +26 -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"].toHashSet
146+ "import", "set!", "declare", "binding"].toHashSet
147147
148148 const intOps = {"+": "+", "-": "-", "*": "*"}.toTable
149149 const intCalls = {"quot": "idiv", "rem": "irem"}.toTable
@@ -1091,6 +1091,31 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) =
10911091 c.pop
10921092 c.line(dst & " = NilV")
10931093 return
1094+ of "binding":
1095+ # Dynamic scope: the vars keep their new values for the duration of
1096+ # the body, on every path out of it.
1097+ let b = args[0]
1098+ if b.kind != kVector or b.items.len mod 2 != 0:
1099+ err("binding requires paired bindings")
1100+ var saved: seq[(string, string)] = @[]
1101+ var i = 0
1102+ while i < b.items.len:
1103+ let nm = symName(b.items[i])
1104+ let v = genExpr(b.items[i + 1], env, c)
1105+ let cell = c.cellFor(nm)
1106+ let keep = c.gensym("bind")
1107+ c.line("var " & keep & ": Value = cellGet(" & cell & ")")
1108+ c.line("setVar(" & nimStr(nm) & ", " & v & ")")
1109+ saved.add (nm, keep)
1110+ i += 2
1111+ c.line("try:")
1112+ c.push; genBody(args[1 .. ^1], dst, env, c); c.pop
1113+ c.line("finally:")
1114+ c.push
1115+ for (nm, keep) in saved:
1116+ c.line("setVar(" & nimStr(nm) & ", " & keep & ")")
1117+ c.pop
1118+ return
10941119 of "try":
10951120 var bodyForms: seq[Value] = @[]
10961121 var catchSym = ""
@@ -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"].toHashSet146+ "import", "set!", "declare", "binding"].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
@@ -1091,6 +1091,31 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) =
1091 c.pop1091 c.pop
1092 c.line(dst & " = NilV")1092 c.line(dst & " = NilV")
1093 return1093 return
1094+ of "binding":
1095+ # Dynamic scope: the vars keep their new values for the duration of
1096+ # the body, on every path out of it.
1097+ let b = args[0]
1098+ if b.kind != kVector or b.items.len mod 2 != 0:
1099+ err("binding requires paired bindings")
1100+ var saved: seq[(string, string)] = @[]
1101+ var i = 0
1102+ while i < b.items.len:
1103+ let nm = symName(b.items[i])
1104+ let v = genExpr(b.items[i + 1], env, c)
1105+ let cell = c.cellFor(nm)
1106+ let keep = c.gensym("bind")
1107+ c.line("var " & keep & ": Value = cellGet(" & cell & ")")
1108+ c.line("setVar(" & nimStr(nm) & ", " & v & ")")
1109+ saved.add (nm, keep)
1110+ i += 2
1111+ c.line("try:")
1112+ c.push; genBody(args[1 .. ^1], dst, env, c); c.pop
1113+ c.line("finally:")
1114+ c.push
1115+ for (nm, keep) in saved:
1116+ c.line("setVar(" & nimStr(nm) & ", " & keep & ")")
1117+ c.pop
1118+ return
1094 of "try":1119 of "try":
1095 var bodyForms: seq[Value] = @[]1120 var bodyForms: seq[Value] = @[]
1096 var catchSym = ""1121 var catchSym = ""
modified src/core.nim +63 -3
@@ -338,6 +338,10 @@ proc lazyConcat(colls: seq[Value], i: int, c: Cursor): Value =
338338 proc def(name: string, f: proc (args: openArray[Value]): Value {.closure.}) =
339339 setVar(name, mkFn(name, f))
340340
341+proc def(name: string, v: Value) =
342+ ## A var whose value is data rather than a function, such as *out*.
343+ setVar(name, v)
344+
341345 # ----------------------------------------------------------------- fusion
342346 ## A pipeline whose intermediate sequences are syntactic temporaries -- nobody
343347 ## named them, so nobody can hold them -- does not need those sequences to
@@ -471,6 +475,13 @@ proc javaFormat(fmt: string, args: openArray[Value]): string =
471475 else: body = " ".repeat(pad) & body
472476 result.add body
473477
478+proc currentOut(): File =
479+ ## Where print and friends write. `binding` can move it to *err*.
480+ if hasVar("clojure.core/*out*") and equals(getVar("clojure.core/*out*"), mkKeyword("stderr")):
481+ stderr
482+ else:
483+ stdout
484+
474485 proc registerCore*() =
475486 # ---- arithmetic
476487 def "+", proc (a: openArray[Value]): Value =
@@ -625,21 +636,37 @@ proc registerCore*() =
625636 var parts: seq[string] = @[]
626637 for x in a: parts.add prStr(x)
627638 mkStr(parts.join(" "))
639+ def "*out*", mkKeyword("stdout")
640+ def "*err*", mkKeyword("stderr")
641+ def "*in*", mkKeyword("stdin")
628642 def "println", proc (a: openArray[Value]): Value =
629643 var parts: seq[string] = @[]
630644 for x in a: parts.add str(x)
631- echo parts.join(" ")
645+ let f = currentOut()
646+ f.write(parts.join(" ") & "\n")
647+ f.flushFile
632648 NilV
633649 def "prn", proc (a: openArray[Value]): Value =
634650 var parts: seq[string] = @[]
635651 for x in a: parts.add prStr(x)
636- echo parts.join(" ")
652+ let f = currentOut()
653+ f.write(parts.join(" ") & "\n")
654+ f.flushFile
637655 NilV
638656 def "print", proc (a: openArray[Value]): Value =
639657 var parts: seq[string] = @[]
640658 for x in a: parts.add str(x)
641- stdout.write parts.join(" ")
659+ currentOut().write parts.join(" ")
660+ NilV
661+ def "pr", proc (a: openArray[Value]): Value =
662+ var parts: seq[string] = @[]
663+ for x in a: parts.add prStr(x)
664+ currentOut().write parts.join(" ")
642665 NilV
666+ def "flush", proc (a: openArray[Value]): Value =
667+ currentOut().flushFile; NilV
668+ def "newline", proc (a: openArray[Value]): Value =
669+ currentOut().write "\n"; NilV
643670 def "name", proc (a: openArray[Value]): Value =
644671 case a[0].kind
645672 of kKeyword, kSymbol, kStr: mkStr(a[0].s)
@@ -701,6 +728,39 @@ proc registerCore*() =
701728 var xs: seq[Value] = @[]
702729 for ch in a[0].s: xs.add mkInt(ord(ch))
703730 mkList(xs)
731+ def "StringBuilder.", proc (a: openArray[Value]): Value =
732+ ## A mutable string: the one place a string value is written in place.
733+ mkStr(if a.len > 0: str(a[0]) else: "")
734+ def ".append", proc (a: openArray[Value]): Value =
735+ if a[0].kind != kStr: err(".append expects a StringBuilder")
736+ a[0].obj.s.add(str(a[1]))
737+ a[0]
738+ def ".toString", proc (a: openArray[Value]): Value = mkStr(str(a[0]))
739+ def ".length", proc (a: openArray[Value]): Value = mkInt(int64(a[0].s.len))
740+ def "*output-stream*", proc (a: openArray[Value]): Value =
741+ ## A write handle, driven by keyword messages so that it stays an
742+ ## ordinary value: (o :write bytes) and (o :close).
743+ let path = a[0].s
744+ var f: File
745+ if not f.open(path, fmWrite): err("Cannot open for writing: " & path)
746+ var closed = false
747+ mkFn("output-stream", proc (args: openArray[Value]): Value =
748+ if args.len == 0 or args[0].kind != kKeyword:
749+ err("output-stream expects a keyword message")
750+ case args[0].s
751+ of "write":
752+ if closed: err("Stream is closed: " & path)
753+ var bytes = ""
754+ for x in elems(args[1]): bytes.add char(uint8(intOf(x) and 0xff))
755+ f.write(bytes)
756+ of "close":
757+ if not closed: f.close(); closed = true
758+ else: err("Unknown stream message: " & prStr(args[0]))
759+ NilV)
760+ def ".write", proc (a: openArray[Value]): Value =
761+ call(a[0], [mkKeyword("write"), a[1]])
762+ def ".close", proc (a: openArray[Value]): Value =
763+ (if a[0].kind == kFn: call(a[0], [mkKeyword("close")]) else: NilV)
704764 def "*delete-file*", proc (a: openArray[Value]): Value =
705765 try:
706766 removeFile(a[0].s)
@@ -338,6 +338,10 @@ proc lazyConcat(colls: seq[Value], i: int, c: Cursor): Value =
338 proc def(name: string, f: proc (args: openArray[Value]): Value {.closure.}) =338 proc def(name: string, f: proc (args: openArray[Value]): Value {.closure.}) =
339 setVar(name, mkFn(name, f))339 setVar(name, mkFn(name, f))
340 340
341+proc def(name: string, v: Value) =
342+ ## A var whose value is data rather than a function, such as *out*.
343+ setVar(name, v)
344+
341 # ----------------------------------------------------------------- fusion345 # ----------------------------------------------------------------- fusion
342 ## A pipeline whose intermediate sequences are syntactic temporaries -- nobody346 ## A pipeline whose intermediate sequences are syntactic temporaries -- nobody
343 ## named them, so nobody can hold them -- does not need those sequences to347 ## named them, so nobody can hold them -- does not need those sequences to
@@ -471,6 +475,13 @@ proc javaFormat(fmt: string, args: openArray[Value]): string =
471 else: body = " ".repeat(pad) & body475 else: body = " ".repeat(pad) & body
472 result.add body476 result.add body
473 477
478+proc currentOut(): File =
479+ ## Where print and friends write. `binding` can move it to *err*.
480+ if hasVar("clojure.core/*out*") and equals(getVar("clojure.core/*out*"), mkKeyword("stderr")):
481+ stderr
482+ else:
483+ stdout
484+
474 proc registerCore*() =485 proc registerCore*() =
475 # ---- arithmetic486 # ---- arithmetic
476 def "+", proc (a: openArray[Value]): Value =487 def "+", proc (a: openArray[Value]): Value =
@@ -625,21 +636,37 @@ proc registerCore*() =
625 var parts: seq[string] = @[]636 var parts: seq[string] = @[]
626 for x in a: parts.add prStr(x)637 for x in a: parts.add prStr(x)
627 mkStr(parts.join(" "))638 mkStr(parts.join(" "))
639+ def "*out*", mkKeyword("stdout")
640+ def "*err*", mkKeyword("stderr")
641+ def "*in*", mkKeyword("stdin")
628 def "println", proc (a: openArray[Value]): Value =642 def "println", proc (a: openArray[Value]): Value =
629 var parts: seq[string] = @[]643 var parts: seq[string] = @[]
630 for x in a: parts.add str(x)644 for x in a: parts.add str(x)
631- echo parts.join(" ")645+ let f = currentOut()
646+ f.write(parts.join(" ") & "\n")
647+ f.flushFile
632 NilV648 NilV
633 def "prn", proc (a: openArray[Value]): Value =649 def "prn", proc (a: openArray[Value]): Value =
634 var parts: seq[string] = @[]650 var parts: seq[string] = @[]
635 for x in a: parts.add prStr(x)651 for x in a: parts.add prStr(x)
636- echo parts.join(" ")652+ let f = currentOut()
653+ f.write(parts.join(" ") & "\n")
654+ f.flushFile
637 NilV655 NilV
638 def "print", proc (a: openArray[Value]): Value =656 def "print", proc (a: openArray[Value]): Value =
639 var parts: seq[string] = @[]657 var parts: seq[string] = @[]
640 for x in a: parts.add str(x)658 for x in a: parts.add str(x)
641- stdout.write parts.join(" ")659+ currentOut().write parts.join(" ")
660+ NilV
661+ def "pr", proc (a: openArray[Value]): Value =
662+ var parts: seq[string] = @[]
663+ for x in a: parts.add prStr(x)
664+ currentOut().write parts.join(" ")
642 NilV665 NilV
666+ def "flush", proc (a: openArray[Value]): Value =
667+ currentOut().flushFile; NilV
668+ def "newline", proc (a: openArray[Value]): Value =
669+ currentOut().write "\n"; NilV
643 def "name", proc (a: openArray[Value]): Value =670 def "name", proc (a: openArray[Value]): Value =
644 case a[0].kind671 case a[0].kind
645 of kKeyword, kSymbol, kStr: mkStr(a[0].s)672 of kKeyword, kSymbol, kStr: mkStr(a[0].s)
@@ -701,6 +728,39 @@ proc registerCore*() =
701 var xs: seq[Value] = @[]728 var xs: seq[Value] = @[]
702 for ch in a[0].s: xs.add mkInt(ord(ch))729 for ch in a[0].s: xs.add mkInt(ord(ch))
703 mkList(xs)730 mkList(xs)
731+ def "StringBuilder.", proc (a: openArray[Value]): Value =
732+ ## A mutable string: the one place a string value is written in place.
733+ mkStr(if a.len > 0: str(a[0]) else: "")
734+ def ".append", proc (a: openArray[Value]): Value =
735+ if a[0].kind != kStr: err(".append expects a StringBuilder")
736+ a[0].obj.s.add(str(a[1]))
737+ a[0]
738+ def ".toString", proc (a: openArray[Value]): Value = mkStr(str(a[0]))
739+ def ".length", proc (a: openArray[Value]): Value = mkInt(int64(a[0].s.len))
740+ def "*output-stream*", proc (a: openArray[Value]): Value =
741+ ## A write handle, driven by keyword messages so that it stays an
742+ ## ordinary value: (o :write bytes) and (o :close).
743+ let path = a[0].s
744+ var f: File
745+ if not f.open(path, fmWrite): err("Cannot open for writing: " & path)
746+ var closed = false
747+ mkFn("output-stream", proc (args: openArray[Value]): Value =
748+ if args.len == 0 or args[0].kind != kKeyword:
749+ err("output-stream expects a keyword message")
750+ case args[0].s
751+ of "write":
752+ if closed: err("Stream is closed: " & path)
753+ var bytes = ""
754+ for x in elems(args[1]): bytes.add char(uint8(intOf(x) and 0xff))
755+ f.write(bytes)
756+ of "close":
757+ if not closed: f.close(); closed = true
758+ else: err("Unknown stream message: " & prStr(args[0]))
759+ NilV)
760+ def ".write", proc (a: openArray[Value]): Value =
761+ call(a[0], [mkKeyword("write"), a[1]])
762+ def ".close", proc (a: openArray[Value]): Value =
763+ (if a[0].kind == kFn: call(a[0], [mkKeyword("close")]) else: NilV)
704 def "*delete-file*", proc (a: openArray[Value]): Value =764 def "*delete-file*", proc (a: openArray[Value]): Value =
705 try:765 try:
706 removeFile(a[0].s)766 removeFile(a[0].s)
modified src/namespaces.nim +63 -1
@@ -21,11 +21,13 @@ type
2121 active: seq[string]
2222 loaded: HashSet[string]
2323 output: seq[Value]
24+ gensym: int
2425
2526 const syntaxHeads = ["quote", "if", "do", "let", "let*", "loop", "loop*",
2627 "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or",
2728 "when", "when-not", "if-not", "cond", "when-let", "if-let", "->", "->>",
28- "doseq", "dotimes", "try", "catch", "finally", "comment", "set!", "declare"]
29+ "doseq", "dotimes", "try", "catch", "finally", "comment", "set!", "declare",
30+ "case", "for", "with-open", "assert", "binding"]
2931
3032 # Unlike macros, these heads cannot be shadowed in operator position.
3133 const specialForms = ["quote", "if", "do", "let*", "loop*", "recur", "fn*",
@@ -289,6 +291,66 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
289291 scope.incl simple(xs[2], "catch binding")
290292 for i in 3 ..< xs.len: xs[i] = r.walk(ns, xs[i], scope)
291293 return mkList(xs)
294+ of "case":
295+ # (case e test-constant result ... default?) — the constants are literal,
296+ # so they are quoted rather than resolved.
297+ if xs.len < 3: fail("case requires an expression and at least one clause")
298+ inc r.gensym
299+ let sym = mkSymbol("case__" & $r.gensym)
300+ var clauses: seq[Value] = @[]
301+ var i = 2
302+ while i + 1 < xs.len:
303+ let test = xs[i]
304+ var pred: Value
305+ if test.kind in {kList, kVector} and test.items.len > 0:
306+ # a group of constants shares one result
307+ var alts: seq[Value] = @[mkSymbol("or")]
308+ for t in test.items:
309+ alts.add mkList(@[mkSymbol("="), sym, mkList(@[mkSymbol("quote"), t])])
310+ pred = mkList(alts)
311+ else:
312+ pred = mkList(@[mkSymbol("="), sym, mkList(@[mkSymbol("quote"), test])])
313+ clauses.add pred
314+ clauses.add xs[i + 1]
315+ i += 2
316+ clauses.add mkSymbol("else")
317+ if i < xs.len: clauses.add xs[i]
318+ else:
319+ clauses.add mkList(@[mkSymbol("throw"),
320+ mkList(@[mkSymbol("str"), mkStr("No matching clause: "), sym])])
321+ return r.walk(ns, mkList(@[mkSymbol("let"), mkVector(@[sym, xs[1]]),
322+ mkList(@[mkSymbol("cond")] & clauses)]), locals)
323+ of "for":
324+ # A comprehension over one or more bindings, without the modifier clauses.
325+ if xs.len < 3 or xs[1].kind != kVector: fail("for requires [sym coll] bindings")
326+ var bs = xs[1].items
327+ if bs.len == 0 or bs.len mod 2 != 0: fail("for requires paired bindings")
328+ for j in countup(0, bs.len - 1, 2):
329+ if bs[j].kind == kKeyword: fail("for modifier clauses are not supported: " & prStr(bs[j]))
330+ var body = mkList(@[mkSymbol("do")] & xs[2 .. ^1])
331+ var j = bs.len - 2
332+ while j >= 0:
333+ let combine = (if j == bs.len - 2: "map" else: "mapcat")
334+ body = mkList(@[mkSymbol(combine),
335+ mkList(@[mkSymbol("fn"), mkVector(@[bs[j]]), body]),
336+ bs[j + 1]])
337+ j -= 2
338+ return r.walk(ns, body, locals)
339+ of "with-open":
340+ if xs.len < 2 or xs[1].kind != kVector or xs[1].items.len != 2:
341+ fail("with-open requires [sym resource]")
342+ let bs = xs[1].items
343+ let body = mkList(@[mkSymbol("do")] & xs[2 .. ^1])
344+ let cleanup = mkList(@[mkSymbol("finally"), mkList(@[mkSymbol(".close"), bs[0]])])
345+ return r.walk(ns, mkList(@[mkSymbol("let"), mkVector(bs),
346+ mkList(@[mkSymbol("try"), body, cleanup])]), locals)
347+ of "assert":
348+ if xs.len < 2: fail("assert requires a test")
349+ var msg = mkList(@[mkSymbol("str"), mkStr("Assert failed: " & prStr(xs[1]))])
350+ if xs.len > 2:
351+ msg = mkList(@[mkSymbol("str"), mkStr("Assert failed: "), xs[2]])
352+ return r.walk(ns, mkList(@[mkSymbol("when-not"), xs[1],
353+ mkList(@[mkSymbol("throw"), msg])]), locals)
292354 of "->", "->>":
293355 # Expand before resolving: a bare step can be a syntax head (e.g. do).
294356 if xs.len < 2: fail(h & " requires an expression")
@@ -21,11 +21,13 @@ type
21 active: seq[string]21 active: seq[string]
22 loaded: HashSet[string]22 loaded: HashSet[string]
23 output: seq[Value]23 output: seq[Value]
24+ gensym: int
24 25
25 const syntaxHeads = ["quote", "if", "do", "let", "let*", "loop", "loop*",26 const syntaxHeads = ["quote", "if", "do", "let", "let*", "loop", "loop*",
26 "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or",27 "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or",
27 "when", "when-not", "if-not", "cond", "when-let", "if-let", "->", "->>",28 "when", "when-not", "if-not", "cond", "when-let", "if-let", "->", "->>",
28- "doseq", "dotimes", "try", "catch", "finally", "comment", "set!", "declare"]29+ "doseq", "dotimes", "try", "catch", "finally", "comment", "set!", "declare",
30+ "case", "for", "with-open", "assert", "binding"]
29 31
30 # Unlike macros, these heads cannot be shadowed in operator position.32 # Unlike macros, these heads cannot be shadowed in operator position.
31 const specialForms = ["quote", "if", "do", "let*", "loop*", "recur", "fn*",33 const specialForms = ["quote", "if", "do", "let*", "loop*", "recur", "fn*",
@@ -289,6 +291,66 @@ proc walk(r: Resolver, ns: Namespace, v: Value, locals: HashSet[string]): Value
289 scope.incl simple(xs[2], "catch binding")291 scope.incl simple(xs[2], "catch binding")
290 for i in 3 ..< xs.len: xs[i] = r.walk(ns, xs[i], scope)292 for i in 3 ..< xs.len: xs[i] = r.walk(ns, xs[i], scope)
291 return mkList(xs)293 return mkList(xs)
294+ of "case":
295+ # (case e test-constant result ... default?) — the constants are literal,
296+ # so they are quoted rather than resolved.
297+ if xs.len < 3: fail("case requires an expression and at least one clause")
298+ inc r.gensym
299+ let sym = mkSymbol("case__" & $r.gensym)
300+ var clauses: seq[Value] = @[]
301+ var i = 2
302+ while i + 1 < xs.len:
303+ let test = xs[i]
304+ var pred: Value
305+ if test.kind in {kList, kVector} and test.items.len > 0:
306+ # a group of constants shares one result
307+ var alts: seq[Value] = @[mkSymbol("or")]
308+ for t in test.items:
309+ alts.add mkList(@[mkSymbol("="), sym, mkList(@[mkSymbol("quote"), t])])
310+ pred = mkList(alts)
311+ else:
312+ pred = mkList(@[mkSymbol("="), sym, mkList(@[mkSymbol("quote"), test])])
313+ clauses.add pred
314+ clauses.add xs[i + 1]
315+ i += 2
316+ clauses.add mkSymbol("else")
317+ if i < xs.len: clauses.add xs[i]
318+ else:
319+ clauses.add mkList(@[mkSymbol("throw"),
320+ mkList(@[mkSymbol("str"), mkStr("No matching clause: "), sym])])
321+ return r.walk(ns, mkList(@[mkSymbol("let"), mkVector(@[sym, xs[1]]),
322+ mkList(@[mkSymbol("cond")] & clauses)]), locals)
323+ of "for":
324+ # A comprehension over one or more bindings, without the modifier clauses.
325+ if xs.len < 3 or xs[1].kind != kVector: fail("for requires [sym coll] bindings")
326+ var bs = xs[1].items
327+ if bs.len == 0 or bs.len mod 2 != 0: fail("for requires paired bindings")
328+ for j in countup(0, bs.len - 1, 2):
329+ if bs[j].kind == kKeyword: fail("for modifier clauses are not supported: " & prStr(bs[j]))
330+ var body = mkList(@[mkSymbol("do")] & xs[2 .. ^1])
331+ var j = bs.len - 2
332+ while j >= 0:
333+ let combine = (if j == bs.len - 2: "map" else: "mapcat")
334+ body = mkList(@[mkSymbol(combine),
335+ mkList(@[mkSymbol("fn"), mkVector(@[bs[j]]), body]),
336+ bs[j + 1]])
337+ j -= 2
338+ return r.walk(ns, body, locals)
339+ of "with-open":
340+ if xs.len < 2 or xs[1].kind != kVector or xs[1].items.len != 2:
341+ fail("with-open requires [sym resource]")
342+ let bs = xs[1].items
343+ let body = mkList(@[mkSymbol("do")] & xs[2 .. ^1])
344+ let cleanup = mkList(@[mkSymbol("finally"), mkList(@[mkSymbol(".close"), bs[0]])])
345+ return r.walk(ns, mkList(@[mkSymbol("let"), mkVector(bs),
346+ mkList(@[mkSymbol("try"), body, cleanup])]), locals)
347+ of "assert":
348+ if xs.len < 2: fail("assert requires a test")
349+ var msg = mkList(@[mkSymbol("str"), mkStr("Assert failed: " & prStr(xs[1]))])
350+ if xs.len > 2:
351+ msg = mkList(@[mkSymbol("str"), mkStr("Assert failed: "), xs[2]])
352+ return r.walk(ns, mkList(@[mkSymbol("when-not"), xs[1],
353+ mkList(@[mkSymbol("throw"), msg])]), locals)
292 of "->", "->>":354 of "->", "->>":
293 # Expand before resolving: a bare step can be a syntax head (e.g. do).355 # Expand before resolving: a bare step can be a syntax head (e.g. do).
294 if xs.len < 2: fail(h & " requires an expression")356 if xs.len < 2: fail(h & " requires an expression")
modified stdlib/clojure/java/io.clj +7 -2
@@ -1,5 +1,10 @@
11 (ns clojure.java.io)
22
33 ;; Native file operations supplied by Clonim's runtime.
4-(defn delete-file [path silently]
5- (*delete-file* path silently))
4+(defn delete-file
5+ ([path] (*delete-file* path false))
6+ ([path silently] (*delete-file* path silently)))
7+
8+;; A write handle for `with-open`: (.write o bytes) then (.close o).
9+(defn output-stream [path]
10+ (*output-stream* path))
@@ -1,5 +1,10 @@
1 (ns clojure.java.io)1 (ns clojure.java.io)
2 2
3 ;; Native file operations supplied by Clonim's runtime.3 ;; Native file operations supplied by Clonim's runtime.
4-(defn delete-file [path silently]4+(defn delete-file
5- (*delete-file* path silently))5+ ([path] (*delete-file* path false))
6+ ([path silently] (*delete-file* path silently)))
7+
8+;; A write handle for `with-open`: (.write o bytes) then (.close o).
9+(defn output-stream [path]
10+ (*output-stream* path))
added tests/macros.expected +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+:zero :one :few :other
2+:nl
3+1
4+No matching clause: 5
5+(0 1 4 9)
6+([1 :x] [1 :y] [2 :x] [2 :y])
7+abc
8+Assert failed: nope
9+Assert failed: (= 1 2)
10+depth 0
11+depth 3
12+depth 0
13+0
14+hello 5
15+hi
new file mode 100644
@@ -0,0 +1,15 @@
1+:zero :one :few :other
2+:nl
3+1
4+No matching clause: 5
5+(0 1 4 9)
6+([1 :x] [1 :y] [2 :x] [2 :y])
7+abc
8+Assert failed: nope
9+Assert failed: (= 1 2)
10+depth 0
11+depth 3
12+depth 0
13+0
14+hello 5
15+hi