Chez Phase 2 (inc S): atom watches + validators (jolt-mn9o)

The Chez atom record gains watches (an alist of key->fn) and validator
slots. swap!/reset! now validate the candidate value before storing and
notify watches after, in the seed's order (core_refs.janet) — the watch fn
is called (key ref old new). compare-and-set!/swap-vals!/reset-vals! route
through reset!/swap! so they validate + notify too.

add-watch/remove-watch/set-validator!/get-validator are native here and
re-asserted in post-prelude.ss: the clojure.core overlay implements them via
jolt.host/ref-put! on (get atom :watches), a Janet-table mutation a Chez atom
record can't answer, so its def-var! would otherwise clobber these. set-
validator! validates the current value immediately (Clojure throws if already
invalid).

Parity 2150 -> 2154, 0 new divergences. New test/chez/_atomwatch.janet 10/10.

The (class x) native and the clojure.walk port were explored this increment
but deferred: class alone makes the String-class-token corpus cases emit-and-
run with a wrong value (the bare token doesn't resolve yet) — filed jolt-13zk
to land the native together with token resolution; clojure.walk is blocked on
list? + map-entry-as-vector (jolt-75sv).
This commit is contained in:
Yogthos 2026-06-19 04:33:07 -04:00
parent 1f96351acb
commit b7864100cb
4 changed files with 135 additions and 12 deletions

View file

@ -11,11 +11,36 @@
;; self-sufficient for atoms without the full prelude (the overlay versions, when
;; the full prelude loads, override these but compose the same native kernel).
(define-record-type jolt-atom (fields (mutable val)) (nongenerative jolt-atom-v1))
;; watches is an alist of (key . watch-fn); validator is a jolt fn or jolt-nil.
;; The overlay's add-watch/set-validator! drive these via jolt.host/ref-put! on a
;; Janet table, which a Chez atom record is not — so the peripheral ops + the
;; notify/validate behaviour live natively here, and post-prelude.ss re-asserts
;; them over the overlay's def-var! (jolt-mn9o).
(define-record-type jolt-atom
(fields (mutable val) (mutable watches) (mutable validator))
(nongenerative jolt-atom-v2))
;; (atom init) — extra :meta/:validator opts are accepted and ignored for now
;; (watches/validators are overlay features layered via jolt.host/ref-put!).
(define (jolt-atom-new v . _opts) (make-jolt-atom v))
;; (atom init) / (atom init :validator f :meta m): scan the trailing keyword opts
;; for :validator (the only one with runtime behaviour; :meta is accepted/ignored).
(define (jolt-atom-new v . opts)
(let loop ((o opts) (validator jolt-nil))
(cond
((or (null? o) (null? (cdr o))) (make-jolt-atom v '() validator))
((and (keyword-t? (car o)) (string=? (keyword-t-name (car o)) "validator"))
(loop (cddr o) (cadr o)))
(else (loop (cddr o) validator)))))
;; validate a candidate value: a non-nil validator that returns falsey rejects.
(define (jolt-atom-validate a v)
(let ((vf (jolt-atom-validator a)))
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf v)))
(error #f "Invalid reference state"))))
;; notify each watch (k ref old new), in insertion order (alist is reverse-built,
;; so walk it reversed to match add order — matches the seed's :pairs iteration).
(define (jolt-atom-notify a old new)
(for-each (lambda (kv) (jolt-invoke (cdr kv) (car kv) a old new))
(reverse (jolt-atom-watches a))))
;; deref reads an atom; it also unwraps a `reduced` (Clojure @(reduced x) => x,
;; which the overlay's `unreduced` relies on). The reduced record is in seq.ss.
@ -25,31 +50,61 @@
((jolt-reduced? x) (jolt-reduced-val x))
(else (error #f "deref: unsupported reference type" x))))
;; (swap! a f arg*) -> (reset! a (f @a arg*)); f is invoked through jolt-invoke
;; (a jolt fn value, keyword, or invokable collection).
;; (swap! a f arg*) -> (reset! a (f @a arg*)); f is invoked through jolt-invoke.
;; Validate the new value BEFORE storing, notify watches AFTER (the seed order).
(define (jolt-swap! a f . args)
(let ((nv (apply jolt-invoke f (jolt-atom-val a) args)))
(let* ((old (jolt-atom-val a))
(nv (apply jolt-invoke f old args)))
(jolt-atom-validate a nv)
(jolt-atom-val-set! a nv)
(jolt-atom-notify a old nv)
nv))
(define (jolt-reset! a v) (jolt-atom-val-set! a v) v)
(define (jolt-reset! a v)
(let ((old (jolt-atom-val a)))
(jolt-atom-validate a v)
(jolt-atom-val-set! a v)
(jolt-atom-notify a old v)
v))
(define (jolt-compare-and-set! a oldv newv)
(if (jolt= (jolt-atom-val a) oldv)
(begin (jolt-atom-val-set! a newv) #t)
(begin (jolt-reset! a newv) #t)
#f))
(define (jolt-swap-vals! a f . args)
(let* ((old (jolt-atom-val a))
(nv (apply jolt-invoke f old args)))
(jolt-atom-val-set! a nv)
(jolt-reset! a nv)
(jolt-vector old nv)))
(define (jolt-reset-vals! a v)
(let ((old (jolt-atom-val a)))
(jolt-atom-val-set! a v)
(jolt-reset! a v)
(jolt-vector old v)))
;; --- watches / validators (jolt-mn9o) ---------------------------------------
;; add-watch interns (key . fn) (replacing any existing key, keeping order);
;; remove-watch drops it; both return the atom. set-validator! installs a
;; validator and validates the CURRENT value immediately (Clojure throws if it's
;; already invalid); get-validator reads the slot.
(define (jolt-add-watch a key f)
(jolt-atom-watches-set! a
(cons (cons key f)
(remp (lambda (kv) (jolt=2 (car kv) key)) (jolt-atom-watches a))))
a)
(define (jolt-remove-watch a key)
(jolt-atom-watches-set! a
(remp (lambda (kv) (jolt=2 (car kv) key)) (jolt-atom-watches a)))
a)
(define (jolt-set-validator! a f)
(let ((vf (if (jolt-nil? f) jolt-nil f)))
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf (jolt-atom-val a))))
(error #f "Invalid reference state"))
(jolt-atom-validator-set! a vf)
jolt-nil))
(define (jolt-get-validator a) (jolt-atom-validator a))
(def-var! "clojure.core" "atom" jolt-atom-new)
(def-var! "clojure.core" "deref" jolt-deref)
(def-var! "clojure.core" "swap!" jolt-swap!)
@ -58,3 +113,9 @@
(def-var! "clojure.core" "swap-vals!" jolt-swap-vals!)
(def-var! "clojure.core" "reset-vals!" jolt-reset-vals!)
(def-var! "clojure.core" "atom?" jolt-atom?)
;; peripheral ops: the overlay (20-coll) re-defs these over jolt.host/ref-put!,
;; which fails on a Chez atom record — post-prelude.ss re-asserts the natives.
(def-var! "clojure.core" "add-watch" jolt-add-watch)
(def-var! "clojure.core" "remove-watch" jolt-remove-watch)
(def-var! "clojure.core" "set-validator!" jolt-set-validator!)
(def-var! "clojure.core" "get-validator" jolt-get-validator)

View file

@ -13,6 +13,13 @@
;; override.)
(def-var! "clojure.core" "char?" jolt-char-pred?)
(def-var! "clojure.core" "atom?" jolt-atom?)
;; atom watches/validators: the overlay drives these via jolt.host/ref-put! on a
;; Janet table (get a :watches), which a Chez atom record is not — re-assert the
;; native versions (defined in atoms.ss), and swap!/reset! notify+validate there.
(def-var! "clojure.core" "add-watch" jolt-add-watch)
(def-var! "clojure.core" "remove-watch" jolt-remove-watch)
(def-var! "clojure.core" "set-validator!" jolt-set-validator!)
(def-var! "clojure.core" "get-validator" jolt-get-validator)
;; volatiles: a Chez volatile is a jvol record, but the overlay vreset!/vswap!/
;; volatile? drive it via jolt.host/ref-put!+get / :jolt/type (tagged-table only).
;; Override with the native versions (defined in natives-xform.ss).

View file

@ -0,0 +1,51 @@
# jolt-mn9o — atom watches + validators on Chez. The Chez atom record carries
# watches (alist) + validator slots; swap!/reset! validate-then-set-then-notify;
# add-watch/remove-watch/set-validator!/get-validator are native (post-prelude.ss
# re-asserts them over the overlay's ref-put!-based versions). Oracle = build/jolt.
#
# janet test/chez/_atomwatch.janet
(def jolt-bin (or (os/getenv "JOLT_BIN") "bin/jolt-chez"))
# [expr throws?] — when throws? the case is expected to exit non-zero on both
# (the corpus :throws contract; the exception message text may differ).
(def cases
[["(let [a (atom 0) seen (atom 0)] (add-watch a :k (fn [k r o n] (reset! seen 1))) (reset! a 5) @seen)" false]
["(let [a (atom 0) seen (atom 0)] (add-watch a :k (fn [k r o n] (swap! seen inc))) (remove-watch a :k) (reset! a 5) @seen)" false]
["(let [a (atom 0) log (atom [])] (add-watch a :k (fn [k r o n] (swap! log conj [o n]))) (reset! a 1) (reset! a 2) @log)" false]
["(let [a (atom 0)] (set-validator! a number?) (reset! a 5) @a)" false]
["(let [a (atom 5)] (set-validator! a pos?) (swap! a inc) @a)" false]
["(let [a (atom 0)] (set-validator! a number?) (fn? (get-validator a)))" false]
["(let [a (atom 0)] (nil? (get-validator a)))" false]
["(let [a (atom 0)] (set-validator! a pos?) (reset! a -1))" true]
["(let [a (atom 5)] (set-validator! a pos?) (swap! a (fn [_] -1)) @a)" true]
["(let [a (atom 0) c (atom 0)] (add-watch a :k (fn [k r o n] (swap! c inc))) (swap! a inc) (swap! a inc) @c)" false]])
(defn run-capture [bin expr]
(def proc (os/spawn [bin "-e" expr] :p {:out :pipe :err :pipe}))
(def out (ev/read (proc :out) 0x100000))
(def err (ev/read (proc :err) 0x100000))
(def code (os/proc-wait proc))
(def lines (filter (fn [l] (not (empty? l)))
(string/split "\n" (string/trim (if out (string out) "")))))
[code (if (empty? lines) "" (last lines)) (string/trim (if err (string err) ""))])
(var pass 0)
(def fails @[])
(each [expr throws?] cases
(def [ocode oracle _] (run-capture "build/jolt" expr))
(def [code got err] (run-capture jolt-bin expr))
(cond
throws?
(if (and (not= ocode 0) (not= code 0)) (++ pass)
(array/push fails [expr (string "expected both throw; oracle exit " ocode ", chez exit " code)]))
(not= ocode 0) (array/push fails [expr (string "ORACLE FAILED exit " ocode)])
(not= code 0) (array/push fails [expr (string "exit " code "; err: " err)])
(= got oracle) (++ pass)
(array/push fails [expr (string "want `" oracle "`, got `" got "`")])))
(printf "\n_atomwatch parity [%s]: %d/%d passed" jolt-bin pass (length cases))
(when (> (length fails) 0)
(printf "%d FAIL(s):" (length fails))
(each [e m] fails (printf " FAIL %s\n %s" e m)))
(flush)
(os/exit (if (empty? fails) 0 1))

View file

@ -240,8 +240,12 @@
# map member dispatch, and the seed's universal object-methods getMessage/getCause/
# toString/hashCode/equals. Also added getMessage/getLocalizedMessage/equals to the
# string method surface so a thrown string / Exception. ctor answers .getMessage) 2150.
# jolt-mn9o (atom watches/validators — the Chez atom record carries watches/
# validator slots; swap!/reset! validate-then-set-then-notify in seed order;
# add-watch/remove-watch/set-validator!/get-validator are native and re-asserted
# in post-prelude.ss over the overlay's ref-put!-on-a-Janet-table versions) 2154.
# Strided runs scale down.
(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_PRELUDE_FLOOR") "2150")))
(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_PRELUDE_FLOOR") "2154")))
(def floor (if (os/getenv "JOLT_CORPUS_LIMIT") 0 base-floor))
(when (or (> (length diverged) 0) (< pass floor))
(printf "REGRESSION: pass %d < floor %d or %d new divergence(s)" pass floor (length diverged)))