diff --git a/host/chez/atoms.ss b/host/chez/atoms.ss index 816d6de..da634fe 100644 --- a/host/chez/atoms.ss +++ b/host/chez/atoms.ss @@ -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) diff --git a/host/chez/post-prelude.ss b/host/chez/post-prelude.ss index 387895d..4f42e6e 100644 --- a/host/chez/post-prelude.ss +++ b/host/chez/post-prelude.ss @@ -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). diff --git a/test/chez/_atomwatch.janet b/test/chez/_atomwatch.janet new file mode 100644 index 0000000..45a4ddc --- /dev/null +++ b/test/chez/_atomwatch.janet @@ -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)) diff --git a/test/chez/run-corpus-prelude.janet b/test/chez/run-corpus-prelude.janet index 69ec7ca..8fb30be 100644 --- a/test/chez/run-corpus-prelude.janet +++ b/test/chez/run-corpus-prelude.janet @@ -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)))