From 88b7d13401a353eb7f74297c8cb6004cc7043b74 Mon Sep 17 00:00:00 2001 From: Dmitri Sotnikov Date: Mon, 15 Jun 2026 01:48:15 +0000 Subject: [PATCH] Stdlib/reader fixes to run markdown-clj (#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bringing up yogthos/markdown-clj surfaced a batch of Clojure-conformance gaps: - clojure.java.io/writer returned nil for a Writer/StringWriter (it only handled paths); now passes a Writer or file handle through, like reader does. - StringWriter had no :close field, so with-open errored closing it. - java.io.Reader had no .readLine method (only the :read-line-fn used by line-seq); markdown's main loop calls .readLine directly. - Writer.write(int) wrote the int's digits instead of the char for that code. StringBuilder.append(int) keeps Java semantics (the digits) — the two differ, so the char-code path is local to the writer, not shared render-piece. - drop-while over a string errored in array/slice; it now char-seqs the string like take-while/remove already do. - re-seq returned an empty seq instead of nil on no match, so (if-let [m (re-seq ...)] ...) always took the truthy branch — an infinite loop in markdown's thaw-string. - The #() reader didn't scan % inside map {} or set #{} literals, so #(identity {:text %}) compiled as a 0-arg fn. re-seq-nil and the #() map/set scan are general bugs, not markdown-specific. Co-authored-by: Yogthos --- src/jolt/clojure/java/io.clj | 8 +++++++- src/jolt/core.janet | 6 +++++- src/jolt/javatime.janet | 15 ++++++++++++--- src/jolt/reader.janet | 8 ++++++++ src/jolt/regex.janet | 4 +++- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/jolt/clojure/java/io.clj b/src/jolt/clojure/java/io.clj index 7f87a4c..1848a54 100644 --- a/src/jolt/clojure/java/io.clj +++ b/src/jolt/clojure/java/io.clj @@ -26,7 +26,13 @@ ;; java.io.Reader surface (.read/.mark/.reset) plus line-seq's ;; :read-line-fn, which a raw janet file handle has neither of :else (java.io.StringReader. (janet/slurp (str x))))) -(defn writer [x] (janet.file/open (str x) :w)) +(defn writer [x] + (cond + ;; already a Writer/StringWriter shim — pass through, like the JVM's + ;; io/writer on a Writer (markdown-clj's md-to-html writes into a StringWriter) + (= :jolt/writer (get x :jolt/type)) x + (= :core/file (janet/type x)) x ; already a janet file handle + :else (janet.file/open (str x) :w))) ; a path (defn input-stream [x] (reader x)) (defn output-stream [x] (writer x)) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index fa78e65..6bd1f6c 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -1237,7 +1237,11 @@ (set cur (ls-rest cur))) (if (seq-done? cur) nil (realize-ls cur)))) (make-lazy-seq (dwstep coll))) - (let [c (realize-for-iteration coll)] + # A string iterates as a seq of chars in Clojure; realize-for-iteration + # passes strings through, so char-seq it here (as take-while/remove do) — + # otherwise pred sees raw bytes and array/slice rejects the string. + (let [c0 (realize-for-iteration coll) + c (if (string? c0) (map make-char (string/bytes c0)) c0)] (var start 0) (while (and (< start (length c)) (pred (c start))) (++ start)) diff --git a/src/jolt/javatime.janet b/src/jolt/javatime.janet index 5986d4d..2dcf199 100644 --- a/src/jolt/javatime.janet +++ b/src/jolt/javatime.janet @@ -176,7 +176,9 @@ :buf (cond (nil? init) @"" (number? init) (buffer/new init) (buffer init))}) (defn make-string-writer [] - @{:jolt/type :jolt/writer :buf @"" :sink nil}) + # :close lets with-open close the writer (core-close-resource calls :close); + # it's a no-op so .toString after with-open still sees the buffer. + @{:jolt/type :jolt/writer :buf @"" :sink nil :close (fn [] nil)}) (defn make-out-writer [] @{:jolt/type :jolt/writer :buf nil :sink prin}) @@ -186,6 +188,12 @@ (and (struct? x) (= :jolt/char (get x :jolt/type))) (string/from-bytes (x :ch)) (string x))) +# Writer.write(int) writes the CHAR for that code (unlike StringBuilder.append(int), +# which appends the int's digits). jolt chars are bytes, so this round-trips UTF-8 +# byte-for-byte with readLine. +(defn- writer-piece [x] + (if (number? x) (string/from-bytes (math/trunc x)) (render-piece x))) + # Read one unit from any reader-ish value: our shims dispatch through their # tagged "read"; a janet core/file reads one byte. -1 at EOF. (defn- read-unit [r] @@ -221,6 +229,7 @@ (let [b ((self :s) (self :pos))] (put self :pos (+ 1 (self :pos))) b))) + "readLine" (fn [self] ((self :read-line-fn))) "mark" (fn [self &opt limit] (put self :marked (self :pos)) nil) "reset" (fn [self] (put self :pos (or (self :marked) 0)) nil) "skip" (fn [self n] (put self :pos (min (length (self :s)) (+ (self :pos) n))) n) @@ -232,8 +241,8 @@ (register-tagged-methods! :jolt/writer @{"write" (fn [self x] (if (self :sink) - ((self :sink) (render-piece x)) - (buffer/push-string (self :buf) (render-piece x))) + ((self :sink) (writer-piece x)) + (buffer/push-string (self :buf) (writer-piece x))) nil) "append" (fn [self x] (if (self :sink) diff --git a/src/jolt/reader.janet b/src/jolt/reader.janet index 9673b6d..af38533 100644 --- a/src/jolt/reader.janet +++ b/src/jolt/reader.janet @@ -456,6 +456,10 @@ (cond (= i :rest) (set has-rest true) (and i (> i max-n)) (set max-n i))) (or (array? f) (tuple? f)) (each x f (scan-pct x)) + # set literal form — scan its elements (#(... #{%} ...)) + (and (struct? f) (= :jolt/set (f :jolt/type))) (each x (f :value) (scan-pct x)) + # map literal form — scan its keys AND values (#(... {:k %} ...)) + (not (nil? (form-kv-order f))) (each x (form-kv-order f) (scan-pct x)) nil)) (scan-pct form) # One canonical gensym per slot 1..max-n (placeholders for unused), plus rest. @@ -473,6 +477,10 @@ f)) (array? f) (array ;(map replace-pct f)) (tuple? f) (tuple ;(map replace-pct f)) + (and (struct? f) (= :jolt/set (f :jolt/type))) + {:jolt/type :jolt/set :value (tuple ;(map replace-pct (f :value)))} + (not (nil? (form-kv-order f))) + (reader-map (array ;(map replace-pct (form-kv-order f)))) f)) (def replaced (replace-pct form)) (def arg-names @[]) diff --git a/src/jolt/regex.janet b/src/jolt/regex.janet index f7e4862..4b239d0 100644 --- a/src/jolt/regex.janet +++ b/src/jolt/regex.janet @@ -397,7 +397,9 @@ (array/push out (groups->result g (re :ngroups))) (set pos (+ pos (max 1 (length whole))))) (++ pos))) - out) + # Clojure's re-seq is nil (not an empty seq) when there are no matches, so + # `(if-let [m (re-seq ...)] ...)` works — an empty seq would be truthy. + (if (= 0 (length out)) nil out)) (defn re-split [re s] (def re (re-pattern re))