From e7169af9858c3e05da27c6ae9fc76c17aeb0d78a Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 5 May 2026 02:59:15 +0000 Subject: [PATCH] =?UTF-8?q?HS:=20when=20:count=20changes=20=E2=80=94=20sco?= =?UTF-8?q?ped=20watch=20+=20parse-cmd=20feature=20boundary=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-part fix for element-scoped reactive expressions: 1. Parser: add when/bind to parse-cmd's feature-keyword nil set so `... then when X changes ...` is parsed as a new feature, not absorbed into the preceding on-handler body as a (ref "when") expression. 2. Parser: parse-when-feat now recognises local (:var) token type so `when :count changes ...` dispatches to the when-changes branch. 3. Runtime + compiler: hs-scoped-set! now fires hs-scoped-fire-watchers! on change; new hs-scoped-watch! / hs-scoped-fire-watchers! registry; compiler emits (hs-scoped-watch! me name (fn (it) body)) for local expressions in when-changes AST nodes. Co-Authored-By: Claude Sonnet 4.6 --- lib/hyperscript/compiler.sx | 28 ++++++++++++++++------- lib/hyperscript/parser.sx | 32 ++++++++++++++++++--------- lib/hyperscript/runtime.sx | 33 +++++++++++++++++++++++++--- shared/static/wasm/sx/hs-compiler.sx | 28 ++++++++++++++++------- shared/static/wasm/sx/hs-parser.sx | 32 ++++++++++++++++++--------- shared/static/wasm/sx/hs-runtime.sx | 33 +++++++++++++++++++++++++--- 6 files changed, 144 insertions(+), 42 deletions(-) diff --git a/lib/hyperscript/compiler.sx b/lib/hyperscript/compiler.sx index f0322c04..ff8dcb5c 100644 --- a/lib/hyperscript/compiler.sx +++ b/lib/hyperscript/compiler.sx @@ -2356,14 +2356,26 @@ ((= head (quote when-changes)) (let ((expr (nth ast 1)) (body (nth ast 2))) - (if - (and (list? expr) (= (first expr) (quote dom-ref))) - (list - (quote hs-dom-watch!) - (hs-to-sx (nth expr 2)) - (nth expr 1) - (list (quote fn) (list (quote it)) (hs-to-sx body))) - nil))) + (cond + ((and (list? expr) (= (first expr) (quote dom-ref))) + (list + (quote hs-dom-watch!) + (hs-to-sx (nth expr 2)) + (nth expr 1) + (list + (quote fn) + (list (quote it)) + (hs-to-sx body)))) + ((and (list? expr) (= (first expr) (quote local))) + (list + (quote hs-scoped-watch!) + (quote me) + (nth expr 1) + (list + (quote fn) + (list (quote it)) + (hs-to-sx body)))) + (true nil)))) ((= head (quote init)) (list (quote hs-init) diff --git a/lib/hyperscript/parser.sx b/lib/hyperscript/parser.sx index 0be063cb..8406f83b 100644 --- a/lib/hyperscript/parser.sx +++ b/lib/hyperscript/parser.sx @@ -2840,7 +2840,7 @@ ((body (parse-cmd-list))) (match-kw "end") (list (quote view-transition!) using body))))) - ((and (= typ "keyword") (or (= val "on") (= val "init") (= val "def") (= val "behavior") (= val "live"))) + ((and (= typ "keyword") (or (= val "on") (= val "init") (= val "def") (= val "behavior") (= val "live") (= val "when") (= val "bind"))) nil) (true (parse-expr)))))) (define @@ -3060,13 +3060,17 @@ (define plf-skip (fn - () + (depth) (cond ((at-end?) nil) - ((and (= (tp-type) "keyword") (or (= (tp-val) "end") (= (tp-val) "on") (= (tp-val) "init") (= (tp-val) "def") (= (tp-val) "behavior") (= (tp-val) "live") (= (tp-val) "when"))) + ((and (= (tp-type) "keyword") (or (= (tp-val) "on") (= (tp-val) "init") (= (tp-val) "def") (= (tp-val) "behavior") (= (tp-val) "live") (= (tp-val) "when"))) nil) - (true (do (adv!) (plf-skip)))))) - (plf-skip) + ((and (= (tp-type) "keyword") (= (tp-val) "end")) + (if (> depth 0) (do (adv!) (plf-skip (- depth 1))) nil)) + ((and (= (tp-type) "keyword") (or (= (tp-val) "if") (= (tp-val) "repeat"))) + (do (adv!) (plf-skip (+ depth 1)))) + (true (do (adv!) (plf-skip depth)))))) + (plf-skip 0) (match-kw "end") (list (quote live-no-op)))) (define @@ -3076,15 +3080,20 @@ (define pwf-skip (fn - () + (depth) (cond ((at-end?) nil) - ((and (= (tp-type) "keyword") (or (= (tp-val) "end") (= (tp-val) "on") (= (tp-val) "init") (= (tp-val) "def") (= (tp-val) "behavior") (= (tp-val) "live") (= (tp-val) "when"))) + ((and (= (tp-type) "keyword") (or (= (tp-val) "on") (= (tp-val) "init") (= (tp-val) "def") (= (tp-val) "behavior") (= (tp-val) "live") (= (tp-val) "when"))) nil) - (true (do (adv!) (pwf-skip)))))) + ((and (= (tp-type) "keyword") (= (tp-val) "end")) + (if (> depth 0) (do (adv!) (pwf-skip (- depth 1))) nil)) + ((and (= (tp-type) "keyword") (or (= (tp-val) "if") (= (tp-val) "repeat"))) + (do (adv!) (pwf-skip (+ depth 1)))) + (true (do (adv!) (pwf-skip depth)))))) (if (or (= (tp-type) "hat") + (= (tp-type) "local") (and (= (tp-type) "keyword") (= (tp-val) "dom"))) (let ((expr (parse-expr))) @@ -3096,10 +3105,13 @@ (match-kw "end") (list (quote when-changes) expr body))) (do - (pwf-skip) + (pwf-skip 0) (match-kw "end") (list (quote when-feat-no-op))))) - (do (pwf-skip) (match-kw "end") (list (quote when-feat-no-op)))))) + (do + (pwf-skip 0) + (match-kw "end") + (list (quote when-feat-no-op)))))) (define parse-bind-feat (fn diff --git a/lib/hyperscript/runtime.sx b/lib/hyperscript/runtime.sx index 1a1f53aa..dcfed022 100644 --- a/lib/hyperscript/runtime.sx +++ b/lib/hyperscript/runtime.sx @@ -1686,7 +1686,34 @@ (define hs-scoped-set! - (fn (el name val) (dom-set-data el (str "hs-local-" name) val))) + (fn + (el name val) + (let + ((changed (not (= (hs-scoped-get el name) val)))) + (do + (dom-set-data el (str "hs-local-" name) val) + (when changed (hs-scoped-fire-watchers! el name val)))))) + +(begin + (define _hs-scoped-watchers (list)) + (define + hs-scoped-watch! + (fn + (el name handler) + (set! + _hs-scoped-watchers + (cons (list el name handler) _hs-scoped-watchers)))) + (define + hs-scoped-fire-watchers! + (fn + (el name val) + (for-each + (fn + (entry) + (when + (and (= (nth entry 0) el) (= (nth entry 1) name)) + ((nth entry 2) val))) + _hs-scoped-watchers)))) (define hs-scoped-get @@ -2701,6 +2728,8 @@ (if match (dom-parent match) nil))) (true el)))))) +;; ── SourceInfo API ──────────────────────────────────────────────── + (define hs-dom-walk (fn @@ -2711,8 +2740,6 @@ ((= (dom-get-attr el "dom-scope") "isolated") nil) (true (hs-dom-walk (dom-parent el) name))))) -;; ── SourceInfo API ──────────────────────────────────────────────── - (define hs-dom-find-owner (fn diff --git a/shared/static/wasm/sx/hs-compiler.sx b/shared/static/wasm/sx/hs-compiler.sx index f0322c04..ff8dcb5c 100644 --- a/shared/static/wasm/sx/hs-compiler.sx +++ b/shared/static/wasm/sx/hs-compiler.sx @@ -2356,14 +2356,26 @@ ((= head (quote when-changes)) (let ((expr (nth ast 1)) (body (nth ast 2))) - (if - (and (list? expr) (= (first expr) (quote dom-ref))) - (list - (quote hs-dom-watch!) - (hs-to-sx (nth expr 2)) - (nth expr 1) - (list (quote fn) (list (quote it)) (hs-to-sx body))) - nil))) + (cond + ((and (list? expr) (= (first expr) (quote dom-ref))) + (list + (quote hs-dom-watch!) + (hs-to-sx (nth expr 2)) + (nth expr 1) + (list + (quote fn) + (list (quote it)) + (hs-to-sx body)))) + ((and (list? expr) (= (first expr) (quote local))) + (list + (quote hs-scoped-watch!) + (quote me) + (nth expr 1) + (list + (quote fn) + (list (quote it)) + (hs-to-sx body)))) + (true nil)))) ((= head (quote init)) (list (quote hs-init) diff --git a/shared/static/wasm/sx/hs-parser.sx b/shared/static/wasm/sx/hs-parser.sx index 0be063cb..8406f83b 100644 --- a/shared/static/wasm/sx/hs-parser.sx +++ b/shared/static/wasm/sx/hs-parser.sx @@ -2840,7 +2840,7 @@ ((body (parse-cmd-list))) (match-kw "end") (list (quote view-transition!) using body))))) - ((and (= typ "keyword") (or (= val "on") (= val "init") (= val "def") (= val "behavior") (= val "live"))) + ((and (= typ "keyword") (or (= val "on") (= val "init") (= val "def") (= val "behavior") (= val "live") (= val "when") (= val "bind"))) nil) (true (parse-expr)))))) (define @@ -3060,13 +3060,17 @@ (define plf-skip (fn - () + (depth) (cond ((at-end?) nil) - ((and (= (tp-type) "keyword") (or (= (tp-val) "end") (= (tp-val) "on") (= (tp-val) "init") (= (tp-val) "def") (= (tp-val) "behavior") (= (tp-val) "live") (= (tp-val) "when"))) + ((and (= (tp-type) "keyword") (or (= (tp-val) "on") (= (tp-val) "init") (= (tp-val) "def") (= (tp-val) "behavior") (= (tp-val) "live") (= (tp-val) "when"))) nil) - (true (do (adv!) (plf-skip)))))) - (plf-skip) + ((and (= (tp-type) "keyword") (= (tp-val) "end")) + (if (> depth 0) (do (adv!) (plf-skip (- depth 1))) nil)) + ((and (= (tp-type) "keyword") (or (= (tp-val) "if") (= (tp-val) "repeat"))) + (do (adv!) (plf-skip (+ depth 1)))) + (true (do (adv!) (plf-skip depth)))))) + (plf-skip 0) (match-kw "end") (list (quote live-no-op)))) (define @@ -3076,15 +3080,20 @@ (define pwf-skip (fn - () + (depth) (cond ((at-end?) nil) - ((and (= (tp-type) "keyword") (or (= (tp-val) "end") (= (tp-val) "on") (= (tp-val) "init") (= (tp-val) "def") (= (tp-val) "behavior") (= (tp-val) "live") (= (tp-val) "when"))) + ((and (= (tp-type) "keyword") (or (= (tp-val) "on") (= (tp-val) "init") (= (tp-val) "def") (= (tp-val) "behavior") (= (tp-val) "live") (= (tp-val) "when"))) nil) - (true (do (adv!) (pwf-skip)))))) + ((and (= (tp-type) "keyword") (= (tp-val) "end")) + (if (> depth 0) (do (adv!) (pwf-skip (- depth 1))) nil)) + ((and (= (tp-type) "keyword") (or (= (tp-val) "if") (= (tp-val) "repeat"))) + (do (adv!) (pwf-skip (+ depth 1)))) + (true (do (adv!) (pwf-skip depth)))))) (if (or (= (tp-type) "hat") + (= (tp-type) "local") (and (= (tp-type) "keyword") (= (tp-val) "dom"))) (let ((expr (parse-expr))) @@ -3096,10 +3105,13 @@ (match-kw "end") (list (quote when-changes) expr body))) (do - (pwf-skip) + (pwf-skip 0) (match-kw "end") (list (quote when-feat-no-op))))) - (do (pwf-skip) (match-kw "end") (list (quote when-feat-no-op)))))) + (do + (pwf-skip 0) + (match-kw "end") + (list (quote when-feat-no-op)))))) (define parse-bind-feat (fn diff --git a/shared/static/wasm/sx/hs-runtime.sx b/shared/static/wasm/sx/hs-runtime.sx index 1a1f53aa..dcfed022 100644 --- a/shared/static/wasm/sx/hs-runtime.sx +++ b/shared/static/wasm/sx/hs-runtime.sx @@ -1686,7 +1686,34 @@ (define hs-scoped-set! - (fn (el name val) (dom-set-data el (str "hs-local-" name) val))) + (fn + (el name val) + (let + ((changed (not (= (hs-scoped-get el name) val)))) + (do + (dom-set-data el (str "hs-local-" name) val) + (when changed (hs-scoped-fire-watchers! el name val)))))) + +(begin + (define _hs-scoped-watchers (list)) + (define + hs-scoped-watch! + (fn + (el name handler) + (set! + _hs-scoped-watchers + (cons (list el name handler) _hs-scoped-watchers)))) + (define + hs-scoped-fire-watchers! + (fn + (el name val) + (for-each + (fn + (entry) + (when + (and (= (nth entry 0) el) (= (nth entry 1) name)) + ((nth entry 2) val))) + _hs-scoped-watchers)))) (define hs-scoped-get @@ -2701,6 +2728,8 @@ (if match (dom-parent match) nil))) (true el)))))) +;; ── SourceInfo API ──────────────────────────────────────────────── + (define hs-dom-walk (fn @@ -2711,8 +2740,6 @@ ((= (dom-get-attr el "dom-scope") "isolated") nil) (true (hs-dom-walk (dom-parent el) name))))) -;; ── SourceInfo API ──────────────────────────────────────────────── - (define hs-dom-find-owner (fn