From 9e079c9c19b0900058edc7f3cbad7c2da602f6f1 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 26 Mar 2026 08:13:27 +0000 Subject: [PATCH] Add (cyst) form: isolated reactive subtree that survives parent re-renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New language feature: (cyst [:key id] body...) creates a DOM container with its own island scope that persists across parent reactive re-renders. On first render, the body is evaluated in a fresh with-island-scope and the resulting DOM is cached. On subsequent renders, the cached DOM node is returned if still connected to the document. This solves the fundamental problem of nesting reactive islands inside other islands' render trees — the child island's DOM (with its event handlers and signal subscriptions) survives when the parent re-renders. Implementation: *memo-cache* dict keyed by cyst id. render-dom checks isConnected before returning cached node. Each cyst gets its own disposer list via with-island-scope. Usage in sx-tools: defisland render preview now wrapped in (cyst :key full-name ...). Real mouse clicks work — counter increments, temperature converts, computed signals update. Verified on both local and live site. Co-Authored-By: Claude Opus 4.6 (1M context) --- shared/static/wasm/sx/adapter-dom.sx | 6 ++++-- sx/sx/sx-tools-editor.sx | 2 +- web/adapter-dom.sx | 6 ++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/shared/static/wasm/sx/adapter-dom.sx b/shared/static/wasm/sx/adapter-dom.sx index c2fa1d30..38c670ba 100644 --- a/shared/static/wasm/sx/adapter-dom.sx +++ b/shared/static/wasm/sx/adapter-dom.sx @@ -4,6 +4,8 @@ (define island-scope? (fn () (not (nil? (scope-peek "sx-island-scope"))))) +(begin (define *memo-cache* (dict)) (define *cyst-counter* 0) (define next-cyst-id (fn () (set! *cyst-counter* (+ *cyst-counter* 1)) (str "sx-cyst-" *cyst-counter*)))) + (define contains-deref? (fn (expr) (if (not (list? expr)) false (if (empty? expr) false (if (and (= (type-of (first expr)) "symbol") (= (symbol-name (first expr)) "deref")) true (some contains-deref? expr)))))) (define dom-on :effects (io) (fn (el name handler) (dom-listen el name (if (lambda? handler) (if (= 0 (len (lambda-params handler))) (fn (_e) (trampoline (call-lambda handler (list))) (run-post-render-hooks)) (fn (e) (trampoline (call-lambda handler (list e))) (run-post-render-hooks))) handler)))) @@ -22,11 +24,11 @@ (define render-dom-unknown-component :effects (render) (fn ((name :as string)) (error (str "Unknown component: " name)))) -(define RENDER_DOM_FORMS (list "if" "when" "cond" "case" "let" "let*" "letrec" "begin" "do" "define" "defcomp" "defisland" "defmacro" "defstyle" "map" "map-indexed" "filter" "for-each" "portal" "error-boundary" "scope" "provide")) +(define RENDER_DOM_FORMS (list "if" "when" "cond" "case" "let" "let*" "letrec" "begin" "do" "define" "defcomp" "defisland" "defmacro" "defstyle" "map" "map-indexed" "filter" "for-each" "portal" "error-boundary" "scope" "provide" "cyst")) (define render-dom-form? :effects () (fn ((name :as string)) (contains? RENDER_DOM_FORMS name))) -(define dispatch-render-form :effects (render) (fn ((name :as string) expr (env :as dict) (ns :as string)) (cond (= name "if") (if (island-scope?) (let ((marker (create-comment "r-if")) (current-nodes (list)) (initial-result nil)) (effect (fn () (let ((result (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) (if cond-val (render-to-dom (nth expr 2) env ns) (if (> (len expr) 3) (render-to-dom (nth expr 3) env ns) (create-fragment)))))) (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (dom-insert-after marker result)) (set! initial-result result))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (set! current-nodes (if (dom-is-fragment? initial-result) (dom-child-nodes initial-result) (list initial-result))) (dom-append frag initial-result)) frag))) (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) (if cond-val (render-to-dom (nth expr 2) env ns) (if (> (len expr) 3) (render-to-dom (nth expr 3) env ns) (create-fragment))))) (= name "when") (if (island-scope?) (let ((marker (create-comment "r-when")) (current-nodes (list)) (initial-result nil)) (effect (fn () (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (list)) (when (trampoline (eval-expr (nth expr 1) env)) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) (set! current-nodes (dom-child-nodes frag)) (dom-insert-after marker frag)))) (when (trampoline (eval-expr (nth expr 1) env)) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) (set! current-nodes (dom-child-nodes frag)) (set! initial-result frag)))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (dom-append frag initial-result)) frag))) (if (not (trampoline (eval-expr (nth expr 1) env))) (create-fragment) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) frag))) (= name "cond") (if (island-scope?) (let ((marker (create-comment "r-cond")) (current-nodes (list)) (initial-result nil)) (effect (fn () (let ((branch (eval-cond (rest expr) env))) (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (list)) (when branch (let ((result (render-to-dom branch env ns))) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (dom-insert-after marker result)))) (when branch (let ((result (render-to-dom branch env ns))) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (set! initial-result result))))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (dom-append frag initial-result)) frag))) (let ((branch (eval-cond (rest expr) env))) (if branch (render-to-dom branch env ns) (create-fragment)))) (= name "case") (render-to-dom (trampoline (eval-expr expr env)) env ns) (or (= name "let") (= name "let*")) (let ((local (process-bindings (nth expr 1) env))) (if (= (len expr) 3) (render-to-dom (nth expr 2) local ns) (let ((frag (create-fragment))) (for-each (fn (i) (let ((result (render-to-dom (nth expr i) local ns))) (when (not (spread? result)) (dom-append frag result)))) (range 2 (len expr))) frag))) (= name "letrec") (let ((bindings (nth expr 1)) (body (slice expr 2)) (local (env-extend env))) (for-each (fn (pair) (let ((pname (if (= (type-of (first pair)) "symbol") (symbol-name (first pair)) (str (first pair))))) (env-bind! local pname nil))) bindings) (for-each (fn (pair) (let ((pname (if (= (type-of (first pair)) "symbol") (symbol-name (first pair)) (str (first pair))))) (env-set! local pname (trampoline (eval-expr (nth pair 1) local))))) bindings) (when (> (len body) 1) (for-each (fn (e) (trampoline (eval-expr e local))) (init body))) (render-to-dom (last body) local ns)) (or (= name "begin") (= name "do")) (if (= (len expr) 2) (render-to-dom (nth expr 1) env ns) (let ((frag (create-fragment))) (for-each (fn (i) (let ((result (render-to-dom (nth expr i) env ns))) (when (not (spread? result)) (dom-append frag result)))) (range 1 (len expr))) frag)) (definition-form? name) (do (trampoline (eval-expr expr env)) (create-fragment)) (= name "map") (let ((coll-expr (nth expr 2))) (if (and (island-scope?) (= (type-of coll-expr) "list") (> (len coll-expr) 1) (= (type-of (first coll-expr)) "symbol") (= (symbol-name (first coll-expr)) "deref")) (let ((f (trampoline (eval-expr (nth expr 1) env))) (sig (trampoline (eval-expr (nth coll-expr 1) env)))) (if (signal? sig) (reactive-list f sig env ns) (let ((coll (deref sig)) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag))) (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag))) (= name "map-indexed") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each-indexed (fn (i item) (let ((val (if (lambda? f) (render-lambda-dom f (list i item) env ns) (render-to-dom (apply f (list i item)) env ns)))) (dom-append frag val))) coll) frag) (= name "filter") (render-to-dom (trampoline (eval-expr expr env)) env ns) (= name "portal") (render-dom-portal (rest expr) env ns) (= name "error-boundary") (render-dom-error-boundary (rest expr) env ns) (= name "for-each") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag) (= name "scope") (let ((scope-name (trampoline (eval-expr (nth expr 1) env))) (rest-args (slice expr 2)) (scope-val nil) (body-exprs nil) (frag (create-fragment))) (if (and (>= (len rest-args) 2) (= (type-of (first rest-args)) "keyword") (= (keyword-name (first rest-args)) "value")) (do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env))) (set! body-exprs (slice rest-args 2))) (set! body-exprs rest-args)) (scope-push! scope-name scope-val) (for-each (fn (e) (dom-append frag (render-to-dom e env ns))) body-exprs) (scope-pop! scope-name) frag) (= name "provide") (let ((prov-name (trampoline (eval-expr (nth expr 1) env))) (prov-val (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (scope-push! prov-name prov-val) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 3 (len expr))) (scope-pop! prov-name) frag) :else (render-to-dom (trampoline (eval-expr expr env)) env ns)))) +(define dispatch-render-form :effects (render) (fn ((name :as string) expr (env :as dict) (ns :as string)) (cond (= name "if") (if (island-scope?) (let ((marker (create-comment "r-if")) (current-nodes (list)) (initial-result nil)) (effect (fn () (let ((result (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) (if cond-val (render-to-dom (nth expr 2) env ns) (if (> (len expr) 3) (render-to-dom (nth expr 3) env ns) (create-fragment)))))) (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (dom-insert-after marker result)) (set! initial-result result))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (set! current-nodes (if (dom-is-fragment? initial-result) (dom-child-nodes initial-result) (list initial-result))) (dom-append frag initial-result)) frag))) (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) (if cond-val (render-to-dom (nth expr 2) env ns) (if (> (len expr) 3) (render-to-dom (nth expr 3) env ns) (create-fragment))))) (= name "when") (if (island-scope?) (let ((marker (create-comment "r-when")) (current-nodes (list)) (initial-result nil)) (effect (fn () (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (list)) (when (trampoline (eval-expr (nth expr 1) env)) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) (set! current-nodes (dom-child-nodes frag)) (dom-insert-after marker frag)))) (when (trampoline (eval-expr (nth expr 1) env)) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) (set! current-nodes (dom-child-nodes frag)) (set! initial-result frag)))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (dom-append frag initial-result)) frag))) (if (not (trampoline (eval-expr (nth expr 1) env))) (create-fragment) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) frag))) (= name "cond") (if (island-scope?) (let ((marker (create-comment "r-cond")) (current-nodes (list)) (initial-result nil)) (effect (fn () (let ((branch (eval-cond (rest expr) env))) (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (list)) (when branch (let ((result (render-to-dom branch env ns))) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (dom-insert-after marker result)))) (when branch (let ((result (render-to-dom branch env ns))) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (set! initial-result result))))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (dom-append frag initial-result)) frag))) (let ((branch (eval-cond (rest expr) env))) (if branch (render-to-dom branch env ns) (create-fragment)))) (= name "case") (render-to-dom (trampoline (eval-expr expr env)) env ns) (or (= name "let") (= name "let*")) (let ((local (process-bindings (nth expr 1) env))) (if (= (len expr) 3) (render-to-dom (nth expr 2) local ns) (let ((frag (create-fragment))) (for-each (fn (i) (let ((result (render-to-dom (nth expr i) local ns))) (when (not (spread? result)) (dom-append frag result)))) (range 2 (len expr))) frag))) (= name "letrec") (let ((bindings (nth expr 1)) (body (slice expr 2)) (local (env-extend env))) (for-each (fn (pair) (let ((pname (if (= (type-of (first pair)) "symbol") (symbol-name (first pair)) (str (first pair))))) (env-bind! local pname nil))) bindings) (for-each (fn (pair) (let ((pname (if (= (type-of (first pair)) "symbol") (symbol-name (first pair)) (str (first pair))))) (env-set! local pname (trampoline (eval-expr (nth pair 1) local))))) bindings) (when (> (len body) 1) (for-each (fn (e) (trampoline (eval-expr e local))) (init body))) (render-to-dom (last body) local ns)) (or (= name "begin") (= name "do")) (if (= (len expr) 2) (render-to-dom (nth expr 1) env ns) (let ((frag (create-fragment))) (for-each (fn (i) (let ((result (render-to-dom (nth expr i) env ns))) (when (not (spread? result)) (dom-append frag result)))) (range 1 (len expr))) frag)) (definition-form? name) (do (trampoline (eval-expr expr env)) (create-fragment)) (= name "map") (let ((coll-expr (nth expr 2))) (if (and (island-scope?) (= (type-of coll-expr) "list") (> (len coll-expr) 1) (= (type-of (first coll-expr)) "symbol") (= (symbol-name (first coll-expr)) "deref")) (let ((f (trampoline (eval-expr (nth expr 1) env))) (sig (trampoline (eval-expr (nth coll-expr 1) env)))) (if (signal? sig) (reactive-list f sig env ns) (let ((coll (deref sig)) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag))) (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag))) (= name "map-indexed") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each-indexed (fn (i item) (let ((val (if (lambda? f) (render-lambda-dom f (list i item) env ns) (render-to-dom (apply f (list i item)) env ns)))) (dom-append frag val))) coll) frag) (= name "filter") (render-to-dom (trampoline (eval-expr expr env)) env ns) (= name "portal") (render-dom-portal (rest expr) env ns) (= name "error-boundary") (render-dom-error-boundary (rest expr) env ns) (= name "for-each") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag) (= name "scope") (let ((scope-name (trampoline (eval-expr (nth expr 1) env))) (rest-args (slice expr 2)) (scope-val nil) (body-exprs nil) (frag (create-fragment))) (if (and (>= (len rest-args) 2) (= (type-of (first rest-args)) "keyword") (= (keyword-name (first rest-args)) "value")) (do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env))) (set! body-exprs (slice rest-args 2))) (set! body-exprs rest-args)) (scope-push! scope-name scope-val) (for-each (fn (e) (dom-append frag (render-to-dom e env ns))) body-exprs) (scope-pop! scope-name) frag) (= name "provide") (let ((prov-name (trampoline (eval-expr (nth expr 1) env))) (prov-val (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (scope-push! prov-name prov-val) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 3 (len expr))) (scope-pop! prov-name) frag) (= name "cyst") (let ((cyst-key (if (and (> (len expr) 2) (= (type-of (nth expr 1)) "keyword") (= (keyword-name (nth expr 1)) "key")) (str (trampoline (eval-expr (nth expr 2) env))) (next-cyst-id))) (cached (get *memo-cache* cyst-key))) (if (and cached (host-get cached "isConnected")) cached (let ((container (dom-create-element "div" nil)) (disposers (list)) (body-exprs (if (and (> (len expr) 2) (= (type-of (nth expr 1)) "keyword") (= (keyword-name (nth expr 1)) "key")) (slice expr 3) (slice expr 1)))) (dom-set-attr container "data-sx-cyst" cyst-key) (let ((body-dom (with-island-scope (fn (d) (append! disposers d)) (fn () (let ((frag (create-fragment))) (for-each (fn (child) (dom-append frag (render-to-dom child env ns))) body-exprs) frag))))) (dom-append container body-dom) (dom-set-data container "sx-disposers" disposers) (dict-set! *memo-cache* cyst-key container) container)))) :else (render-to-dom (trampoline (eval-expr expr env)) env ns)))) (define render-lambda-dom :effects (render) (fn ((f :as lambda) (args :as list) (env :as dict) (ns :as string)) (let ((local (env-merge (lambda-closure f) env))) (for-each-indexed (fn (i p) (env-bind! local p (nth args i))) (lambda-params f)) (render-to-dom (lambda-body f) local ns)))) diff --git a/sx/sx/sx-tools-editor.sx b/sx/sx/sx-tools-editor.sx index 83dcf73f..d6b467d5 100644 --- a/sx/sx/sx-tools-editor.sx +++ b/sx/sx/sx-tools-editor.sx @@ -1 +1 @@ -(defisland ~sx-tools/tree-editor () (let ((source (signal "(defcomp ~card (&key title subtitle)\n (div :class \"card\"\n (h2 title)\n (when subtitle\n (p :class \"sub\" subtitle))))")) (view-mode (signal "tree")) (selected-path (signal nil)) (parsed (signal nil)) (bindings (signal (dict)))) (letrec ((fmt-path (fn (path) (str "[" (join "," (map str path)) "]"))) (node-disp (fn (node) (cond (nil? node) "nil" (= (type-of node) "symbol") (symbol-name node) (= (type-of node) "keyword") (str ":" (keyword-name node)) (string? node) (let ((s (if (> (len node) 35) (str (slice node 0 32) "...") node))) (str "\"" s "\"")) (number? node) (str node) (= (type-of node) "boolean") (if node "true" "false") (list? node) (if (empty? node) "()" (str "(" (node-disp (first node)) (if (> (len node) 1) " ..." "") ")")) :else (str node)))) (is-compact (fn (node) (and (list? node) (<= (len node) 4) (not (some (fn (c) (list? c)) (rest node)))))) (build-entries (fn (node path depth result) (if (list? node) (if (empty? node) (append! result {:expandable false :depth depth :text "()" :is-list false :path path}) (if (is-compact node) (append! result {:expandable false :depth depth :text (str "(" (join " " (map node-disp node)) ")") :is-list true :path path}) (do (append! result {:expandable true :depth depth :text (str "(" (node-disp (first node))) :is-list true :path path}) (for-each (fn (i) (build-entries (nth node i) (concat path (list i)) (+ depth 1) result)) (range 1 (len node)))))) (append! result {:expandable false :depth depth :text (node-disp node) :is-list false :path path})))) (nav (fn (tree path) (reduce (fn (current idx) (if (or (nil? current) (not (list? current)) (>= idx (len current))) nil (nth current idx))) tree path))) (build-context (fn (tree path) (let ((result (list))) (for-each (fn (depth) (let ((prefix (slice path 0 (+ depth 1))) (node (nav tree prefix))) (when (not (nil? node)) (let ((is-target (= (+ depth 1) (len path)))) (append! result {:depth depth :is-target is-target :text (if (list? node) (if (empty? node) "()" (let ((head (node-disp (first node)))) (if (> (len node) 3) (str "(" head " " (node-disp (nth node 1)) " ...)") (str "(" (join " " (map node-disp node)) ")")))) (node-disp node)) :path prefix}))))) (range 0 (len path))) result))) (do-validate (fn (tree) (let ((errors (list))) (letrec ((check (fn (node path) (when (list? node) (when (not (empty? node)) (let ((head (first node))) (when (and (= (type-of head) "symbol") (or (= (symbol-name head) "letrec")) (>= (len node) 2)) (let ((bindings (nth node 1))) (when (list? bindings) (for-each (fn (i) (let ((pair (nth bindings i))) (when (not (and (list? pair) (>= (len pair) 2) (= (type-of (first pair)) "symbol"))) (append! errors (str "Binding " i " at " (fmt-path (concat path (list 1 i))) " is not a (name value) pair"))))) (range 0 (len bindings)))))) (when (and (= (type-of head) "symbol") (or (= (symbol-name head) "defcomp") (= (symbol-name head) "defisland"))) (when (< (len node) 4) (append! errors (str (symbol-name head) " at " (fmt-path path) " missing body")))))) (for-each (fn (i) (check (nth node i) (concat path (list i)))) (range 0 (len node))))))) (for-each (fn (i) (check (nth tree i) (list i))) (range 0 (len tree)))) (if (empty? errors) "OK" (join "\n" errors))))) (extract-params (fn (expr) (let ((head (if (and (list? expr) (not (empty? expr)) (= (type-of (first expr)) "symbol")) (symbol-name (first expr)) nil)) (params-list (if (and (list? expr) (> (len expr) 2)) (nth expr 2) nil))) (if (and (or (= head "defcomp") (= head "defisland")) (list? params-list)) (let ((result (list)) (in-key false)) (for-each (fn (p) (cond (and (= (type-of p) "symbol") (= (symbol-name p) "&key")) (set! in-key true) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest")) (set! in-key false) in-key (cond (and (list? p) (not (empty? p))) (append! result (if (= (type-of (first p)) "symbol") (symbol-name (first p)) (str (first p)))) (= (type-of p) "symbol") (append! result (symbol-name p)) :else nil))) params-list) result) (list)))))) (div :class "space-y-4" (div :class "space-y-2" (label :class "text-sm font-medium text-stone-700" "SX Source") (textarea :class "w-full font-mono text-xs bg-stone-50 border border-stone-200 rounded p-3 leading-relaxed" :rows 6 :bind source)) (div :class "flex gap-2 items-center" (button :class "px-3 py-1 text-xs rounded bg-stone-700 text-white hover:bg-stone-800" :on-click (fn (e) (reset! parsed (sx-parse (deref source))) (reset! selected-path nil)) "Parse") (for-each (fn (mode) (button :class (str "px-3 py-1 text-xs rounded border transition-colors " (if (= (deref view-mode) mode) "bg-violet-600 text-white border-violet-600" "bg-white text-stone-600 border-stone-200 hover:border-violet-300")) :on-click (fn (e) (reset! view-mode mode)) mode)) (list "tree" "context" "validate" "render" "hypersx"))) (when (= (deref view-mode) "render") (let ((tree (deref parsed)) (all-params (let ((acc (list))) (when (and tree (not (empty? tree))) (for-each (fn (expr) (for-each (fn (p) (when (not (some (fn (x) (= x p)) acc)) (append! acc p))) (extract-params expr))) tree)) acc))) (when (not (empty? all-params)) (div :class "space-y-2 p-3 bg-violet-50 rounded border border-violet-200" (div :class "text-xs font-semibold text-violet-700 mb-1" "Parameters") (map (fn (pname) (div :class "flex items-center gap-2" (label :class "text-xs font-mono text-stone-600 w-24 text-right" pname) (input :type "text" :class "flex-1 px-2 py-1 text-xs font-mono border border-stone-300 rounded bg-white" :placeholder (str pname " value") :on-input (fn (e) (reset! bindings (assoc (deref bindings) pname (element-value (host-get e "target")))))))) all-params))))) (if (or (nil? (deref parsed)) (empty? (deref parsed))) (div :class "text-red-500 text-sm font-mono p-3 bg-red-50 rounded" "Parse error — check your s-expression syntax") (div :class "bg-stone-50 rounded border border-stone-200 p-3 font-mono text-xs leading-relaxed overflow-x-auto" (cond (= (deref view-mode) "tree") (let ((entries (list)) (tree (deref parsed))) (for-each (fn (i) (build-entries (nth tree i) (list i) 0 entries)) (range 0 (len tree))) (div :class "space-y-0" (map (fn (entry) (let ((path (get entry "path")) (sel (deref selected-path)) (is-selected (and (not (nil? sel)) (= (fmt-path sel) (fmt-path path))))) (div :class (str "py-0.5 cursor-pointer hover:bg-violet-50 rounded px-1 " (if is-selected "bg-violet-100" "")) :style (str "padding-left:" (* (get entry "depth") 16) "px") :on-click (fn (e) (reset! selected-path path)) (span :class "text-stone-400 mr-2 select-none" (fmt-path path)) (span :class (if (get entry "is-list") "text-sky-700" "text-stone-600") (get entry "text"))))) entries))) (= (deref view-mode) "context") (if (nil? (deref selected-path)) (p :class "text-stone-400 italic" "Click a node in tree view to see its context") (let ((ctx (build-context (deref parsed) (deref selected-path)))) (div :class "space-y-1" (map (fn (item) (div :style (str "padding-left:" (* (get item "depth") 16) "px") :class (if (get item "is-target") "text-violet-700 font-semibold" "text-stone-600") (span :class "text-stone-400 mr-2" (if (get item "is-target") "→ " " ") (fmt-path (get item "path"))) (span (get item "text")))) ctx)))) (= (deref view-mode) "validate") (let ((result (do-validate (deref parsed)))) (div :class (if (= result "OK") "text-emerald-600" "text-amber-600 whitespace-pre-wrap") result)) (= (deref view-mode) "render") (let ((tree (deref parsed))) (div :class "prose prose-sm max-w-none font-sans" (map (fn (expr) (if (not (and (list? expr) (not (empty? expr)) (= (type-of (first expr)) "symbol"))) (div :class "font-mono text-xs text-stone-400 p-2 bg-stone-100 rounded" (sx-serialize expr)) (let ((head-name (symbol-name (first expr)))) (cond (is-html-tag? head-name) expr (= head-name "defcomp") (let ((body (last expr)) (params (extract-params expr)) (b (deref bindings))) (div (div :class "text-xs text-stone-400 mb-2 font-mono" (str head-name " " (node-disp (nth expr 1)))) (letrec ((subst (fn (node) (cond (and (= (type-of node) "symbol") (some (fn (p) (= p (symbol-name node))) params)) (or (get b (symbol-name node)) "") (list? node) (map subst node) :else node)))) (subst body)))) (= head-name "defisland") (let ((full-name (if (= (type-of (nth expr 1)) "symbol") (symbol-name (nth expr 1)) (str (nth expr 1))))) (sx-load-components (sx-serialize expr)) (div (div :class "text-xs text-stone-400 mb-2 font-mono" (str head-name " " full-name " — live preview")) (div :class "border border-violet-200 rounded p-4" (list (make-symbol full-name))))) (= head-name "let") (last expr) :else (div :class "font-mono text-xs text-stone-400 p-2 bg-stone-100 rounded" (sx-serialize expr)))))) tree))) (= (deref view-mode) "hypersx") (pre :class "font-mono text-xs text-stone-700 whitespace-pre-wrap overflow-x-auto bg-transparent p-0" (sx->hypersx (deref parsed))) :else (p "Select a view mode")))))))) +(defisland ~sx-tools/tree-editor () (let ((source (signal "(defcomp ~card (&key title subtitle)\n (div :class \"card\"\n (h2 title)\n (when subtitle\n (p :class \"sub\" subtitle))))")) (view-mode (signal "tree")) (selected-path (signal nil)) (parsed (signal nil)) (bindings (signal (dict)))) (letrec ((fmt-path (fn (path) (str "[" (join "," (map str path)) "]"))) (node-disp (fn (node) (cond (nil? node) "nil" (= (type-of node) "symbol") (symbol-name node) (= (type-of node) "keyword") (str ":" (keyword-name node)) (string? node) (let ((s (if (> (len node) 35) (str (slice node 0 32) "...") node))) (str "\"" s "\"")) (number? node) (str node) (= (type-of node) "boolean") (if node "true" "false") (list? node) (if (empty? node) "()" (str "(" (node-disp (first node)) (if (> (len node) 1) " ..." "") ")")) :else (str node)))) (is-compact (fn (node) (and (list? node) (<= (len node) 4) (not (some (fn (c) (list? c)) (rest node)))))) (build-entries (fn (node path depth result) (if (list? node) (if (empty? node) (append! result {:expandable false :depth depth :text "()" :is-list false :path path}) (if (is-compact node) (append! result {:expandable false :depth depth :text (str "(" (join " " (map node-disp node)) ")") :is-list true :path path}) (do (append! result {:expandable true :depth depth :text (str "(" (node-disp (first node))) :is-list true :path path}) (for-each (fn (i) (build-entries (nth node i) (concat path (list i)) (+ depth 1) result)) (range 1 (len node)))))) (append! result {:expandable false :depth depth :text (node-disp node) :is-list false :path path})))) (nav (fn (tree path) (reduce (fn (current idx) (if (or (nil? current) (not (list? current)) (>= idx (len current))) nil (nth current idx))) tree path))) (build-context (fn (tree path) (let ((result (list))) (for-each (fn (depth) (let ((prefix (slice path 0 (+ depth 1))) (node (nav tree prefix))) (when (not (nil? node)) (let ((is-target (= (+ depth 1) (len path)))) (append! result {:depth depth :is-target is-target :text (if (list? node) (if (empty? node) "()" (let ((head (node-disp (first node)))) (if (> (len node) 3) (str "(" head " " (node-disp (nth node 1)) " ...)") (str "(" (join " " (map node-disp node)) ")")))) (node-disp node)) :path prefix}))))) (range 0 (len path))) result))) (do-validate (fn (tree) (let ((errors (list))) (letrec ((check (fn (node path) (when (list? node) (when (not (empty? node)) (let ((head (first node))) (when (and (= (type-of head) "symbol") (or (= (symbol-name head) "letrec")) (>= (len node) 2)) (let ((bindings (nth node 1))) (when (list? bindings) (for-each (fn (i) (let ((pair (nth bindings i))) (when (not (and (list? pair) (>= (len pair) 2) (= (type-of (first pair)) "symbol"))) (append! errors (str "Binding " i " at " (fmt-path (concat path (list 1 i))) " is not a (name value) pair"))))) (range 0 (len bindings)))))) (when (and (= (type-of head) "symbol") (or (= (symbol-name head) "defcomp") (= (symbol-name head) "defisland"))) (when (< (len node) 4) (append! errors (str (symbol-name head) " at " (fmt-path path) " missing body")))))) (for-each (fn (i) (check (nth node i) (concat path (list i)))) (range 0 (len node))))))) (for-each (fn (i) (check (nth tree i) (list i))) (range 0 (len tree)))) (if (empty? errors) "OK" (join "\n" errors))))) (extract-params (fn (expr) (let ((head (if (and (list? expr) (not (empty? expr)) (= (type-of (first expr)) "symbol")) (symbol-name (first expr)) nil)) (params-list (if (and (list? expr) (> (len expr) 2)) (nth expr 2) nil))) (if (and (or (= head "defcomp") (= head "defisland")) (list? params-list)) (let ((result (list)) (in-key false)) (for-each (fn (p) (cond (and (= (type-of p) "symbol") (= (symbol-name p) "&key")) (set! in-key true) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest")) (set! in-key false) in-key (cond (and (list? p) (not (empty? p))) (append! result (if (= (type-of (first p)) "symbol") (symbol-name (first p)) (str (first p)))) (= (type-of p) "symbol") (append! result (symbol-name p)) :else nil))) params-list) result) (list)))))) (div :class "space-y-4" (div :class "space-y-2" (label :class "text-sm font-medium text-stone-700" "SX Source") (textarea :class "w-full font-mono text-xs bg-stone-50 border border-stone-200 rounded p-3 leading-relaxed" :rows 6 :bind source)) (div :class "flex gap-2 items-center" (button :class "px-3 py-1 text-xs rounded bg-stone-700 text-white hover:bg-stone-800" :on-click (fn (e) (reset! parsed (sx-parse (deref source))) (reset! selected-path nil)) "Parse") (for-each (fn (mode) (button :class (str "px-3 py-1 text-xs rounded border transition-colors " (if (= (deref view-mode) mode) "bg-violet-600 text-white border-violet-600" "bg-white text-stone-600 border-stone-200 hover:border-violet-300")) :on-click (fn (e) (reset! view-mode mode)) mode)) (list "tree" "context" "validate" "render" "hypersx"))) (when (= (deref view-mode) "render") (let ((tree (deref parsed)) (all-params (let ((acc (list))) (when (and tree (not (empty? tree))) (for-each (fn (expr) (for-each (fn (p) (when (not (some (fn (x) (= x p)) acc)) (append! acc p))) (extract-params expr))) tree)) acc))) (when (not (empty? all-params)) (div :class "space-y-2 p-3 bg-violet-50 rounded border border-violet-200" (div :class "text-xs font-semibold text-violet-700 mb-1" "Parameters") (map (fn (pname) (div :class "flex items-center gap-2" (label :class "text-xs font-mono text-stone-600 w-24 text-right" pname) (input :type "text" :class "flex-1 px-2 py-1 text-xs font-mono border border-stone-300 rounded bg-white" :placeholder (str pname " value") :on-input (fn (e) (reset! bindings (assoc (deref bindings) pname (element-value (host-get e "target")))))))) all-params))))) (if (or (nil? (deref parsed)) (empty? (deref parsed))) (div :class "text-red-500 text-sm font-mono p-3 bg-red-50 rounded" "Parse error — check your s-expression syntax") (div :class "bg-stone-50 rounded border border-stone-200 p-3 font-mono text-xs leading-relaxed overflow-x-auto" (cond (= (deref view-mode) "tree") (let ((entries (list)) (tree (deref parsed))) (for-each (fn (i) (build-entries (nth tree i) (list i) 0 entries)) (range 0 (len tree))) (div :class "space-y-0" (map (fn (entry) (let ((path (get entry "path")) (sel (deref selected-path)) (is-selected (and (not (nil? sel)) (= (fmt-path sel) (fmt-path path))))) (div :class (str "py-0.5 cursor-pointer hover:bg-violet-50 rounded px-1 " (if is-selected "bg-violet-100" "")) :style (str "padding-left:" (* (get entry "depth") 16) "px") :on-click (fn (e) (reset! selected-path path)) (span :class "text-stone-400 mr-2 select-none" (fmt-path path)) (span :class (if (get entry "is-list") "text-sky-700" "text-stone-600") (get entry "text"))))) entries))) (= (deref view-mode) "context") (if (nil? (deref selected-path)) (p :class "text-stone-400 italic" "Click a node in tree view to see its context") (let ((ctx (build-context (deref parsed) (deref selected-path)))) (div :class "space-y-1" (map (fn (item) (div :style (str "padding-left:" (* (get item "depth") 16) "px") :class (if (get item "is-target") "text-violet-700 font-semibold" "text-stone-600") (span :class "text-stone-400 mr-2" (if (get item "is-target") "→ " " ") (fmt-path (get item "path"))) (span (get item "text")))) ctx)))) (= (deref view-mode) "validate") (let ((result (do-validate (deref parsed)))) (div :class (if (= result "OK") "text-emerald-600" "text-amber-600 whitespace-pre-wrap") result)) (= (deref view-mode) "render") (let ((tree (deref parsed))) (div :class "prose prose-sm max-w-none font-sans" (map (fn (expr) (if (not (and (list? expr) (not (empty? expr)) (= (type-of (first expr)) "symbol"))) (div :class "font-mono text-xs text-stone-400 p-2 bg-stone-100 rounded" (sx-serialize expr)) (let ((head-name (symbol-name (first expr)))) (cond (is-html-tag? head-name) expr (= head-name "defcomp") (let ((body (last expr)) (params (extract-params expr)) (b (deref bindings))) (div (div :class "text-xs text-stone-400 mb-2 font-mono" (str head-name " " (node-disp (nth expr 1)))) (letrec ((subst (fn (node) (cond (and (= (type-of node) "symbol") (some (fn (p) (= p (symbol-name node))) params)) (or (get b (symbol-name node)) "") (list? node) (map subst node) :else node)))) (subst body)))) (= head-name "defisland") (let ((full-name (if (= (type-of (nth expr 1)) "symbol") (symbol-name (nth expr 1)) (str (nth expr 1))))) (sx-load-components (sx-serialize expr)) (div (div :class "text-xs text-stone-400 mb-2 font-mono" (str head-name " " full-name " — live preview")) (cyst :key full-name (div :class "border border-violet-200 rounded p-4" (list (make-symbol full-name)))))) (= head-name "let") (last expr) :else (div :class "font-mono text-xs text-stone-400 p-2 bg-stone-100 rounded" (sx-serialize expr)))))) tree))) (= (deref view-mode) "hypersx") (pre :class "font-mono text-xs text-stone-700 whitespace-pre-wrap overflow-x-auto bg-transparent p-0" (sx->hypersx (deref parsed))) :else (p "Select a view mode")))))))) diff --git a/web/adapter-dom.sx b/web/adapter-dom.sx index c2fa1d30..38c670ba 100644 --- a/web/adapter-dom.sx +++ b/web/adapter-dom.sx @@ -4,6 +4,8 @@ (define island-scope? (fn () (not (nil? (scope-peek "sx-island-scope"))))) +(begin (define *memo-cache* (dict)) (define *cyst-counter* 0) (define next-cyst-id (fn () (set! *cyst-counter* (+ *cyst-counter* 1)) (str "sx-cyst-" *cyst-counter*)))) + (define contains-deref? (fn (expr) (if (not (list? expr)) false (if (empty? expr) false (if (and (= (type-of (first expr)) "symbol") (= (symbol-name (first expr)) "deref")) true (some contains-deref? expr)))))) (define dom-on :effects (io) (fn (el name handler) (dom-listen el name (if (lambda? handler) (if (= 0 (len (lambda-params handler))) (fn (_e) (trampoline (call-lambda handler (list))) (run-post-render-hooks)) (fn (e) (trampoline (call-lambda handler (list e))) (run-post-render-hooks))) handler)))) @@ -22,11 +24,11 @@ (define render-dom-unknown-component :effects (render) (fn ((name :as string)) (error (str "Unknown component: " name)))) -(define RENDER_DOM_FORMS (list "if" "when" "cond" "case" "let" "let*" "letrec" "begin" "do" "define" "defcomp" "defisland" "defmacro" "defstyle" "map" "map-indexed" "filter" "for-each" "portal" "error-boundary" "scope" "provide")) +(define RENDER_DOM_FORMS (list "if" "when" "cond" "case" "let" "let*" "letrec" "begin" "do" "define" "defcomp" "defisland" "defmacro" "defstyle" "map" "map-indexed" "filter" "for-each" "portal" "error-boundary" "scope" "provide" "cyst")) (define render-dom-form? :effects () (fn ((name :as string)) (contains? RENDER_DOM_FORMS name))) -(define dispatch-render-form :effects (render) (fn ((name :as string) expr (env :as dict) (ns :as string)) (cond (= name "if") (if (island-scope?) (let ((marker (create-comment "r-if")) (current-nodes (list)) (initial-result nil)) (effect (fn () (let ((result (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) (if cond-val (render-to-dom (nth expr 2) env ns) (if (> (len expr) 3) (render-to-dom (nth expr 3) env ns) (create-fragment)))))) (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (dom-insert-after marker result)) (set! initial-result result))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (set! current-nodes (if (dom-is-fragment? initial-result) (dom-child-nodes initial-result) (list initial-result))) (dom-append frag initial-result)) frag))) (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) (if cond-val (render-to-dom (nth expr 2) env ns) (if (> (len expr) 3) (render-to-dom (nth expr 3) env ns) (create-fragment))))) (= name "when") (if (island-scope?) (let ((marker (create-comment "r-when")) (current-nodes (list)) (initial-result nil)) (effect (fn () (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (list)) (when (trampoline (eval-expr (nth expr 1) env)) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) (set! current-nodes (dom-child-nodes frag)) (dom-insert-after marker frag)))) (when (trampoline (eval-expr (nth expr 1) env)) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) (set! current-nodes (dom-child-nodes frag)) (set! initial-result frag)))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (dom-append frag initial-result)) frag))) (if (not (trampoline (eval-expr (nth expr 1) env))) (create-fragment) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) frag))) (= name "cond") (if (island-scope?) (let ((marker (create-comment "r-cond")) (current-nodes (list)) (initial-result nil)) (effect (fn () (let ((branch (eval-cond (rest expr) env))) (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (list)) (when branch (let ((result (render-to-dom branch env ns))) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (dom-insert-after marker result)))) (when branch (let ((result (render-to-dom branch env ns))) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (set! initial-result result))))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (dom-append frag initial-result)) frag))) (let ((branch (eval-cond (rest expr) env))) (if branch (render-to-dom branch env ns) (create-fragment)))) (= name "case") (render-to-dom (trampoline (eval-expr expr env)) env ns) (or (= name "let") (= name "let*")) (let ((local (process-bindings (nth expr 1) env))) (if (= (len expr) 3) (render-to-dom (nth expr 2) local ns) (let ((frag (create-fragment))) (for-each (fn (i) (let ((result (render-to-dom (nth expr i) local ns))) (when (not (spread? result)) (dom-append frag result)))) (range 2 (len expr))) frag))) (= name "letrec") (let ((bindings (nth expr 1)) (body (slice expr 2)) (local (env-extend env))) (for-each (fn (pair) (let ((pname (if (= (type-of (first pair)) "symbol") (symbol-name (first pair)) (str (first pair))))) (env-bind! local pname nil))) bindings) (for-each (fn (pair) (let ((pname (if (= (type-of (first pair)) "symbol") (symbol-name (first pair)) (str (first pair))))) (env-set! local pname (trampoline (eval-expr (nth pair 1) local))))) bindings) (when (> (len body) 1) (for-each (fn (e) (trampoline (eval-expr e local))) (init body))) (render-to-dom (last body) local ns)) (or (= name "begin") (= name "do")) (if (= (len expr) 2) (render-to-dom (nth expr 1) env ns) (let ((frag (create-fragment))) (for-each (fn (i) (let ((result (render-to-dom (nth expr i) env ns))) (when (not (spread? result)) (dom-append frag result)))) (range 1 (len expr))) frag)) (definition-form? name) (do (trampoline (eval-expr expr env)) (create-fragment)) (= name "map") (let ((coll-expr (nth expr 2))) (if (and (island-scope?) (= (type-of coll-expr) "list") (> (len coll-expr) 1) (= (type-of (first coll-expr)) "symbol") (= (symbol-name (first coll-expr)) "deref")) (let ((f (trampoline (eval-expr (nth expr 1) env))) (sig (trampoline (eval-expr (nth coll-expr 1) env)))) (if (signal? sig) (reactive-list f sig env ns) (let ((coll (deref sig)) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag))) (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag))) (= name "map-indexed") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each-indexed (fn (i item) (let ((val (if (lambda? f) (render-lambda-dom f (list i item) env ns) (render-to-dom (apply f (list i item)) env ns)))) (dom-append frag val))) coll) frag) (= name "filter") (render-to-dom (trampoline (eval-expr expr env)) env ns) (= name "portal") (render-dom-portal (rest expr) env ns) (= name "error-boundary") (render-dom-error-boundary (rest expr) env ns) (= name "for-each") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag) (= name "scope") (let ((scope-name (trampoline (eval-expr (nth expr 1) env))) (rest-args (slice expr 2)) (scope-val nil) (body-exprs nil) (frag (create-fragment))) (if (and (>= (len rest-args) 2) (= (type-of (first rest-args)) "keyword") (= (keyword-name (first rest-args)) "value")) (do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env))) (set! body-exprs (slice rest-args 2))) (set! body-exprs rest-args)) (scope-push! scope-name scope-val) (for-each (fn (e) (dom-append frag (render-to-dom e env ns))) body-exprs) (scope-pop! scope-name) frag) (= name "provide") (let ((prov-name (trampoline (eval-expr (nth expr 1) env))) (prov-val (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (scope-push! prov-name prov-val) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 3 (len expr))) (scope-pop! prov-name) frag) :else (render-to-dom (trampoline (eval-expr expr env)) env ns)))) +(define dispatch-render-form :effects (render) (fn ((name :as string) expr (env :as dict) (ns :as string)) (cond (= name "if") (if (island-scope?) (let ((marker (create-comment "r-if")) (current-nodes (list)) (initial-result nil)) (effect (fn () (let ((result (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) (if cond-val (render-to-dom (nth expr 2) env ns) (if (> (len expr) 3) (render-to-dom (nth expr 3) env ns) (create-fragment)))))) (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (dom-insert-after marker result)) (set! initial-result result))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (set! current-nodes (if (dom-is-fragment? initial-result) (dom-child-nodes initial-result) (list initial-result))) (dom-append frag initial-result)) frag))) (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) (if cond-val (render-to-dom (nth expr 2) env ns) (if (> (len expr) 3) (render-to-dom (nth expr 3) env ns) (create-fragment))))) (= name "when") (if (island-scope?) (let ((marker (create-comment "r-when")) (current-nodes (list)) (initial-result nil)) (effect (fn () (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (list)) (when (trampoline (eval-expr (nth expr 1) env)) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) (set! current-nodes (dom-child-nodes frag)) (dom-insert-after marker frag)))) (when (trampoline (eval-expr (nth expr 1) env)) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) (set! current-nodes (dom-child-nodes frag)) (set! initial-result frag)))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (dom-append frag initial-result)) frag))) (if (not (trampoline (eval-expr (nth expr 1) env))) (create-fragment) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) frag))) (= name "cond") (if (island-scope?) (let ((marker (create-comment "r-cond")) (current-nodes (list)) (initial-result nil)) (effect (fn () (let ((branch (eval-cond (rest expr) env))) (if (dom-parent marker) (do (for-each (fn (n) (dom-remove n)) current-nodes) (set! current-nodes (list)) (when branch (let ((result (render-to-dom branch env ns))) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (dom-insert-after marker result)))) (when branch (let ((result (render-to-dom branch env ns))) (set! current-nodes (if (dom-is-fragment? result) (dom-child-nodes result) (list result))) (set! initial-result result))))))) (if (spread? initial-result) initial-result (let ((frag (create-fragment))) (dom-append frag marker) (when initial-result (dom-append frag initial-result)) frag))) (let ((branch (eval-cond (rest expr) env))) (if branch (render-to-dom branch env ns) (create-fragment)))) (= name "case") (render-to-dom (trampoline (eval-expr expr env)) env ns) (or (= name "let") (= name "let*")) (let ((local (process-bindings (nth expr 1) env))) (if (= (len expr) 3) (render-to-dom (nth expr 2) local ns) (let ((frag (create-fragment))) (for-each (fn (i) (let ((result (render-to-dom (nth expr i) local ns))) (when (not (spread? result)) (dom-append frag result)))) (range 2 (len expr))) frag))) (= name "letrec") (let ((bindings (nth expr 1)) (body (slice expr 2)) (local (env-extend env))) (for-each (fn (pair) (let ((pname (if (= (type-of (first pair)) "symbol") (symbol-name (first pair)) (str (first pair))))) (env-bind! local pname nil))) bindings) (for-each (fn (pair) (let ((pname (if (= (type-of (first pair)) "symbol") (symbol-name (first pair)) (str (first pair))))) (env-set! local pname (trampoline (eval-expr (nth pair 1) local))))) bindings) (when (> (len body) 1) (for-each (fn (e) (trampoline (eval-expr e local))) (init body))) (render-to-dom (last body) local ns)) (or (= name "begin") (= name "do")) (if (= (len expr) 2) (render-to-dom (nth expr 1) env ns) (let ((frag (create-fragment))) (for-each (fn (i) (let ((result (render-to-dom (nth expr i) env ns))) (when (not (spread? result)) (dom-append frag result)))) (range 1 (len expr))) frag)) (definition-form? name) (do (trampoline (eval-expr expr env)) (create-fragment)) (= name "map") (let ((coll-expr (nth expr 2))) (if (and (island-scope?) (= (type-of coll-expr) "list") (> (len coll-expr) 1) (= (type-of (first coll-expr)) "symbol") (= (symbol-name (first coll-expr)) "deref")) (let ((f (trampoline (eval-expr (nth expr 1) env))) (sig (trampoline (eval-expr (nth coll-expr 1) env)))) (if (signal? sig) (reactive-list f sig env ns) (let ((coll (deref sig)) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag))) (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag))) (= name "map-indexed") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each-indexed (fn (i item) (let ((val (if (lambda? f) (render-lambda-dom f (list i item) env ns) (render-to-dom (apply f (list i item)) env ns)))) (dom-append frag val))) coll) frag) (= name "filter") (render-to-dom (trampoline (eval-expr expr env)) env ns) (= name "portal") (render-dom-portal (rest expr) env ns) (= name "error-boundary") (render-dom-error-boundary (rest expr) env ns) (= name "for-each") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag) (= name "scope") (let ((scope-name (trampoline (eval-expr (nth expr 1) env))) (rest-args (slice expr 2)) (scope-val nil) (body-exprs nil) (frag (create-fragment))) (if (and (>= (len rest-args) 2) (= (type-of (first rest-args)) "keyword") (= (keyword-name (first rest-args)) "value")) (do (set! scope-val (trampoline (eval-expr (nth rest-args 1) env))) (set! body-exprs (slice rest-args 2))) (set! body-exprs rest-args)) (scope-push! scope-name scope-val) (for-each (fn (e) (dom-append frag (render-to-dom e env ns))) body-exprs) (scope-pop! scope-name) frag) (= name "provide") (let ((prov-name (trampoline (eval-expr (nth expr 1) env))) (prov-val (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (scope-push! prov-name prov-val) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 3 (len expr))) (scope-pop! prov-name) frag) (= name "cyst") (let ((cyst-key (if (and (> (len expr) 2) (= (type-of (nth expr 1)) "keyword") (= (keyword-name (nth expr 1)) "key")) (str (trampoline (eval-expr (nth expr 2) env))) (next-cyst-id))) (cached (get *memo-cache* cyst-key))) (if (and cached (host-get cached "isConnected")) cached (let ((container (dom-create-element "div" nil)) (disposers (list)) (body-exprs (if (and (> (len expr) 2) (= (type-of (nth expr 1)) "keyword") (= (keyword-name (nth expr 1)) "key")) (slice expr 3) (slice expr 1)))) (dom-set-attr container "data-sx-cyst" cyst-key) (let ((body-dom (with-island-scope (fn (d) (append! disposers d)) (fn () (let ((frag (create-fragment))) (for-each (fn (child) (dom-append frag (render-to-dom child env ns))) body-exprs) frag))))) (dom-append container body-dom) (dom-set-data container "sx-disposers" disposers) (dict-set! *memo-cache* cyst-key container) container)))) :else (render-to-dom (trampoline (eval-expr expr env)) env ns)))) (define render-lambda-dom :effects (render) (fn ((f :as lambda) (args :as list) (env :as dict) (ns :as string)) (let ((local (env-merge (lambda-closure f) env))) (for-each-indexed (fn (i p) (env-bind! local p (nth args i))) (lambda-params f)) (render-to-dom (lambda-body f) local ns))))