Rename src/jolt -> stdlib (the runtime-loaded layer; jolt-core stays the seed-baked layer) and update the loader / emit-image / doc paths. Drop dead code: the spike/ experiments, the duplicate clojuredocs-export.edn (json moves to tools/), the Janet-era jolt.http binding, and the orphaned persistent_vector.clj whose ns/path didn't even match. Strip porting residue from comments and docstrings across host/chez, jolt-core, stdlib, tests, and docs: internal issue ids, "Phase N" markers, and the "vs Janet" historical exposition, leaving present-tense descriptions and the real JVM-Clojure semantic contrasts. Same pass over the corpus suite labels. The seed is unchanged (docstrings/comments aren't emitted), so the self-host fixpoint and corpus are untouched. Port tools/spec_coverage.py off the dead janet probe to bin/joltc and regenerate coverage.md; drop the dead :host/janet rule from certify.clj and regenerate the conformance profile. Add docs/host-interop.md (the JVM shims and how to register your own host class from a library) and a writing-style note in CLAUDE.md. Stabilize the four racy concurrency corpus cases (future-cancel and agent send/send-off): give the future a sleeping body and the agent a slow action, so cancel reliably catches an in-flight future and deref reliably reads the pre-update snapshot. They certify deterministically now, so drop their :flaky allowlist entries and the orphaned legend.
121 lines
5.7 KiB
Clojure
121 lines
5.7 KiB
Clojure
;; regen-corpus.clj — set test/chez/corpus.edn :expected to what reference JVM
|
|
;; Clojure produces, making the JVM the source of truth for the spec.
|
|
;;
|
|
;; Runs in ONE JVM process: a single `clojure -M` invocation reads the corpus and
|
|
;; evaluates every row's :actual once, in-process. The per-row watchdog is a thread
|
|
;; (future) with a wall-clock deadline — not a new JVM per case.
|
|
;;
|
|
;; For each row:
|
|
;; - JVM evaluates :actual to a value -> :expected := a self-evaluating source
|
|
;; string for that value (lists/lazy-seqs are vectorized at every nesting depth,
|
|
;; matching the corpus convention; jolt='s cross-type sequential equality makes
|
|
;; a vector :expected match a seq :actual). Only adopted if the rendered string
|
|
;; round-trips (re-reads+evals back to an = value) — else the existing :expected
|
|
;; is kept.
|
|
;; - JVM throws AND the row already expected :throws -> :throws (unchanged).
|
|
;; - Otherwise (JVM can't run the case: jolt-specific host interop / reader, a
|
|
;; timeout, or a throw where jolt has a value) -> the existing :expected is kept.
|
|
;; These are the non-portable rows (profile-tagged) — JVM is not the oracle for
|
|
;; them.
|
|
;;
|
|
;; Run from the repo root:
|
|
;; clojure -M test/conformance/regen-corpus.clj ; rewrites corpus.edn
|
|
;; clojure -M test/conformance/regen-corpus.clj --dry ; report only, no write
|
|
(ns regen-corpus
|
|
(:require [clojure.edn :as edn]
|
|
[clojure.string :as str]
|
|
[clojure.walk :as walk]))
|
|
|
|
(def corpus-path "test/chez/corpus.edn")
|
|
(def dry? (some #{"--dry"} *command-line-args*))
|
|
|
|
;; --- isolated JVM evaluation (same model as certify.clj) ---------------------
|
|
(defn read-program [src]
|
|
(read-string {:read-cond :allow} (str "(do " src ")")))
|
|
|
|
(defn eval-isolated [src]
|
|
(let [sink (java.io.StringWriter.)
|
|
empty-in (java.io.PushbackReader. (java.io.StringReader. ""))]
|
|
(try
|
|
(remove-ns 'user)
|
|
(let [the-ns (create-ns 'user)]
|
|
(binding [*ns* the-ns *out* sink *err* sink *in* empty-in]
|
|
(clojure.core/refer-clojure)
|
|
;; make the stdlib namespaces a corpus case may reference by qualified name
|
|
;; resolvable (clojure.math/floor etc.) so they evaluate to the real JVM
|
|
;; value instead of throwing Unable-to-resolve (-> existing :expected kept).
|
|
(doseq [n '[clojure.string clojure.set clojure.walk clojure.edn
|
|
clojure.math clojure.pprint]]
|
|
(try (require n) (catch Throwable _ nil)))
|
|
(let [form (try (read-program src)
|
|
(catch Throwable t (throw (ex-info "read" {::read t}))))]
|
|
[:ok (eval form)])))
|
|
(catch clojure.lang.ExceptionInfo e
|
|
(if (::read (ex-data e)) [:read-error (::read (ex-data e))] [:throw e]))
|
|
(catch Throwable t [:throw t]))))
|
|
|
|
(def ^:const case-timeout-ms 5000)
|
|
|
|
;; --- rendering a JVM value to a self-evaluating :expected source string ------
|
|
;; Vectorize every list/lazy-seq (at any nesting) so the rendered form is
|
|
;; self-evaluating (no bare call forms) and jolt= to the seq it represents.
|
|
(defn vectorize [v]
|
|
(walk/postwalk (fn [x] (if (and (seq? x) (not (vector? x))) (vec x) x)) v))
|
|
|
|
(defn render [v] (binding [*print-length* nil *print-level* nil] (pr-str (vectorize v))))
|
|
|
|
;; Adopt the JVM value only if its rendered form round-trips back to an = value on
|
|
;; the JVM (guards against records / tagged literals / opaque objects that pr-str
|
|
;; can't reproduce as readable source).
|
|
(defn round-trips? [s v]
|
|
(try
|
|
(let [rt (binding [*ns* (create-ns 'user)] (eval (read-string {:read-cond :allow} s)))]
|
|
(= rt v))
|
|
(catch Throwable _ false)))
|
|
|
|
;; Evaluate AND render inside one watchdog'd future: a row whose :actual returns an
|
|
;; unforced infinite lazy seq evaluates fast (not a timeout) but render/postwalk
|
|
;; would then realize it forever — so rendering must be inside the deadline too.
|
|
;; Returns one of [:value s] / [:throw] / [:read-error] / [:timeout] / [:unrenderable].
|
|
(defn eval+render [src]
|
|
(let [f (future
|
|
(try
|
|
(let [r (eval-isolated src)]
|
|
(if (= (first r) :ok)
|
|
(let [v (second r) s (render v)]
|
|
(if (round-trips? s v) [:value s] [:unrenderable]))
|
|
[(first r)]))
|
|
(catch Throwable _ [:unrenderable])))
|
|
res (deref f case-timeout-ms ::timeout)]
|
|
(if (= res ::timeout) (do (future-cancel f) [:timeout]) res)))
|
|
|
|
(defn regen-row [row]
|
|
(let [{:keys [expected actual]} row
|
|
r (eval+render actual)]
|
|
(case (first r)
|
|
:value (let [s (second r)] [(assoc row :expected s) (if (= s expected) :same :updated)])
|
|
:throw (if (= expected :throws) [row :throws] [row :kept])
|
|
;; read-error / timeout / unrenderable -> JVM is not the oracle; keep existing.
|
|
[row :kept])))
|
|
|
|
;; --- corpus writer (preserves the one-row-per-line layout) -------------------
|
|
(defn row-str [{:keys [suite label expected actual]}]
|
|
(str " {:suite " (pr-str suite)
|
|
" :label " (pr-str label)
|
|
" :expected " (if (= expected :throws) ":throws" (pr-str expected))
|
|
" :actual " (pr-str actual) "}"))
|
|
|
|
(defn -main [& _]
|
|
(let [corpus (edn/read-string (slurp corpus-path))
|
|
n (count corpus)
|
|
results (mapv regen-row corpus)
|
|
rows (mapv first results)
|
|
tally (frequencies (map second results))]
|
|
(println (format "Regenerated %d rows from JVM Clojure %s" n (clojure-version)))
|
|
(doseq [k [:updated :same :throws :kept :unrenderable]]
|
|
(println (format " %-13s %5d" (name k) (get tally k 0))))
|
|
(when-not dry?
|
|
(spit corpus-path (str "[\n" (str/join "\n" (map row-str rows)) "\n]\n"))
|
|
(println (format "\nwrote %s" corpus-path)))))
|
|
|
|
(apply -main *command-line-args*)
|