feat: add characters and the host builtins real programs reach for
A char is now its own value kind (reader literals, printing, equality, hashing) rather than an int, so (str (char 27)) is an escape and not "27". Sets and keywords become callable as lookups. Adds mapcat/into/partition-all/partition-by, format (a java.util.Formatter subset), volatiles, char and Character/*, long and the unchecked coercions, Integer/Long/Double parsing, Math/floor and ceil, System/exit, getProperty and currentTimeMillis, ex-message, boolean, class, instance?, and the missing clojure.string predicates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c408078 parent: 399bfb7 added
examples/hostfns.clj +31 -0 | new file mode 100644 | ||
| @@ -0,0 +1,31 @@ | ||
| 1 | +(ns hostfns (:require [clojure.string :as str])) | |
| 2 | + | |
| 3 | +;; characters | |
| 4 | +(println \a \space (int \A) (char 66) (str \x \y) (char? \a)) ; a 65 B xy true | |
| 5 | +(println (.charAt "abc" 1) (= \a \a) (= \a 97) (#{\- \+} \+)) ; b true false + | |
| 6 | +(prn \a \newline \tab) ; \a \newline \tab | |
| 7 | +(println (Character/isDigit \7) (Character/isLetter \7)) ; true false | |
| 8 | + | |
| 9 | +;; numbers | |
| 10 | +(println (long 3.7) (unchecked-int 4294967295) (unchecked-byte 200)) ; 3 -1 -56 | |
| 11 | +(println (Integer/parseInt "ff" 16) (Long/parseLong "-12")) ; 255 -12 | |
| 12 | +(println (Double/parseDouble "2.5") (Math/floor 2.7) (Math/ceil 2.1)); 2.5 2.0 3.0 | |
| 13 | + | |
| 14 | +;; format | |
| 15 | +(println (format "%02x|%s|%5d|%-4s|%.2f|%X|%%" 10 :kw 42 "ab" 3.14159 255)) | |
| 16 | + | |
| 17 | +;; sequences | |
| 18 | +(println (into [] (mapcat (fn [a b] [a b]) [1 2] [:a :b]))) ; [1 :a 2 :b] | |
| 19 | +(println (partition-all 2 [1 2 3 4 5])) ; ((1 2) (3 4) (5)) | |
| 20 | +(println (partition-by even? [1 3 2 4 5])) ; ((1 3) (2 4) (5)) | |
| 21 | +(println (into #{} [1 1 2]) (into {} [[:a 1]])) ; #{1 2} {:a 1} | |
| 22 | + | |
| 23 | +;; strings | |
| 24 | +(println (str/blank? " ") (str/starts-with? "abc" "ab") | |
| 25 | + (str/ends-with? "abc" "bc") (str/includes? "abc" "b") | |
| 26 | + (str/index-of "abc" "c")) ; true true true true 2 | |
| 27 | + | |
| 28 | +;; volatiles, classes, exceptions | |
| 29 | +(let [v (volatile! 1)] (vreset! v 9) (println @v (vswap! v + 1))) ; 9 10 | |
| 30 | +(println (boolean nil) (instance? (class (int-array 0)) (int-array 2))) | |
| 31 | +(println (try (throw (ex-info "boom" {})) (catch Exception e (ex-message e)))) | |
| new file mode 100644 | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | +(ns hostfns (:require [clojure.string :as str])) | ||
| 2 | + | ||
| 3 | +;; characters | ||
| 4 | +(println \a \space (int \A) (char 66) (str \x \y) (char? \a)) ; a 65 B xy true | ||
| 5 | +(println (.charAt "abc" 1) (= \a \a) (= \a 97) (#{\- \+} \+)) ; b true false + | ||
| 6 | +(prn \a \newline \tab) ; \a \newline \tab | ||
| 7 | +(println (Character/isDigit \7) (Character/isLetter \7)) ; true false | ||
| 8 | + | ||
| 9 | +;; numbers | ||
| 10 | +(println (long 3.7) (unchecked-int 4294967295) (unchecked-byte 200)) ; 3 -1 -56 | ||
| 11 | +(println (Integer/parseInt "ff" 16) (Long/parseLong "-12")) ; 255 -12 | ||
| 12 | +(println (Double/parseDouble "2.5") (Math/floor 2.7) (Math/ceil 2.1)); 2.5 2.0 3.0 | ||
| 13 | + | ||
| 14 | +;; format | ||
| 15 | +(println (format "%02x|%s|%5d|%-4s|%.2f|%X|%%" 10 :kw 42 "ab" 3.14159 255)) | ||
| 16 | + | ||
| 17 | +;; sequences | ||
| 18 | +(println (into [] (mapcat (fn [a b] [a b]) [1 2] [:a :b]))) ; [1 :a 2 :b] | ||
| 19 | +(println (partition-all 2 [1 2 3 4 5])) ; ((1 2) (3 4) (5)) | ||
| 20 | +(println (partition-by even? [1 3 2 4 5])) ; ((1 3) (2 4) (5)) | ||
| 21 | +(println (into #{} [1 1 2]) (into {} [[:a 1]])) ; #{1 2} {:a 1} | ||
| 22 | + | ||
| 23 | +;; strings | ||
| 24 | +(println (str/blank? " ") (str/starts-with? "abc" "ab") | ||
| 25 | + (str/ends-with? "abc" "bc") (str/includes? "abc" "b") | ||
| 26 | + (str/index-of "abc" "c")) ; true true true true 2 | ||
| 27 | + | ||
| 28 | +;; volatiles, classes, exceptions | ||
| 29 | +(let [v (volatile! 1)] (vreset! v 9) (println @v (vswap! v + 1))) ; 9 10 | ||
| 30 | +(println (boolean nil) (instance? (class (int-array 0)) (int-array 2))) | ||
| 31 | +(println (try (throw (ex-info "boom" {})) (catch Exception e (ex-message e)))) | ||
modified
src/app_runtime.nim +1 -0 | @@ -30,6 +30,7 @@ iterator elems*(v: Value): Value = | ||
| 30 | 30 | |
| 31 | 31 | proc mkInt*(x: int64): Value {.importc: "clonim_mk_int".} |
| 32 | 32 | proc mkFloat*(x: float64): Value {.importc: "clonim_mk_float".} |
| 33 | +proc mkChar*(code: int64): Value {.importc: "clonim_mk_char".} | |
| 33 | 34 | proc mkStr*(x: string): Value {.importc: "clonim_mk_str".} |
| 34 | 35 | proc mkKeyword*(x: string): Value {.importc: "clonim_mk_keyword".} |
| 35 | 36 | proc mkSymbol*(x: string): Value {.importc: "clonim_mk_symbol".} |
| @@ -30,6 +30,7 @@ iterator elems*(v: Value): Value = | |||
| 30 | 30 | ||
| 31 | proc mkInt*(x: int64): Value {.importc: "clonim_mk_int".} | 31 | proc mkInt*(x: int64): Value {.importc: "clonim_mk_int".} |
| 32 | proc mkFloat*(x: float64): Value {.importc: "clonim_mk_float".} | 32 | proc mkFloat*(x: float64): Value {.importc: "clonim_mk_float".} |
| 33 | +proc mkChar*(code: int64): Value {.importc: "clonim_mk_char".} | ||
| 33 | proc mkStr*(x: string): Value {.importc: "clonim_mk_str".} | 34 | proc mkStr*(x: string): Value {.importc: "clonim_mk_str".} |
| 34 | proc mkKeyword*(x: string): Value {.importc: "clonim_mk_keyword".} | 35 | proc mkKeyword*(x: string): Value {.importc: "clonim_mk_keyword".} |
| 35 | proc mkSymbol*(x: string): Value {.importc: "clonim_mk_symbol".} | 36 | proc mkSymbol*(x: string): Value {.importc: "clonim_mk_symbol".} |
modified
src/compiler.nim +3 -2 | @@ -175,6 +175,7 @@ proc quoteLit(v: Value): string = | ||
| 175 | 175 | of kBool: (if v.b: "TrueV" else: "FalseV") |
| 176 | 176 | of kInt: "mkInt(" & $v.i & ")" |
| 177 | 177 | of kFloat: "mkFloat(" & $v.f & ")" |
| 178 | + of kChar: "mkChar(" & $v.i & ")" | |
| 178 | 179 | of kStr: "mkStr(" & nimStr(v.s) & ")" |
| 179 | 180 | of kKeyword: "mkKeyword(" & nimStr(v.s) & ")" |
| 180 | 181 | of kSymbol: "mkSymbol(" & nimStr(v.s) & ")" |
| @@ -778,7 +779,7 @@ proc tryExpr(f: Value, env: Env, c: Ctx): string = | ||
| 778 | 779 | ## Keeping a subexpression as an expression is what lets the C compiler hold |
| 779 | 780 | ## it in a register instead of round-tripping it through a Value slot. |
| 780 | 781 | case f.kind |
| 781 | - of kNil, kBool, kInt, kFloat, kStr, kKeyword: | |
| 782 | + of kNil, kBool, kInt, kFloat, kChar, kStr, kKeyword: | |
| 782 | 783 | quoteLit(f) |
| 783 | 784 | of kSymbol: |
| 784 | 785 | let local = env.lookup(f.s) |
| @@ -846,7 +847,7 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | ||
| 846 | 847 | if e.len > 0: |
| 847 | 848 | c.line(dst & " = " & e); return |
| 848 | 849 | case f.kind |
| 849 | - of kNil, kBool, kInt, kFloat, kStr, kKeyword: | |
| 850 | + of kNil, kBool, kInt, kFloat, kChar, kStr, kKeyword: | |
| 850 | 851 | c.line(dst & " = " & quoteLit(f)) |
| 851 | 852 | of kSymbol: |
| 852 | 853 | let local = env.lookup(f.s) |
| @@ -175,6 +175,7 @@ proc quoteLit(v: Value): string = | |||
| 175 | of kBool: (if v.b: "TrueV" else: "FalseV") | 175 | of kBool: (if v.b: "TrueV" else: "FalseV") |
| 176 | of kInt: "mkInt(" & $v.i & ")" | 176 | of kInt: "mkInt(" & $v.i & ")" |
| 177 | of kFloat: "mkFloat(" & $v.f & ")" | 177 | of kFloat: "mkFloat(" & $v.f & ")" |
| 178 | + of kChar: "mkChar(" & $v.i & ")" | ||
| 178 | of kStr: "mkStr(" & nimStr(v.s) & ")" | 179 | of kStr: "mkStr(" & nimStr(v.s) & ")" |
| 179 | of kKeyword: "mkKeyword(" & nimStr(v.s) & ")" | 180 | of kKeyword: "mkKeyword(" & nimStr(v.s) & ")" |
| 180 | of kSymbol: "mkSymbol(" & nimStr(v.s) & ")" | 181 | of kSymbol: "mkSymbol(" & nimStr(v.s) & ")" |
| @@ -778,7 +779,7 @@ proc tryExpr(f: Value, env: Env, c: Ctx): string = | |||
| 778 | ## Keeping a subexpression as an expression is what lets the C compiler hold | 779 | ## Keeping a subexpression as an expression is what lets the C compiler hold |
| 779 | ## it in a register instead of round-tripping it through a Value slot. | 780 | ## it in a register instead of round-tripping it through a Value slot. |
| 780 | case f.kind | 781 | case f.kind |
| 781 | - of kNil, kBool, kInt, kFloat, kStr, kKeyword: | 782 | + of kNil, kBool, kInt, kFloat, kChar, kStr, kKeyword: |
| 782 | quoteLit(f) | 783 | quoteLit(f) |
| 783 | of kSymbol: | 784 | of kSymbol: |
| 784 | let local = env.lookup(f.s) | 785 | let local = env.lookup(f.s) |
| @@ -846,7 +847,7 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | |||
| 846 | if e.len > 0: | 847 | if e.len > 0: |
| 847 | c.line(dst & " = " & e); return | 848 | c.line(dst & " = " & e); return |
| 848 | case f.kind | 849 | case f.kind |
| 849 | - of kNil, kBool, kInt, kFloat, kStr, kKeyword: | 850 | + of kNil, kBool, kInt, kFloat, kChar, kStr, kKeyword: |
| 850 | c.line(dst & " = " & quoteLit(f)) | 851 | c.line(dst & " = " & quoteLit(f)) |
| 851 | of kSymbol: | 852 | of kSymbol: |
| 852 | let local = env.lookup(f.s) | 853 | let local = env.lookup(f.s) |
modified
src/core.nim +187 -1 | @@ -15,7 +15,7 @@ proc isFloaty(vs: openArray[Value]): bool = | ||
| 15 | 15 | |
| 16 | 16 | proc intOf(v: Value): int64 = |
| 17 | 17 | case v.kind |
| 18 | - of kInt: v.i | |
| 18 | + of kInt, kChar: v.i | |
| 19 | 19 | of kFloat: int64(v.f) |
| 20 | 20 | else: err("Not a number: " & prStr(v)) |
| 21 | 21 | |
| @@ -397,6 +397,80 @@ proc fusedCount*(base: Value, ops: openArray[FusedOp]): Value = | ||
| 397 | 397 | inc n |
| 398 | 398 | mkInt(n) |
| 399 | 399 | |
| 400 | +proc parseIntRadix(s: string, radix: int): int64 = | |
| 401 | + ## Java's Integer/Long.parseInt: an optional sign, then digits in `radix`. | |
| 402 | + var i = 0 | |
| 403 | + var neg = false | |
| 404 | + if i < s.len and (s[i] == '-' or s[i] == '+'): | |
| 405 | + neg = s[i] == '-'; inc i | |
| 406 | + if i >= s.len: err("Not a number: " & s) | |
| 407 | + while i < s.len: | |
| 408 | + let c = toLowerAscii(s[i]) | |
| 409 | + let d = (if c in '0'..'9': ord(c) - ord('0') | |
| 410 | + elif c in 'a'..'z': ord(c) - ord('a') + 10 | |
| 411 | + else: 99) | |
| 412 | + if d >= radix: err("Not a number: " & s) | |
| 413 | + result = result * int64(radix) + int64(d) | |
| 414 | + inc i | |
| 415 | + if neg: result = -result | |
| 416 | + | |
| 417 | +proc javaFormat(fmt: string, args: openArray[Value]): string = | |
| 418 | + ## The subset of java.util.Formatter that shows up in Clojure source: | |
| 419 | + ## %s %d %x %X %o %c %f %e %b, with flags "-0+ " and width.precision. | |
| 420 | + var ai = 0 | |
| 421 | + var i = 0 | |
| 422 | + while i < fmt.len: | |
| 423 | + if fmt[i] != '%': result.add fmt[i]; inc i; continue | |
| 424 | + inc i | |
| 425 | + if i < fmt.len and fmt[i] == '%': result.add '%'; inc i; continue | |
| 426 | + var flags = "" | |
| 427 | + while i < fmt.len and fmt[i] in {'-', '0', '+', ' ', ',', '#'}: | |
| 428 | + flags.add fmt[i]; inc i | |
| 429 | + var width = "" | |
| 430 | + while i < fmt.len and fmt[i].isDigit: width.add fmt[i]; inc i | |
| 431 | + var prec = -1 | |
| 432 | + if i < fmt.len and fmt[i] == '.': | |
| 433 | + inc i | |
| 434 | + var p = "" | |
| 435 | + while i < fmt.len and fmt[i].isDigit: p.add fmt[i]; inc i | |
| 436 | + prec = parseInt(p) | |
| 437 | + if i >= fmt.len: err("Bad format string: " & fmt) | |
| 438 | + let conv = fmt[i]; inc i | |
| 439 | + if ai >= args.len: err("Too few arguments for format string: " & fmt) | |
| 440 | + let v = args[ai]; inc ai | |
| 441 | + var body: string | |
| 442 | + case conv | |
| 443 | + of 's', 'S': | |
| 444 | + body = str(v) | |
| 445 | + if prec >= 0 and body.len > prec: body = body[0 ..< prec] | |
| 446 | + if conv == 'S': body = body.toUpperAscii | |
| 447 | + of 'd': body = $intOf(v) | |
| 448 | + of 'x', 'X': | |
| 449 | + body = toHex(intOf(v)).strip(trailing = false, chars = {'0'}) | |
| 450 | + if body.len == 0: body = "0" | |
| 451 | + body = (if conv == 'x': body.toLowerAscii else: body.toUpperAscii) | |
| 452 | + of 'o': | |
| 453 | + body = toOct(intOf(v), 22).strip(trailing = false, chars = {'0'}) | |
| 454 | + if body.len == 0: body = "0" | |
| 455 | + of 'f': body = formatFloat(num(v), ffDecimal, (if prec < 0: 6 else: prec)) | |
| 456 | + of 'e', 'E': | |
| 457 | + body = formatFloat(num(v), ffScientific, (if prec < 0: 6 else: prec)) | |
| 458 | + if conv == 'E': body = body.toUpperAscii | |
| 459 | + of 'b': body = (if truthy(v): "true" else: "false") | |
| 460 | + of 'c': body = str(v) | |
| 461 | + of 'n': body = "\n"; dec ai | |
| 462 | + else: err("Unsupported format conversion: %" & conv | |
| 463 | + ) | |
| 464 | + let w = (if width.len == 0: 0 else: parseInt(width)) | |
| 465 | + if body.len < w: | |
| 466 | + let pad = w - body.len | |
| 467 | + if '-' in flags: body = body & " ".repeat(pad) | |
| 468 | + elif '0' in flags and conv in {'d', 'x', 'X', 'o', 'f', 'e', 'E'}: | |
| 469 | + if body.len > 0 and body[0] == '-': body = "-" & "0".repeat(pad) & body[1 .. ^1] | |
| 470 | + else: body = "0".repeat(pad) & body | |
| 471 | + else: body = " ".repeat(pad) & body | |
| 472 | + result.add body | |
| 473 | + | |
| 400 | 474 | proc registerCore*() = |
| 401 | 475 | # ---- arithmetic |
| 402 | 476 | def "+", proc (a: openArray[Value]): Value = |
| @@ -465,6 +539,37 @@ proc registerCore*() = | ||
| 465 | 539 | def "rand-int", proc (a: openArray[Value]): Value = mkInt(rand(int(intOf(a[0])) - 1)) |
| 466 | 540 | def "double", proc (a: openArray[Value]): Value = mkFloat(num(a[0])) |
| 467 | 541 | def "int", proc (a: openArray[Value]): Value = mkInt(intOf(a[0])) |
| 542 | + def "char", proc (a: openArray[Value]): Value = | |
| 543 | + (if a[0].kind == kChar: a[0] else: mkChar(intOf(a[0]))) | |
| 544 | + def "char?", proc (a: openArray[Value]): Value = mkBool(a[0].kind == kChar) | |
| 545 | + def ".charAt", proc (a: openArray[Value]): Value = | |
| 546 | + let i = int(intOf(a[1])) | |
| 547 | + if i < 0 or i >= a[0].s.len: err("String index out of range: " & $i) | |
| 548 | + mkChar(int64(ord(a[0].s[i]))) | |
| 549 | + def "Character/isDigit", proc (a: openArray[Value]): Value = | |
| 550 | + mkBool(a[0].kind == kChar and a[0].i >= int64(ord('0')) and a[0].i <= int64(ord('9'))) | |
| 551 | + def "Character/isLetter", proc (a: openArray[Value]): Value = | |
| 552 | + mkBool(a[0].kind == kChar and char(a[0].i) in Letters) | |
| 553 | + def "Character/isWhitespace", proc (a: openArray[Value]): Value = | |
| 554 | + mkBool(a[0].kind == kChar and char(a[0].i) in {' ', '\t', '\n', '\r', '\f', '\v'}) | |
| 555 | + def "long", proc (a: openArray[Value]): Value = mkInt(intOf(a[0])) | |
| 556 | + def "unchecked-int", proc (a: openArray[Value]): Value = | |
| 557 | + mkInt(int64(cast[int32](uint32(intOf(a[0]) and 0xffffffff'i64)))) | |
| 558 | + def "unchecked-byte", proc (a: openArray[Value]): Value = | |
| 559 | + mkInt(int64(cast[int8](uint8(intOf(a[0]) and 0xff)))) | |
| 560 | + def "Math/floor", proc (a: openArray[Value]): Value = mkFloat(floor(num(a[0]))) | |
| 561 | + def "Math/ceil", proc (a: openArray[Value]): Value = mkFloat(ceil(num(a[0]))) | |
| 562 | + def "Math/abs", proc (a: openArray[Value]): Value = | |
| 563 | + (if a[0].kind == kFloat: mkFloat(abs(a[0].f)) else: mkInt(abs(a[0].i))) | |
| 564 | + def "Integer/parseInt", proc (a: openArray[Value]): Value = | |
| 565 | + let radix = (if a.len > 1: int(intOf(a[1])) else: 10) | |
| 566 | + mkInt(parseIntRadix(a[0].s.strip, radix)) | |
| 567 | + def "Long/parseLong", proc (a: openArray[Value]): Value = | |
| 568 | + let radix = (if a.len > 1: int(intOf(a[1])) else: 10) | |
| 569 | + mkInt(parseIntRadix(a[0].s.strip, radix)) | |
| 570 | + def "Double/parseDouble", proc (a: openArray[Value]): Value = | |
| 571 | + try: mkFloat(parseFloat(a[0].s.strip)) | |
| 572 | + except ValueError: err("Not a number: " & a[0].s) | |
| 468 | 573 | |
| 469 | 574 | # ---- comparison / predicates |
| 470 | 575 | def "=", proc (a: openArray[Value]): Value = |
| @@ -780,6 +885,45 @@ proc registerCore*() = | ||
| 780 | 885 | r.add mkList(s[i ..< i + n]); i += n |
| 781 | 886 | mkList(r) |
| 782 | 887 | |
| 888 | + def "partition-all", proc (a: openArray[Value]): Value = | |
| 889 | + let n = int(intOf(a[0])) | |
| 890 | + let step = (if a.len > 2: int(intOf(a[1])) else: n) | |
| 891 | + let s = toSeq(a[^1]) | |
| 892 | + var r: seq[Value] = @[] | |
| 893 | + var i = 0 | |
| 894 | + while i < s.len: | |
| 895 | + r.add mkList(s[i ..< min(i + n, s.len)]); i += step | |
| 896 | + mkList(r) | |
| 897 | + def "partition-by", proc (a: openArray[Value]): Value = | |
| 898 | + let s = toSeq(a[1]) | |
| 899 | + var r: seq[Value] = @[] | |
| 900 | + var run: seq[Value] = @[] | |
| 901 | + var key = NilV | |
| 902 | + for x in s: | |
| 903 | + let k = call(a[0], [x]) | |
| 904 | + if run.len == 0 or equals(k, key): run.add x | |
| 905 | + else: | |
| 906 | + r.add mkList(run); run = @[x] | |
| 907 | + key = k | |
| 908 | + if run.len > 0: r.add mkList(run) | |
| 909 | + mkList(r) | |
| 910 | + def "into", proc (a: openArray[Value]): Value = | |
| 911 | + result = a[0] | |
| 912 | + if a.len > 1: | |
| 913 | + for x in elems(a[1]): result = conjOne(result, x) | |
| 914 | + def "mapcat", proc (a: openArray[Value]): Value = | |
| 915 | + ## Like map, then concat: the fn takes one element from each collection. | |
| 916 | + var colls: seq[seq[Value]] = @[] | |
| 917 | + for i in 1 ..< a.len: colls.add toSeq(a[i]) | |
| 918 | + var n = -1 | |
| 919 | + for c in colls: (if n < 0 or c.len < n: n = c.len) | |
| 920 | + var r: seq[Value] = @[] | |
| 921 | + for i in 0 ..< max(n, 0): | |
| 922 | + var args: seq[Value] = @[] | |
| 923 | + for c in colls: args.add c[i] | |
| 924 | + for x in elems(call(a[0], args)): r.add x | |
| 925 | + mkList(r) | |
| 926 | + | |
| 783 | 927 | # ---- higher order |
| 784 | 928 | def "apply", proc (a: openArray[Value]): Value = |
| 785 | 929 | var callArgs: seq[Value] = @[] |
| @@ -879,6 +1023,48 @@ proc registerCore*() = | ||
| 879 | 1023 | let nv = call(a[1], @[cur] & @(a[2 .. ^1])) |
| 880 | 1024 | call(a[0], [mkKeyword("set"), nv]) |
| 881 | 1025 | |
| 1026 | + def "ex-message", proc (a: openArray[Value]): Value = | |
| 1027 | + ## Thrown values reach a catch clause as their message string. | |
| 1028 | + (if a[0].kind == kStr: a[0] else: NilV) | |
| 1029 | + def "volatile!", proc (a: openArray[Value]): Value = | |
| 1030 | + var cell = a[0] | |
| 1031 | + mkFn("volatile", proc (args: openArray[Value]): Value = | |
| 1032 | + if args.len == 0: return cell | |
| 1033 | + cell = args[1] | |
| 1034 | + cell) | |
| 1035 | + def "vreset!", proc (a: openArray[Value]): Value = | |
| 1036 | + call(a[0], [mkKeyword("set"), a[1]]) | |
| 1037 | + def "vswap!", proc (a: openArray[Value]): Value = | |
| 1038 | + let cur = call(a[0], []) | |
| 1039 | + call(a[0], [mkKeyword("set"), call(a[1], @[cur] & @(a[2 .. ^1]))]) | |
| 1040 | + def "boolean", proc (a: openArray[Value]): Value = mkBool(truthy(a[0])) | |
| 1041 | + def "class", proc (a: openArray[Value]): Value = mkKeyword($a[0].kind) | |
| 1042 | + def "instance?", proc (a: openArray[Value]): Value = | |
| 1043 | + mkBool(a[0].kind == kKeyword and a[0].s == $a[1].kind) | |
| 1044 | + def "format", proc (a: openArray[Value]): Value = mkStr(javaFormat(a[0].s, a[1 .. ^1])) | |
| 1045 | + def "System/exit", proc (a: openArray[Value]): Value = | |
| 1046 | + quit(if a.len > 0: int(intOf(a[0])) else: 0) | |
| 1047 | + def "System/currentTimeMillis", proc (a: openArray[Value]): Value = | |
| 1048 | + mkInt(int64(epochTime() * 1000)) | |
| 1049 | + def "System/getProperty", proc (a: openArray[Value]): Value = | |
| 1050 | + ## Only the properties a hosted program can reasonably expect here. | |
| 1051 | + case a[0].s | |
| 1052 | + of "java.io.tmpdir": mkStr(getTempDir().strip(leading = false, chars = {'/'})) | |
| 1053 | + of "user.dir": mkStr(getCurrentDir()) | |
| 1054 | + of "user.name": mkStr(getEnv("USER")) | |
| 1055 | + of "line.separator": mkStr("\n") | |
| 1056 | + else: (if a.len > 1: a[1] else: NilV) | |
| 1057 | + def "clojure.string/blank?", proc (a: openArray[Value]): Value = | |
| 1058 | + mkBool(a[0].kind == kNil or a[0].s.strip.len == 0) | |
| 1059 | + def "clojure.string/starts-with?", proc (a: openArray[Value]): Value = | |
| 1060 | + mkBool(a[0].s.startsWith(a[1].s)) | |
| 1061 | + def "clojure.string/ends-with?", proc (a: openArray[Value]): Value = | |
| 1062 | + mkBool(a[0].s.endsWith(a[1].s)) | |
| 1063 | + def "clojure.string/includes?", proc (a: openArray[Value]): Value = | |
| 1064 | + mkBool(a[1].s in a[0].s) | |
| 1065 | + def "clojure.string/index-of", proc (a: openArray[Value]): Value = | |
| 1066 | + let i = a[0].s.find(a[1].s) | |
| 1067 | + (if i < 0: NilV else: mkInt(int64(i))) | |
| 882 | 1068 | def "throw", proc (a: openArray[Value]): Value = err(str(a[0])) |
| 883 | 1069 | def "ex-info", proc (a: openArray[Value]): Value = mkStr(str(a[0])) |
| 884 | 1070 | def "time-ms", proc (a: openArray[Value]): Value = mkInt(int64(epochTime() * 1000)) |
| @@ -15,7 +15,7 @@ proc isFloaty(vs: openArray[Value]): bool = | |||
| 15 | 15 | ||
| 16 | proc intOf(v: Value): int64 = | 16 | proc intOf(v: Value): int64 = |
| 17 | case v.kind | 17 | case v.kind |
| 18 | - of kInt: v.i | 18 | + of kInt, kChar: v.i |
| 19 | of kFloat: int64(v.f) | 19 | of kFloat: int64(v.f) |
| 20 | else: err("Not a number: " & prStr(v)) | 20 | else: err("Not a number: " & prStr(v)) |
| 21 | 21 | ||
| @@ -397,6 +397,80 @@ proc fusedCount*(base: Value, ops: openArray[FusedOp]): Value = | |||
| 397 | inc n | 397 | inc n |
| 398 | mkInt(n) | 398 | mkInt(n) |
| 399 | 399 | ||
| 400 | +proc parseIntRadix(s: string, radix: int): int64 = | ||
| 401 | + ## Java's Integer/Long.parseInt: an optional sign, then digits in `radix`. | ||
| 402 | + var i = 0 | ||
| 403 | + var neg = false | ||
| 404 | + if i < s.len and (s[i] == '-' or s[i] == '+'): | ||
| 405 | + neg = s[i] == '-'; inc i | ||
| 406 | + if i >= s.len: err("Not a number: " & s) | ||
| 407 | + while i < s.len: | ||
| 408 | + let c = toLowerAscii(s[i]) | ||
| 409 | + let d = (if c in '0'..'9': ord(c) - ord('0') | ||
| 410 | + elif c in 'a'..'z': ord(c) - ord('a') + 10 | ||
| 411 | + else: 99) | ||
| 412 | + if d >= radix: err("Not a number: " & s) | ||
| 413 | + result = result * int64(radix) + int64(d) | ||
| 414 | + inc i | ||
| 415 | + if neg: result = -result | ||
| 416 | + | ||
| 417 | +proc javaFormat(fmt: string, args: openArray[Value]): string = | ||
| 418 | + ## The subset of java.util.Formatter that shows up in Clojure source: | ||
| 419 | + ## %s %d %x %X %o %c %f %e %b, with flags "-0+ " and width.precision. | ||
| 420 | + var ai = 0 | ||
| 421 | + var i = 0 | ||
| 422 | + while i < fmt.len: | ||
| 423 | + if fmt[i] != '%': result.add fmt[i]; inc i; continue | ||
| 424 | + inc i | ||
| 425 | + if i < fmt.len and fmt[i] == '%': result.add '%'; inc i; continue | ||
| 426 | + var flags = "" | ||
| 427 | + while i < fmt.len and fmt[i] in {'-', '0', '+', ' ', ',', '#'}: | ||
| 428 | + flags.add fmt[i]; inc i | ||
| 429 | + var width = "" | ||
| 430 | + while i < fmt.len and fmt[i].isDigit: width.add fmt[i]; inc i | ||
| 431 | + var prec = -1 | ||
| 432 | + if i < fmt.len and fmt[i] == '.': | ||
| 433 | + inc i | ||
| 434 | + var p = "" | ||
| 435 | + while i < fmt.len and fmt[i].isDigit: p.add fmt[i]; inc i | ||
| 436 | + prec = parseInt(p) | ||
| 437 | + if i >= fmt.len: err("Bad format string: " & fmt) | ||
| 438 | + let conv = fmt[i]; inc i | ||
| 439 | + if ai >= args.len: err("Too few arguments for format string: " & fmt) | ||
| 440 | + let v = args[ai]; inc ai | ||
| 441 | + var body: string | ||
| 442 | + case conv | ||
| 443 | + of 's', 'S': | ||
| 444 | + body = str(v) | ||
| 445 | + if prec >= 0 and body.len > prec: body = body[0 ..< prec] | ||
| 446 | + if conv == 'S': body = body.toUpperAscii | ||
| 447 | + of 'd': body = $intOf(v) | ||
| 448 | + of 'x', 'X': | ||
| 449 | + body = toHex(intOf(v)).strip(trailing = false, chars = {'0'}) | ||
| 450 | + if body.len == 0: body = "0" | ||
| 451 | + body = (if conv == 'x': body.toLowerAscii else: body.toUpperAscii) | ||
| 452 | + of 'o': | ||
| 453 | + body = toOct(intOf(v), 22).strip(trailing = false, chars = {'0'}) | ||
| 454 | + if body.len == 0: body = "0" | ||
| 455 | + of 'f': body = formatFloat(num(v), ffDecimal, (if prec < 0: 6 else: prec)) | ||
| 456 | + of 'e', 'E': | ||
| 457 | + body = formatFloat(num(v), ffScientific, (if prec < 0: 6 else: prec)) | ||
| 458 | + if conv == 'E': body = body.toUpperAscii | ||
| 459 | + of 'b': body = (if truthy(v): "true" else: "false") | ||
| 460 | + of 'c': body = str(v) | ||
| 461 | + of 'n': body = "\n"; dec ai | ||
| 462 | + else: err("Unsupported format conversion: %" & conv | ||
| 463 | + ) | ||
| 464 | + let w = (if width.len == 0: 0 else: parseInt(width)) | ||
| 465 | + if body.len < w: | ||
| 466 | + let pad = w - body.len | ||
| 467 | + if '-' in flags: body = body & " ".repeat(pad) | ||
| 468 | + elif '0' in flags and conv in {'d', 'x', 'X', 'o', 'f', 'e', 'E'}: | ||
| 469 | + if body.len > 0 and body[0] == '-': body = "-" & "0".repeat(pad) & body[1 .. ^1] | ||
| 470 | + else: body = "0".repeat(pad) & body | ||
| 471 | + else: body = " ".repeat(pad) & body | ||
| 472 | + result.add body | ||
| 473 | + | ||
| 400 | proc registerCore*() = | 474 | proc registerCore*() = |
| 401 | # ---- arithmetic | 475 | # ---- arithmetic |
| 402 | def "+", proc (a: openArray[Value]): Value = | 476 | def "+", proc (a: openArray[Value]): Value = |
| @@ -465,6 +539,37 @@ proc registerCore*() = | |||
| 465 | def "rand-int", proc (a: openArray[Value]): Value = mkInt(rand(int(intOf(a[0])) - 1)) | 539 | def "rand-int", proc (a: openArray[Value]): Value = mkInt(rand(int(intOf(a[0])) - 1)) |
| 466 | def "double", proc (a: openArray[Value]): Value = mkFloat(num(a[0])) | 540 | def "double", proc (a: openArray[Value]): Value = mkFloat(num(a[0])) |
| 467 | def "int", proc (a: openArray[Value]): Value = mkInt(intOf(a[0])) | 541 | def "int", proc (a: openArray[Value]): Value = mkInt(intOf(a[0])) |
| 542 | + def "char", proc (a: openArray[Value]): Value = | ||
| 543 | + (if a[0].kind == kChar: a[0] else: mkChar(intOf(a[0]))) | ||
| 544 | + def "char?", proc (a: openArray[Value]): Value = mkBool(a[0].kind == kChar) | ||
| 545 | + def ".charAt", proc (a: openArray[Value]): Value = | ||
| 546 | + let i = int(intOf(a[1])) | ||
| 547 | + if i < 0 or i >= a[0].s.len: err("String index out of range: " & $i) | ||
| 548 | + mkChar(int64(ord(a[0].s[i]))) | ||
| 549 | + def "Character/isDigit", proc (a: openArray[Value]): Value = | ||
| 550 | + mkBool(a[0].kind == kChar and a[0].i >= int64(ord('0')) and a[0].i <= int64(ord('9'))) | ||
| 551 | + def "Character/isLetter", proc (a: openArray[Value]): Value = | ||
| 552 | + mkBool(a[0].kind == kChar and char(a[0].i) in Letters) | ||
| 553 | + def "Character/isWhitespace", proc (a: openArray[Value]): Value = | ||
| 554 | + mkBool(a[0].kind == kChar and char(a[0].i) in {' ', '\t', '\n', '\r', '\f', '\v'}) | ||
| 555 | + def "long", proc (a: openArray[Value]): Value = mkInt(intOf(a[0])) | ||
| 556 | + def "unchecked-int", proc (a: openArray[Value]): Value = | ||
| 557 | + mkInt(int64(cast[int32](uint32(intOf(a[0]) and 0xffffffff'i64)))) | ||
| 558 | + def "unchecked-byte", proc (a: openArray[Value]): Value = | ||
| 559 | + mkInt(int64(cast[int8](uint8(intOf(a[0]) and 0xff)))) | ||
| 560 | + def "Math/floor", proc (a: openArray[Value]): Value = mkFloat(floor(num(a[0]))) | ||
| 561 | + def "Math/ceil", proc (a: openArray[Value]): Value = mkFloat(ceil(num(a[0]))) | ||
| 562 | + def "Math/abs", proc (a: openArray[Value]): Value = | ||
| 563 | + (if a[0].kind == kFloat: mkFloat(abs(a[0].f)) else: mkInt(abs(a[0].i))) | ||
| 564 | + def "Integer/parseInt", proc (a: openArray[Value]): Value = | ||
| 565 | + let radix = (if a.len > 1: int(intOf(a[1])) else: 10) | ||
| 566 | + mkInt(parseIntRadix(a[0].s.strip, radix)) | ||
| 567 | + def "Long/parseLong", proc (a: openArray[Value]): Value = | ||
| 568 | + let radix = (if a.len > 1: int(intOf(a[1])) else: 10) | ||
| 569 | + mkInt(parseIntRadix(a[0].s.strip, radix)) | ||
| 570 | + def "Double/parseDouble", proc (a: openArray[Value]): Value = | ||
| 571 | + try: mkFloat(parseFloat(a[0].s.strip)) | ||
| 572 | + except ValueError: err("Not a number: " & a[0].s) | ||
| 468 | 573 | ||
| 469 | # ---- comparison / predicates | 574 | # ---- comparison / predicates |
| 470 | def "=", proc (a: openArray[Value]): Value = | 575 | def "=", proc (a: openArray[Value]): Value = |
| @@ -780,6 +885,45 @@ proc registerCore*() = | |||
| 780 | r.add mkList(s[i ..< i + n]); i += n | 885 | r.add mkList(s[i ..< i + n]); i += n |
| 781 | mkList(r) | 886 | mkList(r) |
| 782 | 887 | ||
| 888 | + def "partition-all", proc (a: openArray[Value]): Value = | ||
| 889 | + let n = int(intOf(a[0])) | ||
| 890 | + let step = (if a.len > 2: int(intOf(a[1])) else: n) | ||
| 891 | + let s = toSeq(a[^1]) | ||
| 892 | + var r: seq[Value] = @[] | ||
| 893 | + var i = 0 | ||
| 894 | + while i < s.len: | ||
| 895 | + r.add mkList(s[i ..< min(i + n, s.len)]); i += step | ||
| 896 | + mkList(r) | ||
| 897 | + def "partition-by", proc (a: openArray[Value]): Value = | ||
| 898 | + let s = toSeq(a[1]) | ||
| 899 | + var r: seq[Value] = @[] | ||
| 900 | + var run: seq[Value] = @[] | ||
| 901 | + var key = NilV | ||
| 902 | + for x in s: | ||
| 903 | + let k = call(a[0], [x]) | ||
| 904 | + if run.len == 0 or equals(k, key): run.add x | ||
| 905 | + else: | ||
| 906 | + r.add mkList(run); run = @[x] | ||
| 907 | + key = k | ||
| 908 | + if run.len > 0: r.add mkList(run) | ||
| 909 | + mkList(r) | ||
| 910 | + def "into", proc (a: openArray[Value]): Value = | ||
| 911 | + result = a[0] | ||
| 912 | + if a.len > 1: | ||
| 913 | + for x in elems(a[1]): result = conjOne(result, x) | ||
| 914 | + def "mapcat", proc (a: openArray[Value]): Value = | ||
| 915 | + ## Like map, then concat: the fn takes one element from each collection. | ||
| 916 | + var colls: seq[seq[Value]] = @[] | ||
| 917 | + for i in 1 ..< a.len: colls.add toSeq(a[i]) | ||
| 918 | + var n = -1 | ||
| 919 | + for c in colls: (if n < 0 or c.len < n: n = c.len) | ||
| 920 | + var r: seq[Value] = @[] | ||
| 921 | + for i in 0 ..< max(n, 0): | ||
| 922 | + var args: seq[Value] = @[] | ||
| 923 | + for c in colls: args.add c[i] | ||
| 924 | + for x in elems(call(a[0], args)): r.add x | ||
| 925 | + mkList(r) | ||
| 926 | + | ||
| 783 | # ---- higher order | 927 | # ---- higher order |
| 784 | def "apply", proc (a: openArray[Value]): Value = | 928 | def "apply", proc (a: openArray[Value]): Value = |
| 785 | var callArgs: seq[Value] = @[] | 929 | var callArgs: seq[Value] = @[] |
| @@ -879,6 +1023,48 @@ proc registerCore*() = | |||
| 879 | let nv = call(a[1], @[cur] & @(a[2 .. ^1])) | 1023 | let nv = call(a[1], @[cur] & @(a[2 .. ^1])) |
| 880 | call(a[0], [mkKeyword("set"), nv]) | 1024 | call(a[0], [mkKeyword("set"), nv]) |
| 881 | 1025 | ||
| 1026 | + def "ex-message", proc (a: openArray[Value]): Value = | ||
| 1027 | + ## Thrown values reach a catch clause as their message string. | ||
| 1028 | + (if a[0].kind == kStr: a[0] else: NilV) | ||
| 1029 | + def "volatile!", proc (a: openArray[Value]): Value = | ||
| 1030 | + var cell = a[0] | ||
| 1031 | + mkFn("volatile", proc (args: openArray[Value]): Value = | ||
| 1032 | + if args.len == 0: return cell | ||
| 1033 | + cell = args[1] | ||
| 1034 | + cell) | ||
| 1035 | + def "vreset!", proc (a: openArray[Value]): Value = | ||
| 1036 | + call(a[0], [mkKeyword("set"), a[1]]) | ||
| 1037 | + def "vswap!", proc (a: openArray[Value]): Value = | ||
| 1038 | + let cur = call(a[0], []) | ||
| 1039 | + call(a[0], [mkKeyword("set"), call(a[1], @[cur] & @(a[2 .. ^1]))]) | ||
| 1040 | + def "boolean", proc (a: openArray[Value]): Value = mkBool(truthy(a[0])) | ||
| 1041 | + def "class", proc (a: openArray[Value]): Value = mkKeyword($a[0].kind) | ||
| 1042 | + def "instance?", proc (a: openArray[Value]): Value = | ||
| 1043 | + mkBool(a[0].kind == kKeyword and a[0].s == $a[1].kind) | ||
| 1044 | + def "format", proc (a: openArray[Value]): Value = mkStr(javaFormat(a[0].s, a[1 .. ^1])) | ||
| 1045 | + def "System/exit", proc (a: openArray[Value]): Value = | ||
| 1046 | + quit(if a.len > 0: int(intOf(a[0])) else: 0) | ||
| 1047 | + def "System/currentTimeMillis", proc (a: openArray[Value]): Value = | ||
| 1048 | + mkInt(int64(epochTime() * 1000)) | ||
| 1049 | + def "System/getProperty", proc (a: openArray[Value]): Value = | ||
| 1050 | + ## Only the properties a hosted program can reasonably expect here. | ||
| 1051 | + case a[0].s | ||
| 1052 | + of "java.io.tmpdir": mkStr(getTempDir().strip(leading = false, chars = {'/'})) | ||
| 1053 | + of "user.dir": mkStr(getCurrentDir()) | ||
| 1054 | + of "user.name": mkStr(getEnv("USER")) | ||
| 1055 | + of "line.separator": mkStr("\n") | ||
| 1056 | + else: (if a.len > 1: a[1] else: NilV) | ||
| 1057 | + def "clojure.string/blank?", proc (a: openArray[Value]): Value = | ||
| 1058 | + mkBool(a[0].kind == kNil or a[0].s.strip.len == 0) | ||
| 1059 | + def "clojure.string/starts-with?", proc (a: openArray[Value]): Value = | ||
| 1060 | + mkBool(a[0].s.startsWith(a[1].s)) | ||
| 1061 | + def "clojure.string/ends-with?", proc (a: openArray[Value]): Value = | ||
| 1062 | + mkBool(a[0].s.endsWith(a[1].s)) | ||
| 1063 | + def "clojure.string/includes?", proc (a: openArray[Value]): Value = | ||
| 1064 | + mkBool(a[1].s in a[0].s) | ||
| 1065 | + def "clojure.string/index-of", proc (a: openArray[Value]): Value = | ||
| 1066 | + let i = a[0].s.find(a[1].s) | ||
| 1067 | + (if i < 0: NilV else: mkInt(int64(i))) | ||
| 882 | def "throw", proc (a: openArray[Value]): Value = err(str(a[0])) | 1068 | def "throw", proc (a: openArray[Value]): Value = err(str(a[0])) |
| 883 | def "ex-info", proc (a: openArray[Value]): Value = mkStr(str(a[0])) | 1069 | def "ex-info", proc (a: openArray[Value]): Value = mkStr(str(a[0])) |
| 884 | def "time-ms", proc (a: openArray[Value]): Value = mkInt(int64(epochTime() * 1000)) | 1070 | def "time-ms", proc (a: openArray[Value]): Value = mkInt(int64(epochTime() * 1000)) |
modified
src/reader.nim +29 -0 | @@ -80,6 +80,33 @@ proc readString(r: var Reader): Value = | ||
| 80 | 80 | s.add c |
| 81 | 81 | mkStr(s) |
| 82 | 82 | |
| 83 | +proc readChar(r: var Reader): Value = | |
| 84 | + ## A character literal: \a, \newline, \uXXXX, or any single character — | |
| 85 | + ## including the delimiters, which never start a name here. | |
| 86 | + discard r.advance # the backslash | |
| 87 | + if r.pos >= r.src.len: r.readerErr("EOF while reading character") | |
| 88 | + let first = r.advance | |
| 89 | + var name = $first | |
| 90 | + if first in Letters or first in Digits: | |
| 91 | + while r.pos < r.src.len: | |
| 92 | + let c = r.peek | |
| 93 | + if c in {' ', '\t', '\n', '\r', ','} or (c in macroChars and c != '\''): break | |
| 94 | + name.add r.advance | |
| 95 | + if name.len == 1: return mkChar(int64(ord(name[0]))) | |
| 96 | + case name | |
| 97 | + of "newline": mkChar(10) | |
| 98 | + of "tab": mkChar(9) | |
| 99 | + of "return": mkChar(13) | |
| 100 | + of "space": mkChar(32) | |
| 101 | + of "backspace": mkChar(8) | |
| 102 | + of "formfeed": mkChar(12) | |
| 103 | + else: | |
| 104 | + if name[0] == 'u' and name.len == 5: | |
| 105 | + try: return mkChar(int64(parseHexInt(name[1 .. ^1]))) | |
| 106 | + except ValueError: discard | |
| 107 | + r.readerErr("Unsupported character literal: \\" & name) | |
| 108 | + NilV | |
| 109 | + | |
| 83 | 110 | proc readToken(r: var Reader): string = |
| 84 | 111 | result = "" |
| 85 | 112 | while r.pos < r.src.len: |
| @@ -135,6 +162,8 @@ proc readForm(r: var Reader): Value = | ||
| 135 | 162 | r.readerErr("Unmatched delimiter: " & c) |
| 136 | 163 | of '"': |
| 137 | 164 | return r.readString |
| 165 | + of '\\': | |
| 166 | + return r.readChar | |
| 138 | 167 | of '\'': |
| 139 | 168 | discard r.advance |
| 140 | 169 | return mkList(@[mkSymbol("quote"), r.readForm]) |
| @@ -80,6 +80,33 @@ proc readString(r: var Reader): Value = | |||
| 80 | s.add c | 80 | s.add c |
| 81 | mkStr(s) | 81 | mkStr(s) |
| 82 | 82 | ||
| 83 | +proc readChar(r: var Reader): Value = | ||
| 84 | + ## A character literal: \a, \newline, \uXXXX, or any single character — | ||
| 85 | + ## including the delimiters, which never start a name here. | ||
| 86 | + discard r.advance # the backslash | ||
| 87 | + if r.pos >= r.src.len: r.readerErr("EOF while reading character") | ||
| 88 | + let first = r.advance | ||
| 89 | + var name = $first | ||
| 90 | + if first in Letters or first in Digits: | ||
| 91 | + while r.pos < r.src.len: | ||
| 92 | + let c = r.peek | ||
| 93 | + if c in {' ', '\t', '\n', '\r', ','} or (c in macroChars and c != '\''): break | ||
| 94 | + name.add r.advance | ||
| 95 | + if name.len == 1: return mkChar(int64(ord(name[0]))) | ||
| 96 | + case name | ||
| 97 | + of "newline": mkChar(10) | ||
| 98 | + of "tab": mkChar(9) | ||
| 99 | + of "return": mkChar(13) | ||
| 100 | + of "space": mkChar(32) | ||
| 101 | + of "backspace": mkChar(8) | ||
| 102 | + of "formfeed": mkChar(12) | ||
| 103 | + else: | ||
| 104 | + if name[0] == 'u' and name.len == 5: | ||
| 105 | + try: return mkChar(int64(parseHexInt(name[1 .. ^1]))) | ||
| 106 | + except ValueError: discard | ||
| 107 | + r.readerErr("Unsupported character literal: \\" & name) | ||
| 108 | + NilV | ||
| 109 | + | ||
| 83 | proc readToken(r: var Reader): string = | 110 | proc readToken(r: var Reader): string = |
| 84 | result = "" | 111 | result = "" |
| 85 | while r.pos < r.src.len: | 112 | while r.pos < r.src.len: |
| @@ -135,6 +162,8 @@ proc readForm(r: var Reader): Value = | |||
| 135 | r.readerErr("Unmatched delimiter: " & c) | 162 | r.readerErr("Unmatched delimiter: " & c) |
| 136 | of '"': | 163 | of '"': |
| 137 | return r.readString | 164 | return r.readString |
| 165 | + of '\\': | ||
| 166 | + return r.readChar | ||
| 138 | of '\'': | 167 | of '\'': |
| 139 | discard r.advance | 168 | discard r.advance |
| 140 | return mkList(@[mkSymbol("quote"), r.readForm]) | 169 | return mkList(@[mkSymbol("quote"), r.readForm]) |
modified
src/runtime.nim +21 -2 | @@ -6,7 +6,7 @@ | ||
| 6 | 6 | ## collection. Maps and sets additionally remember insertion order — every |
| 7 | 7 | ## entry carries an `ord` stamp and iteration sorts by it — so printing and |
| 8 | 8 | ## `keys`/`vals` stay predictable the way Clojure's small array-maps are. |
| 9 | -import std/[tables, strutils, hashes, bitops, algorithm] | |
| 9 | +import std/[tables, strutils, hashes, bitops, algorithm, unicode] | |
| 10 | 10 | import runtime_types |
| 11 | 11 | export runtime_types |
| 12 | 12 | |
| @@ -267,6 +267,7 @@ proc mapEntries*(m: PMap): seq[MEntry] = | ||
| 267 | 267 | proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV) |
| 268 | 268 | proc mkInt*(x: int64): Value {.inline.} = Value(kind: kInt, raw: x) |
| 269 | 269 | proc mkFloat*(x: float64): Value {.inline.} = Value(kind: kFloat, raw: cast[int64](x)) |
| 270 | +proc mkChar*(code: int64): Value {.inline.} = Value(kind: kChar, raw: code) | |
| 270 | 271 | proc mkStr*(x: string): Value = Value(kind: kStr, obj: Obj(kind: kStr, s: x)) |
| 271 | 272 | proc mkKeyword*(x: string): Value = Value(kind: kKeyword, obj: Obj(kind: kKeyword, s: x)) |
| 272 | 273 | proc mkSymbol*(x: string): Value = Value(kind: kSymbol, obj: Obj(kind: kSymbol, s: x)) |
| @@ -445,6 +446,7 @@ proc hashValue*(v: Value): uint32 = | ||
| 445 | 446 | of kNil: 0'u32 |
| 446 | 447 | of kBool: (if v.b: 0x9e3779b9'u32 else: 0x85ebca6b'u32) |
| 447 | 448 | of kInt: uint32(hash(v.i)) |
| 449 | + of kChar: mixHash(5'u32, uint32(hash(v.i))) | |
| 448 | 450 | of kFloat: |
| 449 | 451 | # ints and floats compare equal across kinds, so they must hash alike |
| 450 | 452 | if v.f == float64(int64(v.f)): uint32(hash(int64(v.f))) |
| @@ -536,6 +538,7 @@ proc equals*(a, b: Value): bool = | ||
| 536 | 538 | of kNil: true |
| 537 | 539 | of kBool: a.b == b.b |
| 538 | 540 | of kInt: a.i == b.i |
| 541 | + of kChar: a.i == b.i | |
| 539 | 542 | of kFloat: a.f == b.f |
| 540 | 543 | of kStr, kKeyword, kSymbol: a.s == b.s |
| 541 | 544 | of kSet: |
| @@ -574,6 +577,17 @@ proc toStr*(v: Value, readable: bool): string = | ||
| 574 | 577 | var s = $v.f |
| 575 | 578 | if '.' notin s and 'e' notin s and 'n' notin s and 'i' notin s: s &= ".0" |
| 576 | 579 | s |
| 580 | + of kChar: | |
| 581 | + if not readable: $Rune(v.i) | |
| 582 | + else: | |
| 583 | + case v.i | |
| 584 | + of 10: "\\newline" | |
| 585 | + of 9: "\\tab" | |
| 586 | + of 13: "\\return" | |
| 587 | + of 8: "\\backspace" | |
| 588 | + of 12: "\\formfeed" | |
| 589 | + of 32: "\\space" | |
| 590 | + else: "\\" & $Rune(v.i) | |
| 577 | 591 | of kStr: (if readable: escapeStr(v.s) else: v.s) |
| 578 | 592 | of kKeyword: ":" & v.s |
| 579 | 593 | of kSymbol: v.s |
| @@ -635,11 +649,16 @@ proc call*(f: Value, args: openArray[Value]): Value = | ||
| 635 | 649 | # (:k m) => lookup |
| 636 | 650 | if args.len == 0: err("Wrong number of args to keyword") |
| 637 | 651 | let m = args[0] |
| 638 | - if m.isNil or m.kind != kMap: return NilV | |
| 652 | + if m.isNil or m.kind notin {kMap, kSet}: return NilV | |
| 639 | 653 | mapGet(m.m, f, (if args.len > 1: args[1] else: NilV)) |
| 640 | 654 | of kMap: |
| 641 | 655 | if args.len == 0: err("Wrong number of args to map") |
| 642 | 656 | mapGet(f.m, args[0], (if args.len > 1: args[1] else: NilV)) |
| 657 | + of kSet: | |
| 658 | + # (#{...} x) => the element, or nil | |
| 659 | + if args.len == 0: err("Wrong number of args to set") | |
| 660 | + if mapContains(f.m, args[0]): args[0] | |
| 661 | + else: (if args.len > 1: args[1] else: NilV) | |
| 643 | 662 | of kVector: |
| 644 | 663 | if args.len != 1 or args[0].kind != kInt: err("Vector lookup needs one int") |
| 645 | 664 | vecNth(f.vec, int(args[0].i)) |
| @@ -6,7 +6,7 @@ | |||
| 6 | ## collection. Maps and sets additionally remember insertion order — every | 6 | ## collection. Maps and sets additionally remember insertion order — every |
| 7 | ## entry carries an `ord` stamp and iteration sorts by it — so printing and | 7 | ## entry carries an `ord` stamp and iteration sorts by it — so printing and |
| 8 | ## `keys`/`vals` stay predictable the way Clojure's small array-maps are. | 8 | ## `keys`/`vals` stay predictable the way Clojure's small array-maps are. |
| 9 | -import std/[tables, strutils, hashes, bitops, algorithm] | 9 | +import std/[tables, strutils, hashes, bitops, algorithm, unicode] |
| 10 | import runtime_types | 10 | import runtime_types |
| 11 | export runtime_types | 11 | export runtime_types |
| 12 | 12 | ||
| @@ -267,6 +267,7 @@ proc mapEntries*(m: PMap): seq[MEntry] = | |||
| 267 | proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV) | 267 | proc mkBool*(x: bool): Value = (if x: TrueV else: FalseV) |
| 268 | proc mkInt*(x: int64): Value {.inline.} = Value(kind: kInt, raw: x) | 268 | proc mkInt*(x: int64): Value {.inline.} = Value(kind: kInt, raw: x) |
| 269 | proc mkFloat*(x: float64): Value {.inline.} = Value(kind: kFloat, raw: cast[int64](x)) | 269 | proc mkFloat*(x: float64): Value {.inline.} = Value(kind: kFloat, raw: cast[int64](x)) |
| 270 | +proc mkChar*(code: int64): Value {.inline.} = Value(kind: kChar, raw: code) | ||
| 270 | proc mkStr*(x: string): Value = Value(kind: kStr, obj: Obj(kind: kStr, s: x)) | 271 | proc mkStr*(x: string): Value = Value(kind: kStr, obj: Obj(kind: kStr, s: x)) |
| 271 | proc mkKeyword*(x: string): Value = Value(kind: kKeyword, obj: Obj(kind: kKeyword, s: x)) | 272 | proc mkKeyword*(x: string): Value = Value(kind: kKeyword, obj: Obj(kind: kKeyword, s: x)) |
| 272 | proc mkSymbol*(x: string): Value = Value(kind: kSymbol, obj: Obj(kind: kSymbol, s: x)) | 273 | proc mkSymbol*(x: string): Value = Value(kind: kSymbol, obj: Obj(kind: kSymbol, s: x)) |
| @@ -445,6 +446,7 @@ proc hashValue*(v: Value): uint32 = | |||
| 445 | of kNil: 0'u32 | 446 | of kNil: 0'u32 |
| 446 | of kBool: (if v.b: 0x9e3779b9'u32 else: 0x85ebca6b'u32) | 447 | of kBool: (if v.b: 0x9e3779b9'u32 else: 0x85ebca6b'u32) |
| 447 | of kInt: uint32(hash(v.i)) | 448 | of kInt: uint32(hash(v.i)) |
| 449 | + of kChar: mixHash(5'u32, uint32(hash(v.i))) | ||
| 448 | of kFloat: | 450 | of kFloat: |
| 449 | # ints and floats compare equal across kinds, so they must hash alike | 451 | # ints and floats compare equal across kinds, so they must hash alike |
| 450 | if v.f == float64(int64(v.f)): uint32(hash(int64(v.f))) | 452 | if v.f == float64(int64(v.f)): uint32(hash(int64(v.f))) |
| @@ -536,6 +538,7 @@ proc equals*(a, b: Value): bool = | |||
| 536 | of kNil: true | 538 | of kNil: true |
| 537 | of kBool: a.b == b.b | 539 | of kBool: a.b == b.b |
| 538 | of kInt: a.i == b.i | 540 | of kInt: a.i == b.i |
| 541 | + of kChar: a.i == b.i | ||
| 539 | of kFloat: a.f == b.f | 542 | of kFloat: a.f == b.f |
| 540 | of kStr, kKeyword, kSymbol: a.s == b.s | 543 | of kStr, kKeyword, kSymbol: a.s == b.s |
| 541 | of kSet: | 544 | of kSet: |
| @@ -574,6 +577,17 @@ proc toStr*(v: Value, readable: bool): string = | |||
| 574 | var s = $v.f | 577 | var s = $v.f |
| 575 | if '.' notin s and 'e' notin s and 'n' notin s and 'i' notin s: s &= ".0" | 578 | if '.' notin s and 'e' notin s and 'n' notin s and 'i' notin s: s &= ".0" |
| 576 | s | 579 | s |
| 580 | + of kChar: | ||
| 581 | + if not readable: $Rune(v.i) | ||
| 582 | + else: | ||
| 583 | + case v.i | ||
| 584 | + of 10: "\\newline" | ||
| 585 | + of 9: "\\tab" | ||
| 586 | + of 13: "\\return" | ||
| 587 | + of 8: "\\backspace" | ||
| 588 | + of 12: "\\formfeed" | ||
| 589 | + of 32: "\\space" | ||
| 590 | + else: "\\" & $Rune(v.i) | ||
| 577 | of kStr: (if readable: escapeStr(v.s) else: v.s) | 591 | of kStr: (if readable: escapeStr(v.s) else: v.s) |
| 578 | of kKeyword: ":" & v.s | 592 | of kKeyword: ":" & v.s |
| 579 | of kSymbol: v.s | 593 | of kSymbol: v.s |
| @@ -635,11 +649,16 @@ proc call*(f: Value, args: openArray[Value]): Value = | |||
| 635 | # (:k m) => lookup | 649 | # (:k m) => lookup |
| 636 | if args.len == 0: err("Wrong number of args to keyword") | 650 | if args.len == 0: err("Wrong number of args to keyword") |
| 637 | let m = args[0] | 651 | let m = args[0] |
| 638 | - if m.isNil or m.kind != kMap: return NilV | 652 | + if m.isNil or m.kind notin {kMap, kSet}: return NilV |
| 639 | mapGet(m.m, f, (if args.len > 1: args[1] else: NilV)) | 653 | mapGet(m.m, f, (if args.len > 1: args[1] else: NilV)) |
| 640 | of kMap: | 654 | of kMap: |
| 641 | if args.len == 0: err("Wrong number of args to map") | 655 | if args.len == 0: err("Wrong number of args to map") |
| 642 | mapGet(f.m, args[0], (if args.len > 1: args[1] else: NilV)) | 656 | mapGet(f.m, args[0], (if args.len > 1: args[1] else: NilV)) |
| 657 | + of kSet: | ||
| 658 | + # (#{...} x) => the element, or nil | ||
| 659 | + if args.len == 0: err("Wrong number of args to set") | ||
| 660 | + if mapContains(f.m, args[0]): args[0] | ||
| 661 | + else: (if args.len > 1: args[1] else: NilV) | ||
| 643 | of kVector: | 662 | of kVector: |
| 644 | if args.len != 1 or args[0].kind != kInt: err("Vector lookup needs one int") | 663 | if args.len != 1 or args[0].kind != kInt: err("Vector lookup needs one int") |
| 645 | vecNth(f.vec, int(args[0].i)) | 664 | vecNth(f.vec, int(args[0].i)) |
modified
src/runtime_lib.nim +1 -0 | @@ -24,6 +24,7 @@ proc clonim_truthy(v: Value): bool = truthy(v) | ||
| 24 | 24 | |
| 25 | 25 | proc clonim_mk_int(x: int64): Value = mkInt(x) |
| 26 | 26 | proc clonim_mk_float(x: float64): Value = mkFloat(x) |
| 27 | +proc clonim_mk_char(code: int64): Value = mkChar(code) | |
| 27 | 28 | proc clonim_mk_str(x: string): Value = mkStr(x) |
| 28 | 29 | proc clonim_mk_keyword(x: string): Value = mkKeyword(x) |
| 29 | 30 | proc clonim_mk_symbol(x: string): Value = mkSymbol(x) |
| @@ -24,6 +24,7 @@ proc clonim_truthy(v: Value): bool = truthy(v) | |||
| 24 | 24 | ||
| 25 | proc clonim_mk_int(x: int64): Value = mkInt(x) | 25 | proc clonim_mk_int(x: int64): Value = mkInt(x) |
| 26 | proc clonim_mk_float(x: float64): Value = mkFloat(x) | 26 | proc clonim_mk_float(x: float64): Value = mkFloat(x) |
| 27 | +proc clonim_mk_char(code: int64): Value = mkChar(code) | ||
| 27 | proc clonim_mk_str(x: string): Value = mkStr(x) | 28 | proc clonim_mk_str(x: string): Value = mkStr(x) |
| 28 | proc clonim_mk_keyword(x: string): Value = mkKeyword(x) | 29 | proc clonim_mk_keyword(x: string): Value = mkKeyword(x) |
| 29 | proc clonim_mk_symbol(x: string): Value = mkSymbol(x) | 30 | proc clonim_mk_symbol(x: string): Value = mkSymbol(x) |
modified
src/runtime_types.nim +3 -3 | @@ -4,7 +4,7 @@ | ||
| 4 | 4 | |
| 5 | 5 | type |
| 6 | 6 | Kind* = enum |
| 7 | - kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol, | |
| 7 | + kNil, kBool, kInt, kFloat, kChar, kStr, kKeyword, kSymbol, | |
| 8 | 8 | kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn |
| 9 | 9 | |
| 10 | 10 | VNode* = ref object |
| @@ -53,7 +53,7 @@ type | ||
| 53 | 53 | Obj* = ref ValueObj |
| 54 | 54 | ValueObj* = object |
| 55 | 55 | case kind*: Kind |
| 56 | - of kNil, kBool, kInt, kFloat: discard | |
| 56 | + of kNil, kBool, kInt, kFloat, kChar: discard | |
| 57 | 57 | of kStr, kKeyword, kSymbol: s*: string |
| 58 | 58 | of kList: xs*: seq[Value] |
| 59 | 59 | of kVector: vec*: PVec |
| @@ -133,7 +133,7 @@ proc `=destroy`*(x: var ValueObj) = | ||
| 133 | 133 | `=destroy`(x.thunk) |
| 134 | 134 | if not x.cached.isNil: pendingFree.add x.cached |
| 135 | 135 | `=destroy`(x.cached) |
| 136 | - of kNil, kBool, kInt, kFloat: discard | |
| 136 | + of kNil, kBool, kInt, kFloat, kChar: discard | |
| 137 | 137 | if draining: return |
| 138 | 138 | draining = true |
| 139 | 139 | while pendingFree.len > 0: |
| @@ -4,7 +4,7 @@ | |||
| 4 | 4 | ||
| 5 | type | 5 | type |
| 6 | Kind* = enum | 6 | Kind* = enum |
| 7 | - kNil, kBool, kInt, kFloat, kStr, kKeyword, kSymbol, | 7 | + kNil, kBool, kInt, kFloat, kChar, kStr, kKeyword, kSymbol, |
| 8 | kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn | 8 | kList, kVector, kMap, kSet, kCons, kChunk, kLazy, kFn |
| 9 | 9 | ||
| 10 | VNode* = ref object | 10 | VNode* = ref object |
| @@ -53,7 +53,7 @@ type | |||
| 53 | Obj* = ref ValueObj | 53 | Obj* = ref ValueObj |
| 54 | ValueObj* = object | 54 | ValueObj* = object |
| 55 | case kind*: Kind | 55 | case kind*: Kind |
| 56 | - of kNil, kBool, kInt, kFloat: discard | 56 | + of kNil, kBool, kInt, kFloat, kChar: discard |
| 57 | of kStr, kKeyword, kSymbol: s*: string | 57 | of kStr, kKeyword, kSymbol: s*: string |
| 58 | of kList: xs*: seq[Value] | 58 | of kList: xs*: seq[Value] |
| 59 | of kVector: vec*: PVec | 59 | of kVector: vec*: PVec |
| @@ -133,7 +133,7 @@ proc `=destroy`*(x: var ValueObj) = | |||
| 133 | `=destroy`(x.thunk) | 133 | `=destroy`(x.thunk) |
| 134 | if not x.cached.isNil: pendingFree.add x.cached | 134 | if not x.cached.isNil: pendingFree.add x.cached |
| 135 | `=destroy`(x.cached) | 135 | `=destroy`(x.cached) |
| 136 | - of kNil, kBool, kInt, kFloat: discard | 136 | + of kNil, kBool, kInt, kFloat, kChar: discard |
| 137 | if draining: return | 137 | if draining: return |
| 138 | draining = true | 138 | draining = true |
| 139 | while pendingFree.len > 0: | 139 | while pendingFree.len > 0: |
added
tests/hostfns.expected +16 -0 | new file mode 100644 | ||
| @@ -0,0 +1,16 @@ | ||
| 1 | +a 65 B xy true | |
| 2 | +b true false + | |
| 3 | +\a \newline \tab | |
| 4 | +true false | |
| 5 | +3 -1 -56 | |
| 6 | +255 -12 | |
| 7 | +2.5 2.0 3.0 | |
| 8 | +0a|:kw| 42|ab |3.14|FF|% | |
| 9 | +[1 :a 2 :b] | |
| 10 | +((1 2) (3 4) (5)) | |
| 11 | +((1 3) (2 4) (5)) | |
| 12 | +#{1 2} {:a 1} | |
| 13 | +true true true true 2 | |
| 14 | +9 10 | |
| 15 | +false true | |
| 16 | +boom | |
| new file mode 100644 | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | +a 65 B xy true | ||
| 2 | +b true false + | ||
| 3 | +\a \newline \tab | ||
| 4 | +true false | ||
| 5 | +3 -1 -56 | ||
| 6 | +255 -12 | ||
| 7 | +2.5 2.0 3.0 | ||
| 8 | +0a|:kw| 42|ab |3.14|FF|% | ||
| 9 | +[1 :a 2 :b] | ||
| 10 | +((1 2) (3 4) (5)) | ||
| 11 | +((1 3) (2 4) (5)) | ||
| 12 | +#{1 2} {:a 1} | ||
| 13 | +true true true true 2 | ||
| 14 | +9 10 | ||
| 15 | +false true | ||
| 16 | +boom | ||