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

@@ -25,33 +25,38 @@
;; Evaluate for SX wire format — serialize rendering forms,
;; evaluate control flow and function calls.
(set-render-active! true)
(case (type-of expr)
"number" expr
"string" expr
"boolean" expr
"nil" nil
(let ((result
(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))))
"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)
"keyword" (keyword-name expr)
"list"
(if (empty? expr)
(list)
(aser-list expr env))
"list"
(if (empty? expr)
(list)
(aser-list expr env))
;; Spread — pass through for client rendering
"spread" expr
;; Spread — emit attrs to nearest element provider
"spread" (do (emit! "element-attrs" (spread-attrs expr)) nil)
:else expr)))
:else expr)))
;; Catch spread values from function calls and symbol lookups
(if (spread? result)
(do (emit! "element-attrs" (spread-attrs result)) nil)
result))))
(define aser-list :effects [render]
@@ -110,7 +115,6 @@
(fn ((children :as list) (env :as dict))
;; Serialize (<> child1 child2 ...) to sx source string
;; Must flatten list results (e.g. from map/filter) to avoid nested parens
;; Spreads are filtered — fragments have no parent element to merge into
(let ((parts (list)))
(for-each
(fn (c)
@@ -118,10 +122,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)
@@ -134,9 +138,13 @@
;; Serialize (name :key val child ...) — evaluate args but keep as sx
;; Uses for-each + mutable state (not reduce) so bootstrapper emits for-loops
;; that can contain nested for-each for list flattening.
(let ((parts (list name))
;; Separate attrs and children so emitted spread attrs go before children.
(let ((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
@@ -146,30 +154,34 @@
(< (inc i) (len args)))
(let ((val (aser (nth args (inc i)) env)))
(when (not (nil? val))
(append! parts (str ":" (keyword-name arg)))
(append! parts (serialize val)))
(append! attr-parts (str ":" (keyword-name arg)))
(append! attr-parts (serialize val)))
(set! skip true)
(set! i (inc i)))
(let ((val (aser arg env)))
(when (not (nil? val))
(if (spread? val)
;; Spread child — merge attrs as keyword args into parent element
(if (= (type-of val) "list")
(for-each
(fn (k)
(let ((v (dict-get (spread-attrs val) k)))
(append! parts (str ":" k))
(append! parts (serialize v))))
(keys (spread-attrs val)))
(if (= (type-of val) "list")
(for-each
(fn (item)
(when (not (nil? item))
(append! parts (serialize item))))
val)
(append! parts (serialize val)))))
(fn (item)
(when (not (nil? item))
(append! child-parts (serialize item))))
val)
(append! child-parts (serialize val))))
(set! i (inc i))))))
args)
(str "(" (join " " parts) ")"))))
;; Collect emitted spread attrs — goes 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")
(let ((parts (concat (list name) attr-parts child-parts)))
(str "(" (join " " parts) ")")))))
;; --------------------------------------------------------------------------