From 44837f01abfc0d76a08f0464be4b0517bce2061a Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 27 Jun 2026 15:02:32 -0400 Subject: [PATCH] data.csv: fully passes, three general fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clojure.data.csv runs its whole suite on jolt (4/4 reading/writing/eof/line- endings). Three general gaps fixed, all runtime, no re-mint, JVM-certified: - The prefix-list form of :require/:use — (:require (clojure [string :as str])) means clojure.string :as str — now expands (loader.ss). It silently failed before, trying to load a "clojure" namespace. - extend-protocol to java.io.Reader / Writer / StringReader / PushbackReader now dispatches: those reader/writer host tags carry the right class names in value-host-tags AND are in host-type-set, so extend-protocol registers under the canonical tag instead of a local ns tag (records.ss). data.csv's Read-CSV-From protocol extends to String / Reader / PushbackReader. - (str StringWriter) returns its accumulated content (register-str-render for the "writer" jhost), not the opaque host object — data.csv writes CSV to one and reads it back. Listed in docs/libraries.md + the site. make test green (+2 corpus rows, 0 new divergences), shakesmoke byte-identical. --- docs/libraries.md | 1 + host/chez/java/host-static-classes.ss | 3 ++ host/chez/loader.ss | 72 +++++++++++++++++++-------- host/chez/records.ss | 19 ++++++- test/chez/corpus.edn | 2 + 5 files changed, 74 insertions(+), 23 deletions(-) diff --git a/docs/libraries.md b/docs/libraries.md index c186820..12893f4 100644 --- a/docs/libraries.md +++ b/docs/libraries.md @@ -60,6 +60,7 @@ e.g. the [ring-app example](https://github.com/jolt-lang/examples/tree/main/ring * [data.zip](https://github.com/clojure/data.zip) — zipper navigation, including `clojure.data.zip.xml`; XML parsing via [jolt-lang/xml](https://github.com/jolt-lang/xml) (which now ships `clojure.xml/parse`). +* [data.csv](https://github.com/clojure/data.csv) — reading and writing CSV. * [tick](https://github.com/juxt/tick) — date/time over Jolt's `java.time`; `#time/…` literals via `time-literals`. * [transit-jolt](https://github.com/jolt-lang/transit-jolt) — Transit (JSON) read/write diff --git a/host/chez/java/host-static-classes.ss b/host/chez/java/host-static-classes.ss index aca1bd2..8a0355f 100644 --- a/host/chez/java/host-static-classes.ss +++ b/host/chez/java/host-static-classes.ss @@ -154,6 +154,9 @@ (cons "flush" (lambda (self) jolt-nil)) (cons "close" (lambda (self) jolt-nil)) (cons "toString" (lambda (self) (sb-str self))))) +;; (str sw) / print a StringWriter -> its accumulated content, like the JVM +;; (str calls toString) — data.csv writes CSV to a StringWriter and reads it back. +(register-str-render! (lambda (x) (and (jhost? x) (string=? (jhost-tag x) "writer"))) sb-str) ;; a file-backed writer (clojure.java.io/writer of a File/path): accumulates like ;; StringWriter, then persists to the path on flush/close, so diff --git a/host/chez/loader.ss b/host/chez/loader.ss index fb8691a..c5d6ee7 100644 --- a/host/chez/loader.ss +++ b/host/chez/loader.ss @@ -227,39 +227,67 @@ (else '())))) (and (pair? items) (symbol-t? (car items)) (symbol-t-name (car items))))) +;; A libspec under a prefix joins onto it: a bare symbol `string` -> `prefix.string`, +;; a vector `[string :as s]` -> `[prefix.string :as s]` (opts preserved). +(define (prefix-join prefix lib) + (cond + ((symbol-t? lib) (jolt-symbol #f (string-append prefix "." (symbol-t-name lib)))) + ((pvec? lib) + (let ((items (seq->list lib))) + (if (and (pair? items) (symbol-t? (car items))) + (apply jolt-vector (jolt-symbol #f (string-append prefix "." (symbol-t-name (car items)))) (cdr items)) + lib))) + (else lib))) + +;; The prefix-list form of a require/use spec: a LIST `(prefix lib …)` expands to +;; one spec per lib (prefix.lib), so (:require (clojure [string :as str])) means +;; clojure.string :as str. A vector / symbol spec is already a single lib. +(define (expand-spec s) + (if (or (cseq? s) (empty-list-t? s)) + (let ((items (seq->list s))) + (if (and (pair? items) (symbol-t? (car items)) (pair? (cdr items))) + (map (lambda (lib) (prefix-join (symbol-t-name (car items)) lib)) (cdr items)) + (list s))) + (list s))) + ;; --- require/use that LOAD --------------------------------------------------- ;; Override the alias-only versions from natives-str.ss. Load each spec's target ;; (no-op if baked/already loaded), THEN register its :as/:refer under the caller ;; ns (chez-register-spec! reads the current ns, restored by load-namespace). (define (loader-require . specs) (for-each - (lambda (s) - (let ((target (spec-target-name s))) - (when target (load-namespace target))) - (chez-register-spec! (chez-current-ns) s)) + (lambda (s0) + (for-each + (lambda (s) + (let ((target (spec-target-name s))) + (when target (load-namespace target))) + (chez-register-spec! (chez-current-ns) s)) + (expand-spec s0))) specs) jolt-nil) (def-var! "clojure.core" "require" loader-require) -(define (loader-use . specs) +(define (loader-use . specs0) (for-each - (lambda (spec) - (let ((target (spec-target-name spec))) - (when target (load-namespace target))) - (chez-register-spec! (chez-current-ns) spec) - (let* ((items (cond ((pvec? spec) (seq->list spec)) - ((or (cseq? spec) (empty-list-t? spec)) (seq->list spec)) - ((symbol-t? spec) (list spec)) - (else '()))) - (target (and (pair? items) (symbol-t? (car items)) (symbol-t-name (car items)))) - (filtered (let scan ((xs (if (pair? items) (cdr items) '()))) - (cond ((null? xs) #f) - ((and (keyword? (car xs)) - (member (keyword-t-name (car xs)) '("only" "refer"))) #t) - (else (scan (cdr xs))))))) - (when (and target (not filtered)) - (chez-register-refer-all! (chez-current-ns) target)))) - specs) + (lambda (spec0) + (for-each + (lambda (spec) + (let ((target (spec-target-name spec))) + (when target (load-namespace target))) + (chez-register-spec! (chez-current-ns) spec) + (let* ((items (cond ((pvec? spec) (seq->list spec)) + ((symbol-t? spec) (list spec)) + (else '()))) + (target (and (pair? items) (symbol-t? (car items)) (symbol-t-name (car items)))) + (filtered (let scan ((xs (if (pair? items) (cdr items) '()))) + (cond ((null? xs) #f) + ((and (keyword? (car xs)) + (member (keyword-t-name (car xs)) '("only" "refer"))) #t) + (else (scan (cdr xs))))))) + (when (and target (not filtered)) + (chez-register-refer-all! (chez-current-ns) target)))) + (expand-spec spec0))) + specs0) jolt-nil) (def-var! "clojure.core" "use" loader-use) diff --git a/host/chez/records.ss b/host/chez/records.ss index 4f516d3..cab46a2 100644 --- a/host/chez/records.ss +++ b/host/chez/records.ss @@ -469,6 +469,18 @@ ((and (jhost? obj) (string=? (jhost-tag obj) "uri")) '("URI" "java.net.URI" "Object")) ;; a ByteBuffer — extend-protocol java.nio.ByteBuffer (aws-api util). ((and (jhost? obj) (string=? (jhost-tag obj) "byte-buffer")) '("ByteBuffer" "java.nio.ByteBuffer" "Object")) + ;; java.io readers/writers — so (extend-protocol java.io.Reader …) (data.csv) + ;; and the like dispatch on one. A PushbackReader is also a Reader. + ((and (jhost? obj) (string=? (jhost-tag obj) "string-reader")) + '("StringReader" "java.io.StringReader" "Reader" "java.io.Reader" "Object")) + ((and (jhost? obj) (string=? (jhost-tag obj) "pushback-reader")) + '("PushbackReader" "java.io.PushbackReader" "FilterReader" "java.io.FilterReader" "Reader" "java.io.Reader" "Object")) + ((and (jhost? obj) (string=? (jhost-tag obj) "char-reader")) + '("Reader" "java.io.Reader" "Object")) + ((and (jhost? obj) (string=? (jhost-tag obj) "char-writer")) + '("Writer" "java.io.Writer" "Object")) + ((and (jhost? obj) (string=? (jhost-tag obj) "writer")) + '("Writer" "java.io.Writer" "Object")) ;; arrays dispatch by their JVM array-class name — extend-protocol to ;; (Class/forName "[B") for byte[] (data.json, aws-api), "[C" for char[]. ((and (jolt-array? obj) (eq? (jolt-array-kind obj) 'byte)) '("[B" "Object")) @@ -606,7 +618,12 @@ "ChronoUnit" "ChronoField" "TemporalAmount" "TemporalUnit" "TemporalField" ;; ByteBuffer + JVM array classes (extend-protocol to (Class/forName "[B")) "ByteBuffer" "java.nio.ByteBuffer" - "[B" "[C" "[I" "[J" "[D" "[Ljava.lang.Object;")) + "[B" "[C" "[I" "[J" "[D" "[Ljava.lang.Object;" + ;; java.io readers/writers — extend-protocol java.io.Reader (data.csv) + "Reader" "java.io.Reader" "Writer" "java.io.Writer" + "StringReader" "java.io.StringReader" "PushbackReader" "java.io.PushbackReader" + "BufferedReader" "java.io.BufferedReader" "FilterReader" "java.io.FilterReader" + "InputStream" "java.io.InputStream" "OutputStream" "java.io.OutputStream")) h)) (define (strip-prefix s p) (let ((pl (string-length p))) diff --git a/test/chez/corpus.edn b/test/chez/corpus.edn index cb8877f..4d99330 100644 --- a/test/chez/corpus.edn +++ b/test/chez/corpus.edn @@ -3367,4 +3367,6 @@ {: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\" {}))]"} {:suite "host-interop / class hierarchy" :label "Symbol and Keyword are IFn" :expected "[true true]" :actual "[(isa? clojure.lang.Symbol clojure.lang.IFn) (isa? clojure.lang.Keyword clojure.lang.IFn)]"} {:suite "host-interop / class hierarchy" :label "(class sym)-dispatched multimethod hits an IFn method" :expected ":ifn" :actual "(do (defmulti cm1 class) (defmethod cm1 clojure.lang.IFn [_] :ifn) (cm1 (quote sym)))"} + {:suite "host-interop / extend-protocol java.io" :label "protocol extended to Reader / String dispatches a StringReader and a String" :expected "[:reader :string]" :actual "(do (import (quote (java.io StringReader))) (defprotocol Prdr (mrd [x])) (extend-protocol Prdr java.io.Reader (mrd [_] :reader) String (mrd [_] :string)) [(mrd (StringReader. \"x\")) (mrd \"y\")])"} + {:suite "host-interop / StringWriter" :label "(str StringWriter) returns its accumulated content" :expected "\"hi!\"" :actual "(do (import (quote (java.io StringWriter))) (let [w (StringWriter.)] (.write w \"hi\") (.write w \"!\") (str w)))"} ]