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>
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*", | ||
| 143 | 143 | "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or", |
| 144 | 144 | "when", "when-not", "if-not", "cond", "when-let", "if-let", "->", "->>", |
| 145 | 145 | "doseq", "dotimes", "try", "comment", "ns", "require", "in-ns", "use", |
| 146 | - "import", "set!", "declare"].toHashSet | |
| 146 | + "import", "set!", "declare", "binding"].toHashSet | |
| 147 | 147 | |
| 148 | 148 | const intOps = {"+": "+", "-": "-", "*": "*"}.toTable |
| 149 | 149 | const intCalls = {"quot": "idiv", "rem": "irem"}.toTable |
| @@ -1091,6 +1091,31 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | ||
| 1091 | 1091 | c.pop |
| 1092 | 1092 | c.line(dst & " = NilV") |
| 1093 | 1093 | 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 | 1119 | of "try": |
| 1095 | 1120 | var bodyForms: seq[Value] = @[] |
| 1096 | 1121 | 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"].toHashSet | 146 | + "import", "set!", "declare", "binding"].toHashSet |
| 147 | 147 | ||
| 148 | const intOps = {"+": "+", "-": "-", "*": "*"}.toTable | 148 | const intOps = {"+": "+", "-": "-", "*": "*"}.toTable |
| 149 | const intCalls = {"quot": "idiv", "rem": "irem"}.toTable | 149 | const intCalls = {"quot": "idiv", "rem": "irem"}.toTable |
| @@ -1091,6 +1091,31 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | |||
| 1091 | c.pop | 1091 | c.pop |
| 1092 | c.line(dst & " = NilV") | 1092 | c.line(dst & " = NilV") |
| 1093 | return | 1093 | 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 = | ||
| 338 | 338 | proc def(name: string, f: proc (args: openArray[Value]): Value {.closure.}) = |
| 339 | 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 | 345 | # ----------------------------------------------------------------- fusion |
| 342 | 346 | ## A pipeline whose intermediate sequences are syntactic temporaries -- nobody |
| 343 | 347 | ## 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 | 475 | else: body = " ".repeat(pad) & body |
| 472 | 476 | 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 | 485 | proc registerCore*() = |
| 475 | 486 | # ---- arithmetic |
| 476 | 487 | def "+", proc (a: openArray[Value]): Value = |
| @@ -625,21 +636,37 @@ proc registerCore*() = | ||
| 625 | 636 | var parts: seq[string] = @[] |
| 626 | 637 | for x in a: parts.add prStr(x) |
| 627 | 638 | mkStr(parts.join(" ")) |
| 639 | + def "*out*", mkKeyword("stdout") | |
| 640 | + def "*err*", mkKeyword("stderr") | |
| 641 | + def "*in*", mkKeyword("stdin") | |
| 628 | 642 | def "println", proc (a: openArray[Value]): Value = |
| 629 | 643 | var parts: seq[string] = @[] |
| 630 | 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 | 648 | NilV |
| 633 | 649 | def "prn", proc (a: openArray[Value]): Value = |
| 634 | 650 | var parts: seq[string] = @[] |
| 635 | 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 | 655 | NilV |
| 638 | 656 | def "print", proc (a: openArray[Value]): Value = |
| 639 | 657 | var parts: seq[string] = @[] |
| 640 | 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 | 665 | 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 | 670 | def "name", proc (a: openArray[Value]): Value = |
| 644 | 671 | case a[0].kind |
| 645 | 672 | of kKeyword, kSymbol, kStr: mkStr(a[0].s) |
| @@ -701,6 +728,39 @@ proc registerCore*() = | ||
| 701 | 728 | var xs: seq[Value] = @[] |
| 702 | 729 | for ch in a[0].s: xs.add mkInt(ord(ch)) |
| 703 | 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 | 764 | def "*delete-file*", proc (a: openArray[Value]): Value = |
| 705 | 765 | try: |
| 706 | 766 | 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 | # ----------------------------------------------------------------- fusion | 345 | # ----------------------------------------------------------------- fusion |
| 342 | ## A pipeline whose intermediate sequences are syntactic temporaries -- nobody | 346 | ## A pipeline whose intermediate sequences are syntactic temporaries -- nobody |
| 343 | ## named them, so nobody can hold them -- does not need those sequences to | 347 | ## 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) & body | 475 | else: body = " ".repeat(pad) & body |
| 472 | result.add body | 476 | 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 | # ---- arithmetic | 486 | # ---- 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 | NilV | 648 | 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 | NilV | 655 | 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 | NilV | 665 | 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].kind | 671 | 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 | ||
| 21 | 21 | active: seq[string] |
| 22 | 22 | loaded: HashSet[string] |
| 23 | 23 | output: seq[Value] |
| 24 | + gensym: int | |
| 24 | 25 | |
| 25 | 26 | const syntaxHeads = ["quote", "if", "do", "let", "let*", "loop", "loop*", |
| 26 | 27 | "recur", "fn", "fn*", "def", "defn", "defn-", "defmacro", "and", "or", |
| 27 | 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 | 32 | # Unlike macros, these heads cannot be shadowed in operator position. |
| 31 | 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 | 291 | scope.incl simple(xs[2], "catch binding") |
| 290 | 292 | for i in 3 ..< xs.len: xs[i] = r.walk(ns, xs[i], scope) |
| 291 | 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 | 354 | of "->", "->>": |
| 293 | 355 | # Expand before resolving: a bare step can be a syntax head (e.g. do). |
| 294 | 356 | 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 @@ | ||
| 1 | 1 | (ns clojure.java.io) |
| 2 | 2 | |
| 3 | 3 | ;; 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 | ||