Step 17: streaming render — hyperscript enhancements, WASM builds, live server tests

Streaming chunked transfer with shell-first suspense and resolve scripts.
Hyperscript parser/compiler/runtime expanded for conformance. WASM static
assets added to OCaml host. Playwright streaming and page-level test suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 08:41:38 +00:00
parent 7aefe4da8f
commit 6e27442d57
29 changed files with 65959 additions and 628 deletions

View File

@@ -364,11 +364,15 @@
((match-kw "in")
(list (quote not-in?) left (parse-expr)))
((match-kw "between")
(let ((lo (parse-atom)))
(let
((lo (parse-atom)))
(match-kw "and")
(let ((hi (parse-atom)))
(list (quote not)
(list (quote and)
(let
((hi (parse-atom)))
(list
(quote not)
(list
(quote and)
(list (quote >=) left lo)
(list (quote <=) left hi))))))
((match-kw "really")
@@ -429,10 +433,13 @@
(list (quote >=) left (parse-expr)))
(list (quote >) left (parse-expr)))))
((match-kw "between")
(let ((lo (parse-atom)))
(let
((lo (parse-atom)))
(match-kw "and")
(let ((hi (parse-atom)))
(list (quote and)
(let
((hi (parse-atom)))
(list
(quote and)
(list (quote >=) left lo)
(list (quote <=) left hi)))))
((match-kw "in") (list (quote in?) left (parse-expr)))
@@ -491,10 +498,14 @@
((and (= typ "keyword") (= val "exists"))
(do (adv!) (list (quote exists?) left)))
((and (or (= typ "keyword") (= typ "ident")) (= val "starts"))
(do (adv!) (match-kw "with")
(do
(adv!)
(match-kw "with")
(list (quote starts-with?) left (parse-expr))))
((and (or (= typ "keyword") (= typ "ident")) (= val "ends"))
(do (adv!) (match-kw "with")
(do
(adv!)
(match-kw "with")
(list (quote ends-with?) left (parse-expr))))
((and (= typ "keyword") (= val "matches"))
(do (adv!) (list (quote matches?) left (parse-expr))))
@@ -687,20 +698,26 @@
(if
(= (tp-type) "class")
(let
((cls (get (adv!) "value"))
(extra-classes (list)))
;; Collect additional class refs
(define collect-classes!
(fn ()
(when (= (tp-type) "class")
(set! extra-classes (append extra-classes (list (get (adv!) "value"))))
((cls (get (adv!) "value")) (extra-classes (list)))
(define
collect-classes!
(fn
()
(when
(= (tp-type) "class")
(set!
extra-classes
(append extra-classes (list (get (adv!) "value"))))
(collect-classes!))))
(collect-classes!)
(let
((tgt (parse-tgt-kw "to" (list (quote me)))))
(if (empty? extra-classes)
(if
(empty? extra-classes)
(list (quote add-class) cls tgt)
(cons (quote multi-add-class) (cons tgt (cons cls extra-classes))))))
(cons
(quote multi-add-class)
(cons tgt (cons cls extra-classes))))))
nil)))
(define
parse-remove-cmd
@@ -709,19 +726,26 @@
(if
(= (tp-type) "class")
(let
((cls (get (adv!) "value"))
(extra-classes (list)))
(define collect-classes!
(fn ()
(when (= (tp-type) "class")
(set! extra-classes (append extra-classes (list (get (adv!) "value"))))
((cls (get (adv!) "value")) (extra-classes (list)))
(define
collect-classes!
(fn
()
(when
(= (tp-type) "class")
(set!
extra-classes
(append extra-classes (list (get (adv!) "value"))))
(collect-classes!))))
(collect-classes!)
(let
((tgt (parse-tgt-kw "from" (list (quote me)))))
(if (empty? extra-classes)
(if
(empty? extra-classes)
(list (quote remove-class) cls tgt)
(cons (quote multi-remove-class) (cons tgt (cons cls extra-classes))))))
(cons
(quote multi-remove-class)
(cons tgt (cons cls extra-classes))))))
nil)))
(define
parse-toggle-cmd
@@ -732,12 +756,12 @@
(if
(= (tp-type) "class")
(let
((cls1 (get (adv!) "value")))
((cls1 (do (let ((v (tp-val))) (adv!) v))))
(expect-kw! "and")
(if
(= (tp-type) "class")
(let
((cls2 (get (adv!) "value")))
((cls2 (do (let ((v (tp-val))) (adv!) v))))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote toggle-between) cls1 cls2 tgt)))
@@ -745,10 +769,47 @@
nil))
((= (tp-type) "class")
(let
((cls (get (adv!) "value")))
((cls (do (let ((v (tp-val))) (adv!) v))))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote toggle-class) cls tgt))))
((= (tp-type) "style")
(let
((prop (do (let ((v (tp-val))) (adv!) v))))
(if
(match-kw "between")
(let
((val1 (parse-atom)))
(expect-kw! "and")
(let
((val2 (parse-atom)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote toggle-style-between) prop val1 val2 tgt))))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote toggle-style) prop tgt)))))
((= (tp-type) "attr")
(let
((attr-name (do (let ((v (tp-val))) (adv!) v))))
(if
(match-kw "between")
(let
((val1 (parse-atom)))
(expect-kw! "and")
(let
((val2 (parse-atom)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list
(quote toggle-attr-between)
attr-name
val1
val2
tgt))))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote toggle-attr) attr-name tgt)))))
(true nil))))
(define
parse-set-cmd
@@ -772,9 +833,16 @@
(list (quote put!) value "after" (parse-expr)))
((match-kw "at")
(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)))))
((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
@@ -863,8 +931,10 @@
(let
((expr (parse-expr)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote increment!) expr tgt)))))
((amount (if (match-kw "by") (parse-expr) 1)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote increment!) expr amount tgt))))))
(define
parse-dec-cmd
(fn
@@ -872,8 +942,10 @@
(let
((expr (parse-expr)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote decrement!) expr tgt)))))
((amount (if (match-kw "by") (parse-expr) 1)))
(let
((tgt (parse-tgt-kw "on" (list (quote me)))))
(list (quote decrement!) expr amount tgt))))))
(define
parse-hide-cmd
(fn
@@ -909,12 +981,25 @@
parse-repeat-cmd
(fn
()
(let
((mode (cond ((match-kw "forever") (list (quote forever))) ((match-kw "while") (list (quote while) (parse-expr))) ((match-kw "until") (list (quote until) (parse-expr))) ((= (tp-type) "number") (let ((n (parse-dur (get (adv!) "value")))) (expect-kw! "times") (list (quote times) n))) (true (list (quote forever))))))
(let
((body (parse-cmd-list)))
(match-kw "end")
(list (quote repeat) mode body)))))
(cond
((and (= (tp-type) "keyword") (= (tp-val) "for"))
(do (adv!) (parse-for-cmd)))
((and (= (tp-type) "keyword") (= (tp-val) "in"))
(do
(adv!)
(let
((collection (parse-expr)))
(let
((body (parse-cmd-list)))
(match-kw "end")
(list (quote for) "it" collection nil body)))))
(true
(let
((mode (cond ((match-kw "forever") (list (quote forever))) ((match-kw "while") (list (quote while) (parse-expr))) ((match-kw "until") (list (quote until) (parse-expr))) (true (let ((n (parse-expr))) (if (match-kw "times") (list (quote times) n) (list (quote forever))))))))
(let
((body (parse-cmd-list)))
(match-kw "end")
(list (quote repeat) mode body)))))))
(define
parse-fetch-cmd
(fn
@@ -959,16 +1044,24 @@
parse-take-cmd
(fn
()
(if
(= (tp-type) "class")
(let
((cls (get (adv!) "value")))
(cond
((= (tp-type) "class")
(let
((tgt (if (match-kw "for")
(parse-expr)
(parse-tgt-kw "from" (list (quote me))))))
(list (quote take) cls tgt)))
nil)))
((cls (do (let ((v (tp-val))) (adv!) v))))
(let
((from-sel (if (match-kw "from") (parse-expr) nil)))
(let
((for-tgt (if (match-kw "for") (parse-expr) nil)))
(list (quote take!) "class" cls from-sel for-tgt)))))
((= (tp-type) "attr")
(let
((attr-name (do (let ((v (tp-val))) (adv!) v))))
(let
((from-sel (if (match-kw "from") (parse-expr) nil)))
(let
((for-tgt (if (match-kw "for") (parse-expr) nil)))
(list (quote take!) "attr" attr-name from-sel for-tgt)))))
(true nil))))
(define
parse-go-cmd
(fn () (match-kw "to") (list (quote go) (parse-expr))))
@@ -1124,6 +1217,44 @@
(let
((tgt (parse-expr)))
(list (quote measure) (if (nil? tgt) (list (quote me)) tgt)))))
(define
parse-scroll-cmd
(fn
()
(let
((tgt (if (or (at-end?) (and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end")))) (list (quote me)) (parse-expr))))
(let
((pos (cond ((match-kw "top") "top") ((match-kw "bottom") "bottom") ((match-kw "left") "left") ((match-kw "right") "right") (true "top"))))
(list (quote scroll!) tgt pos)))))
(define
parse-select-cmd
(fn
()
(let
((tgt (if (or (at-end?) (and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end")))) (list (quote me)) (parse-expr))))
(list (quote select!) tgt))))
(define
parse-reset-cmd
(fn
()
(let
((tgt (if (or (at-end?) (and (= (tp-type) "keyword") (or (= (tp-val) "then") (= (tp-val) "end")))) (list (quote me)) (parse-expr))))
(list (quote reset!) tgt))))
(define
parse-default-cmd
(fn
()
(let
((tgt (parse-expr)))
(expect-kw! "to")
(let ((val (parse-expr))) (list (quote default!) tgt val)))))
(define
parse-halt-cmd
(fn
()
(let
((the-event (and (match-kw "the") (or (match-kw "event") (match-kw "default")))))
(list (quote halt!) (if the-event "event" "default")))))
(define
parse-param-list
(fn () (if (= (tp-type) "paren-open") (parse-call-args) (list))))
@@ -1241,7 +1372,6 @@
(let
((typ (tp-type)) (val (tp-val)))
(cond
;; Terminators — these end a command list, not start a command
((and (= typ "keyword") (or (= val "catch") (= val "finally") (= val "end") (= val "else") (= val "otherwise")))
nil)
((and (= typ "keyword") (= val "add"))
@@ -1304,11 +1434,61 @@
(do (adv!) (parse-measure-cmd)))
((and (= typ "keyword") (= val "render"))
(do (adv!) (parse-render-cmd)))
((and (= typ "keyword") (= val "scroll"))
(do (adv!) (parse-scroll-cmd)))
((and (= typ "keyword") (= val "select"))
(do (adv!) (parse-select-cmd)))
((and (= typ "keyword") (= val "reset"))
(do (adv!) (parse-reset-cmd)))
((and (= typ "keyword") (= val "default"))
(do (adv!) (parse-default-cmd)))
((and (= typ "keyword") (= val "halt"))
(do (adv!) (parse-halt-cmd)))
(true (parse-expr))))))
(define
parse-cmd-list
(fn
()
(define
cmd-kw?
(fn
(v)
(or
(= v "add")
(= v "remove")
(= v "toggle")
(= v "set")
(= v "put")
(= v "if")
(= v "wait")
(= v "send")
(= v "trigger")
(= v "log")
(= v "increment")
(= v "decrement")
(= v "hide")
(= v "show")
(= v "transition")
(= v "repeat")
(= v "fetch")
(= v "call")
(= v "take")
(= v "settle")
(= v "go")
(= v "return")
(= v "throw")
(= v "append")
(= v "tell")
(= v "for")
(= v "make")
(= v "install")
(= v "measure")
(= v "render")
(= v "halt")
(= v "default")
(= v "scroll")
(= v "select")
(= v "reset"))))
(define
cl-collect
(fn
@@ -1320,7 +1500,11 @@
acc
(let
((acc2 (append acc (list cmd))))
(if (match-kw "then") (cl-collect acc2) acc2))))))
(cond
((match-kw "then") (cl-collect acc2))
((and (not (at-end?)) (= (tp-type) "keyword") (cmd-kw? (tp-val)))
(cl-collect acc2))
(true acc2)))))))
(let
((cmds (cl-collect (list))))
(cond
@@ -1341,31 +1525,26 @@
((source (if (match-kw "from") (parse-expr) nil)))
(let
((body (parse-cmd-list)))
;; Parse optional catch/finally
(let
((catch-clause
(if (match-kw "catch")
(let ((var (let ((v (tp-val))) (adv!) v))
(handler (parse-cmd-list)))
(list var handler))
nil))
(finally-clause
(if (match-kw "finally")
(parse-cmd-list)
nil)))
(match-kw "end")
(let
((parts (list (quote on) event-name)))
((catch-clause (if (match-kw "catch") (let ((var (let ((v (tp-val))) (adv!) v)) (handler (parse-cmd-list))) (list var handler)) nil))
(finally-clause
(if (match-kw "finally") (parse-cmd-list) nil)))
(match-kw "end")
(let
((parts (if every? (append parts (list :every true)) parts)))
((parts (list (quote on) event-name)))
(let
((parts (if flt (append parts (list :filter flt)) parts)))
((parts (if every? (append parts (list :every true)) parts)))
(let
((parts (if source (append parts (list :from source)) parts)))
(let ((parts (if catch-clause (append parts (list :catch catch-clause)) parts)))
(let ((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
(let ((parts (append parts (list body))))
parts)))))))))))))))
((parts (if flt (append parts (list :filter flt)) parts)))
(let
((parts (if source (append parts (list :from source)) parts)))
(let
((parts (if catch-clause (append parts (list :catch catch-clause)) parts)))
(let
((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
(let
((parts (append parts (list body))))
parts)))))))))))))))
(define
parse-init-feat
(fn
@@ -1403,5 +1582,4 @@
(first features)
(cons (quote do) features))))))
;; ── Convenience: source string → AST ─────────────────────────────
(define hs-compile (fn (src) (hs-parse (hs-tokenize src) src)))