Fix general gaps the hiccup suite shook out

Six correctness fixes, each a general gap (not hiccup-specific):

- deftype is not a map. jolt treated every deftype instance as a map
  (map?/record?/seqable over its fields); in Clojure only a defrecord is
  map-like, a bare deftype is an opaque object. defrecord now marks its type;
  map?/record?/coll?/seq/empty? gate on it, while a deftype implementing a
  collection interface still dispatches through its methods.

- cross-ns extend-protocol on an imported deftype. register-method built the
  type tag from the *calling* ns + bare name, so (extend-protocol P Raw …) in
  one ns missed a Raw value defined in another. A simple-name index resolves
  the bare name to the type's real tag (local ns still wins).

- str vs print. str of a collection is its readable form (nested strings
  quoted: (str ["x"]) => ["x"]); print leaves them raw. jolt defined print
  as str, conflating the two. Split via a __print1 seam.

- clojure.test thrown? now honors the exception hierarchy (instance?), so
  (thrown? IllegalArgumentException …) matches an ArityException subclass.

- java.net.URI is value-equal (= and hash by string form).

- clojure.walk/macroexpand-all was missing; an unresolved qualified var made
  the analyzer report "Unknown class walk".

deftype/defrecord + print are seed sources, re-minted. hiccup 365->381 of its
own suite; the rest are charset-encoding / var-meta niches.
This commit is contained in:
Yogthos 2026-06-26 20:15:39 -04:00
parent 448611a5df
commit 3d80bdc10b
12 changed files with 823 additions and 724 deletions

View file

@ -33,11 +33,26 @@
((null? rs) (jolt-pr-str v)) ((null? rs) (jolt-pr-str v))
(((caar rs) v) ((cdar rs) v)) (((caar rs) v) ((cdar rs) v))
(else (loop (cdr rs)))))))) (else (loop (cdr rs))))))))
;; print/println render non-readably: a nested string is raw. jolt-str-render-one
;; is exactly that (collections fall through to jolt-pr-str). The print family
;; uses this seam, NOT the str fn — which renders readably (below). A top-level nil
;; prints "nil" (str renders it ""), so the seam special-cases it.
(define (jolt-print-one v) (if (jolt-nil? v) "nil" (jolt-str-render-one v)))
(def-var! "clojure.core" "__print1" jolt-print-one)
;; str: a top-level string/scalar renders as jolt-str-render-one (raw string,
;; "Infinity"…), but a COLLECTION renders as its readable form — nested strings
;; are QUOTED ((str ["x"]) => "[\"x\"]"), matching the JVM (a collection's
;; toString is readable). jolt-pr-readable resolves at call time.
(define (jolt-str-one v)
(if (or (pvec? v) (pmap? v) (pset? v) (cseq? v) (empty-list-t? v) (jolt-lazyseq? v))
(jolt-pr-readable v)
(jolt-str-render-one v)))
(define (jolt-str . xs) (define (jolt-str . xs)
(let loop ((xs xs) (acc '())) (let loop ((xs xs) (acc '()))
(if (null? xs) (if (null? xs)
(apply string-append (reverse acc)) (apply string-append (reverse acc))
(loop (cdr xs) (cons (jolt-str-render-one (car xs)) acc))))) (loop (cdr xs) (cons (jolt-str-one (car xs)) acc)))))
;; jolt indices are flonums; substring etc. need exact ints. ;; jolt indices are flonums; substring etc. need exact ints.
(define (jolt->idx n) (exact (truncate n))) (define (jolt->idx n) (exact (truncate n)))

View file

@ -113,7 +113,7 @@
(define %h-set? jolt-set?) (define %h-set? jolt-set?)
(set! jolt-set? (lambda (x) (or (htable-sorted-set? x) (%h-set? x)))) (set! jolt-set? (lambda (x) (or (htable-sorted-set? x) (%h-set? x))))
(def-var! "clojure.core" "set?" jolt-set?) (def-var! "clojure.core" "set?" jolt-set?)
(def-var! "clojure.core" "coll?" (lambda (x) (or (htable-sorted? x) (jrec? x) (jolt-coll-pred? x)))) (def-var! "clojure.core" "coll?" (lambda (x) (or (htable-sorted? x) (jrec-collection? x) (jolt-coll-pred? x))))
;; --- equality / hash --------------------------------------------------------- ;; --- equality / hash ---------------------------------------------------------
;; A sorted coll canonicalizes like its unordered counterpart: ;; A sorted coll canonicalizes like its unordered counterpart:

View file

@ -605,6 +605,14 @@
(cons "hashCode" (lambda (u) (string-hash (uri-field u 'string)))) (cons "hashCode" (lambda (u) (string-hash (uri-field u 'string))))
(cons "equals" (lambda (u o) (and (jhost? o) (string=? (jhost-tag o) "uri") (cons "equals" (lambda (u o) (and (jhost? o) (string=? (jhost-tag o) "uri")
(string=? (uri-field u 'string) (uri-field o 'string))))))) (string=? (uri-field u 'string) (uri-field o 'string)))))))
;; (= u1 u2) is value equality by string form (the .equals method above only
;; serves explicit (.equals …)); hash matches so a URI works as a map key / set
;; member (ring/hiccup compare (URI. "/") values).
(define (uri-jhost? x) (and (jhost? x) (string=? (jhost-tag x) "uri")))
(register-eq-arm! (lambda (a b) (or (uri-jhost? a) (uri-jhost? b)))
(lambda (a b) (and (uri-jhost? a) (uri-jhost? b)
(string=? (uri-field a 'string) (uri-field b 'string)))))
(register-hash-arm! uri-jhost? (lambda (x) (string-hash (uri-field x 'string))))
;; str / pr-str of a uri -> its string form. ;; str / pr-str of a uri -> its string form.
(register-str-render! (lambda (x) (and (jhost? x) (string=? (jhost-tag x) "uri"))) (register-str-render! (lambda (x) (and (jhost? x) (string=? (jhost-tag x) "uri")))
(lambda (x) (uri-field x 'string))) (lambda (x) (uri-field x 'string)))

View file

@ -95,10 +95,10 @@
;; chunked-seq? is true for a vector's seq (a real chunked-seq); the overlay's ;; chunked-seq? is true for a vector's seq (a real chunked-seq); the overlay's
;; always-false stub loaded over the host fn, so re-assert it. ;; always-false stub loaded over the host fn, so re-assert it.
(def-var! "clojure.core" "chunked-seq?" na-chunked-seq?) (def-var! "clojure.core" "chunked-seq?" na-chunked-seq?)
;; record? is a host type check (jrec?), not the overlay's (some? (get x ;; record? is a host type check — true only for a defrecord, not a bare deftype
;; :jolt/deftype)) — the get-trick invokes a sorted-map's comparator on ;; (jrec-record?), matching the JVM (instance? IRecord). The overlay's
;; :jolt/deftype and throws. Matches the JVM (instance? IRecord). ;; (some? (get x :jolt/deftype)) get-trick would invoke a sorted-map comparator.
(def-var! "clojure.core" "record?" (lambda (x) (jrec? x))) (def-var! "clojure.core" "record?" (lambda (x) (jrec-record? x)))
;; read / read+string over a HOST reader jhost (java.io StringReader/PushbackReader): ;; read / read+string over a HOST reader jhost (java.io StringReader/PushbackReader):
;; the overlay's IReader protocol only covers the reify map-reader, so a (read ;; the overlay's IReader protocol only covers the reify map-reader, so a (read

View file

@ -31,6 +31,33 @@
;; map (jolt-nil unless non-field keys have been assoc'd on). ;; map (jolt-nil unless non-field keys have been assoc'd on).
(define-record-type (jrec make-jrec jrec?) (fields desc vals ext) (nongenerative chez-jrec-v2)) (define-record-type (jrec make-jrec jrec?) (fields desc vals ext) (nongenerative chez-jrec-v2))
(define (jrec-tag r) (jrdesc-tag (jrec-desc r))) (define (jrec-tag r) (jrdesc-tag (jrec-desc r)))
;; defrecord vs deftype: a defrecord IS a map (map?/seq/keys/assoc over its
;; fields); a bare deftype is an opaque object with only its declared interfaces,
;; never a map (Clojure semantics). defrecord registers its type tag here; the
;; default jrec-as-map behaviour (map?/record?/field-seq) is gated on it, while
;; method dispatch (a deftype implementing ISeq/Counted/…) stays open to any jrec.
(define chez-record-type-tbl (make-hashtable string-hash string=?))
(define (jrec-record? x) (and (jrec? x) (hashtable-ref chez-record-type-tbl (jrec-tag x) #f) #t))
;; every deftype/defrecord tag, and a simple-name -> tag index. An extend-protocol
;; in a DIFFERENT ns names the type bare (it is :import-ed), so register-method
;; resolves "Raw" to its real tag "a.util.Raw" here instead of prepending the
;; calling ns. The local ns is preferred, so a same-named local type still wins.
(define chez-deftype-tag-set (make-hashtable string-hash string=?))
(define chez-simple-name-tag (make-hashtable string-hash string=?))
;; a jrec that is coll? — a record, or a deftype implementing a collection
;; interface (its seq/count/nth/valAt/cons method is registered). find-method-any-
;; protocol is defined later; resolved at call time. An opaque deftype is not coll?.
(define (jrec-collection? x)
(and (jrec? x)
(or (jrec-record? x)
(let ((tag (jrec-tag x)))
(or (find-method-any-protocol tag "seq")
(find-method-any-protocol tag "count")
(find-method-any-protocol tag "nth")
(find-method-any-protocol tag "valAt")
(find-method-any-protocol tag "cons"))))
#t))
(define jolt-deftype-kw (keyword "jolt" "deftype")) (define jolt-deftype-kw (keyword "jolt" "deftype"))
;; unique present-vs-absent sentinel for extension-map lookups (so a present nil ;; unique present-vs-absent sentinel for extension-map lookups (so a present nil
;; in the extension map is distinguished from a genuine miss). ;; in the extension map is distinguished from a genuine miss).
@ -338,7 +365,9 @@
(define %r-jolt-seq jolt-seq) (define %r-jolt-seq jolt-seq)
(set! jolt-seq (lambda (x) (set! jolt-seq (lambda (x)
(cond ((jrec-cl x "seq") => (lambda (m) (jolt-seq (jolt-invoke m x)))) (cond ((jrec-cl x "seq") => (lambda (m) (jolt-seq (jolt-invoke m x))))
((jrec? x) (list->cseq (jrec-entry-list x))) ;; a record seqs its fields; a bare deftype is not seqable (falls through
;; to %r-jolt-seq, which errors like the JVM).
((jrec-record? x) (list->cseq (jrec-entry-list x)))
(else (%r-jolt-seq x))))) (else (%r-jolt-seq x)))))
(define %r-jolt-conj1 jolt-conj1) (define %r-jolt-conj1 jolt-conj1)
(set! jolt-conj1 (lambda (coll x) (set! jolt-conj1 (lambda (coll x)
@ -350,7 +379,8 @@
;; empty? over a jrec: a map-like deftype is empty iff its entry seq is (data ;; empty? over a jrec: a map-like deftype is empty iff its entry seq is (data
;; .priority-map's peek calls (.isEmpty this) -> empty?). jolt-seq is method-first. ;; .priority-map's peek calls (.isEmpty this) -> empty?). jolt-seq is method-first.
(define %r-jolt-empty? jolt-empty?) (define %r-jolt-empty? jolt-empty?)
(set! jolt-empty? (lambda (coll) (if (jrec? coll) (jolt-nil? (jolt-seq coll)) (%r-jolt-empty? coll)))) (set! jolt-empty? (lambda (coll)
(if (jrec-collection? coll) (jolt-nil? (jolt-seq coll)) (%r-jolt-empty? coll))))
(define %r-jolt-peek jolt-peek) (define %r-jolt-peek jolt-peek)
(set! jolt-peek (lambda (coll) (set! jolt-peek (lambda (coll)
(cond ((jrec-cl coll "peek") => (lambda (m) (jolt-invoke m coll))) (cond ((jrec-cl coll "peek") => (lambda (m) (jolt-invoke m coll)))
@ -365,10 +395,13 @@
;; predicates.ss vars hold a snapshot, so re-def-var! after extending. record? is ;; predicates.ss vars hold a snapshot, so re-def-var! after extending. record? is
;; the overlay's (some? (get x :jolt/deftype)) — works for free since the get ;; the overlay's (some? (get x :jolt/deftype)) — works for free since the get
;; override returns the tag for that key. ;; override returns the tag for that key.
;; only a defrecord is a map (Clojure: a record IS an associative map); a bare
;; deftype is not. coll? additionally covers a deftype implementing a collection
;; interface. predicates.ss vars hold a snapshot, so re-def-var! after extending.
(define %r-jolt-map? jolt-map?) (define %r-jolt-map? jolt-map?)
(set! jolt-map? (lambda (x) (or (jrec? x) (%r-jolt-map? x)))) (set! jolt-map? (lambda (x) (or (jrec-record? x) (%r-jolt-map? x))))
(def-var! "clojure.core" "map?" jolt-map?) (def-var! "clojure.core" "map?" jolt-map?)
(def-var! "clojure.core" "coll?" (lambda (x) (or (jrec? x) (jolt-coll-pred? x)))) (def-var! "clojure.core" "coll?" (lambda (x) (or (jrec-collection? x) (jolt-coll-pred? x))))
;; ---- protocol registry ------------------------------------------------------ ;; ---- protocol registry ------------------------------------------------------
;; type-tag -> (proto-name -> (method-name -> fn)) ;; type-tag -> (proto-name -> (method-name -> fn))
@ -483,6 +516,9 @@
;; even when the runtime current ns is the caller's, not the defining ns ;; even when the runtime current ns is the caller's, not the defining ns
;; (host-new checks class-ctors-tbl before the current-ns var fallback). ;; (host-new checks class-ctors-tbl before the current-ns var fallback).
(register-class-ctor! (symbol-t-name name-sym) ctor) (register-class-ctor! (symbol-t-name name-sym) ctor)
;; index the tag so a cross-ns extend-protocol resolves the bare type name.
(hashtable-set! chez-deftype-tag-set tag #t)
(hashtable-set! chez-simple-name-tag (symbol-t-name name-sym) tag)
;; record the shape for whole-program inference, keyed by the positional ;; record the shape for whole-program inference, keyed by the positional
;; ctor var "ns/->Name" the analyzer resolves a (->Name …) call to. ;; ctor var "ns/->Name" the analyzer resolves a (->Name …) call to.
(register-record-shape! (string-append (chez-current-ns) "/->" (symbol-t-name name-sym)) (register-record-shape! (string-append (chez-current-ns) "/->" (symbol-t-name name-sym))
@ -559,7 +595,14 @@
(when pi (hashtable-set! pi extend-mark #t)))))) (when pi (hashtable-set! pi extend-mark #t))))))
(define (register-method type-name proto-name method-name fn) (define (register-method type-name proto-name method-name fn)
(let* ((host (canonical-host-tag type-name)) (let* ((host (canonical-host-tag type-name))
(tag (or host (string-append (chez-current-ns) "." type-name)))) (local (string-append (chez-current-ns) "." type-name))
;; a host class -> its canonical tag; a deftype defined in THIS ns -> the
;; local tag; an :import-ed deftype from another ns -> its real tag via the
;; simple-name index; otherwise the local tag (a forward extend).
(tag (cond (host host)
((hashtable-ref chez-deftype-tag-set local #f) local)
((hashtable-ref chez-simple-name-tag type-name #f))
(else local))))
(register-protocol-method tag proto-name method-name fn) (register-protocol-method tag proto-name method-name fn)
(mark-extend! tag proto-name) (mark-extend! tag proto-name)
jolt-nil)) jolt-nil))
@ -830,6 +873,14 @@
;; yields (jolt-symbol #f (jrec-tag x)), the ns.Name class-name symbol. ;; yields (jolt-symbol #f (jrec-tag x)), the ns.Name class-name symbol.
(def-var! "clojure.core" "make-deftype-ctor" make-deftype-ctor) (def-var! "clojure.core" "make-deftype-ctor" make-deftype-ctor)
;; defrecord marks its type a record (deftype does not), keyed by the same
;; "ns.Name" tag make-deftype-ctor bakes — so jrec-record? distinguishes the two.
(define (register-record-type! name-sym)
(hashtable-set! chez-record-type-tbl
(string-append (chez-current-ns) "." (symbol-t-name name-sym)) #t)
jolt-nil)
(def-var! "clojure.core" "register-record-type!" register-record-type!)
(def-var! "clojure.core" "make-protocol" make-protocol) (def-var! "clojure.core" "make-protocol" make-protocol)
(def-var! "clojure.core" "register-protocol-methods!" register-protocol-methods!) (def-var! "clojure.core" "register-protocol-methods!" register-protocol-methods!)
(def-var! "clojure.core" "register-method" register-method) (def-var! "clojure.core" "register-method" register-method)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -59,11 +59,13 @@
;; nil, which prints as "nil" (str yields ""). Only the top-level arg needs the ;; nil, which prints as "nil" (str yields ""). Only the top-level arg needs the
;; guard; nil nested in a collection already renders as "nil" via the collection ;; guard; nil nested in a collection already renders as "nil" via the collection
;; printer. ;; printer.
;; print renders non-readably (__print1): a nested string is raw, unlike str/pr
;; which quote it. (print ["x"]) => [x], (str ["x"]) => ["x"].
(defn print [& xs] (defn print [& xs]
(__write (loop [out "" s (seq xs) first? true] (__write (loop [out "" s (seq xs) first? true]
(if s (if s
(let [x (first s) (let [x (first s)
r (if (nil? x) "nil" (str x))] r (__print1 x)]
(recur (str out (if first? "" " ") r) (next s) false)) (recur (str out (if first? "" " ") r) (next s) false))
out))) out)))
nil) nil)

View file

@ -613,6 +613,8 @@
;; deftype already defines ->name (= the ctor); no (name. …) interop needed, ;; deftype already defines ->name (= the ctor); no (name. …) interop needed,
;; so defrecord compiles too. map->name builds via that ctor. ;; so defrecord compiles too. map->name builds via that ctor.
(deftype ~name-sym ~fields) (deftype ~name-sym ~fields)
;; mark the type a record (map?/record?/field-seq); a bare deftype is not.
(register-record-type! (quote ~name-sym))
;; build via the positional ctor for declared fields, then carry any ;; build via the positional ctor for declared fields, then carry any
;; remaining keys as extension fields (JVM keeps them on the record). ;; remaining keys as extension fields (JVM keeps them on the record).
(def ~mapf (fn* [~m] (def ~mapf (fn* [~m]

View file

@ -89,13 +89,19 @@
(cond (cond
;; (is (thrown? Class body...)) ;; (is (thrown? Class body...))
(thrown-form? form "thrown?") (thrown-form? form "thrown?")
(let [klass (name (second form)) (let [klass-sym (second form)
klass (name klass-sym)
body (nthrest form 2)] body (nthrest form 2)]
`(try `(try
~@body ~@body
(clojure.test/fail! (str "expected " '~form " to throw" (when ~msg (str " — " ~msg)))) (clojure.test/fail! (str "expected " '~form " to throw" (when ~msg (str " — " ~msg))))
(catch Throwable e# (catch Throwable e#
(if (clojure.test/class-match? e# ~klass) ;; instance? honors the exception hierarchy (a literal class symbol), so
;; (thrown? IllegalArgumentException …) matches an ArityException subclass
;; like the JVM; class-match? is the simple-name fallback for a class jolt
;; models only by name.
(if (or (clojure.core/instance? ~klass-sym e#)
(clojure.test/class-match? e# ~klass))
(clojure.test/inc-pass!) (clojure.test/inc-pass!)
(clojure.test/fail! (str "expected throw of " ~klass " but got " (clojure.core/class e#))))))) (clojure.test/fail! (str "expected throw of " ~klass " but got " (clojure.core/class e#)))))))

View file

@ -48,6 +48,11 @@
[smap form] [smap form]
(prewalk (fn [x] (if (contains? smap x) (get smap x) x)) form)) (prewalk (fn [x] (if (contains? smap x) (get smap x) x)) form))
(defn macroexpand-all
"Recursively performs all possible macroexpansions in form."
[form]
(prewalk (fn [x] (if (seq? x) (macroexpand x) x)) form))
(defn keywordize-keys (defn keywordize-keys
[m] [m]
(let [f (fn [[k v]] (if (string? k) [(keyword k) v] [k v]))] (let [f (fn [[k v]] (if (string? k) [(keyword k) v] [k v]))]

View file

@ -3269,4 +3269,12 @@
{:suite "seqs / laziness" :label "distinct over a rest-derived seq does not overrun" :expected "2" :actual "(count (distinct (map inc (rest [10 20 30]))))"} {:suite "seqs / laziness" :label "distinct over a rest-derived seq does not overrun" :expected "2" :actual "(count (distinct (map inc (rest [10 20 30]))))"}
{:suite "transducers / map multi-input" :label "the map transducer applies f across all inputs" :expected "[6]" :actual "(((map +) conj) [] 1 2 3)"} {:suite "transducers / map multi-input" :label "the map transducer applies f across all inputs" :expected "[6]" :actual "(((map +) conj) [] 1 2 3)"}
{:suite "transducers / map multi-input" :label "single input is unchanged" :expected "[3]" :actual "(((map +) conj) [] 3)"} {:suite "transducers / map multi-input" :label "single input is unchanged" :expected "[3]" :actual "(((map +) conj) [] 3)"}
{:suite "records / deftype is not a map" :label "a bare deftype is neither map? nor record?, a defrecord is both" :expected "[false false true true]" :actual "(do (deftype DT [a]) (defrecord DR [a]) [(map? (->DT 1)) (record? (->DT 1)) (map? (->DR 1)) (record? (->DR 1))])"}
{:suite "printer / str vs print" :label "str of a vector quotes nested strings" :expected "true" :actual "(= \"[\\\"x\\\"]\" (str [\"x\"]))"}
{:suite "printer / str vs print" :label "print-str of a vector leaves strings raw" :expected "true" :actual "(= \"[x]\" (print-str [\"x\"]))"}
{:suite "printer / str vs print" :label "str of a map quotes nested strings" :expected "true" :actual "(= \"{\\\"a\\\" \\\"b\\\"}\" (str {\"a\" \"b\"}))"}
{:suite "printer / str vs print" :label "print-str of a map leaves strings raw" :expected "true" :actual "(= \"{:a x}\" (print-str {:a \"x\"}))"}
{:suite "printer / str vs print" :label "infinity inside a collection prints readably in str" :expected "true" :actual "(= \"[##Inf]\" (str [##Inf]))"}
{:suite "interop / uri equality" :label "URIs are value-equal and usable as set members" :expected "[true true]" :actual "[(= (java.net.URI. \"/\") (java.net.URI. \"/\")) (= #{(java.net.URI. \"/\")} #{(java.net.URI. \"/\")})]"}
{:suite "stdlib / clojure.walk" :label "macroexpand-all expands a form" :expected "true" :actual "(do (require (quote clojure.walk)) (seq? (clojure.walk/macroexpand-all (quote (when true 1)))))"}
] ]