diff --git a/host/chez/converters.ss b/host/chez/converters.ss index f0726d9..d13ed85 100644 --- a/host/chez/converters.ss +++ b/host/chez/converters.ss @@ -133,7 +133,12 @@ (else (loop (- i 1)))))))) ((keyword? a) (jolt-symbol (keyword-t-ns a) (keyword-t-name a))) (else (error #f "symbol: requires string/symbol" a))))) - ((= (length args) 2) (jolt-symbol (car args) (cadr args))) + ;; (symbol ns name): a nil namespace is the no-ns sentinel #f (NOT jolt-nil), + ;; so (symbol nil "x") equals (symbol "x") and the reader literal 'x — jolt= + ;; compares ns with strict equal?, so a jolt-nil ns would differ from #f. + ((= (length args) 2) + (let ((ns (car args))) + (jolt-symbol (if (jolt-nil? ns) #f ns) (cadr args)))) (else (error #f "symbol: wrong arity")))) ;; gensym: per-process counter. diff --git a/host/chez/java/host-static-classes.ss b/host/chez/java/host-static-classes.ss index 2824447..1e3e983 100644 --- a/host/chez/java/host-static-classes.ss +++ b/host/chez/java/host-static-classes.ss @@ -50,7 +50,7 @@ (cond ((null? args) (make-arraylist '())) ((number? (car args)) (make-arraylist '())) (else (make-arraylist (seq->list (jolt-seq (car args)))))))) -(register-host-methods! "arraylist" +(define arraylist-methods (list (cons "add" (lambda (self . a) ;; (.add x) -> append+true; (.add i x) -> insert at i, returns nil. @@ -58,6 +58,14 @@ (begin (al-push! self (car a)) #t) (begin (al-insert-at! self (jnum->exact (car a)) (cadr a)) jolt-nil)))) (cons "add!" (lambda (self x) (al-push! self x) #t)) + (cons "addAll" (lambda (self . a) + ;; (.addAll coll) appends; (.addAll i coll) inserts at i. + (let* ((at-i (= 2 (length a))) + (i (if at-i (jnum->exact (car a)) (al-cnt self))) + (coll (if at-i (cadr a) (car a)))) + (let loop ((xs (seq->list (jolt-seq coll))) (k i)) + (if (null? xs) (pair? (seq->list (jolt-seq coll))) + (begin (al-insert-at! self k (car xs)) (loop (cdr xs) (fx+ k 1)))))))) (cons "get" (lambda (self i) (vector-ref (al-vec self) (jnum->exact i)))) (cons "set" (lambda (self i x) (let* ((idx (jnum->exact i)) (old (vector-ref (al-vec self) idx))) @@ -72,6 +80,43 @@ (cons "toArray" (lambda (self . _) (apply jolt-vector (al->list self)))) (cons "iterator" (lambda (self) (make-jiterator (list->cseq (al->list self))))) (cons "toString" (lambda (self) (jolt-pr-str (list->cseq (al->list self))))))) +(register-host-methods! "arraylist" arraylist-methods) + +;; java.util.LinkedList: the ArrayList backing plus the Deque surface +;; (addFirst/addLast/removeFirst/removeLast/getFirst/getLast/peek/push/pop). +;; tools.reader holds pending splice forms in one and (seq)s / .remove(0)s it. +(define (al-first self) (vector-ref (al-vec self) 0)) +(define (al-last self) (vector-ref (al-vec self) (fx- (al-cnt self) 1))) +(define linkedlist-methods + (append arraylist-methods + (list + (cons "addFirst" (lambda (self x) (al-insert-at! self 0 x) jolt-nil)) + (cons "addLast" (lambda (self x) (al-push! self x) jolt-nil)) + (cons "offer" (lambda (self x) (al-push! self x) #t)) + (cons "removeFirst" (lambda (self) (let ((o (al-first self))) (al-remove-at! self 0) o))) + (cons "removeLast" (lambda (self) (let ((o (al-last self))) (al-remove-at! self (fx- (al-cnt self) 1)) o))) + (cons "getFirst" al-first) (cons "getLast" al-last) + (cons "peek" (lambda (self) (if (fx=? 0 (al-cnt self)) jolt-nil (al-first self)))) + (cons "poll" (lambda (self) (if (fx=? 0 (al-cnt self)) jolt-nil (let ((o (al-first self))) (al-remove-at! self 0) o)))) + (cons "push" (lambda (self x) (al-insert-at! self 0 x) jolt-nil)) + (cons "pop" (lambda (self) (let ((o (al-first self))) (al-remove-at! self 0) o)))))) +(define (make-linkedlist xs) + (let ((al (make-arraylist xs))) (make-jhost "linkedlist" (jhost-state al)))) +(register-host-methods! "linkedlist" linkedlist-methods) +(let ((ctor (lambda args + (cond ((null? args) (make-linkedlist '())) + (else (make-linkedlist (seq->list (jolt-seq (car args))))))))) + (register-class-ctor! "LinkedList" ctor) + (register-class-ctor! "java.util.LinkedList" ctor)) + +;; ArrayList / LinkedList are Iterable: (seq al) walks the elements (nil if empty), +;; so (seq pending-forms) and reduce/into over one work like the JVM. +(define %al-seq jolt-seq) +(set! jolt-seq + (lambda (x) + (if (and (jhost? x) (or (string=? (jhost-tag x) "arraylist") (string=? (jhost-tag x) "linkedlist"))) + (list->cseq (al->list x)) + (%al-seq x)))) ;; Appendable.append text: append(x) renders x; append(csq,start,end) appends the ;; subsequence csq[start,end) (data.json's writer appends string runs this way). @@ -433,8 +478,12 @@ (list->string (vector->list v))))) ((string? x) x) (else (jolt-str-render-one x))))) +;; (BigInteger. s) | (BigInteger. s radix) — parse a string in the given radix +;; (default 10). tools.reader's integer parser builds (BigInteger. digits radix). (register-class-ctor! "BigInteger" - (lambda (v) (parse-int-or-throw v 10 "BigInteger"))) + (lambda (v . r) (parse-int-or-throw v (if (null? r) 10 (jnum->exact (car r))) "BigInteger"))) +(register-class-ctor! "java.math.BigInteger" + (lambda (v . r) (parse-int-or-throw v (if (null? r) 10 (jnum->exact (car r))) "BigInteger"))) (register-class-ctor! "MapEntry" (lambda (k v) (make-map-entry k v))) ;; JVM exception ctors -> a typed host throwable carrying the canonical :jolt/class ;; (so class / instance? / getMessage / ex-message reflect the real type) and the @@ -568,17 +617,29 @@ (define %hs-rmd2 record-method-dispatch) (set! record-method-dispatch (lambda (obj method-name rest-args) - (if (regex-t? obj) - (let ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args)))) - (cond ((string=? method-name "split") - ;; .split returns a String[] — a seq (prints - ;; (a b c), not a vector). re-split with no limit; drop trailing - ;; empties (JVM default). - (let ((parts (re-split (regex-t-irx obj) (car rest) #f))) - (list->cseq (str-split-drop-trailing parts)))) - ((string=? method-name "pattern") (regex-t-source obj)) - (else (error #f (string-append "No method " method-name " on Pattern"))))) - (%hs-rmd2 obj method-name rest-args)))) + (let ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args)))) + (cond + ((regex-t? obj) + (cond ((string=? method-name "split") + ;; .split returns a String[] — a seq (prints + ;; (a b c), not a vector). re-split with no limit; drop trailing + ;; empties (JVM default). + (let ((parts (re-split (regex-t-irx obj) (car rest) #f))) + (list->cseq (str-split-drop-trailing parts)))) + ((string=? method-name "pattern") (regex-t-source obj)) + ((or (string=? method-name "toString")) (regex-t-source obj)) + ;; (.matcher pattern s) -> a Matcher (matcher-t) for stepping matches. + ((string=? method-name "matcher") (jolt-re-matcher obj (car rest))) + (else (error #f (string-append "No method " method-name " on Pattern"))))) + ;; java.util.regex.Matcher: .matches (anchored whole-region), .find + ;; (next match), .group [n], .groupCount. + ((jolt-matcher? obj) + (cond ((string=? method-name "matches") (jolt-matcher-matches obj)) + ((string=? method-name "find") (not (jolt-nil? (jolt-re-find obj)))) + ((string=? method-name "group") (apply jolt-matcher-group obj rest)) + ((string=? method-name "groupCount") (jolt-matcher-group-count obj)) + (else (error #f (string-append "No method " method-name " on Matcher"))))) + (else (%hs-rmd2 obj method-name rest-args)))))) ;; ---- def-var! the registry entry points so emit can also reach them --------- (def-var! "clojure.core" "host-static-ref" host-static-ref) diff --git a/host/chez/java/host-static-methods.ss b/host/chez/java/host-static-methods.ss index 6850c2b..6b5c894 100644 --- a/host/chez/java/host-static-methods.ss +++ b/host/chez/java/host-static-methods.ss @@ -96,6 +96,70 @@ (register-class-statics! "PersistentArrayMap" (list (cons "createWithCheck" pam-create-with-check))) (register-class-statics! "clojure.lang.PersistentArrayMap" (list (cons "createWithCheck" pam-create-with-check))) +;; clojure.lang.RT/map: build a map from a [k v k v…] array/seq (RT.map). Small +;; maps keep insertion order (PersistentArrayMap). tools.reader builds map and +;; namespaced-map literals this way. +(define (rt-map arr) + (let loop ((xs (if (jolt-nil? arr) '() (seq->list (jolt-seq arr)))) (m (jolt-hash-map))) + (cond ((null? xs) m) + ((null? (cdr xs)) (error #f "RT/map: odd key/value count")) + (else (loop (cddr xs) (jolt-assoc m (car xs) (cadr xs))))))) +(register-class-statics! "RT" (list (cons "map" rt-map))) +(register-class-statics! "clojure.lang.RT" (list (cons "map" rt-map))) + +;; clojure.lang.PersistentList/create: a list (in order) from a seq; empty -> (). +(define (plist-create x) + (let ((items (seq->list (jolt-seq x)))) + (if (null? items) jolt-empty-list (list->cseq items)))) +(register-class-statics! "PersistentList" (list (cons "create" plist-create))) +(register-class-statics! "clojure.lang.PersistentList" (list (cons "create" plist-create))) + +;; clojure.lang.PersistentHashSet/createWithCheck: a set from a seq, throwing on a +;; duplicate element (tools.reader's #{…} reader reports the dup). +(define (phs-create-with-check x) + (let loop ((xs (seq->list (jolt-seq x))) (s (jolt-hash-set))) + (if (null? xs) s + (let ((e (car xs))) + (if (jolt-truthy? (jolt-contains? s e)) + (jolt-throw (jolt-ex-info (string-append "Duplicate key: " (jolt-str-render-one e)) (jolt-hash-map))) + (loop (cdr xs) (jolt-conj1 s e))))))) +(register-class-statics! "PersistentHashSet" (list (cons "createWithCheck" phs-create-with-check))) +(register-class-statics! "clojure.lang.PersistentHashSet" (list (cons "createWithCheck" phs-create-with-check))) + +;; java.lang.Character statics. digit(ch, radix) -> the digit value or -1; ch may +;; be a char or an int codepoint (tools.reader passes (int c)). isDigit/ +;; isWhitespace take a char; valueOf boxes a char (identity on jolt). +(define (char->cp x) (if (char? x) (char->integer x) (jnum->exact x))) +(define (char-digit-value cp radix) + (let ((d (cond ((and (fx>=? cp 48) (fx<=? cp 57)) (fx- cp 48)) ; 0-9 + ((and (fx>=? cp 97) (fx<=? cp 122)) (fx+ 10 (fx- cp 97))) ; a-z + ((and (fx>=? cp 65) (fx<=? cp 90)) (fx+ 10 (fx- cp 65))) ; A-Z + (else 99)))) + (if (fxnum (char-digit-value (char->cp ch) (jnum->exact radix))))) + (cons "isDigit" (lambda (ch) (let ((cp (char->cp ch))) (and (fx>=? cp 48) (fx<=? cp 57))))) + (cons "isWhitespace" (lambda (ch) (char-whitespace? (integer->char (char->cp ch))))) + (cons "valueOf" (lambda (ch) (if (char? ch) ch (integer->char (char->cp ch))))))) +(register-class-statics! "Character" character-statics) +(register-class-statics! "java.lang.Character" character-statics) + +;; java.util.regex.Pattern/compile: a regex value from a string pattern. +(define pattern-statics (list (cons "compile" (lambda (s) (jolt-regex (jolt-str-render-one s)))))) +(register-class-statics! "Pattern" pattern-statics) +(register-class-statics! "java.util.regex.Pattern" pattern-statics) + +;; clojure.lang.BigInt / clojure.lang.Numbers: jolt has one exact-integer type +;; (Chez bignums auto-reduce), so BigInt.fromBigInteger and Numbers.reduceBigInt +;; are identity. tools.reader's number parser threads integers through these. +(define identity-num-statics (list (cons "fromBigInteger" (lambda (x) x)))) +(register-class-statics! "BigInt" identity-num-statics) +(register-class-statics! "clojure.lang.BigInt" identity-num-statics) +(register-class-statics! "Numbers" + (list (cons "reduceBigInt" (lambda (x) x)) (cons "toRatio" (lambda (x) x)))) +(register-class-statics! "clojure.lang.Numbers" + (list (cons "reduceBigInt" (lambda (x) x)) (cons "toRatio" (lambda (x) x)))) + (define (now-millis) (let ((t (current-time 'time-utc))) (+ (* 1000 (time-second t)) (quotient (time-nanosecond t) 1000000)))) diff --git a/host/chez/java/host-static.ss b/host/chez/java/host-static.ss index cd085f3..2c14f0c 100644 --- a/host/chez/java/host-static.ss +++ b/host/chez/java/host-static.ss @@ -88,6 +88,13 @@ ;; Double/Float .isNaN / .isInfinite (a non-flonum is neither). ((string=? method "isNaN") (and (flonum? n) (not (= n n)))) ((string=? method "isInfinite") (and (flonum? n) (infinite? n))) + ;; BigInteger interop: .negate / .bitLength / .signum / .abs. A jolt integer is + ;; a Chez exact integer, so these are native (integer-length = JVM bitLength, + ;; matching for negative values too). tools.reader's number parser uses them. + ((string=? method "negate") (->num (- (jnum->exact n)))) + ((string=? method "abs") (->num (abs (jnum->exact n)))) + ((string=? method "bitLength") (->num (integer-length (jnum->exact n)))) + ((string=? method "signum") (->num (let ((e (jnum->exact n))) (cond ((> e 0) 1) ((< e 0) -1) (else 0))))) (else (error #f (string-append "No method " method " for number"))))) ;; Mutable static fields: "Class" -> (member -> 1-vector cell). A library that diff --git a/host/chez/loader.ss b/host/chez/loader.ss index 87d9db7..fb8691a 100644 --- a/host/chez/loader.ss +++ b/host/chez/loader.ss @@ -264,13 +264,28 @@ (def-var! "clojure.core" "use" loader-use) (def-var! "clojure.core" "load-file" jolt-load-file) -;; load: each arg is a "/"-rooted resource path like "/app/extra"; load the file -;; for it relative to the search roots (strip the leading slash, try .clj/.cljc). + +;; The directory of a namespace's resource path: "clojure.tools.reader-test" -> +;; "clojure/tools" (drop the last segment of ns-name->rel). "" for a top-level ns. +(define (ns-rel-dir name) + (let* ((r (ns-name->rel name))) + (let loop ((k (fx- (string-length r) 1))) + (cond ((fx (string-length p) 0) (char=? (string-ref p 0) #\/)) - (substring p 1 (string-length p)) p)) + (let* ((rel (cond + ((and (> (string-length p) 0) (char=? (string-ref p 0) #\/)) + (substring p 1 (string-length p))) + (else (let ((dir (ns-rel-dir (chez-current-ns)))) + (if (string=? dir "") p (string-append dir "/" p)))))) (f (resolve-on-roots rel))) (if f (load-jolt-file f) (error #f "Could not locate resource on source roots" p)))) diff --git a/host/chez/reader.ss b/host/chez/reader.ss index db4e4db..5cc4ef1 100644 --- a/host/chez/reader.ss +++ b/host/chez/reader.ss @@ -47,6 +47,11 @@ (memv c '(#\( #\) #\[ #\] #\{ #\} #\" #\; #\@ #\^ #\` #\~ #\\)))) (define (rdr-digit? c) (and (char>=? c #\0) (char<=? c #\9))) +(define (rdr-octal? c) (and (char>=? c #\0) (char<=? c #\7))) +;; every char of s in [from,to) is an octal digit (and the span is non-empty). +(define (rdr-all-octal? s from to) + (and (fx= radix 2) (<= radix 36) (let ((v (rdr-parse-radix (substring body (+ ri 1) blen) radix))) (and v (* sign v))))))) + ;; octal 0NNN: a leading 0 followed by octal digits (Clojure reads 042 as 34, + ;; not decimal 42). "0" alone, 0x.., 0r.. and a float "0.5" are handled + ;; elsewhere or fall through (a non-octal digit fails rdr-all-octal?). + ((and (>= blen 2) (char=? (string-ref body 0) #\0) (rdr-all-octal? body 1 blen)) + (let ((o (rdr-parse-radix (substring body 1 blen) 8))) (and o (* sign o)))) ;; bigint suffix N ((and (> blen 1) (char=? (string-ref body (- blen 1)) #\N)) (let ((n (string->number (substring body 0 (- blen 1))))) @@ -174,7 +184,13 @@ ((#\") (loop (+ i 2) (cons #\" acc))) ((#\b) (loop (+ i 2) (cons #\backspace acc))) ((#\f) (loop (+ i 2) (cons #\page acc))) - ((#\0) (loop (+ i 2) (cons #\nul acc))) + ;; octal escape \ooo: 1-3 octal digits (Clojure's \0..\377), so \000 + ;; is one null char, not \0 + literal "00". + ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7) + (let oct ((j (+ i 1)) (val 0) (cnt 0)) + (if (and (fxinteger (string-ref s j)) 48)) (fx+ cnt 1)) + (loop j (cons (integer->char val) acc))))) ((#\u) (let-values (((cp j) (rdr-hex->int s (+ i 2) 4))) ;; A \u escape is a UTF-16 code unit. jolt chars are Unicode scalars, diff --git a/host/chez/regex.ss b/host/chez/regex.ss index 8273d4d..88ad864 100644 --- a/host/chez/regex.ss +++ b/host/chez/regex.ss @@ -221,6 +221,21 @@ (if last (irx-result last) (jolt-throw (jolt-ex-info "No match found" (jolt-hash-map)))))) +;; java.util.regex.Matcher methods over a matcher-t. .matches anchors a full-region +;; match and remembers it for .group; .group n returns submatch n (0 = whole) or +;; nil; .groupCount is the pattern's capturing-group count. +(define (jolt-matcher-matches m) + (let ((mm (irregex-match (matcher-t-irx m) (matcher-t-str m)))) + (matcher-t-last-set! m mm) + (if mm #t #f))) +(define (jolt-matcher-group m . n) + (let ((last (matcher-t-last m))) + (if last + (let ((s (irregex-match-substring last (if (pair? n) (->idx (car n)) 0)))) + (if s s jolt-nil)) + (jolt-throw (jolt-ex-info "No match available" (jolt-hash-map)))))) +(define (jolt-matcher-group-count m) (irregex-num-submatches (matcher-t-irx m))) + ;; All non-overlapping matches, left to right. Advance past each match end (or by ;; one on a zero-width match). nil when there are no matches (Clojure: seq-able as ;; nil, so (if-let [m (re-seq ...)] ...) works). diff --git a/stdlib/clojure/test.clj b/stdlib/clojure/test.clj index 8a38c99..923a96a 100644 --- a/stdlib/clojure/test.clj +++ b/stdlib/clojure/test.clj @@ -107,7 +107,8 @@ ;; (is (thrown-with-msg? Class re body...)) (thrown-form? form "thrown-with-msg?") - (let [klass (name (second form)) + (let [klass-sym (second form) + klass (name klass-sym) re (nth form 2) body (nthrest form 3)] `(try @@ -115,7 +116,11 @@ (clojure.test/fail! (str "expected " '~form " to throw")) (catch Throwable e# (let [m# (or (clojure.core/ex-message e#) (str e#))] - (if (and (clojure.test/class-match? e# ~klass) (re-find ~re m#)) + ;; honor the class hierarchy (ExceptionInfo IS a RuntimeException), + ;; then fall back to a simple-name match like thrown? does. + (if (and (or (clojure.core/instance? ~klass-sym e#) + (clojure.test/class-match? e# ~klass)) + (re-find ~re m#)) (clojure.test/inc-pass!) (clojure.test/fail! (str "expected throw of " ~klass " matching " ~re " but got " (clojure.core/class e#) ": " m#))))))) diff --git a/test/chez/corpus.edn b/test/chez/corpus.edn index e6fa363..43d002f 100644 --- a/test/chez/corpus.edn +++ b/test/chez/corpus.edn @@ -3357,4 +3357,12 @@ {:suite "host-interop / instance? Object" :label "nil is not an instance of Object" :expected "false" :actual "(instance? Object nil)"} {:suite "host-interop / instance? Object" :label "java.lang.Object too" :expected "true" :actual "(instance? java.lang.Object :k)"} {:suite "host-interop / Compiler" :label "@Compiler/LINE is a number" :expected "true" :actual "(number? @clojure.lang.Compiler/LINE)"} + {:suite "reader / octal literals" :label "0NNN is octal" :expected "34" :actual "042"} + {:suite "reader / octal literals" :label "negative octal" :expected "-34" :actual "-042"} + {:suite "reader / octal literals" :label "octal char escape \\oNNN" :expected "true" :actual "(= (char 0377) \\o377)"} + {:suite "reader / octal literals" :label "octal string escape is one char" :expected "true" :actual "(= \"a\\000b\" (str \"a\" (char 0) \"b\"))"} + {:suite "reader / octal literals" :label "hex still decimal-distinct" :expected "[1070 255 0]" :actual "[0x42e 0xff 0]"} + {:suite "symbols / construction" :label "(symbol nil name) equals (symbol name)" :expected "true" :actual "(= (symbol nil \"foo\") (quote foo))"} + {:suite "symbols / construction" :label "(symbol nil name) has no namespace" :expected "true" :actual "(nil? (namespace (symbol nil \"x\")))"} + {:suite "host-interop / instance? hierarchy" :label "ExceptionInfo is a RuntimeException" :expected "[true true true]" :actual "[(instance? RuntimeException (ex-info \"x\" {})) (instance? Exception (ex-info \"x\" {})) (instance? Throwable (ex-info \"x\" {}))]"} ]