Add reactive islands spec: signals.sx + defisland across all adapters

New spec file signals.sx defines the signal runtime: signal, computed,
effect, deref, reset!, swap!, batch, dispose, and island scope tracking.

eval.sx: defisland special form + island? type predicate in eval-call.
boundary.sx: signal primitive declarations (Tier 3).
render.sx: defisland in definition-form?.
adapter-dom.sx: render-dom-island with reactive context, reactive-text,
  reactive-attr, reactive-fragment, reactive-list helpers.
adapter-html.sx: render-html-island for SSR with data-sx-island/state.
adapter-sx.sx: island? handling in wire format serialization.
special-forms.sx: defisland declaration with docs and example.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 09:34:47 +00:00
parent b2aaa3786d
commit a97f4c0e39
8 changed files with 646 additions and 9 deletions

View File

@@ -102,6 +102,12 @@
(contains? HTML_TAGS name)
(render-dom-element name args env ns)
;; Island (~name) — reactive component
(and (starts-with? name "~")
(env-has? env name)
(island? (env-get env name)))
(render-dom-island (env-get env name) args env ns)
;; Component (~name)
(starts-with? name "~")
(let ((comp (env-get env name)))
@@ -284,7 +290,7 @@
(define RENDER_DOM_FORMS
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defmacro" "defstyle" "defhandler"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"map" "map-indexed" "filter" "for-each"))
(define render-dom-form?
@@ -414,6 +420,153 @@
(render-to-dom (lambda-body f) local ns))))
;; --------------------------------------------------------------------------
;; render-dom-island — render a reactive island
;; --------------------------------------------------------------------------
;;
;; Islands render like components but wrapped in a reactive context.
;; The island container element gets data-sx-island and data-sx-state
;; attributes for identification and hydration.
;;
;; Inside the island body, deref calls create reactive DOM subscriptions:
;; - Text bindings: (deref sig) in text position → reactive text node
;; - Attribute bindings: (deref sig) in attr → reactive attribute
;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide
(define render-dom-island
(fn (island args env ns)
;; Parse kwargs and children (same as component)
(let ((kwargs (dict))
(children (list)))
(reduce
(fn (state arg)
(let ((skip (get state "skip")))
(if skip
(assoc state "skip" false "i" (inc (get state "i")))
(if (and (= (type-of arg) "keyword")
(< (inc (get state "i")) (len args)))
(let ((val (trampoline
(eval-expr (nth args (inc (get state "i"))) env))))
(dict-set! kwargs (keyword-name arg) val)
(assoc state "skip" true "i" (inc (get state "i"))))
(do
(append! children arg)
(assoc state "i" (inc (get state "i"))))))))
(dict "i" 0 "skip" false)
args)
;; Build island env: closure + caller env + params
(let ((local (env-merge (component-closure island) env))
(island-name (component-name island)))
;; Bind params from kwargs
(for-each
(fn (p)
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params island))
;; If island accepts children, pre-render them to a fragment
(when (component-has-children? island)
(let ((child-frag (create-fragment)))
(for-each
(fn (c) (dom-append child-frag (render-to-dom c env ns)))
children)
(env-set! local "children" child-frag)))
;; Create the island container element
(let ((container (dom-create-element "div" nil))
(disposers (list)))
;; Mark as island
(dom-set-attr container "data-sx-island" island-name)
;; Render island body inside a scope that tracks disposers
(let ((body-dom
(with-island-scope
(fn (disposable) (append! disposers disposable))
(fn () (render-to-dom (component-body island) local ns)))))
(dom-append container body-dom)
;; Store disposers on the container for cleanup
(dom-set-data container "sx-disposers" disposers)
container))))))
;; --------------------------------------------------------------------------
;; Reactive DOM rendering helpers
;; --------------------------------------------------------------------------
;;
;; These functions create reactive bindings between signals and DOM nodes.
;; They are called by the platform's renderDOM when it detects deref
;; calls inside an island context.
;; reactive-text — create a text node bound to a signal
;; Used when (deref sig) appears in a text position inside an island.
(define reactive-text
(fn (sig)
(let ((node (create-text-node (str (deref sig)))))
(effect (fn ()
(dom-set-text-content node (str (deref sig)))))
node)))
;; reactive-attr — bind an element attribute to a signal expression
;; Used when an attribute value contains (deref sig) inside an island.
(define reactive-attr
(fn (el attr-name compute-fn)
(effect (fn ()
(let ((val (compute-fn)))
(cond
(or (nil? val) (= val false))
(dom-remove-attr el attr-name)
(= val true)
(dom-set-attr el attr-name "")
:else
(dom-set-attr el attr-name (str val))))))))
;; reactive-fragment — conditionally render a fragment based on a signal
;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island.
(define reactive-fragment
(fn (test-fn render-fn env ns)
(let ((marker (create-comment "island-fragment"))
(current-nodes (list)))
(effect (fn ()
;; Remove previous nodes
(for-each (fn (n) (dom-remove n)) current-nodes)
(set! current-nodes (list))
;; If test passes, render and insert after marker
(when (test-fn)
(let ((frag (render-fn)))
(set! current-nodes (dom-child-nodes frag))
(dom-insert-after marker frag)))))
marker)))
;; reactive-list — render a keyed list bound to a signal
;; Used for (map fn (deref items)) inside an island.
(define reactive-list
(fn (map-fn items-sig env ns)
(let ((container (create-fragment))
(marker (create-comment "island-list")))
(dom-append container marker)
(effect (fn ()
;; Simple strategy: clear and re-render
;; Future: keyed reconciliation
(let ((parent (dom-parent marker)))
(when parent
;; Remove all nodes after marker until next sibling marker
(dom-remove-children-after marker)
;; Render new items
(let ((items (deref items-sig)))
(for-each
(fn (item)
(let ((rendered (if (lambda? map-fn)
(render-lambda-dom map-fn (list item) env ns)
(render-to-dom (apply map-fn (list item)) env ns))))
(dom-insert-after marker rendered)))
(reverse items)))))))
container)))
;; --------------------------------------------------------------------------
;; Platform interface — DOM adapter
;; --------------------------------------------------------------------------
@@ -422,11 +575,20 @@
;; (dom-create-element tag ns) → Element (ns=nil for HTML, string for SVG/MathML)
;; (create-text-node s) → Text node
;; (create-fragment) → DocumentFragment
;; (create-comment s) → Comment node
;;
;; Tree mutation:
;; (dom-append parent child) → void (appendChild)
;; (dom-set-attr el name val) → void (setAttribute)
;; (dom-remove-attr el name) → void (removeAttribute)
;; (dom-get-attr el name) → string or nil (getAttribute)
;; (dom-set-text-content n s) → void (set textContent)
;; (dom-remove node) → void (remove from parent)
;; (dom-insert-after ref node) → void (insert node after ref)
;; (dom-parent node) → parent Element or nil
;; (dom-child-nodes frag) → list of child nodes
;; (dom-remove-children-after m)→ void (remove all siblings after marker)
;; (dom-set-data el key val) → void (store arbitrary data on element)
;;
;; Content parsing:
;; (dom-parse-html s) → DocumentFragment from HTML string
@@ -441,11 +603,15 @@
;; From eval.sx:
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
;; env-has?, env-get, env-set!, env-merge
;; lambda?, component?, macro?
;; lambda?, component?, island?, macro?
;; lambda-closure, lambda-params, lambda-body
;; component-params, component-body, component-closure,
;; component-has-children?, component-name
;;
;; From signals.sx:
;; signal, deref, reset!, swap!, computed, effect, batch
;; signal?, with-island-scope
;;
;; Iteration:
;; (for-each-indexed fn coll) → call fn(index, item) for each element
;; --------------------------------------------------------------------------