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.