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

@@ -8,7 +8,7 @@
;; parse-element-args, render-attrs, definition-form?
;; 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
;; ==========================================================================
@@ -50,7 +50,7 @@
(define RENDER_HTML_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-html-form?
@@ -85,6 +85,12 @@
(contains? HTML_TAGS name)
(render-html-element name args env)
;; Island (~name) — reactive component, SSR with hydration markers
(and (starts-with? name "~")
(env-has? env name)
(island? (env-get env name)))
(render-html-island (env-get env name) args env)
;; Component or macro call (~name)
(starts-with? name "~")
(let ((val (env-get env name)))
@@ -287,6 +293,85 @@
"</" tag ">"))))))
;; --------------------------------------------------------------------------
;; render-html-island — SSR rendering of a reactive island
;; --------------------------------------------------------------------------
;;
;; Renders the island body as static HTML wrapped in a container element
;; with data-sx-island and data-sx-state attributes. The client hydrates
;; this by finding these elements and re-rendering with reactive context.
;;
;; On the server, signal/deref/reset!/swap! are simple passthrough:
;; (signal val) → returns val (no container needed server-side)
;; (deref s) → returns s (signal values are plain values server-side)
;; (reset! s v) → no-op
;; (swap! s f) → no-op
(define render-html-island
(fn (island args env)
;; Parse kwargs and children (same pattern as render-html-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 raw HTML
(when (component-has-children? island)
(env-set! local "children"
(make-raw-html
(join "" (map (fn (c) (render-to-html c env)) children)))))
;; Render the island body as HTML
(let ((body-html (render-to-html (component-body island) local))
(state-json (serialize-island-state kwargs)))
;; Wrap in container with hydration attributes
(str "<div data-sx-island=\"" (escape-attr island-name) "\""
(if state-json
(str " data-sx-state=\"" (escape-attr state-json) "\"")
"")
">"
body-html
"</div>"))))))
;; --------------------------------------------------------------------------
;; serialize-island-state — serialize kwargs to JSON for hydration
;; --------------------------------------------------------------------------
;;
;; Only serializes simple values (numbers, strings, booleans, nil, lists, dicts).
;; Functions, components, and other non-serializable values are skipped.
(define serialize-island-state
(fn (kwargs)
(if (empty-dict? kwargs)
nil
(json-serialize kwargs))))
;; --------------------------------------------------------------------------
;; Platform interface — HTML adapter
;; --------------------------------------------------------------------------
@@ -297,7 +382,7 @@
;; 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
@@ -305,6 +390,11 @@
;; Raw HTML construction:
;; (make-raw-html s) → wrap string as raw HTML (not double-escaped)
;;
;; JSON serialization (for island state):
;; (json-serialize dict) → JSON string
;; (empty-dict? d) → boolean
;; (escape-attr s) → HTML attribute escape
;;
;; Iteration:
;; (for-each-indexed fn coll) → call fn(index, item) for each element
;; (map-indexed fn coll) → map fn(index, item) over each element