core: spec 35-var batch A — 1.11 parsers, map/partition variants, with-redefs, ns fns

Fifteen vars from the spec coverage gap (docs/spec/coverage.md):
parse-long/parse-double/parse-boolean (strict validation; scan-number alone
accepts 0x10), newline, current-time-ms (host clock for time), update-keys/
update-vals (PHM base, collisions last-wins), partitionv/partitionv-all/
splitv-at (lazy seqs of vectors; splitv-at's tail stays a seq, matching the
reference), with-redefs/with-redefs-fn (roots restored on throw), time,
macroexpand (expand-1 to fixpoint), alias/ns-unalias (write BOTH alias stores
— require :as uses string-keyed :imports while ns-aliases reads :aliases;
split filed), ns-publics (symbol-keyed map; publics == interns, no privacy).

Three pre-existing bugs fixed along the way:
- (partition n step pad coll) misparsed pad as the coll and returned ()
- (require 'bare.symbol) rejected — only vector specs were accepted
- analyze-form leaked the interpreted analyzer's ns on a punt: a throw out of
  the analyzer left current-ns=jolt.analyzer and :compile-ns set, so the
  fallback interpretation resolved user vars against the wrong namespace
  (bit (var user-sym) under compile mode)

And one overlay-authoring landmine documented in-code: a 20-coll fn must not
use 30-macros macros (with-redefs-fn's dotimes compiled as a forward ref that
resolved to the macro fn at runtime) — loop/recur instead.

Gate: conformance 316x3 (+14 rows), suite 4324 pass / 78 clean (was 4081/72;
parse_*/update_* files now contribute), baselines raised, all specs+unit,
fixpoint, self-host, sci, staged. Coverage: missing-portable 35 -> 20.
This commit is contained in:
Yogthos 2026-06-10 11:16:54 -04:00
parent 894af34b4c
commit eb7a9f1b20
13 changed files with 277 additions and 44 deletions

View file

@ -3,14 +3,14 @@
Generated 2026-06-10 by `tools/spec_coverage.py` — do not edit by hand. Generated 2026-06-10 by `tools/spec_coverage.py` — do not edit by hand.
Surface: **694** clojure.core vars (ClojureDocs export; 648 with Surface: **694** clojure.core vars (ClojureDocs export; 648 with
community examples). jolt interns 534 of them. community examples). jolt interns 550 of them.
| Status | Count | Meaning | | Status | Count | Meaning |
|---|---|---| |---|---|---|
| implemented+tested | 380 | in jolt and exercised by spec/conformance | | implemented+tested | 397 | in jolt and exercised by spec/conformance |
| implemented-untested | 154 | in jolt, no direct test — spec entries will add them | | implemented-untested | 153 | in jolt, no direct test — spec entries will add them |
| resolvable-not-interned | 22 | works in code but invisible to ns introspection (conformance finding) | | resolvable-not-interned | 22 | works in code but invisible to ns introspection (conformance finding) |
| missing-portable | 35 | portable semantics, jolt lacks it — implementation gap | | missing-portable | 19 | portable semantics, jolt lacks it — implementation gap |
| special-form | 13 | specified in §3, not a library var | | special-form | 13 | specified in §3, not a library var |
| dynamic-var | 29 | classification needed: portable default vs host-dependent | | dynamic-var | 29 | classification needed: portable default vs host-dependent |
| agents-taps | 22 | out of scope pending concurrency design note | | agents-taps | 22 | out of scope pending concurrency design note |
@ -103,7 +103,7 @@ UNVERIFIED field; that column will be added as entries land.
| `agent-errors` | agents-taps | | | `agent-errors` | agents-taps | |
| `aget` | implemented+tested | ✓ | | `aget` | implemented+tested | ✓ |
| `alength` | implemented+tested | ✓ | | `alength` | implemented+tested | ✓ |
| `alias` | missing-portable | ✓ | | `alias` | implemented+tested | ✓ |
| `all-ns` | implemented-untested | ✓ | | `all-ns` | implemented-untested | ✓ |
| `alter` | stm-refs | ✓ | | `alter` | stm-refs | ✓ |
| `alter-meta!` | implemented+tested | ✓ | | `alter-meta!` | implemented+tested | ✓ |
@ -390,7 +390,7 @@ UNVERIFIED field; that column will be added as entries land.
| `long-array` | implemented-untested | ✓ | | `long-array` | implemented-untested | ✓ |
| `longs` | implemented-untested | ✓ | | `longs` | implemented-untested | ✓ |
| `loop` | implemented+tested | ✓ | | `loop` | implemented+tested | ✓ |
| `macroexpand` | missing-portable | ✓ | | `macroexpand` | implemented+tested | ✓ |
| `macroexpand-1` | implemented+tested | ✓ | | `macroexpand-1` | implemented+tested | ✓ |
| `make-array` | implemented-untested | ✓ | | `make-array` | implemented-untested | ✓ |
| `make-hierarchy` | implemented+tested | ✓ | | `make-hierarchy` | implemented+tested | ✓ |
@ -423,7 +423,7 @@ UNVERIFIED field; that column will be added as entries land.
| `neg-int?` | implemented+tested | ✓ | | `neg-int?` | implemented+tested | ✓ |
| `neg?` | implemented+tested | ✓ | | `neg?` | implemented+tested | ✓ |
| `new` | special-form | ✓ | | `new` | special-form | ✓ |
| `newline` | missing-portable | ✓ | | `newline` | implemented+tested | ✓ |
| `next` | implemented+tested | ✓ | | `next` | implemented+tested | ✓ |
| `nfirst` | implemented-untested | ✓ | | `nfirst` | implemented-untested | ✓ |
| `nil?` | implemented+tested | ✓ | | `nil?` | implemented+tested | ✓ |
@ -434,15 +434,15 @@ UNVERIFIED field; that column will be added as entries land.
| `not-every?` | implemented+tested | ✓ | | `not-every?` | implemented+tested | ✓ |
| `not=` | implemented+tested | ✓ | | `not=` | implemented+tested | ✓ |
| `ns` | implemented+tested | ✓ | | `ns` | implemented+tested | ✓ |
| `ns-aliases` | implemented-untested | ✓ | | `ns-aliases` | implemented+tested | ✓ |
| `ns-imports` | implemented-untested | ✓ | | `ns-imports` | implemented-untested | ✓ |
| `ns-interns` | implemented-untested | ✓ | | `ns-interns` | implemented-untested | ✓ |
| `ns-map` | implemented-untested | ✓ | | `ns-map` | implemented-untested | ✓ |
| `ns-name` | implemented+tested | ✓ | | `ns-name` | implemented+tested | ✓ |
| `ns-publics` | missing-portable | ✓ | | `ns-publics` | implemented+tested | ✓ |
| `ns-refers` | missing-portable | ✓ | | `ns-refers` | missing-portable | ✓ |
| `ns-resolve` | implemented+tested | ✓ | | `ns-resolve` | implemented+tested | ✓ |
| `ns-unalias` | missing-portable | ✓ | | `ns-unalias` | implemented+tested | ✓ |
| `ns-unmap` | implemented-untested | ✓ | | `ns-unmap` | implemented-untested | ✓ |
| `nth` | implemented+tested | ✓ | | `nth` | implemented+tested | ✓ |
| `nthnext` | implemented+tested | ✓ | | `nthnext` | implemented+tested | ✓ |
@ -454,16 +454,16 @@ UNVERIFIED field; that column will be added as entries land.
| `odd?` | implemented+tested | ✓ | | `odd?` | implemented+tested | ✓ |
| `or` | implemented+tested | ✓ | | `or` | implemented+tested | ✓ |
| `parents` | implemented+tested | ✓ | | `parents` | implemented+tested | ✓ |
| `parse-boolean` | missing-portable | ✓ | | `parse-boolean` | implemented+tested | ✓ |
| `parse-double` | missing-portable | ✓ | | `parse-double` | implemented+tested | ✓ |
| `parse-long` | missing-portable | ✓ | | `parse-long` | implemented+tested | ✓ |
| `parse-uuid` | implemented+tested | ✓ | | `parse-uuid` | implemented+tested | ✓ |
| `partial` | implemented+tested | ✓ | | `partial` | implemented+tested | ✓ |
| `partition` | implemented+tested | ✓ | | `partition` | implemented+tested | ✓ |
| `partition-all` | implemented+tested | ✓ | | `partition-all` | implemented+tested | ✓ |
| `partition-by` | implemented+tested | ✓ | | `partition-by` | implemented+tested | ✓ |
| `partitionv` | missing-portable | | | `partitionv` | implemented+tested | |
| `partitionv-all` | missing-portable | | | `partitionv-all` | implemented+tested | |
| `pcalls` | jvm-specific | ✓ | | `pcalls` | jvm-specific | ✓ |
| `peek` | implemented+tested | ✓ | | `peek` | implemented+tested | ✓ |
| `persistent!` | implemented+tested | ✓ | | `persistent!` | implemented+tested | ✓ |
@ -611,7 +611,7 @@ UNVERIFIED field; that column will be added as entries land.
| `spit` | implemented-untested | ✓ | | `spit` | implemented-untested | ✓ |
| `split-at` | implemented+tested | ✓ | | `split-at` | implemented+tested | ✓ |
| `split-with` | implemented+tested | ✓ | | `split-with` | implemented+tested | ✓ |
| `splitv-at` | missing-portable | | | `splitv-at` | implemented+tested | |
| `str` | implemented+tested | ✓ | | `str` | implemented+tested | ✓ |
| `stream-into!` | jvm-specific | | | `stream-into!` | jvm-specific | |
| `stream-reduce!` | jvm-specific | | | `stream-reduce!` | jvm-specific | |
@ -640,7 +640,7 @@ UNVERIFIED field; that column will be added as entries land.
| `the-ns` | implemented+tested | ✓ | | `the-ns` | implemented+tested | ✓ |
| `thread-bound?` | missing-portable | ✓ | | `thread-bound?` | missing-portable | ✓ |
| `throw` | special-form | ✓ | | `throw` | special-form | ✓ |
| `time` | missing-portable | ✓ | | `time` | implemented+tested | ✓ |
| `to-array` | implemented-untested | ✓ | | `to-array` | implemented-untested | ✓ |
| `to-array-2d` | implemented+tested | ✓ | | `to-array-2d` | implemented+tested | ✓ |
| `trampoline` | implemented+tested | ✓ | | `trampoline` | implemented+tested | ✓ |
@ -678,9 +678,9 @@ UNVERIFIED field; that column will be added as entries land.
| `unsigned-bit-shift-right` | implemented-untested | ✓ | | `unsigned-bit-shift-right` | implemented-untested | ✓ |
| `update` | implemented+tested | ✓ | | `update` | implemented+tested | ✓ |
| `update-in` | implemented+tested | ✓ | | `update-in` | implemented+tested | ✓ |
| `update-keys` | missing-portable | ✓ | | `update-keys` | implemented+tested | ✓ |
| `update-proxy` | implemented-untested | ✓ | | `update-proxy` | implemented-untested | ✓ |
| `update-vals` | missing-portable | ✓ | | `update-vals` | implemented+tested | ✓ |
| `uri?` | implemented-untested | ✓ | | `uri?` | implemented-untested | ✓ |
| `use` | implemented+tested | ✓ | | `use` | implemented+tested | ✓ |
| `uuid?` | implemented+tested | ✓ | | `uuid?` | implemented+tested | ✓ |
@ -714,8 +714,8 @@ UNVERIFIED field; that column will be added as entries land.
| `with-open` | missing-portable | ✓ | | `with-open` | missing-portable | ✓ |
| `with-out-str` | implemented+tested | ✓ | | `with-out-str` | implemented+tested | ✓ |
| `with-precision` | missing-portable | ✓ | | `with-precision` | missing-portable | ✓ |
| `with-redefs` | missing-portable | ✓ | | `with-redefs` | implemented+tested | ✓ |
| `with-redefs-fn` | missing-portable | ✓ | | `with-redefs-fn` | implemented+tested | ✓ |
| `xml-seq` | implemented-untested | ✓ | | `xml-seq` | implemented-untested | ✓ |
| `zero?` | implemented+tested | ✓ | | `zero?` | implemented+tested | ✓ |
| `zipmap` | implemented+tested | ✓ | | `zipmap` | implemented+tested | ✓ |

View file

@ -287,6 +287,46 @@
(defn tagged-literal? [x] (= (get x :jolt/type) :jolt/tagged-literal)) (defn tagged-literal? [x] (= (get x :jolt/type) :jolt/tagged-literal))
(defn record? [x] (some? (get x :jolt/deftype))) (defn record? [x] (some? (get x :jolt/deftype)))
(defn uuid? [x] (= (get x :jolt/type) :jolt/uuid)) (defn uuid? [x] (= (get x :jolt/type) :jolt/uuid))
;; Clojure 1.11 map transformers. PHM base so transformed keys canonicalize
;; (collisions: last entry in seq order wins, matching the reference).
(defn update-keys [m f]
(reduce-kv (fn [acc k v] (assoc acc (f k) v)) (hash-map) m))
(defn update-vals [m f]
(reduce-kv (fn [acc k v] (assoc acc k (f v))) (hash-map) m))
;; Vector-returning partition variants (1.11): lazy seqs OF vectors.
(defn partitionv
([n coll] (map vec (partition n coll)))
([n step coll] (map vec (partition n step coll)))
([n step pad coll] (map vec (partition n step pad coll))))
(defn partitionv-all
([n coll] (map vec (partition-all n coll)))
([n step coll] (map vec (partition-all n step coll))))
;; First part a vector, rest a seq — matching the reference implementation.
(defn splitv-at [n coll]
[(vec (take n coll)) (drop n coll)])
;; with-redefs-fn: temporarily set each var's root to the mapped value, run
;; the thunk, restore the saved roots even on throw. The with-redefs macro
;; (30-macros) builds the {var val} map from names.
(defn with-redefs-fn [binding-map func]
(let [vars (vec (keys binding-map))
saved (mapv var-get vars)]
(doseq [v vars] (var-set v (get binding-map v)))
(try
(func)
(finally
;; loop/recur, not dotimes: dotimes is a 30-macros macro and this tier
;; compiles before it exists (a forward ref would resolve to the macro
;; fn at runtime and mis-apply it).
(loop [i 0]
(when (< i (count vars))
(var-set (nth vars i) (nth saved i))
(recur (inc i))))))))
;; Jolt has no chunked seqs (Phase 5 territory), so this is always false. ;; Jolt has no chunked seqs (Phase 5 territory), so this is always false.
(defn chunked-seq? [x] false) (defn chunked-seq? [x] false)

View file

@ -57,6 +57,22 @@
;; defonce: define name only if it isn't already bound to a non-nil root; ;; defonce: define name only if it isn't already bound to a non-nil root;
;; returns the existing var untouched otherwise (matching the prior arm). ;; returns the existing var untouched otherwise (matching the prior arm).
;; time: evaluate expr, print the elapsed wall-clock, return the value.
;; current-time-ms is the host's monotonic clock.
(defmacro time [expr]
`(let [start# (current-time-ms)
ret# ~expr]
(println (str "Elapsed time: " (- (current-time-ms) start#) " msecs"))
ret#))
;; with-redefs: temporary root rebinding, restored on exit (incl. throw).
;; Builds (hash-map (var n1) v1 ...) — a call form, since map-literal forms
;; can't carry call forms as keys.
(defmacro with-redefs [bindings & body]
(let [pairs (reduce (fn [acc p] (conj (conj acc `(var ~(first p))) (second p)))
[] (partition 2 bindings))]
`(with-redefs-fn (hash-map ~@pairs) (fn [] ~@body))))
(defmacro defonce [name expr] (defmacro defonce [name expr]
`(let [v# (resolve (quote ~name))] `(let [v# (resolve (quote ~name))]
(if (and v# (some? (var-get v#))) (if (and v# (some? (var-get v#)))

View file

@ -351,15 +351,24 @@
# jolt.analyzer), and the interpreter rebinds current-ns to a fn's defining ns # jolt.analyzer), and the interpreter rebinds current-ns to a fn's defining ns
# while it runs — so h/current-ns must read this instead of ctx-current-ns. # while it runs — so h/current-ns must read this instead of ctx-current-ns.
(put (ctx :env) :compile-ns (ctx-current-ns ctx)) (put (ctx :env) :compile-ns (ctx-current-ns ctx))
(def saved-ns (ctx-current-ns ctx))
(def av (ns-find (ctx-find-ns ctx "jolt.analyzer") "analyze")) (def av (ns-find (ctx-find-ns ctx "jolt.analyzer") "analyze"))
# Pre-kernel bootstrap: ensure-analyzer is gated until the kernel tier loads # Pre-kernel bootstrap: ensure-analyzer is gated until the kernel tier loads
# (see api/load-core-overlay!), so a compile request from an earlier tier (e.g. # (see api/load-core-overlay!), so a compile request from an earlier tier (e.g.
# 00-syntax's destructure defn) finds no analyzer. That fallback is DESIGNED — # 00-syntax's destructure defn) finds no analyzer. That fallback is DESIGNED —
# route it through the sanctioned punt channel rather than crashing on a nil var. # route it through the sanctioned punt channel rather than crashing on a nil var.
(unless av (error "jolt/uncompilable: analyzer not built (pre-kernel bootstrap)")) (unless av
(def r ((var-get av) ctx form)) (put (ctx :env) :compile-ns nil)
(error "jolt/uncompilable: analyzer not built (pre-kernel bootstrap)"))
# The analyzer runs INTERPRETED; the interpreter rebinds current-ns to a fn's
# defining ns (jolt.analyzer) while it runs and only restores on normal return.
# A punt THROWS out of those frames, leaking jolt.analyzer as current-ns (and
# :compile-ns stayed set) — the fallback interpretation then resolves user vars
# against the wrong ns. Restore both on every exit.
(def r (protect ((var-get av) ctx form)))
(put (ctx :env) :compile-ns nil) (put (ctx :env) :compile-ns nil)
r) (ctx-set-current-ns ctx saved-ns)
(if (r 0) (r 1) (error (r 1))))
# The analyzer's deliberate punt signal — (uncompilable why) throws the string # The analyzer's deliberate punt signal — (uncompilable why) throws the string
# "jolt/uncompilable: <why>". Anything else escaping the compile step is an # "jolt/uncompilable: <why>". Anything else escaping the compile step is an

View file

@ -1409,12 +1409,15 @@
# (core/20-coll.clj). # (core/20-coll.clj).
(defn core-partition (defn core-partition
"(partition n coll) or (partition n step coll). Only complete partitions of "(partition n coll), (partition n step coll), or (partition n step pad coll).
size n are kept (use partition-all to keep the trailing remainder)." Only complete partitions of size n are kept; with pad, the final partial
partition is padded from pad (possibly to fewer than n if pad runs out)."
[n & rest] [n & rest]
(let [has-step (> (length rest) 1) (let [argc (length rest)
step (if has-step (first rest) n) step (if (>= argc 2) (first rest) n)
coll (if has-step (in rest 1) (first rest))] pad (if (>= argc 3) (in rest 1) nil)
has-pad (>= argc 3)
coll (case argc 1 (first rest) 2 (in rest 1) 3 (in rest 2))]
# Option A: always lazy. # Option A: always lazy.
(defn pstep [c] (defn pstep [c]
(fn [] (fn []
@ -1425,9 +1428,16 @@
(array/push part (core-first cur)) (array/push part (core-first cur))
(set cur (core-rest cur)) (set cur (core-rest cur))
(++ i)) (++ i))
(if (= i n) (cond
(= i n)
(let [next-cur (if (= step n) cur (lazy-from (core-drop (- step n) cur)))] (let [next-cur (if (= step n) cur (lazy-from (core-drop (- step n) cur)))]
@[(tuple/slice (tuple ;part)) (pstep next-cur)]) @[(tuple/slice (tuple ;part)) (pstep next-cur)])
# partial final partition: pad it (last partition, then stop)
(and has-pad (> i 0))
(do
(each x (realize-for-iteration pad)
(when (< (length part) n) (array/push part x)))
@[(tuple/slice (tuple ;part)) (fn [] nil)])
nil))))) nil)))))
(make-lazy-seq (pstep (lazy-from coll))))) (make-lazy-seq (pstep (lazy-from coll)))))
@ -1775,6 +1785,44 @@
(prin "\n") (prin "\n")
nil) nil)
(defn core-newline [] (prin "\n") nil)
# Clojure 1.11 string->scalar parsers: nil on malformed input, throw on a
# non-string. Validation is strict (scan-number alone accepts 0x10 etc.).
(defn- parse-arg-str [s who]
(if (or (string? s) (buffer? s)) (string s)
(error (string who " requires a string, got " (type s)))))
(defn core-parse-long [s]
(def str* (parse-arg-str s "parse-long"))
(def n (length str*))
(def start (if (and (> n 0) (or (= 43 (in str* 0)) (= 45 (in str* 0)))) 1 0))
(if (and (> n start)
(do (var ok true)
(for i start n (when (or (< (in str* i) 48) (> (in str* i) 57)) (set ok false)))
ok))
(scan-number str*)
nil))
(defn core-parse-double [s]
(def str* (parse-arg-str s "parse-double"))
# strict float shape: [+-] digits [. digits] [eE [+-] digits] — at least one
# digit overall; "Infinity"/"-Infinity"/"NaN" accepted like the reference.
(cond
(= str* "Infinity") math/inf
(= str* "-Infinity") (- math/inf)
(= str* "NaN") math/nan
(do
(def pat (peg/compile ~(sequence (opt (set "+-")) (choice (sequence (some :d) (opt (sequence "." (any :d)))) (sequence "." (some :d))) (opt (sequence (set "eE") (opt (set "+-")) (some :d))) -1)))
(if (peg/match pat str*) (scan-number str*) nil))))
(defn core-parse-boolean [s]
(def str* (parse-arg-str s "parse-boolean"))
(case str* "true" true "false" false nil))
# Host time source for the `time` macro (monotonic, milliseconds).
(defn core-current-time-ms [] (* 1000 (os/clock :monotonic)))
# Capture *out*: run thunk with Janet's :out dynamic bound to a buffer, so all # Capture *out*: run thunk with Janet's :out dynamic bound to a buffer, so all
# print/println/pr/prn output (which go through `prin` -> (dyn :out)) is collected # print/println/pr/prn output (which go through `prin` -> (dyn :out)) is collected
# and returned as a string. The with-out-str macro (overlay) wraps a body thunk. # and returned as a string. The with-out-str macro (overlay) wraps a body thunk.
@ -2942,6 +2990,11 @@
"hash-unordered-coll" core-hash-unordered-coll "hash-unordered-coll" core-hash-unordered-coll
"prefers" core-prefers "prefers" core-prefers
"random-uuid" core-random-uuid "random-uuid" core-random-uuid
"parse-long" core-parse-long
"parse-double" core-parse-double
"parse-boolean" core-parse-boolean
"newline" core-newline
"current-time-ms" core-current-time-ms
"parse-uuid" core-parse-uuid "parse-uuid" core-parse-uuid
"interpose" core-interpose "interpose" core-interpose
"mapcat" core-mapcat "mapcat" core-mapcat

View file

@ -784,13 +784,14 @@
(defn require-impl (defn require-impl
"(require '[ns :as a :refer [...]] ...) — load + alias/refer each spec. A fn, so "(require '[ns :as a :refer [...]] ...) — load + alias/refer each spec. A fn, so
the args (quoted specs) arrive evaluated. Varargs (Clojure-compatible); each spec the args (quoted specs) arrive evaluated. Varargs (Clojure-compatible); each spec
is a vector. eval-require does the namespace load + alias/refer into current-ns." is a vector [ns & opts] or a bare ns symbol (treated as [ns])."
[ctx & specs] [ctx & specs]
(each spec specs (each spec specs
(let [s (if (pvec? spec) (pv->array spec) spec)] (let [s (if (pvec? spec) (pv->array spec) spec)]
(if (and (indexed? s) (> (length s) 0)) (cond
(eval-require ctx s) (and (indexed? s) (> (length s) 0)) (eval-require ctx s)
(error "require expects a vector spec")))) (and (struct? s) (= :symbol (s :jolt/type))) (eval-require ctx @[s])
(error "require expects a vector spec or a namespace symbol"))))
nil) nil)
(defn in-ns-impl (defn in-ns-impl
@ -1102,15 +1103,47 @@
# Reader / expansion as plain fns: read-string parses one form; macroexpand-1 # Reader / expansion as plain fns: read-string parses one form; macroexpand-1
# expands a (quoted, already-evaluated) call form once via its macro var. # expands a (quoted, already-evaluated) call form once via its macro var.
(ns-intern core "read-string" (fn [s] (parse-string s))) (ns-intern core "read-string" (fn [s] (parse-string s)))
(ns-intern core "macroexpand-1" (def expand-1 (fn [the-form]
(if (and (array? the-form) (> (length the-form) 0)
(struct? (first the-form)) (= :symbol ((first the-form) :jolt/type)))
(let [v (resolve-var ctx @{} (first the-form))]
(if (and v (var-macro? v))
(apply (var-get v) (tuple/slice the-form 1))
the-form))
the-form)))
(ns-intern core "macroexpand-1" expand-1)
# macroexpand: expand repeatedly until the head is no longer a macro (the
# form's SUBFORMS are not expanded, matching Clojure).
(ns-intern core "macroexpand"
(fn [the-form] (fn [the-form]
(if (and (array? the-form) (> (length the-form) 0) (var cur the-form)
(struct? (first the-form)) (= :symbol ((first the-form) :jolt/type))) (var nxt (expand-1 cur))
(let [v (resolve-var ctx @{} (first the-form))] (while (not= cur nxt) (set cur nxt) (set nxt (expand-1 cur)))
(if (and v (var-macro? v)) cur))
(apply (var-get v) (tuple/slice the-form 1)) # alias/ns-unalias: alias bookkeeping is currently split (require :as writes
the-form)) # the string-keyed :imports table that resolution reads; :aliases is the
the-form))) # introspection table ns-aliases reads) — write/remove BOTH until unified.
(ns-intern core "alias"
(fn [alias-sym ns-sym]
(def cur (ctx-find-ns ctx (ctx-current-ns ctx)))
(ns-import cur (alias-sym :name) (ns-sym :name))
(ns-add-alias cur alias-sym (ctx-find-ns ctx (ns-sym :name)))
nil))
(ns-intern core "ns-unalias"
(fn [ns-d alias-sym]
(def ns (ns-or-current ns-d))
(put (ns :imports) (alias-sym :name) nil)
(put (ns :aliases) alias-sym nil)
nil))
# ns-publics: {symbol -> var} (jolt has no private vars, so publics = interns).
# Keys are symbol structs (value-hashed), matching Clojure's symbol keys.
(ns-intern core "ns-publics"
(fn [&opt ns-d]
(def ns (ns-or-current ns-d))
(var m (make-phm))
(loop [[nm v] :pairs (ns :mappings)]
(set m (phm-assoc m {:jolt/type :symbol :ns nil :name nm} v)))
m))
core) core)
# Dispatch a special form by its string name. # Dispatch a special form by its string name.

View file

@ -43,9 +43,9 @@
# Raised 4004 -> 4034 / clean 66 -> 67 porting partition-all + repeatedly to the # Raised 4004 -> 4034 / clean 66 -> 67 porting partition-all + repeatedly to the
# overlay, which required fixing two leniencies (a char is not callable; take # overlay, which required fixing two leniencies (a char is not callable; take
# validates its count) — correct beyond those fns, so the suite rose broadly. # validates its count) — correct beyond those fns, so the suite rose broadly.
(def baseline-pass 4081) (def baseline-pass 4324)
# A file is "clean" when it ran with zero failures AND zero errors. # A file is "clean" when it ran with zero failures AND zero errors.
(def baseline-clean-files 72) (def baseline-clean-files 78)
# Per-file wall-clock budget (seconds). Normal files finish in well under 1s, so # Per-file wall-clock budget (seconds). Normal files finish in well under 1s, so
# this normally only fires on genuinely-infinite-sequence hangs. It's an env var # this normally only fires on genuinely-infinite-sequence hangs. It's an env var
# (JOLT_SUITE_TIMEOUT) so CI — whose runners are slower than a dev machine — can # (JOLT_SUITE_TIMEOUT) so CI — whose runners are slower than a dev machine — can

View file

@ -177,6 +177,22 @@
["parse-uuid case =" "true" "(= (parse-uuid \"b6883c0a-0342-4007-9966-bc2dfa6b109e\") (parse-uuid \"B6883C0A-0342-4007-9966-BC2DFA6B109E\"))"] ["parse-uuid case =" "true" "(= (parse-uuid \"b6883c0a-0342-4007-9966-bc2dfa6b109e\") (parse-uuid \"B6883C0A-0342-4007-9966-BC2DFA6B109E\"))"]
["parse-uuid bad nil" "nil" "(parse-uuid \"df0993\")"] ["parse-uuid bad nil" "nil" "(parse-uuid \"df0993\")"]
["uuid as map key" ":v" "(get {(parse-uuid \"b6883c0a-0342-4007-9966-bc2dfa6b109e\") :v} (parse-uuid \"b6883c0a-0342-4007-9966-bc2dfa6b109e\"))"] ["uuid as map key" ":v" "(get {(parse-uuid \"b6883c0a-0342-4007-9966-bc2dfa6b109e\") :v} (parse-uuid \"b6883c0a-0342-4007-9966-bc2dfa6b109e\"))"]
### ---- 1.11 additions + ns fns (spec 35-var batch A) ----
["parse-long" "42" "(parse-long \"42\")"]
["parse-long bad" "nil" "(parse-long \"4.2\")"]
["parse-double" "1500.0" "(parse-double \"1.5e3\")"]
["parse-boolean" "true" "(parse-boolean \"true\")"]
["update-keys" "{\"a\" 1}" "(update-keys {:a 1} name)"]
["update-vals" "{:a 2}" "(update-vals {:a 1} inc)"]
["partitionv pad" "[[1 2] [3 :p]]" "(partitionv 2 2 [:p] [1 2 3])"]
["partition pad" "[[0 1 2 3] [4 5 6 7] [8 9 :a]]" "(partition 4 4 [:a] (range 10))"]
["splitv-at" "[[1 2] [3 4]]" "(splitv-at 2 [1 2 3 4])"]
["with-redefs" "[42 1]" "(do (defn cwr [] 1) [(with-redefs [cwr (fn [] 42)] (cwr)) (cwr)])"]
["time returns value" "3" "(time (+ 1 2))"]
["macroexpand" "true" "(= (quote if) (first (macroexpand (quote (when-not false 1)))))"]
["require bare symbol" "\"a,b\"" "(do (require (quote clojure.string)) (clojure.string/join \",\" [\"a\" \"b\"]))"]
["ns-publics lookup" "true" "(do (def cnp 7) (some? (get (ns-publics (quote user)) (quote cnp))))"]
["macroexpand-1 when" "2" "(count (rest (macroexpand-1 (quote (when true 1)))))"] ["macroexpand-1 when" "2" "(count (rest (macroexpand-1 (quote (when true 1)))))"]
### ---- HIGH: aliased namespace calls ---- ### ---- HIGH: aliased namespace calls ----

View file

@ -72,3 +72,16 @@
["binding rebinds" "99" "(do (def ^:dynamic *bx* 10) (binding [*bx* 99] *bx*))"] ["binding rebinds" "99" "(do (def ^:dynamic *bx* 10) (binding [*bx* 99] *bx*))"]
["binding restores" "10" "(do (def ^:dynamic *by* 10) (binding [*by* 99] *by*) *by*)"] ["binding restores" "10" "(do (def ^:dynamic *by* 10) (binding [*by* 99] *by*) *by*)"]
["binding seen by fn" "7" "(do (def ^:dynamic *bz* 0) (defn rdz [] *bz*) (binding [*bz* 7] (rdz)))"]) ["binding seen by fn" "7" "(do (def ^:dynamic *bz* 0) (defn rdz [] *bz*) (binding [*bz* 7] (rdz)))"])
# time returns the body value (the timing line goes to *out*); with-redefs
# temporarily rebinds var roots and restores on exit (even on throw);
# macroexpand expands repeatedly until the head is no longer a macro.
(defspec "macros / time, with-redefs, macroexpand"
["time returns value" "3" "(time (+ 1 2))"]
["with-redefs rebinds" "42" "(do (defn wr-f [] 1) (with-redefs [wr-f (fn [] 42)] (wr-f)))"]
["with-redefs restores" "1" "(do (defn wr-g [] 1) (with-redefs [wr-g (fn [] 42)]) (wr-g))"]
["with-redefs restores on throw" "1"
"(do (defn wr-h [] 1) (try (with-redefs [wr-h (fn [] 42)] (throw (ex-info \"x\" {}))) (catch :default e nil)) (wr-h))"]
["with-redefs-fn" "42" "(do (defn wr-i [] 1) (with-redefs-fn {(var wr-i) (fn [] 42)} (fn [] (wr-i))))"]
["macroexpand full" "true" "(let [e (macroexpand (quote (when-not false 1)))] (= (quote if) (first e)))"]
["macroexpand non-macro" "[1 2]" "(macroexpand (quote [1 2]))"])

View file

@ -144,3 +144,14 @@
["dissoc keeps nil" "true" "(contains? (dissoc {:a nil :b 1} :b) :a)"] ["dissoc keeps nil" "true" "(contains? (dissoc {:a nil :b 1} :b) :a)"]
["reduce-kv sees nil" "true" "(= #{:a :b} (reduce-kv (fn [acc k v] (conj acc k)) #{} {:a nil :b 2}))"] ["reduce-kv sees nil" "true" "(= #{:a :b} (reduce-kv (fn [acc k v] (conj acc k)) #{} {:a nil :b 2}))"]
["nil-free stays fast" "true" "(= {:a 1 :b 2} {:b 2 :a 1})"]) ["nil-free stays fast" "true" "(= {:a 1 :b 2} {:b 2 :a 1})"])
# Clojure 1.11 map transformers.
(defspec "map / update-keys & update-vals (1.11)"
["update-keys" "{\"a\" 1, \"b\" 2}" "(update-keys {:a 1 :b 2} name)"]
["update-keys empty" "{}" "(update-keys {} inc)"]
["update-keys nil" "{}" "(update-keys nil str)"]
["update-keys collide last wins" "1" "(count (update-keys {:a 1 :b 2} (fn [_] :k)))"]
["update-vals" "{:a 2, :b 3}" "(update-vals {:a 1 :b 2} inc)"]
["update-vals empty" "{}" "(update-vals {} inc)"]
["update-vals nil" "{}" "(update-vals nil inc)"]
["update-vals keeps keys" "[:a :b]" "(sort (keys (update-vals {:a 1 :b 2} inc)))"])

View file

@ -35,3 +35,10 @@
["require clojure.walk" "{:a 2}" "(do (require '[clojure.walk :as w]) (w/postwalk (fn [x] (if (number? x) (inc x) x)) {:a 1}))"] ["require clojure.walk" "{:a 2}" "(do (require '[clojure.walk :as w]) (w/postwalk (fn [x] (if (number? x) (inc x) x)) {:a 1}))"]
["walk keywordize-keys" "{:a 1}" "(do (require '[clojure.walk :as w]) (w/keywordize-keys {\"a\" 1}))"] ["walk keywordize-keys" "{:a 1}" "(do (require '[clojure.walk :as w]) (w/keywordize-keys {\"a\" 1}))"]
["walk stringify-keys" "true" "(do (require '[clojure.walk :as w]) (= {\"a\" 1} (w/stringify-keys {:a 1})))"]) ["walk stringify-keys" "true" "(do (require '[clojure.walk :as w]) (= {\"a\" 1} (w/stringify-keys {:a 1})))"])
(defspec "namespaces / alias, ns-unalias, ns-publics"
["alias + use" "\"1,2\"" "(do (require (quote clojure.string)) (alias (quote st) (quote clojure.string)) (st/join \",\" [1 2]))"]
["ns-unalias removes" "true"
"(do (require (quote clojure.string)) (alias (quote st2) (quote clojure.string)) (ns-unalias (quote user) (quote st2)) (nil? (get (ns-aliases (quote user)) (quote st2))))"]
["ns-publics has var" "true" "(do (def npv 1) (some? (get (ns-publics (quote user)) (quote npv))))"]
["newline returns nil" "nil" "(newline)"])

View file

@ -134,3 +134,26 @@
["rand n in [0,n)" "true" "(let [r (rand 10)] (and (>= r 0) (< r 10)))"] ["rand n in [0,n)" "true" "(let [r (rand 10)] (and (>= r 0) (< r 10)))"]
["rand-nth member" "true" "(contains? #{:a :b :c} (rand-nth [:a :b :c]))"] ["rand-nth member" "true" "(contains? #{:a :b :c} (rand-nth [:a :b :c]))"]
["rand-nth single" ":x" "(rand-nth [:x])"]) ["rand-nth single" ":x" "(rand-nth [:x])"])
# Clojure 1.11 string->scalar parsers: nil on malformed, throw on non-string.
(defspec "numbers / parse fns (1.11)"
["parse-long" "42" "(parse-long \"42\")"]
["parse-long negative" "-7" "(parse-long \"-7\")"]
["parse-long plus" "7" "(parse-long \"+7\")"]
["parse-long float nil" "nil" "(parse-long \"1.5\")"]
["parse-long hex nil" "nil" "(parse-long \"0x10\")"]
["parse-long empty nil" "nil" "(parse-long \"\")"]
["parse-long junk nil" "nil" "(parse-long \"12ab\")"]
["parse-long throws" :throws "(parse-long 42)"]
["parse-double" "1.5" "(parse-double \"1.5\")"]
["parse-double int" "4.0" "(parse-double \"4\")"]
["parse-double sci" "1500.0" "(parse-double \"1.5e3\")"]
["parse-double neg" "-0.5" "(parse-double \"-0.5\")"]
["parse-double junk" "nil" "(parse-double \"abc\")"]
["parse-double trail" "nil" "(parse-double \"1.5x\")"]
["parse-double throws" :throws "(parse-double :k)"]
["parse-boolean true" "true" "(parse-boolean \"true\")"]
["parse-boolean false" "false" "(parse-boolean \"false\")"]
["parse-boolean case" "nil" "(parse-boolean \"True\")"]
["parse-boolean junk" "nil" "(parse-boolean \"yes\")"]
["parse-boolean throws" :throws "(parse-boolean true)"])

View file

@ -261,3 +261,15 @@
["dedupe consecutive" "[1 2 3 1]" "(dedupe [1 1 2 2 3 1 1])"] ["dedupe consecutive" "[1 2 3 1]" "(dedupe [1 1 2 2 3 1 1])"]
["dedupe empty" "[]" "(dedupe [])"] ["dedupe empty" "[]" "(dedupe [])"]
["dedupe no dups" "[1 2 3]" "(dedupe [1 2 3])"]) ["dedupe no dups" "[1 2 3]" "(dedupe [1 2 3])"])
# Clojure 1.11 vector-returning partition/split variants.
(defspec "seq / partitionv & splitv-at (1.11)"
["partitionv" "[[1 2] [3 4]]" "(partitionv 2 [1 2 3 4 5])"]
["partitionv elems are vectors" "true" "(every? vector? (partitionv 2 [1 2 3 4]))"]
["partitionv step" "[[1 2] [3 4]]" "(partitionv 2 2 [1 2 3 4 5])"]
["partitionv pad" "[[1 2] [3 :p]]" "(partitionv 2 2 [:p] [1 2 3])"]
["partitionv-all" "[[1 2] [3]]" "(partitionv-all 2 [1 2 3])"]
["partitionv-all vectors" "true" "(every? vector? (partitionv-all 2 [1 2 3]))"]
["splitv-at" "[[1 2] [3 4]]" "(splitv-at 2 [1 2 3 4])"]
["splitv-at first is vector" "true" "(vector? (first (splitv-at 2 [1 2 3])))"]
["splitv-at past end" "[[1 2] []]" "(splitv-at 5 [1 2])"])