spec: rows for every untested var (131 -> 0); the probes found five real bugs

test/spec/untested-vars-spec.janet adds 143 rows asserting jolt's documented
behavior for the whole implemented-untested category — primed arithmetic,
the array/aset/coercion stubs, unchecked-*, the chunk family, JVM-shape
stubs (class/bean/proxy/memfn as resolve-only or :throws), ns/REPL
machinery, and the misc seqs. tools/spec_coverage.py now checks each var as
a whole TOKEN in the test sources (call-position-only matching missed *1,
+', ., .., /, and bare transducer refs like cat).

Writing rows from probed truth surfaced five real bugs, all fixed:
- comp with a jolt-IFn stage silently returned nil ((comp seq :content)) —
  raw Janet keyword application is not jolt invoke. comp is the canonical
  overlay defn now (fixed-arity composed fn, so the hot 1-arg path is two
  direct calls); the seed keeps a private td-comp only for the transducer
  machinery. hof bench +9% vs native, the price of correct IFn dispatch.
- extend (the fn) was a nil-expanding stub MACRO shadowing any definition;
  it's a real fn over register-method now, and extends? (a constant-false
  stub) is real over extenders
- (.. x f g) hit the 'ClassName.' constructor branch (a name ending in a
  dot) and died; .. is the canonical threading macro now
- aclone errored on pvecs; ns-interns/ns-imports returned live host tables
  that count/seq reject (now structs)

Thread/sleep + Thread/yield land as Thread statics beside Math/: sleep parks
the WORKER's own event loop (each future thread has one), which makes timed
deref provably fire — futures-spec gains the timeout-fires, sleep-in-body,
and timed-out-future-still-completes rows. The futures impl itself already
ran on real OS threads (ev/spawn-thread + marshalled results); jolt-ejx was
stale.

Dashboard: implemented+tested 433 -> 564 of 694; implemented-untested and
missing-portable are both EMPTY. Gate: jpm exit 0, all tests passed.
This commit is contained in:
Yogthos 2026-06-10 17:52:30 -04:00
parent daab2ab4cc
commit d06b3fe636
8 changed files with 414 additions and 151 deletions

View file

@ -1402,7 +1402,12 @@
# every-pred now lives in the Clojure collection tier (core/20-coll.clj).
(def core-comp
# Public comp lives in the overlay now (20-coll) — its stages can be any jolt
# IFn (keyword/map/set/vector), which raw Janet calls mishandle ((comp seq
# :content) returned nil: janet keyword-apply is not jolt invoke). This
# private composer remains ONLY for the transducer machinery below, where the
# stages are always real fns.
(def- td-comp
(fn [& fs]
(case (length fs)
0 identity
@ -1852,7 +1857,10 @@
# below stay native (they build the mutable backing).
(defn core-aclone [arr]
(if (buffer? arr) (buffer/slice arr) (array/slice arr)))
(cond
(buffer? arr) (buffer/slice arr)
(pvec? arr) (array ;(pv->array arr))
(array/slice arr)))
# Numeric / object arrays: (T-array size) | (T-array size init) | (T-array seq)
(defn- make-num-array [a rest init]
@ -2284,7 +2292,7 @@
(def core-satisfies? (fn [proto-sym obj] false))
(def core-extends? (fn [& args] false))
# extends? is a real overlay fn now (30-macros, over extenders).
(def core-implements? (fn [& args] false))
(def core-type->str (fn [& args] ""))
@ -2419,7 +2427,7 @@
(let [n (length args)
coll (in args (- n 1))
xforms (array/slice args 0 (- n 1))
xform (if (= 0 (length xforms)) (fn [rf] rf) (apply core-comp xforms))]
xform (if (= 0 (length xforms)) (fn [rf] rf) (apply td-comp xforms))]
(core-into (make-vec @[]) xform coll)))
(defn core->Eduction [xform coll] (core-into (make-vec @[]) xform coll))
(defn core-proxy-super [& args] (error "proxy-super: JVM proxies are not supported in Jolt"))
@ -2800,7 +2808,6 @@
"range" core-range
"identity" core-identity
"constantly" core-constantly
"comp" core-comp
"vector" core-vector
"hash-map" core-hash-map
"array-map" core-array-map
@ -2948,7 +2955,6 @@
"Object" core-Object
"make-protocol" core-make-protocol
"satisfies?" core-satisfies?
"extends?" core-extends?
"implements?" core-implements?
"type->str" core-type->str
"volatile!" core-volatile!

View file

@ -417,12 +417,23 @@
"PI" math/pi "E" math/e
"random" (fn [&] (math/random))})
# Thread statics (the JVM shapes portable code actually uses). sleep parks the
# CURRENT thread's event loop — inside a future body that's the worker OS
# thread (ev/spawn-thread gives each worker its own loop), so a sleeping
# future doesn't block the parent.
(def- thread-statics
{"sleep" (fn [ms] (ev/sleep (/ ms 1000)) nil)
"yield" (fn [] (ev/sleep 0) nil)})
(defn- resolve-sym
[ctx bindings sym-s]
(let [name (sym-s :name) ns (sym-s :ns)]
(if (= ns "Math")
(let [v (get math-statics name)]
(if (nil? v) (error (string "Unsupported Math member: Math/" name)) v))
(if (= ns "Thread")
(let [v (get thread-statics name)]
(if (nil? v) (error (string "Unsupported Thread member: Thread/" name)) v))
(if (not (nil? ns))
(let [current-ns (ctx-find-ns ctx (ctx-current-ns ctx))
aliased-ns (ns-import-lookup current-ns ns)
@ -472,8 +483,7 @@
# No implicit Janet fallback (Stage 3): an unresolved
# Clojure symbol is an error. Host access is the explicit
# janet/ prefix above.
(error (string "Unable to resolve symbol: " name))))))))))))))
(error (string "Unable to resolve symbol: " name)))))))))))))))
(defn- parse-arg-names
"Parse a parameter vector, handling & rest args.
Returns {:fixed [names...] :rest name-or-nil :all [names...]}"
@ -1013,9 +1023,11 @@
(ns-intern core "remove-ns" (fn [x] (remove-ns ctx (ns-name-of x))))
(ns-intern core "all-ns" (fn [] (all-ns ctx)))
(ns-intern core "the-ns" (fn [&opt x] (ns-or-current x)))
(ns-intern core "ns-interns" (fn [&opt x] ((ns-or-current x) :mappings)))
# interns/imports return a jolt MAP (struct), not the live host table — so
# count/seq/keys work on them, and callers can't mutate the ns through them.
(ns-intern core "ns-interns" (fn [&opt x] (table/to-struct ((ns-or-current x) :mappings))))
(ns-intern core "ns-aliases" (fn [&opt x] ((ns-or-current x) :aliases)))
(ns-intern core "ns-imports" (fn [&opt x] ((ns-or-current x) :imports)))
(ns-intern core "ns-imports" (fn [&opt x] (table/to-struct ((ns-or-current x) :imports))))
# (ns-resolve ns sym) -> the var or nil. Unqualified syms look in ns's own
# mappings; ns-qualified syms resolve through ns's aliases. (types/ns-resolve
# keys ns-find with the symbol struct instead of its name string, so it never
@ -1678,8 +1690,10 @@
(let [field-name (string/slice sym-name 2)
target (eval-form ctx bindings (in form 1))]
(get target (keyword field-name)))
# Handle ClassName. constructor syntax
(if (and (> (length sym-name) 0) (= (sym-name (- (length sym-name) 1)) 46))
# Handle ClassName. constructor syntax (".." is the member-threading
# macro, not a constructor named ".")
(if (and (> (length sym-name) 1) (not= sym-name "..")
(= (sym-name (- (length sym-name) 1)) 46))
(let [type-name (string/slice sym-name 0 (- (length sym-name) 1))
type-sym {:jolt/type :symbol :ns (first-form :ns) :name type-name}
ctor (eval-form ctx bindings type-sym)