edn: port read-string onto the reader; fix special-form ns shadowing

clojure.edn/read-string now delegates to clojure.core/read-string (the
reader-based special form — parses, never evals) via a private helper, with the
opts-map :eof arity and nil/blank-input handling. 43 -> 47 in the stdlib battery.

This required an evaluator fix: special forms were matched by name only, so
clojure.edn/read-string dispatched the core read-string SPECIAL FORM instead of
the var. Now a head qualified to a non-core namespace resolves to its var; only
unqualified or clojure.core-qualified heads are special forms. Conformance
218/218 all modes; full suite green.

Remaining edn gaps (jolt-b7y): set literals read as a set FORM not a constructed
set, and #uuid has no real type.
This commit is contained in:
Yogthos 2026-06-06 19:45:30 -04:00
parent fc88ecdc74
commit 1bd676c242
3 changed files with 31 additions and 12 deletions

View file

@ -1,13 +1,27 @@
; Jolt Standard Library: clojure.edn
; EDN reading and writing (stubs using the Jolt reader).
;; clojure.edn — reading EDN data. Delegates to the Jolt reader via
;; clojure.core/read-string (which parses, never evaluates — safe for EDN), and
;; adds the opts-map arity with :eof plus nil/blank-input handling.
(ns clojure.edn
"Reading EDN data."
(:require [clojure.string :as cstr]))
;; Private helper, NOT named read-string: an unqualified (read-string …) call
;; dispatches the core read-string SPECIAL FORM (by name, regardless of ns), so
;; the 1-arity can't delegate to the 2-arity through that name.
(defn- read-edn [opts s]
(if (or (nil? s) (cstr/blank? s))
(get opts :eof nil)
(clojure.core/read-string s)))
(defn read-string
[s]
(let [ctx ((get (dyn :current-env) (symbol "init")))]
((get (dyn :current-env) (symbol "eval-string")) ctx s)))
"Reads one object from the string s. Returns the :eof option value (default
nil) for nil or blank input. opts is an options map; :eof sets the value
returned at end of input."
([s] (read-edn {} s))
([opts s] (read-edn opts s)))
(defn read
"Reads the next line from reader and parses one EDN object from it."
[reader]
(let [line ((get (dyn :current-env) (symbol "file/read")) reader :line)]
(when line
(read-string line))))
(when line (read-string line))))

View file

@ -611,9 +611,13 @@
(defn- eval-list
[ctx bindings form]
(def first-form (first form))
# Safe name extraction: non-symbol heads (e.g. keywords) fall through to default
# Safe name extraction: non-symbol heads (e.g. keywords) fall through to default.
# A head qualified to a NON-core namespace (e.g. clojure.edn/read-string) must
# resolve to that var, not the like-named clojure.core special form — so only
# unqualified or clojure.core-qualified heads dispatch as special forms.
(def name (if (and (struct? first-form) (= :symbol (first-form :jolt/type)))
(first-form :name)
(let [ns (first-form :ns)]
(if (or (nil? ns) (= ns "clojure.core")) (first-form :name) nil))
nil))
(match name
"quote" (in form 1)

View file

@ -12,9 +12,10 @@
[["clojure/walk_test/walk.cljc" 34 true]
["clojure/zip_test/zip.cljc" 33 true]
["clojure/data_test/diff.cljc" 61 true]
# clojure.edn is still a stub (no opts/:eof arity, broken set/nil handling);
# tracked separately. Guard its current passing subset so it can't regress.
["clojure/edn_test/read_string.cljc" 43 false]])
# clojure.edn now reads via clojure.core/read-string with opts/:eof + nil/blank
# handling. The remaining 3 fails are sets (the reader yields a set FORM, not a
# constructed set) and #uuid — tracked in jolt-b7y. Guard the passing subset.
["clojure/edn_test/read_string.cljc" 47 false]])
(def root "test/clojure-stdlib")
(def per-file-timeout 6)