jolt/test/spec/futures-spec.janet
Yogthos 4a1a9e3aec core: lazy realization is shared across walks (once-only effects); pmap family
Every walk over a lazy seq created FRESH wrapper tables around the shared
rest-thunks (ls-rest, ls-seq/ls-count, realize-for-iteration, the printers,
reduce — each had its own make-lazy-seq loop), so independent walks re-ran
the thunks: side effects duplicated, and a doall'd seq of futures was
re-spawned serially by the deref walk. Every walker now goes through
ls-rest-cached, which memoizes the rest wrapper on its node — thunks run
exactly once, as in Clojure. Costs ~10% on walk-heavy benches (the per-node
cache get/put — Clojure's LazySeq pays the same); net still -9% vs the
pre-linear-walks baseline. Three regression rows pin once-only effects and
value stability across walks.

On top of that: pmap/pcalls/pvalues (jolt-oeu) over the real-thread futures
— spawn-all-then-deref (the once-only fix is what makes the doall actually
mean that), snapshot semantics documented, multi-coll arity via the
canonical vector-zip. System/currentTimeMillis + nanoTime land as System
statics (the realtime clock — os/time is whole seconds, which quantized
every elapsed measurement to 1000ms). Seven pmap rows incl. a generous-
margin parallelism check (4 x 200ms sleeps under 700ms after warmup).
2026-06-10 19:14:49 -04:00

64 lines
4 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))))"])
# pmap/pcalls/pvalues over the real-thread futures: spawn-all-then-deref, so
# independent work overlaps; snapshot semantics (pure fns only). The wall-time
# row uses generous margins (4 x 150ms in parallel vs 600ms serial).
(defspec "clojure.core / pmap family"
["pmap values in order" "[2 3 4]" "(vec (pmap inc [1 2 3]))"]
["pmap multi-coll" "[5 7 9]" "(vec (pmap + [1 2 3] [4 5 6]))"]
["pmap empty" "()" "(pmap inc [])"]
["pmap is parallel" "true"
"(do (deref (future :warmup)) (let [t0 (System/currentTimeMillis)] (dorun (pmap (fn [_] (Thread/sleep 200)) [1 2 3 4])) (< (- (System/currentTimeMillis) t0) 700)))"]
["pcalls" "[1 2]" "(vec (pcalls (fn [] 1) (fn [] 2)))"]
["pvalues" "[3 7]" "(vec (pvalues (+ 1 2) (+ 3 4)))"]
["snapshot semantics" "0" "(let [a (atom 0)] (dorun (pmap (fn [_] (swap! a inc)) [1 2])) (deref a))"])