Add spread + collect primitives, rewrite ~cssx/tw as defcomp

New SX primitives for child-to-parent communication in the render tree:
- spread type: make-spread, spread?, spread-attrs — child injects attrs
  onto parent element (class joins with space, style with semicolon)
- collect!/collected/clear-collected! — render-time accumulation with
  dedup into named buckets

~cssx/tw is now a proper defcomp returning a spread value instead of a
macro wrapping children. ~cssx/flush reads collected "cssx" rules and
emits a single <style data-cssx> tag.

All four render adapters (html, async, dom, aser) handle spread values.
Both bootstraps (Python + JS) regenerated. Also fixes length→len in
cssx.sx (length was never a registered primitive).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 02:38:31 +00:00
parent c2efa192c5
commit 41097eeef9
15 changed files with 844 additions and 230 deletions

View File

@@ -48,6 +48,7 @@
"string" (escape-html expr)
"number" (escape-html (str expr))
"raw-html" (raw-html-content expr)
"spread" expr
"symbol" (let ((val (async-eval expr env ctx)))
(async-render val env ctx))
"keyword" (escape-html (keyword-name expr))
@@ -79,9 +80,10 @@
(= name "raw!")
(async-render-raw args env ctx)
;; Fragment
;; Fragment (spreads filtered — no parent element)
(= name "<>")
(join "" (async-map-render args env ctx))
(join "" (filter (fn (r) (not (spread? r)))
(async-map-render args env ctx)))
;; html: prefix
(starts-with? name "html:")
@@ -167,16 +169,24 @@
(let ((class-val (dict-get attrs "class")))
(when (and (not (nil? class-val)) (not (= class-val false)))
(css-class-collect! (str class-val))))
;; Build opening tag
(let ((opening (str "<" tag (render-attrs attrs) ">")))
(if (contains? VOID_ELEMENTS tag)
opening
(let ((token (if (or (= tag "svg") (= tag "math"))
(svg-context-set! true)
nil))
(child-html (join "" (async-map-render children env ctx))))
(when token (svg-context-reset! token))
(str opening child-html "</" tag ">")))))))
(if (contains? VOID_ELEMENTS tag)
(str "<" tag (render-attrs attrs) ">")
;; Render children, collecting spreads and content separately
(let ((token (if (or (= tag "svg") (= tag "math"))
(svg-context-set! true)
nil))
(content-parts (list)))
(for-each
(fn (c)
(let ((result (async-render c env ctx)))
(if (spread? result)
(merge-spread-attrs attrs (spread-attrs result))
(append! content-parts result))))
children)
(when token (svg-context-reset! token))
(str "<" tag (render-attrs attrs) ">"
(join "" content-parts)
"</" tag ">"))))))
;; --------------------------------------------------------------------------
@@ -221,10 +231,17 @@
(for-each
(fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params comp))
;; Pre-render children to raw HTML (filter spreads — no parent element)
(when (component-has-children? comp)
(env-set! local "children"
(make-raw-html
(join "" (async-map-render children env ctx)))))
(let ((parts (list)))
(for-each
(fn (c)
(let ((r (async-render c env ctx)))
(when (not (spread? r))
(append! parts r))))
children)
(env-set! local "children"
(make-raw-html (join "" parts)))))
(async-render (component-body comp) local ctx)))))
@@ -242,10 +259,17 @@
(for-each
(fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params island))
;; Pre-render children (filter spreads — no parent element)
(when (component-has-children? island)
(env-set! local "children"
(make-raw-html
(join "" (async-map-render children env ctx)))))
(let ((parts (list)))
(for-each
(fn (c)
(let ((r (async-render c env ctx)))
(when (not (spread? r))
(append! parts r))))
children)
(env-set! local "children"
(make-raw-html (join "" parts)))))
(let ((body-html (async-render (component-body island) local ctx))
(state-json (serialize-island-state kwargs)))
(str "<span data-sx-island=\"" (escape-attr island-name) "\""
@@ -343,11 +367,14 @@
(async-render (nth expr 3) env ctx)
"")))
;; when
;; when — single body: pass through (spread propagates). Multi: join strings.
(= name "when")
(if (not (async-eval (nth expr 1) env ctx))
""
(join "" (async-map-render (slice expr 2) env ctx)))
(if (= (len expr) 3)
(async-render (nth expr 2) env ctx)
(let ((results (async-map-render (slice expr 2) env ctx)))
(join "" (filter (fn (r) (not (spread? r))) results)))))
;; cond — uses cond-scheme? (every? check) from eval.sx
(= name "cond")
@@ -360,43 +387,52 @@
(= name "case")
(async-render (async-eval expr env ctx) env ctx)
;; let / let*
;; let / let* — single body: pass through. Multi: join strings.
(or (= name "let") (= name "let*"))
(let ((local (async-process-bindings (nth expr 1) env ctx)))
(join "" (async-map-render (slice expr 2) local ctx)))
(if (= (len expr) 3)
(async-render (nth expr 2) local ctx)
(let ((results (async-map-render (slice expr 2) local ctx)))
(join "" (filter (fn (r) (not (spread? r))) results)))))
;; begin / do
;; begin / do — single body: pass through. Multi: join strings.
(or (= name "begin") (= name "do"))
(join "" (async-map-render (rest expr) env ctx))
(if (= (len expr) 2)
(async-render (nth expr 1) env ctx)
(let ((results (async-map-render (rest expr) env ctx)))
(join "" (filter (fn (r) (not (spread? r))) results))))
;; Definition forms
(definition-form? name)
(do (async-eval expr env ctx) "")
;; map
;; map — spreads filtered
(= name "map")
(let ((f (async-eval (nth expr 1) env ctx))
(coll (async-eval (nth expr 2) env ctx)))
(join ""
(async-map-fn-render f coll env ctx)))
(filter (fn (r) (not (spread? r)))
(async-map-fn-render f coll env ctx))))
;; map-indexed
;; map-indexed — spreads filtered
(= name "map-indexed")
(let ((f (async-eval (nth expr 1) env ctx))
(coll (async-eval (nth expr 2) env ctx)))
(join ""
(async-map-indexed-fn-render f coll env ctx)))
(filter (fn (r) (not (spread? r)))
(async-map-indexed-fn-render f coll env ctx))))
;; filter — eval fully then render
(= name "filter")
(async-render (async-eval expr env ctx) env ctx)
;; for-each (render variant)
;; for-each (render variant) — spreads filtered
(= name "for-each")
(let ((f (async-eval (nth expr 1) env ctx))
(coll (async-eval (nth expr 2) env ctx)))
(join ""
(async-map-fn-render f coll env ctx)))
(filter (fn (r) (not (spread? r)))
(async-map-fn-render f coll env ctx))))
;; Fallback
:else
@@ -565,6 +601,9 @@
"dict" (async-aser-dict expr env ctx)
;; Spread — pass through for client rendering
"spread" expr
"list"
(if (empty? expr)
(list)
@@ -1250,6 +1289,14 @@
;; (svg-context-reset! token) — reset SVG context
;; (css-class-collect! val) — collect CSS classes
;;
;; Spread + collect (from render.sx):
;; (spread? x) — check if spread value
;; (spread-attrs s) — extract attrs dict from spread
;; (merge-spread-attrs tgt src) — merge spread attrs onto target
;; (collect! bucket value) — add to render-time accumulator
;; (collected bucket) — read render-time accumulator
;; (clear-collected! bucket) — clear accumulator
;;
;; Raw HTML:
;; (is-raw-html? x) — check if raw HTML marker
;; (make-raw-html s) — wrap string as raw HTML