Refactor spread to use provide/emit! internally

Spreads now emit their attrs into the nearest element's provide scope
instead of requiring per-child spread? checks at every intermediate
layer. emit! is tolerant (no-op when no provider), so spreads in
non-element contexts silently vanish.

- adapter-html: element/lake/marsh wrap children in provide, collect
  emitted; removed 14 spread filters from fragment, forms, components
- adapter-sx: aser wraps result to catch spread values from fn calls;
  aser-call uses provide with attr-parts/child-parts ordering
- adapter-async: same pattern for both render and aser paths
- adapter-dom: added emit! in spread dispatch + provide in element
  rendering; kept spread? checks for reactive/island and DOM safety
- platform: emit! returns NIL when no provider instead of erroring
- 3 new aser tests: stored spread, nested element, silent drop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 15:41:32 +00:00
parent 859aad4333
commit 2de4ba8c57
9 changed files with 444 additions and 526 deletions

View File

@@ -48,7 +48,7 @@
"string" (escape-html expr)
"number" (escape-html (str expr))
"raw-html" (raw-html-content expr)
"spread" expr
"spread" (do (emit! "element-attrs" (spread-attrs expr)) "")
"symbol" (let ((val (async-eval expr env ctx)))
(async-render val env ctx))
"keyword" (escape-html (keyword-name expr))
@@ -80,10 +80,9 @@
(= name "raw!")
(async-render-raw args env ctx)
;; Fragment (spreads filtered — no parent element)
;; Fragment
(= name "<>")
(join "" (filter (fn (r) (not (spread? r)))
(async-map-render args env ctx)))
(join "" (async-map-render args env ctx))
;; html: prefix
(starts-with? name "html:")
@@ -171,18 +170,19 @@
(css-class-collect! (str class-val))))
(if (contains? VOID_ELEMENTS tag)
(str "<" tag (render-attrs attrs) ">")
;; Render children, collecting spreads and content separately
;; Provide scope for spread emit!
(let ((token (if (or (= tag "svg") (= tag "math"))
(svg-context-set! true)
nil))
(content-parts (list)))
(provide-push! "element-attrs" nil)
(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))))
(fn (c) (append! content-parts (async-render c env ctx)))
children)
(for-each
(fn (spread-dict) (merge-spread-attrs attrs spread-dict))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(when token (svg-context-reset! token))
(str "<" tag (render-attrs attrs) ">"
(join "" content-parts)
@@ -231,14 +231,11 @@
(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)
;; Pre-render children to raw HTML
(when (component-has-children? comp)
(let ((parts (list)))
(for-each
(fn (c)
(let ((r (async-render c env ctx)))
(when (not (spread? r))
(append! parts r))))
(fn (c) (append! parts (async-render c env ctx)))
children)
(env-set! local "children"
(make-raw-html (join "" parts)))))
@@ -259,14 +256,11 @@
(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)
;; Pre-render children
(when (component-has-children? island)
(let ((parts (list)))
(for-each
(fn (c)
(let ((r (async-render c env ctx)))
(when (not (spread? r))
(append! parts r))))
(fn (c) (append! parts (async-render c env ctx)))
children)
(env-set! local "children"
(make-raw-html (join "" parts)))))
@@ -367,14 +361,13 @@
(async-render (nth expr 3) env ctx)
"")))
;; when — single body: pass through (spread propagates). Multi: join strings.
;; when — single body: pass through. Multi: join strings.
(= name "when")
(if (not (async-eval (nth expr 1) 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)))))
(join "" (async-map-render (slice expr 2) env ctx))))
;; cond — uses cond-scheme? (every? check) from eval.sx
(= name "cond")
@@ -392,47 +385,39 @@
(let ((local (async-process-bindings (nth expr 1) env 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)))))
(join "" (async-map-render (slice expr 2) local ctx))))
;; begin / do — single body: pass through. Multi: join strings.
(or (= name "begin") (= name "do"))
(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))))
(join "" (async-map-render (rest expr) env ctx)))
;; Definition forms
(definition-form? name)
(do (async-eval expr env ctx) "")
;; map — spreads filtered
;; map
(= name "map")
(let ((f (async-eval (nth expr 1) env ctx))
(coll (async-eval (nth expr 2) env ctx)))
(join ""
(filter (fn (r) (not (spread? r)))
(async-map-fn-render f coll env ctx))))
(join "" (async-map-fn-render f coll env ctx)))
;; map-indexed — spreads filtered
;; map-indexed
(= name "map-indexed")
(let ((f (async-eval (nth expr 1) env ctx))
(coll (async-eval (nth expr 2) env ctx)))
(join ""
(filter (fn (r) (not (spread? r)))
(async-map-indexed-fn-render f coll env ctx))))
(join "" (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) — spreads filtered
;; for-each (render variant)
(= name "for-each")
(let ((f (async-eval (nth expr 1) env ctx))
(coll (async-eval (nth expr 2) env ctx)))
(join ""
(filter (fn (r) (not (spread? r)))
(async-map-fn-render f coll env ctx))))
(join "" (async-map-fn-render f coll env ctx)))
;; provide — render-time dynamic scope
(= name "provide")
@@ -443,8 +428,7 @@
(provide-push! prov-name prov-val)
(let ((result (if (= body-count 1)
(async-render (nth expr body-start) env ctx)
(let ((results (async-map-render (slice expr body-start) env ctx)))
(join "" (filter (fn (r) (not (spread? r))) results))))))
(join "" (async-map-render (slice expr body-start) env ctx)))))
(provide-pop! prov-name)
result))
@@ -595,35 +579,34 @@
(define-async async-aser :effects [render io]
(fn (expr (env :as dict) ctx)
(case (type-of expr)
"number" expr
"string" expr
"boolean" expr
"nil" nil
"symbol"
(let ((name (symbol-name expr)))
(cond
(env-has? env name) (env-get env name)
(primitive? name) (get-primitive name)
(= name "true") true
(= name "false") false
(= name "nil") nil
:else (error (str "Undefined symbol: " name))))
"keyword" (keyword-name expr)
"dict" (async-aser-dict expr env ctx)
;; Spread — pass through for client rendering
"spread" expr
"list"
(if (empty? expr)
(list)
(async-aser-list expr env ctx))
:else expr)))
(let ((t (type-of expr))
(result nil))
(cond
(= t "number") (set! result expr)
(= t "string") (set! result expr)
(= t "boolean") (set! result expr)
(= t "nil") (set! result nil)
(= t "symbol")
(let ((name (symbol-name expr)))
(set! result
(cond
(env-has? env name) (env-get env name)
(primitive? name) (get-primitive name)
(= name "true") true
(= name "false") false
(= name "nil") nil
:else (error (str "Undefined symbol: " name)))))
(= t "keyword") (set! result (keyword-name expr))
(= t "dict") (set! result (async-aser-dict expr env ctx))
;; Spread — emit attrs to nearest element provider
(= t "spread") (do (emit! "element-attrs" (spread-attrs expr))
(set! result nil))
(= t "list") (set! result (if (empty? expr) (list) (async-aser-list expr env ctx)))
:else (set! result expr))
;; Catch spread values from function calls and symbol lookups
(if (spread? result)
(do (emit! "element-attrs" (spread-attrs result)) nil)
result))))
(define-async async-aser-dict :effects [render io]
@@ -775,7 +758,6 @@
(define-async async-aser-fragment :effects [render io]
(fn ((children :as list) (env :as dict) ctx)
;; Spreads are filtered — fragments have no parent element to merge into
(let ((parts (list)))
(for-each
(fn (c)
@@ -783,10 +765,10 @@
(if (= (type-of result) "list")
(for-each
(fn (item)
(when (and (not (nil? item)) (not (spread? item)))
(when (not (nil? item))
(append! parts (serialize item))))
result)
(when (and (not (nil? result)) (not (spread? result)))
(when (not (nil? result))
(append! parts (serialize result))))))
children)
(if (empty? parts)
@@ -860,9 +842,12 @@
(let ((token (if (or (= name "svg") (= name "math"))
(svg-context-set! true)
nil))
(parts (list name))
(attr-parts (list))
(child-parts (list))
(skip false)
(i 0))
;; Provide scope for spread emit!
(provide-push! "element-attrs" nil)
(for-each
(fn (arg)
(if skip
@@ -872,39 +857,43 @@
(< (inc i) (len args)))
(let ((val (async-aser (nth args (inc i)) env ctx)))
(when (not (nil? val))
(append! parts (str ":" (keyword-name arg)))
(append! attr-parts (str ":" (keyword-name arg)))
(if (= (type-of val) "list")
(let ((live (filter (fn (v) (not (nil? v))) val)))
(if (empty? live)
(append! parts "nil")
(append! attr-parts "nil")
(let ((items (map serialize live)))
(if (some (fn (v) (sx-expr? v)) live)
(append! parts (str "(<> " (join " " items) ")"))
(append! parts (str "(list " (join " " items) ")"))))))
(append! parts (serialize val))))
(append! attr-parts (str "(<> " (join " " items) ")"))
(append! attr-parts (str "(list " (join " " items) ")"))))))
(append! attr-parts (serialize val))))
(set! skip true)
(set! i (inc i)))
(let ((result (async-aser arg env ctx)))
(when (not (nil? result))
(if (spread? result)
;; Spread child — merge attrs as keyword args into parent element
(if (= (type-of result) "list")
(for-each
(fn (k)
(let ((v (dict-get (spread-attrs result) k)))
(append! parts (str ":" k))
(append! parts (serialize v))))
(keys (spread-attrs result)))
(if (= (type-of result) "list")
(for-each
(fn (item)
(when (not (nil? item))
(append! parts (serialize item))))
result)
(append! parts (serialize result)))))
(fn (item)
(when (not (nil? item))
(append! child-parts (serialize item))))
result)
(append! child-parts (serialize result))))
(set! i (inc i))))))
args)
;; Collect emitted spread attrs — after explicit attrs, before children
(for-each
(fn (spread-dict)
(for-each
(fn (k)
(let ((v (dict-get spread-dict k)))
(append! attr-parts (str ":" k))
(append! attr-parts (serialize v))))
(keys spread-dict)))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(when token (svg-context-reset! token))
(make-sx-expr (str "(" (join " " parts) ")")))))
(let ((parts (concat (list name) attr-parts child-parts)))
(make-sx-expr (str "(" (join " " parts) ")"))))))
;; --------------------------------------------------------------------------