Level 2-3: lake morphing — server content flows through reactive islands

Lake tag (lake :id "name" children...) creates server-morphable slots
within islands. During morph, the engine enters hydrated islands and
updates data-sx-lake elements by ID while preserving surrounding
reactive DOM (signals, effects, event listeners).

Specced in .sx, bootstrapped to JS and Python:
- adapter-dom.sx: render-dom-lake, reactive-attr marks data-sx-reactive-attrs
- adapter-html.sx: render-html-lake SSR output
- adapter-sx.sx: lake serialized in wire format
- engine.sx: morph-island-children (lake-by-ID matching),
  sync-attrs skips reactive attributes
- ~sx-header uses lakes for logo and copyright
- Hegelian essay updated with lake code example

Also includes: lambda nil-padding for missing args, page env ordering fix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 14:29:54 +00:00
parent d5e416e478
commit 9b9fc6b6a5
15 changed files with 351 additions and 63 deletions

View File

@@ -77,6 +77,10 @@
(= name "<>")
(render-dom-fragment args env ns)
;; lake — server-morphable slot within an island
(= name "lake")
(render-dom-lake args env ns)
;; html: prefix → force element rendering
(starts-with? name "html:")
(render-dom-element (slice name 5) args env ns)
@@ -649,6 +653,50 @@
container))))))
;; --------------------------------------------------------------------------
;; render-dom-lake — server-morphable slot within an island
;; --------------------------------------------------------------------------
;;
;; (lake :id "name" children...)
;;
;; Renders as <div data-sx-lake="name">children</div>.
;; During morph, the server can replace lake content while the surrounding
;; reactive island DOM is preserved. This is the "water around the rocks" —
;; server substance flowing through client territory.
;;
;; Supports :tag keyword to change wrapper element (default "div").
(define render-dom-lake
(fn (args env ns)
(let ((lake-id nil)
(lake-tag "div")
(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 ((kname (keyword-name arg))
(kval (trampoline (eval-expr (nth args (inc (get state "i"))) env))))
(cond
(= kname "id") (set! lake-id kval)
(= kname "tag") (set! lake-tag kval))
(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)
(let ((el (dom-create-element lake-tag nil)))
(dom-set-attr el "data-sx-lake" (or lake-id ""))
(for-each
(fn (c) (dom-append el (render-to-dom c env ns)))
children)
el))))
;; --------------------------------------------------------------------------
;; Reactive DOM rendering helpers
;; --------------------------------------------------------------------------
@@ -668,8 +716,14 @@
;; reactive-attr — bind an element attribute to a signal expression
;; Used when an attribute value contains (deref sig) inside an island.
;; Marks the attribute name on the element via data-sx-reactive-attrs so
;; the morph algorithm knows not to overwrite it with server content.
(define reactive-attr
(fn (el attr-name compute-fn)
;; Mark this attribute as reactively managed
(let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") ""))
(updated (if (empty? existing) attr-name (str existing "," attr-name))))
(dom-set-attr el "data-sx-reactive-attrs" updated))
(effect (fn ()
(let ((val (compute-fn)))
(cond