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.
51 lines
3.1 KiB
Text
51 lines
3.1 KiB
Text
# Specification: clojure.core futures on Janet OS threads (ev/thread).
|
|
#
|
|
# A `future` runs its body on a *real* OS thread (ev/thread), so it can use a
|
|
# second core for CPU-bound work — unlike the cooperatively-scheduled go blocks.
|
|
# Because Janet threads have separate heaps, the body and its captured state are
|
|
# MARSHALLED (copied) to the worker thread and the result is marshalled back: a
|
|
# future sees a snapshot of captured state and communicates only via its return
|
|
# value (mutations to captured atoms do NOT propagate back). `deref`/`@` blocks
|
|
# (parks) until the worker finishes; the result is cached for later derefs.
|
|
(use ../support/harness)
|
|
|
|
(defspec "clojure.core / futures — deref"
|
|
["future + deref" "3" "(deref (future (+ 1 2)))"]
|
|
["@ reader macro derefs" "42" "@(future (* 6 7))"]
|
|
["future returns collection" "[2 3 4]" "(deref (future (mapv inc [1 2 3])))"]
|
|
["future returns a map" "{:a 1}" "(deref (future {:a 1}))"]
|
|
["deref is cached/idempotent" "[2 2]" "(let [f (future (+ 1 1))] [(deref f) (deref f)])"]
|
|
["timed deref of ready future" "42" "(let [f (future 42)] (deref f) (deref f 1000 :nope))"]
|
|
["body error re-raised on deref" :throws "(deref (future (throw \"boom\")))"]
|
|
# Thread/sleep parks the WORKER's own event loop (each future thread has one),
|
|
# so a sleeping future doesn't block the parent — and timed deref can fire.
|
|
["timed deref times out" ":timed-out" "(deref (future (do (Thread/sleep 300) :late)) 10 :timed-out)"]
|
|
["Thread/sleep in body" ":slept" "(deref (future (do (Thread/sleep 5) :slept)))"]
|
|
["timed-out future still completes" ":late"
|
|
"(let [f (future (do (Thread/sleep 30) :late))] (deref f 5 :early) (deref f))"])
|
|
|
|
(defspec "clojure.core / futures — predicates"
|
|
["future? true" "true" "(future? (future 1))"]
|
|
["future? false" "false" "(future? 42)"]
|
|
["future-done? after deref" "true" "(let [f (future 1)] (deref f) (future-done? f))"]
|
|
["realized? after deref" "true" "(let [f (future 1)] (deref f) (realized? f))"]
|
|
# Cancel marks the future done (the worker can't be interrupted, but the
|
|
# future object reflects the cancellation: deref raises, predicates flip).
|
|
["cancel an in-flight future returns true" "true"
|
|
"(let [f (future 1)] (future-cancel f))"]
|
|
["future-cancelled? after cancel" "true"
|
|
"(let [f (future 1)] (future-cancel f) (future-cancelled? f))"]
|
|
["future-done? after cancel" "true"
|
|
"(let [f (future 1)] (future-cancel f) (future-done? f))"]
|
|
["cancel an already-completed future returns false" "false"
|
|
"(let [f (future 1)] (deref f) (future-cancel f))"]
|
|
["future-cancelled? fresh is false" "false"
|
|
"(future-cancelled? (future 1))"])
|
|
|
|
(defspec "clojure.core / futures — snapshot (copy) semantics"
|
|
# The worker thread swaps its *copy* of the atom; the parent's atom is untouched.
|
|
["captured atom is snapshotted, not shared"
|
|
"0" "(let [a (atom 0)] (deref (future (swap! a inc))) @a)"]
|
|
# The future's own return value still reflects the swap on its copy.
|
|
["future sees its own mutation"
|
|
"1" "(let [a (atom 0)] (deref (future (swap! a inc))))"])
|