diff --git a/src/jolt/reader.janet b/src/jolt/reader.janet
index 145ac9a..c2e6164 100644
--- a/src/jolt/reader.janet
+++ b/src/jolt/reader.janet
@@ -159,41 +159,78 @@
(read-fractional s pos (+ end 1))
end))
+# Value of an alphanumeric digit for radix parsing (0-9, a-z/A-Z = 10-35).
+(defn- radix-digit-val [c]
+ (cond
+ (and (>= c 48) (<= c 57)) (- c 48) # 0-9
+ (and (>= c 97) (<= c 122)) (+ 10 (- c 97)) # a-z
+ (and (>= c 65) (<= c 90)) (+ 10 (- c 65)) # A-Z
+ nil))
+(defn- read-alnum [s pos end]
+ (if (and (< end (length s)) (not (nil? (radix-digit-val (s end)))))
+ (read-alnum s pos (+ end 1))
+ end))
+(defn- read-exponent
+ "If s[end] is e/E (optionally with sign) followed by digits, return the index
+ past the exponent; else end."
+ [s end]
+ (let [len (length s)]
+ (if (and (< end len) (or (= (s end) 101) (= (s end) 69))) # e / E
+ (let [p (if (and (< (+ end 1) len) (or (= (s (+ end 1)) 43) (= (s (+ end 1)) 45))) (+ end 2) (+ end 1))
+ de (read-digits s p p)]
+ (if (> de p) de end))
+ end)))
+
+# Jolt has no true bignum/ratio types (see README): an integer/float literal
+# suffixed N (bigint) or M (bigdec) reads as the plain number, a ratio a/b reads
+# as the double quotient, and radixed integers (2r101, 16rFF) are parsed by base.
(defn read-number [s pos]
(var start pos) # start is mutable for sign handling
(var neg false)
-
+ (def len (length s))
# optional sign
- (if (and (< pos (length s)) (= (s pos) 45))
+ (if (and (< pos len) (= (s pos) 45))
(do (set start (+ pos 1)) (set neg true)))
-
+
(let [pos start
- hex? (and (< (+ pos 1) (length s))
- (= (s pos) 48) (= (s (+ pos 1)) 120))
- start (if hex? (+ pos 2) pos)
- end (if hex?
- (read-hex-digits s start start)
- (read-digits s start start))]
- (if (= end start) (error (string "Expected number at " pos)))
-
- # check for fractional part
- (if (and (not hex?)
- (< end (length s))
- (= (s end) 46))
- (let [frac-start (+ end 1)
- frac-end (read-fractional s frac-start frac-start)]
- (if (= frac-end frac-start) (error "Expected digit after ."))
- (let [num-str (string/slice s start frac-end)
- val (scan-number num-str)]
- [(if neg (- val) val) frac-end]))
-
- # integer or hex
- (let [num-str (string/slice s start end)
- val (if hex?
- (string/format "0x%s" num-str)
- num-str)
- val (scan-number val)]
- [(if neg (- val) val) end]))))
+ hex? (and (< (+ pos 1) len)
+ (= (s pos) 48) (or (= (s (+ pos 1)) 120) (= (s (+ pos 1)) 88)))] # 0x / 0X
+ (if hex?
+ (let [hs (+ pos 2) he (read-hex-digits s hs hs)]
+ (if (= he hs) (error (string "Expected hex digits at " pos)))
+ (let [he2 (if (and (< he len) (= (s he) 78)) (+ he 1) he) # trailing N
+ val (scan-number (string "0x" (string/slice s hs he)))]
+ [(if neg (- val) val) he2]))
+ (let [iend (read-digits s pos pos)]
+ (if (= iend pos) (error (string "Expected number at " pos)))
+ (cond
+ # radix integer: r, e.g. 2r1010, 16rFF, 36rZ
+ (and (< iend len) (or (= (s iend) 114) (= (s iend) 82)))
+ (let [base (scan-number (string/slice s pos iend))
+ ds (+ iend 1)
+ de (read-alnum s ds ds)]
+ (if (= de ds) (error (string "Expected radix digits at " ds)))
+ (var acc 0)
+ (var i ds)
+ (while (< i de) (set acc (+ (* acc base) (radix-digit-val (s i)))) (++ i))
+ [(if neg (- acc) acc) de])
+ # ratio: / (only when a digit follows the slash)
+ (and (< (+ iend 1) len) (= (s iend) 47) (digit? (s (+ iend 1))))
+ (let [ds (+ iend 1) de (read-digits s ds ds)
+ numr (scan-number (string/slice s pos iend))
+ den (scan-number (string/slice s ds de))]
+ [(if neg (- (/ numr den)) (/ numr den)) de])
+ # fractional and/or exponent, optional trailing N/M
+ (let [frac-end (if (and (< iend len) (= (s iend) 46))
+ (let [fs (+ iend 1) fe (read-fractional s fs fs)]
+ (if (= fe fs) (error "Expected digit after .")) fe)
+ iend)
+ exp-end (read-exponent s frac-end)
+ val (scan-number (string/slice s start exp-end))
+ # consume a trailing N (bigint) or M (bigdec) suffix
+ fin (if (and (< exp-end len) (or (= (s exp-end) 78) (= (s exp-end) 77)))
+ (+ exp-end 1) exp-end)]
+ [(if neg (- val) val) fin]))))))
(defn read-list [s pos]
# pos is at opening paren
diff --git a/test/integration/clojure-test-suite-test.janet b/test/integration/clojure-test-suite-test.janet
index c23c5a0..f3f080c 100644
--- a/test/integration/clojure-test-suite-test.janet
+++ b/test/integration/clojure-test-suite-test.janet
@@ -18,9 +18,9 @@
# Baseline: assertions Jolt currently passes across the suite. Raise as Jolt
# improves so a regression (previously-passing assertion breaking) is caught.
-(def baseline-pass 1900)
+(def baseline-pass 3450)
# A file is "clean" when it ran with zero failures AND zero errors.
-(def baseline-clean-files 22)
+(def baseline-clean-files 38)
# Per-file wall-clock budget (seconds). Normal files finish in well under 1s;
# this only fires on infinite-sequence hangs.
(def per-file-timeout 6)
@@ -55,11 +55,15 @@
(if (and ok data) (string data) nil))
(defn- parse-counts [s]
- # s is "pass fail error"
- (def parts (string/split " " (string/trim s)))
- (if (= 3 (length parts))
- [(scan-number (parts 0)) (scan-number (parts 1)) (scan-number (parts 2))]
- nil))
+ # Find the "@@COUNTS p f e" sentinel line (a test body may have printed other
+ # lines to stdout, e.g. with-out-str tests).
+ (var result nil)
+ (each line (string/split "\n" s)
+ (when (string/has-prefix? "@@COUNTS " line)
+ (let [parts (string/split " " (string/trim line))]
+ (when (= 4 (length parts))
+ (set result [(scan-number (parts 1)) (scan-number (parts 2)) (scan-number (parts 3))])))))
+ result)
(if (not (os/stat suite-dir))
(print "clojure-test-suite: ~/src/clojure-test-suite not present — skipped")
diff --git a/test/integration/suite-worker.janet b/test/integration/suite-worker.janet
index d22c942..bfa0b5b 100644
--- a/test/integration/suite-worker.janet
+++ b/test/integration/suite-worker.janet
@@ -37,4 +37,10 @@
(def p (eval-string ctx "(clojure.test/n-pass)"))
(def f (eval-string ctx "(clojure.test/n-fail)"))
(def e (eval-string ctx "(clojure.test/n-error)"))
- (printf "%d %d %d" (if (number? p) p 0) (if (number? f) f 0) (if (number? e) e 0)))
+ # A "dump" 2nd arg (or SUITE_DUMP env) also prints each failure/error message
+ # (one DUMP line each) for triage.
+ (when (or (os/getenv "SUITE_DUMP") (= "dump" (get (dyn :args) 2)))
+ (eval-string ctx "(doseq [m (clojure.test/failures)] (println (str \"DUMP \" m)))"))
+ # Counts on a sentinel line so parsers find it even if a test body printed to
+ # stdout (e.g. with-out-str / println-str tests).
+ (printf "@@COUNTS %d %d %d" (if (number? p) p 0) (if (number? f) f 0) (if (number? e) e 0)))
diff --git a/test/spec/numbers-spec.janet b/test/spec/numbers-spec.janet
index 05d7bfa..bb9cdbb 100644
--- a/test/spec/numbers-spec.janet
+++ b/test/spec/numbers-spec.janet
@@ -72,6 +72,24 @@
["int? ##Inf false" "false" "(int? ##Inf)"]
["pos-int? ##Inf" "false" "(pos-int? ##Inf)"])
+# Numeric literal syntaxes. Jolt has no true bignum/ratio/bigdec types, so the
+# N (bigint) and M (bigdec) suffixes read as the plain number, ratios as the
+# double quotient; radix integers (NrDDD) are parsed by base.
+(defspec "numbers / literal syntax"
+ ["bigint suffix N" "42" "42N"]
+ ["bigint zero" "0" "0N"]
+ ["bigdec suffix M" "1.5" "1.5M"]
+ ["bigdec int M" "0" "0.0M"]
+ ["ratio -> double" "0.5" "1/2"]
+ ["ratio 3/4" "0.75" "3/4"]
+ ["neg ratio" "-0.5" "-1/2"]
+ ["radix binary" "10" "2r1010"]
+ ["radix hex-ish" "255" "16rFF"]
+ ["radix base36" "35" "36rZ"]
+ ["hex" "255" "0xFF"]
+ ["exponent" "1000.0" "1e3"]
+ ["exponent neg" "0.015" "1.5e-2"])
+
(defspec "numbers / bit-ops & math"
["bit-and" "4" "(bit-and 12 6)"]
["bit-or" "14" "(bit-or 12 6)"]
diff --git a/test/support/clojure_test.clj b/test/support/clojure_test.clj
index c634321..dfab28d 100644
--- a/test/support/clojure_test.clj
+++ b/test/support/clojure_test.clj
@@ -79,7 +79,7 @@
;; Run every deftest registered since the last reset, isolating crashes.
(defn run-registered []
(doseq [t @registry]
- (try (t) (catch :default e (clojure.test/err! "deftest crashed"))))
+ (try (t) (catch :default e (clojure.test/err! (str "deftest crashed: " (clojure.core/ex-message e))))))
nil)
;; clojure.test entry points the suite may call — no-ops; the Janet runner
@@ -96,10 +96,12 @@
;; Gate a test on whether its target var exists in this dialect. Jolt only
;; implements a subset of clojure.core, so unimplemented fns get skipped
;; cleanly rather than erroring.
+;; Skips silently (no stdout) so the worker's stdout stays just the count line;
+;; an unimplemented var simply contributes no assertions.
(defmacro when-var-exists [var-sym & body]
(if (resolve var-sym)
`(do ~@body)
- `(println "SKIP -" '~var-sym)))
+ nil))
;; `(thrown? body)` — true iff evaluating body throws. The suite always uses
;; the single-arg (no exception-class) form via this portability helper.