HS: fix empty/halt/morph/reset/dialog — 17 upstream tests pass

- parser `empty` no-target → (ref "me") (was bogus (sym "me"))
- parser `halt` modes distinguish: "all"/"bubbling"/"default" halt execution
  (raise hs-return), "the-event"/"the event's" only stop propagation/default.
  "'s" now matched as op token, not keyword.
- parser `get` cmd: dispatch + cmd-kw list + parse-get-cmd (parses expr with
  optional `as TYPE`). Required for `get result as JSON` in fetch chains.
- compiler empty-target for (local X): emit (set! X (hs-empty-like X)) so
  arrays/sets/maps clear the variable, not call DOM empty on the value.
- runtime hs-empty-like: container-of-same-type empty value.
- runtime hs-empty-target!: drop dead FORM branch that was short-circuiting
  to innerHTML=""; the querySelectorAll-over-inputs branch now runs.
- runtime hs-halt!: take ev param (was free `event` lookup); raise hs-return
  to stop execution unless mode is "the-event".
- runtime hs-reset!: type-aware — FORM → reset, INPUT/TEXTAREA → value/checked
  from defaults, SELECT → defaultSelected option.
- runtime hs-open!/hs-close!: toggle `open` attribute on details elements
  (not just the prop) so dom-has-attr? assertions work.
- runtime hs-coerce JSON: json-stringify dict/list (was str).
- test-runner mock: host-get on List + "length"/"size" (was only Dict);
  dom-set-attr tracks defaultChecked / defaultSelected / defaultValue;
  mock_query_all supports comma-separated selector groups.
- generator: emit boolean attrs (checked/selected/etc) even with null value;
  drop overcautious "skip HS with bare quotes or embedded HTML" guard so
  morph tests (source contains embedded <div>) emit properly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 15:36:01 +00:00
parent 5c66095b0f
commit 802ccd23e8
12 changed files with 1340 additions and 345 deletions

View File

@@ -180,6 +180,16 @@
((= typ "style")
(do (adv!) (list (quote style) val (list (quote me)))))
((= typ "local") (do (adv!) (list (quote local) val)))
((= typ "hat")
(do (adv!) (list (quote dom-ref) val (list (quote me)))))
((and (= typ "keyword") (= val "dom"))
(do
(adv!)
(let
((name (tp-val)))
(do
(adv!)
(list (quote dom-ref) name (list (quote me)))))))
((= typ "class")
(do (adv!) (list (quote query) (str "." val))))
((= typ "ident") (do (adv!) (list (quote ref) val)))
@@ -288,7 +298,7 @@
(adv!)
(let
((name val) (args (parse-call-args)))
(list (quote call) (list (quote ref) name) args))))
(cons (quote call) (cons (list (quote ref) name) args)))))
(true nil)))))
(define
parse-poss
@@ -301,7 +311,7 @@
((= (tp-type) "paren-open")
(let
((args (parse-call-args)))
(list (quote call) obj args)))
(cons (quote call) (cons obj args))))
((= (tp-type) "bracket-open")
(do
(adv!)
@@ -479,20 +489,24 @@
(list (quote type-check-strict) left type-name)
(list (quote type-check) left type-name))))))
(true
(if
(and
(= (tp-type) "ident")
(not (hs-keyword? (tp-val))))
(let
((prop-name (tp-val)))
(do (adv!) (list (quote prop-is) left prop-name)))
(let
((right (parse-expr)))
(let
((right (parse-expr)))
(if
(match-kw "ignoring")
(do
(match-kw "case")
(list (quote eq-ignore-case) left right))
(if
(match-kw "ignoring")
(do
(match-kw "case")
(list (quote eq-ignore-case) left right))
(and
(list? right)
(= (len right) 2)
(= (first right) (quote ref))
(string? (nth right 1)))
(list
(quote hs-is)
left
(list (quote fn) (list) right)
(nth right 1))
(list (quote =) left right))))))))
((and (= typ "keyword") (= val "am"))
(do
@@ -504,12 +518,34 @@
(list (quote not-in?) left (parse-expr)))
((match-kw "empty")
(list (quote not) (list (quote empty?) left)))
((match-kw "between")
(let
((lo (parse-atom)))
(match-kw "and")
(let
((hi (parse-atom)))
(list
(quote not)
(list
(quote and)
(list (quote >=) left lo)
(list (quote <=) left hi))))))
(true
(let
((right (parse-expr)))
(list (quote not) (list (quote =) left right))))))
((match-kw "in") (list (quote in?) left (parse-expr)))
((match-kw "empty") (list (quote empty?) left))
((match-kw "between")
(let
((lo (parse-atom)))
(match-kw "and")
(let
((hi (parse-atom)))
(list
(quote and)
(list (quote >=) left lo)
(list (quote <=) left hi)))))
(true
(let
((right (parse-expr)))
@@ -639,6 +675,14 @@
(list
(quote not)
(list (quote ends-with?) left (parse-expr)))))
((or (match-kw "precede") (match-kw "precedes"))
(list
(quote not)
(list (quote precedes?) left (parse-atom))))
((or (match-kw "follow") (match-kw "follows"))
(list
(quote not)
(list (quote follows?) left (parse-atom))))
(true left))))
((and (= typ "keyword") (= val "equals"))
(do (adv!) (list (quote =) left (parse-expr))))
@@ -877,7 +921,7 @@
(collect-classes!))))
(collect-classes!)
(let
((tgt (if (match-kw "from") (parse-expr) nil)))
((tgt (if (match-kw "from") (parse-expr) (list (quote me)))))
(if
(empty? extra-classes)
(list (quote remove-class) cls tgt)
@@ -1097,7 +1141,12 @@
((match-kw "on")
(let
((target (parse-expr)))
(list (quote set-on) tgt target)))
(if
(match-kw "to")
(let
((value (parse-expr)))
(list (quote set-on!) tgt target value))
(list (quote set-on) tgt target))))
(true (error (str "Expected to/on at position " p)))))))
(define
parse-put-cmd
@@ -1105,28 +1154,31 @@
()
(let
((value (parse-expr)))
(cond
((match-kw "into") (list (quote set!) (parse-expr) value))
((match-kw "before")
(list (quote put!) value "before" (parse-expr)))
((match-kw "after")
(list (quote put!) value "after" (parse-expr)))
((match-kw "at")
(do
(match-kw "the")
(cond
((match-kw "start")
(do
(expect-kw! "of")
(list (quote put!) value "start" (parse-expr))))
((match-kw "end")
(do
(expect-kw! "of")
(list (quote put!) value "end" (parse-expr))))
(true
(error (str "Expected start/end after at, position " p))))))
(true
(error (str "Expected into/before/after/at at position " p)))))))
(let
((value (if (and (list? value) (= (first value) (quote dom-ref)) (match-kw "on")) (list (quote dom-ref) (nth value 1) (parse-expr)) value)))
(cond
((match-kw "into") (list (quote set!) (parse-expr) value))
((match-kw "before")
(list (quote put!) value "before" (parse-expr)))
((match-kw "after")
(list (quote put!) value "after" (parse-expr)))
((match-kw "at")
(do
(match-kw "the")
(cond
((match-kw "start")
(do
(expect-kw! "of")
(list (quote put!) value "start" (parse-expr))))
((match-kw "end")
(do
(expect-kw! "of")
(list (quote put!) value "end" (parse-expr))))
(true
(error
(str "Expected start/end after at, position " p))))))
(true
(error (str "Expected into/before/after/at at position " p))))))))
(define
parse-if-cmd
(fn
@@ -1153,14 +1205,33 @@
(adv!)
(let
((source (if (match-kw "from") (parse-expr) nil)))
(if
source
(list (quote wait-for) event-name :from source)
(list (quote wait-for) event-name)))))
(let
((timeout-dur (if (match-kw "or") (if (= (tp-type) "number") (let ((tok (adv!))) (let ((raw (get tok "value")) (suffix (if (and (= (tp-type) "ident") (or (= (tp-val) "ms") (= (tp-val) "s"))) (get (adv!) "value") ""))) (parse-dur (str raw suffix)))) nil) nil)))
(cond
((and source timeout-dur)
(list
(quote wait-for)
event-name
:from source
:or timeout-dur))
(source
(list (quote wait-for) event-name :from source))
(timeout-dur
(list (quote wait-for) event-name :or timeout-dur))
(true (list (quote wait-for) event-name)))))))
((= (tp-type) "number")
(let
((tok (adv!)))
(list (quote wait) (parse-dur (get tok "value")))))
(let
((raw (get tok "value"))
(suffix
(if
(and
(= (tp-type) "ident")
(or (= (tp-val) "ms") (= (tp-val) "s")))
(get (adv!) "value")
"")))
(list (quote wait) (parse-dur (str raw suffix))))))
(true (list (quote wait) 0)))))
(define
parse-detail-dict
@@ -1241,10 +1312,13 @@
(let
((expr (parse-expr)))
(let
((amount (if (match-kw "by") (parse-expr) 1)))
((by-amount (if (match-kw "by") (parse-expr) nil)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote increment!) expr amount tgt))))))
(if
by-amount
(list (quote increment!) expr by-amount tgt)
(list (quote increment!) expr tgt)))))))
(define
parse-dec-cmd
(fn
@@ -1252,10 +1326,13 @@
(let
((expr (parse-expr)))
(let
((amount (if (match-kw "by") (parse-expr) 1)))
((by-amount (if (match-kw "by") (parse-expr) nil)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote decrement!) expr amount tgt))))))
(if
by-amount
(list (quote decrement!) expr by-amount tgt)
(list (quote decrement!) expr tgt)))))))
(define
parse-hide-cmd
(fn
@@ -1279,7 +1356,7 @@
(fn
()
(let
((tgt (cond ((and (= (tp-type) "ident") (= (tp-val) "element")) (do (adv!) (parse-atom))) ((= (tp-type) "id") (parse-atom)) ((= (tp-type) "class") (parse-atom)) ((= (tp-type) "selector") (parse-atom)) (true nil))))
((tgt (cond ((and (= (tp-type) "ident") (= (tp-val) "element")) (do (adv!) (parse-atom))) ((and (= (tp-type) "keyword") (= (tp-val) "its")) (do (adv!) (list (quote ref) "it"))) ((= (tp-type) "id") (parse-atom)) ((= (tp-type) "class") (parse-atom)) ((= (tp-type) "selector") (parse-atom)) (true nil))))
(define
parse-one-transition
(fn
@@ -1370,11 +1447,13 @@
(fn
()
(if
(and (= (tp-type) "keyword") (= (tp-val) "gql"))
(and
(or (= (tp-type) "keyword") (= (tp-type) "ident"))
(= (tp-val) "gql"))
(do
(adv!)
(let
((gql-source (if (= (tp-type) "brace-open") (do (adv!) (define collect-gql (fn (acc depth) (cond ((at-end?) (join " " acc)) ((= (tp-type) "brace-open") (do (adv!) (collect-gql (append acc (list "{")) (+ depth 1)))) ((= (tp-type) "brace-close") (if (= depth 0) (do (adv!) (join " " acc)) (do (adv!) (collect-gql (append acc (list "}")) (- depth 1))))) (true (let ((v (tp-val))) (adv!) (collect-gql (append acc (list (if v v ""))) depth)))))) (str "{ " (collect-gql (list) 0) " }")) (if (or (and (= (tp-type) "keyword") (= (tp-val) "query")) (and (= (tp-type) "keyword") (= (tp-val) "mutation")) (and (= (tp-type) "keyword") (= (tp-val) "subscription"))) (let ((op-word (tp-val))) (adv!) (if (= (tp-type) "brace-open") (do (adv!) (define collect-gql2 (fn (acc depth) (cond ((at-end?) (join " " acc)) ((= (tp-type) "brace-open") (do (adv!) (collect-gql2 (append acc (list "{")) (+ depth 1)))) ((= (tp-type) "brace-close") (if (= depth 0) (do (adv!) (join " " acc)) (do (adv!) (collect-gql2 (append acc (list "}")) (- depth 1))))) (true (let ((v (tp-val))) (adv!) (collect-gql2 (append acc (list (if v v ""))) depth)))))) (str op-word " { " (collect-gql2 (list) 0) " }")) (str op-word ""))) ""))))
((gql-source (if (= (tp-type) "brace-open") (do (adv!) (define collect-gql (fn (acc depth) (cond ((at-end?) (join " " acc)) ((= (tp-type) "brace-open") (do (adv!) (collect-gql (append acc (list "{")) (+ depth 1)))) ((= (tp-type) "brace-close") (if (= depth 0) (do (adv!) (join " " acc)) (do (adv!) (collect-gql (append acc (list "}")) (- depth 1))))) (true (let ((v (tp-val))) (adv!) (collect-gql (append acc (list (if v v ""))) depth)))))) (str "{ " (collect-gql (list) 0) " }")) (if (or (and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "query")) (and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "mutation")) (and (or (= (tp-type) "keyword") (= (tp-type) "ident")) (= (tp-val) "subscription"))) (let ((op-word (tp-val))) (adv!) (if (= (tp-type) "brace-open") (do (adv!) (define collect-gql2 (fn (acc depth) (cond ((at-end?) (join " " acc)) ((= (tp-type) "brace-open") (do (adv!) (collect-gql2 (append acc (list "{")) (+ depth 1)))) ((= (tp-type) "brace-close") (if (= depth 0) (do (adv!) (join " " acc)) (do (adv!) (collect-gql2 (append acc (list "}")) (- depth 1))))) (true (let ((v (tp-val))) (adv!) (collect-gql2 (append acc (list (if v v ""))) depth)))))) (str op-word " { " (collect-gql2 (list) 0) " }")) (str op-word ""))) ""))))
(let
((url (if (and (= (tp-type) "keyword") (= (tp-val) "from")) (do (adv!) (parse-arith (parse-poss (parse-atom)))) nil)))
(list (quote fetch-gql) gql-source url))))
@@ -1383,7 +1462,7 @@
(let
((url (if (nil? url-atom) url-atom (parse-arith (parse-poss url-atom)))))
(let
((fmt-before (if (match-kw "as") (let ((f (tp-val))) (adv!) f) nil)))
((fmt-before (if (match-kw "as") (do (when (and (or (= (tp-type) "ident") (= (tp-type) "keyword")) (or (= (tp-val) "an") (= (tp-val) "a"))) (adv!)) (let ((f (tp-val))) (adv!) f)) nil)))
(when (= (tp-type) "brace-open") (parse-expr))
(when
(match-kw "with")
@@ -1392,7 +1471,7 @@
(parse-expr)
(parse-expr)))
(let
((fmt-after (if (and (not fmt-before) (match-kw "as")) (let ((f (tp-val))) (adv!) f) nil)))
((fmt-after (if (and (not fmt-before) (match-kw "as")) (do (when (and (or (= (tp-type) "ident") (= (tp-type) "keyword")) (or (= (tp-val) "an") (= (tp-val) "a"))) (adv!)) (let ((f (tp-val))) (adv!) f)) nil)))
(let
((fmt (or fmt-before fmt-after "text")))
(list (quote fetch) url fmt)))))))))
@@ -1413,18 +1492,8 @@
(if (= (tp-type) "comma") (adv!) nil)
(ca-collect (append acc (list arg)))))))
(ca-collect (list))))
(define
parse-call-cmd
(fn
()
(let
((name (get (adv!) "value")))
(if
(= (tp-type) "paren-open")
(let
((args (parse-call-args)))
(cons (quote call) (cons name args)))
(list (quote call) name)))))
(define parse-call-cmd (fn () (parse-expr)))
(define parse-get-cmd (fn () (parse-expr)))
(define
parse-take-cmd
(fn
@@ -1458,6 +1527,103 @@
attr-val
with-val)))))))
(true nil))))
(define
parse-pick-cmd
(fn
()
(let
((typ (tp-type)) (val (tp-val)))
(cond
((and (= typ "keyword") (= val "first"))
(do
(adv!)
(let
((n (parse-atom)))
(do
(expect-kw! "of")
(let
((coll (parse-expr)))
(list (quote pick-first) coll n))))))
((and (= typ "keyword") (= val "last"))
(do
(adv!)
(let
((n (parse-atom)))
(do
(expect-kw! "of")
(let
((coll (parse-expr)))
(list (quote pick-last) coll n))))))
((and (= typ "keyword") (= val "random"))
(do
(adv!)
(if
(match-kw "of")
(let
((coll (parse-expr)))
(list (quote pick-random) coll nil))
(let
((n (parse-atom)))
(do
(expect-kw! "of")
(let
((coll (parse-expr)))
(list (quote pick-random) coll n)))))))
((and (= typ "ident") (= val "items"))
(do
(adv!)
(let
((start-expr (parse-atom)))
(do
(expect-kw! "to")
(let
((end-expr (parse-atom)))
(do
(expect-kw! "of")
(let
((coll (parse-expr)))
(list (quote pick-items) coll start-expr end-expr))))))))
((and (= typ "keyword") (= val "match"))
(do
(adv!)
(expect-kw! "of")
(let
((regex (parse-expr)))
(do
(cond
((match-kw "of") nil)
((match-kw "from") nil)
(true
(error
(str
"Expected of/from after pick match regex at "
p))))
(let
((haystack (parse-expr)))
(list (quote pick-match) regex haystack))))))
((and (= typ "keyword") (= val "matches"))
(do
(adv!)
(expect-kw! "of")
(let
((regex (parse-expr)))
(do
(cond
((match-kw "of") nil)
((match-kw "from") nil)
(true
(error
(str
"Expected of/from after pick matches regex at "
p))))
(let
((haystack (parse-expr)))
(list (quote pick-matches) regex haystack))))))
(true
(error
(str
"Expected first/last/random/items/match/matches after 'pick' at "
p)))))))
(define
parse-go-cmd
(fn () (match-kw "to") (list (quote go) (parse-expr))))
@@ -1683,7 +1849,7 @@
(fn
()
(let
((mode (cond ((match-kw "the") (do (match-kw "event") (match-kw "'s") "event")) ((or (match-kw "default") (and (= (tp-val) "default") (do (adv!) true))) "default") ((or (match-kw "bubbling") (and (= (tp-val) "bubbling") (do (adv!) true))) "bubbling") (true "event"))))
((mode (cond ((match-kw "the") (do (match-kw "event") (when (and (= (tp-type) "op") (= (tp-val) "'s")) (adv!)) "the-event")) ((or (match-kw "default") (and (= (tp-val) "default") (do (adv!) true))) "default") ((or (match-kw "bubbling") (and (= (tp-val) "bubbling") (do (adv!) true))) "bubbling") (true "all"))))
(list (quote halt!) mode))))
(define
parse-param-list
@@ -1807,7 +1973,7 @@
(fn
()
(let
((target (cond ((at-end?) (list (quote sym) "me")) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end"))) (list (quote sym) "me")) (true (parse-expr)))))
((target (cond ((at-end?) (list (quote ref) "me")) ((and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end"))) (list (quote ref) "me")) (true (parse-expr)))))
(list (quote empty-target) target))))
(define
parse-swap-cmd
@@ -1817,6 +1983,16 @@
((lhs (parse-expr)))
(match-kw "with")
(let ((rhs (parse-expr))) (list (quote swap!) lhs rhs)))))
(define
parse-morph-cmd
(fn
()
(let
((target (parse-expr)))
(expect-kw! "to")
(let
((content (parse-expr)))
(list (quote morph!) target content)))))
(define
parse-open-cmd
(fn
@@ -1874,10 +2050,14 @@
(do (adv!) (parse-repeat-cmd)))
((and (= typ "keyword") (= val "fetch"))
(do (adv!) (parse-fetch-cmd)))
((and (= typ "keyword") (= val "get"))
(do (adv!) (parse-get-cmd)))
((and (= typ "keyword") (= val "call"))
(do (adv!) (parse-call-cmd)))
((and (= typ "keyword") (= val "take"))
(do (adv!) (parse-take-cmd)))
((and (= typ "keyword") (= val "pick"))
(do (adv!) (parse-pick-cmd)))
((and (= typ "keyword") (= val "settle"))
(do (adv!) (list (quote settle))))
((and (= typ "keyword") (= val "go"))
@@ -1918,6 +2098,8 @@
(do (adv!) (parse-empty-cmd)))
((and (= typ "keyword") (= val "swap"))
(do (adv!) (parse-swap-cmd)))
((and (= typ "keyword") (= val "morph"))
(do (adv!) (parse-morph-cmd)))
((and (= typ "keyword") (= val "open"))
(do (adv!) (parse-open-cmd)))
((and (= typ "keyword") (= val "close"))
@@ -1955,6 +2137,7 @@
(= v "transition")
(= v "repeat")
(= v "fetch")
(= v "get")
(= v "call")
(= v "take")
(= v "settle")
@@ -1977,8 +2160,10 @@
(= v "empty")
(= v "clear")
(= v "swap")
(= v "morph")
(= v "open")
(= v "close"))))
(= v "close")
(= v "pick"))))
(define
cl-collect
(fn
@@ -2047,6 +2232,53 @@
((body (parse-cmd-list)))
(match-kw "end")
(list (quote init) body))))
(define
parse-live-feat
(fn
()
(define
plf-skip
(fn
()
(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")))
nil)
(true (do (adv!) (plf-skip))))))
(plf-skip)
(match-kw "end")
(list (quote live-no-op))))
(define
parse-when-feat
(fn
()
(define
pwf-skip
(fn
()
(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")))
nil)
(true (do (adv!) (pwf-skip))))))
(if
(or
(= (tp-type) "hat")
(and (= (tp-type) "keyword") (= (tp-val) "dom")))
(let
((expr (parse-expr)))
(if
(match-kw "changes")
(let
((body (parse-cmd-list)))
(do
(match-kw "end")
(list (quote when-changes) expr body)))
(do
(pwf-skip)
(match-kw "end")
(list (quote when-feat-no-op)))))
(do (pwf-skip) (match-kw "end") (list (quote when-feat-no-op))))))
(define
parse-feat
(fn
@@ -2058,6 +2290,8 @@
((= val "init") (do (adv!) (parse-init-feat)))
((= val "def") (do (adv!) (parse-def-feat)))
((= val "behavior") (do (adv!) (parse-behavior-feat)))
((= val "live") (do (adv!) (parse-live-feat)))
((= val "when") (do (adv!) (parse-when-feat)))
(true (parse-cmd-list))))))
(define
coll-feats