HS: when :count changes — scoped watch + parse-cmd feature boundary fix
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 47s

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 02:59:15 +00:00
parent abbb1fe5c6
commit e7169af985
6 changed files with 144 additions and 42 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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