Strict reader tokens; edn mode with the reference's error contracts

The reader now rejects what the JVM reader rejects: a token that starts
like a number but doesn't parse is NumberFormatException (1a, 08, 0x2g,
2r2 — never a symbol); ratio parts are digit runs (1/-1 invalid) with a
zero denominator throwing ArithmeticException; empty ns/name parts are
invalid tokens (:, ::, foo/, /foo) while /, ns//, and :/ stay valid;
duplicate map keys and set elements throw at read; unsupported string
escapes and octal escapes past \377 throw; a stray close delimiter is
'Unmatched delimiter'; \r ends line comments. #inst validates its
calendar fields progressively (leap years included) and #uuid demands
canonical hex. 1-arg symbol splits its ns at the FIRST slash
(Symbol.intern): (symbol "foo/bar/baz") is foo/"bar/baz".

clojure.edn gets its own strict seam (__read-form-edn): auto-resolved
keywords are invalid there, every #_ discarded form validates through the
same :readers/:default pipeline (an unreadable tagged element throws even
when discarded), built-in tags win over :default, M literals construct
BigDecimals, lists satisfy list?, and EOF honors :eof — an opts map
without :eof makes end-of-input an error.

clojure.edn-test.read-string goes 246 pass / 46 fail / 5 errors -> 297/0/0
(fully clean). cts baseline 5904 -> 5955 pass, 23 errors, 56 baselined
namespaces. 9 JVM-certified corpus rows; reader spec section.
This commit is contained in:
Yogthos 2026-07-02 12:00:13 -04:00
parent 95186a6782
commit 44d4875a24
8 changed files with 1006 additions and 781 deletions

View file

@ -3565,4 +3565,13 @@
{:suite "multimethods / deferred definition" :label "defmulti/defmethod inside a fn intern in the ns they were written in" :expected ":hit" :actual "((fn [] (defmulti cdm-deferred first) (defmethod cdm-deferred :a [_] :hit) (cdm-deferred [:a 1])))"}
{:suite "host-interop / wrapper ctors and TYPE" :label "Boolean/Integer/Double ctors; primitive TYPE tokens" :expected "[false true 12 1.5 \"int\" \"double\"]" :actual "[(Boolean. \"yes\") (Boolean. \"TRUE\") (Integer. \"12\") (Double. \"1.5\") (str Integer/TYPE) (str Double/TYPE)]"}
{:suite "host-interop / IReduce" :label ".reduce drives a collection's own reduce" :expected "[7 6]" :actual "[(.reduce [1 2 3] + 1) (.reduce [1 2 3] +)]"}
{:suite "edn / strictness" :label "duplicate literal keys and elements throw" :expected "[:dup :dup]" :actual "[(try (clojure.edn/read-string \"{:a 1 :a 2}\") (catch IllegalArgumentException _ :dup)) (try (clojure.edn/read-string \"#{:a :a}\") (catch IllegalArgumentException _ :dup))]"}
{:suite "edn / strictness" :label "invalid numbers and ratio edges throw" :expected "[:ae :nfe :nfe :nfe :nfe]" :actual "[(try (clojure.edn/read-string \"1/0\") (catch ArithmeticException _ :ae)) (try (clojure.edn/read-string \"1/-1\") (catch NumberFormatException _ :nfe)) (try (clojure.edn/read-string \"08\") (catch NumberFormatException _ :nfe)) (try (clojure.edn/read-string \"1a\") (catch NumberFormatException _ :nfe)) (try (clojure.edn/read-string \"0x2g\") (catch NumberFormatException _ :nfe))]"}
{:suite "edn / strictness" :label "invalid tokens throw; :/ and ns// are valid" :expected "[:t :t :t :t \"/\" \"/\"]" :actual "[(try (clojure.edn/read-string \"::foo\") (catch Throwable _ :t)) (try (clojure.edn/read-string \":\") (catch Throwable _ :t)) (try (clojure.edn/read-string \"foo/\") (catch Throwable _ :t)) (try (clojure.edn/read-string \"/foo\") (catch Throwable _ :t)) (name (clojure.edn/read-string \":/\")) (name (clojure.edn/read-string \"foo//\"))]"}
{:suite "edn / strictness" :label "unmatched delimiters, bad escapes, octal range throw" :expected "[:t :t :t :t]" :actual "[(try (clojure.edn/read-string \")\") (catch Throwable _ :t)) (try (clojure.edn/read-string \"]\") (catch Throwable _ :t)) (try (clojure.edn/read-string \"\\\"\\\\q\\\"\") (catch Throwable _ :t)) (try (clojure.edn/read-string \"\\\\o400\") (catch Throwable _ :t))]"}
{:suite "edn / strictness" :label "eof honors :eof; a missing :eof is an error; \\r ends comments" :expected "[:END :t 3]" :actual "[(clojure.edn/read-string {:eof :END} \"\") (try (clojure.edn/read-string {} \" \") (catch Throwable _ :t)) (clojure.edn/read-string \";c\\r3\\n5\")]"}
{:suite "edn / strictness" :label "M literals construct; lists are lists; discarded tags validate" :expected "[-3.14M true :t]" :actual "[(clojure.edn/read-string \"-3.14M\") (list? (clojure.edn/read-string \"(1 (2))\")) (try (clojure.edn/read-string \"#_ #foo 0\") (catch Throwable _ :t))]"}
{:suite "edn / strictness" :label "#inst and #uuid validate their fields" :expected "[:t :t :t]" :actual "[(try (clojure.edn/read-string \"#inst \\\"2010-02-29T00:00:00.000Z\\\"\") (catch Throwable _ :t)) (try (clojure.edn/read-string \"#inst \\\"2010-01-01T24:00:00.000Z\\\"\") (catch Throwable _ :t)) (try (clojure.edn/read-string \"#uuid \\\"not-a-uuid\\\"\") (catch Throwable _ :t))]"}
{:suite "reader / strict tokens" :label "the core reader rejects invalid numbers and duplicate keys too" :expected "[:nfe :dup 34]" :actual "[(try (read-string \"1a\") (catch NumberFormatException _ :nfe)) (try (read-string \"{:a 1 :a 2}\") (catch IllegalArgumentException _ :dup)) (read-string \"042\")]"}
{:suite "reader / symbol intern" :label "1-arg symbol splits ns at the first slash (Symbol.intern)" :expected "[\"foo\" \"bar/baz\" \"/\"]" :actual "[(namespace (symbol \"foo/bar/baz\")) (name (symbol \"foo/bar/baz\")) (name (symbol \"foo//\"))]"}
]

View file

@ -57,4 +57,3 @@ clojure.core-test.update 1 0
clojure.core-test.vals 0 3
clojure.core-test.vec 1 0
clojure.core-test.when-let 1 0
clojure.edn-test.read-string 46 5