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)) ((= head (quote when-changes))
(let (let
((expr (nth ast 1)) (body (nth ast 2))) ((expr (nth ast 1)) (body (nth ast 2)))
(if (cond
(and (list? expr) (= (first expr) (quote dom-ref))) ((and (list? expr) (= (first expr) (quote dom-ref)))
(list (list
(quote hs-dom-watch!) (quote hs-dom-watch!)
(hs-to-sx (nth expr 2)) (hs-to-sx (nth expr 2))
(nth expr 1) (nth expr 1)
(list (quote fn) (list (quote it)) (hs-to-sx body))) (list
nil))) (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)) ((= head (quote init))
(list (list
(quote hs-init) (quote hs-init)

View File

@@ -2840,7 +2840,7 @@
((body (parse-cmd-list))) ((body (parse-cmd-list)))
(match-kw "end") (match-kw "end")
(list (quote view-transition!) using body))))) (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) nil)
(true (parse-expr)))))) (true (parse-expr))))))
(define (define
@@ -3060,13 +3060,17 @@
(define (define
plf-skip plf-skip
(fn (fn
() (depth)
(cond (cond
((at-end?) nil) ((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) nil)
(true (do (adv!) (plf-skip)))))) ((and (= (tp-type) "keyword") (= (tp-val) "end"))
(plf-skip) (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") (match-kw "end")
(list (quote live-no-op)))) (list (quote live-no-op))))
(define (define
@@ -3076,15 +3080,20 @@
(define (define
pwf-skip pwf-skip
(fn (fn
() (depth)
(cond (cond
((at-end?) nil) ((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) 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 (if
(or (or
(= (tp-type) "hat") (= (tp-type) "hat")
(= (tp-type) "local")
(and (= (tp-type) "keyword") (= (tp-val) "dom"))) (and (= (tp-type) "keyword") (= (tp-val) "dom")))
(let (let
((expr (parse-expr))) ((expr (parse-expr)))
@@ -3096,10 +3105,13 @@
(match-kw "end") (match-kw "end")
(list (quote when-changes) expr body))) (list (quote when-changes) expr body)))
(do (do
(pwf-skip) (pwf-skip 0)
(match-kw "end") (match-kw "end")
(list (quote when-feat-no-op))))) (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 (define
parse-bind-feat parse-bind-feat
(fn (fn

View File

@@ -1686,7 +1686,34 @@
(define (define
hs-scoped-set! 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 (define
hs-scoped-get hs-scoped-get
@@ -2701,6 +2728,8 @@
(if match (dom-parent match) nil))) (if match (dom-parent match) nil)))
(true el)))))) (true el))))))
;; ── SourceInfo API ────────────────────────────────────────────────
(define (define
hs-dom-walk hs-dom-walk
(fn (fn
@@ -2711,8 +2740,6 @@
((= (dom-get-attr el "dom-scope") "isolated") nil) ((= (dom-get-attr el "dom-scope") "isolated") nil)
(true (hs-dom-walk (dom-parent el) name))))) (true (hs-dom-walk (dom-parent el) name)))))
;; ── SourceInfo API ────────────────────────────────────────────────
(define (define
hs-dom-find-owner hs-dom-find-owner
(fn (fn

View File

@@ -2356,14 +2356,26 @@
((= head (quote when-changes)) ((= head (quote when-changes))
(let (let
((expr (nth ast 1)) (body (nth ast 2))) ((expr (nth ast 1)) (body (nth ast 2)))
(if (cond
(and (list? expr) (= (first expr) (quote dom-ref))) ((and (list? expr) (= (first expr) (quote dom-ref)))
(list (list
(quote hs-dom-watch!) (quote hs-dom-watch!)
(hs-to-sx (nth expr 2)) (hs-to-sx (nth expr 2))
(nth expr 1) (nth expr 1)
(list (quote fn) (list (quote it)) (hs-to-sx body))) (list
nil))) (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)) ((= head (quote init))
(list (list
(quote hs-init) (quote hs-init)

View File

@@ -2840,7 +2840,7 @@
((body (parse-cmd-list))) ((body (parse-cmd-list)))
(match-kw "end") (match-kw "end")
(list (quote view-transition!) using body))))) (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) nil)
(true (parse-expr)))))) (true (parse-expr))))))
(define (define
@@ -3060,13 +3060,17 @@
(define (define
plf-skip plf-skip
(fn (fn
() (depth)
(cond (cond
((at-end?) nil) ((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) nil)
(true (do (adv!) (plf-skip)))))) ((and (= (tp-type) "keyword") (= (tp-val) "end"))
(plf-skip) (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") (match-kw "end")
(list (quote live-no-op)))) (list (quote live-no-op))))
(define (define
@@ -3076,15 +3080,20 @@
(define (define
pwf-skip pwf-skip
(fn (fn
() (depth)
(cond (cond
((at-end?) nil) ((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) 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 (if
(or (or
(= (tp-type) "hat") (= (tp-type) "hat")
(= (tp-type) "local")
(and (= (tp-type) "keyword") (= (tp-val) "dom"))) (and (= (tp-type) "keyword") (= (tp-val) "dom")))
(let (let
((expr (parse-expr))) ((expr (parse-expr)))
@@ -3096,10 +3105,13 @@
(match-kw "end") (match-kw "end")
(list (quote when-changes) expr body))) (list (quote when-changes) expr body)))
(do (do
(pwf-skip) (pwf-skip 0)
(match-kw "end") (match-kw "end")
(list (quote when-feat-no-op))))) (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 (define
parse-bind-feat parse-bind-feat
(fn (fn

View File

@@ -1686,7 +1686,34 @@
(define (define
hs-scoped-set! 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 (define
hs-scoped-get hs-scoped-get
@@ -2701,6 +2728,8 @@
(if match (dom-parent match) nil))) (if match (dom-parent match) nil)))
(true el)))))) (true el))))))
;; ── SourceInfo API ────────────────────────────────────────────────
(define (define
hs-dom-walk hs-dom-walk
(fn (fn
@@ -2711,8 +2740,6 @@
((= (dom-get-attr el "dom-scope") "isolated") nil) ((= (dom-get-attr el "dom-scope") "isolated") nil)
(true (hs-dom-walk (dom-parent el) name))))) (true (hs-dom-walk (dom-parent el) name)))))
;; ── SourceInfo API ────────────────────────────────────────────────
(define (define
hs-dom-find-owner hs-dom-find-owner
(fn (fn